feat: redesign home page, use post card
This commit is contained in:
parent
e06bc00bfd
commit
75c2b1f163
|
|
@ -2,6 +2,8 @@ import { ConnectButton } from "~/components/common/ConnectButton"
|
|||
import { DarkModeSwitch } from "~/components/common/DarkModeSwitch"
|
||||
import { Logo } from "~/components/common/Logo"
|
||||
import HomeTabs from "~/components/home/HomeTabs"
|
||||
import { BackToTopFAB } from "~/components/site/BackToTopFAB"
|
||||
import { FABContainer } from "~/components/ui/FAB"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { APP_NAME, DISCORD_LINK, GITHUB_LINK, TWITTER_LINK } from "~/lib/env"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
|
|
@ -15,26 +17,30 @@ export default async function HomeLayout({
|
|||
<>
|
||||
<header className="py-5 fixed w-full top-0 bg-white z-[1]">
|
||||
<div className="max-w-screen-xl px-5 mx-auto flex justify-between items-center">
|
||||
<UniLink
|
||||
href="/"
|
||||
className="text-2xl font-extrabold flex items-center"
|
||||
>
|
||||
<div className="inline-block w-9 h-9 mr-3">
|
||||
<Logo type="lottie" width={36} height={36} autoplay={false} />
|
||||
<div className="space-x-14 flex">
|
||||
<UniLink
|
||||
href="/"
|
||||
className="text-2xl font-extrabold flex items-center"
|
||||
>
|
||||
<div className="inline-block w-9 h-9 mr-3">
|
||||
<Logo type="lottie" width={36} height={36} autoplay={false} />
|
||||
</div>
|
||||
xLog
|
||||
</UniLink>
|
||||
<div className="space-x-14 text-zinc-500 flex">
|
||||
<HomeTabs />
|
||||
</div>
|
||||
xLog
|
||||
</UniLink>
|
||||
</div>
|
||||
<div className="space-x-14 text-zinc-500 flex">
|
||||
<HomeTabs />
|
||||
<ConnectButton size="base" variantColor="black" />
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
<section className="pt-24">
|
||||
<div className="max-w-screen-lg px-5 mx-auto flex">{children}</div>
|
||||
<div className="max-w-screen-xl px-5 mx-auto flex">{children}</div>
|
||||
</section>
|
||||
<footer className="mt-10 font-medium border-t">
|
||||
<div className="max-w-screen-lg px-5 py-14 mx-auto flex justify-between">
|
||||
<div className="max-w-screen-xl px-5 py-14 mx-auto flex justify-between">
|
||||
<span className="text-zinc-700 ml-2 inline-flex items-center space-x-5 align-middle">
|
||||
{GITHUB_LINK && (
|
||||
<UniLink className="flex items-center" href={GITHUB_LINK}>
|
||||
|
|
@ -68,6 +74,9 @@ export default async function HomeLayout({
|
|||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
<FABContainer>
|
||||
<BackToTopFAB />
|
||||
</FABContainer>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,8 +5,7 @@ import { usePathname, useRouter } from "next/navigation"
|
|||
import { useAccountState, useConnectModal } from "@crossbell/connect-kit"
|
||||
|
||||
import { Tabs } from "~/components/ui/Tabs"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { Trans, useTranslation } from "~/lib/i18n/client"
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
|
||||
export const HomeActivitiesTabs = () => {
|
||||
const pathname = usePathname()
|
||||
|
|
@ -42,40 +41,5 @@ export const HomeActivitiesTabs = () => {
|
|||
},
|
||||
]
|
||||
|
||||
const features = ["Write", "Own", "Earn"]
|
||||
|
||||
return (
|
||||
<>
|
||||
<UniLink
|
||||
href="/about"
|
||||
className="block bg-zinc-50/70 py-6 rounded-3xl text-center mb-6 space-y-2"
|
||||
>
|
||||
<span className="block text-2xl sm:text-3xl font-bold">
|
||||
{features.map((feature) => (
|
||||
<span
|
||||
key={feature}
|
||||
className={`text-feature text-feature-${feature.toLocaleLowerCase()} block sm:inline-block sm:mr-1 mb-2`}
|
||||
>
|
||||
{t(feature)}
|
||||
{t(".")}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
<span className="block text-zinc-800 text-lg sm:text-xl font-light">
|
||||
<Trans i18n={i18n} i18nKey="description" ns="index">
|
||||
<strong className="font-medium">xLog</strong> is the best{" "}
|
||||
<span className="underline decoration-2 text-yellow-400 font-medium">
|
||||
on-chain
|
||||
</span>{" "}
|
||||
and{" "}
|
||||
<span className="underline decoration-2 text-green-400 font-medium">
|
||||
open-source
|
||||
</span>{" "}
|
||||
blogging community for everyone.
|
||||
</Trans>
|
||||
</span>
|
||||
</UniLink>
|
||||
<Tabs items={tabs} className="border-none text-lg"></Tabs>
|
||||
</>
|
||||
)
|
||||
return <Tabs items={tabs} className="border-none text-lg"></Tabs>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,24 +1,25 @@
|
|||
"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 { Virtuoso } from "react-virtuoso"
|
||||
import { VirtuosoGrid } from "react-virtuoso"
|
||||
|
||||
import { useAccountState, useConnectModal } from "@crossbell/connect-kit"
|
||||
import { Switch } from "@headlessui/react"
|
||||
|
||||
import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
|
||||
import FadeIn from "~/components/common/FadeIn"
|
||||
import { Titles } from "~/components/common/Titles"
|
||||
import PostCover from "~/components/site/PostCover"
|
||||
import PostCover from "~/components/home/PostCover"
|
||||
import { EmptyState } from "~/components/ui/EmptyState"
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { Skeleton } from "~/components/ui/Skeleton"
|
||||
import { Tabs } from "~/components/ui/Tabs"
|
||||
import { Tooltip } from "~/components/ui/Tooltip"
|
||||
import { useDate } from "~/hooks/useDate"
|
||||
import { DEFAULT_AVATAR } from "~/lib/env"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
import { getStorage, setStorage } from "~/lib/storage"
|
||||
|
|
@ -30,59 +31,115 @@ import { useGetFeed } from "~/queries/home"
|
|||
import topics from "../../../data/topics.json"
|
||||
|
||||
const PostCard = ({
|
||||
character,
|
||||
post,
|
||||
simple,
|
||||
isComment,
|
||||
keyword,
|
||||
comment,
|
||||
}: {
|
||||
character?: CharacterEntity
|
||||
post: ExpandedNote
|
||||
simple?: boolean
|
||||
isComment?: boolean
|
||||
keyword?: string
|
||||
comment?: string
|
||||
}) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("common")
|
||||
const date = useDate()
|
||||
|
||||
return (
|
||||
<Link
|
||||
href={`/post/${post.character?.handle}/${post.metadata?.content?.slug}`}
|
||||
href={`/post/${character?.handle}/${post.metadata?.content?.slug}`}
|
||||
className={cn(
|
||||
"xlog-post sm:hover:bg-hover transition-all p-4 ml-10 sm:rounded-xl flex flex-col sm:flex-row items-center hover:opacity-100 group",
|
||||
simple && "opacity-90",
|
||||
isComment ? "bg-zinc-50" : "bg-white",
|
||||
"xlog-post sm:hover:bg-hover transition-all sm:rounded-2xl flex flex-col items-center hover:opacity-100 group border",
|
||||
)}
|
||||
>
|
||||
{!simple && <PostCover cover={post.metadata?.content.cover} />}
|
||||
<div className="flex-1 flex justify-center flex-col w-full min-w-0">
|
||||
<PostCover
|
||||
cover={
|
||||
post.metadata?.content.cover ||
|
||||
character?.metadata?.content?.avatars?.[0] ||
|
||||
DEFAULT_AVATAR
|
||||
}
|
||||
/>
|
||||
<div className="p-5 pt-4 w-full min-w-0 h-[204px] flex flex-col">
|
||||
<div className="flex items-center space-x-2 mb-4">
|
||||
<CharacterFloatCard siteId={character?.handle}>
|
||||
<span
|
||||
className="flex items-center space-x-2 cursor-pointer"
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
window.open(
|
||||
`${getSiteLink({
|
||||
subdomain: character?.handle || "",
|
||||
})}`,
|
||||
)
|
||||
}}
|
||||
>
|
||||
<span className="w-6 h-6 inline-block">
|
||||
<Image
|
||||
className="rounded-full object-cover"
|
||||
src={
|
||||
character?.metadata?.content?.avatars?.[0] || DEFAULT_AVATAR
|
||||
}
|
||||
alt={character?.handle || ""}
|
||||
width="24"
|
||||
height="24"
|
||||
></Image>
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{character?.metadata?.content?.name || character?.handle}
|
||||
</span>
|
||||
</span>
|
||||
</CharacterFloatCard>
|
||||
<Titles characterId={character?.characterId} />
|
||||
<span className="text-zinc-400">·</span>
|
||||
<time
|
||||
dateTime={date.formatToISO(post.createdAt)}
|
||||
className="xlog-post-date whitespace-nowrap text-zinc-400 text-sm"
|
||||
>
|
||||
{t("ago", {
|
||||
time: date.dayjs
|
||||
.duration(
|
||||
date.dayjs(post?.createdAt).diff(date.dayjs(), "minute"),
|
||||
"minute",
|
||||
)
|
||||
.humanize(),
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
{comment && (
|
||||
<div className="font-medium text-zinc-700 mb-2 line-clamp-2">
|
||||
<i className="icon-[mingcute--comment-fill] mr-2" />
|
||||
{comment}
|
||||
</div>
|
||||
)}
|
||||
<h2
|
||||
className={cn(
|
||||
"xlog-post-title font-bold text-zinc-700 line-clamp-2",
|
||||
simple ? "text-xl" : "text-2xl",
|
||||
"xlog-post-title font-bold line-clamp-2 text-xl",
|
||||
comment ? "text-zinc-500" : "text-zinc-700",
|
||||
)}
|
||||
>
|
||||
{post.metadata?.content?.title}
|
||||
</h2>
|
||||
{!simple && (
|
||||
{(!!post.metadata?.content?.tags?.filter(
|
||||
(tag) => tag !== "post" && tag !== "page",
|
||||
).length ||
|
||||
post.stat?.viewDetailCount) && (
|
||||
<div className="xlog-post-meta text-sm text-zinc-400 mt-1 space-x-4 flex items-center mr-8">
|
||||
{!!post.metadata?.content?.tags?.filter(
|
||||
(tag) => tag !== "post" && tag !== "page",
|
||||
).length && (
|
||||
<span className="xlog-post-tags space-x-1 truncate min-w-0">
|
||||
{post.metadata?.content?.tags
|
||||
?.filter((tag) => tag !== "post" && tag !== "page")
|
||||
.map((tag, index) => (
|
||||
<span
|
||||
className="hover:text-zinc-600"
|
||||
key={tag + index}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
router.push(`/tag/${tag}`)
|
||||
}}
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
)}
|
||||
<span className="xlog-post-tags space-x-1 truncate min-w-0">
|
||||
{post.metadata?.content?.tags
|
||||
?.filter((tag) => tag !== "post" && tag !== "page")
|
||||
.map((tag, index) => (
|
||||
<span
|
||||
className="hover:text-zinc-600"
|
||||
key={tag + index}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
router.push(`/tag/${tag}`)
|
||||
}}
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
))}
|
||||
</span>
|
||||
{post.stat?.viewDetailCount && (
|
||||
<span className="xlog-post-views inline-flex items-center">
|
||||
<i className="icon-[mingcute--eye-line] mr-[2px]" />
|
||||
|
|
@ -91,140 +148,45 @@ const PostCard = ({
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={cn(
|
||||
"xlog-post-excerpt mt-3 text-zinc-500",
|
||||
simple ? "line-clamp-1" : "line-clamp-2",
|
||||
)}
|
||||
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>
|
||||
{!comment && (
|
||||
<div
|
||||
className="xlog-post-excerpt mt-2 text-zinc-500 line-clamp-2 text-sm"
|
||||
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>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
const Post = ({
|
||||
post,
|
||||
previousPost,
|
||||
filtering,
|
||||
keyword,
|
||||
}: {
|
||||
post: ExpandedNote
|
||||
previousPost?: ExpandedNote
|
||||
filtering: number
|
||||
keyword?: string
|
||||
}) => {
|
||||
const { t } = useTranslation("common")
|
||||
const date = useDate()
|
||||
let simple = post.characterId === previousPost?.characterId
|
||||
|
||||
if (
|
||||
filtering &&
|
||||
post.metadata?.content?.score?.number !== undefined &&
|
||||
post.metadata.content.score.number <= filtering
|
||||
) {
|
||||
return null
|
||||
}
|
||||
|
||||
const Post = ({ post, keyword }: { post: ExpandedNote; keyword?: string }) => {
|
||||
let isComment
|
||||
if (post.metadata?.content?.tags?.includes("comment") && post.toNote) {
|
||||
isComment = true
|
||||
if (post.toNote?.metadata?.content?.tags?.includes("comment")) {
|
||||
return null
|
||||
}
|
||||
if (
|
||||
!post.metadata?.content?.summary ||
|
||||
getStringLength(post.metadata.content.summary) < 6
|
||||
) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<FadeIn className={cn(simple ? "!mt-4" : "")}>
|
||||
<>
|
||||
{!simple && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<CharacterFloatCard siteId={post.character?.handle}>
|
||||
<Link
|
||||
target="_blank"
|
||||
href={`${getSiteLink({
|
||||
subdomain: post.character?.handle || "",
|
||||
})}`}
|
||||
className="flex items-center space-x-4 cursor-pointer"
|
||||
prefetch={false}
|
||||
>
|
||||
<span className="w-10 h-10 inline-block">
|
||||
<Image
|
||||
className="rounded-full object-cover"
|
||||
src={
|
||||
post.character?.metadata?.content?.avatars?.[0] ||
|
||||
"ipfs://bafkreiabgixxp63pg64moxnsydz7hewmpdkxxi3kdsa4oqv4pb6qvwnmxa"
|
||||
}
|
||||
alt={post.character?.handle || ""}
|
||||
width="40"
|
||||
height="40"
|
||||
></Image>
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{post.character?.metadata?.content?.name ||
|
||||
post.character?.handle}
|
||||
</span>
|
||||
</Link>
|
||||
</CharacterFloatCard>
|
||||
<Titles characterId={post.characterId} />
|
||||
<span className="text-zinc-400">·</span>
|
||||
<time
|
||||
dateTime={date.formatToISO(post.createdAt)}
|
||||
className="xlog-post-date whitespace-nowrap text-zinc-400 text-sm"
|
||||
>
|
||||
{t("ago", {
|
||||
time: date.dayjs
|
||||
.duration(
|
||||
date.dayjs(post?.createdAt).diff(date.dayjs(), "minute"),
|
||||
"minute",
|
||||
)
|
||||
.humanize(),
|
||||
})}
|
||||
</time>
|
||||
</div>
|
||||
)}
|
||||
{isComment && (
|
||||
<Link
|
||||
href={`/post/${post.toNote?.character?.handle}/${
|
||||
(post.toNote as ExpandedNote)?.metadata?.content?.slug
|
||||
}`}
|
||||
className="block p-4 ml-10 font-medium text-zinc-700"
|
||||
>
|
||||
<i className="icon-[mingcute--comment-fill] align-middle mr-2" />
|
||||
<span className="align-middle">
|
||||
{post.metadata?.content?.summary}
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
<PostCard
|
||||
post={isComment ? (post.toNote as ExpandedNote) : post}
|
||||
simple={simple || isComment}
|
||||
isComment={isComment}
|
||||
keyword={keyword}
|
||||
/>
|
||||
</>
|
||||
</FadeIn>
|
||||
<PostCard
|
||||
character={post.character || undefined}
|
||||
post={isComment ? (post.toNote as ExpandedNote) : post}
|
||||
keyword={keyword}
|
||||
comment={isComment ? post.metadata?.content?.summary : undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -314,6 +276,38 @@ export const HomeFeed = ({
|
|||
},
|
||||
]
|
||||
|
||||
const [feedInOne, setFeedInOne] = useState<ExpandedNote[]>([])
|
||||
useEffect(() => {
|
||||
if (feed.data?.pages?.length) {
|
||||
setFeedInOne(
|
||||
feed.data.pages
|
||||
.reduce((acc, cur) => {
|
||||
return acc.concat((cur?.list || []) as ExpandedNote[])
|
||||
}, [] as ExpandedNote[])
|
||||
.filter((post) => {
|
||||
if (
|
||||
aiFiltering &&
|
||||
post.metadata?.content?.score?.number !== undefined &&
|
||||
post.metadata.content.score.number <= 60
|
||||
) {
|
||||
return false
|
||||
} else if (
|
||||
post.toNote?.metadata?.content?.tags?.includes("comment")
|
||||
) {
|
||||
return false
|
||||
} else if (
|
||||
!post.metadata?.content?.summary ||
|
||||
getStringLength(post.metadata.content.summary) < 6
|
||||
) {
|
||||
return false
|
||||
} else {
|
||||
return true
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}, [feed.data?.pages, aiFiltering])
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="space-y-10">
|
||||
|
|
@ -364,35 +358,24 @@ export const HomeFeed = ({
|
|||
<EmptyState />
|
||||
) : (
|
||||
<div className="xlog-posts my-8">
|
||||
<Virtuoso
|
||||
overscan={5}
|
||||
<VirtuosoGrid
|
||||
overscan={2604}
|
||||
endReached={() => feed.hasNextPage && feed.fetchNextPage()}
|
||||
useWindowScroll
|
||||
data={feed.data?.pages}
|
||||
itemContent={(_, posts) => {
|
||||
if (!posts?.list.length) return <div className="h-[1px]"></div>
|
||||
data={feedInOne}
|
||||
totalCount={feed.data?.pages[0]?.count || 0}
|
||||
listClassName="grid gap-6 grid-cols-3"
|
||||
itemContent={(index) => {
|
||||
const post = feedInOne[index]
|
||||
return (
|
||||
<div className="space-y-8 mb-8">
|
||||
{posts?.list.map((post, index, array) => {
|
||||
return (
|
||||
<MemoedPost
|
||||
key={`${post.characterId}-${post.noteId}`}
|
||||
post={post}
|
||||
previousPost={
|
||||
(type === "latest" || type === "topic") &&
|
||||
index >= 1
|
||||
? array[index - 1]
|
||||
: undefined
|
||||
}
|
||||
filtering={aiFiltering ? 60 : 0}
|
||||
keyword={searchParams?.get("q") || undefined}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<MemoedPost
|
||||
key={`${post.characterId}-${post.noteId}`}
|
||||
post={post}
|
||||
keyword={searchParams?.get("q") || undefined}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
></Virtuoso>
|
||||
></VirtuosoGrid>
|
||||
|
||||
{feed.isFetching && feed.hasNextPage && <Loader />}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,26 @@
|
|||
import { Image } from "~/components/ui/Image"
|
||||
|
||||
export default function PostCover({
|
||||
cover,
|
||||
priority,
|
||||
}: {
|
||||
cover?: string
|
||||
priority?: boolean
|
||||
}) {
|
||||
if (!cover) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="xlog-post-cover rounded-t-2xl overflow-hidden flex items-center relative w-full aspect-video border-b">
|
||||
<Image
|
||||
className="object-cover w-full sm:group-hover:scale-105 sm:transition-transform sm:duration-400 sm:ease-in-out"
|
||||
alt="cover"
|
||||
src={cover}
|
||||
width={624}
|
||||
height={351}
|
||||
priority={priority}
|
||||
></Image>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue