-
-
Notifications
You must be signed in to change notification settings - Fork 771
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Signed-off-by: Innei <i@innei.in>
- Loading branch information
Showing
6 changed files
with
603 additions
and
386 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,157 @@ | ||
'use client' | ||
|
||
import React, { useMemo } from 'react' | ||
import clsx from 'clsx' | ||
import Link from 'next/link' | ||
import RemoveMarkdown from 'remove-markdown' | ||
import type { ReactActivityType } from './types' | ||
|
||
import { | ||
FaSolidFeatherAlt, | ||
IcTwotoneSignpost, | ||
MdiLightbulbOn20, | ||
} from '~/components/icons/menu-collection' | ||
import { routeBuilder, Routes } from '~/lib/route-builder' | ||
import { useAggregationSelector } from '~/providers/root/aggregation-data-provider' | ||
|
||
export const iconClassName = | ||
'rounded-full border shrink-0 border-accent/30 text-xs center inline-flex size-6 text-accent' | ||
|
||
export const ActivityCard = ({ activity }: { activity: ReactActivityType }) => { | ||
const siteOwner = useAggregationSelector((state) => state.user) | ||
const Content = useMemo(() => { | ||
switch (activity.bizType) { | ||
case 'comment': { | ||
return ( | ||
<div className="relative flex flex-col justify-center gap-2"> | ||
<div | ||
className={clsx( | ||
'absolute left-0 top-1/2 -translate-y-1/4', | ||
iconClassName, | ||
)} | ||
> | ||
<i className="icon-[mingcute--comment-line]" /> | ||
</div> | ||
<div className="flex items-center gap-2 pl-8"> | ||
<div className="space-x-2"> | ||
{activity.avatar && ( | ||
<img | ||
src={activity.avatar} | ||
className="inline size-[16px] rounded-full ring-2 ring-slate-200 dark:ring-zinc-800" | ||
/> | ||
)} | ||
<span className="font-medium">{activity.author}</span>{' '} | ||
<small>在</small>{' '} | ||
<Link | ||
className="shiro-link--underline" | ||
href={ | ||
activity.slug | ||
? `/posts/${activity.slug}` | ||
: `/notes/${activity.nid}` | ||
} | ||
> | ||
<b>{activity.title}</b> | ||
</Link>{' '} | ||
<small>说:</small> | ||
</div> | ||
</div> | ||
<div className="flex pl-8"> | ||
<div | ||
className={clsx( | ||
'relative inline-block rounded-xl p-3 text-zinc-800 dark:text-zinc-200', | ||
'rounded-tl-sm bg-zinc-600/5 dark:bg-zinc-500/20', | ||
'max-w-full overflow-auto', | ||
)} | ||
> | ||
{RemoveMarkdown(activity.text)} | ||
</div> | ||
</div> | ||
</div> | ||
) | ||
} | ||
case 'note': { | ||
return ( | ||
<div className="flex translate-y-1/4 gap-2"> | ||
<div className={clsx(iconClassName)}> | ||
<FaSolidFeatherAlt /> | ||
</div> | ||
<div className="space-x-2"> | ||
<small>发布了</small>{' '} | ||
<Link href={routeBuilder(Routes.Note, { id: activity.nid })}> | ||
<b>{activity.title}</b> | ||
</Link> | ||
</div> | ||
</div> | ||
) | ||
} | ||
case 'post': { | ||
return ( | ||
<div className="flex translate-y-1/4 gap-2"> | ||
<div className={clsx(iconClassName)}> | ||
<IcTwotoneSignpost /> | ||
</div> | ||
<div className="space-x-2"> | ||
<small>发布了</small>{' '} | ||
<Link href={`/posts/${activity.slug}`}> | ||
<b>{activity.title}</b> | ||
</Link> | ||
</div> | ||
</div> | ||
) | ||
} | ||
case 'recent': { | ||
return ( | ||
<div className="relative flex flex-col justify-center gap-2"> | ||
<div | ||
className={clsx( | ||
'absolute left-0 top-1/2 -translate-y-1/4', | ||
iconClassName, | ||
)} | ||
> | ||
<MdiLightbulbOn20 /> | ||
</div> | ||
|
||
<div className="flex gap-2 pl-8"> | ||
<img | ||
src={siteOwner?.avatar} | ||
className="mt-4 hidden size-6 rounded-full lg:inline" | ||
/> | ||
<div | ||
className={clsx( | ||
'relative inline-block rounded-xl p-3 text-zinc-800 dark:text-zinc-200', | ||
'rounded-tl-sm bg-zinc-600/5 dark:bg-zinc-500/20', | ||
'max-w-full overflow-auto', | ||
)} | ||
> | ||
{RemoveMarkdown(activity.content)} | ||
</div> | ||
</div> | ||
</div> | ||
) | ||
} | ||
case 'like': { | ||
return ( | ||
<div className="flex translate-y-1/4 items-start gap-2"> | ||
<span className={clsx(iconClassName)}> | ||
<i className="icon-[mingcute--heart-line]" /> | ||
</span> | ||
<div className="space-x-2"> | ||
<small>有人点赞了</small>{' '} | ||
<Link | ||
href={ | ||
activity.slug | ||
? `/posts/${activity.slug}` | ||
: `/notes/${activity.nid}` | ||
} | ||
> | ||
<b>{activity.title}</b> | ||
</Link> | ||
</div> | ||
</div> | ||
) | ||
} | ||
} | ||
}, [activity, siteOwner?.avatar]) | ||
|
||
return <div className="pb-4 text-base">{Content}</div> | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
'use client' | ||
|
||
import React from 'react' | ||
import { m } from 'framer-motion' | ||
import Link from 'next/link' | ||
|
||
import { Divider } from '~/components/ui/divider' | ||
import { RelativeTime } from '~/components/ui/relative-time' | ||
import { softBouncePreset } from '~/constants/spring' | ||
import { routeBuilder, Routes } from '~/lib/route-builder' | ||
|
||
import { useHomeQueryData } from '../query' | ||
|
||
export const ActivityPostList = () => { | ||
const { notes, posts } = useHomeQueryData() | ||
return ( | ||
<m.section | ||
initial={{ opacity: 0.0001, y: 50 }} | ||
animate={{ opacity: 1, y: 0 }} | ||
transition={softBouncePreset} | ||
className="mt-8 flex flex-col gap-4 lg:mt-0" | ||
> | ||
<h2 className="text-2xl font-medium leading-loose">最近更新的文稿</h2> | ||
<ul className="shiro-timeline mt-4"> | ||
{posts.map((post) => { | ||
return ( | ||
<li key={post.id} className="flex min-w-0 justify-between"> | ||
<Link | ||
prefetch | ||
className="min-w-0 shrink truncate" | ||
href={routeBuilder(Routes.Post, { | ||
category: post.category.slug, | ||
slug: post.slug, | ||
})} | ||
> | ||
{post.title} | ||
</Link> | ||
|
||
<span className="ml-2 shrink-0 self-end text-xs opacity-70"> | ||
<RelativeTime | ||
date={post.created} | ||
displayAbsoluteTimeAfterDay={180} | ||
/> | ||
</span> | ||
</li> | ||
) | ||
})} | ||
</ul> | ||
|
||
<Link | ||
className="flex items-center justify-end opacity-70 duration-200 hover:text-accent" | ||
href={routeBuilder(Routes.Posts, {})} | ||
> | ||
<i className="icon-[mingcute--arrow-right-circle-line]" /> | ||
<span className="ml-2">还有更多</span> | ||
</Link> | ||
|
||
<Divider /> | ||
<h2 className="text-2xl font-medium leading-loose">最近更新的手记</h2> | ||
<ul className="shiro-timeline mt-4"> | ||
{notes.map((note, i) => { | ||
return ( | ||
<li key={note.id} className="flex min-w-0 justify-between"> | ||
<Link | ||
className="min-w-0 shrink truncate" | ||
href={routeBuilder(Routes.Note, { | ||
id: note.nid, | ||
})} | ||
> | ||
{note.title} | ||
</Link> | ||
|
||
<span className="ml-2 shrink-0 self-end text-xs opacity-70"> | ||
<RelativeTime | ||
date={note.created} | ||
displayAbsoluteTimeAfterDay={180} | ||
/> | ||
</span> | ||
</li> | ||
) | ||
})} | ||
</ul> | ||
<Link | ||
className="flex items-center justify-end opacity-70 duration-200 hover:text-accent" | ||
href={routeBuilder(Routes.Timelime, { type: 'note' })} | ||
> | ||
<i className="icon-[mingcute--arrow-right-circle-line]" /> | ||
<span className="ml-2">还有更多</span> | ||
</Link> | ||
</m.section> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,90 @@ | ||
'use client' | ||
|
||
import { useQuery } from '@tanstack/react-query' | ||
import React, { useMemo } from 'react' | ||
import clsx from 'clsx' | ||
import { m } from 'framer-motion' | ||
import type { ReactActivityType } from './types' | ||
|
||
import { ScrollArea } from '~/components/ui/scroll-area' | ||
import { softBouncePreset } from '~/constants/spring' | ||
import { apiClient } from '~/lib/request' | ||
|
||
import { ActivityCard, iconClassName } from './ActivityCard' | ||
|
||
export const ActivityRecent = () => { | ||
const { data, isLoading } = useQuery({ | ||
queryKey: ['home-activity-recent'], | ||
queryFn: async () => { | ||
return (await apiClient.activity.getRecentActivities()).$serialized | ||
}, | ||
refetchOnMount: true, | ||
meta: { | ||
persist: true, | ||
}, | ||
}) | ||
|
||
const flatData = useMemo(() => { | ||
return [...Object.entries(data || {})] | ||
.map(([type, items]) => { | ||
if (!Array.isArray(items)) return [] | ||
return items.map((item: any) => { | ||
return { ...item, bizType: type } | ||
}) | ||
}) | ||
.flat() | ||
.sort((a, b) => { | ||
return new Date(b.created).getTime() - new Date(a.created).getTime() | ||
}) as ReactActivityType[] | ||
// .slice(0, 6) as ReactActivityType[] | ||
}, [data]) | ||
|
||
return ( | ||
<m.div | ||
initial={{ opacity: 0.0001, y: 50 }} | ||
animate={{ opacity: 1, y: 0 }} | ||
transition={softBouncePreset} | ||
className="mt-8 w-full text-lg lg:mt-0" | ||
> | ||
<m.h2 className="mb-8 text-2xl font-medium leading-loose lg:ml-14"> | ||
最近发生的事 | ||
</m.h2> | ||
|
||
{isLoading ? ( | ||
<div className="relative h-[400px] max-h-[80vh]"> | ||
<ul className="shiro-timeline mt-4 flex animate-pulse flex-col pb-4 pl-2 text-slate-200 dark:!text-neutral-700"> | ||
{new Array(6).fill(null).map((_, i) => { | ||
return ( | ||
<li key={i} className="flex w-full items-center gap-2"> | ||
<div | ||
className={clsx( | ||
iconClassName, | ||
'border-0 bg-current text-inherit', | ||
)} | ||
/> | ||
|
||
<div className="mb-4 box-content h-16 w-full rounded-md bg-current" /> | ||
</li> | ||
) | ||
})} | ||
</ul> | ||
</div> | ||
) : ( | ||
<ScrollArea.ScrollArea rootClassName="h-[400px] relative max-h-[80vh]"> | ||
<ul className="shiro-timeline mt-4 flex flex-col pb-8 pl-2"> | ||
{flatData.map((activity) => { | ||
return ( | ||
<li | ||
key={`${activity.bizType}-${activity.id}-${activity.created}`} | ||
className="flex min-w-0 justify-between" | ||
> | ||
<ActivityCard activity={activity} /> | ||
</li> | ||
) | ||
})} | ||
</ul> | ||
</ScrollArea.ScrollArea> | ||
)} | ||
</m.div> | ||
) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
'use client' | ||
|
||
import React, { forwardRef, useRef } from 'react' | ||
import { useInView } from 'framer-motion' | ||
import type { PropsWithChildren } from 'react' | ||
|
||
import { isDev } from '~/lib/env' | ||
import { clsxm } from '~/lib/helper' | ||
|
||
const debugStyle = { | ||
outline: '1px solid #0088cc', | ||
} | ||
export const Screen = forwardRef< | ||
HTMLDivElement, | ||
PropsWithChildren<{ | ||
className?: string | ||
}> | ||
>((props, ref) => { | ||
return ( | ||
<InViewScreen | ||
ref={ref} | ||
className={clsxm( | ||
'h-dvh min-h-[800px] min-w-0 max-w-screen overflow-hidden', | ||
props.className, | ||
)} | ||
> | ||
{props.children} | ||
</InViewScreen> | ||
) | ||
}) | ||
|
||
export const InViewScreen = forwardRef< | ||
HTMLDivElement, | ||
PropsWithChildren<{ | ||
className?: string | ||
}> | ||
>((props, ref) => { | ||
const inViewRef = useRef<HTMLSpanElement>(null) | ||
const inView = useInView(inViewRef, { once: true }) | ||
|
||
return ( | ||
<div | ||
ref={ref} | ||
style={isDev ? debugStyle : undefined} | ||
className={clsxm('relative flex flex-col center', props.className)} | ||
> | ||
<span ref={inViewRef} /> | ||
{inView && props.children} | ||
</div> | ||
) | ||
}) | ||
|
||
InViewScreen.displayName = 'InViewScreen' | ||
Screen.displayName = 'Screen' |
Oops, something went wrong.