feat: アニメーションの一部実装(#176) (#178)
#176 完了 Co-authored-by: miteruzo <miteruzo@naver.com> Reviewed-on: #178
This commit was merged in pull request #178.
This commit is contained in:
Generated
+43
@@ -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
|
...((tag.children
|
||||||
?.sort ((a, b) => a.name < b.name ? -1 : 1)
|
?.sort ((a, b) => a.name < b.name ? -1 : 1)
|
||||||
.flatMap (child => renderTagTree (child, nestLevel + 1, key)) ?? [])]
|
.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 && (
|
<motion.div layout>
|
||||||
<div className="my-3" key={cat}>
|
{CATEGORIES.map ((cat: Category) => cat in tags && (
|
||||||
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
|
<motion.div layout className="my-3" key={cat}>
|
||||||
<ul>
|
<SubsectionTitle>{categoryNames[cat]}</SubsectionTitle>
|
||||||
{tags[cat].map (tag => renderTagTree (tag, 0, `cat-${ cat }`))}
|
|
||||||
</ul>
|
<motion.ul layout>
|
||||||
</div>))}
|
<AnimatePresence initial={false}>
|
||||||
{post && (
|
{tags[cat].map (tag => renderTagTree (tag, 0, `cat-${ cat }`))}
|
||||||
<div>
|
</AnimatePresence>
|
||||||
<SectionTitle>情報</SectionTitle>
|
</motion.ul>
|
||||||
<ul>
|
</motion.div>))}
|
||||||
<li>Id.: {post.id}</li>
|
{post && (
|
||||||
{/* TODO: uploadedUser の取得を対応したらコメント外す */}
|
<div>
|
||||||
{/*
|
<SectionTitle>情報</SectionTitle>
|
||||||
<li>
|
<ul>
|
||||||
<>耕作者: </>
|
<li>Id.: {post.id}</li>
|
||||||
{post.uploadedUser
|
{/* TODO: uploadedUser の取得を対応したらコメント外す */}
|
||||||
? (
|
{/*
|
||||||
<Link to={`/users/${ post.uploadedUser.id }`}>
|
<li>
|
||||||
{post.uploadedUser.name || '名もなきニジラー'}
|
<>耕作者: </>
|
||||||
</Link>)
|
{post.uploadedUser
|
||||||
: 'bot操作'}
|
? (
|
||||||
</li>
|
<Link to={`/users/${ post.uploadedUser.id }`}>
|
||||||
*/}
|
{post.uploadedUser.name || '名もなきニジラー'}
|
||||||
<li>耕作日時: {(new Date (post.createdAt)).toLocaleString ()}</li>
|
</Link>)
|
||||||
<li>
|
: 'bot操作'}
|
||||||
<>リンク: </>
|
</li>
|
||||||
<a
|
*/}
|
||||||
className="break-all"
|
<li>耕作日時: {(new Date (post.createdAt)).toLocaleString ()}</li>
|
||||||
href={post.url}
|
<li>
|
||||||
target="_blank"
|
<>リンク: </>
|
||||||
rel="noopener noreferrer nofollow">
|
<a
|
||||||
{post.url}
|
className="break-all"
|
||||||
</a>
|
href={post.url}
|
||||||
</li>
|
target="_blank"
|
||||||
<li>
|
rel="noopener noreferrer nofollow">
|
||||||
{/* TODO: 表示形式きしょすぎるので何とかする */}
|
{post.url}
|
||||||
<>オリジナルの投稿日時: </>
|
</a>
|
||||||
{!(post.originalCreatedFrom) && !(post.originalCreatedBefore)
|
</li>
|
||||||
? '不明'
|
<li>
|
||||||
: (
|
{/* TODO: 表示形式きしょすぎるので何とかする */}
|
||||||
<>
|
<>オリジナルの投稿日時: </>
|
||||||
{post.originalCreatedFrom
|
{!(post.originalCreatedFrom) && !(post.originalCreatedBefore)
|
||||||
&& `${ (new Date (post.originalCreatedFrom)).toLocaleString () } 以降 `}
|
? '不明'
|
||||||
{post.originalCreatedBefore
|
: (
|
||||||
&& `${ (new Date (post.originalCreatedBefore)).toLocaleString () } より前`}
|
<>
|
||||||
</>)}
|
{post.originalCreatedFrom
|
||||||
</li>
|
&& `${ (new Date (post.originalCreatedFrom)).toLocaleString () } 以降 `}
|
||||||
</ul>
|
{post.originalCreatedBefore
|
||||||
</div>)}
|
&& `${ (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>
|
<AnimatePresence initial={false}>
|
||||||
<ul>
|
{tagsVsbl && (
|
||||||
{CATEGORIES.flatMap (cat => cat in tags ? (
|
<motion.div
|
||||||
tags[cat].map (tag => (
|
key="sptags"
|
||||||
<li key={tag.id} className="mb-1">
|
className="md:block mt-4"
|
||||||
<TagLink tag={tag}/>
|
variants={{ hidden: { clipPath: 'inset(0 0 100% 0)',
|
||||||
</li>))) : [])}
|
height: 0 },
|
||||||
</ul>
|
visible: { clipPath: 'inset(0 0 0% 0)',
|
||||||
<SectionTitle>関聯</SectionTitle>
|
height: 'auto'} }}
|
||||||
{posts.length > 0 && (
|
initial="hidden"
|
||||||
<a href="#"
|
animate="visible"
|
||||||
onClick={ev => {
|
exit="hidden"
|
||||||
ev.preventDefault ()
|
transition={{ duration: .2, ease: 'easeOut' }}>
|
||||||
void ((async () => {
|
<SectionTitle>タグ</SectionTitle>
|
||||||
try
|
<ul>
|
||||||
{
|
{CATEGORIES.flatMap (cat => cat in tags ? (
|
||||||
const { data } = await axios.get (`${ API_BASE_URL }/posts/random`,
|
tags[cat].map (tag => (
|
||||||
{ params: { tags: tagsQuery.split (' ').filter (e => e !== '').join (' '),
|
<li key={tag.id} className="mb-1">
|
||||||
match: (anyFlg ? 'any' : 'all') } })
|
<TagLink tag={tag}/>
|
||||||
navigate (`/posts/${ (data as Post).id }`)
|
</li>))) : [])}
|
||||||
}
|
</ul>
|
||||||
catch
|
<SectionTitle>関聯</SectionTitle>
|
||||||
{
|
{posts.length > 0 && (
|
||||||
;
|
<a href="#"
|
||||||
}
|
onClick={ev => {
|
||||||
}) ())
|
ev.preventDefault ()
|
||||||
}}>
|
void ((async () => {
|
||||||
ランダム
|
try
|
||||||
</a>)}
|
{
|
||||||
</div>
|
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',
|
<AnimatePresence initial={false}>
|
||||||
'bg-yellow-200 dark:bg-red-975 items-start')}>
|
{menuOpen && (
|
||||||
<Separator/>
|
<motion.div
|
||||||
{menu.map ((item, i) => (
|
key="spmenu"
|
||||||
<Fragment key={i}>
|
className={cn ('flex flex-col md:hidden',
|
||||||
<Link to={i === openItemIdx ? item.to : '#'}
|
'bg-yellow-200 dark:bg-red-975 items-start')}
|
||||||
className={cn ('w-full min-h-[40px] flex items-center pl-8',
|
variants={{ closed: { clipPath: 'inset(0 0 100% 0)',
|
||||||
((i === openItemIdx)
|
height: 0 },
|
||||||
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}
|
open: { clipPath: 'inset(0 0 0% 0)',
|
||||||
onClick={ev => {
|
height: 'auto' } }}
|
||||||
if (i !== openItemIdx)
|
initial="closed"
|
||||||
{
|
animate="open"
|
||||||
ev.preventDefault ()
|
exit="closed"
|
||||||
setOpenItemIdx (i)
|
transition={{ duration: .2, ease: 'easeOut' }}>
|
||||||
}
|
<Separator/>
|
||||||
}}>
|
{menu.map ((item, i) => (
|
||||||
{item.name}
|
<Fragment key={i}>
|
||||||
</Link>
|
<Link to={i === openItemIdx ? item.to : '#'}
|
||||||
{i === openItemIdx && (
|
className={cn ('w-full min-h-[40px] flex items-center pl-8',
|
||||||
item.subMenu
|
((i === openItemIdx)
|
||||||
.filter (subItem => subItem.visible ?? true)
|
&& 'font-bold bg-yellow-50 dark:bg-red-950'))}
|
||||||
.map ((subItem, j) => 'component' in subItem ? subItem.component : (
|
onClick={ev => {
|
||||||
<Link key={j}
|
if (i !== openItemIdx)
|
||||||
to={subItem.to}
|
{
|
||||||
className="w-full min-h-[36px] flex items-center pl-12
|
ev.preventDefault ()
|
||||||
bg-yellow-50 dark:bg-red-950">
|
setOpenItemIdx (i)
|
||||||
{subItem.name}
|
}
|
||||||
</Link>)))}
|
}}>
|
||||||
</Fragment>))}
|
{item.name}
|
||||||
<TopNavUser user={user} sp/>
|
</Link>
|
||||||
<Separator/>
|
{i === openItemIdx && (
|
||||||
</div>
|
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user