✨ Personal Page
This commit is contained in:
		| @@ -14,6 +14,8 @@ type Post struct { | ||||
| 	DislikedAccounts []PostDislike `json:"disliked_accounts"` | ||||
| 	RepostTo         *Post         `json:"repost_to" gorm:"foreignKey:RepostID"` | ||||
| 	ReplyTo          *Post         `json:"reply_to" gorm:"foreignKey:ReplyID"` | ||||
| 	PinnedAt         *time.Time    `json:"pinned_at"` | ||||
| 	EditedAt         *time.Time    `json:"edited_at"` | ||||
| 	PublishedAt      time.Time     `json:"published_at"` | ||||
| 	RepostID         *uint         `json:"repost_id"` | ||||
| 	ReplyID          *uint         `json:"reply_id"` | ||||
|   | ||||
| @@ -13,16 +13,22 @@ import ( | ||||
| func listPost(c *fiber.Ctx) error { | ||||
| 	take := c.QueryInt("take", 0) | ||||
| 	offset := c.QueryInt("offset", 0) | ||||
| 	authorId := c.QueryInt("authorId", 0) | ||||
|  | ||||
| 	tx := database.C.Where(&models.Post{RealmID: nil}).Order("created_at desc") | ||||
|  | ||||
| 	if authorId > 0 { | ||||
| 		tx = tx.Where(&models.Post{AuthorID: uint(authorId)}) | ||||
| 	} | ||||
|  | ||||
| 	var count int64 | ||||
| 	if err := database.C. | ||||
| 		Where(&models.Post{RealmID: nil}). | ||||
| 	if err := tx. | ||||
| 		Model(&models.Post{}). | ||||
| 		Count(&count).Error; err != nil { | ||||
| 		return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	posts, err := services.ListPost(take, offset) | ||||
| 	posts, err := services.ListPost(tx, take, offset) | ||||
| 	if err != nil { | ||||
| 		return fiber.NewError(fiber.StatusBadRequest, err.Error()) | ||||
| 	} | ||||
|   | ||||
| @@ -57,7 +57,8 @@ func NewServer() { | ||||
| 	api := A.Group("/api").Name("API") | ||||
| 	{ | ||||
| 		api.Get("/users/me", auth, getUserinfo) | ||||
| 		api.Get("/users/:accountId/follow", auth, doFollowAccount) | ||||
| 		api.Get("/users/:accountId", getOthersInfo) | ||||
| 		api.Post("/users/:accountId/follow", auth, doFollowAccount) | ||||
|  | ||||
| 		api.Get("/auth", doLogin) | ||||
| 		api.Get("/auth/callback", doPostLogin) | ||||
|   | ||||
| @@ -20,6 +20,19 @@ func getUserinfo(c *fiber.Ctx) error { | ||||
| 	return c.JSON(data) | ||||
| } | ||||
|  | ||||
| func getOthersInfo(c *fiber.Ctx) error { | ||||
| 	accountId, _ := c.ParamsInt("accountId", 0) | ||||
|  | ||||
| 	var data models.Account | ||||
| 	if err := database.C. | ||||
| 		Where(&models.Account{BaseModel: models.BaseModel{ID: uint(accountId)}}). | ||||
| 		First(&data).Error; err != nil { | ||||
| 		return fiber.NewError(fiber.StatusInternalServerError, err.Error()) | ||||
| 	} | ||||
|  | ||||
| 	return c.JSON(data) | ||||
| } | ||||
|  | ||||
| func doFollowAccount(c *fiber.Ctx) error { | ||||
| 	user := c.Locals("principal").(models.Account) | ||||
| 	id, _ := c.ParamsInt("accountId", 0) | ||||
|   | ||||
| @@ -10,13 +10,11 @@ import ( | ||||
| 	"gorm.io/gorm" | ||||
| ) | ||||
|  | ||||
| func ListPost(take int, offset int) ([]*models.Post, error) { | ||||
| func ListPost(tx *gorm.DB, take int, offset int) ([]*models.Post, error) { | ||||
| 	var posts []*models.Post | ||||
| 	if err := database.C. | ||||
| 		Where(&models.Post{RealmID: nil}). | ||||
| 	if err := tx. | ||||
| 		Limit(take). | ||||
| 		Offset(offset). | ||||
| 		Order("created_at desc"). | ||||
| 		Preload("Author"). | ||||
| 		Find(&posts).Error; err != nil { | ||||
| 		return posts, err | ||||
|   | ||||
							
								
								
									
										3
									
								
								pkg/view/src/components/NameCard.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								pkg/view/src/components/NameCard.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| .description { | ||||
|     color: var(--fallback-bc, oklch(var(--bc)/.8)); | ||||
| } | ||||
							
								
								
									
										58
									
								
								pkg/view/src/components/NameCard.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										58
									
								
								pkg/view/src/components/NameCard.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,58 @@ | ||||
| import { createSignal } from "solid-js"; | ||||
|  | ||||
| import styles from "./NameCard.module.css"; | ||||
|  | ||||
| export default function NameCard(props: { accountId: number, onError: (messasge: string | null) => void }) { | ||||
|   const [info, setInfo] = createSignal<any>(null); | ||||
|  | ||||
|   const [_, setLoading] = createSignal(true); | ||||
|  | ||||
|   async function readInfo() { | ||||
|     setLoading(true); | ||||
|     const res = await fetch(`/api/users/${props.accountId}`); | ||||
|     if (res.status !== 200) { | ||||
|       props.onError(await res.text()); | ||||
|     } else { | ||||
|       setInfo(await res.json()); | ||||
|       props.onError(null); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   } | ||||
|  | ||||
|   readInfo(); | ||||
|  | ||||
|   return ( | ||||
|     <div class="relative"> | ||||
|       <figure id="banner"> | ||||
|         <img class="object-cover w-full h-36" src="https://images.unsplash.com/photo-1464822759023-fed622ff2c3b" | ||||
|              alt="banner" /> | ||||
|       </figure> | ||||
|  | ||||
|       <div id="avatar" class="avatar absolute border-4 border-base-200 left-[20px] top-[4.5rem]"> | ||||
|         <div class="w-24"> | ||||
|           <img src={info()?.avatar} alt="avatar" /> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div id="actions" class="flex justify-end"> | ||||
|         <div> | ||||
|           <button type="button" class="btn btn-primary"> | ||||
|             <i class="fa-solid fa-plus"></i> | ||||
|             Follow | ||||
|           </button> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <div id="description" class="px-6 pb-7"> | ||||
|         <h2 class="text-2xl font-bold">{info()?.name}</h2> | ||||
|         <p class="text-md">{info()?.description}</p> | ||||
|         <div class={`mt-2 ${styles.description}`}> | ||||
|           <p class="text-xs"> | ||||
|             <i class="fa-solid fa-calendar-days me-2"></i> | ||||
|             Joined at {new Date(info()?.created_at).toLocaleString()} | ||||
|           </p> | ||||
|         </div> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
| @@ -24,8 +24,9 @@ export default function PostItem(props: { post: any, onError: (message: string | | ||||
|   return ( | ||||
|     <div class="post-item"> | ||||
|  | ||||
|       <a href={`/accounts/${props.post.author.id}`}> | ||||
|         <div class="flex bg-base-200"> | ||||
|         <div class="avatar"> | ||||
|           <div class="avatar pl-[20px]"> | ||||
|             <div class="w-12"> | ||||
|               <Show when={props.post.author.avatar} | ||||
|                     fallback={<span class="text-3xl">{props.post.author.name.substring(0, 1)}</span>}> | ||||
| @@ -40,6 +41,7 @@ export default function PostItem(props: { post: any, onError: (message: string | | ||||
|             </div> | ||||
|           </div> | ||||
|         </div> | ||||
|       </a> | ||||
|  | ||||
|       <article class="py-5 px-7"> | ||||
|         <h2 class="card-title">{props.post.title}</h2> | ||||
| @@ -48,7 +50,7 @@ export default function PostItem(props: { post: any, onError: (message: string | | ||||
|  | ||||
|       <div class="grid grid-cols-3 border-y border-base-200"> | ||||
|  | ||||
|         <div class="col-span-2 grid grid-cols-4"> | ||||
|         <div class="grid grid-cols-2"> | ||||
|           <div class="tooltip" data-tip="Daisuki"> | ||||
|             <button type="button" class="btn btn-ghost btn-block" disabled={reacting()} | ||||
|                     onClick={() => reactPost(props.post, "like")}> | ||||
| @@ -64,7 +66,9 @@ export default function PostItem(props: { post: any, onError: (message: string | | ||||
|               <code class="font-mono">{props.post.dislike_count}</code> | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="col-span-2 flex justify-end"> | ||||
|           <div class="tooltip" data-tip="Reply"> | ||||
|             <button type="button" class="btn btn-ghost btn-block"> | ||||
|               <i class="fa-solid fa-reply"></i> | ||||
| @@ -76,9 +80,7 @@ export default function PostItem(props: { post: any, onError: (message: string | | ||||
|               <i class="fa-solid fa-retweet"></i> | ||||
|             </button> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|         <div class="flex justify-end"> | ||||
|           <div class="dropdown dropdown-end"> | ||||
|             <div tabIndex="0" role="button" class="btn btn-ghost w-12"> | ||||
|               <i class="fa-solid fa-ellipsis-vertical"></i> | ||||
|   | ||||
| @@ -1,33 +1,24 @@ | ||||
| import { createMemo, createSignal, For, Show } from "solid-js"; | ||||
|  | ||||
| import styles from "./PostList.module.css"; | ||||
|  | ||||
| import PostPublish from "./PostPublish.tsx"; | ||||
| import PostItem from "./PostItem.tsx"; | ||||
|  | ||||
| export default function PostList(props: { onError: (message: string | null) => void }) { | ||||
| export default function PostList(props: { | ||||
|   info: { data: any[], count: number } | null, | ||||
|   onUpdate: (pn: number) => Promise<void>, | ||||
|   onError: (message: string | null) => void | ||||
| }) { | ||||
|   const [loading, setLoading] = createSignal(true); | ||||
|  | ||||
|   const [posts, setPosts] = createSignal<any[]>([]); | ||||
|   const [postCount, setPostCount] = createSignal(0); | ||||
|   const posts = createMemo(() => props.info?.data) | ||||
|   const postCount = createMemo<number>(() => props.info?.count ?? 0) | ||||
|  | ||||
|   const [page, setPage] = createSignal(1); | ||||
|   const pageCount = createMemo(() => Math.ceil(postCount() / 10)); | ||||
|  | ||||
|   async function readPosts() { | ||||
|     setLoading(true); | ||||
|     const res = await fetch("/api/posts?" + new URLSearchParams({ | ||||
|       take: (10).toString(), | ||||
|       offset: ((page() - 1) * 10).toString() | ||||
|     })); | ||||
|     if (res.status !== 200) { | ||||
|       props.onError(await res.text()); | ||||
|     } else { | ||||
|       const data = await res.json(); | ||||
|       setPosts(data["data"]); | ||||
|       setPostCount(data["count"]); | ||||
|       props.onError(null); | ||||
|     } | ||||
|     await props.onUpdate(page()); | ||||
|     setLoading(false); | ||||
|   } | ||||
|  | ||||
| @@ -42,8 +33,6 @@ export default function PostList(props: { onError: (message: string | null) => v | ||||
|  | ||||
|   return ( | ||||
|     <div id="post-list"> | ||||
|       <PostPublish onPost={() => readPosts()} onError={props.onError} /> | ||||
|  | ||||
|       <div id="posts"> | ||||
|         <For each={posts()}> | ||||
|           {item => <PostItem post={item} onReact={() => readPosts()} onError={props.onError} />} | ||||
|   | ||||
| @@ -20,7 +20,10 @@ render(() => ( | ||||
|   <WellKnownProvider> | ||||
|     <UserinfoProvider> | ||||
|       <Router root={RootLayout}> | ||||
|         <Route path="/" component={lazy(() => import("./pages/feed.tsx"))} /> | ||||
|         <Route path="/" component={lazy(() => import("./pages/feed.tsx"))}> | ||||
|           <Route path="/" component={lazy(() => import("./pages/global.tsx"))} /> | ||||
|           <Route path="/accounts/:accountId" component={lazy(() => import("./pages/account.tsx"))} /> | ||||
|         </Route> | ||||
|         <Route path="/auth" component={lazy(() => import("./pages/auth/callout.tsx"))} /> | ||||
|         <Route path="/auth/callback" component={lazy(() => import("./pages/auth/callback.tsx"))} /> | ||||
|       </Router> | ||||
|   | ||||
							
								
								
									
										50
									
								
								pkg/view/src/pages/account.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								pkg/view/src/pages/account.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | ||||
| import { createSignal, Show } from "solid-js"; | ||||
| import { useParams } from "@solidjs/router"; | ||||
|  | ||||
| import PostList from "../components/PostList.tsx"; | ||||
| import NameCard from "../components/NameCard.tsx"; | ||||
|  | ||||
| export default function DashboardPage() { | ||||
|   const [error, setError] = createSignal<string | null>(null); | ||||
|  | ||||
|   const [page, setPage] = createSignal(0); | ||||
|   const [info, setInfo] = createSignal<any>(null); | ||||
|  | ||||
|   const params = useParams(); | ||||
|  | ||||
|   async function readPosts(pn?: number) { | ||||
|     if (pn) setPage(pn); | ||||
|     const res = await fetch("/api/posts?" + new URLSearchParams({ | ||||
|       take: (10).toString(), | ||||
|       offset: ((page() - 1) * 10).toString(), | ||||
|       authorId: params["accountId"] | ||||
|     })); | ||||
|     if (res.status !== 200) { | ||||
|       setError(await res.text()); | ||||
|     } else { | ||||
|       setError(null); | ||||
|       setInfo(await res.json()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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> | ||||
|  | ||||
|       <NameCard accountId={parseInt(params["accountId"])} onError={setError} /> | ||||
|  | ||||
|       <PostList info={info()} onUpdate={readPosts} onError={setError} /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
| @@ -1,33 +1,12 @@ | ||||
| import { createEffect, createSignal, For, Show } from "solid-js"; | ||||
|  | ||||
| import styles from "./feed.module.css"; | ||||
|  | ||||
| import PostList from "../components/PostList.tsx"; | ||||
|  | ||||
| export default function DashboardPage() { | ||||
|   const [error, setError] = createSignal<string | null>(null); | ||||
|  | ||||
| export default function DashboardPage(props: any) { | ||||
|   return ( | ||||
|     <div class={`${styles.wrapper} container mx-auto`}> | ||||
|       <div id="trending" class="card shadow-xl h-fit"></div> | ||||
|  | ||||
|       <div id="content" class="card shadow-xl"> | ||||
|  | ||||
|         <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> | ||||
|          | ||||
|         <PostList onError={setError} /> | ||||
|  | ||||
|         {props.children} | ||||
|       </div> | ||||
|  | ||||
|       <div id="well-known" class="card shadow-xl h-fit"></div> | ||||
|   | ||||
							
								
								
									
										46
									
								
								pkg/view/src/pages/global.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										46
									
								
								pkg/view/src/pages/global.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,46 @@ | ||||
| import { createSignal, Show } from "solid-js"; | ||||
|  | ||||
| import PostList from "../components/PostList.tsx"; | ||||
| import PostPublish from "../components/PostPublish.tsx"; | ||||
|  | ||||
| export default function DashboardPage() { | ||||
|   const [error, setError] = createSignal<string | null>(null); | ||||
|  | ||||
|   const [page, setPage] = createSignal(0); | ||||
|   const [info, setInfo] = createSignal<any>(null); | ||||
|  | ||||
|   async function readPosts(pn?: number) { | ||||
|     if (pn) setPage(pn); | ||||
|     const res = await fetch("/api/posts?" + new URLSearchParams({ | ||||
|       take: (10).toString(), | ||||
|       offset: ((page() - 1) * 10).toString() | ||||
|     })); | ||||
|     if (res.status !== 200) { | ||||
|       setError(await res.text()); | ||||
|     } else { | ||||
|       setError(null); | ||||
|       setInfo(await res.json()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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> | ||||
|  | ||||
|       <PostPublish onPost={() => readPosts()} onError={setError} /> | ||||
|  | ||||
|       <PostList info={info()} onUpdate={readPosts} onError={setError} /> | ||||
|     </> | ||||
|   ); | ||||
| } | ||||
		Reference in New Issue
	
	Block a user