dh_demo

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

commit 1ab3b5257016688a0ae46d0d1f4a434dda8763c3
parent 3d70e10ff3815e60e5f336da1aee80456b560f61
Author: Yongbin Kim <iam@yongbin.kim>
Date:   Sun, 29 Jan 2023 04:03:06 +0900

feat: 헤더 브랜딩에 현재 위키 이름 표시하도록 수정

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

Diffstat:
Mcomponents/layout/Navbar.module.css | 2+-
Mcomponents/layout/Navbar.module.css.map | 4++--
Mcomponents/layout/Navbar.module.scss | 14++++++++++++++
Mcomponents/layout/Navbar.tsx | 41+++++++++++++++++++++++++++++++++++++++--
Mlib/utils/wiki.ts | 5++++-
Apages/api/wiki/[slug]/index.ts | 34++++++++++++++++++++++++++++++++++
6 files changed, 94 insertions(+), 6 deletions(-)

diff --git a/components/layout/Navbar.module.css b/components/layout/Navbar.module.css @@ -1 +1 @@ -.navbar{padding:.5rem 0;z-index:10;background-color:#fafdfa;color:#191c1b}.navbar .item:hover{background-color:rgba(25,28,27,.1)}.navbar.is-primary{background-color:#58fbda;color:#00201a}.navbar.is-primary .item:hover{background-color:rgba(0,32,26,.1)}.navbar.is-secondary{background-color:#f5d9ff;color:#30004a}.navbar.is-secondary .item:hover{background-color:rgba(48,0,74,.1)}.navbar.is-tertiary{background-color:#c7e7ff;color:#001e2e}.navbar.is-tertiary .item:hover{background-color:rgba(0,30,46,.1)}.navbar.is-error{background-color:#ffdad6;color:#410002}.navbar.is-error .item:hover{background-color:rgba(65,0,2,.1)}@media(prefers-color-scheme: dark){.navbar{background-color:#191c1b;color:#e1e3e0}.navbar .item:hover{background-color:rgba(225,227,224,.1)}.navbar.is-primary{background-color:#005143;color:#58fbda}.navbar.is-primary .item:hover{background-color:rgba(88,251,218,.1)}.navbar.is-secondary{background-color:#61317e;color:#f5d9ff}.navbar.is-secondary .item:hover{background-color:rgba(245,217,255,.1)}.navbar.is-tertiary{background-color:#2a4a5f;color:#c7e7ff}.navbar.is-tertiary .item:hover{background-color:rgba(199,231,255,.1)}.navbar.is-error{background-color:#93000a;color:#ffdad6}.navbar.is-error .item:hover{background-color:rgba(255,218,214,.1)}}.navbar>.container{display:flex;padding:0 1rem;align-items:stretch;min-height:2.5rem}.brand,.menu{display:flex;align-items:stretch}.brand{flex-shrink:0;line-height:1.5rem;font-size:1rem;letter-spacing:.009375rem;font-weight:500}.brand .navbar-item{font-weight:bold}.menu{flex:1;justify-content:flex-end}.menu.is-centered{justify-content:center;margin-right:0}.item{display:flex;align-items:center;padding:0 1rem;line-height:2.5rem;border-radius:9999px}.item[href]{color:inherit}.item[href],.item[href]:hover{text-decoration:none}.item.is-dropdown{position:relative;cursor:default}.item.is-dropdown .dropdown{position:absolute;top:100%;z-index:10;min-width:10rem;margin-top:.5rem;padding:.5rem 0;box-shadow:0 14px 28px rgba(0,0,0,.25),0 10px 10px rgba(0,0,0,.22);border-radius:.5rem;opacity:0;visibility:hidden;transform:translateY(-0.5rem);transition:transform 100ms ease-in-out,opacity 150ms,visibility 150ms;box-shadow:0px 4px 4px 0px rgba(0, 0, 0, 0.2),0px 8px 12px 6px rgba(0, 0, 0, 0.1);background-color:#fafdfa;color:#191c1b;background-color:#f1f6f2}.menu .item.is-dropdown .dropdown{right:0}.brand .item.is-dropdown .dropdown{left:0}@media(prefers-color-scheme: dark){.item.is-dropdown .dropdown{background-color:#191c1b;color:#e1e3e0;background-color:#242a28}}.item.is-dropdown .dropdown hr{margin:.5rem 0;border:0;border-top:1px solid rgba(63,73,70,.24)}@media(prefers-color-scheme: dark){.item.is-dropdown .dropdown hr{border-top:1px solid rgba(191,201,196,.24)}}.item.is-dropdown .dropdown .item{padding:0 1rem;line-height:2.5rem;border-radius:0}.item.is-dropdown:hover .dropdown{opacity:1;visibility:visible;transform:translateY(0)}.dropdown-label{display:flex;align-items:center;padding:.5rem 1rem;line-height:2.5rem;border-radius:9999px;cursor:default;line-height:1rem;font-size:.75rem;letter-spacing:.0416666667rem;font-weight:500}/*# sourceMappingURL=Navbar.module.css.map */ +.navbar{padding:.5rem 0;z-index:10;background-color:#fafdfa;color:#191c1b}.navbar .item:hover{background-color:rgba(25,28,27,.1)}.navbar.is-primary{background-color:#58fbda;color:#00201a}.navbar.is-primary .item:hover{background-color:rgba(0,32,26,.1)}.navbar.is-secondary{background-color:#f5d9ff;color:#30004a}.navbar.is-secondary .item:hover{background-color:rgba(48,0,74,.1)}.navbar.is-tertiary{background-color:#c7e7ff;color:#001e2e}.navbar.is-tertiary .item:hover{background-color:rgba(0,30,46,.1)}.navbar.is-error{background-color:#ffdad6;color:#410002}.navbar.is-error .item:hover{background-color:rgba(65,0,2,.1)}@media(prefers-color-scheme: dark){.navbar{background-color:#191c1b;color:#e1e3e0}.navbar .item:hover{background-color:rgba(225,227,224,.1)}.navbar.is-primary{background-color:#005143;color:#58fbda}.navbar.is-primary .item:hover{background-color:rgba(88,251,218,.1)}.navbar.is-secondary{background-color:#61317e;color:#f5d9ff}.navbar.is-secondary .item:hover{background-color:rgba(245,217,255,.1)}.navbar.is-tertiary{background-color:#2a4a5f;color:#c7e7ff}.navbar.is-tertiary .item:hover{background-color:rgba(199,231,255,.1)}.navbar.is-error{background-color:#93000a;color:#ffdad6}.navbar.is-error .item:hover{background-color:rgba(255,218,214,.1)}}.navbar>.container{display:flex;padding:0 1rem;align-items:stretch;min-height:2.5rem}.brand,.menu{display:flex;align-items:stretch}.brand{flex-shrink:0;line-height:1.5rem;font-size:1rem;letter-spacing:.009375rem;font-weight:500}.brand .navbar-item{font-weight:bold}.menu{flex:1;justify-content:flex-end}.menu.is-centered{justify-content:center;margin-right:0}.item{display:flex;align-items:center;padding:0 1rem;line-height:2.5rem;border-radius:9999px}.item[href]{color:inherit}.item[href],.item[href]:hover{text-decoration:none}.item.is-dropdown{position:relative;cursor:default}.item.is-dropdown .dropdown{position:absolute;top:100%;z-index:10;min-width:10rem;margin-top:.5rem;padding:.5rem 0;box-shadow:0 14px 28px rgba(0,0,0,.25),0 10px 10px rgba(0,0,0,.22);border-radius:.5rem;opacity:0;visibility:hidden;transform:translateY(-0.5rem);transition:transform 100ms ease-in-out,opacity 150ms,visibility 150ms;box-shadow:0px 4px 4px 0px rgba(0, 0, 0, 0.2),0px 8px 12px 6px rgba(0, 0, 0, 0.1);background-color:#fafdfa;color:#191c1b;background-color:#f1f6f2}.menu .item.is-dropdown .dropdown{right:0}.brand .item.is-dropdown .dropdown{left:0}@media(prefers-color-scheme: dark){.item.is-dropdown .dropdown{background-color:#191c1b;color:#e1e3e0;background-color:#242a28}}.item.is-dropdown .dropdown hr{margin:.5rem 0;border:0;border-top:1px solid rgba(63,73,70,.24)}@media(prefers-color-scheme: dark){.item.is-dropdown .dropdown hr{border-top:1px solid rgba(191,201,196,.24)}}.item.is-dropdown .dropdown .item{padding:0 1rem;line-height:2.5rem;border-radius:0}.item.is-dropdown:hover .dropdown{opacity:1;visibility:visible;transform:translateY(0)}.dropdown-label{display:flex;align-items:center;padding:.5rem 1rem;line-height:2.5rem;border-radius:9999px;cursor:default;line-height:1rem;font-size:.75rem;letter-spacing:.0416666667rem;font-weight:500}.branding-divider{width:1rem;line-height:2.5rem;text-align:center}.branding-divider::before{content:"|"}/*# sourceMappingURL=Navbar.module.css.map */ diff --git a/components/layout/Navbar.module.css.map b/components/layout/Navbar.module.css.map @@ -1 +1 @@ -{"version":3,"sourceRoot":"","sources":["Navbar.module.scss","../../styles/core/_colors.scss","../../styles/core/_typography.scss","../../styles/core/_elevate.scss"],"names":[],"mappings":"AA0BA,QACE,gBACA,WAVA,yBACA,cAEA,oBACE,mCAYE,mBAhBJ,yBACA,cAEA,+BACE,kCAYE,qBAhBJ,yBACA,cAEA,iCACE,kCAYE,oBAhBJ,yBACA,cAEA,gCACE,kCAYE,iBAhBJ,yBACA,cAEA,6BACE,iCC0GF,mCDtGF,QARE,yBACA,cAEA,oBACE,sCAYE,mBAhBJ,yBACA,cAEA,+BACE,qCAYE,qBAhBJ,yBACA,cAEA,iCACE,sCAYE,oBAhBJ,yBACA,cAEA,gCACE,sCAYE,iBAhBJ,yBACA,cAEA,6BACE,uCAkBF,mBACE,aACA,eACA,oBACA,WAnCU,OA2Cd,aAEE,aACA,oBAGF,OACE,cE2DE,mBACA,eACA,0BACA,gBF3DF,oBACE,iBAIJ,MACE,OACA,yBAEA,kBACE,uBACA,eAQJ,MACE,aACA,mBACA,QA1Ea,OA2Eb,YA5EY,OA6EZ,cA3EY,OA6EZ,YACE,cAEA,8BACE,qBASN,kBACE,kBACA,eAEA,4BACE,kBACA,SACA,WACA,UAhGa,MAiGb,iBACA,gBACA,WAzGK,wDA0GL,cAnGc,MAoGd,UACA,kBACA,8BACA,sEGvDF,kFHoEI,yBACA,cG/EJ,yBHmEE,kCACE,QAGF,mCACE,OCIJ,mCDvBA,4BAyBI,yBACA,cG/EJ,0BHoFE,+BACE,eACA,SAEE,wCCZN,mCDQE,+BAII,4CAIJ,kCACE,QAvIS,OAwIT,YAzIQ,OA0IR,gBASF,kCACE,UACA,mBACA,wBAKN,gBACE,aACA,mBACA,QAxJuB,WAyJvB,YA/JY,OAgKZ,cA9JY,OA+JZ,eEpDE,iBACA,iBACA,8BACA","file":"Navbar.module.css"} -\ No newline at end of file +{"version":3,"sourceRoot":"","sources":["Navbar.module.scss","../../styles/core/_colors.scss","../../styles/core/_typography.scss","../../styles/core/_elevate.scss","../../styles/core/_vars.scss"],"names":[],"mappings":"AA0BA,QACE,gBACA,WAVA,yBACA,cAEA,oBACE,mCAYE,mBAhBJ,yBACA,cAEA,+BACE,kCAYE,qBAhBJ,yBACA,cAEA,iCACE,kCAYE,oBAhBJ,yBACA,cAEA,gCACE,kCAYE,iBAhBJ,yBACA,cAEA,6BACE,iCC0GF,mCDtGF,QARE,yBACA,cAEA,oBACE,sCAYE,mBAhBJ,yBACA,cAEA,+BACE,qCAYE,qBAhBJ,yBACA,cAEA,iCACE,sCAYE,oBAhBJ,yBACA,cAEA,gCACE,sCAYE,iBAhBJ,yBACA,cAEA,6BACE,uCAkBF,mBACE,aACA,eACA,oBACA,WAnCU,OA2Cd,aAEE,aACA,oBAGF,OACE,cE2DE,mBACA,eACA,0BACA,gBF3DF,oBACE,iBAIJ,MACE,OACA,yBAEA,kBACE,uBACA,eAQJ,MACE,aACA,mBACA,QA1Ea,OA2Eb,YA5EY,OA6EZ,cA3EY,OA6EZ,YACE,cAEA,8BACE,qBASN,kBACE,kBACA,eAEA,4BACE,kBACA,SACA,WACA,UAhGa,MAiGb,iBACA,gBACA,WAzGK,wDA0GL,cAnGc,MAoGd,UACA,kBACA,8BACA,sEGvDF,kFHoEI,yBACA,cG/EJ,yBHmEE,kCACE,QAGF,mCACE,OCIJ,mCDvBA,4BAyBI,yBACA,cG/EJ,0BHoFE,+BACE,eACA,SAEE,wCCZN,mCDQE,+BAII,4CAIJ,kCACE,QAvIS,OAwIT,YAzIQ,OA0IR,gBASF,kCACE,UACA,mBACA,wBAKN,gBACE,aACA,mBACA,QAxJuB,WAyJvB,YA/JY,OAgKZ,cA9JY,OA+JZ,eEpDE,iBACA,iBACA,8BACA,gBF0DJ,kBACE,MIpLI,KJqLJ,YA5KY,OA6KZ,kBAEA,0BACE","file":"Navbar.module.css"} +\ No newline at end of file diff --git a/components/layout/Navbar.module.scss b/components/layout/Navbar.module.scss @@ -172,3 +172,17 @@ $dropdown-label-padding: 0.5rem vars.$gap; @include typography.apply('label-medium'); } + +// ---------------------------------------- +// Branding Divider +// ---------------------------------------- + +.branding-divider { + width: vars.$gap; + line-height: $item-height; + text-align: center; + + &::before { + content: '|' + } +} diff --git a/components/layout/Navbar.tsx b/components/layout/Navbar.tsx @@ -1,7 +1,10 @@ import Logo from '@/components/elements/Logo' import Container from '@/components/layout/Container' +import { WikiInfo } from '@/lib/models/wiki_info' +import { getSlugAndPath } from '@/lib/utils/wiki' import Link from 'next/link' -import { ReactNode } from 'react' +import { useRouter } from 'next/router' +import { ReactNode, useEffect, useMemo, useState } from 'react' import styles from './Navbar.module.css' interface NavbarProps { @@ -9,14 +12,48 @@ interface NavbarProps { } export default function Navbar (props: NavbarProps) { + const router = useRouter() + const [wikiName, setWikiName] = useState<string | null>(null) + + const [slug] = getSlugAndPath(router) + + useEffect(() => { + if (slug == null) { + setWikiName(null) + return + } + + fetch(`/api/wiki/${slug}`, { + method: 'GET' + }).then<{ wiki: WikiInfo }, null>((res) => { + return res.json() + }, (err) => { + console.error('failed to get current wiki information', err) + return null + }).then((data) => { + if (data == null) { + return + } + + setWikiName(data.wiki.title) + }) + }, [slug]) + return ( <div className={styles.navbar}> <Container className={styles.container}> <nav className={styles.brand}> - {/* TODO: Show current wiki name here */} <Link href="/" className={styles.item}> <Logo /> </Link> + {slug != null && ( + <> + <div className={styles['branding-divider']}/> + <Link href={`/wiki/${slug}`} className={styles.item}> + {wikiName} + </Link> + </> + )} </nav> <nav className={styles.menu}> {props.children} diff --git a/lib/utils/wiki.ts b/lib/utils/wiki.ts @@ -7,9 +7,12 @@ export const getSlugAndPath = ( context: GetServerSidePropsContext | NextApiRequest | NextRouter, ): [slug: string | null, path: string | null] => { const { slug, path } = context.query - if (typeof slug !== 'string' || path == null || !Array.isArray(path)) { + if (typeof slug !== 'string') { return [null, null] } + if (path == null || !Array.isArray(path)) { + return [slug, null] + } return [slug, path.join('')] } diff --git a/pages/api/wiki/[slug]/index.ts b/pages/api/wiki/[slug]/index.ts @@ -0,0 +1,34 @@ +import { ApiError, ERR_NOT_FOUND } from '@/lib/apierror' +import { getWiki, getWikiViaSlug, WikiInfo } from '@/lib/models/wiki_info' +import { ACL_ACTION_READ, resolveACL } from '@/lib/security/acl' +import { authenticationFromCookies } from '@/lib/security/token' +import { getSlugAndPath } from '@/lib/utils/wiki' +import { NextApiRequest, NextApiResponse } from 'next' + +export default async function handler ( + req: NextApiRequest, + res: NextApiResponse<ApiError | { wiki: WikiInfo }>, +) { + const [slug] = getSlugAndPath(req) + if (slug == null) { + res.status(404).json(ERR_NOT_FOUND) + return + } + + const token = authenticationFromCookies(req.cookies) + + const wiki = await getWikiViaSlug([slug]) + if (wiki == null) { + res.status(404).json(ERR_NOT_FOUND) + return + } + + if (!resolveACL(token, wiki.acl, ACL_ACTION_READ)) { + res.status(404).json(ERR_NOT_FOUND) + return + } + + res.status(200).json({ + wiki: wiki, + }) +}