feat: use app router for site home
This commit is contained in:
parent
d3f83e3090
commit
5d9194787b
|
|
@ -1,6 +1,5 @@
|
|||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
/// <reference types="next/navigation-types/compat/navigation" />
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/basic-features/typescript for more information.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
{
|
||||
"intlDateTime": "{{val, datetime}}",
|
||||
"ago": "{{time}} ago",
|
||||
"joined ago": "Joined {{time}} ago"
|
||||
"joined ago": "Joined {{time}} ago",
|
||||
"signed and stored on the blockchain": "Ownership of this {{name}} data is guaranteed by blockchain and smart contracts to the creator alone."
|
||||
}
|
||||
|
|
@ -1,4 +1,3 @@
|
|||
{
|
||||
"load more": "There are {{count}} more {{name}}, click to load more",
|
||||
"signed and stored on the blockchain": "Ownership of this {{name}} data is guaranteed by blockchain and smart contracts to the creator alone."
|
||||
"load more": "There are {{count}} more {{name}}, click to load more"
|
||||
}
|
||||
|
|
@ -55,5 +55,6 @@
|
|||
"Showcase": "ショーケース",
|
||||
"AI-generated summary": "AIが生成した要約",
|
||||
"Generating": "生成中",
|
||||
"Show more": "もっと表示する"
|
||||
"Show more": "もっと表示する",
|
||||
"signed and stored on the blockchain": "{{name}}は、創作者によって署名され、ブロックチェーンに安全に保存されています。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
"About": "紹介",
|
||||
"Tags": "タグ",
|
||||
"load more": "まだ{{count}}件の{{name}}があります。",
|
||||
"signed and stored on the blockchain": "{{name}}は、創作者によって署名され、ブロックチェーンに安全に保存されています。",
|
||||
"Write a comment on the blockchain": "ブロックチェーンでコメントする",
|
||||
"powered by": "<span>このサイトは </span><name/><span> によって作動します</span>"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,87 +1,88 @@
|
|||
{
|
||||
"Connect": "連線",
|
||||
"Followers": "粉絲",
|
||||
"Following": "追蹤中",
|
||||
"Followings": "追蹤中",
|
||||
"Follow": "追蹤",
|
||||
"Unfollow": "取消追蹤",
|
||||
"intlDateTime": "{{val, datetime}}",
|
||||
"post": "文章",
|
||||
"posts": "文章",
|
||||
"page": "頁面",
|
||||
"pages": "頁面",
|
||||
"comment": "留言",
|
||||
"comments": "留言",
|
||||
"Comment": "留言",
|
||||
"Comments": "留言",
|
||||
"reply": "回覆",
|
||||
"replies": "回覆",
|
||||
"like": "讚",
|
||||
"likes": "讚",
|
||||
"blog": "部落格",
|
||||
"Owner": "擁有者",
|
||||
"Transaction Hash": "交易雜湊值",
|
||||
"IPFS Address": "IPFS 位址",
|
||||
"BNB Greenfield Address": "BNB Greenfield 位址",
|
||||
"Creation": "創建",
|
||||
"Last Update": "最後更新",
|
||||
"Create Character": "創建身份",
|
||||
"Submit": "提交",
|
||||
"Reply": "回覆",
|
||||
"Cancel Reply": "取消回覆",
|
||||
"Edit": "編輯",
|
||||
"Cancel Edit": "取消編輯",
|
||||
"Confirm Modification": "確認編輯",
|
||||
"ago": "{{time}}前",
|
||||
"joined ago": "{{time}}前加入",
|
||||
"obtained ago": "{{time}}前獲得",
|
||||
"Like": "點讚",
|
||||
"Mint to an NFT": "珍藏為 NFT",
|
||||
"Like successfully": "點讚成功",
|
||||
"Mint successfully": "珍藏完成~",
|
||||
"like stored": "你的點讚已被安全地儲存在區塊鏈上,可以在 <2>Crossbell Scan</2> 上查看",
|
||||
"mint stored": "你已將這篇文章珍藏成 NFT,可以在 <2>xChar</2> 或 <6>Crossbell Scan</6> 上查看",
|
||||
"Got it, thanks!": "知道啦,感恩的心~",
|
||||
"Like List": "按讚列表",
|
||||
"Mint List": "珍藏列表",
|
||||
"Revert": "收回",
|
||||
"Confirm to revert": "確定收回",
|
||||
"like revert": "請確定是否想要收回這個點讚操作?",
|
||||
"Cancel": "算了",
|
||||
"Confirm": "確定",
|
||||
"No Content Yet.": "什麼都沒",
|
||||
"Close": "關閉",
|
||||
"Dashboard": "主控臺",
|
||||
"Copied!": "拷貝完成",
|
||||
"Operator Sign": "簽名授權",
|
||||
"Switch Characters": "切換身份",
|
||||
"Upgrade to Wallet": "升級為錢包認證",
|
||||
"Disconnect": "中斷連線",
|
||||
"Loading": "載入中...",
|
||||
"Showcase": "展示櫃",
|
||||
"AI-generated summary": "AI 生成的摘要",
|
||||
"Generating": "生成中...",
|
||||
"Show more": "顯示更多",
|
||||
"Patron": "贊助",
|
||||
"Become a patron of {{name}}": "成為 {{name}} 的贊助者",
|
||||
"Latest patrons": "最新贊助者",
|
||||
"Latest tipper": "最新贊助者",
|
||||
"You are here to be the first patron.": "你是第一位贊助者。",
|
||||
"You are here to be the first tipper.": "你是第一位贊助者。",
|
||||
"Select a tier": "選擇一個等級",
|
||||
"One-time": "一次性",
|
||||
"Monthly and NFT Rewards": "每月和 NFT 獎勵",
|
||||
"Coming soon": "即將推出",
|
||||
"What is MIRA? Where can I get some?": "MIRA 是什麼?能吃嗎?我在哪裡可以獲得?",
|
||||
"Tip": "贊助",
|
||||
"Tip the post: {{name}}": "贊助文章:{{name}}",
|
||||
"Custom": "自訂",
|
||||
"Mintable": "Mintable",
|
||||
"Successfully followed": "嘿!你已經開始<2>追蹤他們的最新動態</2>囉。",
|
||||
"Enable AI Filtering": "AI 過濾",
|
||||
"Filter out possible low-quality content based on AI ratings.": "基於 AI 評分過濾可能不佳的內容",
|
||||
"Refreshing": "重新載入中",
|
||||
"Search for your interest": "搜你所想",
|
||||
"results": "項結果",
|
||||
"My xLog": "你的 xLog"
|
||||
"Connect": "連線",
|
||||
"Followers": "粉絲",
|
||||
"Following": "追蹤中",
|
||||
"Followings": "追蹤中",
|
||||
"Follow": "追蹤",
|
||||
"Unfollow": "取消追蹤",
|
||||
"intlDateTime": "{{val, datetime}}",
|
||||
"post": "文章",
|
||||
"posts": "文章",
|
||||
"page": "頁面",
|
||||
"pages": "頁面",
|
||||
"comment": "留言",
|
||||
"comments": "留言",
|
||||
"Comment": "留言",
|
||||
"Comments": "留言",
|
||||
"reply": "回覆",
|
||||
"replies": "回覆",
|
||||
"like": "讚",
|
||||
"likes": "讚",
|
||||
"blog": "部落格",
|
||||
"Owner": "擁有者",
|
||||
"Transaction Hash": "交易雜湊值",
|
||||
"IPFS Address": "IPFS 位址",
|
||||
"BNB Greenfield Address": "BNB Greenfield 位址",
|
||||
"Creation": "創建",
|
||||
"Last Update": "最後更新",
|
||||
"Create Character": "創建身份",
|
||||
"Submit": "提交",
|
||||
"Reply": "回覆",
|
||||
"Cancel Reply": "取消回覆",
|
||||
"Edit": "編輯",
|
||||
"Cancel Edit": "取消編輯",
|
||||
"Confirm Modification": "確認編輯",
|
||||
"ago": "{{time}}前",
|
||||
"joined ago": "{{time}}前加入",
|
||||
"obtained ago": "{{time}}前獲得",
|
||||
"Like": "點讚",
|
||||
"Mint to an NFT": "珍藏為 NFT",
|
||||
"Like successfully": "點讚成功",
|
||||
"Mint successfully": "珍藏完成~",
|
||||
"like stored": "你的點讚已被安全地儲存在區塊鏈上,可以在 <2>Crossbell Scan</2> 上查看",
|
||||
"mint stored": "你已將這篇文章珍藏成 NFT,可以在 <2>xChar</2> 或 <6>Crossbell Scan</6> 上查看",
|
||||
"Got it, thanks!": "知道啦,感恩的心~",
|
||||
"Like List": "按讚列表",
|
||||
"Mint List": "珍藏列表",
|
||||
"Revert": "收回",
|
||||
"Confirm to revert": "確定收回",
|
||||
"like revert": "請確定是否想要收回這個點讚操作?",
|
||||
"Cancel": "算了",
|
||||
"Confirm": "確定",
|
||||
"No Content Yet.": "什麼都沒",
|
||||
"Close": "關閉",
|
||||
"Dashboard": "主控臺",
|
||||
"Copied!": "拷貝完成",
|
||||
"Operator Sign": "簽名授權",
|
||||
"Switch Characters": "切換身份",
|
||||
"Upgrade to Wallet": "升級為錢包認證",
|
||||
"Disconnect": "中斷連線",
|
||||
"Loading": "載入中...",
|
||||
"Showcase": "展示櫃",
|
||||
"AI-generated summary": "AI 生成的摘要",
|
||||
"Generating": "生成中...",
|
||||
"Show more": "顯示更多",
|
||||
"Patron": "贊助",
|
||||
"Become a patron of {{name}}": "成為 {{name}} 的贊助者",
|
||||
"Latest patrons": "最新贊助者",
|
||||
"Latest tipper": "最新贊助者",
|
||||
"You are here to be the first patron.": "你是第一位贊助者。",
|
||||
"You are here to be the first tipper.": "你是第一位贊助者。",
|
||||
"Select a tier": "選擇一個等級",
|
||||
"One-time": "一次性",
|
||||
"Monthly and NFT Rewards": "每月和 NFT 獎勵",
|
||||
"Coming soon": "即將推出",
|
||||
"What is MIRA? Where can I get some?": "MIRA 是什麼?能吃嗎?我在哪裡可以獲得?",
|
||||
"Tip": "贊助",
|
||||
"Tip the post: {{name}}": "贊助文章:{{name}}",
|
||||
"Custom": "自訂",
|
||||
"Mintable": "Mintable",
|
||||
"Successfully followed": "嘿!你已經開始<2>追蹤他們的最新動態</2>囉。",
|
||||
"Enable AI Filtering": "AI 過濾",
|
||||
"Filter out possible low-quality content based on AI ratings.": "基於 AI 評分過濾可能不佳的內容",
|
||||
"Refreshing": "重新載入中",
|
||||
"Search for your interest": "搜你所想",
|
||||
"results": "項結果",
|
||||
"My xLog": "你的 xLog",
|
||||
"signed and stored on the blockchain": "此{{name}}數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。"
|
||||
}
|
||||
|
|
@ -4,7 +4,6 @@
|
|||
"About": "關於",
|
||||
"Tags": "標籤",
|
||||
"load more": "還有{{count}}篇{{name}},點選載入更多文章",
|
||||
"signed and stored on the blockchain": "此{{name}}數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。",
|
||||
"Write a comment on the blockchain": "在區塊鏈上留言。",
|
||||
"powered by": "<span>由 </span><name/><span> 提供支援</span>",
|
||||
"This address is in local editing preview mode and cannot be viewed by the public.": "此地址處於本地編輯預覽模式,尚未公開。",
|
||||
|
|
|
|||
|
|
@ -83,5 +83,6 @@
|
|||
"Refreshing": "刷新中",
|
||||
"Search for your interest": "搜索你感兴趣的内容",
|
||||
"results": "条结果",
|
||||
"My xLog": "我的 xLog"
|
||||
"My xLog": "我的 xLog",
|
||||
"signed and stored on the blockchain": "此{{name}}数据所有权由区块链加密技术和智能合约保障仅归创作者所有。"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
"About": "关于",
|
||||
"Tags": "标签",
|
||||
"load more": "还有 {{count}} 篇{{name}},点击加载更多",
|
||||
"signed and stored on the blockchain": "此{{name}}数据所有权由区块链加密技术和智能合约保障仅归创作者所有。",
|
||||
"Write a comment on the blockchain": "在区块链上写下你的评论",
|
||||
"powered by": "<span>由 </span><name/><span> 提供支持</span>",
|
||||
"This address is in local editing preview mode and cannot be viewed by the public.":
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
import { notFound } from "next/navigation"
|
||||
|
||||
import { Hydrate, dehydrate } from "@tanstack/react-query"
|
||||
|
||||
import { BlockchainInfo } from "~/components/common/BlockchainInfo"
|
||||
import { Style } from "~/components/common/Style"
|
||||
import { BackToTopFAB } from "~/components/site/BackToTopFAB"
|
||||
import { SiteFooter } from "~/components/site/SiteFooter"
|
||||
import { SiteHeader } from "~/components/site/SiteHeader"
|
||||
import { FABContainer } from "~/components/ui/FAB"
|
||||
import getQueryClient from "~/lib/query-client"
|
||||
import { ExpandedNote } from "~/lib/types"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { fetchGetPage } from "~/queries/page.server"
|
||||
import {
|
||||
fetchGetSite,
|
||||
prefetchGetSiteSubscriptions,
|
||||
prefetchGetSiteToSubscriptions,
|
||||
} from "~/queries/site.server"
|
||||
|
||||
export default async function SiteLayout({
|
||||
children,
|
||||
params,
|
||||
}: {
|
||||
children?: React.ReactNode
|
||||
params: {
|
||||
site: string
|
||||
page?: string
|
||||
tag?: string
|
||||
}
|
||||
}) {
|
||||
const queryClient = getQueryClient()
|
||||
|
||||
const site = await fetchGetSite(params.site, queryClient)
|
||||
|
||||
let page
|
||||
if (site?.characterId) {
|
||||
const result = await Promise.all([
|
||||
prefetchGetSiteSubscriptions(
|
||||
{
|
||||
characterId: site.characterId,
|
||||
},
|
||||
queryClient,
|
||||
),
|
||||
prefetchGetSiteToSubscriptions(
|
||||
{
|
||||
characterId: site.characterId,
|
||||
},
|
||||
queryClient,
|
||||
),
|
||||
new Promise<ExpandedNote | null>(async (resolve, reject) => {
|
||||
if (params.page) {
|
||||
try {
|
||||
const page = await fetchGetPage(
|
||||
{
|
||||
characterId: site.characterId,
|
||||
slug: params.page,
|
||||
useStat: true,
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
|
||||
if (
|
||||
!page ||
|
||||
new Date(page!.metadata?.content?.date_published || "") >
|
||||
new Date()
|
||||
) {
|
||||
reject(notFound())
|
||||
} else {
|
||||
resolve(page)
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
} else {
|
||||
// if (!options?.skipPages) {
|
||||
// await prefetchGetPagesBySite(
|
||||
// {
|
||||
// characterId: site.characterId,
|
||||
// ...(options?.limit && { limit: options.limit }),
|
||||
// type: "post",
|
||||
// visibility: PageVisibilityEnum.Published,
|
||||
// ...(params.tag && { tags: [params.tag] }),
|
||||
// ...(options?.useStat && {
|
||||
// useStat: true,
|
||||
// }),
|
||||
// },
|
||||
// queryClient,
|
||||
// )
|
||||
// }
|
||||
resolve(null)
|
||||
}
|
||||
}),
|
||||
])
|
||||
page = result[2]
|
||||
} else {
|
||||
notFound()
|
||||
}
|
||||
|
||||
const dehydratedState = dehydrate(queryClient)
|
||||
|
||||
return (
|
||||
<Hydrate state={dehydratedState}>
|
||||
<div
|
||||
className={cn()
|
||||
// TODO
|
||||
// {
|
||||
// "xlog-user": true,
|
||||
// "xlog-user-login": isConnected,
|
||||
// "xlog-user-site-owner": userRole?.data === "owner",
|
||||
// "xlog-user-site-operator": userRole?.data === "operator",
|
||||
// "xlog-user-site-follower": subscription?.data,
|
||||
// "xlog-user-post-liker": isLiked,
|
||||
// "xlog-user-post-minter": isMint?.data?.count,
|
||||
// },
|
||||
// `xlog-page-${type}`,
|
||||
}
|
||||
>
|
||||
{/* <SEOHead
|
||||
title={title || tag || page.data?.metadata?.content?.title || ""}
|
||||
siteName={site.data?.metadata?.content?.name || ""}
|
||||
description={
|
||||
page.data?.metadata?.content?.summary ??
|
||||
site.data?.metadata?.content?.bio?.replace(/<[^>]*>/g, "")
|
||||
}
|
||||
image={
|
||||
page.data?.metadata?.content?.cover ||
|
||||
getUserContentsUrl(site.data?.metadata?.content?.avatars?.[0])
|
||||
}
|
||||
icon={getUserContentsUrl(site.data?.metadata?.content?.avatars?.[0])}
|
||||
site={domainOrSubdomain}
|
||||
/> */}
|
||||
<Style content={site?.metadata?.content?.css} />
|
||||
{site && <SiteHeader site={site} />}
|
||||
<div
|
||||
className={cn(
|
||||
"max-w-screen-md mx-auto px-5 pt-12 relative",
|
||||
page && `xlog-post-id-${page.characterId}-${page.noteId}`,
|
||||
page?.metadata?.content?.tags?.map((tag) => `xlog-post-tag-${tag}`),
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{site && (
|
||||
<div className="max-w-screen-md mx-auto pt-12 pb-10">
|
||||
<BlockchainInfo site={site} page={page || undefined} />
|
||||
</div>
|
||||
)}
|
||||
<SiteFooter site={site || undefined} />
|
||||
|
||||
<FABContainer>
|
||||
<BackToTopFAB />
|
||||
</FABContainer>
|
||||
</div>
|
||||
</Hydrate>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import { Hydrate, dehydrate } from "@tanstack/react-query"
|
||||
|
||||
import SiteHome from "~/components/site/SiteHome"
|
||||
import getQueryClient from "~/lib/query-client"
|
||||
import { PageVisibilityEnum } from "~/lib/types"
|
||||
import { prefetchGetPagesBySite } from "~/queries/page.server"
|
||||
import { fetchGetSite } from "~/queries/site.server"
|
||||
|
||||
async function SiteIndexPage({
|
||||
params,
|
||||
}: {
|
||||
params: {
|
||||
site: string
|
||||
}
|
||||
}) {
|
||||
const queryClient = getQueryClient()
|
||||
|
||||
const site = await fetchGetSite(params.site, queryClient)
|
||||
prefetchGetPagesBySite(
|
||||
{
|
||||
characterId: site?.characterId,
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
useStat: true,
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
|
||||
const dehydratedState = dehydrate(queryClient)
|
||||
|
||||
return (
|
||||
<Hydrate state={dehydratedState}>
|
||||
<SiteHome handle={params.site} />
|
||||
</Hydrate>
|
||||
)
|
||||
}
|
||||
|
||||
export default SiteIndexPage
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
import { useTranslation } from "next-i18next"
|
||||
"use client"
|
||||
|
||||
import { Disclosure } from "@headlessui/react"
|
||||
|
||||
import { BlockchainIcon } from "~/components/icons/BlockchainIcon"
|
||||
import { CSB_SCAN } from "~/lib/env"
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
import { toCid, toGateway, toIPFS } from "~/lib/ipfs-parser"
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
|
@ -13,7 +14,7 @@ export const BlockchainInfo: React.FC<{
|
|||
site?: ExpandedCharacter
|
||||
page?: ExpandedNote
|
||||
}> = ({ site, page }) => {
|
||||
const { t } = useTranslation(["common", "site"])
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
const ipfs = (page ? page.metadata?.uri : site?.metadata?.uri) || ""
|
||||
const greenfieldId = useGetGreenfieldId(toCid(ipfs))
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { throttle } from "~/lib/utils"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,9 @@
|
|||
import { Trans } from "next-i18next"
|
||||
import Script from "next/script"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { Logo } from "~/components/common/Logo"
|
||||
import { Platform } from "~/components/site/Platform"
|
||||
import { SITE_URL } from "~/lib/env"
|
||||
import { Trans } from "~/lib/i18n"
|
||||
import { ExpandedCharacter } from "~/lib/types"
|
||||
|
||||
import { DarkModeSwitch } from "../common/DarkModeSwitch"
|
||||
|
|
@ -13,19 +12,13 @@ import { UniLink } from "../ui/UniLink"
|
|||
export const SiteFooter: React.FC<{
|
||||
site?: ExpandedCharacter
|
||||
}> = ({ site }) => {
|
||||
const [logoType, setLogoType] = useState<"svg" | "png" | "lottie">("svg")
|
||||
|
||||
useEffect(() => {
|
||||
setLogoType("lottie")
|
||||
}, [])
|
||||
|
||||
const LogoWithLink = () => {
|
||||
return (
|
||||
<UniLink
|
||||
href={SITE_URL}
|
||||
className="inline-flex items-center align-text-top mx-1"
|
||||
>
|
||||
<Logo type={logoType} width={20} height={20} autoplay={false} />
|
||||
<Logo type="lottie" width={20} height={20} autoplay={false} />
|
||||
</UniLink>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,8 @@
|
|||
"use client"
|
||||
|
||||
import chroma from "chroma-js"
|
||||
import { FastAverageColor } from "fast-average-color"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
import { usePathname } from "next/navigation"
|
||||
import { MutableRefObject, RefObject, useEffect, useRef, useState } from "react"
|
||||
|
||||
import { XCharLogo, XFeedLogo } from "@crossbell/ui"
|
||||
|
|
@ -17,6 +18,7 @@ import { Modal } from "~/components/ui/Modal"
|
|||
import { Tooltip } from "~/components/ui/Tooltip"
|
||||
import { useIsDark } from "~/hooks/useDarkMode"
|
||||
import { CSB_IO, CSB_SCAN, CSB_XCHAR } from "~/lib/env"
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
import { ExpandedCharacter } from "~/lib/types"
|
||||
import { getUserContentsUrl } from "~/lib/user-contents"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
|
@ -37,9 +39,9 @@ type HeaderLinkType = {
|
|||
const fac = new FastAverageColor()
|
||||
|
||||
const HeaderLink: React.FC<{ link: HeaderLinkType }> = ({ link }) => {
|
||||
const router = useRouter()
|
||||
const pathname = usePathname()
|
||||
const { t } = useTranslation("site")
|
||||
const active = router.asPath === link.url
|
||||
const active = pathname === link.url // TODO
|
||||
return (
|
||||
<UniLink
|
||||
href={link.url}
|
||||
|
|
|
|||
|
|
@ -1,29 +1,30 @@
|
|||
import { useTranslation } from "next-i18next"
|
||||
"use client"
|
||||
|
||||
import Link from "next/link"
|
||||
import { useRouter } from "next/router"
|
||||
import { useRouter } from "next/navigation"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import type { InfiniteData } from "@tanstack/react-query"
|
||||
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { EmptyState } from "~/components/ui/EmptyState"
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { useDate } from "~/hooks/useDate"
|
||||
import { getSlugUrl } from "~/lib/helpers"
|
||||
import { ExpandedNote } from "~/lib/types"
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
import { PageVisibilityEnum } from "~/lib/types"
|
||||
import { useGetPagesBySiteLite } from "~/queries/page"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
|
||||
import { EmptyState } from "../ui/EmptyState"
|
||||
|
||||
export const SiteHome: React.FC<{
|
||||
posts?: InfiniteData<{
|
||||
list: ExpandedNote[]
|
||||
count: number
|
||||
}>
|
||||
fetchNextPage: () => void
|
||||
hasNextPage?: boolean
|
||||
isFetchingNextPage?: boolean
|
||||
}> = ({ posts, fetchNextPage, hasNextPage, isFetchingNextPage }) => {
|
||||
export default function SiteHome({ handle }: { handle: string }) {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation(["common", "site"])
|
||||
const site = useGetSite(handle)
|
||||
const posts = useGetPagesBySiteLite({
|
||||
characterId: site.data?.characterId,
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
useStat: true,
|
||||
})
|
||||
|
||||
const { t } = useTranslation("site")
|
||||
const date = useDate()
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
|
@ -32,16 +33,16 @@ export const SiteHome: React.FC<{
|
|||
setIsMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!posts?.pages?.length) return null
|
||||
if (!posts.data?.pages?.length) return null
|
||||
|
||||
let currentLength = 0
|
||||
|
||||
return (
|
||||
<>
|
||||
{!posts.pages[0].count && <EmptyState />}
|
||||
{!!posts.pages[0].count && (
|
||||
{!posts.data.pages[0].count && <EmptyState />}
|
||||
{!!posts.data.pages[0].count && (
|
||||
<div className="xlog-posts space-y-8">
|
||||
{posts.pages.map((posts) =>
|
||||
{posts.data.pages.map((posts) =>
|
||||
posts.list.map((post) => {
|
||||
currentLength++
|
||||
return (
|
||||
|
|
@ -127,20 +128,21 @@ export const SiteHome: React.FC<{
|
|||
)}
|
||||
</div>
|
||||
)}
|
||||
{hasNextPage && (
|
||||
{posts.hasNextPage && (
|
||||
<Button
|
||||
className="mt-8 w-full bg-zinc-50 text-sm"
|
||||
variant="text"
|
||||
onClick={fetchNextPage}
|
||||
isLoading={isFetchingNextPage}
|
||||
onClick={() => posts.fetchNextPage()}
|
||||
isLoading={posts.isFetchingNextPage}
|
||||
aria-label="load more"
|
||||
>
|
||||
{t("load more", {
|
||||
ns: "site",
|
||||
name: t(
|
||||
"post" + (posts?.pages[0].count - currentLength > 1 ? "s" : ""),
|
||||
"post" +
|
||||
(posts.data?.pages[0].count - currentLength > 1 ? "s" : ""),
|
||||
),
|
||||
count: posts?.pages[0].count - currentLength,
|
||||
count: posts.data?.pages[0].count - currentLength,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
"use client"
|
||||
|
||||
import React, { memo } from "react"
|
||||
|
||||
import { cn } from "~/lib/utils"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
"use client"
|
||||
|
||||
import React, { FC, PropsWithChildren, useEffect, useState } from "react"
|
||||
|
||||
import { useGetState } from "~/hooks/useGetState"
|
||||
|
|
|
|||
|
|
@ -8,9 +8,10 @@ import localizedFormat from "dayjs/plugin/localizedFormat"
|
|||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import tz from "dayjs/plugin/timezone"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
|
||||
dayjs.extend(localizedFormat)
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(tz)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,42 @@
|
|||
import dayjs from "dayjs"
|
||||
import "dayjs/locale/en"
|
||||
import "dayjs/locale/ja"
|
||||
import "dayjs/locale/zh"
|
||||
import "dayjs/locale/zh-tw"
|
||||
import duration from "dayjs/plugin/duration"
|
||||
import localizedFormat from "dayjs/plugin/localizedFormat"
|
||||
import relativeTime from "dayjs/plugin/relativeTime"
|
||||
import tz from "dayjs/plugin/timezone"
|
||||
import utc from "dayjs/plugin/utc"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import { useTranslation } from "~/lib/i18n"
|
||||
|
||||
dayjs.extend(localizedFormat)
|
||||
dayjs.extend(utc)
|
||||
dayjs.extend(tz)
|
||||
dayjs.extend(duration)
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
export async function useDate() {
|
||||
const { i18n } = await useTranslation()
|
||||
|
||||
const memoizedDateUtils = useMemo(() => {
|
||||
dayjs.locale(i18n.resolvedLanguage)
|
||||
|
||||
return {
|
||||
dayjs,
|
||||
formatDate: (date: string | Date, format = "ll", timezone?: string) => {
|
||||
return dayjs(date).tz(timezone).format(format)
|
||||
},
|
||||
formatToISO: (date: string | Date) => {
|
||||
return dayjs(date || undefined).toISOString()
|
||||
},
|
||||
inLocalTimezone: (date: string | Date) => {
|
||||
return dayjs(date).tz().toDate()
|
||||
},
|
||||
}
|
||||
}, [i18n.resolvedLanguage])
|
||||
|
||||
return memoizedDateUtils
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { NoteEntity } from "crossbell.js"
|
||||
import type { NoteEntity } from "crossbell.js"
|
||||
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
|
||||
import { useLang } from "~/hooks/useLang"
|
||||
|
||||
import { getOptions } from "./settings"
|
||||
import { defaultNS, getOptions } from "./settings"
|
||||
|
||||
i18next
|
||||
.use(initReactI18next)
|
||||
|
|
@ -23,7 +23,7 @@ i18next
|
|||
...getOptions(),
|
||||
})
|
||||
|
||||
export function useTranslation(ns: string) {
|
||||
export function useTranslation(ns: string = defaultNS) {
|
||||
const { lang } = useLang()
|
||||
if (i18next.resolvedLanguage !== lang) {
|
||||
i18next.changeLanguage(lang)
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import { initReactI18next } from "react-i18next/initReactI18next"
|
|||
|
||||
import { useAcceptLang } from "~/hooks/useAcceptLang"
|
||||
|
||||
import { getOptions } from "./settings"
|
||||
import { defaultNS, getOptions } from "./settings"
|
||||
|
||||
const initI18next = async (lng: string, ns: string) => {
|
||||
const i18nInstance = createInstance()
|
||||
|
|
@ -21,7 +21,7 @@ const initI18next = async (lng: string, ns: string) => {
|
|||
return i18nInstance
|
||||
}
|
||||
|
||||
export async function useTranslation(ns: string) {
|
||||
export async function useTranslation(ns: string = defaultNS) {
|
||||
const lang = useAcceptLang()
|
||||
const i18nextInstance = await initI18next(lang, ns)
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { CharacterEntity, NoteEntity } from "crossbell.js"
|
||||
import type { CharacterEntity, NoteEntity } from "crossbell.js"
|
||||
|
||||
export type Site = {
|
||||
id: string
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ export default async function middleware(req: NextRequest) {
|
|||
|
||||
if (tenant?.subdomain) {
|
||||
const url = req.nextUrl.clone()
|
||||
url.pathname = `/_site/${tenant?.subdomain}${url.pathname}`
|
||||
url.pathname = `/site/${tenant?.subdomain}${url.pathname}`
|
||||
return NextResponse.rewrite(url)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -371,45 +371,46 @@ export function useGetSummary(input: { cid?: string; lang?: string }) {
|
|||
})
|
||||
}
|
||||
|
||||
export function useGetMirrorXyz(input: { address?: string }) {
|
||||
return useQuery(["getMirror", input.address], async () => {
|
||||
const { getDefaultSlug } = await import("~/lib/default-slug")
|
||||
// TODO
|
||||
// export function useGetMirrorXyz(input: { address?: string }) {
|
||||
// return useQuery(["getMirror", input.address], async () => {
|
||||
// const { getDefaultSlug } = await import("~/lib/default-slug")
|
||||
|
||||
if (!input.address) {
|
||||
return null
|
||||
}
|
||||
const response = await (
|
||||
await fetch(
|
||||
"/api/import/mirror.xyz?" +
|
||||
new URLSearchParams({
|
||||
...input,
|
||||
} as any),
|
||||
)
|
||||
).json()
|
||||
// if (!input.address) {
|
||||
// return null
|
||||
// }
|
||||
// const response = await (
|
||||
// await fetch(
|
||||
// "/api/import/mirror.xyz?" +
|
||||
// new URLSearchParams({
|
||||
// ...input,
|
||||
// } as any),
|
||||
// )
|
||||
// ).json()
|
||||
|
||||
return response?.data?.projectFeed?.posts?.map((post: any) => {
|
||||
return {
|
||||
title: post.title,
|
||||
date_published: new Date(
|
||||
post.publishedAtTimestamp * 1000,
|
||||
).toISOString(),
|
||||
slug: getDefaultSlug(post.title, post.digest),
|
||||
tags: ["Mirror.xyz"],
|
||||
content: post.body,
|
||||
external_urls: [`https://mirror.xyz/${input.address}/${post.digest}`],
|
||||
}
|
||||
}) as {
|
||||
title: string
|
||||
type: string
|
||||
size: number
|
||||
date_published: string
|
||||
slug: string
|
||||
tags: string[]
|
||||
content: string
|
||||
external_urls: string[]
|
||||
}[]
|
||||
})
|
||||
}
|
||||
// return response?.data?.projectFeed?.posts?.map((post: any) => {
|
||||
// return {
|
||||
// title: post.title,
|
||||
// date_published: new Date(
|
||||
// post.publishedAtTimestamp * 1000,
|
||||
// ).toISOString(),
|
||||
// slug: getDefaultSlug(post.title, post.digest),
|
||||
// tags: ["Mirror.xyz"],
|
||||
// content: post.body,
|
||||
// external_urls: [`https://mirror.xyz/${input.address}/${post.digest}`],
|
||||
// }
|
||||
// }) as {
|
||||
// title: string
|
||||
// type: string
|
||||
// size: number
|
||||
// date_published: string
|
||||
// slug: string
|
||||
// tags: string[]
|
||||
// content: string
|
||||
// external_urls: string[]
|
||||
// }[]
|
||||
// })
|
||||
// }
|
||||
|
||||
export function useCheckMirror(characterId?: number) {
|
||||
return useQuery(["checkMirror", characterId], async () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue