✨ Better categories
This commit is contained in:
		
							
								
								
									
										89
									
								
								pkg/server/categories_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										89
									
								
								pkg/server/categories_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,89 @@ | |||||||
|  | package server | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||||
|  | 	"code.smartsheep.studio/hydrogen/interactive/pkg/services" | ||||||
|  | 	"github.com/gofiber/fiber/v2" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func listCategroies(c *fiber.Ctx) error { | ||||||
|  | 	categories, err := services.ListCategory() | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusNotFound, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.JSON(categories) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func newCategory(c *fiber.Ctx) error { | ||||||
|  | 	user := c.Locals("principal").(models.Account) | ||||||
|  | 	if user.PowerLevel <= 55 { | ||||||
|  | 		return fiber.NewError(fiber.StatusForbidden, "require power level 55 to create categories") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var data struct { | ||||||
|  | 		Alias       string `json:"alias" validate:"required"` | ||||||
|  | 		Name        string `json:"name" validate:"required"` | ||||||
|  | 		Description string `json:"description"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := BindAndValidate(c, &data); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	category, err := services.NewCategory(data.Alias, data.Name, data.Description) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.JSON(category) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func editCategory(c *fiber.Ctx) error { | ||||||
|  | 	user := c.Locals("principal").(models.Account) | ||||||
|  | 	if user.PowerLevel <= 55 { | ||||||
|  | 		return fiber.NewError(fiber.StatusForbidden, "require power level 55 to edit categories") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	id, _ := c.ParamsInt("categoryId", 0) | ||||||
|  | 	category, err := services.GetCategoryWithID(uint(id)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusNotFound, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var data struct { | ||||||
|  | 		Alias       string `json:"alias" validate:"required"` | ||||||
|  | 		Name        string `json:"name" validate:"required"` | ||||||
|  | 		Description string `json:"description"` | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := BindAndValidate(c, &data); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	category, err = services.EditCategory(category, data.Alias, data.Name, data.Description) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.JSON(category) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func deleteCategory(c *fiber.Ctx) error { | ||||||
|  | 	user := c.Locals("principal").(models.Account) | ||||||
|  | 	if user.PowerLevel <= 55 { | ||||||
|  | 		return fiber.NewError(fiber.StatusForbidden, "require power level 55 to delete categories") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	id, _ := c.ParamsInt("categoryId", 0) | ||||||
|  | 	category, err := services.GetCategoryWithID(uint(id)) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusNotFound, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if err := services.DeleteCategory(category); err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.JSON(category) | ||||||
|  | } | ||||||
| @@ -54,7 +54,7 @@ func listAvailableRealm(c *fiber.Ctx) error { | |||||||
| func createRealm(c *fiber.Ctx) error { | func createRealm(c *fiber.Ctx) error { | ||||||
| 	user := c.Locals("principal").(models.Account) | 	user := c.Locals("principal").(models.Account) | ||||||
| 	if user.PowerLevel < 10 { | 	if user.PowerLevel < 10 { | ||||||
| 		return fiber.NewError(fiber.StatusForbidden, "require power level 10 to create realm") | 		return fiber.NewError(fiber.StatusForbidden, "require power level 10 to create realms") | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var data struct { | 	var data struct { | ||||||
|   | |||||||
| @@ -1,6 +1,10 @@ | |||||||
| package server | package server | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"net/http" | ||||||
|  | 	"strings" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/view" | 	"code.smartsheep.studio/hydrogen/interactive/pkg/view" | ||||||
| 	"github.com/gofiber/fiber/v2" | 	"github.com/gofiber/fiber/v2" | ||||||
| 	"github.com/gofiber/fiber/v2/middleware/cache" | 	"github.com/gofiber/fiber/v2/middleware/cache" | ||||||
| @@ -11,9 +15,6 @@ import ( | |||||||
| 	jsoniter "github.com/json-iterator/go" | 	jsoniter "github.com/json-iterator/go" | ||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| 	"github.com/spf13/viper" | 	"github.com/spf13/viper" | ||||||
| 	"net/http" |  | ||||||
| 	"strings" |  | ||||||
| 	"time" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var A *fiber.App | var A *fiber.App | ||||||
| @@ -76,6 +77,11 @@ func NewServer() { | |||||||
| 		api.Put("/posts/:postId", auth, editPost) | 		api.Put("/posts/:postId", auth, editPost) | ||||||
| 		api.Delete("/posts/:postId", auth, deletePost) | 		api.Delete("/posts/:postId", auth, deletePost) | ||||||
|  |  | ||||||
|  | 		api.Get("/categories", listCategroies) | ||||||
|  | 		api.Post("/categories", auth, newCategory) | ||||||
|  | 		api.Put("/categories/:categoryId", auth, editCategory) | ||||||
|  | 		api.Delete("/categories/:categoryId", auth, deleteCategory) | ||||||
|  |  | ||||||
| 		api.Get("/creators/posts", auth, listOwnPost) | 		api.Get("/creators/posts", auth, listOwnPost) | ||||||
| 		api.Get("/creators/posts/:postId", auth, getOwnPost) | 		api.Get("/creators/posts/:postId", auth, getOwnPost) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,29 +1,66 @@ | |||||||
| package services | package services | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
|  | 	"errors" | ||||||
|  |  | ||||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||||
| 	"errors" |  | ||||||
| 	"gorm.io/gorm" | 	"gorm.io/gorm" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func GetCategory(alias, name string) (models.Category, error) { | func ListCategory() ([]models.Category, error) { | ||||||
|  | 	var categories []models.Category | ||||||
|  |  | ||||||
|  | 	err := database.C.Find(&categories).Error | ||||||
|  |  | ||||||
|  | 	return categories, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetCategory(alias string) (models.Category, error) { | ||||||
| 	var category models.Category | 	var category models.Category | ||||||
| 	if err := database.C.Where(models.Category{Alias: alias}).First(&category).Error; err != nil { | 	if err := database.C.Where(models.Category{Alias: alias}).First(&category).Error; err != nil { | ||||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { |  | ||||||
| 			category = models.Category{ |  | ||||||
| 				Alias: alias, |  | ||||||
| 				Name:  name, |  | ||||||
| 			} |  | ||||||
| 			err := database.C.Save(&category).Error |  | ||||||
| 			return category, err |  | ||||||
| 		} |  | ||||||
| 		return category, err | 		return category, err | ||||||
| 	} | 	} | ||||||
| 	return category, nil | 	return category, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| func GetTag(alias, name string) (models.Tag, error) { | func GetCategoryWithID(id uint) (models.Category, error) { | ||||||
|  | 	var category models.Category | ||||||
|  | 	if err := database.C.Where(models.Category{ | ||||||
|  | 		BaseModel: models.BaseModel{ID: id}, | ||||||
|  | 	}).First(&category).Error; err != nil { | ||||||
|  | 		return category, err | ||||||
|  | 	} | ||||||
|  | 	return category, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func NewCategory(alias, name, description string) (models.Category, error) { | ||||||
|  | 	category := models.Category{ | ||||||
|  | 		Alias:       alias, | ||||||
|  | 		Name:        name, | ||||||
|  | 		Description: description, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err := database.C.Save(&category).Error | ||||||
|  |  | ||||||
|  | 	return category, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func EditCategory(category models.Category, alias, name, description string) (models.Category, error) { | ||||||
|  | 	category.Alias = alias | ||||||
|  | 	category.Name = name | ||||||
|  | 	category.Description = description | ||||||
|  |  | ||||||
|  | 	err := database.C.Save(&category).Error | ||||||
|  |  | ||||||
|  | 	return category, err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func DeleteCategory(category models.Category) error { | ||||||
|  | 	return database.C.Delete(category).Error | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func GetTagOrCreate(alias, name string) (models.Tag, error) { | ||||||
| 	var tag models.Tag | 	var tag models.Tag | ||||||
| 	if err := database.C.Where(models.Category{Alias: alias}).First(&tag).Error; err != nil { | 	if err := database.C.Where(models.Category{Alias: alias}).First(&tag).Error; err != nil { | ||||||
| 		if errors.Is(err, gorm.ErrRecordNotFound) { | 		if errors.Is(err, gorm.ErrRecordNotFound) { | ||||||
|   | |||||||
| @@ -3,9 +3,10 @@ package services | |||||||
| import ( | import ( | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/gofiber/fiber/v2" | 	"github.com/gofiber/fiber/v2" | ||||||
| 	"github.com/rs/zerolog/log" | 	"github.com/rs/zerolog/log" | ||||||
| 	"time" |  | ||||||
|  |  | ||||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||||
| 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||||
| @@ -141,13 +142,13 @@ func NewPost( | |||||||
| 	var err error | 	var err error | ||||||
| 	var post models.Post | 	var post models.Post | ||||||
| 	for idx, category := range categories { | 	for idx, category := range categories { | ||||||
| 		categories[idx], err = GetCategory(category.Alias, category.Name) | 		categories[idx], err = GetCategory(category.Alias) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return post, err | 			return post, err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	for idx, tag := range tags { | 	for idx, tag := range tags { | ||||||
| 		tags[idx], err = GetTag(tag.Alias, tag.Name) | 		tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return post, err | 			return post, err | ||||||
| 		} | 		} | ||||||
| @@ -248,13 +249,13 @@ func EditPost( | |||||||
| ) (models.Post, error) { | ) (models.Post, error) { | ||||||
| 	var err error | 	var err error | ||||||
| 	for idx, category := range categories { | 	for idx, category := range categories { | ||||||
| 		categories[idx], err = GetCategory(category.Alias, category.Name) | 		categories[idx], err = GetCategory(category.Alias) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return post, err | 			return post, err | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 	for idx, tag := range tags { | 	for idx, tag := range tags { | ||||||
| 		tags[idx], err = GetTag(tag.Alias, tag.Name) | 		tags[idx], err = GetTagOrCreate(tag.Alias, tag.Name) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return post, err | 			return post, err | ||||||
| 		} | 		} | ||||||
|   | |||||||
| @@ -5,24 +5,34 @@ import { getAtk, useUserinfo } from "../../stores/userinfo.tsx"; | |||||||
| import styles from "./PostPublish.module.css"; | import styles from "./PostPublish.module.css"; | ||||||
|  |  | ||||||
| export default function PostEditActions(props: { | export default function PostEditActions(props: { | ||||||
|   editing?: any, |   editing?: any; | ||||||
|   onInputAlias: (value: string) => void, |   onInputAlias: (value: string) => void; | ||||||
|   onInputPublish: (value: string) => void, |   onInputPublish: (value: string) => void; | ||||||
|   onInputAttachments: (value: any[]) => void, |   onInputAttachments: (value: any[]) => void; | ||||||
|   onInputCategories: (categories: any[]) => void, |   onInputCategories: (categories: any[]) => void; | ||||||
|   onInputTags: (tags: any[]) => void, |   onInputTags: (tags: any[]) => void; | ||||||
|   onError: (message: string | null) => void, |   onError: (message: string | null) => void; | ||||||
| }) { | }) { | ||||||
|   const userinfo = useUserinfo(); |   const userinfo = useUserinfo(); | ||||||
|  |  | ||||||
|   const [uploading, setUploading] = createSignal(false); |   const [uploading, setUploading] = createSignal(false); | ||||||
|  |  | ||||||
|   const [attachments, setAttachments] = createSignal<any[]>(props.editing?.attachments ?? []); |   const [attachments, setAttachments] = createSignal<any[]>(props.editing?.attachments ?? []); | ||||||
|   const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>(props.editing?.categories ?? []); |   const [categories, setCategories] = createSignal<{ alias: string; name: string }[]>(props.editing?.categories ?? []); | ||||||
|   const [tags, setTags] = createSignal<{ alias: string, name: string }[]>(props.editing?.tags ?? []); |   const [tags, setTags] = createSignal<{ alias: string; name: string }[]>(props.editing?.tags ?? []); | ||||||
|  |  | ||||||
|  |   const [availableCategories, setAvailableCategories] = createSignal<any[]>([]); | ||||||
|   const [attachmentMode, setAttachmentMode] = createSignal(0); |   const [attachmentMode, setAttachmentMode] = createSignal(0); | ||||||
|  |  | ||||||
|  |   async function readCategories() { | ||||||
|  |     const res = await fetch("/api/categories"); | ||||||
|  |     if (res.status === 200) { | ||||||
|  |       setAvailableCategories(await res.json()); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   readCategories(); | ||||||
|  |  | ||||||
|   async function uploadAttachment(evt: SubmitEvent) { |   async function uploadAttachment(evt: SubmitEvent) { | ||||||
|     evt.preventDefault(); |     evt.preventDefault(); | ||||||
|  |  | ||||||
| @@ -33,8 +43,8 @@ export default function PostEditActions(props: { | |||||||
|     setUploading(true); |     setUploading(true); | ||||||
|     const res = await fetch("/api/attachments", { |     const res = await fetch("/api/attachments", { | ||||||
|       method: "POST", |       method: "POST", | ||||||
|       headers: { "Authorization": `Bearer ${getAtk()}` }, |       headers: { Authorization: `Bearer ${getAtk()}` }, | ||||||
|       body: data |       body: data, | ||||||
|     }); |     }); | ||||||
|     if (res.status !== 200) { |     if (res.status !== 200) { | ||||||
|       props.onError(await res.text()); |       props.onError(await res.text()); | ||||||
| @@ -54,10 +64,14 @@ export default function PostEditActions(props: { | |||||||
|     const form = evt.target as HTMLFormElement; |     const form = evt.target as HTMLFormElement; | ||||||
|     const data = Object.fromEntries(new FormData(form)); |     const data = Object.fromEntries(new FormData(form)); | ||||||
|  |  | ||||||
|     setAttachments(attachments().concat([{ |     setAttachments( | ||||||
|       ...data, |       attachments().concat([ | ||||||
|       author_id: userinfo?.profiles?.id |         { | ||||||
|     }])); |           ...data, | ||||||
|  |           author_id: userinfo?.profiles?.id, | ||||||
|  |         }, | ||||||
|  |       ]), | ||||||
|  |     ); | ||||||
|     props.onInputAttachments(attachments()); |     props.onInputAttachments(attachments()); | ||||||
|     form.reset(); |     form.reset(); | ||||||
|   } |   } | ||||||
| @@ -74,10 +88,11 @@ export default function PostEditActions(props: { | |||||||
|  |  | ||||||
|     const form = evt.target as HTMLFormElement; |     const form = evt.target as HTMLFormElement; | ||||||
|     const data = Object.fromEntries(new FormData(form)); |     const data = Object.fromEntries(new FormData(form)); | ||||||
|     if (!data.alias) data.alias = crypto.randomUUID().replace(/-/g, ""); |     if (!data.category) return; | ||||||
|     if (!data.name) return; |  | ||||||
|  |  | ||||||
|     setCategories(categories().concat([data as any])); |     const item = availableCategories().find((item) => item.alias === data.category); | ||||||
|  |  | ||||||
|  |     setCategories(categories().concat([item])); | ||||||
|     props.onInputCategories(categories()); |     props.onInputCategories(categories()); | ||||||
|     form.reset(); |     form.reset(); | ||||||
|   } |   } | ||||||
| @@ -134,19 +149,21 @@ export default function PostEditActions(props: { | |||||||
|               <span class="label-text">Alias</span> |               <span class="label-text">Alias</span> | ||||||
|             </div> |             </div> | ||||||
|             <input |             <input | ||||||
|               name="alias" type="text" placeholder="Type here" |               name="alias" | ||||||
|  |               type="text" | ||||||
|  |               placeholder="Type here" | ||||||
|               class="input input-bordered w-full" |               class="input input-bordered w-full" | ||||||
|               value={props.editing?.alias ?? ""} |               value={props.editing?.alias ?? ""} | ||||||
|               onInput={(evt) => props.onInputAlias(evt.target.value)} |               onInput={(evt) => props.onInputAlias(evt.target.value)} | ||||||
|             /> |             /> | ||||||
|             <div class="label"> |             <div class="label"> | ||||||
|               <span class="label-text-alt"> |               <span class="label-text-alt">Leave blank to generate a random string.</span> | ||||||
|                Leave blank to generate a random string. |  | ||||||
|               </span> |  | ||||||
|             </div> |             </div> | ||||||
|           </label> |           </label> | ||||||
|           <div class="modal-action"> |           <div class="modal-action"> | ||||||
|             <button type="button" class="btn" onClick={() => closeModel("#alias")}>Close</button> |             <button type="button" class="btn" onClick={() => closeModel("#alias")}> | ||||||
|  |               Close | ||||||
|  |             </button> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </dialog> |       </dialog> | ||||||
| @@ -159,7 +176,8 @@ export default function PostEditActions(props: { | |||||||
|               <span class="label-text">Published At</span> |               <span class="label-text">Published At</span> | ||||||
|             </div> |             </div> | ||||||
|             <input |             <input | ||||||
|               name="published_at" type="datetime-local" |               name="published_at" | ||||||
|  |               type="datetime-local" | ||||||
|               placeholder="Pick a date" |               placeholder="Pick a date" | ||||||
|               class="input input-bordered w-full" |               class="input input-bordered w-full" | ||||||
|               value={props.editing?.published_at ?? ""} |               value={props.editing?.published_at ?? ""} | ||||||
| @@ -167,13 +185,14 @@ export default function PostEditActions(props: { | |||||||
|             /> |             /> | ||||||
|             <div class="label"> |             <div class="label"> | ||||||
|               <span class="label-text-alt"> |               <span class="label-text-alt"> | ||||||
|                 Before this time, your post will not be visible for everyone. |                 Before this time, your post will not be visible for everyone. You can modify this plan on Creator Hub. | ||||||
|                 You can modify this plan on Creator Hub. |  | ||||||
|               </span> |               </span> | ||||||
|             </div> |             </div> | ||||||
|           </label> |           </label> | ||||||
|           <div class="modal-action"> |           <div class="modal-action"> | ||||||
|             <button type="button" class="btn" onClick={() => closeModel("#planning-publish")}>Close</button> |             <button type="button" class="btn" onClick={() => closeModel("#planning-publish")}> | ||||||
|  |               Close | ||||||
|  |             </button> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </dialog> |       </dialog> | ||||||
| @@ -183,10 +202,24 @@ export default function PostEditActions(props: { | |||||||
|           <h3 class="font-bold text-lg mx-1">Attachments</h3> |           <h3 class="font-bold text-lg mx-1">Attachments</h3> | ||||||
|  |  | ||||||
|           <div role="tablist" class="tabs tabs-boxed mt-3"> |           <div role="tablist" class="tabs tabs-boxed mt-3"> | ||||||
|             <input type="radio" name="attachment" role="tab" class="tab" aria-label="File picker" |             <input | ||||||
|                    checked={attachmentMode() === 0} onClick={() => setAttachmentMode(0)} /> |               type="radio" | ||||||
|             <input type="radio" name="attachment" role="tab" class="tab" aria-label="External link" |               name="attachment" | ||||||
|                    checked={attachmentMode() === 1} onClick={() => setAttachmentMode(1)} /> |               role="tab" | ||||||
|  |               class="tab" | ||||||
|  |               aria-label="File picker" | ||||||
|  |               checked={attachmentMode() === 0} | ||||||
|  |               onClick={() => setAttachmentMode(0)} | ||||||
|  |             /> | ||||||
|  |             <input | ||||||
|  |               type="radio" | ||||||
|  |               name="attachment" | ||||||
|  |               role="tab" | ||||||
|  |               class="tab" | ||||||
|  |               aria-label="External link" | ||||||
|  |               checked={attachmentMode() === 1} | ||||||
|  |               onClick={() => setAttachmentMode(1)} | ||||||
|  |             /> | ||||||
|           </div> |           </div> | ||||||
|  |  | ||||||
|           <Switch> |           <Switch> | ||||||
| @@ -197,8 +230,12 @@ export default function PostEditActions(props: { | |||||||
|                     <span class="label-text">Pick a file</span> |                     <span class="label-text">Pick a file</span> | ||||||
|                   </div> |                   </div> | ||||||
|                   <div class="join"> |                   <div class="join"> | ||||||
|                     <input required type="file" name="attachment" |                     <input | ||||||
|                            class="join-item file-input file-input-bordered w-full" /> |                       required | ||||||
|  |                       type="file" | ||||||
|  |                       name="attachment" | ||||||
|  |                       class="join-item file-input file-input-bordered w-full" | ||||||
|  |                     /> | ||||||
|                     <button type="submit" class="join-item btn btn-primary" disabled={uploading()}> |                     <button type="submit" class="join-item btn btn-primary" disabled={uploading()}> | ||||||
|                       <i class="fa-solid fa-upload"></i> |                       <i class="fa-solid fa-upload"></i> | ||||||
|                     </button> |                     </button> | ||||||
| @@ -216,14 +253,29 @@ export default function PostEditActions(props: { | |||||||
|                     <span class="label-text">Attach an external file</span> |                     <span class="label-text">Attach an external file</span> | ||||||
|                   </div> |                   </div> | ||||||
|                   <div class="join"> |                   <div class="join"> | ||||||
|                     <input required type="text" name="mimetype" class="join-item input input-bordered w-full" |                     <input | ||||||
|                            placeholder="Mimetype" /> |                       required | ||||||
|                     <input required type="text" name="filename" class="join-item input input-bordered w-full" |                       type="text" | ||||||
|                            placeholder="Name" /> |                       name="mimetype" | ||||||
|  |                       class="join-item input input-bordered w-full" | ||||||
|  |                       placeholder="Mimetype" | ||||||
|  |                     /> | ||||||
|  |                     <input | ||||||
|  |                       required | ||||||
|  |                       type="text" | ||||||
|  |                       name="filename" | ||||||
|  |                       class="join-item input input-bordered w-full" | ||||||
|  |                       placeholder="Name" | ||||||
|  |                     /> | ||||||
|                   </div> |                   </div> | ||||||
|                   <div class="join"> |                   <div class="join"> | ||||||
|                     <input required type="text" name="external_url" class="join-item input input-bordered w-full" |                     <input | ||||||
|                            placeholder="External URL" /> |                       required | ||||||
|  |                       type="text" | ||||||
|  |                       name="external_url" | ||||||
|  |                       class="join-item input input-bordered w-full" | ||||||
|  |                       placeholder="External URL" | ||||||
|  |                     /> | ||||||
|                     <button type="submit" class="join-item btn btn-primary"> |                     <button type="submit" class="join-item btn btn-primary"> | ||||||
|                       <i class="fa-solid fa-plus"></i> |                       <i class="fa-solid fa-plus"></i> | ||||||
|                     </button> |                     </button> | ||||||
| @@ -240,19 +292,23 @@ export default function PostEditActions(props: { | |||||||
|             <h3 class="font-bold mt-3 mx-1">Attachment list</h3> |             <h3 class="font-bold mt-3 mx-1">Attachment list</h3> | ||||||
|             <ol class="mt-2 mx-1 text-sm"> |             <ol class="mt-2 mx-1 text-sm"> | ||||||
|               <For each={attachments()}> |               <For each={attachments()}> | ||||||
|                 {(item, idx) => <li> |                 {(item, idx) => ( | ||||||
|                   <i class="fa-regular fa-file me-2"></i> |                   <li> | ||||||
|                   {item.filename} |                     <i class="fa-regular fa-file me-2"></i> | ||||||
|                   <button class="ml-2" onClick={() => removeAttachment(idx())}> |                     {item.filename} | ||||||
|                     <i class="fa-solid fa-delete-left"></i> |                     <button class="ml-2" onClick={() => removeAttachment(idx())}> | ||||||
|                   </button> |                       <i class="fa-solid fa-delete-left"></i> | ||||||
|                 </li>} |                     </button> | ||||||
|  |                   </li> | ||||||
|  |                 )} | ||||||
|               </For> |               </For> | ||||||
|             </ol> |             </ol> | ||||||
|           </Show> |           </Show> | ||||||
|  |  | ||||||
|           <div class="modal-action"> |           <div class="modal-action"> | ||||||
|             <button type="button" class="btn" onClick={() => closeModel("#attachments")}>Close</button> |             <button type="button" class="btn" onClick={() => closeModel("#attachments")}> | ||||||
|  |               Close | ||||||
|  |             </button> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </dialog> |       </dialog> | ||||||
| @@ -266,18 +322,13 @@ export default function PostEditActions(props: { | |||||||
|                 <span class="label-text">Add a category</span> |                 <span class="label-text">Add a category</span> | ||||||
|               </div> |               </div> | ||||||
|               <div class="join"> |               <div class="join"> | ||||||
|                 <input type="text" name="alias" placeholder="Alias" class="join-item input input-bordered w-full" /> |                 <select name="category" class="join-item select select-bordered w-full"> | ||||||
|                 <input type="text" name="name" placeholder="Name" class="join-item input input-bordered w-full" /> |                   <For each={availableCategories()}>{(item) => <option value={item.alias}>{item.name}</option>}</For> | ||||||
|  |                 </select> | ||||||
|                 <button type="submit" class="join-item btn btn-primary"> |                 <button type="submit" class="join-item btn btn-primary"> | ||||||
|                   <i class="fa-solid fa-plus"></i> |                   <i class="fa-solid fa-plus"></i> | ||||||
|                 </button> |                 </button> | ||||||
|               </div> |               </div> | ||||||
|               <div class="label"> |  | ||||||
|                 <span class="label-text-alt"> |  | ||||||
|                   Alias is the url key of this category. Lowercase only, required length 4-24. |  | ||||||
|                   Leave blank for auto generate. |  | ||||||
|                 </span> |  | ||||||
|               </div> |  | ||||||
|             </label> |             </label> | ||||||
|           </form> |           </form> | ||||||
|  |  | ||||||
| @@ -285,13 +336,15 @@ export default function PostEditActions(props: { | |||||||
|             <h3 class="font-bold mt-3 mx-1">Category list</h3> |             <h3 class="font-bold mt-3 mx-1">Category list</h3> | ||||||
|             <ol class="mt-2 mx-1 text-sm"> |             <ol class="mt-2 mx-1 text-sm"> | ||||||
|               <For each={categories()}> |               <For each={categories()}> | ||||||
|                 {(item, idx) => <li> |                 {(item, idx) => ( | ||||||
|                   <i class="fa-solid fa-layer-group me-2"></i> |                   <li> | ||||||
|                   {item.name} <span class={styles.description}>#{item.alias}</span> |                     <i class="fa-solid fa-layer-group me-2"></i> | ||||||
|                   <button class="ml-2" onClick={() => removeCategory(idx())}> |                     {item.name} <span class={styles.description}>#{item.alias}</span> | ||||||
|                     <i class="fa-solid fa-delete-left"></i> |                     <button class="ml-2" onClick={() => removeCategory(idx())}> | ||||||
|                   </button> |                       <i class="fa-solid fa-delete-left"></i> | ||||||
|                 </li>} |                     </button> | ||||||
|  |                   </li> | ||||||
|  |                 )} | ||||||
|               </For> |               </For> | ||||||
|             </ol> |             </ol> | ||||||
|           </Show> |           </Show> | ||||||
| @@ -310,8 +363,7 @@ export default function PostEditActions(props: { | |||||||
|               </div> |               </div> | ||||||
|               <div class="label"> |               <div class="label"> | ||||||
|                 <span class="label-text-alt"> |                 <span class="label-text-alt"> | ||||||
|                   Alias is the url key of this tag. Lowercase only, required length 4-24. |                   Alias is the url key of this tag. Lowercase only, required length 4-24. Leave blank for auto generate. | ||||||
|                   Leave blank for auto generate. |  | ||||||
|                 </span> |                 </span> | ||||||
|               </div> |               </div> | ||||||
|             </label> |             </label> | ||||||
| @@ -321,19 +373,23 @@ export default function PostEditActions(props: { | |||||||
|             <h3 class="font-bold mt-3 mx-1">Category list</h3> |             <h3 class="font-bold mt-3 mx-1">Category list</h3> | ||||||
|             <ol class="mt-2 mx-1 text-sm"> |             <ol class="mt-2 mx-1 text-sm"> | ||||||
|               <For each={tags()}> |               <For each={tags()}> | ||||||
|                 {(item, idx) => <li> |                 {(item, idx) => ( | ||||||
|                   <i class="fa-solid fa-tag me-2"></i> |                   <li> | ||||||
|                   {item.name} <span class={styles.description}>#{item.alias}</span> |                     <i class="fa-solid fa-tag me-2"></i> | ||||||
|                   <button class="ml-2" onClick={() => removeTag(idx())}> |                     {item.name} <span class={styles.description}>#{item.alias}</span> | ||||||
|                     <i class="fa-solid fa-delete-left"></i> |                     <button class="ml-2" onClick={() => removeTag(idx())}> | ||||||
|                   </button> |                       <i class="fa-solid fa-delete-left"></i> | ||||||
|                 </li>} |                     </button> | ||||||
|  |                   </li> | ||||||
|  |                 )} | ||||||
|               </For> |               </For> | ||||||
|             </ol> |             </ol> | ||||||
|           </Show> |           </Show> | ||||||
|  |  | ||||||
|           <div class="modal-action"> |           <div class="modal-action"> | ||||||
|             <button type="button" class="btn" onClick={() => closeModel("#categories-and-tags")}>Close</button> |             <button type="button" class="btn" onClick={() => closeModel("#categories-and-tags")}> | ||||||
|  |               Close | ||||||
|  |             </button> | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </dialog> |       </dialog> | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| debug = true | debug = false | ||||||
|  |  | ||||||
| name = "Goatplaza" | name = "Goatplaza" | ||||||
| maintainer = "SmartSheep Studio" | maintainer = "SmartSheep Studio" | ||||||
| @@ -9,8 +9,6 @@ secret = "LtTjzAGFLshwXhN4ZD4nG5KlMv1MWcsvfv03TSZYnT1VhiAnLIZFTnHUwR0XhGgi" | |||||||
|  |  | ||||||
| content = "uploads" | content = "uploads" | ||||||
|  |  | ||||||
| everyone_postable = false |  | ||||||
|  |  | ||||||
| [passport] | [passport] | ||||||
| client_id = "goatplaza" | client_id = "goatplaza" | ||||||
| client_secret = "Z9k9AFTj^p" | client_secret = "Z9k9AFTj^p" | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user