dh_demo

DreamHanks demo project
git clone git://git.lair.cx/dh_demo
Log | Files | Refs | README

commit 4f727a4e81dba277bad41be200ee47051a510e4d
parent 627368401c9d0ec4c0cec035311a1e936424b8cd
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Fri, 27 Jan 2023 09:36:32 +0900

feat: Thread List에 Pagination 추가

Signed-off-by: Yongbin Kim <iam@yongbin.kim>

Diffstat:
Mcomponents/threads/ThreadList.module.css | 2+-
Mcomponents/threads/ThreadList.module.css.map | 4++--
Mcomponents/threads/ThreadList.module.scss | 1+
Mlib/models/wiki_talk.ts | 17+++++++++++++++++
Mpages/talk/[slug]/[...path].tsx | 45++++++++++++++++++++++++++++++++++++++-------
5 files changed, 59 insertions(+), 10 deletions(-)

diff --git a/components/threads/ThreadList.module.css b/components/threads/ThreadList.module.css @@ -1 +1 @@ -.threads{display:flex;flex-direction:column;width:100%;gap:1rem}.thread{display:flex;flex-direction:column;align-items:flex-start;width:100%;padding:.5rem;cursor:pointer;user-select:none;border:1px solid #6f7975;background:#fafdfa;border-radius:.5rem}.thread a{color:inherit;font-weight:bold}.thread:hover{background-color:#d8dbd9;box-shadow:0px 1px 2px 0px rgba(0, 0, 0, 0.2),0px 2px 6px 2px rgba(0, 0, 0, 0.1)}@media(prefers-color-scheme: dark){.thread:hover{background-color:#373a39;box-shadow:0px 1px 2px 0px rgba(0, 0, 0, 0.2),0px 2px 6px 2px rgba(0, 0, 0, 0.1)}}@media(prefers-color-scheme: dark){.thread{border:1px solid #89938f;background:#191c1b;border-radius:.5rem}}.thread .title{display:block;padding:.5rem;line-height:1.75rem;font-size:1.375rem;letter-spacing:0rem;font-weight:400}.thread .meta{padding:.25rem .5rem;line-height:1rem;font-size:.75rem;letter-spacing:.0333333333rem;font-weight:400}.thread .meta a:hover{text-decoration:underline}.thread .preview{padding:.25rem .5rem;line-height:1.25rem;font-size:.875rem;letter-spacing:.0178571429rem;font-weight:400}/*# sourceMappingURL=ThreadList.module.css.map */ +.threads{display:flex;flex-direction:column;width:100%;gap:1rem;margin-bottom:1rem}.thread{display:flex;flex-direction:column;align-items:flex-start;width:100%;padding:.5rem;cursor:pointer;user-select:none;border:1px solid #6f7975;background:#fafdfa;border-radius:.5rem}.thread a{color:inherit;font-weight:bold}.thread:hover{background-color:#d8dbd9;box-shadow:0px 1px 2px 0px rgba(0, 0, 0, 0.2),0px 2px 6px 2px rgba(0, 0, 0, 0.1)}@media(prefers-color-scheme: dark){.thread:hover{background-color:#373a39;box-shadow:0px 1px 2px 0px rgba(0, 0, 0, 0.2),0px 2px 6px 2px rgba(0, 0, 0, 0.1)}}@media(prefers-color-scheme: dark){.thread{border:1px solid #89938f;background:#191c1b;border-radius:.5rem}}.thread .title{display:block;padding:.5rem;line-height:1.75rem;font-size:1.375rem;letter-spacing:0rem;font-weight:400}.thread .meta{padding:.25rem .5rem;line-height:1rem;font-size:.75rem;letter-spacing:.0333333333rem;font-weight:400}.thread .meta a:hover{text-decoration:underline}.thread .preview{padding:.25rem .5rem;line-height:1.25rem;font-size:.875rem;letter-spacing:.0178571429rem;font-weight:400}/*# sourceMappingURL=ThreadList.module.css.map */ diff --git a/components/threads/ThreadList.module.css.map b/components/threads/ThreadList.module.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["ThreadList.module.scss","../../styles/core/_vars.scss","../../styles/core/_elevate.scss","../../styles/core/_colors.scss","../../styles/core/_typography.scss"],"names":[],"mappings":"AAKA,SACE,aACA,sBACA,WACA,ICTI,KDYN,QACE,aACA,sBACA,uBACA,WACA,cACA,eACA,iBAeE,yBACA,mBACA,oBAfF,UACE,cACA,iBAGF,cE0BA,yBAUA,iFCkEA,mCHtGA,cE0BA,yBAUA,kFCkEA,mCHpHF,QAsBI,yBACA,mBACA,qBAGF,eACE,cACA,cI6EA,oBACA,mBACA,oBACA,gBJ5EF,cACE,qBIwEA,iBACA,iBACA,8BACA,gBJxEA,sBACE,0BAIJ,iBACE,qBI+DA,oBACA,kBACA,8BACA","file":"ThreadList.module.css"} -\ No newline at end of file +{"version":3,"sourceRoot":"","sources":["ThreadList.module.scss","../../styles/core/_vars.scss","../../styles/core/_elevate.scss","../../styles/core/_colors.scss","../../styles/core/_typography.scss"],"names":[],"mappings":"AAKA,SACE,aACA,sBACA,WACA,ICTI,KDUJ,cCVI,KDaN,QACE,aACA,sBACA,uBACA,WACA,cACA,eACA,iBAeE,yBACA,mBACA,oBAfF,UACE,cACA,iBAGF,cEyBA,yBAUA,iFCkEA,mCHrGA,cEyBA,yBAUA,kFCkEA,mCHnHF,QAsBI,yBACA,mBACA,qBAGF,eACE,cACA,cI4EA,oBACA,mBACA,oBACA,gBJ3EF,cACE,qBIuEA,iBACA,iBACA,8BACA,gBJvEA,sBACE,0BAIJ,iBACE,qBI8DA,oBACA,kBACA,8BACA","file":"ThreadList.module.css"} +\ No newline at end of file diff --git a/components/threads/ThreadList.module.scss b/components/threads/ThreadList.module.scss @@ -8,6 +8,7 @@ flex-direction: column; width: 100%; gap: vars.$gap; + margin-bottom: vars.$gap; } .thread { diff --git a/lib/models/wiki_talk.ts b/lib/models/wiki_talk.ts @@ -2,6 +2,23 @@ import { modelBehaviour } from '@/lib/model_helpers' import { createThread, Thread } from '@/lib/models/thread' import { OkPacket, RowDataPacket } from 'mysql2' +const SQL_COUNT_WIKI_TALKS = ` + select count(*) + from wiki_talks + where page_id = ? +` + +export const countWikiTalks = modelBehaviour< + [pageId: number], + number +>(async (conn, args) => { + const [rows] = await conn.query<RowDataPacket[]>({ + sql: SQL_COUNT_WIKI_TALKS, + }, args) + + return rows[0][0] +}) + const SQL_LIST_WIKI_TALKS = ` select t.id, t.author_id, diff --git a/pages/talk/[slug]/[...path].tsx b/pages/talk/[slug]/[...path].tsx @@ -7,22 +7,28 @@ import Container from '@/components/layout/Container' import Divider from '@/components/layout/Divider' import Hero from '@/components/layout/Hero' import Section from '@/components/layout/Section' +import { Pagination } from '@/components/Pagination' import ThreadList from '@/components/threads/ThreadList' import { ApiError } from '@/lib/apierror' +import { withConnection } from '@/lib/model_helpers' import { Thread } from '@/lib/models/thread' import { getWikiAndPageACLViaPath } from '@/lib/models/wiki_page' -import { listWikiTalks } from '@/lib/models/wiki_talk' +import { countWikiTalks, listWikiTalks } from '@/lib/models/wiki_talk' +import { parseIntOrDefault } from '@/lib/utils/number' import { CreateTalkResponse } from '@/pages/api/talks/[slug]/[...path]' import { GetServerSideProps } from 'next' -import Link from 'next/link' import { useRouter } from 'next/router' import { useState } from 'react' +const PAGE_SIZE = 10 + export interface TestPageProps { wikiSlug: string pageId: number pagePath: string talks: Thread[] + counts: number + page: number } export const getServerSideProps: GetServerSideProps<TestPageProps> = async (context) => { @@ -35,21 +41,39 @@ export const getServerSideProps: GetServerSideProps<TestPageProps> = async (cont const path = paths.join('/') - const pageACLInfo = await getWikiAndPageACLViaPath([slug, path]) - if (pageACLInfo === null) { + const page = Math.max(1, parseIntOrDefault(context.query.page, 1)) + + const result = await withConnection(async (conn): Promise<[ + aclInfo: Exclude<Awaited<ReturnType<typeof getWikiAndPageACLViaPath>>, null>, + talks: Awaited<ReturnType<typeof listWikiTalks>>, + counts: number, + ] | null> => { + const aclInfo = await getWikiAndPageACLViaPath(conn, [slug, path]) + if (aclInfo === null) { + return null + } + + const talks = await listWikiTalks(conn, [aclInfo.pageId, PAGE_SIZE, (page - 1) * PAGE_SIZE]) + const counts = await countWikiTalks(conn, [aclInfo.pageId]) + + return [aclInfo, talks, counts] + }) + if (result === null) { return { notFound: true, } } - const talks = await listWikiTalks([pageACLInfo.pageId, 10, 0]) + const [aclInfo, talks, counts] = result return { props: { wikiSlug: slug, - pageId: pageACLInfo.pageId, + pageId: aclInfo.pageId, pagePath: path, talks: talks, + counts: counts, + page: page, }, } } @@ -65,7 +89,14 @@ export default function TestPage (props: TestPageProps) { {props.talks.length === 0 ? ( <div>No talks</div> ) : ( - <ThreadList threads={props.talks} /> + <> + <ThreadList threads={props.talks}/> + <Pagination + page={props.page} + totalCount={props.counts} + pageSize={PAGE_SIZE} + /> + </> )} </Section>