Skip to content

Commit

Permalink
feat: say view
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Jul 1, 2023
1 parent 9585c1e commit e6ba8b2
Show file tree
Hide file tree
Showing 15 changed files with 359 additions and 19 deletions.
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,6 @@ A theme for [Mix Space](https://github.com/mx-space)

## License

2023 Innei, GPLv3.
2022 © Innei, Released under the MIT License.

> [Personal Website](https://innei.in/) · GitHub [@Innei](https://github.com/innei/)
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export default function Page() {
}),
)}

{hasNextPage && <LoadMoreIndicator onClick={fetchNextPage} />}
{hasNextPage && <LoadMoreIndicator onLoading={fetchNextPage} />}
</TimelineList>
</main>
</BottomToUpSoftScaleTransitionView>
Expand Down
10 changes: 2 additions & 8 deletions src/app/posts/page.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { EmptyIcon } from '~/components/icons/empty'
import { NormalContainer } from '~/components/layout/container/Normal'
import { BottomToUpTransitionView } from '~/components/ui/transition/BottomToUpTransitionView'
import { PostItem } from '~/components/widgets/post/PostItem'
import { PostPagination } from '~/components/widgets/post/PostPagination'
import { NothingFound } from '~/components/widgets/shared/NothingFound'
import { apiClient } from '~/utils/request'

interface Props {
Expand All @@ -25,13 +25,7 @@ export default async (props: Props) => {
const { data, pagination } = $serialized

if (!data?.length) {
return (
<NormalContainer className="flex h-[500px] flex-col space-y-4 center [&_p]:my-4">
<EmptyIcon />
<p>这里空空如也</p>
<p>稍后再来看看吧!</p>
</NormalContainer>
)
return <NothingFound />
}
return (
<NormalContainer>
Expand Down
11 changes: 11 additions & 0 deletions src/app/says/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { Metadata } from 'next'
import type { PropsWithChildren } from 'react'

import { WiderContainer } from '~/components/layout/container/Wider'

export const metadata: Metadata = {
title: '一言',
}
export default async function Layout(props: PropsWithChildren) {
return <WiderContainer>{props.children}</WiderContainer>
}
148 changes: 148 additions & 0 deletions src/app/says/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
'use client'

import { useInfiniteQuery } from '@tanstack/react-query'
import { memo, useRef } from 'react'
import { m } from 'framer-motion'
import Markdown from 'markdown-to-jsx'
import type { SayModel } from '@mx-space/api-client'
import type { MarkdownToJSX } from 'markdown-to-jsx'

import { useIsMobile } from '~/atoms'
import { Loading } from '~/components/ui/loading'
import { Masonry } from '~/components/ui/masonry'
import { RelativeTime } from '~/components/ui/relative-time'
import { BottomToUpTransitionView } from '~/components/ui/transition/BottomToUpTransitionView'
import { LoadMoreIndicator } from '~/components/widgets/shared/LoadMoreIndicator'
import { NothingFound } from '~/components/widgets/shared/NothingFound'
import { useIsDark } from '~/hooks/common/use-is-dark'
import { addAlphaToHSL, getColorScheme, stringToHue } from '~/lib/color'
import { apiClient } from '~/utils/request'

import { sayQueryKey } from './query'

export default function Page() {
const { fetchNextPage, hasNextPage, data, isLoading } = useInfiniteQuery({
queryKey: sayQueryKey,
queryFn: async ({ pageParam }) => {
const data = await apiClient.say.getAllPaginated(pageParam)
return data
},
getNextPageParam: (lastPage) =>
lastPage.pagination.hasNextPage
? lastPage.pagination.currentPage + 1
: undefined,
})

const isMobile = useIsMobile()

if (isLoading) {
return <Loading useDefaultLoadingText />
}

if (!data || data.pages.length === 0) return <NothingFound />

const list = data.pages
.map((page) => page.data)
.flat()
.map((say) => {
return {
text: say.text,
item: say,
id: say.id,
}
})

return (
<div>
<header className="prose">
<h1>一言</h1>
</header>

<main className="mt-10">
<Masonry Component={Item} columns={isMobile ? 1 : 2} list={list} />

{hasNextPage && (
<LoadMoreIndicator onLoading={fetchNextPage} className="mt-12">
<Masonry
Component={SaySkeleton}
columns={isMobile ? 1 : 2}
list={placeholderData}
/>
</LoadMoreIndicator>
)}
</main>
</div>
)
}
const placeholderData = Array.from({ length: 10 }).map((_, index) => ({
index,
text: '',
id: index.toFixed(),
item: {} as SayModel,
}))
const SaySkeleton = memo(() => {
return (
<div className="relative mb-4 border-l-[3px] border-l-slate-500 bg-gray-200 px-4 py-3 dark:bg-neutral-700">
<div className="mb-2 h-6 w-full rounded bg-gray-300 dark:bg-neutral-600" />
<div className="flex text-sm text-base-content/60 md:justify-between">
<div className="mb-2 h-4 w-14 rounded bg-gray-300 dark:bg-neutral-600 md:mb-0" />
<div className="ml-auto text-right">
<div className="h-4 w-1/4 rounded bg-gray-300 dark:bg-neutral-600" />
</div>
</div>
</div>
)
})
SaySkeleton.displayName = 'SaySkeleton'

const options = {
disableParsingRawHTML: true,
forceBlock: true,
} satisfies MarkdownToJSX.Options

const Item = memo<{
item: SayModel
index: number
}>(({ item: say, index: i }) => {
const hasSource = !!say.source
const hasAuthor = !!say.author
// const color = colorsMap.get(say.id)
const { dark: darkColors, light: lightColors } = useRef(
getColorScheme(stringToHue(say.id)),
).current
const isDark = useIsDark()

return (
<BottomToUpTransitionView delay={i * 50} key={say.id}>
<m.blockquote
layout
key={say.id}
className="mb-4 border-l-[3px] px-4 py-3"
style={{
borderLeftColor: isDark ? darkColors.accent : lightColors.accent,
backgroundColor: addAlphaToHSL(
isDark ? darkColors.background : lightColors.background,
0.15,
),
}}
>
<Markdown className="mb-2" options={options}>{`${say.text}`}</Markdown>
<div className="flex flex-wrap text-sm text-base-content/60 md:justify-between">
<div className="mb-2 w-full md:mb-0 md:w-auto">
<span className="mr-2">发布于</span>
<RelativeTime date={say.created} />
</div>
<div className="w-full text-right md:ml-auto md:w-auto">
<div>
{hasSource && `出自“${say.source}”`}
{hasSource && hasAuthor && ', '}
{hasAuthor && `作者:${say.author}`}
{!hasAuthor && !hasSource && '站长说'}
</div>
</div>
</div>
</m.blockquote>
</BottomToUpTransitionView>
)
})
Item.displayName = 'Item'
1 change: 1 addition & 0 deletions src/app/says/query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const sayQueryKey = ['says']
16 changes: 16 additions & 0 deletions src/components/layout/container/Wider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { clsxm } from '~/utils/helper'

export const WiderContainer: Component = (props) => {
const { children, className } = props

return (
<div
className={clsxm(
'mx-auto mt-14 max-w-5xl px-2 lg:mt-[120px] lg:px-0 2xl:max-w-6xl',
className,
)}
>
{children}
</div>
)
}
8 changes: 4 additions & 4 deletions src/components/ui/code-highlighter/CodeHighlighter.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import React, { useCallback, useInsertionEffect, useRef } from 'react'
import { useTheme } from 'next-themes'
import type { FC } from 'react'

import { useIsPrintMode } from '~/atoms/css-media'
import { useIsDark } from '~/hooks/common/use-is-dark'
import { loadScript, loadStyleSheet } from '~/lib/load-script'
import { toast } from '~/lib/toast'

Expand All @@ -28,13 +28,13 @@ export const HighLighter: FC<Props> = (props) => {
}, [value])

const prevThemeCSS = useRef<ReturnType<typeof loadStyleSheet>>()
const { theme, systemTheme } = useTheme()
const isPrintMode = useIsPrintMode()
const isDark = useIsDark()

useInsertionEffect(() => {
const css = loadStyleSheet(
`https://lf3-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism-themes/1.9.0/prism-one-${
isPrintMode ? 'light' : theme === 'system' ? systemTheme : theme
isPrintMode ? 'light' : isDark ? 'dark' : 'light'
}.css`,
)

Expand All @@ -46,7 +46,7 @@ export const HighLighter: FC<Props> = (props) => {
}

prevThemeCSS.current = css
}, [theme, isPrintMode, systemTheme])
}, [isDark, isPrintMode])
useInsertionEffect(() => {
loadStyleSheet(
'https://lf26-cdn-tos.bytecdntp.com/cdn/expire-1-M/prism/1.23.0/plugins/line-numbers/prism-line-numbers.min.css',
Expand Down
60 changes: 60 additions & 0 deletions src/components/ui/masonry/Masonry.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import React, { useEffect, useState } from 'react'

interface MasonryProps<T> {
list: Array<{ id: string; text: string; item: T }>
columns: number
Component: React.NamedExoticComponent<{
text: string
item: T
index: number
}>
}

export function Masonry<T>({ list, columns, Component }: MasonryProps<T>) {
const [columnWrapperStyle, setColumnWrapperStyle] = useState({})
const [elements, setElements] = useState<Array<JSX.Element[]>>([])

useEffect(() => {
setColumnWrapperStyle({
columnCount: columns,
columnGap: '1em',
})

const elements = list.reduce(
(accumulator: Array<JSX.Element[]>, item, i) => {
const element = (
<div key={item.id} className="break-inside-avoid">
<Component text={item.text} item={item.item} index={i} />
</div>
)

const columnIndex = i % columns
accumulator[columnIndex] = [
...(accumulator[columnIndex] || []),
element,
]

return accumulator
},
[],
)

setElements(elements)
}, [list, columns])

return (
<div style={columnWrapperStyle} className="relative w-full">
{elements.map((colElements, i) => (
<div
key={i}
className="relative block w-full align-top"
// style={{
// width: `calc(100% / ${columns})`,
// }}
>
{colElements}
</div>
))}
</div>
)
}
1 change: 1 addition & 0 deletions src/components/ui/masonry/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './Masonry'
2 changes: 1 addition & 1 deletion src/components/widgets/comment/Comments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ export const Comments: FC<CommentBaseProps> = ({ refId }) => {
)}
</ul>
{hasNextPage && (
<LoadMoreIndicator onClick={fetchNextPage}>
<LoadMoreIndicator onLoading={fetchNextPage}>
<CommentSkeleton />
</LoadMoreIndicator>
)}
Expand Down
12 changes: 8 additions & 4 deletions src/components/widgets/shared/LoadMoreIndicator.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,17 @@ import { useInView } from 'react-intersection-observer'
import { Loading } from '~/components/ui/loading'

export const LoadMoreIndicator: Component<{
onClick: () => void
}> = ({ onClick, children }) => {
onLoading: () => void
}> = ({ onLoading, children, className }) => {
const { ref } = useInView({
rootMargin: '1px',
onChange(inView) {
if (inView) onClick()
if (inView) onLoading()
},
})
return <div ref={ref}>{children ?? <Loading />}</div>
return (
<div className={className} ref={ref}>
{children ?? <Loading />}
</div>
)
}
12 changes: 12 additions & 0 deletions src/components/widgets/shared/NothingFound.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { EmptyIcon } from '~/components/icons/empty'
import { NormalContainer } from '~/components/layout/container/Normal'

export const NothingFound: Component = () => {
return (
<NormalContainer className="flex h-[500px] flex-col space-y-4 center [&_p]:my-4">
<EmptyIcon />
<p>这里空空如也</p>
<p>稍后再来看看吧!</p>
</NormalContainer>
)
}
6 changes: 6 additions & 0 deletions src/hooks/common/use-is-dark.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { useTheme } from 'next-themes'

export const useIsDark = () => {
const { theme, systemTheme } = useTheme()
return theme === 'dark' || (theme === 'system' && systemTheme === 'dark')
}
Loading

1 comment on commit e6ba8b2

@vercel
Copy link

@vercel vercel bot commented on e6ba8b2 Jul 1, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

shiro – ./

springtide.vercel.app
shiro-innei.vercel.app
innei.in
shiro-git-main-innei.vercel.app

Please sign in to comment.