feat: topic page
This commit is contained in:
parent
d0ec1b3565
commit
ad0578208a
|
|
@ -56,5 +56,6 @@
|
|||
"Submit yours": "提交你的",
|
||||
"xLog's open design allows it to integrate with many other open protocols and applications without friction.": "xLog 的开放式设计使其能够与许多其他开放协议和应用程序无缝集成,没有任何限制。",
|
||||
"Dashboard": "仪表盘",
|
||||
"Suggested creators for you": "为你推荐的创作者"
|
||||
"Suggested creators for you": "为你推荐的创作者",
|
||||
"Hot Topics": "热门话题"
|
||||
}
|
||||
|
|
@ -127,8 +127,9 @@ const Post = ({
|
|||
}
|
||||
|
||||
export const MainFeed: React.FC<{
|
||||
type?: "latest" | "recommend" | "following"
|
||||
}> = ({ type }) => {
|
||||
type?: "latest" | "recommend" | "following" | "topic"
|
||||
noteIds?: string[]
|
||||
}> = ({ type, noteIds }) => {
|
||||
const { t } = useTranslation(["common", "site"])
|
||||
|
||||
const currentCharacterId = useAccountState(
|
||||
|
|
@ -138,6 +139,7 @@ export const MainFeed: React.FC<{
|
|||
const feed = useGetFeed({
|
||||
type: type,
|
||||
characterId: currentCharacterId,
|
||||
noteIds: noteIds,
|
||||
})
|
||||
|
||||
const hasFiltering = type === "latest"
|
||||
|
|
|
|||
|
|
@ -0,0 +1,130 @@
|
|||
import { useState } from "react"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { useConnectedAction } from "@crossbell/connect-kit"
|
||||
import { useRefCallback } from "@crossbell/util-hooks"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { useGetSites } from "~/queries/site"
|
||||
import showcase from "../../../showcase.json"
|
||||
import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
|
||||
import { useAccountSites, useSubscribeToSites } from "~/queries/site"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import topics from "../../../topics.json"
|
||||
|
||||
export function MainSidebar() {
|
||||
const showcaseSites = useGetSites(showcase)
|
||||
const { t } = useTranslation("index")
|
||||
|
||||
const userSite = useAccountSites()
|
||||
const subscribeToSites = useSubscribeToSites()
|
||||
|
||||
const doSubscribeToSites = useRefCallback(() => {
|
||||
subscribeToSites.mutate({
|
||||
characterIds: showcaseSites.data
|
||||
?.map((s: { characterId?: string }) => s.characterId)
|
||||
.filter(Boolean)
|
||||
.map(Number),
|
||||
siteIds: showcaseSites.data?.map((s: { handle: string }) => s.handle),
|
||||
} as any)
|
||||
})
|
||||
|
||||
const followAll = useConnectedAction(() => {
|
||||
doSubscribeToSites()
|
||||
})
|
||||
|
||||
const [showcaseMore, setShowcaseMore] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="w-80 pl-10 pt-4 hidden sm:block">
|
||||
<div className="text-center">
|
||||
<div className="mb-10 text-zinc-700 space-y-3">
|
||||
<p className="font-bold text-lg">{t("Hot Topics")}</p>
|
||||
<ul className="overflow-y-clip relative text-left space-y-2">
|
||||
{topics.map((topic: any) => (
|
||||
<li className="flex align-middle" key={topic.name}>
|
||||
<UniLink
|
||||
href={`/topic/${topic.name}`}
|
||||
className="flex flex-col sm:hover:bg-hover px-4 py-2 -mx-4 rounded-lg transition-colors"
|
||||
>
|
||||
<span className="font-medium">{topic.name}</span>
|
||||
<span className="text-zinc-400 text-sm">
|
||||
{topic.description}
|
||||
</span>
|
||||
</UniLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-center">
|
||||
<div className="mb-10 text-zinc-700 space-y-3">
|
||||
<p className="font-bold text-lg">{t("Suggested creators for you")}</p>
|
||||
<Button
|
||||
onClick={followAll}
|
||||
isLoading={
|
||||
showcaseSites.isLoading ||
|
||||
userSite.isLoading ||
|
||||
subscribeToSites.isLoading
|
||||
}
|
||||
isDisabled={subscribeToSites.isSuccess}
|
||||
>
|
||||
🥳{" "}
|
||||
{subscribeToSites.isSuccess
|
||||
? t("Already Followed All!")
|
||||
: t("Follow All!")}
|
||||
</Button>
|
||||
<ul
|
||||
className={`overflow-y-clip relative text-left space-y-4 ${
|
||||
showcaseMore ? "" : "max-h-[540px]"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute bottom-0 h-14 left-0 right-0 bg-gradient-to-t from-white via-white flex items-end justify-center font-bold cursor-pointer z-[1] text-sm ${
|
||||
showcaseMore ? "hidden" : ""
|
||||
}`}
|
||||
onClick={() => setShowcaseMore(true)}
|
||||
>
|
||||
{t("Show more")}
|
||||
</div>
|
||||
{showcaseSites.data?.map((site: any) => (
|
||||
<li className="flex align-middle" key={site.handle}>
|
||||
<UniLink
|
||||
href={getSiteLink({
|
||||
subdomain: site.handle,
|
||||
})}
|
||||
className="inline-flex align-middle w-full"
|
||||
>
|
||||
<CharacterFloatCard siteId={site.handle}>
|
||||
<span className="w-10 h-10 inline-block">
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src={
|
||||
site.metadata.content?.avatars?.[0] ||
|
||||
"ipfs://bafkreiabgixxp63pg64moxnsydz7hewmpdkxxi3kdsa4oqv4pb6qvwnmxa"
|
||||
}
|
||||
alt={site.handle}
|
||||
width="40"
|
||||
height="40"
|
||||
></Image>
|
||||
</span>
|
||||
</CharacterFloatCard>
|
||||
<span className="ml-3 min-w-0 flex-1 justify-center inline-flex flex-col">
|
||||
<span className="truncate w-full inline-block font-medium">
|
||||
{site.metadata.content?.name}
|
||||
</span>
|
||||
{site.metadata.content?.bio && (
|
||||
<span className="text-gray-500 text-xs truncate w-full inline-block mt-1">
|
||||
{site.metadata.content?.bio}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</UniLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,6 +1,9 @@
|
|||
import { ExpandedNote } from "~/lib/types"
|
||||
import { indexer } from "@crossbell/indexer"
|
||||
import { toCid } from "~/lib/ipfs-parser"
|
||||
import { createClient } from "@urql/core"
|
||||
import axios from "axios"
|
||||
import { NoteEntity } from "crossbell.js"
|
||||
|
||||
const expandPage = async (
|
||||
page: ExpandedNote,
|
||||
|
|
@ -56,11 +59,13 @@ export async function getFeed({
|
|||
cursor,
|
||||
limit = 10,
|
||||
characterId,
|
||||
noteIds,
|
||||
}: {
|
||||
type?: "latest" | "recommend" | "following"
|
||||
type?: "latest" | "recommend" | "following" | "topic"
|
||||
cursor?: string
|
||||
limit?: number
|
||||
characterId?: number
|
||||
noteIds?: string[]
|
||||
}) {
|
||||
switch (type) {
|
||||
case "latest":
|
||||
|
|
@ -108,7 +113,7 @@ export async function getFeed({
|
|||
)
|
||||
})
|
||||
.map(async (page: any) => {
|
||||
return expandPage(page.note, true)
|
||||
return expandPage(page.note)
|
||||
}),
|
||||
)
|
||||
|
||||
|
|
@ -118,5 +123,68 @@ export async function getFeed({
|
|||
count: result.count,
|
||||
}
|
||||
}
|
||||
case "topic":
|
||||
if (!noteIds) {
|
||||
return {
|
||||
list: [],
|
||||
cursor: "",
|
||||
count: 0,
|
||||
}
|
||||
}
|
||||
const client = createClient({
|
||||
url: "https://indexer.crossbell.io/v1/graphql",
|
||||
})
|
||||
|
||||
const orString = noteIds
|
||||
.map(
|
||||
(note) =>
|
||||
`{ noteId: { equals: ${
|
||||
note.split("-")[1]
|
||||
} }, characterId: { equals: ${note.split("-")[0]}}},`,
|
||||
)
|
||||
.join("\n")
|
||||
const topicResult = await client
|
||||
.query(
|
||||
`
|
||||
query getNotes {
|
||||
notes(
|
||||
where: {
|
||||
OR: [
|
||||
${orString}
|
||||
]
|
||||
},
|
||||
orderBy: [{ createdAt: desc }],
|
||||
take: 1000,
|
||||
) {
|
||||
characterId
|
||||
noteId
|
||||
character {
|
||||
handle
|
||||
metadata {
|
||||
content
|
||||
}
|
||||
}
|
||||
createdAt
|
||||
metadata {
|
||||
uri
|
||||
content
|
||||
}
|
||||
}
|
||||
}`,
|
||||
{},
|
||||
)
|
||||
.toPromise()
|
||||
|
||||
const topicList = await Promise.all(
|
||||
topicResult?.data?.notes.map(async (page: any) => {
|
||||
return expandPage(page)
|
||||
}),
|
||||
)
|
||||
|
||||
return {
|
||||
list: topicList,
|
||||
cursor: "",
|
||||
count: topicList?.length || 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,27 +1,16 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { ReactElement, useState } from "react"
|
||||
import { MainLayout } from "~/components/main/MainLayout"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import {
|
||||
useAccountState,
|
||||
useConnectedAction,
|
||||
useConnectModal,
|
||||
} from "@crossbell/connect-kit"
|
||||
import { useRefCallback } from "@crossbell/util-hooks"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { useAccountState, useConnectModal } from "@crossbell/connect-kit"
|
||||
import { dehydrate, QueryClient } from "@tanstack/react-query"
|
||||
import { prefetchGetSites } from "~/queries/site.server"
|
||||
import { useGetSites } from "~/queries/site"
|
||||
import showcase from "../../showcase.json"
|
||||
import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
|
||||
import { useAccountSites, useSubscribeToSites } from "~/queries/site"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useAccountSites } from "~/queries/site"
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations"
|
||||
import { languageDetector } from "~/lib/language-detector"
|
||||
import { MainFeed } from "~/components/main/MainFeed"
|
||||
import { Tabs } from "~/components/ui/Tabs"
|
||||
import { MainSidebar } from "~/components/main/MainSidebar"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const queryClient = new QueryClient()
|
||||
|
|
@ -40,27 +29,7 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||
}
|
||||
|
||||
function Activities() {
|
||||
const showcaseSites = useGetSites(showcase)
|
||||
const { t } = useTranslation("index")
|
||||
|
||||
const userSite = useAccountSites()
|
||||
const subscribeToSites = useSubscribeToSites()
|
||||
|
||||
const doSubscribeToSites = useRefCallback(() => {
|
||||
subscribeToSites.mutate({
|
||||
characterIds: showcaseSites.data
|
||||
?.map((s: { characterId?: string }) => s.characterId)
|
||||
.filter(Boolean)
|
||||
.map(Number),
|
||||
siteIds: showcaseSites.data?.map((s: { handle: string }) => s.handle),
|
||||
} as any)
|
||||
})
|
||||
|
||||
const followAll = useConnectedAction(() => {
|
||||
doSubscribeToSites()
|
||||
})
|
||||
|
||||
const [showcaseMore, setShowcaseMore] = useState(false)
|
||||
|
||||
const currentCharacterId = useAccountState(
|
||||
(s) => s.computed.account?.characterId,
|
||||
|
|
@ -96,76 +65,7 @@ function Activities() {
|
|||
<Tabs items={tabs} className="border-none text-lg"></Tabs>
|
||||
<MainFeed type={feedType} />
|
||||
</div>
|
||||
<div className="w-80 pl-10 pt-4 hidden sm:block">
|
||||
<div className="text-center">
|
||||
<div className="mb-10 text-zinc-700 space-y-3">
|
||||
<p className="font-medium">{t("Suggested creators for you")}</p>
|
||||
<Button
|
||||
onClick={followAll}
|
||||
isLoading={
|
||||
showcaseSites.isLoading ||
|
||||
userSite.isLoading ||
|
||||
subscribeToSites.isLoading
|
||||
}
|
||||
isDisabled={subscribeToSites.isSuccess}
|
||||
>
|
||||
🥳{" "}
|
||||
{subscribeToSites.isSuccess
|
||||
? t("Already Followed All!")
|
||||
: t("Follow All!")}
|
||||
</Button>
|
||||
<ul
|
||||
className={`overflow-y-clip relative text-left space-y-4 ${
|
||||
showcaseMore ? "" : "max-h-[540px]"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute bottom-0 h-14 left-0 right-0 bg-gradient-to-t from-white via-white flex items-end justify-center font-bold cursor-pointer z-[1] text-sm ${
|
||||
showcaseMore ? "hidden" : ""
|
||||
}`}
|
||||
onClick={() => setShowcaseMore(true)}
|
||||
>
|
||||
{t("Show more")}
|
||||
</div>
|
||||
{showcaseSites.data?.map((site: any) => (
|
||||
<li className="flex align-middle" key={site.handle}>
|
||||
<UniLink
|
||||
href={getSiteLink({
|
||||
subdomain: site.handle,
|
||||
})}
|
||||
className="inline-flex align-middle w-full"
|
||||
>
|
||||
<CharacterFloatCard siteId={site.handle}>
|
||||
<span className="w-10 h-10 inline-block">
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src={
|
||||
site.metadata.content?.avatars?.[0] ||
|
||||
"ipfs://bafkreiabgixxp63pg64moxnsydz7hewmpdkxxi3kdsa4oqv4pb6qvwnmxa"
|
||||
}
|
||||
alt={site.handle}
|
||||
width="40"
|
||||
height="40"
|
||||
></Image>
|
||||
</span>
|
||||
</CharacterFloatCard>
|
||||
<span className="ml-3 min-w-0 flex-1 justify-center inline-flex flex-col">
|
||||
<span className="truncate w-full inline-block font-medium">
|
||||
{site.metadata.content?.name}
|
||||
</span>
|
||||
{site.metadata.content?.bio && (
|
||||
<span className="text-gray-500 text-xs truncate w-full inline-block mt-1">
|
||||
{site.metadata.content?.bio}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</UniLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<MainSidebar />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { ReactElement, useState } from "react"
|
||||
import { MainLayout } from "~/components/main/MainLayout"
|
||||
import { dehydrate, QueryClient } from "@tanstack/react-query"
|
||||
import { prefetchGetSites } from "~/queries/site.server"
|
||||
import showcase from "../../../showcase.json"
|
||||
import topics from "../../../topics.json"
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations"
|
||||
import { languageDetector } from "~/lib/language-detector"
|
||||
import { MainFeed } from "~/components/main/MainFeed"
|
||||
import { MainSidebar } from "~/components/main/MainSidebar"
|
||||
import { useRouter } from "next/router"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
||||
const queryClient = new QueryClient()
|
||||
await prefetchGetSites(showcase, queryClient)
|
||||
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations(languageDetector(ctx), [
|
||||
"common",
|
||||
"index",
|
||||
"dashboard",
|
||||
])),
|
||||
dehydratedState: dehydrate(queryClient),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function Topic() {
|
||||
const router = useRouter()
|
||||
const topic = router.query.topic as string
|
||||
|
||||
const info = topics.find((t) => t.name === topic)
|
||||
|
||||
return (
|
||||
<section className="pt-24">
|
||||
<div className="max-w-screen-lg px-5 mx-auto flex">
|
||||
<div className="flex-1 min-w-[300px]">
|
||||
<h2 className="text-3xl font-bold">Topic: {topic}</h2>
|
||||
<p className="text-zinc-400 mt-4">{info?.description}</p>
|
||||
<div className="mt-10">
|
||||
<MainFeed type="topic" noteIds={info?.notes} />
|
||||
</div>
|
||||
</div>
|
||||
<MainSidebar />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Topic.getLayout = (page: ReactElement) => {
|
||||
return <MainLayout>{page}</MainLayout>
|
||||
}
|
||||
|
||||
export default Topic
|
||||
|
|
@ -1,11 +1,12 @@
|
|||
import { useInfiniteQuery } from "@tanstack/react-query"
|
||||
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"
|
||||
|
||||
import * as homeModel from "~/models/home.model"
|
||||
|
||||
export const useGetFeed = (data?: {
|
||||
type?: "latest" | "recommend" | "following"
|
||||
type?: "latest" | "recommend" | "following" | "topic"
|
||||
characterId?: number
|
||||
limit?: number
|
||||
noteIds?: string[]
|
||||
}) => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["getFeed", data],
|
||||
|
|
|
|||
|
|
@ -0,0 +1,120 @@
|
|||
[
|
||||
{
|
||||
"name": "xLog",
|
||||
"description": "Let's talk about xLog, whether to praise or criticize it.",
|
||||
"notes": [
|
||||
"10-50",
|
||||
"30-24",
|
||||
"30-25",
|
||||
"77-105",
|
||||
"5825-1",
|
||||
"26375-18",
|
||||
"31132-9",
|
||||
"32022-4",
|
||||
"32022-5",
|
||||
"32022-7",
|
||||
"32022-8",
|
||||
"32022-11",
|
||||
"32022-13",
|
||||
"32022-14",
|
||||
"32022-15",
|
||||
"32022-16",
|
||||
"32022-18",
|
||||
"32022-19",
|
||||
"32022-20",
|
||||
"32022-21",
|
||||
"32022-22",
|
||||
"33210-13",
|
||||
"33224-1",
|
||||
"33276-24",
|
||||
"33446-4",
|
||||
"33446-32",
|
||||
"33592-1",
|
||||
"45899-71",
|
||||
"47356-1",
|
||||
"50114-6",
|
||||
"51374-2",
|
||||
"51408-11",
|
||||
"51485-1",
|
||||
"51645-1",
|
||||
"51796-3",
|
||||
"51819-1",
|
||||
"45-176",
|
||||
"40657-1",
|
||||
"50093-1",
|
||||
"50472-2",
|
||||
"51726-1",
|
||||
"51493-1"
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "ChatGPT",
|
||||
"description": "The world is undergoing a fusion, and we are in the center of it.",
|
||||
"notes": [
|
||||
"30-84",
|
||||
"77-41",
|
||||
"723-96",
|
||||
"723-97",
|
||||
"723-98",
|
||||
"723-99",
|
||||
"26375-26",
|
||||
"45899-447",
|
||||
"47328-2",
|
||||
"47328-3",
|
||||
"47345-3",
|
||||
"49120-167",
|
||||
"49213-3",
|
||||
"50143-2",
|
||||
"50622-1",
|
||||
"50622-2",
|
||||
"50622-13",
|
||||
"50622-14",
|
||||
"50708-11",
|
||||
"51068-2",
|
||||
"51072-52",
|
||||
"51124-28",
|
||||
"51134-1",
|
||||
"51154-9",
|
||||
"51191-6",
|
||||
"51191-9",
|
||||
"51191-10",
|
||||
"51302-1",
|
||||
"51302-2",
|
||||
"51356-2",
|
||||
"51356-4",
|
||||
"51494-14",
|
||||
"51539-3",
|
||||
"51683-30",
|
||||
"51683-33",
|
||||
"51683-49",
|
||||
"51683-50",
|
||||
"51718-1",
|
||||
"51784-1",
|
||||
"51996-3",
|
||||
"52003-1",
|
||||
"22033-73",
|
||||
"33276-29",
|
||||
"33492-1",
|
||||
"45701-2",
|
||||
"45828-10",
|
||||
"48952-84",
|
||||
"49041-9",
|
||||
"50140-13",
|
||||
"50161-36",
|
||||
"50701-17",
|
||||
"51061-1",
|
||||
"51068-1",
|
||||
"51068-2",
|
||||
"51139-8",
|
||||
"51197-154",
|
||||
"51350-2",
|
||||
"51379-5",
|
||||
"51408-14",
|
||||
"51460-13",
|
||||
"51478-5",
|
||||
"51478-8",
|
||||
"51481-1",
|
||||
"51494-2"
|
||||
]
|
||||
}
|
||||
]
|
||||
Loading…
Reference in New Issue