feat: PostCard component

This commit is contained in:
DIYgod 2023-07-17 22:12:41 +01:00
parent 52f54e44c6
commit 5abd845c2e
No known key found for this signature in database
2 changed files with 195 additions and 184 deletions

View File

@ -0,0 +1,190 @@
"use client"
import { CharacterEntity } from "crossbell"
import Link from "next/link"
import { useRouter } from "next/navigation"
import { memo } from "react"
import reactStringReplace from "react-string-replace"
import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
import { Titles } from "~/components/common/Titles"
import PostCover from "~/components/home/PostCover"
import { Avatar } from "~/components/ui/Avatar"
import { useDate } from "~/hooks/useDate"
import { useIsMobileLayout } from "~/hooks/useMobileLayout"
import { getSiteLink } from "~/lib/helpers"
import { useTranslation } from "~/lib/i18n/client"
import { ExpandedNote } from "~/lib/types"
import { cn } from "~/lib/utils"
const Card = ({
character,
post,
keyword,
comment,
createdAt,
}: {
character?: CharacterEntity
post: ExpandedNote
keyword?: string
comment?: string
createdAt?: string
}) => {
const router = useRouter()
const { t } = useTranslation("common")
const date = useDate()
const isMobileLayout = useIsMobileLayout()
return (
<Link
target={isMobileLayout ? "_blank" : undefined}
href={
isMobileLayout
? `/site/${post?.character?.handle}/${post.metadata?.content?.slug}`
: `/post/${post?.character?.handle}/${post.metadata?.content?.slug}`
}
className={cn(
"xlog-post sm:hover:bg-hover transition-all rounded-2xl flex flex-col items-center hover:opacity-100 group border",
)}
>
<PostCover
cover={post.metadata?.content.cover}
title={post.metadata?.content?.title}
/>
<div className="px-3 py-2 sm:px-5 sm:py-4 w-full min-w-0 h-[148px] sm:h-[166px] flex flex-col space-y-2 text-sm">
<div className="line-clamp-3 space-y-2 h-[74px]">
{comment && (
<div className="font-medium text-zinc-700 line-clamp-2">
<i className="icon-[mingcute--comment-fill] mr-2" />
{comment}
</div>
)}
<h2
className={cn(
"xlog-post-title font-bold text-base",
comment ? "text-zinc-500" : "text-zinc-700",
)}
>
{post.metadata?.content?.title}
</h2>
{!comment && (
<div
className="xlog-post-excerpt text-zinc-500"
style={{
wordBreak: "break-word",
}}
>
{keyword
? reactStringReplace(
post.metadata?.content?.summary || "",
keyword,
(match, i) => (
<span key={i} className="bg-yellow-200">
{match}
</span>
),
)
: post.metadata?.content?.summary}
{post.metadata?.content?.summary && "..."}
</div>
)}
</div>
<div className="xlog-post-meta text-zinc-400 space-x-2 flex items-center text-[13px] h-[26px] truncate">
{!!post.metadata?.content?.tags?.[1] && (
<span
className="xlog-post-tags hover:text-zinc-600 hover:bg-zinc-200 border transition-colors text-zinc-500 inline-flex items-center bg-zinc-100 rounded-full px-2 py-[2px] truncate"
onClick={(e) => {
e.preventDefault()
router.push(`/tag/${post.metadata?.content?.tags?.[1]}`)
}}
>
<i className="icon-[mingcute--tag-line] mr-[2px]" />
{post.metadata?.content?.tags?.[1]}
</span>
)}
<span className="xlog-post-word-count inline-flex items-center">
<i className="icon-[mingcute--time-line] mr-[2px]" />
<span
style={{
wordSpacing: "-.2ch",
}}
>
{post.metadata?.content?.readingTime} {t("min")}
</span>
</span>
{!!post.stat?.viewDetailCount && (
<span className="xlog-post-views inline-flex items-center">
<i className="icon-[mingcute--eye-line] mr-[2px]" />
<span>{post.stat?.viewDetailCount}</span>
</span>
)}
</div>
<div className="flex items-center space-x-1 text-xs sm:text-sm overflow-hidden">
<CharacterFloatCard siteId={character?.handle}>
<span
className="flex items-center cursor-pointer"
onClick={(e) => {
e.preventDefault()
window.open(
`${getSiteLink({
subdomain: character?.handle || "",
})}`,
)
}}
>
<span className="w-5 h-5 inline-block mr-[6px]">
<Avatar
cid={character?.characterId}
images={character?.metadata?.content?.avatars || []}
size={20}
name={character?.metadata?.content?.name}
></Avatar>
</span>
<span className="font-medium truncate text-zinc-600">
{character?.metadata?.content?.name || character?.handle}
</span>
</span>
</CharacterFloatCard>
<Titles characterId={character?.characterId} />
<span className="text-zinc-400 hidden sm:inline-block">·</span>
<time
dateTime={date.formatToISO(createdAt || post.createdAt)}
className="xlog-post-date whitespace-nowrap text-zinc-400 hidden sm:inline-block"
>
{t("ago", {
time: date.dayjs
.duration(
date
.dayjs(createdAt || post?.createdAt)
.diff(date.dayjs(), "minute"),
"minute",
)
.humanize(),
})}
</time>
</div>
</div>
</Link>
)
}
const Post = ({ post, keyword }: { post: ExpandedNote; keyword?: string }) => {
let isComment
if (post.metadata?.content?.tags?.includes("comment") && post.toNote) {
isComment = true
}
return (
<Card
character={post.character || undefined}
post={isComment ? (post.toNote as ExpandedNote) : post}
keyword={keyword}
comment={isComment ? post.metadata?.content?.summary : undefined}
createdAt={isComment ? post?.createdAt : post.toNote?.createdAt}
/>
)
}
const PostCard = memo(Post)
export default PostCard

View File

@ -1,205 +1,26 @@
"use client"
import { CharacterEntity } from "crossbell"
import Link from "next/link"
import { useParams, useRouter, useSearchParams } from "next/navigation"
import { memo, useEffect, useState } from "react"
import reactStringReplace from "react-string-replace"
import { useParams, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
import { VirtuosoGrid } from "react-virtuoso"
import { useAccountState, useConnectModal } from "@crossbell/connect-kit"
import { Switch } from "@headlessui/react"
import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
import { Titles } from "~/components/common/Titles"
import PostCover from "~/components/home/PostCover"
import { Avatar } from "~/components/ui/Avatar"
import PostCard from "~/components/common/PostCard"
import { EmptyState } from "~/components/ui/EmptyState"
import { Skeleton } from "~/components/ui/Skeleton"
import { Tabs } from "~/components/ui/Tabs"
import { Tooltip } from "~/components/ui/Tooltip"
import { useDate } from "~/hooks/useDate"
import { useIsMobileLayout } from "~/hooks/useMobileLayout"
import { getSiteLink } from "~/lib/helpers"
import { useTranslation } from "~/lib/i18n/client"
import { getStorage, setStorage } from "~/lib/storage"
import { ExpandedNote } from "~/lib/types"
import { cn, getStringLength } from "~/lib/utils"
import { getStringLength } from "~/lib/utils"
import type { FeedType, SearchType } from "~/models/home.model"
import { useGetFeed } from "~/queries/home"
import topics from "../../../data/topics.json"
const PostCard = ({
character,
post,
keyword,
comment,
createdAt,
}: {
character?: CharacterEntity
post: ExpandedNote
keyword?: string
comment?: string
createdAt?: string
}) => {
const router = useRouter()
const { t } = useTranslation("common")
const date = useDate()
const isMobileLayout = useIsMobileLayout()
return (
<Link
target={isMobileLayout ? "_blank" : undefined}
href={
isMobileLayout
? `/site/${post?.character?.handle}/${post.metadata?.content?.slug}`
: `/post/${post?.character?.handle}/${post.metadata?.content?.slug}`
}
className={cn(
"xlog-post sm:hover:bg-hover transition-all rounded-2xl flex flex-col items-center hover:opacity-100 group border",
)}
>
<PostCover
cover={post.metadata?.content.cover}
title={post.metadata?.content?.title}
/>
<div className="px-3 py-2 sm:px-5 sm:py-4 w-full min-w-0 h-[148px] sm:h-[166px] flex flex-col space-y-2 text-sm">
<div className="line-clamp-3 space-y-2 h-[74px]">
{comment && (
<div className="font-medium text-zinc-700 line-clamp-2">
<i className="icon-[mingcute--comment-fill] mr-2" />
{comment}
</div>
)}
<h2
className={cn(
"xlog-post-title font-bold text-base",
comment ? "text-zinc-500" : "text-zinc-700",
)}
>
{post.metadata?.content?.title}
</h2>
{!comment && (
<div
className="xlog-post-excerpt text-zinc-500"
style={{
wordBreak: "break-word",
}}
>
{keyword
? reactStringReplace(
post.metadata?.content?.summary || "",
keyword,
(match, i) => (
<span key={i} className="bg-yellow-200">
{match}
</span>
),
)
: post.metadata?.content?.summary}
{post.metadata?.content?.summary && "..."}
</div>
)}
</div>
<div className="xlog-post-meta text-zinc-400 space-x-2 flex items-center text-[13px] h-[26px] truncate">
{!!post.metadata?.content?.tags?.[1] && (
<span
className="xlog-post-tags hover:text-zinc-600 hover:bg-zinc-200 border transition-colors text-zinc-500 inline-flex items-center bg-zinc-100 rounded-full px-2 py-[2px] truncate"
onClick={(e) => {
e.preventDefault()
router.push(`/tag/${post.metadata?.content?.tags?.[1]}`)
}}
>
<i className="icon-[mingcute--tag-line] mr-[2px]" />
{post.metadata?.content?.tags?.[1]}
</span>
)}
<span className="xlog-post-word-count inline-flex items-center">
<i className="icon-[mingcute--time-line] mr-[2px]" />
<span
style={{
wordSpacing: "-.2ch",
}}
>
{post.metadata?.content?.readingTime} {t("min")}
</span>
</span>
{!!post.stat?.viewDetailCount && (
<span className="xlog-post-views inline-flex items-center">
<i className="icon-[mingcute--eye-line] mr-[2px]" />
<span>{post.stat?.viewDetailCount}</span>
</span>
)}
</div>
<div className="flex items-center space-x-1 text-xs sm:text-sm overflow-hidden">
<CharacterFloatCard siteId={character?.handle}>
<span
className="flex items-center cursor-pointer"
onClick={(e) => {
e.preventDefault()
window.open(
`${getSiteLink({
subdomain: character?.handle || "",
})}`,
)
}}
>
<span className="w-5 h-5 inline-block mr-[6px]">
<Avatar
cid={character?.characterId}
images={character?.metadata?.content?.avatars || []}
size={20}
name={character?.metadata?.content?.name}
></Avatar>
</span>
<span className="font-medium truncate text-zinc-600">
{character?.metadata?.content?.name || character?.handle}
</span>
</span>
</CharacterFloatCard>
<Titles characterId={character?.characterId} />
<span className="text-zinc-400 hidden sm:inline-block">·</span>
<time
dateTime={date.formatToISO(createdAt || post.createdAt)}
className="xlog-post-date whitespace-nowrap text-zinc-400 hidden sm:inline-block"
>
{t("ago", {
time: date.dayjs
.duration(
date
.dayjs(createdAt || post?.createdAt)
.diff(date.dayjs(), "minute"),
"minute",
)
.humanize(),
})}
</time>
</div>
</div>
</Link>
)
}
const Post = ({ post, keyword }: { post: ExpandedNote; keyword?: string }) => {
let isComment
if (post.metadata?.content?.tags?.includes("comment") && post.toNote) {
isComment = true
}
return (
<PostCard
character={post.character || undefined}
post={isComment ? (post.toNote as ExpandedNote) : post}
keyword={keyword}
comment={isComment ? post.metadata?.content?.summary : undefined}
createdAt={isComment ? post?.createdAt : post.toNote?.createdAt}
/>
)
}
const MemoedPost = memo(Post)
export const HomeFeed = ({ type }: { type?: FeedType }) => {
const { t } = useTranslation("common")
const searchParams = useSearchParams()
@ -411,7 +232,7 @@ export const HomeFeed = ({ type }: { type?: FeedType }) => {
const post = feedInOne[index]
if (!post) return null
return (
<MemoedPost
<PostCard
key={`${post.characterId}-${post.noteId}`}
post={post}
keyword={searchParams?.get("q") || undefined}