feat: topic page

This commit is contained in:
DIYgod 2023-03-31 18:43:44 +01:00
parent d0ec1b3565
commit ad0578208a
No known key found for this signature in database
8 changed files with 389 additions and 111 deletions

View File

@ -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": "热门话题"
}

View File

@ -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"

View File

@ -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>
)
}

View File

@ -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,
}
}
}

View File

@ -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>
)

View File

@ -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

View File

@ -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],

120
topics.json Normal file
View File

@ -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"
]
}
]