✨ Creator hub
This commit is contained in:
		
							
								
								
									
										57
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								.idea/codeStyles/Project.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | <component name="ProjectCodeStyleConfiguration"> | ||||||
|  |   <code_scheme name="Project" version="173"> | ||||||
|  |     <HTMLCodeStyleSettings> | ||||||
|  |       <option name="HTML_SPACE_INSIDE_EMPTY_TAG" value="true" /> | ||||||
|  |     </HTMLCodeStyleSettings> | ||||||
|  |     <JSCodeStyleSettings version="0"> | ||||||
|  |       <option name="FORCE_SEMICOLON_STYLE" value="true" /> | ||||||
|  |       <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" /> | ||||||
|  |       <option name="FORCE_QUOTE_STYlE" value="true" /> | ||||||
|  |       <option name="ENFORCE_TRAILING_COMMA" value="Remove" /> | ||||||
|  |       <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" /> | ||||||
|  |       <option name="SPACES_WITHIN_IMPORTS" value="true" /> | ||||||
|  |     </JSCodeStyleSettings> | ||||||
|  |     <TypeScriptCodeStyleSettings version="0"> | ||||||
|  |       <option name="FORCE_SEMICOLON_STYLE" value="true" /> | ||||||
|  |       <option name="SPACE_BEFORE_FUNCTION_LEFT_PARENTH" value="false" /> | ||||||
|  |       <option name="FORCE_QUOTE_STYlE" value="true" /> | ||||||
|  |       <option name="ENFORCE_TRAILING_COMMA" value="Remove" /> | ||||||
|  |       <option name="SPACES_WITHIN_OBJECT_LITERAL_BRACES" value="true" /> | ||||||
|  |       <option name="SPACES_WITHIN_IMPORTS" value="true" /> | ||||||
|  |     </TypeScriptCodeStyleSettings> | ||||||
|  |     <VueCodeStyleSettings> | ||||||
|  |       <option name="INTERPOLATION_NEW_LINE_AFTER_START_DELIMITER" value="false" /> | ||||||
|  |       <option name="INTERPOLATION_NEW_LINE_BEFORE_END_DELIMITER" value="false" /> | ||||||
|  |     </VueCodeStyleSettings> | ||||||
|  |     <codeStyleSettings language="HTML"> | ||||||
|  |       <option name="SOFT_MARGINS" value="120" /> | ||||||
|  |       <indentOptions> | ||||||
|  |         <option name="INDENT_SIZE" value="2" /> | ||||||
|  |         <option name="CONTINUATION_INDENT_SIZE" value="2" /> | ||||||
|  |         <option name="TAB_SIZE" value="2" /> | ||||||
|  |       </indentOptions> | ||||||
|  |     </codeStyleSettings> | ||||||
|  |     <codeStyleSettings language="JavaScript"> | ||||||
|  |       <option name="SOFT_MARGINS" value="120" /> | ||||||
|  |       <indentOptions> | ||||||
|  |         <option name="INDENT_SIZE" value="2" /> | ||||||
|  |         <option name="CONTINUATION_INDENT_SIZE" value="2" /> | ||||||
|  |         <option name="TAB_SIZE" value="2" /> | ||||||
|  |       </indentOptions> | ||||||
|  |     </codeStyleSettings> | ||||||
|  |     <codeStyleSettings language="TypeScript"> | ||||||
|  |       <option name="SOFT_MARGINS" value="120" /> | ||||||
|  |       <indentOptions> | ||||||
|  |         <option name="INDENT_SIZE" value="2" /> | ||||||
|  |         <option name="CONTINUATION_INDENT_SIZE" value="2" /> | ||||||
|  |         <option name="TAB_SIZE" value="2" /> | ||||||
|  |       </indentOptions> | ||||||
|  |     </codeStyleSettings> | ||||||
|  |     <codeStyleSettings language="Vue"> | ||||||
|  |       <option name="SOFT_MARGINS" value="120" /> | ||||||
|  |       <indentOptions> | ||||||
|  |         <option name="CONTINUATION_INDENT_SIZE" value="2" /> | ||||||
|  |       </indentOptions> | ||||||
|  |     </codeStyleSettings> | ||||||
|  |   </code_scheme> | ||||||
|  | </component> | ||||||
							
								
								
									
										5
									
								
								.idea/codeStyles/codeStyleConfig.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								.idea/codeStyles/codeStyleConfig.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,5 @@ | |||||||
|  | <component name="ProjectCodeStyleConfiguration"> | ||||||
|  |   <state> | ||||||
|  |     <option name="USE_PER_PROJECT_SETTINGS" value="true" /> | ||||||
|  |   </state> | ||||||
|  | </component> | ||||||
							
								
								
									
										12
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								.idea/dataSources.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,12 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="DataSourceManagerImpl" format="xml" multifile-model="true"> | ||||||
|  |     <data-source source="LOCAL" name="hy_interactive@localhost" uuid="2e2101b2-4037-47ee-88ed-456dc2cb4423"> | ||||||
|  |       <driver-ref>postgresql</driver-ref> | ||||||
|  |       <synchronize>true</synchronize> | ||||||
|  |       <jdbc-driver>org.postgresql.Driver</jdbc-driver> | ||||||
|  |       <jdbc-url>jdbc:postgresql://localhost:5432/hy_interactive</jdbc-url> | ||||||
|  |       <working-dir>$ProjectFileDir$</working-dir> | ||||||
|  |     </data-source> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										11
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								.idea/inspectionProfiles/Project_Default.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,11 @@ | |||||||
|  | <component name="InspectionProjectProfileManager"> | ||||||
|  |   <profile version="1.0"> | ||||||
|  |     <option name="myName" value="Project Default" /> | ||||||
|  |     <inspection_tool class="DuplicatedCode" enabled="true" level="WEAK WARNING" enabled_by_default="true"> | ||||||
|  |       <Languages> | ||||||
|  |         <language minSize="54" name="TypeScript" /> | ||||||
|  |       </Languages> | ||||||
|  |     </inspection_tool> | ||||||
|  |     <inspection_tool class="SqlDialectInspection" enabled="false" level="WARNING" enabled_by_default="false" /> | ||||||
|  |   </profile> | ||||||
|  | </component> | ||||||
							
								
								
									
										6
									
								
								.idea/sqldialects.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.idea/sqldialects.xml
									
									
									
										generated
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | |||||||
|  | <?xml version="1.0" encoding="UTF-8"?> | ||||||
|  | <project version="4"> | ||||||
|  |   <component name="SqlDialectMappings"> | ||||||
|  |     <file url="file://$PROJECT_DIR$/pkg/server/posts_api.go" dialect="PostgreSQL" /> | ||||||
|  |   </component> | ||||||
|  | </project> | ||||||
							
								
								
									
										
											BIN
										
									
								
								pkg/.DS_Store
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								pkg/.DS_Store
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -5,6 +5,7 @@ import "time" | |||||||
| type Post struct { | type Post struct { | ||||||
| 	BaseModel | 	BaseModel | ||||||
|  |  | ||||||
|  | 	// TODO Introduce thumbnail | ||||||
| 	Alias            string        `json:"alias" gorm:"uniqueIndex"` | 	Alias            string        `json:"alias" gorm:"uniqueIndex"` | ||||||
| 	Title            string        `json:"title"` | 	Title            string        `json:"title"` | ||||||
| 	Content          string        `json:"content"` | 	Content          string        `json:"content"` | ||||||
|   | |||||||
							
								
								
									
										95
									
								
								pkg/server/creators_api.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								pkg/server/creators_api.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,95 @@ | |||||||
|  | package server | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"code.smartsheep.studio/hydrogen/interactive/pkg/database" | ||||||
|  | 	"code.smartsheep.studio/hydrogen/interactive/pkg/models" | ||||||
|  | 	"code.smartsheep.studio/hydrogen/interactive/pkg/services" | ||||||
|  | 	"github.com/gofiber/fiber/v2" | ||||||
|  | 	"github.com/samber/lo" | ||||||
|  | 	"time" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | func getOwnPost(c *fiber.Ctx) error { | ||||||
|  | 	user := c.Locals("principal").(models.Account) | ||||||
|  |  | ||||||
|  | 	id := c.Params("postId") | ||||||
|  | 	take := c.QueryInt("take", 0) | ||||||
|  | 	offset := c.QueryInt("offset", 0) | ||||||
|  |  | ||||||
|  | 	tx := database.C.Where(&models.Post{ | ||||||
|  | 		Alias:    id, | ||||||
|  | 		AuthorID: user.ID, | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	post, err := services.GetPost(tx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusNotFound, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tx = database.C. | ||||||
|  | 		Where(&models.Post{ReplyID: &post.ID}). | ||||||
|  | 		Where("published_at <= ? OR published_at IS NULL", time.Now()). | ||||||
|  | 		Order("created_at desc") | ||||||
|  |  | ||||||
|  | 	var count int64 | ||||||
|  | 	if err := tx. | ||||||
|  | 		Model(&models.Post{}). | ||||||
|  | 		Count(&count).Error; err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	posts, err := services.ListPost(tx, take, offset) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.JSON(fiber.Map{ | ||||||
|  | 		"data":    post, | ||||||
|  | 		"count":   count, | ||||||
|  | 		"related": posts, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  |  | ||||||
|  | func listOwnPost(c *fiber.Ctx) error { | ||||||
|  | 	take := c.QueryInt("take", 0) | ||||||
|  | 	offset := c.QueryInt("offset", 0) | ||||||
|  | 	realmId := c.QueryInt("realmId", 0) | ||||||
|  |  | ||||||
|  | 	user := c.Locals("principal").(models.Account) | ||||||
|  |  | ||||||
|  | 	tx := database.C. | ||||||
|  | 		Where(&models.Post{AuthorID: user.ID}). | ||||||
|  | 		Where("published_at <= ? OR published_at IS NULL", time.Now()). | ||||||
|  | 		Order("created_at desc") | ||||||
|  |  | ||||||
|  | 	if realmId > 0 { | ||||||
|  | 		tx = tx.Where(&models.Post{RealmID: lo.ToPtr(uint(realmId))}) | ||||||
|  | 	} else { | ||||||
|  | 		tx = tx.Where("realm_id IS NULL") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(c.Query("category")) > 0 { | ||||||
|  | 		tx = services.FilterPostWithCategory(tx, c.Query("category")) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if len(c.Query("tag")) > 0 { | ||||||
|  | 		tx = services.FilterPostWithTag(tx, c.Query("tag")) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var count int64 | ||||||
|  | 	if err := tx. | ||||||
|  | 		Model(&models.Post{}). | ||||||
|  | 		Count(&count).Error; err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	posts, err := services.ListPost(tx, take, offset) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.JSON(fiber.Map{ | ||||||
|  | 		"count": count, | ||||||
|  | 		"data":  posts, | ||||||
|  | 	}) | ||||||
|  | } | ||||||
| @@ -19,7 +19,7 @@ func getPost(c *fiber.Ctx) error { | |||||||
|  |  | ||||||
| 	tx := database.C.Where(&models.Post{ | 	tx := database.C.Where(&models.Post{ | ||||||
| 		Alias: id, | 		Alias: id, | ||||||
| 	}) | 	}).Where("published_at <= ? OR published_at IS NULL", time.Now()) | ||||||
|  |  | ||||||
| 	post, err := services.GetPost(tx) | 	post, err := services.GetPost(tx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
|   | |||||||
| @@ -40,6 +40,17 @@ func listOwnedRealm(c *fiber.Ctx) error { | |||||||
| 	return c.JSON(realms) | 	return c.JSON(realms) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func listAvailableRealm(c *fiber.Ctx) error { | ||||||
|  | 	user := c.Locals("principal").(models.Account) | ||||||
|  |  | ||||||
|  | 	realms, err := services.ListRealmIsAvailable(user) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return c.JSON(realms) | ||||||
|  | } | ||||||
|  |  | ||||||
| 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 { | ||||||
|   | |||||||
| @@ -76,8 +76,12 @@ 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("/creators/posts", auth, listOwnPost) | ||||||
|  | 		api.Get("/creators/posts/:postId", auth, getOwnPost) | ||||||
|  |  | ||||||
| 		api.Get("/realms", listRealm) | 		api.Get("/realms", listRealm) | ||||||
| 		api.Get("/realms/me", auth, listOwnedRealm) | 		api.Get("/realms/me", auth, listOwnedRealm) | ||||||
|  | 		api.Get("/realms/me/available", auth, listAvailableRealm) | ||||||
| 		api.Get("/realms/:realmId", getRealm) | 		api.Get("/realms/:realmId", getRealm) | ||||||
| 		api.Post("/realms", auth, createRealm) | 		api.Post("/realms", auth, createRealm) | ||||||
| 		api.Post("/realms/:realmId/invite", auth, inviteRealm) | 		api.Post("/realms/:realmId/invite", auth, inviteRealm) | ||||||
|   | |||||||
| @@ -3,6 +3,7 @@ package services | |||||||
| import ( | import ( | ||||||
| 	"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" | ||||||
|  | 	"github.com/samber/lo" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| func ListRealm() ([]models.Realm, error) { | func ListRealm() ([]models.Realm, error) { | ||||||
| @@ -23,6 +24,28 @@ func ListRealmWithUser(user models.Account) ([]models.Realm, error) { | |||||||
| 	return realms, nil | 	return realms, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | func ListRealmIsAvailable(user models.Account) ([]models.Realm, error) { | ||||||
|  | 	var realms []models.Realm | ||||||
|  | 	var members []models.RealmMember | ||||||
|  | 	if err := database.C.Where(&models.RealmMember{ | ||||||
|  | 		AccountID: user.ID, | ||||||
|  | 	}).Find(&members).Error; err != nil { | ||||||
|  | 		return realms, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	idx := lo.Map(members, func(item models.RealmMember, index int) uint { | ||||||
|  | 		return item.RealmID | ||||||
|  | 	}) | ||||||
|  |  | ||||||
|  | 	if err := database.C.Where(&models.Realm{ | ||||||
|  | 		IsPublic: true, | ||||||
|  | 	}).Or("id IN ?", idx).Find(&realms).Error; err != nil { | ||||||
|  | 		return realms, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return realms, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| func NewRealm(user models.Account, name, description string, isPublic bool) (models.Realm, error) { | func NewRealm(user models.Account, name, description string, isPublic bool) (models.Realm, error) { | ||||||
| 	realm := models.Realm{ | 	realm := models.Realm{ | ||||||
| 		Name:        name, | 		Name:        name, | ||||||
|   | |||||||
| @@ -12,6 +12,7 @@ | |||||||
|     "@fortawesome/fontawesome-free": "^6.5.1", |     "@fortawesome/fontawesome-free": "^6.5.1", | ||||||
|     "@solidjs/router": "^0.10.10", |     "@solidjs/router": "^0.10.10", | ||||||
|     "artplayer": "^5.1.1", |     "artplayer": "^5.1.1", | ||||||
|  |     "cherry-markdown": "^0.8.38", | ||||||
|     "dompurify": "^3.0.8", |     "dompurify": "^3.0.8", | ||||||
|     "flv.js": "^1.6.2", |     "flv.js": "^1.6.2", | ||||||
|     "hls.js": "^1.5.3", |     "hls.js": "^1.5.3", | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								pkg/view/src/components/LoadingAnimation.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								pkg/view/src/components/LoadingAnimation.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | export default function LoadingAnimation() { | ||||||
|  |   return ( | ||||||
|  |     <div class="w-full border-b border-base-200 pt-5 pb-7 text-center"> | ||||||
|  |       <p class="loading loading-lg loading-infinity"></p> | ||||||
|  |       <p>Listening to the latest news...</p> | ||||||
|  |     </div> | ||||||
|  |   ) | ||||||
|  | } | ||||||
| @@ -1,119 +1,28 @@ | |||||||
| import { createEffect, createSignal, For, Match, Show, Switch } from "solid-js"; | import { closeModel, openModel } from "../../scripts/modals.ts"; | ||||||
| import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; | import { createSignal, For, Match, Show, Switch } from "solid-js"; | ||||||
|  | import { getAtk, useUserinfo } from "../../stores/userinfo.tsx"; | ||||||
| 
 | 
 | ||||||
| import styles from "./PostPublish.module.css"; | import styles from "./PostPublish.module.css"; | ||||||
| import { closeModel, openModel } from "../scripts/modals.ts"; |  | ||||||
| 
 | 
 | ||||||
| export default function PostPublish(props: { | export default function PostEditActions(props: { | ||||||
|   replying?: any, |  | ||||||
|   reposting?: any, |  | ||||||
|   editing?: any, |   editing?: any, | ||||||
|   realmId?: number, |   onInputAlias: (value: string) => void, | ||||||
|   onReset: () => void, |   onInputPublish: (value: string) => void, | ||||||
|  |   onInputAttachments: (value: any[]) => void, | ||||||
|  |   onInputCategories: (categories: any[]) => void, | ||||||
|  |   onInputTags: (tags: any[]) => void, | ||||||
|   onError: (message: string | null) => void, |   onError: (message: string | null) => void, | ||||||
|   onPost: () => void |  | ||||||
| }) { | }) { | ||||||
|   const userinfo = useUserinfo(); |   const userinfo = useUserinfo() | ||||||
| 
 | 
 | ||||||
|   if (!userinfo?.isLoggedIn) { |  | ||||||
|     return ( |  | ||||||
|       <div class="py-9 flex justify-center items-center"> |  | ||||||
|         <div class="text-center"> |  | ||||||
|           <h2 class="text-lg font-bold">Login!</h2> |  | ||||||
|           <p>Or keep silent.</p> |  | ||||||
|         </div> |  | ||||||
|       </div> |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   const [submitting, setSubmitting] = createSignal(false); |  | ||||||
|   const [uploading, setUploading] = createSignal(false); |   const [uploading, setUploading] = createSignal(false); | ||||||
| 
 | 
 | ||||||
|   const [attachments, setAttachments] = createSignal<any[]>([]); |   const [attachments, setAttachments] = createSignal<any[]>(props.editing?.attachments ?? []); | ||||||
|   const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]); |   const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>(props.editing?.categories ?? []); | ||||||
|   const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]); |   const [tags, setTags] = createSignal<{ alias: string, name: string }[]>(props.editing?.tags ?? []); | ||||||
| 
 | 
 | ||||||
|   const [attachmentMode, setAttachmentMode] = createSignal(0); |   const [attachmentMode, setAttachmentMode] = createSignal(0); | ||||||
| 
 | 
 | ||||||
|   createEffect(() => { |  | ||||||
|     setAttachments(props.editing?.attachments ?? []); |  | ||||||
|     setCategories(props.editing?.categories ?? []); |  | ||||||
|     setTags(props.editing?.tags ?? []); |  | ||||||
|   }, [props.editing]); |  | ||||||
| 
 |  | ||||||
|   async function doPost(evt: SubmitEvent) { |  | ||||||
|     evt.preventDefault(); |  | ||||||
| 
 |  | ||||||
|     const form = evt.target as HTMLFormElement; |  | ||||||
|     const data = Object.fromEntries(new FormData(form)); |  | ||||||
|     if (!data.content) return; |  | ||||||
| 
 |  | ||||||
|     setSubmitting(true); |  | ||||||
|     const res = await fetch("/api/posts", { |  | ||||||
|       method: "POST", |  | ||||||
|       headers: { |  | ||||||
|         "Content-Type": "application/json", |  | ||||||
|         "Authorization": `Bearer ${getAtk()}` |  | ||||||
|       }, |  | ||||||
|       body: JSON.stringify({ |  | ||||||
|         alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""), |  | ||||||
|         title: data.title, |  | ||||||
|         content: data.content, |  | ||||||
|         attachments: attachments(), |  | ||||||
|         categories: categories(), |  | ||||||
|         tags: tags(), |  | ||||||
|         realm_id: data.publish_in_realm ? props.realmId : undefined, |  | ||||||
|         published_at: data.published_at ? new Date(data.published_at as string) : new Date(), |  | ||||||
|         repost_to: props.reposting?.id, |  | ||||||
|         reply_to: props.replying?.id |  | ||||||
|       }) |  | ||||||
|     }); |  | ||||||
|     if (res.status !== 200) { |  | ||||||
|       props.onError(await res.text()); |  | ||||||
|     } else { |  | ||||||
|       form.reset(); |  | ||||||
|       props.onError(null); |  | ||||||
|       props.onPost(); |  | ||||||
|     } |  | ||||||
|     setSubmitting(false); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function doEdit(evt: SubmitEvent) { |  | ||||||
|     evt.preventDefault(); |  | ||||||
| 
 |  | ||||||
|     const form = evt.target as HTMLFormElement; |  | ||||||
|     const data = Object.fromEntries(new FormData(form)); |  | ||||||
|     if (!data.content) return; |  | ||||||
|     if (uploading()) return; |  | ||||||
| 
 |  | ||||||
|     setSubmitting(true); |  | ||||||
|     const res = await fetch(`/api/posts/${props.editing?.id}`, { |  | ||||||
|       method: "PUT", |  | ||||||
|       headers: { |  | ||||||
|         "Content-Type": "application/json", |  | ||||||
|         "Authorization": `Bearer ${getAtk()}` |  | ||||||
|       }, |  | ||||||
|       body: JSON.stringify({ |  | ||||||
|         alias: data.alias ?? crypto.randomUUID().replace(/-/g, ""), |  | ||||||
|         title: data.title, |  | ||||||
|         content: data.content, |  | ||||||
|         attachments: attachments(), |  | ||||||
|         categories: categories(), |  | ||||||
|         tags: tags(), |  | ||||||
|         realm_id: props.realmId, |  | ||||||
|         published_at: data.published_at ? new Date(data.published_at as string) : new Date() |  | ||||||
|       }) |  | ||||||
|     }); |  | ||||||
|     if (res.status !== 200) { |  | ||||||
|       props.onError(await res.text()); |  | ||||||
|     } else { |  | ||||||
|       form.reset(); |  | ||||||
|       props.onError(null); |  | ||||||
|       props.onPost(); |  | ||||||
|     } |  | ||||||
|     setSubmitting(false); |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async function uploadAttachment(evt: SubmitEvent) { |   async function uploadAttachment(evt: SubmitEvent) { | ||||||
|     evt.preventDefault(); |     evt.preventDefault(); | ||||||
| 
 | 
 | ||||||
| @@ -148,6 +57,7 @@ export default function PostPublish(props: { | |||||||
|       ...data, |       ...data, | ||||||
|       author_id: userinfo?.profiles?.id |       author_id: userinfo?.profiles?.id | ||||||
|     }])); |     }])); | ||||||
|  |     props.onInputCategories(categories()) | ||||||
|     form.reset(); |     form.reset(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -160,6 +70,7 @@ export default function PostPublish(props: { | |||||||
|     if (!data.name) return; |     if (!data.name) return; | ||||||
| 
 | 
 | ||||||
|     setCategories(categories().concat([data as any])); |     setCategories(categories().concat([data as any])); | ||||||
|  |     props.onInputCategories(categories()) | ||||||
|     form.reset(); |     form.reset(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -176,90 +87,17 @@ export default function PostPublish(props: { | |||||||
|     if (!data.name) return; |     if (!data.name) return; | ||||||
| 
 | 
 | ||||||
|     setTags(tags().concat([data as any])); |     setTags(tags().concat([data as any])); | ||||||
|  |     props.onInputTags(tags()) | ||||||
|     form.reset(); |     form.reset(); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   function removeTag(target: any) { |   function removeTag(target: any) { | ||||||
|     setTags(tags().filter(item => item.alias !== target.alias)); |     setTags(tags().filter(item => item.alias !== target.alias)); | ||||||
|   } |     props.onInputTags(tags()) | ||||||
| 
 |  | ||||||
|   function resetForm() { |  | ||||||
|     setAttachments([]); |  | ||||||
|     setCategories([]); |  | ||||||
|     setTags([]); |  | ||||||
|     props.onReset(); |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   return ( |   return ( | ||||||
|     <> |     <> | ||||||
|       <form id="publish" onSubmit={(evt) => (props.editing ? doEdit : doPost)(evt)} onReset={() => resetForm()}> |  | ||||||
|         <div id="publish-identity" class="flex border-y border-base-200"> |  | ||||||
|           <div class="avatar pl-[20px]"> |  | ||||||
|             <div class="w-12"> |  | ||||||
|               <Show when={userinfo?.profiles?.avatar} |  | ||||||
|                     fallback={<span class="text-3xl">{userinfo?.displayName.substring(0, 1)}</span>}> |  | ||||||
|                 <img alt="avatar" src={userinfo?.profiles?.avatar} /> |  | ||||||
|               </Show> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|           <div class="flex flex-grow"> |  | ||||||
|             <input name="title" value={props.editing?.title ?? ""} |  | ||||||
|                    class={`${styles.publishInput} input w-full`} |  | ||||||
|                    placeholder="The describe for a long content" /> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
| 
 |  | ||||||
|         <Show when={props.reposting}> |  | ||||||
|           <div role="alert" class="bg-base-200 flex justify-between"> |  | ||||||
|             <div class="px-5 py-3"> |  | ||||||
|               <i class="fa-solid fa-circle-info me-3"></i> |  | ||||||
|               You are reposting a post from <b>{props.reposting?.author?.nick}</b> |  | ||||||
|             </div> |  | ||||||
|             <button type="reset" class="btn btn-ghost w-12" disabled={submitting()}> |  | ||||||
|               <i class="fa-solid fa-xmark"></i> |  | ||||||
|             </button> |  | ||||||
|           </div> |  | ||||||
|         </Show> |  | ||||||
|         <Show when={props.replying}> |  | ||||||
|           <div role="alert" class="bg-base-200 flex justify-between"> |  | ||||||
|             <div class="px-5 py-3"> |  | ||||||
|               <i class="fa-solid fa-circle-info me-3"></i> |  | ||||||
|               You are replying a post from <b>{props.replying?.author?.nick}</b> |  | ||||||
|             </div> |  | ||||||
|             <button type="reset" class="btn btn-ghost w-12" disabled={submitting()}> |  | ||||||
|               <i class="fa-solid fa-xmark"></i> |  | ||||||
|             </button> |  | ||||||
|           </div> |  | ||||||
|         </Show> |  | ||||||
|         <Show when={props.editing}> |  | ||||||
|           <div role="alert" class="bg-base-200 flex justify-between"> |  | ||||||
|             <div class="px-5 py-3"> |  | ||||||
|               <i class="fa-solid fa-circle-info me-3"></i> |  | ||||||
|               You are editing a post published at{" "} |  | ||||||
|               <b>{new Date(props.editing?.created_at).toLocaleString()}</b> |  | ||||||
|             </div> |  | ||||||
|             <button type="reset" class="btn btn-ghost w-12" disabled={submitting()}> |  | ||||||
|               <i class="fa-solid fa-xmark"></i> |  | ||||||
|             </button> |  | ||||||
|           </div> |  | ||||||
|         </Show> |  | ||||||
| 
 |  | ||||||
|         <Show when={props.realmId && !props.editing}> |  | ||||||
|           <div class="border-b border-base-200 px-5 h-[48px] flex items-center"> |  | ||||||
|             <div class="form-control flex-grow"> |  | ||||||
|               <label class="label cursor-pointer"> |  | ||||||
|                 <span class="label-text">Publish in this realm</span> |  | ||||||
|                 <input name="publish_in_realm" type="checkbox" checked class="checkbox checkbox-primary" /> |  | ||||||
|               </label> |  | ||||||
|             </div> |  | ||||||
|           </div> |  | ||||||
|         </Show> |  | ||||||
| 
 |  | ||||||
|         <textarea required name="content" value={props.editing?.content ?? ""} |  | ||||||
|                   class={`${styles.publishInput} textarea w-full`} |  | ||||||
|                   placeholder="What's happened?! (Support markdown)" /> |  | ||||||
| 
 |  | ||||||
|         <div id="publish-actions" class="flex justify-between border-y border-base-200"> |  | ||||||
|       <div class="flex pl-[20px]"> |       <div class="flex pl-[20px]"> | ||||||
|         <button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#alias")}> |         <button type="button" class="btn btn-ghost w-12" onClick={() => openModel("#alias")}> | ||||||
|           <i class="fa-solid fa-link"></i> |           <i class="fa-solid fa-link"></i> | ||||||
| @@ -275,15 +113,6 @@ export default function PostPublish(props: { | |||||||
|         </button> |         </button> | ||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|           <div> |  | ||||||
|             <button type="submit" class="btn btn-primary" disabled={submitting()}> |  | ||||||
|               <Show when={submitting()} fallback={props.editing ? "Save changes" : "Post a post"}> |  | ||||||
|                 <span class="loading"></span> |  | ||||||
|               </Show> |  | ||||||
|             </button> |  | ||||||
|           </div> |  | ||||||
|         </div> |  | ||||||
| 
 |  | ||||||
|       <dialog id="alias" class="modal"> |       <dialog id="alias" class="modal"> | ||||||
|         <div class="modal-box"> |         <div class="modal-box"> | ||||||
|           <h3 class="font-bold text-lg mx-1">Permalink</h3> |           <h3 class="font-bold text-lg mx-1">Permalink</h3> | ||||||
| @@ -291,7 +120,12 @@ export default function PostPublish(props: { | |||||||
|             <div class="label"> |             <div class="label"> | ||||||
|               <span class="label-text">Alias</span> |               <span class="label-text">Alias</span> | ||||||
|             </div> |             </div> | ||||||
|               <input name="alias" type="text" placeholder="Type here" class="input input-bordered w-full" /> |             <input | ||||||
|  |               name="alias" type="text" placeholder="Type here" | ||||||
|  |               class="input input-bordered w-full" | ||||||
|  |               value={props.editing?.alias ?? ""} | ||||||
|  |               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. |                Leave blank to generate a random string. | ||||||
| @@ -311,8 +145,13 @@ export default function PostPublish(props: { | |||||||
|             <div class="label"> |             <div class="label"> | ||||||
|               <span class="label-text">Published At</span> |               <span class="label-text">Published At</span> | ||||||
|             </div> |             </div> | ||||||
|               <input name="published_at" type="datetime-local" placeholder="Pick a date" |             <input | ||||||
|                      class="input input-bordered w-full" /> |               name="published_at" type="datetime-local" | ||||||
|  |               placeholder="Pick a date" | ||||||
|  |               class="input input-bordered w-full" | ||||||
|  |               value={props.editing?.published_at ?? ""} | ||||||
|  |               onInput={(evt) => props.onInputAlias(evt.target.value)} | ||||||
|  |             /> | ||||||
|             <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. | ||||||
| @@ -325,7 +164,6 @@ export default function PostPublish(props: { | |||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|       </dialog> |       </dialog> | ||||||
|       </form> |  | ||||||
| 
 | 
 | ||||||
|       <dialog id="attachments" class="modal"> |       <dialog id="attachments" class="modal"> | ||||||
|         <div class="modal-box"> |         <div class="modal-box"> | ||||||
							
								
								
									
										210
									
								
								pkg/view/src/components/posts/PostEditor.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								pkg/view/src/components/posts/PostEditor.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,210 @@ | |||||||
|  | import { createEffect, createMemo, createSignal, For, onMount, Show } from "solid-js"; | ||||||
|  |  | ||||||
|  | import Cherry from "cherry-markdown"; | ||||||
|  | import "cherry-markdown/dist/cherry-markdown.min.css"; | ||||||
|  | import { getAtk } from "../../stores/userinfo.tsx"; | ||||||
|  | import PostEditActions from "./PostEditActions.tsx"; | ||||||
|  |  | ||||||
|  | export default function PostEditor(props: { | ||||||
|  |   editing?: any, | ||||||
|  |   onError: (message: string | null) => void, | ||||||
|  |   onPost: () => void | ||||||
|  | }) { | ||||||
|  |   let editorContainer: any; | ||||||
|  |   const [editor, setEditor] = createSignal<Cherry>(); | ||||||
|  |   const [realmList, setRealmList] = createSignal<any[]>([]); | ||||||
|  |  | ||||||
|  |   const [submitting, setSubmitting] = createSignal(false); | ||||||
|  |  | ||||||
|  |   const [alias, setAlias] = createSignal(""); | ||||||
|  |   const [publishedAt, setPublishedAt] = createSignal(""); | ||||||
|  |   const [attachments, setAttachments] = createSignal<any[]>([]); | ||||||
|  |   const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]); | ||||||
|  |   const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]); | ||||||
|  |  | ||||||
|  |   const theme = createMemo(() => { | ||||||
|  |     if (window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches) { | ||||||
|  |       return "dark"; | ||||||
|  |     } else { | ||||||
|  |       return "light"; | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   createEffect(() => { | ||||||
|  |     editor()?.setTheme(theme()); | ||||||
|  |   }, [editor(), theme()]); | ||||||
|  |  | ||||||
|  |   onMount(() => { | ||||||
|  |     if (editorContainer) { | ||||||
|  |       setEditor(new Cherry({ | ||||||
|  |         el: editorContainer, | ||||||
|  |         value: "Welcome to the creator hub! " + | ||||||
|  |           "We provide a better editor than normal mode for you! " + | ||||||
|  |           "So you can tell us your mind clearly. " + | ||||||
|  |           "Delete this paragraph and getting start!" | ||||||
|  |       })); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   createEffect(() => { | ||||||
|  |     setAttachments(props.editing?.attachments ?? []); | ||||||
|  |     setCategories(props.editing?.categories ?? []); | ||||||
|  |     setTags(props.editing?.tags ?? []); | ||||||
|  |   }, [props.editing]); | ||||||
|  |  | ||||||
|  |   async function listRealm() { | ||||||
|  |     const res = await fetch("/api/realms/me/available", { | ||||||
|  |       headers: { "Authorization": `Bearer ${getAtk()}` } | ||||||
|  |     }); | ||||||
|  |     if (res.status === 200) { | ||||||
|  |       setRealmList(await res.json()); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   listRealm(); | ||||||
|  |  | ||||||
|  |   async function doPost(evt: SubmitEvent) { | ||||||
|  |     evt.preventDefault(); | ||||||
|  |  | ||||||
|  |     const form = evt.target as HTMLFormElement; | ||||||
|  |     const data = Object.fromEntries(new FormData(form)); | ||||||
|  |     if (!editor()?.getValue()) return; | ||||||
|  |  | ||||||
|  |     setSubmitting(true); | ||||||
|  |     const res = await fetch("/api/posts", { | ||||||
|  |       method: "POST", | ||||||
|  |       headers: { | ||||||
|  |         "Content-Type": "application/json", | ||||||
|  |         "Authorization": `Bearer ${getAtk()}` | ||||||
|  |       }, | ||||||
|  |       body: JSON.stringify({ | ||||||
|  |         alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""), | ||||||
|  |         title: data.title, | ||||||
|  |         content: editor()?.getValue(), | ||||||
|  |         attachments: attachments(), | ||||||
|  |         categories: categories(), | ||||||
|  |         tags: tags(), | ||||||
|  |         realm_id: parseInt(data.realm as string) !== 0 ? parseInt(data.realm as string) : undefined, | ||||||
|  |         published_at: publishedAt() ? new Date(publishedAt()) : new Date() | ||||||
|  |       }) | ||||||
|  |     }); | ||||||
|  |     if (res.status !== 200) { | ||||||
|  |       props.onError(await res.text()); | ||||||
|  |     } else { | ||||||
|  |       form.reset(); | ||||||
|  |       props.onError(null); | ||||||
|  |       props.onPost(); | ||||||
|  |     } | ||||||
|  |     setSubmitting(false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async function doEdit(evt: SubmitEvent) { | ||||||
|  |     evt.preventDefault(); | ||||||
|  |  | ||||||
|  |     const form = evt.target as HTMLFormElement; | ||||||
|  |     const data = Object.fromEntries(new FormData(form)); | ||||||
|  |     if (!editor()?.getValue()) return; | ||||||
|  |  | ||||||
|  |     setSubmitting(true); | ||||||
|  |     const res = await fetch(`/api/posts/${props.editing?.id}`, { | ||||||
|  |       method: "PUT", | ||||||
|  |       headers: { | ||||||
|  |         "Content-Type": "application/json", | ||||||
|  |         "Authorization": `Bearer ${getAtk()}` | ||||||
|  |       }, | ||||||
|  |       body: JSON.stringify({ | ||||||
|  |         alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""), | ||||||
|  |         title: data.title, | ||||||
|  |         content: editor()?.getValue(), | ||||||
|  |         attachments: attachments(), | ||||||
|  |         categories: categories(), | ||||||
|  |         tags: tags(), | ||||||
|  |         published_at: publishedAt() ? new Date(publishedAt()) : new Date() | ||||||
|  |       }) | ||||||
|  |     }); | ||||||
|  |     if (res.status !== 200) { | ||||||
|  |       props.onError(await res.text()); | ||||||
|  |     } else { | ||||||
|  |       form.reset(); | ||||||
|  |       props.onError(null); | ||||||
|  |       props.onPost(); | ||||||
|  |     } | ||||||
|  |     setSubmitting(false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function resetForm() { | ||||||
|  |     setAttachments([]); | ||||||
|  |     setCategories([]); | ||||||
|  |     setTags([]); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <form onReset={resetForm} onSubmit={(evt) => props.editing ? doEdit(evt) : doPost(evt)}> | ||||||
|  |       <div> | ||||||
|  |         <div ref={editorContainer}></div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="border-y border-base-200"> | ||||||
|  |         <PostEditActions | ||||||
|  |           onInputAlias={setAlias} | ||||||
|  |           onInputPublish={setPublishedAt} | ||||||
|  |           onInputAttachments={setAttachments} | ||||||
|  |           onInputCategories={setCategories} | ||||||
|  |           onInputTags={setTags} | ||||||
|  |           onError={props.onError} | ||||||
|  |         /> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="pt-3 pb-7 px-7"> | ||||||
|  |         <label class="form-control w-full"> | ||||||
|  |           <div class="label"> | ||||||
|  |             <span class="label-text">Publish region</span> | ||||||
|  |           </div> | ||||||
|  |           <select name="realm" class="select select-bordered" disabled={props.editing}> | ||||||
|  |             <option value={0} selected>Global</option> | ||||||
|  |             <For each={realmList()}> | ||||||
|  |               {item => <option value={item.id}>{item.name}</option>} | ||||||
|  |             </For> | ||||||
|  |           </select> | ||||||
|  |           <div class="label"> | ||||||
|  |             <span class="label-text-alt">Will show realms you joined or created.</span> | ||||||
|  |           </div> | ||||||
|  |         </label> | ||||||
|  |  | ||||||
|  |         <label class="form-control w-full"> | ||||||
|  |           <div class="label"> | ||||||
|  |             <span class="label-text">Post title</span> | ||||||
|  |           </div> | ||||||
|  |           <input value={props.editing?.title ?? ""} name="title" type="text" placeholder="Type here" | ||||||
|  |                  class="input input-bordered w-full" /> | ||||||
|  |         </label> | ||||||
|  |  | ||||||
|  |         <label class="form-control w-full"> | ||||||
|  |           <div class="label"> | ||||||
|  |             <span class="label-text">Post description</span> | ||||||
|  |           </div> | ||||||
|  |           <textarea value={props.editing?.description ?? ""} disabled name="description" | ||||||
|  |                     placeholder="Not available now" | ||||||
|  |                     class="textarea textarea-bordered w-full" /> | ||||||
|  |           <div class="label"> | ||||||
|  |             <span class="label-text-alt">Won't display in the post list when your post is too long.</span> | ||||||
|  |           </div> | ||||||
|  |         </label> | ||||||
|  |  | ||||||
|  |         <label class="form-control w-full"> | ||||||
|  |           <div class="label"> | ||||||
|  |             <span class="label-text">Post thumbnail</span> | ||||||
|  |           </div> | ||||||
|  |           <input disabled name="thumbnail" type="file" placeholder="Not available now" | ||||||
|  |                  class="file-input file-input-bordered w-full" /> | ||||||
|  |         </label> | ||||||
|  |  | ||||||
|  |         <button type="submit" class="btn btn-primary mt-7" disabled={submitting()}> | ||||||
|  |           <Show when={submitting()} fallback={"Submit"}> | ||||||
|  |             <span class="loading"></span> | ||||||
|  |           </Show> | ||||||
|  |         </button> | ||||||
|  |       </div> | ||||||
|  |     </form> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { createSignal, For, Show } from "solid-js"; | import { createSignal, For, Show } from "solid-js"; | ||||||
| import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; | import { getAtk, useUserinfo } from "../../stores/userinfo.tsx"; | ||||||
| import PostAttachments from "./PostAttachments.tsx"; | import PostAttachments from "./PostAttachments.tsx"; | ||||||
| import * as marked from "marked"; | import * as marked from "marked"; | ||||||
| import DOMPurify from "dompurify"; | import DOMPurify from "dompurify"; | ||||||
| @@ -2,7 +2,8 @@ import { createMemo, createSignal, For, Show } from "solid-js"; | |||||||
| 
 | 
 | ||||||
| import styles from "./PostList.module.css"; | import styles from "./PostList.module.css"; | ||||||
| import PostItem from "./PostItem.tsx"; | import PostItem from "./PostItem.tsx"; | ||||||
| import { getAtk } from "../stores/userinfo.tsx"; | import LoadingAnimation from "../LoadingAnimation.tsx"; | ||||||
|  | import { getAtk } from "../../stores/userinfo.tsx"; | ||||||
| 
 | 
 | ||||||
| export default function PostList(props: { | export default function PostList(props: { | ||||||
|   noRelated?: boolean, |   noRelated?: boolean, | ||||||
| @@ -86,10 +87,7 @@ export default function PostList(props: { | |||||||
|         </div> |         </div> | ||||||
| 
 | 
 | ||||||
|         <Show when={loading()}> |         <Show when={loading()}> | ||||||
|           <div class="w-full border-b border-base-200 pt-5 pb-7 text-center"> |           <LoadingAnimation /> | ||||||
|             <p class="loading loading-lg loading-infinity"></p> |  | ||||||
|             <p>Creating fake news...</p> |  | ||||||
|           </div> |  | ||||||
|         </Show> |         </Show> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
							
								
								
									
										212
									
								
								pkg/view/src/components/posts/PostPublish.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										212
									
								
								pkg/view/src/components/posts/PostPublish.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,212 @@ | |||||||
|  | import { createEffect, createSignal, Show } from "solid-js"; | ||||||
|  | import { getAtk, useUserinfo } from "../../stores/userinfo.tsx"; | ||||||
|  |  | ||||||
|  | import styles from "./PostPublish.module.css"; | ||||||
|  | import PostEditActions from "./PostEditActions.tsx"; | ||||||
|  |  | ||||||
|  | export default function PostPublish(props: { | ||||||
|  |   replying?: any, | ||||||
|  |   reposting?: any, | ||||||
|  |   editing?: any, | ||||||
|  |   realmId?: number, | ||||||
|  |   onReset: () => void, | ||||||
|  |   onError: (message: string | null) => void, | ||||||
|  |   onPost: () => void | ||||||
|  | }) { | ||||||
|  |   const userinfo = useUserinfo(); | ||||||
|  |  | ||||||
|  |   if (!userinfo?.isLoggedIn) { | ||||||
|  |     return ( | ||||||
|  |       <div class="py-9 flex justify-center items-center"> | ||||||
|  |         <div class="text-center"> | ||||||
|  |           <h2 class="text-lg font-bold">Login!</h2> | ||||||
|  |           <p>Or keep silent.</p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const [submitting, setSubmitting] = createSignal(false); | ||||||
|  |  | ||||||
|  |   const [alias, setAlias] = createSignal(""); | ||||||
|  |   const [publishedAt, setPublishedAt] = createSignal(""); | ||||||
|  |   const [attachments, setAttachments] = createSignal<any[]>([]); | ||||||
|  |   const [categories, setCategories] = createSignal<{ alias: string, name: string }[]>([]); | ||||||
|  |   const [tags, setTags] = createSignal<{ alias: string, name: string }[]>([]); | ||||||
|  |  | ||||||
|  |   createEffect(() => { | ||||||
|  |     setAttachments(props.editing?.attachments ?? []); | ||||||
|  |     setCategories(props.editing?.categories ?? []); | ||||||
|  |     setTags(props.editing?.tags ?? []); | ||||||
|  |   }, [props.editing]); | ||||||
|  |  | ||||||
|  |   async function doPost(evt: SubmitEvent) { | ||||||
|  |     evt.preventDefault(); | ||||||
|  |  | ||||||
|  |     const form = evt.target as HTMLFormElement; | ||||||
|  |     const data = Object.fromEntries(new FormData(form)); | ||||||
|  |     if (!data.content) return; | ||||||
|  |  | ||||||
|  |     setSubmitting(true); | ||||||
|  |     const res = await fetch("/api/posts", { | ||||||
|  |       method: "POST", | ||||||
|  |       headers: { | ||||||
|  |         "Content-Type": "application/json", | ||||||
|  |         "Authorization": `Bearer ${getAtk()}` | ||||||
|  |       }, | ||||||
|  |       body: JSON.stringify({ | ||||||
|  |         alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""), | ||||||
|  |         title: data.title, | ||||||
|  |         content: data.content, | ||||||
|  |         attachments: attachments(), | ||||||
|  |         categories: categories(), | ||||||
|  |         tags: tags(), | ||||||
|  |         realm_id: data.publish_in_realm ? props.realmId : undefined, | ||||||
|  |         published_at: publishedAt() ? new Date(publishedAt()) : new Date(), | ||||||
|  |         repost_to: props.reposting?.id, | ||||||
|  |         reply_to: props.replying?.id | ||||||
|  |       }) | ||||||
|  |     }); | ||||||
|  |     if (res.status !== 200) { | ||||||
|  |       props.onError(await res.text()); | ||||||
|  |     } else { | ||||||
|  |       form.reset(); | ||||||
|  |       props.onError(null); | ||||||
|  |       props.onPost(); | ||||||
|  |     } | ||||||
|  |     setSubmitting(false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   async function doEdit(evt: SubmitEvent) { | ||||||
|  |     evt.preventDefault(); | ||||||
|  |  | ||||||
|  |     const form = evt.target as HTMLFormElement; | ||||||
|  |     const data = Object.fromEntries(new FormData(form)); | ||||||
|  |     if (!data.content) return; | ||||||
|  |  | ||||||
|  |     setSubmitting(true); | ||||||
|  |     const res = await fetch(`/api/posts/${props.editing?.id}`, { | ||||||
|  |       method: "PUT", | ||||||
|  |       headers: { | ||||||
|  |         "Content-Type": "application/json", | ||||||
|  |         "Authorization": `Bearer ${getAtk()}` | ||||||
|  |       }, | ||||||
|  |       body: JSON.stringify({ | ||||||
|  |         alias: alias() ? alias() : crypto.randomUUID().replace(/-/g, ""), | ||||||
|  |         title: data.title, | ||||||
|  |         content: data.content, | ||||||
|  |         attachments: attachments(), | ||||||
|  |         categories: categories(), | ||||||
|  |         tags: tags(), | ||||||
|  |         realm_id: props.realmId, | ||||||
|  |         published_at: publishedAt() ? new Date(publishedAt()) : new Date(), | ||||||
|  |       }) | ||||||
|  |     }); | ||||||
|  |     if (res.status !== 200) { | ||||||
|  |       props.onError(await res.text()); | ||||||
|  |     } else { | ||||||
|  |       form.reset(); | ||||||
|  |       props.onError(null); | ||||||
|  |       props.onPost(); | ||||||
|  |     } | ||||||
|  |     setSubmitting(false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   function resetForm() { | ||||||
|  |     setAttachments([]); | ||||||
|  |     setCategories([]); | ||||||
|  |     setTags([]); | ||||||
|  |     props.onReset(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <form id="publish" onSubmit={(evt) => (props.editing ? doEdit : doPost)(evt)} onReset={() => resetForm()}> | ||||||
|  |         <div id="publish-identity" class="flex border-y border-base-200"> | ||||||
|  |           <div class="avatar pl-[20px]"> | ||||||
|  |             <div class="w-12"> | ||||||
|  |               <Show when={userinfo?.profiles?.avatar} | ||||||
|  |                     fallback={<span class="text-3xl">{userinfo?.displayName.substring(0, 1)}</span>}> | ||||||
|  |                 <img alt="avatar" src={userinfo?.profiles?.avatar} /> | ||||||
|  |               </Show> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |           <div class="flex flex-grow"> | ||||||
|  |             <input name="title" value={props.editing?.title ?? ""} | ||||||
|  |                    class={`${styles.publishInput} input w-full`} | ||||||
|  |                    placeholder="The describe for a long content" /> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <Show when={props.reposting}> | ||||||
|  |           <div role="alert" class="bg-base-200 flex justify-between"> | ||||||
|  |             <div class="px-5 py-3"> | ||||||
|  |               <i class="fa-solid fa-circle-info me-3"></i> | ||||||
|  |               You are reposting a post from <b>{props.reposting?.author?.nick}</b> | ||||||
|  |             </div> | ||||||
|  |             <button type="reset" class="btn btn-ghost w-12" disabled={submitting()}> | ||||||
|  |               <i class="fa-solid fa-xmark"></i> | ||||||
|  |             </button> | ||||||
|  |           </div> | ||||||
|  |         </Show> | ||||||
|  |         <Show when={props.replying}> | ||||||
|  |           <div role="alert" class="bg-base-200 flex justify-between"> | ||||||
|  |             <div class="px-5 py-3"> | ||||||
|  |               <i class="fa-solid fa-circle-info me-3"></i> | ||||||
|  |               You are replying a post from <b>{props.replying?.author?.nick}</b> | ||||||
|  |             </div> | ||||||
|  |             <button type="reset" class="btn btn-ghost w-12" disabled={submitting()}> | ||||||
|  |               <i class="fa-solid fa-xmark"></i> | ||||||
|  |             </button> | ||||||
|  |           </div> | ||||||
|  |         </Show> | ||||||
|  |         <Show when={props.editing}> | ||||||
|  |           <div role="alert" class="bg-base-200 flex justify-between"> | ||||||
|  |             <div class="px-5 py-3"> | ||||||
|  |               <i class="fa-solid fa-circle-info me-3"></i> | ||||||
|  |               You are editing a post published at{" "} | ||||||
|  |               <b>{new Date(props.editing?.created_at).toLocaleString()}</b> | ||||||
|  |             </div> | ||||||
|  |             <button type="reset" class="btn btn-ghost w-12" disabled={submitting()}> | ||||||
|  |               <i class="fa-solid fa-xmark"></i> | ||||||
|  |             </button> | ||||||
|  |           </div> | ||||||
|  |         </Show> | ||||||
|  |  | ||||||
|  |         <Show when={props.realmId && !props.editing}> | ||||||
|  |           <div class="border-b border-base-200 px-5 h-[48px] flex items-center"> | ||||||
|  |             <div class="form-control flex-grow"> | ||||||
|  |               <label class="label cursor-pointer"> | ||||||
|  |                 <span class="label-text">Publish in this realm</span> | ||||||
|  |                 <input name="publish_in_realm" type="checkbox" checked class="checkbox checkbox-primary" /> | ||||||
|  |               </label> | ||||||
|  |             </div> | ||||||
|  |           </div> | ||||||
|  |         </Show> | ||||||
|  |  | ||||||
|  |         <textarea required name="content" value={props.editing?.content ?? ""} | ||||||
|  |                   class={`${styles.publishInput} textarea w-full`} | ||||||
|  |                   placeholder="What's happened?! (Support markdown)" /> | ||||||
|  |  | ||||||
|  |         <div id="publish-actions" class="flex justify-between border-y border-base-200"> | ||||||
|  |           <PostEditActions | ||||||
|  |             onInputAlias={setAlias} | ||||||
|  |             onInputPublish={setPublishedAt} | ||||||
|  |             onInputAttachments={setAttachments} | ||||||
|  |             onInputCategories={setCategories} | ||||||
|  |             onInputTags={setTags} | ||||||
|  |             onError={props.onError} | ||||||
|  |           /> | ||||||
|  |  | ||||||
|  |           <div> | ||||||
|  |             <button type="submit" class="btn btn-primary" disabled={submitting()}> | ||||||
|  |               <Show when={submitting()} fallback={props.editing ? "Save changes" : "Post a post"}> | ||||||
|  |                 <span class="loading"></span> | ||||||
|  |               </Show> | ||||||
|  |             </button> | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </form> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -23,3 +23,17 @@ html, body { | |||||||
|     display: none; |     display: none; | ||||||
|     width: 0; |     width: 0; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .cherry, .cherry-toolbar, .cherry-editor, .cherry-previewer, .cherry-drag { | ||||||
|  |     box-shadow: none !important; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .cherry-drag { | ||||||
|  |     width: 2px !important; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (prefers-color-scheme: dark) { | ||||||
|  |     .cherry-drag { | ||||||
|  |         background: oklch(var(--b2)) !important; | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -11,9 +11,10 @@ import { Route, Router } from "@solidjs/router"; | |||||||
| import "@fortawesome/fontawesome-free/css/all.css"; | import "@fortawesome/fontawesome-free/css/all.css"; | ||||||
|  |  | ||||||
| import RootLayout from "./layouts/RootLayout.tsx"; | import RootLayout from "./layouts/RootLayout.tsx"; | ||||||
| import Feed from "./pages/view.tsx"; | import FeedView from "./pages/view.tsx"; | ||||||
| import Global from "./pages/global.tsx"; | import Global from "./pages/global.tsx"; | ||||||
| import PostReference from "./pages/post.tsx"; | import PostReference from "./pages/post.tsx"; | ||||||
|  | import CreatorView from "./pages/creators/view.tsx"; | ||||||
| import { UserinfoProvider } from "./stores/userinfo.tsx"; | import { UserinfoProvider } from "./stores/userinfo.tsx"; | ||||||
| import { WellKnownProvider } from "./stores/wellKnown.tsx"; | import { WellKnownProvider } from "./stores/wellKnown.tsx"; | ||||||
|  |  | ||||||
| @@ -23,7 +24,7 @@ render(() => ( | |||||||
|   <WellKnownProvider> |   <WellKnownProvider> | ||||||
|     <UserinfoProvider> |     <UserinfoProvider> | ||||||
|       <Router root={RootLayout}> |       <Router root={RootLayout}> | ||||||
|         <Route path="/" component={Feed}> |         <Route path="/" component={FeedView}> | ||||||
|           <Route path="/" component={Global} /> |           <Route path="/" component={Global} /> | ||||||
|           <Route path="/posts/:postId" component={PostReference} /> |           <Route path="/posts/:postId" component={PostReference} /> | ||||||
|           <Route path="/search" component={lazy(() => import("./pages/search.tsx"))} /> |           <Route path="/search" component={lazy(() => import("./pages/search.tsx"))} /> | ||||||
| @@ -31,6 +32,11 @@ render(() => ( | |||||||
|           <Route path="/realms/:realmId" component={lazy(() => import("./pages/realms/realm.tsx"))} /> |           <Route path="/realms/:realmId" component={lazy(() => import("./pages/realms/realm.tsx"))} /> | ||||||
|           <Route path="/accounts/:accountId" component={lazy(() => import("./pages/account.tsx"))} /> |           <Route path="/accounts/:accountId" component={lazy(() => import("./pages/account.tsx"))} /> | ||||||
|         </Route> |         </Route> | ||||||
|  |         <Route path="/creators" component={CreatorView}> | ||||||
|  |           <Route path="/" component={lazy(() => import("./pages/creators"))} /> | ||||||
|  |           <Route path="/publish" component={lazy(() => import("./pages/creators/publish.tsx"))} /> | ||||||
|  |           <Route path="/edit/:postId" component={lazy(() => import("./pages/creators/edit.tsx"))} /> | ||||||
|  |         </Route> | ||||||
|         <Route path="/auth" component={lazy(() => import("./pages/auth/callout.tsx"))} /> |         <Route path="/auth" component={lazy(() => import("./pages/auth/callout.tsx"))} /> | ||||||
|         <Route path="/auth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} /> |         <Route path="/auth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} /> | ||||||
|       </Router> |       </Router> | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ export default function RootLayout(props: any) { | |||||||
|   }, [ready, userinfo]); |   }, [ready, userinfo]); | ||||||
|  |  | ||||||
|   function keepGate(path: string, embedded: boolean, e?: BeforeLeaveEventArgs) { |   function keepGate(path: string, embedded: boolean, e?: BeforeLeaveEventArgs) { | ||||||
|     const blacklist = ["/creator"]; |     const blacklist = ["/creators"]; | ||||||
|  |  | ||||||
|     if (!userinfo?.isLoggedIn && blacklist.includes(path)) { |     if (!userinfo?.isLoggedIn && blacklist.includes(path)) { | ||||||
|       if (!e?.defaultPrevented) e?.preventDefault(); |       if (!e?.defaultPrevented) e?.preventDefault(); | ||||||
|   | |||||||
| @@ -11,6 +11,7 @@ interface MenuItem { | |||||||
|  |  | ||||||
| export default function Navbar() { | export default function Navbar() { | ||||||
|   const nav: MenuItem[] = [ |   const nav: MenuItem[] = [ | ||||||
|  |     { label: "Creators", href: "/creators" }, | ||||||
|     { label: "Feed", href: "/" }, |     { label: "Feed", href: "/" }, | ||||||
|     { label: "Realms", href: "/realms" } |     { label: "Realms", href: "/realms" } | ||||||
|   ]; |   ]; | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| import { createSignal, Show } from "solid-js"; | import { createSignal, Show } from "solid-js"; | ||||||
| import { useParams } from "@solidjs/router"; | import { useParams } from "@solidjs/router"; | ||||||
|  |  | ||||||
| import PostList from "../components/PostList.tsx"; | import PostList from "../components/posts/PostList.tsx"; | ||||||
| import NameCard from "../components/NameCard.tsx"; | import NameCard from "../components/NameCard.tsx"; | ||||||
| import PostPublish from "../components/PostPublish.tsx"; | import PostPublish from "../components/posts/PostPublish.tsx"; | ||||||
| import { createStore } from "solid-js/store"; | import { createStore } from "solid-js/store"; | ||||||
| import { closeModel, openModel } from "../scripts/modals.ts"; | import { closeModel, openModel } from "../scripts/modals.ts"; | ||||||
|  |  | ||||||
| @@ -66,7 +66,6 @@ export default function AccountPage() { | |||||||
|  |  | ||||||
|       <NameCard accountId={params["accountId"]} onError={setError} /> |       <NameCard accountId={params["accountId"]} onError={setError} /> | ||||||
|  |  | ||||||
|  |  | ||||||
|       <dialog id="post-publish" class="modal"> |       <dialog id="post-publish" class="modal"> | ||||||
|         <div class="modal-box p-0 w-[540px]"> |         <div class="modal-box p-0 w-[540px]"> | ||||||
|           <PostPublish |           <PostPublish | ||||||
|   | |||||||
							
								
								
									
										57
									
								
								pkg/view/src/pages/creators/edit.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								pkg/view/src/pages/creators/edit.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,57 @@ | |||||||
|  | import PostEdit from "../../components/posts/PostEditor.tsx"; | ||||||
|  | import { useNavigate, useParams } from "@solidjs/router"; | ||||||
|  | import { createSignal, Show } from "solid-js"; | ||||||
|  | import { getAtk } from "../../stores/userinfo.tsx"; | ||||||
|  |  | ||||||
|  | export default function PublishPost() { | ||||||
|  |   const navigate = useNavigate(); | ||||||
|  |   const params = useParams(); | ||||||
|  |  | ||||||
|  |   const [error, setError] = createSignal<string | null>(null); | ||||||
|  |   const [post, setPost] = createSignal<any>(); | ||||||
|  |  | ||||||
|  |   async function readPost() { | ||||||
|  |     const res = await fetch(`/api/creators/posts/${params["postId"]}`, { | ||||||
|  |       headers: { "Authorization": `Bearer ${getAtk()}` } | ||||||
|  |     }); | ||||||
|  |     if (res.status === 200) { | ||||||
|  |       setPost(await res.json()); | ||||||
|  |     } else { | ||||||
|  |       setError(await res.text()); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   readPost(); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <div class="flex pt-1 border-b border-base-200"> | ||||||
|  |         <a class="btn btn-ghost ml-[20px] w-12 h-12" href="/creators"> | ||||||
|  |           <i class="fa-solid fa-angle-left"></i> | ||||||
|  |         </a> | ||||||
|  |         <div class="px-5 flex items-center"> | ||||||
|  |           <p>Publish a new post</p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div id="alerts"> | ||||||
|  |         <Show when={error()}> | ||||||
|  |           <div role="alert" class="alert alert-error"> | ||||||
|  |             <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" | ||||||
|  |                  viewBox="0 0 24 24"> | ||||||
|  |               <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||||||
|  |                     d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> | ||||||
|  |             </svg> | ||||||
|  |             <span class="capitalize">{error()}</span> | ||||||
|  |           </div> | ||||||
|  |         </Show> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <PostEdit | ||||||
|  |         editing={post()} | ||||||
|  |         onError={setError} | ||||||
|  |         onPost={() => navigate("/creators")} | ||||||
|  |       /> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										120
									
								
								pkg/view/src/pages/creators/index.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								pkg/view/src/pages/creators/index.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,120 @@ | |||||||
|  | import { createMemo, createSignal, For, Show } from "solid-js"; | ||||||
|  | import { getAtk } from "../../stores/userinfo.tsx"; | ||||||
|  | import LoadingAnimation from "../../components/LoadingAnimation.tsx"; | ||||||
|  | import styles from "../../components/posts/PostList.module.css"; | ||||||
|  |  | ||||||
|  | export default function CreatorHub() { | ||||||
|  |   const [error, setError] = createSignal<string | null>(null); | ||||||
|  |  | ||||||
|  |   const [posts, setPosts] = createSignal<any[]>([]); | ||||||
|  |   const [postCount, setPostCount] = createSignal(0); | ||||||
|  |  | ||||||
|  |   const [page, setPage] = createSignal(1); | ||||||
|  |   const [loading, setLoading] = createSignal(false); | ||||||
|  |  | ||||||
|  |   const pageCount = createMemo(() => Math.ceil(postCount() / 10)); | ||||||
|  |  | ||||||
|  |   async function readPosts(pn?: number) { | ||||||
|  |     if (pn) setPage(pn); | ||||||
|  |     setLoading(true); | ||||||
|  |     const res = await fetch("/api/creators/posts?" + new URLSearchParams({ | ||||||
|  |       take: (10).toString(), | ||||||
|  |       offset: ((page() - 1) * 10).toString() | ||||||
|  |     }), { headers: { "Authorization": `Bearer ${getAtk()}` } }); | ||||||
|  |     if (res.status !== 200) { | ||||||
|  |       setError(await res.text()); | ||||||
|  |     } else { | ||||||
|  |       const data = await res.json(); | ||||||
|  |       setError(null); | ||||||
|  |       setPosts(data["data"]); | ||||||
|  |       setPostCount(data["count"]); | ||||||
|  |     } | ||||||
|  |     setLoading(false); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   readPosts(); | ||||||
|  |  | ||||||
|  |   function changePage(pn: number) { | ||||||
|  |     readPosts(pn).then(() => { | ||||||
|  |       setTimeout(() => window.scrollTo({ top: 0, behavior: "smooth" }), 16); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <div id="alerts"> | ||||||
|  |         <Show when={error()}> | ||||||
|  |           <div role="alert" class="alert alert-error"> | ||||||
|  |             <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" | ||||||
|  |                  viewBox="0 0 24 24"> | ||||||
|  |               <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||||||
|  |                     d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> | ||||||
|  |             </svg> | ||||||
|  |             <span class="capitalize">{error()}</span> | ||||||
|  |           </div> | ||||||
|  |         </Show> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="mt-1 px-7 flex items-center justify-between border-b border-base-200"> | ||||||
|  |         <h3 class="py-3 font-bold">Your posts</h3> | ||||||
|  |         <a class="btn btn-primary" href="/creators/publish"> | ||||||
|  |           <i class="fa-solid fa-plus"></i> | ||||||
|  |         </a> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="grid justify-items-strench"> | ||||||
|  |         <For each={posts()}> | ||||||
|  |           {item => | ||||||
|  |             <a href={`/creators/edit/${item.alias}`}> | ||||||
|  |               <div class="card sm:card-side hover:bg-base-200 transition-colors sm:max-w-none"> | ||||||
|  |                 <div class="card-body"> | ||||||
|  |                   <Show when={item?.title} fallback={ | ||||||
|  |                     <div class="line-clamp-3"> | ||||||
|  |                       {item?.content?.replaceAll("#", "").replaceAll("*", "").trim()} | ||||||
|  |                     </div> | ||||||
|  |                   }> | ||||||
|  |                     <h2 class="text-xl">{item?.title}</h2> | ||||||
|  |                     <div class="mx-[-2px] mt-[-4px]"> | ||||||
|  |                       {item?.categories?.map((category: any) => ( | ||||||
|  |                         <span class="badge badge-primary">{category.name}</span> | ||||||
|  |                       ))} | ||||||
|  |                       {item?.tags?.map((tag: any) => ( | ||||||
|  |                         <span class="badge badge-secondary">{tag.name}</span> | ||||||
|  |                       ))} | ||||||
|  |                     </div> | ||||||
|  |                     <div class="text-sm opacity-80 line-clamp-3"> | ||||||
|  |                       {item?.content?.substring(0, 160).replaceAll("#", "").replaceAll("*", "").trim() + "……"} | ||||||
|  |                     </div> | ||||||
|  |                   </Show> | ||||||
|  |  | ||||||
|  |                   <div class="text-xs opacity-70 flex gap-2"> | ||||||
|  |                     <span>Post #{item?.id}</span> | ||||||
|  |                     <span>Published at {new Date(item?.published_at).toLocaleString()}</span> | ||||||
|  |                   </div> | ||||||
|  |                 </div> | ||||||
|  |               </div> | ||||||
|  |             </a> | ||||||
|  |           } | ||||||
|  |         </For> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div class="flex justify-center"> | ||||||
|  |         <div class="join"> | ||||||
|  |           <button class={`join-item btn btn-ghost ${styles.paginationControl}`} disabled={page() <= 1} | ||||||
|  |                   onClick={() => changePage(page() - 1)}> | ||||||
|  |             <i class="fa-solid fa-caret-left"></i> | ||||||
|  |           </button> | ||||||
|  |           <button class="join-item btn btn-ghost">Page {page()}</button> | ||||||
|  |           <button class={`join-item btn btn-ghost ${styles.paginationControl}`} disabled={page() >= pageCount()} | ||||||
|  |                   onClick={() => changePage(page() + 1)}> | ||||||
|  |             <i class="fa-solid fa-caret-right"></i> | ||||||
|  |           </button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <Show when={loading()}> | ||||||
|  |         <LoadingAnimation /> | ||||||
|  |       </Show> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										40
									
								
								pkg/view/src/pages/creators/publish.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								pkg/view/src/pages/creators/publish.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,40 @@ | |||||||
|  | import PostEdit from "../../components/posts/PostEditor.tsx"; | ||||||
|  | import { useNavigate } from "@solidjs/router"; | ||||||
|  | import { createSignal, Show } from "solid-js"; | ||||||
|  |  | ||||||
|  | export default function PublishPost() { | ||||||
|  |   const navigate = useNavigate(); | ||||||
|  |  | ||||||
|  |   const [error, setError] = createSignal<string | null>(null); | ||||||
|  |  | ||||||
|  |   return ( | ||||||
|  |     <> | ||||||
|  |       <div class="flex pt-1 border-b border-base-200"> | ||||||
|  |         <a class="btn btn-ghost ml-[20px] w-12 h-12" href="/creators"> | ||||||
|  |           <i class="fa-solid fa-angle-left"></i> | ||||||
|  |         </a> | ||||||
|  |         <div class="px-5 flex items-center"> | ||||||
|  |           <p>Publish a new post</p> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div id="alerts"> | ||||||
|  |         <Show when={error()}> | ||||||
|  |           <div role="alert" class="alert alert-error"> | ||||||
|  |             <svg xmlns="http://www.w3.org/2000/svg" class="stroke-current shrink-0 h-6 w-6" fill="none" | ||||||
|  |                  viewBox="0 0 24 24"> | ||||||
|  |               <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" | ||||||
|  |                     d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" /> | ||||||
|  |             </svg> | ||||||
|  |             <span class="capitalize">{error()}</span> | ||||||
|  |           </div> | ||||||
|  |         </Show> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <PostEdit | ||||||
|  |         onError={setError} | ||||||
|  |         onPost={() => navigate("/creators")} | ||||||
|  |       /> | ||||||
|  |     </> | ||||||
|  |   ); | ||||||
|  | } | ||||||
							
								
								
									
										13
									
								
								pkg/view/src/pages/creators/view.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								pkg/view/src/pages/creators/view.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | |||||||
|  | .wrapper { | ||||||
|  |     display: grid; | ||||||
|  |     grid-template-columns: 1fr; | ||||||
|  |     column-gap: 20px; | ||||||
|  |  | ||||||
|  |     max-height: calc(100vh - 64px); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | @media (min-width: 1024px) { | ||||||
|  |     .wrapper { | ||||||
|  |         grid-template-columns: 1fr 2fr; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										16
									
								
								pkg/view/src/pages/creators/view.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								pkg/view/src/pages/creators/view.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,16 @@ | |||||||
|  | import styles from "./view.module.css"; | ||||||
|  |  | ||||||
|  | export default function CreatorView(props: any) { | ||||||
|  |   return ( | ||||||
|  |     <div class={`${styles.wrapper} container mx-auto`}> | ||||||
|  |       <div id="nav" class="card shadow-xl h-fit"> | ||||||
|  |         <h2 class="text-xl font-bold mt-1 py-5 px-7">Creator Hub</h2> | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |       <div id="content" class="card shadow-xl"> | ||||||
|  |         {props.children} | ||||||
|  |       </div> | ||||||
|  |  | ||||||
|  |     </div> | ||||||
|  |   ); | ||||||
|  | } | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| import { createSignal, Show } from "solid-js"; | import { createSignal, Show } from "solid-js"; | ||||||
|  |  | ||||||
| import PostList from "../components/PostList.tsx"; | import PostList from "../components/posts/PostList.tsx"; | ||||||
| import PostPublish from "../components/PostPublish.tsx"; | import PostPublish from "../components/posts/PostPublish.tsx"; | ||||||
| import { createStore } from "solid-js/store"; | import { createStore } from "solid-js/store"; | ||||||
|  |  | ||||||
| export default function DashboardPage() { | export default function DashboardPage() { | ||||||
|   | |||||||
| @@ -2,9 +2,9 @@ import { createSignal, Show } from "solid-js"; | |||||||
| import { useNavigate, useParams, useSearchParams } from "@solidjs/router"; | import { useNavigate, useParams, useSearchParams } from "@solidjs/router"; | ||||||
| import { createStore } from "solid-js/store"; | import { createStore } from "solid-js/store"; | ||||||
| import { closeModel, openModel } from "../scripts/modals.ts"; | import { closeModel, openModel } from "../scripts/modals.ts"; | ||||||
| import PostPublish from "../components/PostPublish.tsx"; | import PostPublish from "../components/posts/PostPublish.tsx"; | ||||||
| import PostList from "../components/PostList.tsx"; | import PostList from "../components/posts/PostList.tsx"; | ||||||
| import PostItem from "../components/PostItem.tsx"; | import PostItem from "../components/posts/PostItem.tsx"; | ||||||
| import { getAtk } from "../stores/userinfo.tsx"; | import { getAtk } from "../stores/userinfo.tsx"; | ||||||
|  |  | ||||||
| export default function PostPage() { | export default function PostPage() { | ||||||
|   | |||||||
| @@ -2,8 +2,8 @@ import { createSignal, Show } from "solid-js"; | |||||||
| import { createStore } from "solid-js/store"; | import { createStore } from "solid-js/store"; | ||||||
| import { useNavigate, useParams } from "@solidjs/router"; | import { useNavigate, useParams } from "@solidjs/router"; | ||||||
|  |  | ||||||
| import PostList from "../../components/PostList.tsx"; | import PostList from "../../components/posts/PostList.tsx"; | ||||||
| import PostPublish from "../../components/PostPublish.tsx"; | import PostPublish from "../../components/posts/PostPublish.tsx"; | ||||||
|  |  | ||||||
| import styles from "./realm.module.css"; | import styles from "./realm.module.css"; | ||||||
| import { getAtk, useUserinfo } from "../../stores/userinfo.tsx"; | import { getAtk, useUserinfo } from "../../stores/userinfo.tsx"; | ||||||
|   | |||||||
| @@ -1,8 +1,8 @@ | |||||||
| import { useNavigate, useSearchParams } from "@solidjs/router"; | import { useNavigate, useSearchParams } from "@solidjs/router"; | ||||||
| import { createSignal, Show } from "solid-js"; | import { createSignal, Show } from "solid-js"; | ||||||
| import { createStore } from "solid-js/store"; | import { createStore } from "solid-js/store"; | ||||||
| import PostPublish from "../components/PostPublish.tsx"; | import PostPublish from "../components/posts/PostPublish.tsx"; | ||||||
| import PostList from "../components/PostList.tsx"; | import PostList from "../components/posts/PostList.tsx"; | ||||||
| import { closeModel, openModel } from "../scripts/modals.ts"; | import { closeModel, openModel } from "../scripts/modals.ts"; | ||||||
|  |  | ||||||
| export default function SearchPage() { | export default function SearchPage() { | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import styles from "./view.module.css"; | import styles from "./view.module.css"; | ||||||
|  |  | ||||||
| export default function DashboardPage(props: any) { | export default function FeedView(props: any) { | ||||||
|   return ( |   return ( | ||||||
|     <div class={`${styles.wrapper} container mx-auto`}> |     <div class={`${styles.wrapper} container mx-auto`}> | ||||||
|       <div id="trending" class="card shadow-xl h-fit"></div> |       <div id="trending" class="card shadow-xl h-fit"></div> | ||||||
|  |  | ||||||
|       <div id="content max-w-[100vw]" class="card shadow-xl"> |       <div id="content" class="card shadow-xl"> | ||||||
|         {props.children} |         {props.children} | ||||||
|       </div> |       </div> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user