✨ Pagination
This commit is contained in:
		
							
								
								
									
										99
									
								
								pkg/view/src/components/PostItem.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										99
									
								
								pkg/view/src/components/PostItem.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,99 @@ | ||||
| import { createSignal, Show } from "solid-js"; | ||||
| import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; | ||||
|  | ||||
| export default function PostItem(props: { post: any, onError: (message: string | null) => void, onReact: () => void }) { | ||||
|   const [reacting, setReacting] = createSignal(false); | ||||
|  | ||||
|   const userinfo = useUserinfo(); | ||||
|  | ||||
|   async function reactPost(item: any, type: string) { | ||||
|     setReacting(true); | ||||
|     const res = await fetch(`/api/posts/${item.id}/react/${type}`, { | ||||
|       method: "POST", | ||||
|       headers: { "Authorization": `Bearer ${getAtk()}` } | ||||
|     }); | ||||
|     if (res.status !== 201 && res.status !== 204) { | ||||
|       props.onError(await res.text()); | ||||
|     } else { | ||||
|       props.onReact(); | ||||
|       props.onError(null); | ||||
|     } | ||||
|     setReacting(false); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div class="post-item"> | ||||
|  | ||||
|       <div class="flex bg-base-200"> | ||||
|         <div class="avatar"> | ||||
|           <div class="w-12"> | ||||
|             <Show when={props.post.author.avatar} | ||||
|                   fallback={<span class="text-3xl">{props.post.author.name.substring(0, 1)}</span>}> | ||||
|               <img alt="avatar" src={props.post.author.avatar} /> | ||||
|             </Show> | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="flex items-center px-5"> | ||||
|           <div> | ||||
|             <h3 class="font-bold text-sm">{props.post.author.name}</h3> | ||||
|             <p class="text-xs">{props.post.author.description}</p> | ||||
|           </div> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <article class="py-5 px-7"> | ||||
|         <h2 class="card-title">{props.post.title}</h2> | ||||
|         <article class="prose">{props.post.content}</article> | ||||
|       </article> | ||||
|  | ||||
|       <div class="grid grid-cols-3 border-y border-base-200"> | ||||
|  | ||||
|         <div class="col-span-2 grid grid-cols-4"> | ||||
|           <div class="tooltip" data-tip="Daisuki"> | ||||
|             <button type="button" class="btn btn-ghost btn-block" disabled={reacting()} | ||||
|                     onClick={() => reactPost(props.post, "like")}> | ||||
|               <i class="fa-solid fa-thumbs-up"></i> | ||||
|               <code class="font-mono">{props.post.like_count}</code> | ||||
|             </button> | ||||
|           </div> | ||||
|  | ||||
|           <div class="tooltip" data-tip="Daikirai"> | ||||
|             <button type="button" class="btn btn-ghost btn-block" disabled={reacting()} | ||||
|                     onClick={() => reactPost(props.post, "dislike")}> | ||||
|               <i class="fa-solid fa-thumbs-down"></i> | ||||
|               <code class="font-mono">{props.post.dislike_count}</code> | ||||
|             </button> | ||||
|           </div> | ||||
|  | ||||
|           <div class="tooltip" data-tip="Reply"> | ||||
|             <button type="button" class="btn btn-ghost btn-block"> | ||||
|               <i class="fa-solid fa-reply"></i> | ||||
|             </button> | ||||
|           </div> | ||||
|  | ||||
|           <div class="tooltip" data-tip="Repost"> | ||||
|             <button type="button" class="btn btn-ghost btn-block"> | ||||
|               <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> | ||||
|             </div> | ||||
|             <ul tabIndex="0" class="dropdown-content z-[1] menu p-2 shadow bg-base-100 rounded-box w-52"> | ||||
|               <Show when={userinfo?.profiles?.id === props.post.author_id}> | ||||
|                 <li><a>Edit</a></li> | ||||
|               </Show> | ||||
|               <li><a>Report</a></li> | ||||
|             </ul> | ||||
|           </div> | ||||
|         </div> | ||||
|  | ||||
|       </div> | ||||
|  | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										3
									
								
								pkg/view/src/components/PostList.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								pkg/view/src/components/PostList.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | ||||
| .paginationControl { | ||||
|     background-color: transparent !important; | ||||
| } | ||||
							
								
								
									
										75
									
								
								pkg/view/src/components/PostList.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								pkg/view/src/components/PostList.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | ||||
| 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 }) { | ||||
|   const [loading, setLoading] = createSignal(true); | ||||
|  | ||||
|   const [posts, setPosts] = createSignal<any[]>([]); | ||||
|   const [postCount, setPostCount] = createSignal(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); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   } | ||||
|  | ||||
|   readPosts(); | ||||
|  | ||||
|   function changePage(pn: number) { | ||||
|     setPage(pn); | ||||
|     readPosts().then(() => { | ||||
|       setTimeout(() => window.scrollTo({ top: 0, behavior: "smooth" }), 16); | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   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} />} | ||||
|         </For> | ||||
|  | ||||
|         <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()}> | ||||
|           <div class="w-full border-b border-base-200 pt-5 pb-7 text-center"> | ||||
|             <p class="loading loading-lg loading-infinity"></p> | ||||
|             <p>Creating fake news...</p> | ||||
|           </div> | ||||
|         </Show> | ||||
|       </div> | ||||
|     </div> | ||||
|   ); | ||||
| } | ||||
							
								
								
									
										4
									
								
								pkg/view/src/components/PostPublish.module.css
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								pkg/view/src/components/PostPublish.module.css
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| .publishInput { | ||||
|     outline-style: none !important; | ||||
|     outline-width: 0 !important; | ||||
| } | ||||
							
								
								
									
										81
									
								
								pkg/view/src/components/PostPublish.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										81
									
								
								pkg/view/src/components/PostPublish.tsx
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,81 @@ | ||||
| import { createSignal, Show } from "solid-js"; | ||||
| import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; | ||||
|  | ||||
| import styles from "./PostPublish.module.css"; | ||||
|  | ||||
| export default function PostPublish(props: { | ||||
|   replying?: any, | ||||
|   reposting?: any, | ||||
|   onError: (message: string | null) => void, | ||||
|   onPost: () => void | ||||
| }) { | ||||
|   const userinfo = useUserinfo(); | ||||
|  | ||||
|   const [submitting, setSubmitting] = createSignal(false); | ||||
|  | ||||
|   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 | ||||
|       }) | ||||
|     }); | ||||
|     if (res.status !== 200) { | ||||
|       props.onError(await res.text()); | ||||
|     } else { | ||||
|       form.reset(); | ||||
|       props.onPost(); | ||||
|       props.onError(null); | ||||
|     } | ||||
|     setSubmitting(false); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <form id="publish" onSubmit={doPost}> | ||||
|       <div id="publish-identity" class="flex border-y border-base-200"> | ||||
|         <div class="avatar"> | ||||
|           <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" class={`${styles.publishInput} input w-full`} | ||||
|                  placeholder="The describe for a long content (Optional)" /> | ||||
|         </div> | ||||
|       </div> | ||||
|  | ||||
|       <textarea name="content" class={`${styles.publishInput} textarea w-full`} | ||||
|                 placeholder="What's happend?!" /> | ||||
|  | ||||
|       <div id="publish-actions" class="flex justify-between border-y border-base-200"> | ||||
|         <div> | ||||
|           <button type="button" class="btn btn-ghost"> | ||||
|             <i class="fa-solid fa-paperclip"></i> | ||||
|           </button> | ||||
|         </div> | ||||
|  | ||||
|         <button type="submit" class="btn btn-primary" disabled={submitting()}> | ||||
|           <Show when={submitting()} fallback={"Post a post"}> | ||||
|             <span class="loading"></span> | ||||
|           </Show> | ||||
|         </button> | ||||
|       </div> | ||||
|     </form> | ||||
|   ); | ||||
| } | ||||
| @@ -1,8 +1,3 @@ | ||||
| .publishInput { | ||||
|     outline-style: none !important; | ||||
|     outline-width: 0 !important; | ||||
| } | ||||
|  | ||||
| .wrapper { | ||||
|     display: grid; | ||||
|     grid-template-columns: 1fr; | ||||
|   | ||||
| @@ -1,84 +1,11 @@ | ||||
| import { getAtk, useUserinfo } from "../stores/userinfo.tsx"; | ||||
| 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 userinfo = useUserinfo(); | ||||
|  | ||||
|   const [error, setError] = createSignal<string | null>(null); | ||||
|   const [loading, setLoading] = createSignal(true); | ||||
|   const [submitting, setSubmitting] = createSignal(false); | ||||
|   const [reacting, setReacting] = createSignal(false); | ||||
|  | ||||
|   const [posts, setPosts] = createSignal<any[]>([]); | ||||
|   const [postCount, setPostCount] = createSignal(0); | ||||
|  | ||||
|   const [page, setPage] = createSignal(1); | ||||
|  | ||||
|   async function readPosts() { | ||||
|     setLoading(true); | ||||
|     const res = await fetch("/api/posts?" + new URLSearchParams({ | ||||
|       take: (10).toString(), | ||||
|       skip: ((page() - 1) * 10).toString() | ||||
|     })); | ||||
|     if (res.status !== 200) { | ||||
|       setError(await res.text()); | ||||
|     } else { | ||||
|       const data = await res.json(); | ||||
|       setPosts(data["data"]); | ||||
|       setPostCount(data["count"]); | ||||
|       setError(null); | ||||
|     } | ||||
|     setLoading(false); | ||||
|   } | ||||
|  | ||||
|   createEffect(() => readPosts(), [page()]); | ||||
|  | ||||
|   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 | ||||
|       }) | ||||
|     }); | ||||
|     if (res.status !== 200) { | ||||
|       setError(await res.text()); | ||||
|     } else { | ||||
|       await readPosts(); | ||||
|       form.reset(); | ||||
|       setError(null); | ||||
|     } | ||||
|     setSubmitting(false); | ||||
|   } | ||||
|  | ||||
|   async function reactPost(item: any, type: string) { | ||||
|     setReacting(true); | ||||
|     const res = await fetch(`/api/posts/${item.id}/react/${type}`, { | ||||
|       method: "POST", | ||||
|       headers: { "Authorization": `Bearer ${getAtk()}` } | ||||
|     }); | ||||
|     if (res.status !== 201 && res.status !== 204) { | ||||
|       setError(await res.text()); | ||||
|     } else { | ||||
|       await readPosts(); | ||||
|       setError(null); | ||||
|     } | ||||
|     setReacting(false); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     <div class={`${styles.wrapper} container mx-auto`}> | ||||
| @@ -98,107 +25,8 @@ export default function DashboardPage() { | ||||
|             </div> | ||||
|           </Show> | ||||
|         </div> | ||||
|  | ||||
|         <form id="publish" onSubmit={doPost}> | ||||
|           <div id="publish-identity" class="flex border-y border-base-200"> | ||||
|             <div class="avatar"> | ||||
|               <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" class={`${styles.publishInput} input w-full`} | ||||
|                      placeholder="The describe for a long content (Optional)" /> | ||||
|             </div> | ||||
|           </div> | ||||
|  | ||||
|           <textarea name="content" class={`${styles.publishInput} textarea w-full`} | ||||
|                     placeholder="What's happend?!" /> | ||||
|  | ||||
|           <div id="publish-actions" class="flex justify-between border-y border-base-200"> | ||||
|             <div> | ||||
|               <button type="button" class="btn btn-ghost"> | ||||
|                 <i class="fa-solid fa-paperclip"></i> | ||||
|               </button> | ||||
|             </div> | ||||
|  | ||||
|             <button type="submit" class="btn btn-primary" disabled={submitting()}> | ||||
|               <Show when={submitting()} fallback={"Post a post"}> | ||||
|                 <span class="loading"></span> | ||||
|               </Show> | ||||
|             </button> | ||||
|           </div> | ||||
|         </form> | ||||
|  | ||||
|         <div id="posts"> | ||||
|           <For each={posts()}> | ||||
|             {item => <div class="post-item"> | ||||
|  | ||||
|               <div class="flex bg-base-200"> | ||||
|               <div class="avatar"> | ||||
|                   <div class="w-12"> | ||||
|                     <Show when={item.author.avatar} | ||||
|                           fallback={<span class="text-3xl">{item.author.name.substring(0, 1)}</span>}> | ||||
|                       <img alt="avatar" src={item.author.avatar} /> | ||||
|                     </Show> | ||||
|                   </div> | ||||
|                 </div> | ||||
|                 <div class="flex items-center px-5"> | ||||
|                   <div> | ||||
|                     <h3 class="font-bold text-sm">{item.author.name}</h3> | ||||
|                     <p class="text-xs">{item.author.description}</p> | ||||
|                   </div> | ||||
|                 </div> | ||||
|               </div> | ||||
|  | ||||
|               <article class="py-5 px-7"> | ||||
|                 <h2 class="card-title">{item.title}</h2> | ||||
|                 <article class="prose">{item.content}</article> | ||||
|               </article> | ||||
|  | ||||
|               <div class="grid grid-cols-4 border-y border-base-200"> | ||||
|  | ||||
|                 <div class="tooltip" data-tip="Daisuki"> | ||||
|                   <button type="button" class="btn btn-ghost btn-block" disabled={reacting()} | ||||
|                           onClick={() => reactPost(item, "like")}> | ||||
|                     <i class="fa-solid fa-thumbs-up"></i> | ||||
|                     <code class="font-mono">{item.like_count}</code> | ||||
|                   </button> | ||||
|                 </div> | ||||
|  | ||||
|                 <div class="tooltip" data-tip="Daikirai"> | ||||
|                   <button type="button" class="btn btn-ghost btn-block" disabled={reacting()} | ||||
|                           onClick={() => reactPost(item, "dislike")}> | ||||
|                     <i class="fa-solid fa-thumbs-down"></i> | ||||
|                     <code class="font-mono">{item.dislike_count}</code> | ||||
|                   </button> | ||||
|                 </div> | ||||
|  | ||||
|                 <button type="button" class="btn btn-ghost"> | ||||
|                   <i class="fa-solid fa-reply"></i> | ||||
|                   <span>Reply</span> | ||||
|                 </button> | ||||
|  | ||||
|                 <button type="button" class="btn btn-ghost"> | ||||
|                   <i class="fa-solid fa-retweet"></i> | ||||
|                   <span>Forward</span> | ||||
|                 </button> | ||||
|  | ||||
|               </div> | ||||
|  | ||||
|             </div>} | ||||
|           </For> | ||||
|  | ||||
|           <Show when={loading()}> | ||||
|             <div class="w-full border-b border-base-200 pt-5 pb-7 text-center"> | ||||
|               <p class="loading loading-lg loading-infinity"></p> | ||||
|               <p>Creating fake news...</p> | ||||
|             </div> | ||||
|           </Show> | ||||
|         </div> | ||||
|          | ||||
|         <PostList onError={setError} /> | ||||
|  | ||||
|       </div> | ||||
|  | ||||
|   | ||||
		Reference in New Issue
	
	Block a user