| @@ -17,6 +17,7 @@ | |||||
| "camelcase-keys": "^9.1.3", | "camelcase-keys": "^9.1.3", | ||||
| "class-variance-authority": "^0.7.1", | "class-variance-authority": "^0.7.1", | ||||
| "clsx": "^2.1.1", | "clsx": "^2.1.1", | ||||
| "framer-motion": "^12.23.26", | |||||
| "humps": "^2.0.1", | "humps": "^2.0.1", | ||||
| "lucide-react": "^0.511.0", | "lucide-react": "^0.511.0", | ||||
| "markdown-it": "^14.1.0", | "markdown-it": "^14.1.0", | ||||
| @@ -3573,6 +3574,33 @@ | |||||
| "url": "https://github.com/sponsors/rawify" | "url": "https://github.com/sponsors/rawify" | ||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/framer-motion": { | |||||
| "version": "12.23.26", | |||||
| "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-12.23.26.tgz", | |||||
| "integrity": "sha512-cPcIhgR42xBn1Uj+PzOyheMtZ73H927+uWPDVhUMqxy8UHt6Okavb6xIz9J/phFUHUj0OncR6UvMfJTXoc/LKA==", | |||||
| "license": "MIT", | |||||
| "dependencies": { | |||||
| "motion-dom": "^12.23.23", | |||||
| "motion-utils": "^12.23.6", | |||||
| "tslib": "^2.4.0" | |||||
| }, | |||||
| "peerDependencies": { | |||||
| "@emotion/is-prop-valid": "*", | |||||
| "react": "^18.0.0 || ^19.0.0", | |||||
| "react-dom": "^18.0.0 || ^19.0.0" | |||||
| }, | |||||
| "peerDependenciesMeta": { | |||||
| "@emotion/is-prop-valid": { | |||||
| "optional": true | |||||
| }, | |||||
| "react": { | |||||
| "optional": true | |||||
| }, | |||||
| "react-dom": { | |||||
| "optional": true | |||||
| } | |||||
| } | |||||
| }, | |||||
| "node_modules/fsevents": { | "node_modules/fsevents": { | ||||
| "version": "2.3.3", | "version": "2.3.3", | ||||
| "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", | ||||
| @@ -5216,6 +5244,21 @@ | |||||
| "node": ">=16 || 14 >=14.17" | "node": ">=16 || 14 >=14.17" | ||||
| } | } | ||||
| }, | }, | ||||
| "node_modules/motion-dom": { | |||||
| "version": "12.23.23", | |||||
| "resolved": "https://registry.npmjs.org/motion-dom/-/motion-dom-12.23.23.tgz", | |||||
| "integrity": "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA==", | |||||
| "license": "MIT", | |||||
| "dependencies": { | |||||
| "motion-utils": "^12.23.6" | |||||
| } | |||||
| }, | |||||
| "node_modules/motion-utils": { | |||||
| "version": "12.23.6", | |||||
| "resolved": "https://registry.npmjs.org/motion-utils/-/motion-utils-12.23.6.tgz", | |||||
| "integrity": "sha512-eAWoPgr4eFEOFfg2WjIsMoqJTW6Z8MTUCgn/GZ3VRpClWBdnbjryiA3ZSNLyxCTmCQx4RmYX6jX1iWHbenUPNQ==", | |||||
| "license": "MIT" | |||||
| }, | |||||
| "node_modules/ms": { | "node_modules/ms": { | ||||
| "version": "2.1.3", | "version": "2.1.3", | ||||
| "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", | ||||
| @@ -19,6 +19,7 @@ | |||||
| "camelcase-keys": "^9.1.3", | "camelcase-keys": "^9.1.3", | ||||
| "class-variance-authority": "^0.7.1", | "class-variance-authority": "^0.7.1", | ||||
| "clsx": "^2.1.1", | "clsx": "^2.1.1", | ||||
| "framer-motion": "^12.23.26", | |||||
| "humps": "^2.0.1", | "humps": "^2.0.1", | ||||
| "lucide-react": "^0.511.0", | "lucide-react": "^0.511.0", | ||||
| "markdown-it": "^14.1.0", | "markdown-it": "^14.1.0", | ||||
| @@ -1,3 +1,4 @@ | |||||
| import { AnimatePresence, motion } from 'framer-motion' | |||||
| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||
| import TagLink from '@/components/TagLink' | import TagLink from '@/components/TagLink' | ||||
| @@ -18,18 +19,23 @@ const renderTagTree = ( | |||||
| tag: Tag, | tag: Tag, | ||||
| nestLevel: number, | nestLevel: number, | ||||
| path: string, | path: string, | ||||
| ): ReactNode[] => { | |||||
| ): ReactNode[] => { | |||||
| const key = `${ path }-${ tag.id }` | const key = `${ path }-${ tag.id }` | ||||
| const self = ( | const self = ( | ||||
| <li key={key} className="mb-1"> | |||||
| <motion.li | |||||
| key={key} | |||||
| layout | |||||
| transition={{ duration: .2, ease: 'easeOut' }} | |||||
| className="mb-1"> | |||||
| <TagLink tag={tag} nestLevel={nestLevel}/> | <TagLink tag={tag} nestLevel={nestLevel}/> | ||||
| </li>) | |||||
| </motion.li>) | |||||
| return [self, | return [self, | ||||
| ...(tag.children | |||||
| ?.sort ((a, b) => a.name < b.name ? -1 : 1) | |||||
| .flatMap (child => renderTagTree (child, nestLevel + 1, key)) ?? [])] | |||||
| ...((tag.children | |||||
| ?.sort ((a, b) => a.name < b.name ? -1 : 1) | |||||
| .flatMap (child => renderTagTree (child, nestLevel + 1, key))) | |||||
| ?? [])] | |||||
| } | } | ||||
| @@ -70,55 +76,60 @@ export default (({ post }: Props) => { | |||||
| return ( | return ( | ||||
| <SidebarComponent> | <SidebarComponent> | ||||
| <TagSearch/> | <TagSearch/> | ||||
| {CATEGORIES.map ((cat: Category) => cat in tags && ( | |||||
| <div className="my-3" key={cat}> | |||||
| <SubsectionTitle>{categoryNames[cat]}</SubsectionTitle> | |||||
| <ul> | |||||
| {tags[cat].map (tag => renderTagTree (tag, 0, `cat-${ cat }`))} | |||||
| </ul> | |||||
| </div>))} | |||||
| {post && ( | |||||
| <div> | |||||
| <SectionTitle>情報</SectionTitle> | |||||
| <ul> | |||||
| <li>Id.: {post.id}</li> | |||||
| {/* TODO: uploadedUser の取得を対応したらコメント外す */} | |||||
| {/* | |||||
| <li> | |||||
| <>耕作者: </> | |||||
| {post.uploadedUser | |||||
| ? ( | |||||
| <Link to={`/users/${ post.uploadedUser.id }`}> | |||||
| {post.uploadedUser.name || '名もなきニジラー'} | |||||
| </Link>) | |||||
| : 'bot操作'} | |||||
| </li> | |||||
| */} | |||||
| <li>耕作日時: {(new Date (post.createdAt)).toLocaleString ()}</li> | |||||
| <li> | |||||
| <>リンク: </> | |||||
| <a | |||||
| className="break-all" | |||||
| href={post.url} | |||||
| target="_blank" | |||||
| rel="noopener noreferrer nofollow"> | |||||
| {post.url} | |||||
| </a> | |||||
| </li> | |||||
| <li> | |||||
| {/* TODO: 表示形式きしょすぎるので何とかする */} | |||||
| <>オリジナルの投稿日時: </> | |||||
| {!(post.originalCreatedFrom) && !(post.originalCreatedBefore) | |||||
| ? '不明' | |||||
| : ( | |||||
| <> | |||||
| {post.originalCreatedFrom | |||||
| && `${ (new Date (post.originalCreatedFrom)).toLocaleString () } 以降 `} | |||||
| {post.originalCreatedBefore | |||||
| && `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`} | |||||
| </>)} | |||||
| </li> | |||||
| </ul> | |||||
| </div>)} | |||||
| <motion.div layout> | |||||
| {CATEGORIES.map ((cat: Category) => cat in tags && ( | |||||
| <motion.div layout className="my-3" key={cat}> | |||||
| <SubsectionTitle>{categoryNames[cat]}</SubsectionTitle> | |||||
| <motion.ul layout> | |||||
| <AnimatePresence initial={false}> | |||||
| {tags[cat].map (tag => renderTagTree (tag, 0, `cat-${ cat }`))} | |||||
| </AnimatePresence> | |||||
| </motion.ul> | |||||
| </motion.div>))} | |||||
| {post && ( | |||||
| <div> | |||||
| <SectionTitle>情報</SectionTitle> | |||||
| <ul> | |||||
| <li>Id.: {post.id}</li> | |||||
| {/* TODO: uploadedUser の取得を対応したらコメント外す */} | |||||
| {/* | |||||
| <li> | |||||
| <>耕作者: </> | |||||
| {post.uploadedUser | |||||
| ? ( | |||||
| <Link to={`/users/${ post.uploadedUser.id }`}> | |||||
| {post.uploadedUser.name || '名もなきニジラー'} | |||||
| </Link>) | |||||
| : 'bot操作'} | |||||
| </li> | |||||
| */} | |||||
| <li>耕作日時: {(new Date (post.createdAt)).toLocaleString ()}</li> | |||||
| <li> | |||||
| <>リンク: </> | |||||
| <a | |||||
| className="break-all" | |||||
| href={post.url} | |||||
| target="_blank" | |||||
| rel="noopener noreferrer nofollow"> | |||||
| {post.url} | |||||
| </a> | |||||
| </li> | |||||
| <li> | |||||
| {/* TODO: 表示形式きしょすぎるので何とかする */} | |||||
| <>オリジナルの投稿日時: </> | |||||
| {!(post.originalCreatedFrom) && !(post.originalCreatedBefore) | |||||
| ? '不明' | |||||
| : ( | |||||
| <> | |||||
| {post.originalCreatedFrom | |||||
| && `${ (new Date (post.originalCreatedFrom)).toLocaleString () } 以降 `} | |||||
| {post.originalCreatedBefore | |||||
| && `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`} | |||||
| </>)} | |||||
| </li> | |||||
| </ul> | |||||
| </div>)} | |||||
| </motion.div> | |||||
| </SidebarComponent>) | </SidebarComponent>) | ||||
| }) satisfies FC<Props> | }) satisfies FC<Props> | ||||
| @@ -1,4 +1,5 @@ | |||||
| import axios from 'axios' | import axios from 'axios' | ||||
| import { AnimatePresence, motion } from 'framer-motion' | |||||
| import { useEffect, useState } from 'react' | import { useEffect, useState } from 'react' | ||||
| import { useLocation, useNavigate } from 'react-router-dom' | import { useLocation, useNavigate } from 'react-router-dom' | ||||
| @@ -8,7 +9,6 @@ import SectionTitle from '@/components/common/SectionTitle' | |||||
| import SidebarComponent from '@/components/layout/SidebarComponent' | import SidebarComponent from '@/components/layout/SidebarComponent' | ||||
| import { API_BASE_URL } from '@/config' | import { API_BASE_URL } from '@/config' | ||||
| import { CATEGORIES } from '@/consts' | import { CATEGORIES } from '@/consts' | ||||
| import { cn } from '@/lib/utils' | |||||
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| @@ -61,37 +61,52 @@ export default (({ posts }: Props) => { | |||||
| return ( | return ( | ||||
| <SidebarComponent> | <SidebarComponent> | ||||
| <TagSearch/> | <TagSearch/> | ||||
| <div className={cn (!(tagsVsbl) && 'hidden', 'md:block mt-4')}> | |||||
| <SectionTitle>タグ</SectionTitle> | |||||
| <ul> | |||||
| {CATEGORIES.flatMap (cat => cat in tags ? ( | |||||
| tags[cat].map (tag => ( | |||||
| <li key={tag.id} className="mb-1"> | |||||
| <TagLink tag={tag}/> | |||||
| </li>))) : [])} | |||||
| </ul> | |||||
| <SectionTitle>関聯</SectionTitle> | |||||
| {posts.length > 0 && ( | |||||
| <a href="#" | |||||
| onClick={ev => { | |||||
| ev.preventDefault () | |||||
| void ((async () => { | |||||
| try | |||||
| { | |||||
| const { data } = await axios.get (`${ API_BASE_URL }/posts/random`, | |||||
| { params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '), | |||||
| match: (anyFlg ? 'any' : 'all') } }) | |||||
| navigate (`/posts/${ (data as Post).id }`) | |||||
| } | |||||
| catch | |||||
| { | |||||
| ; | |||||
| } | |||||
| }) ()) | |||||
| }}> | |||||
| ランダム | |||||
| </a>)} | |||||
| </div> | |||||
| <AnimatePresence initial={false}> | |||||
| {tagsVsbl && ( | |||||
| <motion.div | |||||
| key="sptags" | |||||
| className="md:block mt-4" | |||||
| variants={{ hidden: { clipPath: 'inset(0 0 100% 0)', | |||||
| height: 0 }, | |||||
| visible: { clipPath: 'inset(0 0 0% 0)', | |||||
| height: 'auto'} }} | |||||
| initial="hidden" | |||||
| animate="visible" | |||||
| exit="hidden" | |||||
| transition={{ duration: .2, ease: 'easeOut' }}> | |||||
| <SectionTitle>タグ</SectionTitle> | |||||
| <ul> | |||||
| {CATEGORIES.flatMap (cat => cat in tags ? ( | |||||
| tags[cat].map (tag => ( | |||||
| <li key={tag.id} className="mb-1"> | |||||
| <TagLink tag={tag}/> | |||||
| </li>))) : [])} | |||||
| </ul> | |||||
| <SectionTitle>関聯</SectionTitle> | |||||
| {posts.length > 0 && ( | |||||
| <a href="#" | |||||
| onClick={ev => { | |||||
| ev.preventDefault () | |||||
| void ((async () => { | |||||
| try | |||||
| { | |||||
| const { data } = await axios.get (`${ API_BASE_URL }/posts/random`, | |||||
| { params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '), | |||||
| match: (anyFlg ? 'any' : 'all') } }) | |||||
| navigate (`/posts/${ (data as Post).id }`) | |||||
| } | |||||
| catch | |||||
| { | |||||
| ; | |||||
| } | |||||
| }) ()) | |||||
| }}> | |||||
| ランダム | |||||
| </a>)} | |||||
| </motion.div>)} | |||||
| </AnimatePresence> | |||||
| <a href="#" | <a href="#" | ||||
| className="md:hidden block my-2 text-center text-sm | className="md:hidden block my-2 text-center text-sm | ||||
| text-gray-500 hover:text-gray-400 | text-gray-500 hover:text-gray-400 | ||||
| @@ -1,5 +1,6 @@ | |||||
| import axios from 'axios' | import axios from 'axios' | ||||
| import toCamel from 'camelcase-keys' | import toCamel from 'camelcase-keys' | ||||
| import { AnimatePresence, motion } from 'framer-motion' | |||||
| import { Fragment, useState, useEffect } from 'react' | import { Fragment, useState, useEffect } from 'react' | ||||
| import { Link, useLocation } from 'react-router-dom' | import { Link, useLocation } from 'react-router-dom' | ||||
| @@ -136,37 +137,50 @@ export default (({ user }: Props) => { | |||||
| </Link>))} | </Link>))} | ||||
| </div> | </div> | ||||
| <div className={cn (menuOpen ? 'flex flex-col md:hidden' : 'hidden', | |||||
| 'bg-yellow-200 dark:bg-red-975 items-start')}> | |||||
| <Separator/> | |||||
| {menu.map ((item, i) => ( | |||||
| <Fragment key={i}> | |||||
| <Link to={i === openItemIdx ? item.to : '#'} | |||||
| className={cn ('w-full min-h-[40px] flex items-center pl-8', | |||||
| ((i === openItemIdx) | |||||
| && 'font-bold bg-yellow-50 dark:bg-red-950'))} | |||||
| onClick={ev => { | |||||
| if (i !== openItemIdx) | |||||
| { | |||||
| ev.preventDefault () | |||||
| setOpenItemIdx (i) | |||||
| } | |||||
| }}> | |||||
| {item.name} | |||||
| </Link> | |||||
| {i === openItemIdx && ( | |||||
| item.subMenu | |||||
| .filter (subItem => subItem.visible ?? true) | |||||
| .map ((subItem, j) => 'component' in subItem ? subItem.component : ( | |||||
| <Link key={j} | |||||
| to={subItem.to} | |||||
| className="w-full min-h-[36px] flex items-center pl-12 | |||||
| bg-yellow-50 dark:bg-red-950"> | |||||
| {subItem.name} | |||||
| </Link>)))} | |||||
| </Fragment>))} | |||||
| <TopNavUser user={user} sp/> | |||||
| <Separator/> | |||||
| </div> | |||||
| <AnimatePresence initial={false}> | |||||
| {menuOpen && ( | |||||
| <motion.div | |||||
| key="spmenu" | |||||
| className={cn ('flex flex-col md:hidden', | |||||
| 'bg-yellow-200 dark:bg-red-975 items-start')} | |||||
| variants={{ closed: { clipPath: 'inset(0 0 100% 0)', | |||||
| height: 0 }, | |||||
| open: { clipPath: 'inset(0 0 0% 0)', | |||||
| height: 'auto' } }} | |||||
| initial="closed" | |||||
| animate="open" | |||||
| exit="closed" | |||||
| transition={{ duration: .2, ease: 'easeOut' }}> | |||||
| <Separator/> | |||||
| {menu.map ((item, i) => ( | |||||
| <Fragment key={i}> | |||||
| <Link to={i === openItemIdx ? item.to : '#'} | |||||
| className={cn ('w-full min-h-[40px] flex items-center pl-8', | |||||
| ((i === openItemIdx) | |||||
| && 'font-bold bg-yellow-50 dark:bg-red-950'))} | |||||
| onClick={ev => { | |||||
| if (i !== openItemIdx) | |||||
| { | |||||
| ev.preventDefault () | |||||
| setOpenItemIdx (i) | |||||
| } | |||||
| }}> | |||||
| {item.name} | |||||
| </Link> | |||||
| {i === openItemIdx && ( | |||||
| item.subMenu | |||||
| .filter (subItem => subItem.visible ?? true) | |||||
| .map ((subItem, j) => 'component' in subItem ? subItem.component : ( | |||||
| <Link key={j} | |||||
| to={subItem.to} | |||||
| className="w-full min-h-[36px] flex items-center pl-12 | |||||
| bg-yellow-50 dark:bg-red-950"> | |||||
| {subItem.name} | |||||
| </Link>)))} | |||||
| </Fragment>))} | |||||
| <TopNavUser user={user} sp/> | |||||
| <Separator/> | |||||
| </motion.div>)} | |||||
| </AnimatePresence> | |||||
| </>) | </>) | ||||
| }) satisfies FC<Props> | }) satisfies FC<Props> | ||||