)
}
-
-ImportPage.getLayout = (page: ReactElement) => {
- return {page}
-}
diff --git a/src/components/dashboard/DashboardLayout.tsx b/src/app/dashboard/[subdomain]/layout.tsx
similarity index 94%
rename from src/components/dashboard/DashboardLayout.tsx
rename to src/app/dashboard/[subdomain]/layout.tsx
index 4105789e..8b7d5f64 100644
--- a/src/components/dashboard/DashboardLayout.tsx
+++ b/src/app/dashboard/[subdomain]/layout.tsx
@@ -1,4 +1,6 @@
-import { useRouter } from "next/router"
+"use client"
+
+import { useParams, usePathname } from "next/navigation"
import React, { useEffect } from "react"
import { useTranslation } from "react-i18next"
@@ -10,10 +12,13 @@ import {
import { ConnectButton } from "~/components/common/ConnectButton"
import { Logo } from "~/components/common/Logo"
+import { DashboardSidebar } from "~/components/dashboard/DashboardSidebar"
+import { DashboardTopbar } from "~/components/dashboard/DashboardTopbar"
import { Avatar } from "~/components/ui/Avatar"
+import { UniLink } from "~/components/ui/UniLink"
import { useIsMobileLayout } from "~/hooks/useMobileLayout"
import { useUserRole } from "~/hooks/useUserRole"
-import { APP_NAME, DISCORD_LINK } from "~/lib/env"
+import { DISCORD_LINK } from "~/lib/env"
import { getSiteLink } from "~/lib/helpers"
import { toGateway } from "~/lib/ipfs-parser"
import { getStorage } from "~/lib/storage"
@@ -21,20 +26,15 @@ import { cn } from "~/lib/utils"
import { useGetPagesBySite } from "~/queries/page"
import { useGetSite } from "~/queries/site"
-import { SEOHead } from "../common/SEOHead"
-import { UniLink } from "../ui/UniLink"
-import { DashboardSidebar } from "./DashboardSidebar"
-import { DashboardTopbar } from "./DashboardTopbar"
-
-export function DashboardLayout({
+export default function DashboardLayout({
children,
title,
}: {
children: React.ReactNode
title: string
}) {
- const router = useRouter()
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const site = useGetSite(subdomain)
const userRole = useUserRole(subdomain)
@@ -48,6 +48,7 @@ export function DashboardLayout({
const { t } = useTranslation("dashboard")
const isMobileLayout = useIsMobileLayout()
+ const pathname = usePathname()
useEffect(() => {
if (ssrReady) {
@@ -92,7 +93,7 @@ export function DashboardLayout({
const links: {
href?: string
onClick?: () => void
- isActive: (ctx: { href: string; pathname: string }) => boolean
+ isActive: (ctx: { href: string; pathname: string | null }) => boolean
icon: React.ReactNode
text: string
}[] = [
@@ -131,7 +132,7 @@ export function DashboardLayout({
{
href: `/dashboard/${subdomain}/import`,
isActive: ({ pathname }) =>
- pathname.startsWith(`/dashboard/${subdomain}/import`),
+ !!pathname?.startsWith(`/dashboard/${subdomain}/import`),
icon: "icon-[mingcute--file-import-line]",
text: "Import",
},
@@ -158,7 +159,7 @@ export function DashboardLayout({
{
href: `/dashboard/${subdomain}/settings/general`,
isActive: ({ pathname }) =>
- pathname.startsWith(`/dashboard/${subdomain}/settings`),
+ !!pathname?.startsWith(`/dashboard/${subdomain}/settings`),
icon: "icon-[mingcute--settings-3-line]",
text: "Settings",
},
@@ -167,7 +168,8 @@ export function DashboardLayout({
return ready ? (
hasPermission ? (
<>
-
+ {/* TODO */}
+ {/* */}
{site?.data?.metadata?.content?.css && (
{
- const { props: layoutProps } = await getLayoutServerSideProps(ctx)
-
- return {
- props: {
- ...layoutProps,
- },
- }
- },
-)
-
export default function SubdomainIndex() {
- const router = useRouter()
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const site = useGetSite(subdomain)
const characterId = site.data?.characterId
const stat = useGetStat({
@@ -280,7 +265,3 @@ export default function SubdomainIndex() {
)
}
-
-SubdomainIndex.getLayout = (page: ReactElement) => {
- return {page}
-}
diff --git a/src/app/dashboard/[subdomain]/pages/page.tsx b/src/app/dashboard/[subdomain]/pages/page.tsx
new file mode 100644
index 00000000..e4b17db4
--- /dev/null
+++ b/src/app/dashboard/[subdomain]/pages/page.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import { PagesManager } from "~/components/dashboard/PagesManager"
+
+export default function SubdomainPages() {
+ return
+}
diff --git a/src/app/dashboard/[subdomain]/posts/page.tsx b/src/app/dashboard/[subdomain]/posts/page.tsx
new file mode 100644
index 00000000..2ba92c48
--- /dev/null
+++ b/src/app/dashboard/[subdomain]/posts/page.tsx
@@ -0,0 +1,7 @@
+"use client"
+
+import { PagesManager } from "~/components/dashboard/PagesManager"
+
+export default function SubdomainPosts() {
+ return
+}
diff --git a/src/pages/dashboard/[subdomain]/settings/css.tsx b/src/app/dashboard/[subdomain]/settings/css/page.tsx
similarity index 81%
rename from src/pages/dashboard/[subdomain]/settings/css.tsx
rename to src/app/dashboard/[subdomain]/settings/css/page.tsx
index 09b19555..3e4dcff0 100644
--- a/src/pages/dashboard/[subdomain]/settings/css.tsx
+++ b/src/app/dashboard/[subdomain]/settings/css/page.tsx
@@ -1,34 +1,19 @@
-import { GetServerSideProps } from "next"
-import { useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
+"use client"
+
+import { useParams } from "next/navigation"
import { useEffect, useState } from "react"
-import type { ReactElement } from "react"
import toast from "react-hot-toast"
import { MonacoEditor } from "~/components/common/Monaco"
-import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
import { SettingsLayout } from "~/components/dashboard/SettingsLayout"
import { Button } from "~/components/ui/Button"
import { FieldLabel } from "~/components/ui/FieldLabel"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
+import { useTranslation } from "~/lib/i18n/client"
import { useGetSite, useUpdateSite } from "~/queries/site"
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const { props: layoutProps } = await getLayoutServerSideProps(ctx)
-
- return {
- props: {
- ...layoutProps,
- },
- }
- },
-)
-
export default function SettingsCSSPage() {
- const router = useRouter()
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const updateSite = useUpdateSite()
const site = useGetSite(subdomain)
@@ -132,7 +117,3 @@ export default function SettingsCSSPage() {
)
}
-
-SettingsCSSPage.getLayout = (page: ReactElement) => {
- return {page}
-}
diff --git a/src/pages/dashboard/[subdomain]/settings/domains.tsx b/src/app/dashboard/[subdomain]/settings/domains/page.tsx
similarity index 91%
rename from src/pages/dashboard/[subdomain]/settings/domains.tsx
rename to src/app/dashboard/[subdomain]/settings/domains/page.tsx
index 91989d6b..c762564b 100644
--- a/src/pages/dashboard/[subdomain]/settings/domains.tsx
+++ b/src/app/dashboard/[subdomain]/settings/domains/page.tsx
@@ -1,8 +1,7 @@
-import { GetServerSideProps } from "next"
-import { useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
+"use client"
+
+import { useParams, useRouter } from "next/navigation"
import { useEffect, useState } from "react"
-import type { ReactElement } from "react"
import { useForm } from "react-hook-form"
import toast from "react-hot-toast"
@@ -11,8 +10,6 @@ import {
useUpgradeEmailAccountModal,
} from "@crossbell/connect-kit"
-import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
import { SettingsLayout } from "~/components/dashboard/SettingsLayout"
import { Button } from "~/components/ui/Button"
import { Input } from "~/components/ui/Input"
@@ -20,30 +17,19 @@ import { UniLink } from "~/components/ui/UniLink"
import { useUserRole } from "~/hooks/useUserRole"
import { OUR_DOMAIN } from "~/lib/env"
import { getSiteLink } from "~/lib/helpers"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
+import { useTranslation } from "~/lib/i18n/client"
import { checkDomain, getSite } from "~/models/site.model"
import { useGetSite, useUpdateSite } from "~/queries/site"
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const { props: layoutProps } = await getLayoutServerSideProps(ctx)
-
- return {
- props: {
- ...layoutProps,
- },
- }
- },
-)
-
export default function SettingsDomainsPage() {
- const router = useRouter()
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const updateSite = useUpdateSite()
const site = useGetSite(subdomain)
const userRole = useUserRole(subdomain)
const { t } = useTranslation("dashboard")
+ const router = useRouter()
const isEmailAccount = useAccountState(
(s) => s.computed.account?.type === "email",
@@ -312,7 +298,3 @@ export default function SettingsDomainsPage() {
)
}
-
-SettingsDomainsPage.getLayout = (page: ReactElement) => {
- return {page}
-}
diff --git a/src/pages/dashboard/[subdomain]/settings/general.tsx b/src/app/dashboard/[subdomain]/settings/general/page.tsx
similarity index 88%
rename from src/pages/dashboard/[subdomain]/settings/general.tsx
rename to src/app/dashboard/[subdomain]/settings/general/page.tsx
index e7ca85a1..5382820c 100644
--- a/src/pages/dashboard/[subdomain]/settings/general.tsx
+++ b/src/app/dashboard/[subdomain]/settings/general/page.tsx
@@ -1,38 +1,22 @@
-import { GetServerSideProps } from "next"
-import { Trans, useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
+"use client"
+
+import { useParams } from "next/navigation"
import { useEffect, useState } from "react"
-import type { ReactElement } from "react"
import { Controller, useForm } from "react-hook-form"
import toast from "react-hot-toast"
-import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
import { SettingsLayout } from "~/components/dashboard/SettingsLayout"
import { Button } from "~/components/ui/Button"
import { ImageUploader } from "~/components/ui/ImageUploader"
import { Input } from "~/components/ui/Input"
import { UniLink } from "~/components/ui/UniLink"
+import { Trans, useTranslation } from "~/lib/i18n/client"
import { toIPFS } from "~/lib/ipfs-parser"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
import { useGetSite, useUpdateSite } from "~/queries/site"
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const { props: layoutProps } = await getLayoutServerSideProps(ctx)
-
- return {
- props: {
- ...layoutProps,
- },
- }
- },
-)
-
export default function SiteSettingsGeneralPage() {
- const router = useRouter()
-
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const updateSite = useUpdateSite()
const site = useGetSite(subdomain)
@@ -224,8 +208,8 @@ export default function SiteSettingsGeneralPage() {
help={
- Integrate Umami Cloud Analytics into your site. You can follow the
- instructions{" "}
+ Integrate Umami Cloud Analytics into your site. You can follow
+ the instructions{" "}
)
}
-
-SiteSettingsGeneralPage.getLayout = (page: ReactElement) => {
- return {page}
-}
diff --git a/src/pages/dashboard/[subdomain]/settings/navigation.tsx b/src/app/dashboard/[subdomain]/settings/navigation/page.tsx
similarity index 87%
rename from src/pages/dashboard/[subdomain]/settings/navigation.tsx
rename to src/app/dashboard/[subdomain]/settings/navigation/page.tsx
index 66a53933..d1bc9b2e 100644
--- a/src/pages/dashboard/[subdomain]/settings/navigation.tsx
+++ b/src/app/dashboard/[subdomain]/settings/navigation/page.tsx
@@ -1,34 +1,19 @@
+"use client"
+
import equal from "fast-deep-equal"
import { nanoid } from "nanoid"
-import { GetServerSideProps } from "next"
-import { useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
+import { useParams } from "next/navigation"
import { ChangeEvent, FormEvent, useEffect, useMemo, useState } from "react"
-import type { ReactElement } from "react"
import toast from "react-hot-toast"
import { ReactSortable } from "react-sortablejs"
-import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
import { SettingsLayout } from "~/components/dashboard/SettingsLayout"
import { Button } from "~/components/ui/Button"
import { Input } from "~/components/ui/Input"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
+import { useTranslation } from "~/lib/i18n/client"
import { SiteNavigationItem } from "~/lib/types"
import { useGetSite, useUpdateSite } from "~/queries/site"
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const { props: layoutProps } = await getLayoutServerSideProps(ctx)
-
- return {
- props: {
- ...layoutProps,
- },
- }
- },
-)
-
type UpdateItem = (id: string, newItem: Partial) => void
type RemoveItem = (id: string) => void
@@ -80,9 +65,8 @@ const SortableNavigationItem: React.FC<{
}
export default function SiteSettingsNavigationPage() {
- const router = useRouter()
-
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const updateSite = useUpdateSite()
const site = useGetSite(subdomain)
@@ -211,7 +195,3 @@ export default function SiteSettingsNavigationPage() {
)
}
-
-SiteSettingsNavigationPage.getLayout = (page: ReactElement) => {
- return {page}
-}
diff --git a/src/pages/dashboard/[subdomain]/settings/operator.tsx b/src/app/dashboard/[subdomain]/settings/operator/page.tsx
similarity index 89%
rename from src/pages/dashboard/[subdomain]/settings/operator.tsx
rename to src/app/dashboard/[subdomain]/settings/operator/page.tsx
index 5311402a..cdf4d545 100644
--- a/src/pages/dashboard/[subdomain]/settings/operator.tsx
+++ b/src/app/dashboard/[subdomain]/settings/operator/page.tsx
@@ -1,8 +1,7 @@
-import { GetServerSideProps } from "next"
-import { useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
+"use client"
+
+import { useParams } from "next/navigation"
import { useEffect, useState } from "react"
-import type { ReactElement } from "react"
import toast from "react-hot-toast"
import {
@@ -12,15 +11,13 @@ import {
import { Dialog } from "@headlessui/react"
import { CharacterCard } from "~/components/common/CharacterCard"
-import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
import { SettingsLayout } from "~/components/dashboard/SettingsLayout"
import { Button } from "~/components/ui/Button"
import { Input } from "~/components/ui/Input"
import { UniLink } from "~/components/ui/UniLink"
import { useUserRole } from "~/hooks/useUserRole"
import { getSiteLink } from "~/lib/helpers"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
+import { useTranslation } from "~/lib/i18n/client"
import {
useAddOperator,
useGetOperators,
@@ -28,18 +25,6 @@ import {
useRemoveOperator,
} from "~/queries/site"
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const { props: layoutProps } = await getLayoutServerSideProps(ctx)
-
- return {
- props: {
- ...layoutProps,
- },
- }
- },
-)
-
type RemoveItem = (operator: string) => void
const SortableNavigationItem: React.FC<{
@@ -78,9 +63,8 @@ const SortableNavigationItem: React.FC<{
}
export default function SettingsOperatorPage() {
- const router = useRouter()
-
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const addOperator = useAddOperator()
const removeOperator = useRemoveOperator()
@@ -275,7 +259,3 @@ export default function SettingsOperatorPage() {
)
}
-
-SettingsOperatorPage.getLayout = (page: ReactElement) => {
- return {page}
-}
diff --git a/src/pages/dashboard/[subdomain]/settings/social-platforms.tsx b/src/app/dashboard/[subdomain]/settings/social-platforms/page.tsx
similarity index 88%
rename from src/pages/dashboard/[subdomain]/settings/social-platforms.tsx
rename to src/app/dashboard/[subdomain]/settings/social-platforms/page.tsx
index 68ab7a18..1995bb3c 100644
--- a/src/pages/dashboard/[subdomain]/settings/social-platforms.tsx
+++ b/src/app/dashboard/[subdomain]/settings/social-platforms/page.tsx
@@ -1,35 +1,20 @@
+"use client"
+
import equal from "fast-deep-equal"
import { nanoid } from "nanoid"
-import { GetServerSideProps } from "next"
-import { Trans, useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
+import { useParams } from "next/navigation"
import { ChangeEvent, FormEvent, useEffect, useMemo, useState } from "react"
-import type { ReactElement } from "react"
import toast from "react-hot-toast"
import { ReactSortable } from "react-sortablejs"
-import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
import { SettingsLayout } from "~/components/dashboard/SettingsLayout"
import { Platform } from "~/components/site/Platform"
import { Button } from "~/components/ui/Button"
import { Input } from "~/components/ui/Input"
import { UniLink } from "~/components/ui/UniLink"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
+import { Trans, useTranslation } from "~/lib/i18n/client"
import { useGetSite, useUpdateSite } from "~/queries/site"
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const { props: layoutProps } = await getLayoutServerSideProps(ctx)
-
- return {
- props: {
- ...layoutProps,
- },
- }
- },
-)
-
type Item = {
identity: string
platform: string
@@ -90,9 +75,8 @@ const SortableNavigationItem: React.FC<{
}
export default function SiteSettingsNavigationPage() {
- const router = useRouter()
-
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const updateSite = useUpdateSite()
const site = useGetSite(subdomain)
@@ -251,7 +235,3 @@ export default function SiteSettingsNavigationPage() {
)
}
-
-SiteSettingsNavigationPage.getLayout = (page: ReactElement) => {
- return {page}
-}
diff --git a/src/pages/dashboard/[subdomain]/tokens.tsx b/src/app/dashboard/[subdomain]/tokens/page.tsx
similarity index 81%
rename from src/pages/dashboard/[subdomain]/tokens.tsx
rename to src/app/dashboard/[subdomain]/tokens/page.tsx
index 0754053c..bd1c4547 100644
--- a/src/pages/dashboard/[subdomain]/tokens.tsx
+++ b/src/app/dashboard/[subdomain]/tokens/page.tsx
@@ -1,7 +1,6 @@
-import { GetServerSideProps } from "next"
-import { useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
-import type { ReactElement } from "react"
+"use client"
+
+import { useParams } from "next/navigation"
import {
useAccountBalance,
@@ -9,32 +8,19 @@ import {
useWalletClaimCSBModal,
} from "@crossbell/connect-kit"
-import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
import { DashboardMain } from "~/components/dashboard/DashboardMain"
import { Button } from "~/components/ui/Button"
import { UniLink } from "~/components/ui/UniLink"
import { MIRA_LINK } from "~/lib/env"
import { getSiteLink } from "~/lib/helpers"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
+import { useTranslation } from "~/lib/i18n/client"
import { useGetMiraBalance, useGetSite } from "~/queries/site"
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const { props: layoutProps } = await getLayoutServerSideProps(ctx)
-
- return {
- props: {
- ...layoutProps,
- },
- }
- },
-)
-
export default function TokensPage() {
- const router = useRouter()
- const { t } = useTranslation(["dashboard", "index"])
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const { t } = useTranslation("dashboard")
+ const { t: indexT } = useTranslation("index")
+ const subdomain = params?.subdomain as string
const site = useGetSite(subdomain)
const miraBalance = useGetMiraBalance(site.data?.characterId)
@@ -138,7 +124,7 @@ export default function TokensPage() {
- {t("features.Earn.description", { ns: "index" })}
+ {indexT("features.Earn.description")}
{tokens.map((token) => {
return (
@@ -161,7 +147,3 @@ export default function TokensPage() {
)
}
-
-TokensPage.getLayout = (page: ReactElement) => {
- return
{page}
-}
diff --git a/src/pages/dashboard/index.tsx b/src/app/dashboard/page.tsx
similarity index 97%
rename from src/pages/dashboard/index.tsx
rename to src/app/dashboard/page.tsx
index 8500f5c6..465d981c 100644
--- a/src/pages/dashboard/index.tsx
+++ b/src/app/dashboard/page.tsx
@@ -1,4 +1,6 @@
-import { useRouter } from "next/router"
+"use client"
+
+import { useRouter } from "next/navigation"
import { useEffect, useRef } from "react"
import {
diff --git a/src/app/feed/hottest/route.ts b/src/app/feed/hottest/route.ts
new file mode 100644
index 00000000..672dc679
--- /dev/null
+++ b/src/app/feed/hottest/route.ts
@@ -0,0 +1,28 @@
+import { SITE_URL } from "~/lib/env"
+import { parsePost } from "~/lib/json-feed"
+import { NextServerResponse } from "~/lib/server-helper"
+import { ExpandedNote } from "~/lib/types"
+import { getFeed } from "~/models/home.model"
+
+export async function GET(request: Request) {
+ const searchParams = new URLSearchParams(request.url.split("?")[1])
+
+ const feed = await getFeed({
+ type: "hot",
+ daysInterval: parseInt(searchParams.get("interval") || "0"),
+ })
+
+ const data = {
+ version: "https://jsonfeed.org/version/1",
+ title: "xLog Latest",
+ icon: "https://ipfs.4everland.xyz/ipfs/bafkreigxdnr5lvtjxqin5upquomrti2s77hlgtjy5zaeu43uhpny75rbga",
+ home_page_url: `${SITE_URL}/activities`,
+ feed_url: `${SITE_URL}/feed/latest`,
+ items: feed?.list?.map((post: ExpandedNote) => parsePost(post)),
+ }
+
+ const format = searchParams.get("format") === "xml" ? "xml" : "json"
+
+ const res = new NextServerResponse()
+ return res.status(200).rss(data, format)
+}
diff --git a/src/pages/feed/latest.tsx b/src/app/feed/latest/route.ts
similarity index 54%
rename from src/pages/feed/latest.tsx
rename to src/app/feed/latest/route.ts
index 50266bc0..8c692b61 100644
--- a/src/pages/feed/latest.tsx
+++ b/src/app/feed/latest/route.ts
@@ -1,15 +1,10 @@
-// @ts-ignore
-import jsonfeedToRSS from "jsonfeed-to-rss"
-import { GetServerSideProps } from "next"
-
import { SITE_URL } from "~/lib/env"
-import { parsePost, setHeader } from "~/lib/json-feed"
+import { parsePost } from "~/lib/json-feed"
+import { NextServerResponse } from "~/lib/server-helper"
import { ExpandedNote } from "~/lib/types"
import { getFeed } from "~/models/home.model"
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- setHeader(ctx)
-
+export async function GET(request: Request) {
const feed = await getFeed({
type: "latest",
})
@@ -23,16 +18,11 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
items: feed?.list?.map((post: ExpandedNote) => parsePost(post)),
}
- ctx.res.write(
- ctx.query.format === "xml" ? jsonfeedToRSS(data) : JSON.stringify(data),
- )
- ctx.res.end()
+ const format =
+ new URLSearchParams(request.url.split("?")[1]).get("format") === "xml"
+ ? "xml"
+ : "json"
- return {
- props: {},
- }
+ const res = new NextServerResponse()
+ return res.status(200).rss(data, format)
}
-
-const LatestFeed: React.FC = () => null
-
-export default LatestFeed
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
new file mode 100644
index 00000000..2562a84e
--- /dev/null
+++ b/src/app/layout.tsx
@@ -0,0 +1,88 @@
+import "aplayer-react/dist/index.css"
+import { dir } from "i18next"
+import { Metadata } from "next"
+import { Toaster } from "react-hot-toast"
+
+import "~/css/main.css"
+import { useAcceptLang } from "~/hooks/useAcceptLang"
+import { APP_DESCRIPTION, APP_NAME, APP_SLOGAN, SITE_URL } from "~/lib/env"
+
+import Providers from "./providers"
+
+export const metadata: Metadata = {
+ title: `${APP_NAME} - ${APP_SLOGAN}`,
+ description: APP_DESCRIPTION,
+ applicationName: APP_NAME,
+ generator: APP_NAME,
+ keywords: [
+ "blog",
+ "xlog",
+ "blockchain",
+ "ethereum",
+ "web3",
+ "dapp",
+ "crypto",
+ ],
+ themeColor: "#ffffff",
+ alternates: {
+ types: {
+ "application/rss+xml": [
+ { url: "/feed/latest?format=xml", title: "xLog Latest" },
+ { url: "/feed/hottest?interval=0&format=xml", title: "xLog Hottest" },
+ {
+ url: "/feed/hottest?interval=1&format=xml",
+ title: "xLog Hottest of the Day",
+ },
+ {
+ url: "/feed/hottest?interval=7&format=xml",
+ title: "xLog Hottest of the Week",
+ },
+ {
+ url: "/feed/hottest?interval=30&format=xml",
+ title: "xLog Hottest of the Month",
+ },
+ ],
+ "application/feed+json": [
+ { url: "/feed/latest", title: "xLog Latest" },
+ { url: "/feed/hottest?interval=0", title: "xLog Hottest" },
+ { url: "/feed/hottest?interval=1", title: "xLog Hottest of the Day" },
+ { url: "/feed/hottest?interval=7", title: "xLog Hottest of the Week" },
+ {
+ url: "/feed/hottest?interval=30",
+ title: "xLog Hottest of the Month",
+ },
+ ],
+ },
+ },
+ icons: `${SITE_URL}/assets/logo.svg`,
+ openGraph: {
+ siteName: `${APP_NAME} - ${APP_SLOGAN}`,
+ description: APP_DESCRIPTION,
+ images: [`${SITE_URL}/assets/logo.svg`],
+ },
+ twitter: {
+ card: "summary_large_image",
+ title: `${APP_NAME} - ${APP_SLOGAN}`,
+ description: APP_DESCRIPTION,
+ images: [`${SITE_URL}/assets/logo.svg`],
+ site: "@_xLog",
+ creator: "@_xLog",
+ },
+}
+
+export default function RootLayout({
+ children,
+}: {
+ children: React.ReactNode
+}) {
+ const lang = useAcceptLang()
+
+ return (
+
+
+
{children}
+
+
+
+ )
+}
diff --git a/src/pages/_app.tsx b/src/app/providers.tsx
similarity index 54%
rename from src/pages/_app.tsx
rename to src/app/providers.tsx
index 35dd12e0..a11f4069 100644
--- a/src/pages/_app.tsx
+++ b/src/app/providers.tsx
@@ -1,9 +1,6 @@
-import "aplayer-react/dist/index.css"
-import { Network } from "crossbell.js"
-import { appWithTranslation } from "next-i18next"
-import NextNProgress from "nextjs-progressbar"
-import { Toaster } from "react-hot-toast"
-import { AppPropsWithLayout } from "types/next"
+"use client"
+
+import { useState } from "react"
import { WagmiConfig, createClient } from "wagmi"
import {
@@ -14,30 +11,21 @@ import {
NotificationModal,
NotificationModalColorScheme,
} from "@crossbell/notification"
-import { Hydrate, QueryClient } from "@tanstack/react-query"
+import { QueryClient } from "@tanstack/react-query"
import { PersistQueryClientProvider } from "@tanstack/react-query-persist-client"
-import "~/css/main.css"
// eslint-disable-next-line import/no-unresolved
import { useDarkMode } from "~/hooks/useDarkMode"
import { useMobileLayout } from "~/hooks/useMobileLayout"
-import { APP_NAME, IPFS_GATEWAY } from "~/lib/env"
+import { useNProgress } from "~/hooks/useNProgress"
+import { APP_NAME } from "~/lib/env"
import { toGateway } from "~/lib/ipfs-parser"
import { createIDBPersister } from "~/lib/persister.client"
import { urlComposer } from "~/lib/url-composer"
-
-Network.setIpfsGateway(IPFS_GATEWAY)
+import { LangProvider } from "~/providers/LangProvider"
const wagmiClient = createClient(getDefaultClientConfig({ appName: APP_NAME }))
-const queryClient = new QueryClient({
- defaultOptions: {
- queries: {
- cacheTime: 1000 * 60 * 60 * 24, // 24 hours
- },
- },
-})
-
const persister = createIDBPersister()
const colorScheme: NotificationModalColorScheme = {
@@ -47,11 +35,27 @@ const colorScheme: NotificationModalColorScheme = {
border: `var(--border-color)`,
}
-function MyApp({ Component, pageProps }: AppPropsWithLayout) {
- const getLayout = Component.getLayout ?? ((page) => page)
-
+export default function Providers({
+ children,
+ lang,
+}: {
+ children: React.ReactNode
+ lang: string
+}) {
useDarkMode()
useMobileLayout()
+ useNProgress()
+
+ const [queryClient] = useState(
+ () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ cacheTime: 1000 * 60 * 60 * 24, // 24 hours
+ },
+ },
+ }),
+ )
return (
@@ -78,31 +82,10 @@ function MyApp({ Component, pageProps }: AppPropsWithLayout) {
signInStrategy="simple"
ignoreWalletDisconnectEvent={true}
>
-
- {/* */}
-
- {getLayout()}
-
-
-
+ {children}
+
)
}
-
-// Only uncomment this method if you have blocking data requirements for
-// every single page in your application. This disables the ability to
-// perform automatic static optimization, causing every page in your app to
-// be server-side rendered.
-//
-// MyApp.getInitialProps = async (appContext) => {
-// // calls page's `getInitialProps` and fills `appProps.pageProps`
-// const appProps = await App.getInitialProps(appContext);
-//
-// return { ...appProps }
-// }
-
-export default appWithTranslation(MyApp)
diff --git a/src/app/robots.ts b/src/app/robots.ts
new file mode 100644
index 00000000..5a741fd4
--- /dev/null
+++ b/src/app/robots.ts
@@ -0,0 +1,11 @@
+import { MetadataRoute } from "next"
+
+export default function robots(): MetadataRoute.Robots {
+ return {
+ rules: {
+ userAgent: "*",
+ allow: "/",
+ disallow: ["/dashboard/", "/preview/", "/api/"],
+ },
+ }
+}
diff --git a/src/app/site/[site]/[slug]/page.tsx b/src/app/site/[site]/[slug]/page.tsx
new file mode 100644
index 00000000..06b94f37
--- /dev/null
+++ b/src/app/site/[site]/[slug]/page.tsx
@@ -0,0 +1,103 @@
+import { Metadata } from "next"
+import { notFound } from "next/navigation"
+
+import { Hydrate, dehydrate } from "@tanstack/react-query"
+
+import { SitePage } from "~/components/site/SitePage"
+import { SITE_URL } from "~/lib/env"
+import { useTranslation } from "~/lib/i18n"
+import getQueryClient from "~/lib/query-client"
+import { fetchGetPage } from "~/queries/page.server"
+import { fetchGetSite } from "~/queries/site.server"
+
+export async function generateMetadata({
+ params,
+}: {
+ params: {
+ site: string
+ slug: string
+ }
+}): Promise
{
+ const queryClient = getQueryClient()
+
+ const site = await fetchGetSite(params.site, queryClient)
+
+ const page = await fetchGetPage(
+ {
+ characterId: site?.characterId,
+ slug: params.slug,
+ useStat: true,
+ },
+ queryClient,
+ )
+
+ const title = `${page?.metadata?.content?.title} - ${
+ site?.metadata?.content?.name || site?.handle
+ }`
+ const description = page?.metadata?.content?.summary
+ const siteImages =
+ site?.metadata?.content?.avatars || `${SITE_URL}/assets/logo.svg`
+ const images = page?.metadata?.content?.cover || siteImages
+ const twitterCreator =
+ "@" +
+ site?.metadata?.content?.connected_accounts
+ ?.find((account) => account?.endsWith?.("@twitter"))
+ ?.match(/csb:\/\/account:([^@]+)@twitter/)?.[1]
+
+ return {
+ title,
+ description,
+ openGraph: {
+ siteName: title,
+ description,
+ images,
+ },
+ twitter: {
+ card: "summary_large_image",
+ title,
+ description,
+ images,
+ site: "@_xLog",
+ creator: twitterCreator,
+ },
+ }
+}
+
+export default async function SitePagePage({
+ params,
+}: {
+ params: {
+ site: string
+ slug: string
+ }
+}) {
+ const queryClient = getQueryClient()
+
+ const site = await fetchGetSite(params.site, queryClient)
+
+ const page = await fetchGetPage(
+ {
+ characterId: site?.characterId,
+ slug: params.slug,
+ useStat: true,
+ },
+ queryClient,
+ )
+
+ if (
+ !page ||
+ new Date(page!.metadata?.content?.date_published || "") > new Date()
+ ) {
+ notFound()
+ }
+
+ const dehydratedState = dehydrate(queryClient)
+
+ const { t } = await useTranslation("site")
+
+ return (
+
+
+
+ )
+}
diff --git a/src/app/site/[site]/archives/page.tsx b/src/app/site/[site]/archives/page.tsx
new file mode 100644
index 00000000..eb95025c
--- /dev/null
+++ b/src/app/site/[site]/archives/page.tsx
@@ -0,0 +1,56 @@
+import { Metadata } from "next"
+
+import { Hydrate, dehydrate } from "@tanstack/react-query"
+
+import { SiteArchives } from "~/components/site/SiteArchives"
+import getQueryClient from "~/lib/query-client"
+import { PageVisibilityEnum } from "~/lib/types"
+import { prefetchGetPagesBySite } from "~/queries/page.server"
+import { fetchGetSite } from "~/queries/site.server"
+
+export async function generateMetadata({
+ params,
+}: {
+ params: {
+ site: string
+ }
+}): Promise {
+ const queryClient = getQueryClient()
+
+ const site = await fetchGetSite(params.site, queryClient)
+
+ const title = `Archives - ${site?.metadata?.content?.name || site?.handle}`
+
+ return {
+ title,
+ }
+}
+
+export default async function SiteArchivesPage({
+ params,
+}: {
+ params: {
+ site: string
+ }
+}) {
+ const queryClient = getQueryClient()
+
+ const site = await fetchGetSite(params.site, queryClient)
+ await prefetchGetPagesBySite(
+ {
+ characterId: site?.characterId,
+ type: "post",
+ visibility: PageVisibilityEnum.Published,
+ limit: 100,
+ },
+ queryClient,
+ )
+
+ const dehydratedState = dehydrate(queryClient)
+
+ return (
+
+
+
+ )
+}
diff --git a/src/app/site/[site]/layout.tsx b/src/app/site/[site]/layout.tsx
new file mode 100644
index 00000000..59e46ceb
--- /dev/null
+++ b/src/app/site/[site]/layout.tsx
@@ -0,0 +1,154 @@
+import { Metadata } from "next"
+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 { SITE_URL } from "~/lib/env"
+import getQueryClient from "~/lib/query-client"
+import { cn } from "~/lib/utils"
+import {
+ fetchGetSite,
+ prefetchGetSiteSubscriptions,
+ prefetchGetSiteToSubscriptions,
+} from "~/queries/site.server"
+
+export async function generateMetadata({
+ params,
+}: {
+ params: {
+ site: string
+ }
+}): Promise {
+ const queryClient = getQueryClient()
+
+ const site = await fetchGetSite(params.site, queryClient)
+
+ const title = site?.metadata?.content?.name || site?.handle
+ const description = site?.metadata?.content?.bio
+ const images =
+ site?.metadata?.content?.avatars || `${SITE_URL}/assets/logo.svg`
+ const twitterCreator =
+ "@" +
+ site?.metadata?.content?.connected_accounts
+ ?.find((account) => account?.endsWith?.("@twitter"))
+ ?.match(/csb:\/\/account:([^@]+)@twitter/)?.[1]
+
+ return {
+ title,
+ description,
+ themeColor: "#ffffff", // TODO
+ alternates: {
+ types: {
+ "application/rss+xml": [
+ { url: "/feed?format=xml", title },
+ { url: "/feed/comments?format=xml", title: `Comments on ${title}` },
+ ],
+ "application/feed+json": [
+ { url: "/feed", title },
+ { url: "/feed/comments", title: `Comments on ${title}` },
+ ],
+ },
+ },
+ icons: images,
+ openGraph: {
+ siteName: title,
+ description,
+ images,
+ },
+ twitter: {
+ card: "summary_large_image",
+ title,
+ description,
+ images,
+ site: "@_xLog",
+ creator: twitterCreator,
+ },
+ }
+}
+
+export default async function SiteLayout({
+ children,
+ params,
+}: {
+ children?: React.ReactNode
+ params: {
+ site: string
+ slug?: string
+ tag?: string
+ }
+}) {
+ const queryClient = getQueryClient()
+
+ const site = await fetchGetSite(params.site, queryClient)
+
+ let page
+ if (site?.characterId) {
+ await Promise.all([
+ prefetchGetSiteSubscriptions(
+ {
+ characterId: site.characterId,
+ },
+ queryClient,
+ ),
+ prefetchGetSiteToSubscriptions(
+ {
+ characterId: site.characterId,
+ },
+ queryClient,
+ ),
+ ])
+ } else {
+ notFound()
+ }
+
+ const dehydratedState = dehydrate(queryClient)
+
+ return (
+
+
+
+ {site &&
}
+
`xlog-post-tag-${tag}`),
+ )}
+ >
+ {children}
+
+ {site && (
+
+
+
+ )}
+
+
+
+
+
+
+
+ )
+}
diff --git a/src/app/site/[site]/nft/page.tsx b/src/app/site/[site]/nft/page.tsx
new file mode 100644
index 00000000..8613cedb
--- /dev/null
+++ b/src/app/site/[site]/nft/page.tsx
@@ -0,0 +1,77 @@
+import { Metadata } from "next"
+import Script from "next/script"
+import type { Asset } from "unidata.js"
+
+import { UniLink } from "~/components/ui/UniLink"
+import { UniMedia } from "~/components/ui/UniMedia"
+import { useTranslation } from "~/lib/i18n"
+import getQueryClient from "~/lib/query-client"
+import { fetchGetSite, getNFTs } from "~/queries/site.server"
+
+export async function generateMetadata({
+ params,
+}: {
+ params: {
+ site: string
+ }
+}): Promise {
+ const queryClient = getQueryClient()
+
+ const site = await fetchGetSite(params.site, queryClient)
+
+ const title = `NFT - ${site?.metadata?.content?.name || site?.handle}`
+
+ return {
+ title,
+ }
+}
+
+export default async function SiteNFTPage({
+ params,
+}: {
+ params: {
+ site: string
+ }
+}) {
+ const queryClient = getQueryClient()
+
+ const site = await fetchGetSite(params.site, queryClient)
+ const { t } = await useTranslation("common")
+
+ const nfts = await getNFTs(site?.owner)
+
+ return (
+ <>
+
+ NFT {t("Showcase")}
+
+
+ {nfts.list
+ ?.filter((nft: Asset) => nft.items?.[0]?.address)
+ .map((nft: Asset) => (
+
+
+
+ {nft.name}
+
+
+ ))}
+
+
+ >
+ )
+}
diff --git a/src/app/site/[site]/not-found.tsx b/src/app/site/[site]/not-found.tsx
new file mode 100644
index 00000000..cc29b9d5
--- /dev/null
+++ b/src/app/site/[site]/not-found.tsx
@@ -0,0 +1,27 @@
+import { SitePage } from "~/components/site/SitePage"
+import { SITE_URL } from "~/lib/env"
+import { useTranslation } from "~/lib/i18n"
+
+export default async function NotFound() {
+ const { t } = await useTranslation("site")
+
+ return (
+
+ )
+}
diff --git a/src/app/site/[site]/page.tsx b/src/app/site/[site]/page.tsx
new file mode 100644
index 00000000..359a02a0
--- /dev/null
+++ b/src/app/site/[site]/page.tsx
@@ -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)
+ await prefetchGetPagesBySite(
+ {
+ characterId: site?.characterId,
+ type: "post",
+ visibility: PageVisibilityEnum.Published,
+ useStat: true,
+ },
+ queryClient,
+ )
+
+ const dehydratedState = dehydrate(queryClient)
+
+ return (
+
+
+
+ )
+}
+
+export default SiteIndexPage
diff --git a/src/app/site/[site]/preview/[previewId]/page.tsx b/src/app/site/[site]/preview/[previewId]/page.tsx
new file mode 100644
index 00000000..96f672bf
--- /dev/null
+++ b/src/app/site/[site]/preview/[previewId]/page.tsx
@@ -0,0 +1,37 @@
+"use client"
+
+import { SitePage } from "~/components/site/SitePage"
+import { useTranslation } from "~/lib/i18n/client"
+import { useGetPage } from "~/queries/page"
+import { useGetSite } from "~/queries/site"
+
+export default function SitePreviewPage({
+ params,
+}: {
+ params: {
+ site: string
+ previewId: string
+ }
+}) {
+ const site = useGetSite(params.site)
+
+ const page = useGetPage({
+ characterId: site.data?.characterId,
+ noteId:
+ params.previewId && /\d+/.test(params.previewId)
+ ? +params.previewId
+ : undefined,
+ slug: params.previewId,
+ useStat: true,
+ })
+
+ const { t } = useTranslation("site")
+
+ return (
+
+ )
+}
diff --git a/src/app/site/[site]/search/page.tsx b/src/app/site/[site]/search/page.tsx
new file mode 100644
index 00000000..99269945
--- /dev/null
+++ b/src/app/site/[site]/search/page.tsx
@@ -0,0 +1,41 @@
+import { Metadata } from "next"
+
+import { SearchInput } from "~/components/common/SearchInput"
+import { SiteSearch } from "~/components/site/SiteSearch"
+import getQueryClient from "~/lib/query-client"
+import { fetchGetSite } from "~/queries/site.server"
+
+export async function generateMetadata({
+ params,
+ searchParams,
+}: {
+ params: {
+ site: string
+ }
+ searchParams: {
+ [key: string]: string | string[] | undefined
+ }
+}): Promise {
+ const queryClient = getQueryClient()
+
+ const site = await fetchGetSite(params.site, queryClient)
+
+ const title = `Search: ${searchParams.q} - ${
+ site?.metadata?.content?.name || site?.handle
+ }`
+
+ return {
+ title,
+ }
+}
+
+export default async function SiteSearchPage() {
+ return (
+ <>
+
+
+
+
+ >
+ )
+}
diff --git a/src/app/site/[site]/tag/[tag]/page.tsx b/src/app/site/[site]/tag/[tag]/page.tsx
new file mode 100644
index 00000000..98a6e308
--- /dev/null
+++ b/src/app/site/[site]/tag/[tag]/page.tsx
@@ -0,0 +1,63 @@
+import { Metadata } from "next"
+
+import { Hydrate, dehydrate } from "@tanstack/react-query"
+
+import { SiteArchives } from "~/components/site/SiteArchives"
+import getQueryClient from "~/lib/query-client"
+import { PageVisibilityEnum } from "~/lib/types"
+import { prefetchGetPagesBySite } from "~/queries/page.server"
+import { fetchGetSite } from "~/queries/site.server"
+
+export async function generateMetadata({
+ params,
+}: {
+ params: {
+ site: string
+ tag: string
+ }
+}): Promise {
+ const queryClient = getQueryClient()
+
+ const site = await fetchGetSite(params.site, queryClient)
+
+ params.tag = decodeURIComponent(params.tag)
+ const title = `Tag: ${params.tag} - ${
+ site?.metadata?.content?.name || site?.handle
+ }`
+
+ return {
+ title,
+ }
+}
+
+export default async function SiteTagPage({
+ params,
+}: {
+ params: {
+ site: string
+ tag: string
+ }
+}) {
+ params.tag = decodeURIComponent(params.tag)
+ const queryClient = getQueryClient()
+
+ const site = await fetchGetSite(params.site, queryClient)
+ await prefetchGetPagesBySite(
+ {
+ characterId: site?.characterId,
+ type: "post",
+ visibility: PageVisibilityEnum.Published,
+ limit: 100,
+ tags: [params.tag],
+ },
+ queryClient,
+ )
+
+ const dehydratedState = dehydrate(queryClient)
+
+ return (
+
+
+
+ )
+}
diff --git a/src/components/common/AchievementItem.tsx b/src/components/common/AchievementItem.tsx
index cf26507f..1009bae2 100644
--- a/src/components/common/AchievementItem.tsx
+++ b/src/components/common/AchievementItem.tsx
@@ -1,4 +1,3 @@
-import { useTranslation } from "next-i18next"
import { useState } from "react"
import { Indicator } from "@mantine/core"
@@ -6,6 +5,7 @@ import { Indicator } from "@mantine/core"
import { AchievementModal } from "~/components/common/AchievementModal"
import { Image } from "~/components/ui/Image"
import { useDate } from "~/hooks/useDate"
+import { useTranslation } from "~/lib/i18n/client"
import type { AchievementSection } from "~/models/site.model"
export const Badge = ({
diff --git a/src/components/common/AchievementModal.tsx b/src/components/common/AchievementModal.tsx
index 40fb81a3..7dfc4c94 100644
--- a/src/components/common/AchievementModal.tsx
+++ b/src/components/common/AchievementModal.tsx
@@ -1,4 +1,3 @@
-import { useTranslation } from "next-i18next"
import Tilt from "react-parallax-tilt"
import { Modal, Stepper } from "@mantine/core"
@@ -8,6 +7,7 @@ import { BlockchainIcon } from "~/components/icons/BlockchainIcon"
import { Button } from "~/components/ui/Button"
import { Image } from "~/components/ui/Image"
import { useDate } from "~/hooks/useDate"
+import { useTranslation } from "~/lib/i18n/client"
import { cn } from "~/lib/utils"
import type { AchievementSection } from "~/models/site.model"
import { useMintAchievement } from "~/queries/site"
diff --git a/src/components/common/BlockchainInfo.tsx b/src/components/common/BlockchainInfo.tsx
index 5cec7ccb..1e83faa0 100644
--- a/src/components/common/BlockchainInfo.tsx
+++ b/src/components/common/BlockchainInfo.tsx
@@ -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))
diff --git a/src/components/common/CharacterCard.tsx b/src/components/common/CharacterCard.tsx
index 1d3f85bf..db108847 100644
--- a/src/components/common/CharacterCard.tsx
+++ b/src/components/common/CharacterCard.tsx
@@ -1,10 +1,9 @@
-import { useTranslation } from "next-i18next"
-
import { FollowingButton } from "~/components/common/FollowingButton"
import { FollowingCount } from "~/components/common/FollowingCount"
import { Titles } from "~/components/common/Titles"
import { Avatar } from "~/components/ui/Avatar"
import { useDate } from "~/hooks/useDate"
+import { useTranslation } from "~/lib/i18n/client"
import { cn } from "~/lib/utils"
import { useGetCharacterCard } from "~/queries/site"
diff --git a/src/components/common/CharacterFloatCard.tsx b/src/components/common/CharacterFloatCard.tsx
index 1d2fe6b3..e57e5aa9 100644
--- a/src/components/common/CharacterFloatCard.tsx
+++ b/src/components/common/CharacterFloatCard.tsx
@@ -1,3 +1,5 @@
+"use client"
+
import { useState } from "react"
import {
diff --git a/src/components/common/CharacterList.tsx b/src/components/common/CharacterList.tsx
index 9d7d9eb9..cba044fc 100644
--- a/src/components/common/CharacterList.tsx
+++ b/src/components/common/CharacterList.tsx
@@ -1,8 +1,8 @@
-import { useTranslation } from "next-i18next"
import React, { useCallback, useState } from "react"
import { Virtuoso } from "react-virtuoso"
import { Modal } from "~/components/ui/Modal"
+import { useTranslation } from "~/lib/i18n/client"
import { ExpandedCharacter } from "~/lib/types"
import { Button } from "../ui/Button"
diff --git a/src/components/common/Comment.tsx b/src/components/common/Comment.tsx
index 0ceb39c3..ecd67b1e 100644
--- a/src/components/common/Comment.tsx
+++ b/src/components/common/Comment.tsx
@@ -1,8 +1,10 @@
-import { useTranslation } from "next-i18next"
+"use client"
+
import { Virtuoso } from "react-virtuoso"
import { CommentInput } from "~/components/common/CommentInput"
import { CommentItem } from "~/components/common/CommentItem"
+import { useTranslation } from "~/lib/i18n/client"
import { ExpandedNote } from "~/lib/types"
import { cn } from "~/lib/utils"
import { useGetComments } from "~/queries/page"
diff --git a/src/components/common/CommentInput.tsx b/src/components/common/CommentInput.tsx
index d6d6a397..69a467b6 100644
--- a/src/components/common/CommentInput.tsx
+++ b/src/components/common/CommentInput.tsx
@@ -1,5 +1,4 @@
import { CharacterEntity, NoteEntity } from "crossbell.js"
-import { useTranslation } from "next-i18next"
import { useEffect } from "react"
import { useForm } from "react-hook-form"
@@ -9,6 +8,7 @@ import { Popover } from "@headlessui/react"
import { Avatar } from "~/components/ui/Avatar"
import { Button } from "~/components/ui/Button"
import { Input } from "~/components/ui/Input"
+import { useTranslation } from "~/lib/i18n/client"
import { useCommentPage, useUpdateComment } from "~/queries/page"
import { EmojiPicker } from "./EmojiPicker"
@@ -33,7 +33,7 @@ export const CommentInput: React.FC<{
const account = useAccountState((s) => s.computed.account)
const commentPage = useCommentPage()
const updateComment = useUpdateComment()
- const { t } = useTranslation(["common", "site"])
+ const { t } = useTranslation("site")
const form = useForm({
defaultValues: {
@@ -94,9 +94,7 @@ export const CommentInput: React.FC<{
multiline
maxLength={600}
className="mb-2"
- placeholder={
- t("Write a comment on the blockchain", { ns: "site" }) || ""
- }
+ placeholder={t("Write a comment on the blockchain") || ""}
{...form.register("content", {})}
/>
diff --git a/src/components/common/CommentItem.tsx b/src/components/common/CommentItem.tsx
index 44b9818d..9132ed14 100644
--- a/src/components/common/CommentItem.tsx
+++ b/src/components/common/CommentItem.tsx
@@ -1,5 +1,6 @@
+"use client"
+
import { CharacterEntity, NoteEntity } from "crossbell.js"
-import { useTranslation } from "next-i18next"
import { useState } from "react"
import { useAccountState } from "@crossbell/connect-kit"
@@ -16,6 +17,7 @@ import { UniLink } from "~/components/ui/UniLink"
import { useDate } from "~/hooks/useDate"
import { CSB_SCAN } from "~/lib/env"
import { getSiteLink } from "~/lib/helpers"
+import { useTranslation } from "~/lib/i18n/client"
import { cn } from "~/lib/utils"
export const CommentItem: React.FC<{
diff --git a/src/components/common/ConnectButton.tsx b/src/components/common/ConnectButton.tsx
index c062171b..b39a6263 100644
--- a/src/components/common/ConnectButton.tsx
+++ b/src/components/common/ConnectButton.tsx
@@ -1,4 +1,5 @@
-import { useTranslation } from "next-i18next"
+"use client"
+
import React, { useEffect, useState } from "react"
import {
@@ -31,6 +32,7 @@ import { Button, type Variant, type VariantColor } from "~/components/ui/Button"
import { Menu } from "~/components/ui/Menu"
import { SITE_URL } from "~/lib/env"
import { getSiteLink } from "~/lib/helpers"
+import { useTranslation } from "~/lib/i18n/client"
import { cn } from "~/lib/utils"
type HeaderLinkType = {
diff --git a/src/components/common/DarkModeSwitch.tsx b/src/components/common/DarkModeSwitch.tsx
index 42365463..988dce9c 100644
--- a/src/components/common/DarkModeSwitch.tsx
+++ b/src/components/common/DarkModeSwitch.tsx
@@ -1,3 +1,5 @@
+"use client"
+
import { Switch } from "@headlessui/react"
import { useDarkModeSwitch, useIsDark } from "~/hooks/useDarkMode"
diff --git a/src/components/common/FollowAllButton.tsx b/src/components/common/FollowAllButton.tsx
index 779c9fa4..1eee793d 100644
--- a/src/components/common/FollowAllButton.tsx
+++ b/src/components/common/FollowAllButton.tsx
@@ -1,10 +1,12 @@
-import { useTranslation } from "next-i18next"
+"use client"
+
import { useState } from "react"
import { useAccountState } from "@crossbell/connect-kit"
import { useRefCallback } from "@crossbell/util-hooks"
import { Button } from "~/components/ui/Button"
+import { useTranslation } from "~/lib/i18n/client"
import { getSubscriptionsFromList } from "~/models/site.model"
import { useSubscribeToSites } from "~/queries/site"
diff --git a/src/components/common/FollowingButton.tsx b/src/components/common/FollowingButton.tsx
index 6b69dbe2..31328bdb 100644
--- a/src/components/common/FollowingButton.tsx
+++ b/src/components/common/FollowingButton.tsx
@@ -1,4 +1,3 @@
-import { Trans, useTranslation } from "next-i18next"
import { useEffect } from "react"
import { toast } from "react-hot-toast"
@@ -6,6 +5,7 @@ import { Button } from "~/components/ui/Button"
import type { Variant } from "~/components/ui/Button"
import { UniLink } from "~/components/ui/UniLink"
import { SITE_URL } from "~/lib/env"
+import { Trans, useTranslation } from "~/lib/i18n/client"
import { ExpandedCharacter } from "~/lib/types"
import { cn } from "~/lib/utils"
import {
diff --git a/src/components/common/FollowingCount.tsx b/src/components/common/FollowingCount.tsx
index 6e24e51c..36c25dc8 100644
--- a/src/components/common/FollowingCount.tsx
+++ b/src/components/common/FollowingCount.tsx
@@ -1,8 +1,8 @@
-import { useTranslation } from "next-i18next"
import { useState } from "react"
import { CharacterList } from "~/components/common/CharacterList"
import { Button } from "~/components/ui/Button"
+import { useTranslation } from "~/lib/i18n/client"
import {
useGetSiteSubscriptions,
useGetSiteToSubscriptions,
diff --git a/src/components/common/Logo.tsx b/src/components/common/Logo.tsx
index 5771a165..6f22aeb7 100644
--- a/src/components/common/Logo.tsx
+++ b/src/components/common/Logo.tsx
@@ -1,3 +1,5 @@
+"use client"
+
import Lottie, { type LottieRefCurrentProps } from "lottie-react"
import React, { useRef } from "react"
diff --git a/src/components/common/PageContent.tsx b/src/components/common/PageContent.tsx
index ceba87d0..9e057898 100644
--- a/src/components/common/PageContent.tsx
+++ b/src/components/common/PageContent.tsx
@@ -1,9 +1,10 @@
+"use client"
+
import { MutableRefObject, useEffect, useMemo } from "react"
-import { scroller } from "react-scroll"
import { PostToc } from "~/components/site/PostToc"
import { useCodeCopy } from "~/hooks/useCodeCopy"
-import { cn } from "~/lib/utils"
+import { calculateElementTop, cn } from "~/lib/utils"
import { renderPageContent } from "~/markdown"
export const PageContent: React.FC<{
@@ -42,10 +43,18 @@ export const PageContent: React.FC<{
const hashChangeHandler = () => {
const hash = decodeURIComponent(location.hash.slice(1))
if (hash) {
- scroller.scrollTo(`user-content-${decodeURIComponent(hash)}`, {
- smooth: true,
- offset: -20,
- duration: 500,
+ if (history.state?.preventScrollToToc) {
+ history.state.preventScrollToToc = false
+ return
+ }
+ const targetElement = document.querySelector(
+ `#user-content-${decodeURIComponent(hash)}`,
+ ) as HTMLElement
+ if (!targetElement) return
+
+ window.scrollTo({
+ top: calculateElementTop(targetElement) - 20,
+ behavior: "smooth",
})
}
}
diff --git a/src/components/common/PatronButton.tsx b/src/components/common/PatronButton.tsx
index 8a4e566a..5892c07f 100644
--- a/src/components/common/PatronButton.tsx
+++ b/src/components/common/PatronButton.tsx
@@ -1,8 +1,8 @@
-import { useTranslation } from "next-i18next"
import { useState } from "react"
import { PatronModal } from "~/components/common/PatronModal"
import { Button } from "~/components/ui/Button"
+import { useTranslation } from "~/lib/i18n/client"
import { ExpandedCharacter } from "~/lib/types"
import { cn } from "~/lib/utils"
diff --git a/src/components/common/PatronModal.tsx b/src/components/common/PatronModal.tsx
index 6787fa43..d36fb409 100644
--- a/src/components/common/PatronModal.tsx
+++ b/src/components/common/PatronModal.tsx
@@ -1,5 +1,4 @@
import confetti from "canvas-confetti"
-import { useTranslation } from "next-i18next"
import { useEffect, useRef, useState } from "react"
import { toast } from "react-hot-toast"
@@ -11,6 +10,7 @@ import { Button } from "~/components/ui/Button"
import { Modal } from "~/components/ui/Modal"
import { CSB_SCAN, MIRA_LINK } from "~/lib/env"
import { getSiteLink } from "~/lib/helpers"
+import { useTranslation } from "~/lib/i18n/client"
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
import { useGetTips, useTipCharacter } from "~/queries/site"
diff --git a/src/components/common/ReactionLike.tsx b/src/components/common/ReactionLike.tsx
index 849216e5..09e5bdf4 100644
--- a/src/components/common/ReactionLike.tsx
+++ b/src/components/common/ReactionLike.tsx
@@ -1,5 +1,6 @@
+"use client"
+
import confetti from "canvas-confetti"
-import { Trans, useTranslation } from "next-i18next"
import { useEffect, useMemo, useRef, useState } from "react"
import { CharacterList } from "~/components/common/CharacterList"
@@ -7,6 +8,7 @@ import { Modal } from "~/components/ui/Modal"
import { Tooltip } from "~/components/ui/Tooltip"
import { UniLink } from "~/components/ui/UniLink"
import { CSB_SCAN } from "~/lib/env"
+import { Trans, useTranslation } from "~/lib/i18n/client"
import { cn } from "~/lib/utils"
import {
useCheckLike,
@@ -24,7 +26,7 @@ export const ReactionLike: React.FC<{
noteId?: number
}> = ({ size, characterId, noteId }) => {
const toggleLikePage = useToggleLikePage()
- const { t } = useTranslation("common")
+ const { t, i18n } = useTranslation("common")
const [isLikeOpen, setIsLikeOpen] = useState(false)
const [isLikeListOpen, setIsLikeListOpen] = useState(false)
@@ -149,7 +151,7 @@ export const ReactionLike: React.FC<{
title={t("Like successfully") || ""}
>
-
+
Your like has been stored on the blockchain, view it on{" "}
-
+
Do you really want to revert this like action?
diff --git a/src/components/common/ReactionMint.tsx b/src/components/common/ReactionMint.tsx
index 68255be1..a07cfef4 100644
--- a/src/components/common/ReactionMint.tsx
+++ b/src/components/common/ReactionMint.tsx
@@ -1,5 +1,6 @@
+"use client"
+
import confetti from "canvas-confetti"
-import { Trans, useTranslation } from "next-i18next"
import { useEffect, useMemo, useRef, useState } from "react"
import { useAccountState } from "@crossbell/connect-kit"
@@ -10,6 +11,7 @@ import { Modal } from "~/components/ui/Modal"
import { Tooltip } from "~/components/ui/Tooltip"
import { UniLink } from "~/components/ui/UniLink"
import { CSB_SCAN, CSB_XCHAR } from "~/lib/env"
+import { Trans, useTranslation } from "~/lib/i18n/client"
import { noopArr } from "~/lib/noop"
import { cn } from "~/lib/utils"
import { useCheckMint, useGetMints, useMintPage } from "~/queries/page"
@@ -23,7 +25,7 @@ export const ReactionMint: React.FC<{
characterId?: number
}> = ({ size, noteId, characterId }) => {
const mintPage = useMintPage()
- const { t } = useTranslation("common")
+ const { t, i18n } = useTranslation("common")
const account = useAccountState((s) => s.computed.account)
@@ -136,7 +138,7 @@ export const ReactionMint: React.FC<{
title={t("Mint successfully") || ""}
>
-
+
This post has been minted to NFT by you, view it on{" "}
= ({ siteName, title, description, image, icon, site }) => {
- return (
-
- {title ? `${title} - ${siteName}` : `${siteName}`}
-
-
-
-
-
-
-
- {image && (
- <>
-
-
- >
- )}
-
- {site ? (
- <>
-
-
-
-
-
- >
- ) : (
- <>
-
-
-
-
-
-
-
-
-
-
- >
- )}
-
- )
-}
diff --git a/src/components/common/SearchInput.tsx b/src/components/common/SearchInput.tsx
index 4bd7576e..a42d9fac 100644
--- a/src/components/common/SearchInput.tsx
+++ b/src/components/common/SearchInput.tsx
@@ -1,20 +1,22 @@
-import { useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
+"use client"
+
+import { useRouter, useSearchParams } from "next/navigation"
import { useForm } from "react-hook-form"
+import { useTranslation } from "~/lib/i18n/client"
import { cn } from "~/lib/utils"
export const SearchInput: React.FC<{
- value?: string
noBorder?: boolean
onSubmit?: (value?: string) => void
-}> = ({ value, noBorder, onSubmit }) => {
+}> = ({ noBorder, onSubmit }) => {
const router = useRouter()
- const { t } = useTranslation(["common", "site"])
+ const searchParams = useSearchParams()
+ const { t } = useTranslation("common")
const form = useForm({
defaultValues: {
- content: value || "",
+ content: searchParams?.get("q") || "",
},
})
diff --git a/src/components/dashboard/DashboardLayout.server.tsx b/src/components/dashboard/DashboardLayout.server.tsx
deleted file mode 100644
index 91387c77..00000000
--- a/src/components/dashboard/DashboardLayout.server.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { serverSideTranslations } from "next-i18next/serverSideTranslations"
-
-import { languageDetector } from "~/lib/language-detector"
-
-export const getServerSideProps = async (ctx: any) => {
- return {
- props: {
- ...(await serverSideTranslations(languageDetector(ctx), [
- "common",
- "dashboard",
- "index",
- "site",
- ])),
- },
- }
-}
diff --git a/src/components/dashboard/DashboardMain.tsx b/src/components/dashboard/DashboardMain.tsx
index 5b72c644..ebbaecb2 100644
--- a/src/components/dashboard/DashboardMain.tsx
+++ b/src/components/dashboard/DashboardMain.tsx
@@ -1,6 +1,5 @@
-import { useTranslation } from "next-i18next"
-
import { useIsMobileLayout } from "~/hooks/useMobileLayout"
+import { useTranslation } from "~/lib/i18n/client"
import { cn } from "~/lib/utils"
export const DashboardMain: React.FC<{
diff --git a/src/components/dashboard/ImportPreview.tsx b/src/components/dashboard/ImportPreview.tsx
index 6f3934eb..40ee0296 100644
--- a/src/components/dashboard/ImportPreview.tsx
+++ b/src/components/dashboard/ImportPreview.tsx
@@ -1,8 +1,8 @@
import type { NoteMetadata } from "crossbell.js"
-import { useTranslation } from "next-i18next"
import { useState } from "react"
import { useDate } from "~/hooks/useDate"
+import { useTranslation } from "~/lib/i18n/client"
import { PageContent } from "../common/PageContent"
diff --git a/src/components/dashboard/PagesManager.tsx b/src/components/dashboard/PagesManager.tsx
index d66a864a..978377b1 100644
--- a/src/components/dashboard/PagesManager.tsx
+++ b/src/components/dashboard/PagesManager.tsx
@@ -1,13 +1,18 @@
import { nanoid } from "nanoid"
-import { Trans, useTranslation } from "next-i18next"
import Link from "next/link"
-import { useRouter } from "next/router"
+import {
+ useParams,
+ usePathname,
+ useRouter,
+ useSearchParams,
+} from "next/navigation"
import { Fragment, useMemo, useState } from "react"
import { Menu } from "@headlessui/react"
import { useQueryClient } from "@tanstack/react-query"
import { useDate } from "~/hooks/useDate"
+import { Trans, useTranslation } from "~/lib/i18n/client"
import { getPageVisibility } from "~/lib/page-helpers"
import { readFiles } from "~/lib/read-files"
import { setStorage } from "~/lib/storage"
@@ -28,19 +33,23 @@ import { PagesManagerMenu } from "./PagesManagerMenu"
export const PagesManager: React.FC<{
isPost: boolean
}> = ({ isPost }) => {
- const router = useRouter()
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const site = useGetSite(subdomain)
+ const searchParams = useSearchParams()
+ const router = useRouter()
+ const pathname = usePathname()
const visibility = useMemo(
() =>
- router.query.visibility
- ? (router.query.visibility as PageVisibilityEnum)
+ searchParams?.get("visibility")
+ ? (searchParams?.get("visibility") as PageVisibilityEnum)
: PageVisibilityEnum.All,
- [router.query.visibility],
+ [searchParams],
)
- const { t } = useTranslation(["dashboard", "site"])
+ const { t } = useTranslation("dashboard")
+ const { t: siteT } = useTranslation("site")
const date = useDate()
const pages = useGetPagesBySite({
@@ -75,16 +84,14 @@ export const PagesManager: React.FC<{
text: item.text,
onClick: () => {
const newQuery: Record = {
- ...router.query,
+ ...searchParams,
visibility: item.value,
}
if (item.value === PageVisibilityEnum.All) {
delete newQuery["visibility"]
}
const search = new URLSearchParams(newQuery).toString()
- router.push({
- search,
- })
+ router.push(pathname + "?" + search)
},
active: item.value === visibility,
}))
@@ -349,7 +356,7 @@ export const PagesManager: React.FC<{
onClick={pages.fetchNextPage as () => void}
isLoading={pages.isFetchingNextPage}
>
- {t("load more", {
+ {siteT("load more", {
name: t(
isPost
? "post"
@@ -359,7 +366,6 @@ export const PagesManager: React.FC<{
: ""),
),
count: (pages.data?.pages?.[0].count || 0) - currentLength,
- ns: "site",
})}
)}
diff --git a/src/components/dashboard/PagesManagerBatchSelectActionTab.tsx b/src/components/dashboard/PagesManagerBatchSelectActionTab.tsx
index 4a10621c..1f3f4c3c 100644
--- a/src/components/dashboard/PagesManagerBatchSelectActionTab.tsx
+++ b/src/components/dashboard/PagesManagerBatchSelectActionTab.tsx
@@ -1,5 +1,4 @@
-import { useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
+import { useParams } from "next/navigation"
import React, { useState } from "react"
import toast from "react-hot-toast"
@@ -8,6 +7,7 @@ import { useQueryClient } from "@tanstack/react-query"
import { type TabItem, Tabs } from "~/components/ui/Tabs"
import { APP_NAME } from "~/lib/env"
+import { useTranslation } from "~/lib/i18n/client"
import { delStorage, getStorage, setStorage } from "~/lib/storage"
import { ExpandedNote } from "~/lib/types"
import { useCreateOrUpdatePage, useDeletePage } from "~/queries/page"
@@ -23,10 +23,10 @@ export const PagesManagerBatchSelectActionTab: React.FC<{
batchSelected: (string | number)[]
setBatchSelected: (selected: string[]) => void
}> = ({ isPost, isNotxLogContent, pages, batchSelected, setBatchSelected }) => {
- const { t } = useTranslation(["dashboard", "site"])
+ const { t } = useTranslation("dashboard")
- const router = useRouter()
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const queryClient = useQueryClient()
diff --git a/src/components/dashboard/PagesManagerMenu.tsx b/src/components/dashboard/PagesManagerMenu.tsx
index 9603d4d9..3a6d4ec9 100644
--- a/src/components/dashboard/PagesManagerMenu.tsx
+++ b/src/components/dashboard/PagesManagerMenu.tsx
@@ -1,5 +1,4 @@
-import { useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
+import { useParams, useRouter } from "next/navigation"
import { FC, useEffect, useState } from "react"
import toast from "react-hot-toast"
@@ -9,6 +8,7 @@ import { useQueryClient } from "@tanstack/react-query"
import { useGetState } from "~/hooks/useGetState"
import { APP_NAME } from "~/lib/env"
import { getNoteSlugFromNote, getTwitterShareUrl } from "~/lib/helpers"
+import { useTranslation } from "~/lib/i18n/client"
import { delStorage, getStorage, setStorage } from "~/lib/storage"
import { ExpandedNote } from "~/lib/types"
import { useCreateOrUpdatePage, useDeletePage } from "~/queries/page"
@@ -17,8 +17,8 @@ import { useGetSite } from "~/queries/site"
import { DeleteConfirmationModal } from "./DeleteConfirmationModal"
const usePageEditLink = (page: ExpandedNote, isPost: boolean) => {
- const router = useRouter()
- const subdomain = router.query.subdomain as string
+ const params = useParams()
+ const subdomain = params?.subdomain as string
return `/dashboard/${subdomain}/editor?id=${page.noteId}&type=${
isPost ? "post" : "page"
@@ -35,14 +35,15 @@ export const PagesManagerMenu: FC<{
page: ExpandedNote
onClick: () => void
}> = ({ isPost, page, onClick: onClose }) => {
- const { t } = useTranslation(["dashboard", "site"])
+ const { t } = useTranslation("dashboard")
const isCrossbell = !page.metadata?.content?.sources?.includes("xlog")
const router = useRouter()
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const createOrUpdatePage = useCreateOrUpdatePage()
const editLink = usePageEditLink(page, isPost)
- const subdomain = router.query.subdomain as string
const queryClient = useQueryClient()
const deletePage = useDeletePage()
@@ -157,7 +158,7 @@ export const PagesManagerMenu: FC<{
onClick() {
const slug = getNoteSlugFromNote(page)
if (!slug) return
- window.open(`/_site/${subdomain}/${slug}`)
+ window.open(`/site/${subdomain}/${slug}`)
},
},
{
diff --git a/src/components/dashboard/PublishButton.tsx b/src/components/dashboard/PublishButton.tsx
index 34f46c14..c05c0308 100644
--- a/src/components/dashboard/PublishButton.tsx
+++ b/src/components/dashboard/PublishButton.tsx
@@ -1,7 +1,8 @@
-import { useTranslation } from "next-i18next"
import { useEffect, useRef, useState } from "react"
import useOnClickOutside from "use-onclickoutside"
+import { useTranslation } from "~/lib/i18n/client"
+
import { Button, ButtonGroup } from "../ui/Button"
import { DeleteConfirmationModal } from "./DeleteConfirmationModal"
diff --git a/src/components/dashboard/SettingsLayout.tsx b/src/components/dashboard/SettingsLayout.tsx
index f4d11796..10c3099d 100644
--- a/src/components/dashboard/SettingsLayout.tsx
+++ b/src/components/dashboard/SettingsLayout.tsx
@@ -1,9 +1,10 @@
-import { useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
+import { useParams, usePathname } from "next/navigation"
import React from "react"
import { useXSettingsModal } from "@crossbell/connect-kit"
+import { useTranslation } from "~/lib/i18n/client"
+
import { type TabItem, Tabs } from "../ui/Tabs"
import { DashboardMain } from "./DashboardMain"
@@ -11,11 +12,12 @@ export const SettingsLayout: React.FC<{
title: string
children: React.ReactNode
}> = ({ title, children }) => {
- const router = useRouter()
const { t } = useTranslation("dashboard")
const xSettingsModal = useXSettingsModal()
- const subdomain = router.query.subdomain as string
+ const pathname = usePathname()
+ const params = useParams()
+ const subdomain = params?.subdomain as string
const tabItems: TabItem[] = [
{ text: "General", href: `/dashboard/${subdomain}/settings/general` },
{
@@ -40,7 +42,7 @@ export const SettingsLayout: React.FC<{
text: "Export data",
href: `https://export.crossbell.io/?handle=${subdomain}`,
},
- ].map((item) => ({ ...item, active: router.asPath === item.href }))
+ ].map((item) => ({ ...item, active: pathname === item.href }))
return (
diff --git a/src/components/home/EntranceButton.tsx b/src/components/home/EntranceButton.tsx
new file mode 100644
index 00000000..b606e0cb
--- /dev/null
+++ b/src/components/home/EntranceButton.tsx
@@ -0,0 +1,29 @@
+"use client"
+
+import { useRouter } from "next/navigation"
+import React from "react"
+
+import { useAccountState } from "@crossbell/connect-kit"
+
+import { Button } from "~/components/ui/Button"
+
+interface Props {
+ connectedContent: React.ReactNode
+ unconnectedContent: React.ReactNode
+}
+
+export default function EntranceButton(props: Props) {
+ const router = useRouter()
+ const isConnected = useAccountState((s) => !!s.computed.account)
+
+ return (
+
+ )
+}
diff --git a/src/components/main/MainFeed.tsx b/src/components/home/HomeFeed.tsx
similarity index 87%
rename from src/components/main/MainFeed.tsx
rename to src/components/home/HomeFeed.tsx
index bc3eb852..338e080e 100644
--- a/src/components/main/MainFeed.tsx
+++ b/src/components/home/HomeFeed.tsx
@@ -1,11 +1,12 @@
-import { useTranslation } from "next-i18next"
+"use client"
+
import Link from "next/link"
-import { useRouter } from "next/router"
+import { useRouter, useSearchParams } from "next/navigation"
import { memo, useEffect, useState } from "react"
import reactStringReplace from "react-string-replace"
import { Virtuoso } from "react-virtuoso"
-import { useAccountState } from "@crossbell/connect-kit"
+import { useAccountState, useConnectModal } from "@crossbell/connect-kit"
import { Switch } from "@headlessui/react"
import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
@@ -14,6 +15,7 @@ import { Image } from "~/components/ui/Image"
import { Tabs } from "~/components/ui/Tabs"
import { Tooltip } from "~/components/ui/Tooltip"
import { useDate } from "~/hooks/useDate"
+import { useTranslation } from "~/lib/i18n/client"
import { getStorage, setStorage } from "~/lib/storage"
import { ExpandedNote } from "~/lib/types"
import type { FeedType, SearchType } from "~/models/home.model"
@@ -31,7 +33,7 @@ const Post = ({
keyword?: string
}) => {
const router = useRouter()
- const { t } = useTranslation(["common", "site"])
+ const { t } = useTranslation("common")
const date = useDate()
if (
@@ -50,6 +52,7 @@ const Post = ({
target="_blank"
href={`/api/redirection?characterId=${post.characterId}`}
className="flex items-center space-x-4 cursor-pointer"
+ prefetch={false}
>
@@ -159,12 +163,15 @@ const Post = ({
const MemoedPost = memo(Post)
-export const MainFeed: React.FC<{
- type?: FeedType
+export const HomeFeed: React.FC<{
noteIds?: string[]
keyword?: string
-}> = ({ type, noteIds, keyword }) => {
- const { t } = useTranslation(["common", "site"])
+ type?: FeedType
+}> = ({ noteIds, keyword, type }) => {
+ const { t } = useTranslation("common")
+ const searchParams = useSearchParams()
+
+ const [feedType, setFeedType] = useState(type || "latest")
const currentCharacterId = useAccountState(
(s) => s.computed.account?.characterId,
@@ -174,15 +181,15 @@ export const MainFeed: React.FC<{
const [searchType, setSearchType] = useState("latest")
const feed = useGetFeed({
- type: type,
+ type: feedType,
characterId: currentCharacterId,
noteIds: noteIds,
daysInterval: hotInterval,
- searchKeyword: keyword,
+ searchKeyword: searchParams?.get("q") || undefined,
searchType,
})
- const hasFiltering = type === "latest"
+ const hasFiltering = feedType === "latest"
const [aiFiltering, setAiFiltering] = useState(true)
@@ -226,8 +233,35 @@ export const MainFeed: React.FC<{
},
]
+ const connectModal = useConnectModal()
+
+ const tabs = [
+ {
+ text: "Latest",
+ onClick: () => setFeedType("latest"),
+ active: feedType === "latest",
+ },
+ {
+ text: "Hottest",
+ onClick: () => setFeedType("hot"),
+ active: feedType === "hot",
+ },
+ {
+ text: "Following",
+ onClick: () => {
+ if (!currentCharacterId) {
+ connectModal.show()
+ } else {
+ setFeedType("following")
+ }
+ },
+ active: feedType === "following",
+ },
+ ]
+
return (
<>
+ {!type && }
{hasFiltering && (
@@ -263,10 +297,10 @@ export const MainFeed: React.FC<{
)}
- {type === "hot" && (
+ {feedType === "hot" && (
)}
- {type === "search" && (
+ {feedType === "search" && (
)}
@@ -288,7 +322,7 @@ export const MainFeed: React.FC<{
key={`${post.characterId}-${post.noteId}`}
post={post}
filtering={aiFiltering ? 60 : 0}
- keyword={keyword}
+ keyword={searchParams?.get("q") || undefined}
/>
)
})
diff --git a/src/components/main/MainSidebar.tsx b/src/components/home/HomeSidebar.tsx
similarity index 97%
rename from src/components/main/MainSidebar.tsx
rename to src/components/home/HomeSidebar.tsx
index 8352b37a..d6893f39 100644
--- a/src/components/main/MainSidebar.tsx
+++ b/src/components/home/HomeSidebar.tsx
@@ -1,4 +1,5 @@
-import { useTranslation } from "next-i18next"
+"use client"
+
import { useState } from "react"
import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
@@ -6,12 +7,13 @@ import { SearchInput } from "~/components/common/SearchInput"
import { Image } from "~/components/ui/Image"
import { UniLink } from "~/components/ui/UniLink"
import { getSiteLink } from "~/lib/helpers"
+import { useTranslation } from "~/lib/i18n/client"
import { useGetShowcase } from "~/queries/home"
import topics from "../../../data/topics.json"
import { FollowAllButton } from "../common/FollowAllButton"
-export function MainSidebar({ hideSearch }: { hideSearch?: boolean }) {
+export function HomeSidebar({ hideSearch }: { hideSearch?: boolean }) {
const showcaseSites = useGetShowcase()
const { t } = useTranslation("index")
diff --git a/src/components/home/HomeTabs.tsx b/src/components/home/HomeTabs.tsx
new file mode 100644
index 00000000..24ab3b04
--- /dev/null
+++ b/src/components/home/HomeTabs.tsx
@@ -0,0 +1,55 @@
+"use client"
+
+import { usePathname } from "next/navigation"
+
+import { Image } from "~/components/ui/Image"
+import { GITHUB_LINK } from "~/lib/env"
+import { useTranslation } from "~/lib/i18n/client"
+import { cn } from "~/lib/utils"
+
+import { UniLink } from "../ui/UniLink"
+
+const tabs = [
+ {
+ name: "Home",
+ link: "/",
+ },
+ {
+ name: "Activities",
+ link: "/activities",
+ },
+ {
+ name: (
+
+ ),
+ link: GITHUB_LINK,
+ },
+]
+
+export default function HomeTabs() {
+ const pathname = usePathname()
+ const { t } = useTranslation("index")
+
+ return (
+ <>
+ {tabs?.map((tab, index) => (
+
+ {typeof tab.name === "string" ? t(tab.name) : tab.name}
+
+ ))}
+ >
+ )
+}
diff --git a/src/components/home/Integrations.tsx b/src/components/home/Integrations.tsx
new file mode 100644
index 00000000..f332a568
--- /dev/null
+++ b/src/components/home/Integrations.tsx
@@ -0,0 +1,142 @@
+"use client"
+
+import {
+ CrossbellChainLogo,
+ XCharLogo,
+ XFeedLogo,
+ XShopLogo,
+ XSyncLogo,
+} from "@crossbell/ui"
+import { RssIcon } from "@heroicons/react/24/outline"
+
+import { Image } from "~/components/ui/Image"
+import { Tooltip } from "~/components/ui/Tooltip"
+import { UniLink } from "~/components/ui/UniLink"
+import { getSiteLink } from "~/lib/helpers"
+
+export function Integration() {
+ const integrations = [
+ {
+ name: "RSS",
+ icon:
,
+ url:
+ getSiteLink({
+ subdomain: "xlog",
+ }) + "/feed?format=xml",
+ },
+ {
+ name: "JSON Feed",
+ icon:
,
+ url:
+ getSiteLink({
+ subdomain: "xlog",
+ }) + "/feed",
+ },
+ {
+ name: "xChar",
+ icon:
,
+ url: "https://xchar.app/",
+ },
+ {
+ name: "xFeed",
+ icon:
,
+ url: "https://crossbell.io/feed",
+ },
+ {
+ name: "xSync",
+ icon:
,
+ url: "https://xsync.app/",
+ },
+ {
+ name: "xShop",
+ icon:
,
+ text: "Coming soon",
+ },
+ {
+ name: "Crossbell Scan",
+ icon:
,
+ url: "https://scan.crossbell.io/",
+ },
+ {
+ name: "Crossbell Faucet",
+ icon:
,
+ url: "https://faucet.crossbell.io/",
+ },
+ {
+ name: "Crossbell Export",
+ icon:
,
+ url: "https://export.crossbell.io/",
+ },
+ {
+ name: "Crossbell SDK",
+ icon:
,
+ url: "https://crossbell-box.github.io/crossbell.js/",
+ },
+ {
+ name: "RSS3",
+ icon: (
+
+ ),
+ url: "https://rss3.io/",
+ },
+ {
+ name: "Hoot It",
+ icon: (
+
+ ),
+ url: "https://hoot.it/search/xLog",
+ },
+ {
+ name: "Raycast",
+ icon:
,
+ url: "https://www.raycast.com/Songkeys/crossbell",
+ },
+ {
+ name: "Obsidian",
+ icon: (
+
+ ),
+ text: "Coming soon",
+ },
+ ]
+ return (
+ <>
+ {integrations.map((item, index) => (
+
+ {item.url ? (
+
+
+ {item.icon}
+
+
+ {item.name}
+
+
+ ) : (
+
+
+
+ {item.icon}
+
+
+ {item.name}
+
+
+
+ )}
+
+ ))}
+ >
+ )
+}
diff --git a/src/components/home/Showcase.tsx b/src/components/home/Showcase.tsx
new file mode 100644
index 00000000..4d96ddcf
--- /dev/null
+++ b/src/components/home/Showcase.tsx
@@ -0,0 +1,80 @@
+"use client"
+
+import { useState } from "react"
+
+import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
+import { FollowAllButton } from "~/components/common/FollowAllButton"
+import { Image } from "~/components/ui/Image"
+import { UniLink } from "~/components/ui/UniLink"
+import { getSiteLink } from "~/lib/helpers"
+import { useTranslation } from "~/lib/i18n/client"
+import { useGetShowcase } from "~/queries/home"
+
+export function ShowCase() {
+ const showcaseSites = useGetShowcase()
+ const [showcaseMore, setShowcaseMore] = useState(false)
+ const { t } = useTranslation("index")
+
+ return (
+ <>
+
s.characterId)
+ .filter(Boolean)
+ .map(Number)}
+ siteIds={showcaseSites.data?.map((s: { handle: string }) => s.handle)}
+ />
+
+ >
+ )
+}
diff --git a/src/components/site/BackToTopFAB.tsx b/src/components/site/BackToTopFAB.tsx
index a7d853cc..d78eeb96 100644
--- a/src/components/site/BackToTopFAB.tsx
+++ b/src/components/site/BackToTopFAB.tsx
@@ -1,3 +1,5 @@
+"use client"
+
import { useEffect, useState } from "react"
import { throttle } from "~/lib/utils"
diff --git a/src/components/site/PostLink.tsx b/src/components/site/PostLink.tsx
deleted file mode 100644
index 0275bd90..00000000
--- a/src/components/site/PostLink.tsx
+++ /dev/null
@@ -1,20 +0,0 @@
-import { Link } from "react-scroll"
-
-export const PostLink: React.FC = ({ children, ...props }) => {
- if (props.href?.startsWith("#")) {
- return (
-
- {children}
-
- )
- } else {
- return {children}
- }
-}
diff --git a/src/components/site/PostMeta.tsx b/src/components/site/PostMeta.tsx
index e569aed7..64d76b4a 100644
--- a/src/components/site/PostMeta.tsx
+++ b/src/components/site/PostMeta.tsx
@@ -1,4 +1,6 @@
-import { useTranslation } from "next-i18next"
+"use client"
+
+// TODO
import { useEffect, useState } from "react"
import { BlockchainIcon } from "~/components/icons/BlockchainIcon"
@@ -6,6 +8,7 @@ import { UniLink } from "~/components/ui/UniLink"
import { useDate } from "~/hooks/useDate"
import { useUserRole } from "~/hooks/useUserRole"
import { CSB_SCAN, SITE_URL } from "~/lib/env"
+import { useTranslation } from "~/lib/i18n/client"
import { toCid } from "~/lib/ipfs-parser"
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
import { useGetSummary } from "~/queries/page"
diff --git a/src/components/site/PostToc.tsx b/src/components/site/PostToc.tsx
index f176b5d8..20e32cef 100644
--- a/src/components/site/PostToc.tsx
+++ b/src/components/site/PostToc.tsx
@@ -4,8 +4,7 @@ import katex from "katex"
import type { List } from "mdast"
import { toHast } from "mdast-util-to-hast"
import type { Result as TocResult } from "mdast-util-toc"
-import React, { useEffect, useRef, useState } from "react"
-import { Link } from "react-scroll"
+import React, { createElement, useEffect, useRef, useState } from "react"
const inlineElements = ["delete", "strong", "emphasis", "inlineCode"]
@@ -40,6 +39,13 @@ function useActiveId(itemIds: string[]) {
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
+ const state = history.state
+
+ history.replaceState(
+ { ...state, preventScrollToToc: true },
+ "",
+ entry.target.getAttribute("href"),
+ )
setActiveId(entry.target.getAttribute("href"))
}
})
@@ -63,12 +69,13 @@ function useActiveId(itemIds: string[]) {
}, [itemIds])
return activeId
}
-
-function renderItems(
- items: TocResult["map"],
- activeId?: string | null,
- prefix = "",
-) {
+interface ItemsProps {
+ items: TocResult["map"]
+ activeId?: string | null
+ prefix?: string
+}
+function Items(props: ItemsProps) {
+ const { items, activeId, prefix = "" } = props
return (
{items?.children?.map((item, index) => (
@@ -91,14 +98,7 @@ function renderItems(
return (
{child.type === "paragraph" && child.children?.[0]?.url && (
-
-
+
)}
{child.type === "list" &&
- renderItems(child, activeId, `${index + 1}.`)}
+ createElement(Items, {
+ items: child,
+ activeId,
+ prefix: `${index + 1}.`,
+ })}
)
})}
@@ -161,7 +165,7 @@ export const PostToc: React.FC<{
overflowY: "auto",
}}
>
- {renderItems(data?.map, activeId)}
+
)
diff --git a/src/components/site/SiteArchives.tsx b/src/components/site/SiteArchives.tsx
index 78b3baae..7bbb739d 100644
--- a/src/components/site/SiteArchives.tsx
+++ b/src/components/site/SiteArchives.tsx
@@ -1,43 +1,43 @@
-import { useTranslation } from "next-i18next"
-import Link from "next/link"
-import { useMemo } from "react"
+"use client"
-import type { InfiniteData } from "@tanstack/react-query"
+import Link from "next/link"
+import { useParams } from "next/navigation"
+import { useMemo } from "react"
import { Button } from "~/components/ui/Button"
import { useDate } from "~/hooks/useDate"
-import { ExpandedNote } from "~/lib/types"
+import { useTranslation } from "~/lib/i18n/client"
+import { ExpandedNote, PageVisibilityEnum } from "~/lib/types"
+import { useGetPagesBySiteLite } from "~/queries/page"
+import { useGetSite } from "~/queries/site"
import { EmptyState } from "../ui/EmptyState"
import { UniLink } from "../ui/UniLink"
-export const SiteArchives: React.FC<{
- title?: string
- showTags?: boolean
- posts?: InfiniteData<{
- list: ExpandedNote[]
- count: number
- }>
- fetchNextPage: () => void
- hasNextPage?: boolean
- isFetchingNextPage?: boolean
-}> = ({
- title,
- showTags,
- posts,
- fetchNextPage,
- hasNextPage,
- isFetchingNextPage,
-}) => {
+export const SiteArchives: React.FC = () => {
let currentLength = 0
const date = useDate()
- const { t } = useTranslation(["common", "site"])
+ const { t } = useTranslation("site")
+ const { t: commonT } = useTranslation("common")
+ const params = useParams()
+ if (params?.tag) {
+ params.tag = decodeURIComponent(params.tag as string)
+ }
+
+ const site = useGetSite(params?.site as string)
+ const posts = useGetPagesBySiteLite({
+ characterId: site.data?.characterId,
+ limit: 100,
+ type: "post",
+ visibility: PageVisibilityEnum.Published,
+ ...(params?.tag && { tags: [params.tag as string] }),
+ })
const groupedByYear = useMemo
),
- url: `https://hoot.it/search/${site?.handle}.csb/activities`,
+ url: `https://hoot.it/search/${site.data?.handle}.csb/activities`,
},
{
text: "View on Crossbell Scan",
icon: ,
- url: `${CSB_SCAN}/address/${site?.owner}`,
+ url: `${CSB_SCAN}/address/${site.data?.owner}`,
},
{
text: "Subscribe to JSON Feed",
@@ -197,13 +199,13 @@ export const SiteHeader: React.FC<{
>
{(() => {
switch (
- site?.metadata?.content?.banners?.[0]?.mime_type?.split("/")[0]
+ site.data?.metadata?.content?.banners?.[0]?.mime_type?.split("/")[0]
) {
case "image":
return (
}
@@ -213,7 +215,7 @@ export const SiteHeader: React.FC<{
return (
diff --git a/src/components/site/SiteHome.tsx b/src/components/site/SiteHome.tsx
index 0fccfbfe..264eb9e4 100644
--- a/src/components/site/SiteHome.tsx
+++ b/src/components/site/SiteHome.tsx
@@ -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 && }
- {!!posts.pages[0].count && (
+ {!posts.data.pages[0].count && }
+ {!!posts.data.pages[0].count && (
- {posts.pages.map((posts) =>
+ {posts.data.pages.map((posts) =>
posts.list.map((post) => {
currentLength++
return (
@@ -127,20 +128,21 @@ export const SiteHome: React.FC<{
)}
)}
- {hasNextPage && (
+ {posts.hasNextPage && (
)}
diff --git a/src/components/site/SiteLayout.server.tsx b/src/components/site/SiteLayout.server.tsx
deleted file mode 100644
index b4baa8a4..00000000
--- a/src/components/site/SiteLayout.server.tsx
+++ /dev/null
@@ -1,101 +0,0 @@
-import { serverSideTranslations } from "next-i18next/serverSideTranslations"
-
-import { QueryClient, dehydrate } from "@tanstack/react-query"
-
-import { languageDetector } from "~/lib/language-detector"
-import { notFound } from "~/lib/server-side-props"
-import { PageVisibilityEnum } from "~/lib/types"
-import { fetchGetPage, prefetchGetPagesBySite } from "~/queries/page.server"
-import {
- fetchGetSite,
- prefetchGetSiteSubscriptions,
- prefetchGetSiteToSubscriptions,
-} from "~/queries/site.server"
-
-export const getServerSideProps = async (
- ctx: any,
- queryClient: QueryClient,
- options?: {
- limit?: number
- useStat?: boolean
- skipPages?: boolean
- preview?: boolean
- },
-) => {
- const domainOrSubdomain = ctx.params!.site as string
- const pageSlug = ctx.params!.page as string
- const tag = ctx.params!.tag as string
- const site = await fetchGetSite(domainOrSubdomain, queryClient)
-
- if (site?.characterId) {
- await Promise.all([
- prefetchGetSiteSubscriptions(
- {
- characterId: site.characterId,
- },
- queryClient,
- ),
- prefetchGetSiteToSubscriptions(
- {
- characterId: site.characterId,
- },
- queryClient,
- ),
- new Promise(async (resolve, reject) => {
- if (options?.preview) {
- // do nothing
- } else if (pageSlug) {
- try {
- const page = await fetchGetPage(
- {
- characterId: site.characterId,
- slug: pageSlug,
- ...(options?.useStat && {
- useStat: true,
- }),
- },
- queryClient,
- )
-
- if (
- !page ||
- new Date(page!.metadata?.content?.date_published || "") >
- new Date()
- ) {
- reject(notFound())
- }
- } catch (error) {
- reject(error)
- }
- } else {
- if (!options?.skipPages) {
- await prefetchGetPagesBySite(
- {
- characterId: site.characterId,
- ...(options?.limit && { limit: options.limit }),
- type: "post",
- visibility: PageVisibilityEnum.Published,
- ...(tag && { tags: [tag] }),
- ...(options?.useStat && {
- useStat: true,
- }),
- },
- queryClient,
- )
- }
- }
- resolve(null)
- }),
- ])
- }
-
- return {
- props: {
- ...(await serverSideTranslations(languageDetector(ctx), [
- "common",
- "site",
- ])),
- dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))),
- },
- }
-}
diff --git a/src/components/site/SiteLayout.tsx b/src/components/site/SiteLayout.tsx
deleted file mode 100644
index 6ced9cf3..00000000
--- a/src/components/site/SiteLayout.tsx
+++ /dev/null
@@ -1,130 +0,0 @@
-import { useRouter } from "next/router"
-import React, { useEffect } from "react"
-
-import { useAccountState } from "@crossbell/connect-kit"
-
-import { BlockchainInfo } from "~/components/common/BlockchainInfo"
-import { Style } from "~/components/common/Style"
-import { useUserRole } from "~/hooks/useUserRole"
-import { IS_PROD, IS_VERCEL_PREVIEW } from "~/lib/constants"
-import { OUR_DOMAIN, SITE_URL } from "~/lib/env"
-import { getUserContentsUrl } from "~/lib/user-contents"
-import { cn } from "~/lib/utils"
-import { useCheckLike, useCheckMint, useGetPage } from "~/queries/page"
-import { useGetSite, useGetSubscription } from "~/queries/site"
-
-import { SEOHead } from "../common/SEOHead"
-import { FABContainer } from "../ui/FAB"
-import { BackToTopFAB } from "./BackToTopFAB"
-import { SiteFooter } from "./SiteFooter"
-import { SiteHeader } from "./SiteHeader"
-
-export type SiteLayoutProps = {
- children: React.ReactNode
- title?: string | null
- siteId?: string
- useStat?: boolean
- type: "index" | "post" | "tag" | "nft" | "404" | "archive"
-}
-
-export const SiteLayout: React.FC = ({
- children,
- title,
- siteId,
- useStat,
- type,
-}) => {
- const router = useRouter()
- const domainOrSubdomain = (router.query.site || siteId) as string
- const pageSlug = router.query.page as string
- const tag = router.query.tag as string
-
- const site = useGetSite(domainOrSubdomain)
-
- const page = useGetPage({
- characterId: site.data?.characterId,
- slug: pageSlug,
- ...(useStat && {
- useStat: true,
- }),
- })
-
- const isConnected = useAccountState((s) => !!s.computed.account)
- const userRole = useUserRole(domainOrSubdomain)
- const subscription = useGetSubscription(site.data?.characterId)
- const [{ isLiked }] = useCheckLike({
- characterId: page.data?.characterId,
- noteId: page.data?.noteId,
- })
- const isMint = useCheckMint({
- characterId: page.data?.characterId,
- noteId: page.data?.noteId,
- })
-
- useEffect(() => {
- if (site.data) {
- if (
- window.location.host.split(".").slice(-2).join(".") !== OUR_DOMAIN &&
- window.location.host !== site.data?.metadata?.content?.custom_domain &&
- IS_PROD &&
- !IS_VERCEL_PREVIEW
- ) {
- window.location.href = SITE_URL
- }
- }
- }, [site.isSuccess, site.data])
-
- return (
-
-
]*>/g, "")
- }
- image={
- page.data?.metadata?.content?.cover ||
- getUserContentsUrl(site.data?.metadata?.content?.avatars?.[0])
- }
- icon={getUserContentsUrl(site.data?.metadata?.content?.avatars?.[0])}
- site={domainOrSubdomain}
- />
-
- {site.data && }
- `xlog-post-tag-${tag}`,
- ),
- )}
- >
- {children}
-
- {site.data && (
-
-
-
- )}
-
-
-
-
-
-
- )
-}
diff --git a/src/components/site/SitePage.tsx b/src/components/site/SitePage.tsx
index 892053b0..1186894c 100644
--- a/src/components/site/SitePage.tsx
+++ b/src/components/site/SitePage.tsx
@@ -1,21 +1,23 @@
-import { useTranslation } from "next-i18next"
-import Head from "next/head"
+import { TFunction } from "i18next"
import serialize from "serialize-javascript"
+import { PageContent } from "~/components/common/PageContent"
+import { PostFooter } from "~/components/site/PostFooter"
+import { PostMeta } from "~/components/site/PostMeta"
import { getSiteLink } from "~/lib/helpers"
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
-import { PageContent } from "../common/PageContent"
-import { PostFooter } from "./PostFooter"
-import { PostMeta } from "./PostMeta"
-
-export const SitePage: React.FC<{
+export function SitePage({
+ page,
+ site,
+ preview,
+ t,
+}: {
page?: ExpandedNote
site?: ExpandedCharacter
preview?: boolean
-}> = ({ page, site, preview }) => {
- const { t } = useTranslation("site")
-
+ t: TFunction
+}) {
function addPageJsonLd() {
return {
__html: serialize({
@@ -42,12 +44,10 @@ export const SitePage: React.FC<{
return (
<>
-
-
-
+
{preview && (
{t(
diff --git a/src/components/site/SiteSearch.tsx b/src/components/site/SiteSearch.tsx
index a9d6a806..396c3088 100644
--- a/src/components/site/SiteSearch.tsx
+++ b/src/components/site/SiteSearch.tsx
@@ -1,53 +1,51 @@
-import { useTranslation } from "next-i18next"
+"use client"
+
import Link from "next/link"
-import { useRouter } from "next/router"
+import { useParams, useRouter, useSearchParams } from "next/navigation"
import { useEffect, useState } from "react"
import reactStringReplace from "react-string-replace"
import { Button } from "~/components/ui/Button"
import { Image } from "~/components/ui/Image"
import { useDate } from "~/hooks/useDate"
-import { ExpandedNote } from "~/lib/types"
+import { useTranslation } from "~/lib/i18n/client"
+import { useGetSearchPagesBySite } from "~/queries/page"
+import { useGetSite } from "~/queries/site"
import { EmptyState } from "../ui/EmptyState"
-export const SiteSearch: React.FC<{
- postPages?: {
- list: ExpandedNote[]
- count: number
- cursor: string | null
- }[]
- fetchNextPage: () => void
- hasNextPage?: boolean
- isFetchingNextPage?: boolean
- keyword?: string
-}> = ({
- postPages,
- fetchNextPage,
- hasNextPage,
- isFetchingNextPage,
- keyword,
-}) => {
+export const SiteSearch: React.FC = () => {
const router = useRouter()
- const { t } = useTranslation(["common", "site"])
+ const { t } = useTranslation("site")
const date = useDate()
+ const searchParams = useSearchParams()
+ const params = useParams()
+ const site = useGetSite(params?.site as string)
+ const keyword = searchParams?.get("q") || undefined
+ const posts = useGetSearchPagesBySite({
+ characterId: site.data?.characterId,
+ keyword,
+ })
+
const [isMounted, setIsMounted] = useState(false)
useEffect(() => {
setIsMounted(true)
}, [])
- if (!postPages?.length) return null
-
let currentLength = 0
return (
<>
- {!postPages[0].count &&
}
- {!!postPages[0].count && (
+
+ {posts.data?.pages?.[0].count || "0"} {t("results")}
+
+ {posts.isLoading && <>{t("Loading")}...>}
+ {!posts.data?.pages?.[0].count &&
}
+ {!!posts.data?.pages?.[0].count && (
- {postPages.map((posts) =>
+ {posts.data?.pages.map((posts) =>
posts.list.map((post) => {
currentLength++
return (
@@ -136,20 +134,20 @@ export const SiteSearch: React.FC<{
)}
)}
- {hasNextPage && (
+ {posts.hasNextPage && posts.data?.pages[0].count && (
)}
diff --git a/src/components/ui/BoxRadio.tsx b/src/components/ui/BoxRadio.tsx
index c86fe09c..444e234f 100644
--- a/src/components/ui/BoxRadio.tsx
+++ b/src/components/ui/BoxRadio.tsx
@@ -1,5 +1,4 @@
import { nanoid } from "nanoid"
-import { useTranslation } from "next-i18next"
import React, {
ChangeEvent,
Dispatch,
@@ -10,6 +9,7 @@ import React, {
} from "react"
import { Input } from "~/components/ui/Input"
+import { useTranslation } from "~/lib/i18n/client"
import { cn } from "~/lib/utils"
export type RadioItem = {
@@ -23,7 +23,7 @@ export const BoxRadio: React.FC<{
setValue: Dispatch
>
items: RadioItem[]
}> = ({ value, setValue, items }) => {
- const { t } = useTranslation(["common"])
+ const { t } = useTranslation("common")
const randomId = useMemo(() => nanoid(), [])
const [isCustom, setIsCustom] = useState(false)
diff --git a/src/components/ui/Button.tsx b/src/components/ui/Button.tsx
index 0420f3d6..f3ac97cf 100644
--- a/src/components/ui/Button.tsx
+++ b/src/components/ui/Button.tsx
@@ -1,3 +1,5 @@
+"use client"
+
import React, { memo } from "react"
import { cn } from "~/lib/utils"
diff --git a/src/components/ui/EditorToolbar.tsx b/src/components/ui/EditorToolbar.tsx
index 579a4058..ad200326 100644
--- a/src/components/ui/EditorToolbar.tsx
+++ b/src/components/ui/EditorToolbar.tsx
@@ -1,4 +1,3 @@
-import { useTranslation } from "next-i18next"
import { FC, memo, useCallback, useState } from "react"
import { usePopper } from "react-popper"
@@ -6,6 +5,7 @@ import { EditorView } from "@codemirror/view"
import { Popover } from "@headlessui/react"
import { ICommand } from "~/editor"
+import { useTranslation } from "~/lib/i18n/client"
import { Tooltip } from "./Tooltip"
diff --git a/src/components/ui/FAB.tsx b/src/components/ui/FAB.tsx
index f50e277a..1758d52e 100644
--- a/src/components/ui/FAB.tsx
+++ b/src/components/ui/FAB.tsx
@@ -1,3 +1,5 @@
+"use client"
+
import React, { FC, PropsWithChildren, useEffect, useState } from "react"
import { useGetState } from "~/hooks/useGetState"
diff --git a/src/components/ui/Image.tsx b/src/components/ui/Image.tsx
index 4ec7e3d1..8252d31d 100644
--- a/src/components/ui/Image.tsx
+++ b/src/components/ui/Image.tsx
@@ -1,3 +1,5 @@
+"use client"
+
import { ImageProps, default as NextImage } from "next/image"
import React, { useEffect } from "react"
diff --git a/src/components/ui/Mermaid.tsx b/src/components/ui/Mermaid.tsx
index 6ec8120c..2259ecb6 100644
--- a/src/components/ui/Mermaid.tsx
+++ b/src/components/ui/Mermaid.tsx
@@ -1,3 +1,5 @@
+"use client"
+
import { nanoid } from "nanoid"
import { FC, memo, useEffect, useState } from "react"
diff --git a/src/components/ui/Tabs.tsx b/src/components/ui/Tabs.tsx
index 0771cec1..92fabb01 100644
--- a/src/components/ui/Tabs.tsx
+++ b/src/components/ui/Tabs.tsx
@@ -1,7 +1,7 @@
-import { useTranslation } from "next-i18next"
import React from "react"
import { Tooltip } from "~/components/ui/Tooltip"
+import { useTranslation } from "~/lib/i18n/client"
import { cn } from "~/lib/utils"
import { UniLink } from "./UniLink"
@@ -19,7 +19,7 @@ export const Tabs: React.FC<{ items: TabItem[]; className?: string }> = ({
items,
className,
}) => {
- const { t } = useTranslation(["dashboard"])
+ const { t } = useTranslation("dashboard")
return (
{
+ 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
+}
diff --git a/src/hooks/useLang.ts b/src/hooks/useLang.ts
new file mode 100644
index 00000000..369d2adc
--- /dev/null
+++ b/src/hooks/useLang.ts
@@ -0,0 +1,13 @@
+import { useContext } from "react"
+
+import { LangContext, LangContextType } from "~/providers/LangProvider"
+
+export function useLang(): LangContextType {
+ const context = useContext(LangContext)
+
+ if (!context) {
+ throw new Error("useLang must be used within a LangProvider")
+ }
+
+ return context
+}
diff --git a/src/hooks/useNProgress.ts b/src/hooks/useNProgress.ts
new file mode 100644
index 00000000..decba3e5
--- /dev/null
+++ b/src/hooks/useNProgress.ts
@@ -0,0 +1,25 @@
+"use client"
+
+import Progress from "qier-progress"
+import { useEffect, useRef } from "react"
+
+import { useAppRouterEventerListener } from "./useRouterEvents"
+
+export const useNProgress = () => {
+ const events = useAppRouterEventerListener()
+
+ const instance = useRef(
+ new Progress({ color: "var(--theme-color)", colorful: false }),
+ )
+ useEffect(() => {
+ const disposers = [] as any[]
+
+ disposers.push(
+ events.onStart(() => {
+ instance.current.start()
+ }),
+ )
+ disposers.push(events.onComplete(() => instance.current.finish()))
+ return () => disposers.forEach((disposer) => disposer())
+ }, [])
+}
diff --git a/src/hooks/useRouterEvents.ts b/src/hooks/useRouterEvents.ts
new file mode 100644
index 00000000..93027b7f
--- /dev/null
+++ b/src/hooks/useRouterEvents.ts
@@ -0,0 +1,98 @@
+import { usePathname, useRouter, useSearchParams } from "next/navigation"
+import { useEffect, useRef, useState } from "react"
+
+import { pick } from "~/lib/utils"
+
+interface RouterNavigationEvent {}
+
+type RouterEventFunction = (e: RouterNavigationEvent) => void
+
+// TODO detect error event
+export const useAppRouterEventerListener = () => {
+ const [isRouterComplete, setIsRouterComplete] = useState(false)
+
+ const startChangeCallback = () => {
+ setIsRouterComplete(false)
+ eventsRegisters.current.onStartQ.forEach(($) => $(buildEvent()))
+ }
+ const router = useRouter()
+ useEffect(() => {
+ const rawPush = router.push
+ const rawReplace = router.replace
+
+ const popstateHandler = () => {
+ startChangeCallback()
+ }
+
+ window.addEventListener("popstate", popstateHandler)
+
+ router.push = (...rest) => {
+ startChangeCallback()
+
+ // eslint-disable-next-line prefer-spread
+ rawPush.apply(null, rest)
+ }
+
+ router.replace = (...rest) => {
+ startChangeCallback()
+
+ // eslint-disable-next-line prefer-spread
+ rawReplace.apply(null, rest)
+ }
+
+ return () => {
+ router.push = rawPush
+ router.replace = rawReplace
+
+ window.removeEventListener("popstate", popstateHandler)
+ }
+ }, [])
+
+ const eventsRegisters = useRef({
+ onStartQ: [] as RouterEventFunction[],
+ // onErrorQ: [] as RouterEventFunction[],
+ onCompleteQ: [] as RouterEventFunction[],
+ onStart(cb: RouterEventFunction) {
+ eventsRegisters.current.onStartQ.push(cb)
+ return () => {
+ eventsRegisters.current.onStartQ =
+ eventsRegisters.current.onStartQ.filter(($) => $ !== cb)
+ }
+ },
+
+ onComplete(cb: RouterEventFunction) {
+ eventsRegisters.current.onCompleteQ.push(cb)
+ return () => {
+ eventsRegisters.current.onCompleteQ =
+ eventsRegisters.current.onCompleteQ.filter(($) => $ !== cb)
+ }
+ },
+ })
+
+ const buildEvent = (): RouterNavigationEvent => {
+ return {
+ url: location.pathname + location.search,
+ }
+ }
+
+ useEffect(() => {
+ if (!isRouterComplete) return
+
+ eventsRegisters.current.onCompleteQ.forEach(($) => $(buildEvent()))
+ }, [isRouterComplete])
+
+ const currentPathname = usePathname()
+ const searchParams = useSearchParams()
+
+ useEffect(() => {
+ if (
+ currentPathname === location.pathname &&
+ searchParams?.toString() ===
+ new URLSearchParams(location.search).toString()
+ ) {
+ setIsRouterComplete(true)
+ }
+ }, [currentPathname, searchParams])
+
+ return pick(eventsRegisters.current, ["onStart", "onComplete"])
+}
diff --git a/src/lib/default-slug.ts b/src/lib/default-slug.ts
index 633621f5..cd13f308 100644
--- a/src/lib/default-slug.ts
+++ b/src/lib/default-slug.ts
@@ -1,11 +1,12 @@
-import pinyin from "pinyin"
+import { pinyin } from "pinyin-pro"
export const getDefaultSlug = (title: string, id?: string) => {
let generated =
- pinyin(title as string, {
- style: pinyin.STYLE_NORMAL,
- compact: true,
- })?.[0]
+ pinyin(title, {
+ toneType: "none",
+ type: "array",
+ nonZh: "consecutive",
+ })
?.map((word) => word.trim())
?.filter((word) => word)
?.join("-")
diff --git a/src/lib/env.ts b/src/lib/env.ts
index 78fe1beb..cef5ede6 100644
--- a/src/lib/env.ts
+++ b/src/lib/env.ts
@@ -1,6 +1,8 @@
import { IS_PROD } from "./constants"
export const APP_NAME = process.env.NEXT_PUBLIC_APP_NAME || "xLog"
+export const APP_SLOGAN =
+ process.env.NEXT_PUBLIC_APP_SLOGAN || "Write. Own. Earn."
export const OUR_DOMAIN =
process.env.NEXT_PUBLIC_OUR_DOMAIN || process.env.NEXT_PUBLIC_VERCEL_URL
export const SCORE_API_DOMAIN = process.env.NEXT_PUBLIC_SCORE_API_DOMAIN
diff --git a/src/lib/helpers.ts b/src/lib/helpers.ts
index 67efac86..e8bc3379 100644
--- a/src/lib/helpers.ts
+++ b/src/lib/helpers.ts
@@ -1,4 +1,4 @@
-import { NoteEntity } from "crossbell.js"
+import type { NoteEntity } from "crossbell.js"
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
@@ -15,7 +15,7 @@ export const getSiteLink = ({
subdomain: string
noProtocol?: boolean
}) => {
- if (IS_VERCEL_PREVIEW) return `/_site/${subdomain}`
+ if (IS_VERCEL_PREVIEW) return `/site/${subdomain}`
if (domain) {
return `https://${domain}`
@@ -30,11 +30,11 @@ export const getSiteLink = ({
export const getSlugUrl = (slug: string) => {
if (!isServerSide() && IS_VERCEL_PREVIEW) {
const pathArr = new URL(location.href).pathname.split("/").filter(($) => $)
- const indicatorIndex = pathArr.findIndex(($) => $ === "_site")
+ const indicatorIndex = pathArr.findIndex(($) => $ === "site")
if (-~indicatorIndex) {
const handle = pathArr[indicatorIndex + 1]
- return `/_site/${handle}${slug}`
+ return `/site/${handle}${slug}`
}
}
diff --git a/src/lib/i18n/client.ts b/src/lib/i18n/client.ts
new file mode 100644
index 00000000..f3825ac8
--- /dev/null
+++ b/src/lib/i18n/client.ts
@@ -0,0 +1,35 @@
+"use client"
+
+import i18next from "i18next"
+import resourcesToBackend from "i18next-resources-to-backend"
+import {
+ initReactI18next,
+ useTranslation as useTranslationOrg,
+} from "react-i18next"
+import { Trans as TransW } from "react-i18next/TransWithoutContext"
+
+import { useLang } from "~/hooks/useLang"
+
+import { defaultNS, getOptions } from "./settings"
+
+i18next
+ .use(initReactI18next)
+ .use(
+ resourcesToBackend(
+ (language: string, namespace: string) =>
+ import(`./locales/${language}/${namespace}.json`),
+ ),
+ )
+ .init({
+ ...getOptions(),
+ })
+
+export function useTranslation(ns: string = defaultNS) {
+ const { lang } = useLang()
+ if (i18next.resolvedLanguage !== lang) {
+ i18next.changeLanguage(lang)
+ }
+ return useTranslationOrg(ns)
+}
+
+export const Trans = TransW
diff --git a/src/lib/i18n/index.ts b/src/lib/i18n/index.ts
new file mode 100644
index 00000000..a8c99868
--- /dev/null
+++ b/src/lib/i18n/index.ts
@@ -0,0 +1,33 @@
+import { createInstance } from "i18next"
+import resourcesToBackend from "i18next-resources-to-backend"
+import { Trans as TransW } from "react-i18next/TransWithoutContext"
+import { initReactI18next } from "react-i18next/initReactI18next"
+
+import { useAcceptLang } from "~/hooks/useAcceptLang"
+
+import { defaultNS, getOptions } from "./settings"
+
+const initI18next = async (lng: string, ns: string) => {
+ const i18nInstance = createInstance()
+ await i18nInstance
+ .use(initReactI18next)
+ .use(
+ resourcesToBackend(
+ (language: string, namespace: string) =>
+ import(`./locales/${language}/${namespace}.json`),
+ ),
+ )
+ .init(getOptions(lng, ns))
+ return i18nInstance
+}
+
+export async function useTranslation(ns: string = defaultNS) {
+ const lang = useAcceptLang()
+ const i18nextInstance = await initI18next(lang, ns)
+ return {
+ t: i18nextInstance.getFixedT(lang, ns),
+ i18n: i18nextInstance,
+ }
+}
+
+export const Trans = TransW
diff --git a/src/lib/i18n/locales/en/common.json b/src/lib/i18n/locales/en/common.json
new file mode 100644
index 00000000..34fbe517
--- /dev/null
+++ b/src/lib/i18n/locales/en/common.json
@@ -0,0 +1,5 @@
+{
+ "intlDateTime": "{{val, datetime}}",
+ "ago": "{{time}} ago",
+ "joined ago": "Joined {{time}} ago"
+}
\ No newline at end of file
diff --git a/src/lib/i18n/locales/en/dashboard.json b/src/lib/i18n/locales/en/dashboard.json
new file mode 100644
index 00000000..7a21d580
--- /dev/null
+++ b/src/lib/i18n/locales/en/dashboard.json
@@ -0,0 +1,5 @@
+{
+ "link post-vs-page" : "https://wordpress.com/zh-cn/support/post-vs-page/",
+ "delete_confirmation_post": "Are you sure you want to DELETE this post?",
+ "delete_confirmation_page": "Are you sure you want to DELETE this page?"
+}
\ No newline at end of file
diff --git a/src/lib/i18n/locales/en/index.json b/src/lib/i18n/locales/en/index.json
new file mode 100644
index 00000000..4ef89d59
--- /dev/null
+++ b/src/lib/i18n/locales/en/index.json
@@ -0,0 +1,29 @@
+{
+ "features": {
+ "Write": {
+ "subtitle": "Write when inspiration strikes",
+ "description": "Frees you from time-consuming, unnecessary processes that slow your writing, so you and your team can focus on creating."
+ },
+ "Own": {
+ "subtitle": "Own content on the blockchain",
+ "description": "You own your content by publishing content on the Crossbell blockchain. xLog won't store any data and can't take away or modify your rights and content, even if xLog wanted to.
Learn how Crossbell works.",
+ "extra": {
+ "title": "Don't Trust, Verify",
+ "description": "It's a good habit not to trust anyone easily on the Internet, including xLog. Therefore, we strongly recommend that you visit any site on xLog, check the transaction history at the bottom of the page, read the blockchain contract code, and verify for yourself what xLog claims.",
+ "button": "Verify it"
+ }
+ },
+ "Earn": {
+ "subtitle": "Earn tokens and rights",
+ "description": "Great content deserves to be rewarded, and a blockchain offers the wonderful opportunity for creators to be rewarded transparently and fairly. An xLog DAO will further promote the growth of our community."
+ }
+ },
+ "Easy to get started text": "xLog supports
web3 wallets and
email connections, allowing you to quickly create a customized blog with features like a custom domain, subscriptions, comments, NFT minting, RSS feeds, and AI enhancements in just 5 minutes - with no application or costs required.",
+ "Elegant experience text": "xLog offers a dual-pane editor with
real-time preview for an excellent writing experience, using standard
Markdown syntax with support for
HTML,
audio/video, and math expressions. The elegantly designed page provides a comfortable reading experience for readers.",
+ "Fast text": "Blockchain doesn't always mean low efficiency. xLog operates at peak performance thanks to its efficient caching mechanism and numerous optimizations. The site also supports
PWA for local installation and use.",
+ "Safe text": "All blog data, including configs, posts, subscriptions, comments, etc., are signed and securely stored on the
blockchain with your own hands, with control only accessible through the
private key held by yourself. No one else, including xLog, can make any changes.",
+ "Customizable text": "You're free to use your
own domain, customize your website, and
design it in any way you like. This is your website, with no restrictions. xLog encourage and will provide a rich system of
themes and plugins to help you customize it.",
+ "Open text": "xLog offers import and export tools, as well as rich
APIs and third-party
integrations. All code is
open source on GitHub, and all data is
transparent on the blockchain. xLog has nothing to hide.",
+ "Creator Incentives text": "In the early stages, xLog provides
incentives in
MIRA. We are actively constructing the tokenomics to ensure xLog remain the best place to publish high-quality content.",
+ "DAO text": "An xLog DAO will be in place, where creators vote with
tokens."
+}
diff --git a/src/lib/i18n/locales/en/site.json b/src/lib/i18n/locales/en/site.json
new file mode 100644
index 00000000..0fe088ce
--- /dev/null
+++ b/src/lib/i18n/locales/en/site.json
@@ -0,0 +1,4 @@
+{
+ "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."
+}
\ No newline at end of file
diff --git a/src/lib/i18n/locales/ja/common.json b/src/lib/i18n/locales/ja/common.json
new file mode 100644
index 00000000..2dd0cc98
--- /dev/null
+++ b/src/lib/i18n/locales/ja/common.json
@@ -0,0 +1,59 @@
+{
+ "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 ãĒããŽãđ",
+ "Creation": "ä―æ",
+ "Last Update": "æåūæīæ°",
+ "Create Character": "æ°čĶããĢãĐãŊãŋãž",
+ "Submit": "éäŋĄ",
+ "Reply": "čŋäŋĄ",
+ "Cancel Reply": "ããĢãģãŧãŦ",
+ "ago": "{{time}}å",
+ "joined ago": "{{time}}ãŦåå ãã",
+ "Mint to an NFT": "NFTãĻããĶããģã",
+ "Like successfully": "ãããæļ",
+ "Mint successfully": "ããģãæļ",
+ "like stored": "ããããŊåŪå
ĻãŦããããŊãã§ãžãģãŦäŋåãããūããã <2>Crossbell Scan2> ã§įĒščŠã§ããūãã",
+ "mint stored": "ããŪčĻäšãNFTãĻããĶããģãããūããã <2>xChar2> ãūããŊ <6>Crossbell Scan6> ã§įĒščŠã§ããūãã",
+ "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": "ããĢãĻčĄĻįĪšãã"
+}
diff --git a/src/lib/i18n/locales/ja/dashboard.json b/src/lib/i18n/locales/ja/dashboard.json
new file mode 100644
index 00000000..e33daacd
--- /dev/null
+++ b/src/lib/i18n/locales/ja/dashboard.json
@@ -0,0 +1,124 @@
+{
+ "Dashboard": "ããã·ãĨããžã",
+ "Posts": "æįŦ ",
+ "Pages": "ããžãļ",
+ "Notifications": "éįĨ",
+ "Unread notifications": "æŠčŠãŪéįĨ",
+ "Settings": "čĻåŪ",
+ "Site Stats": "ãĩãĪããŪįĩąčĻæ
å ą",
+ "Total Posts": "čĻäšæ°",
+ "Total Comments": "ãģãĄãģãæ°",
+ "Total Followers": "ããĐããŊãžæ°",
+ "Total Views": "éēčͧæ°",
+ "Site Duration": "ãĩãĪããŪéåķæé",
+ "days": "æĨ",
+ "Deleted!": "åéĪãããūããïž",
+ "Fail to Deleted.": "åéĪã§ããūããã§ããã",
+ "Converted!": "åĪæãããūããïž",
+ "Failed to convert.": "åĪæãŦåĪąæããūããã",
+ "All Posts": "ããđãĶãŪčĻäš",
+ "All Pages": "ããđãĶãŪããžãļ",
+ "Published": "å
Žéæļãŋ",
+ "published": "å
Žéæļãŋ",
+ "Draft": "äļæļã",
+ "draft": "äļæļã",
+ "Scheduled": "äšįīæļãŋ",
+ "scheduled": "äšįīæļãŋ",
+ "published and local modified": "å
Žéæļãŋã§ãããžãŦãŦã§ãŪį·Ļéãã",
+ "link post-vs-page": "https://wordpress.com/ja/support/post-vs-page/",
+ "posts description": "čĻäšãŊãæéé ãŦčĄĻįĪšãããūããčŠč
ãŦæ°ãããģãģããģããæäūãįķããããĻã§ãããã°ãŪããĐããžãŊãåĒããã§ãããã<2>čĻäš2>",
+ "pages description": "ããžãļãŊæĨäŧãŦå―ąéŋãåããããĻããããūããããį§ããĄãŦãĪããĶãããåãåããããŠãĐãŪãããŦããžãļãæīŧįĻããūãããã<2>ããžãļ2>",
+ "pages add": "ããžãļãä―æããåūã<2>ãããēãžã·ã§ãģãĄããĨãžãŦčŋ―å ãã2>ããĻãã§ããūãããããēãžã·ã§ãģãĄããĨãžãŦčŋ―å ããããĻã§čĻŠåč
ããããčĶãĪãããããŠããūãã",
+ "New Post": "æ°ããčĻäš",
+ "New Page": "æ°ããããžãļ",
+ "Import": "ãĪãģããžã",
+ "Import markdown file with front matter supported": "Markdown ããĄãĪãŦïžFront MatterïžããĪãģããžãããūãã",
+ "View Site": "ãĩãĪããčĄĻįĪš",
+ "hello": {
+ "welcome": "
ãããŦãĄãŊã
xLog ããåĐįĻããã ãããããĻãããããūãïž
xLog ãä―ŋãå§ãããããŪãããĪããŪäūŋåĐãŠãŠãģãŊãäŧĨäļãŦãããūãã
",
+ "community": "
ãģããĨãããĢãŦåå ããĶãæ°ããåéãä―ãĢãããxLog ãäļį·ãŦä―ãäļãããããūãããã
"
+ },
+ "Convert to Page": "ããžãļãŦåĪæ",
+ "Convert to Post": "čĻäšãŦåĪæ",
+ "Select All": "ããđãĶéļæ",
+ "Deselect All": "éļæč§ĢéĪ",
+ "Delete": "åéĪ",
+ "Heading": "čĶåšã",
+ "Bold": "åĪŠå",
+ "Italic": "æä―",
+ "Strikethrough": "åãæķãį·",
+ "Underline": "äļį·",
+ "Quote": "åžįĻ",
+ "Inline Code": "ãĪãģãĐãĪãģãģãžã",
+ "Code Block": "ãģãžãããããŊ",
+ "Unordered List": "įŪæĄæļã",
+ "Ordered List": "įŠå·äŧããŠãđã",
+ "Link": "ãŠãģãŊ",
+ "Image": "įŧå",
+ "Upload Image": "įŧåããĒããããžã",
+ "Tip: xLog Flavored Markdown": "ããģãïžxLog éĒĻãŪããžãŊããĶãģ",
+ "Preview": "ããŽããĨãž",
+ "Publish": "å
Žé",
+ "Update": "æīæ°",
+ "Discard Changes": "åĪæīãį īæĢ",
+ "Publish at": "å
ŽéæĨæ",
+ "This post will be accessible from this time": "ããŪčĻäšãŊãããŪæéãããĒãŊãŧãđåŊč―ãŦãŠããūã",
+ "This page will be accessible from this time": "ããŪããžãļãŊãããŪæéãããĒãŊãŧãđåŊč―ãŦãŠããūã",
+ "Post slug": "čĻäšãđãĐãã°",
+ "Page slug": "ããžãļãđãĐãã°",
+ "This post will be accessible at": "ããŪčĻäšãŊäŧĨäļãŪãŠãģãŊãããĒãŊãŧãđåŊč―ãŦãŠããūã",
+ "This page will be accessible at": "ããŪããžãļãŊäŧĨäļãŪãŠãģãŊãããĒãŊãŧãđåŊč―ãŦãŠããūã",
+ "Tags": "ãŋã°",
+ "Separate multiple tags with English commas": "čĪæ°ãŪãŋã°ãåč§ãŪãģãģãïž,ïžã§åšåãĢãĶå
ĨåããĶãã ãã",
+ "Excerpt": "æįē",
+ "Leave it blank to use auto-generated excerpt": "čŠåįæãããæįēãä―ŋįĻããå īåãŊįĐšį―ãŪãūãūãŦããĶãã ãã",
+ "General": "äļčŽ",
+ "Social Platforms": "ã―ãžã·ãĢãŦããĐããããĐãžã ",
+ "Navigation": "ãããēãžã·ã§ãģ",
+ "Domains": "ããĄãĪãģ",
+ "Custom CSS": "ãŦãđãŋã CSS",
+ "Operators": "ãŠããŽãžãŋãž",
+ "Export data": "ããžãŋãŪãĻãŊãđããžã",
+ "Site Settings": "ãĩãĪãčĻåŪ",
+ "Icon": "ãĒãĪãģãģ",
+ "Banner": "ãããž",
+ "Supports both pictures and videos.": "įŧåãĻåįŧãŪäļĄæđãŦåŊūåŋããĶããūãã",
+ "Name": "åå",
+ "Description": "芎æ",
+ "Integrate Google Analytics": "Google Analytics ãĻįīäŧãããūããMeasurement ID ãŪæĪįīĒæđæģãŦãĪããĶãŊã<2>ããĄããŪæįŦ 2>ããčͧãã ããã",
+ "Integrate Umami Cloud Analytics": "Umami Cloud Analytics ãĻįīäŧãããūããWebsite ID ãŪæĪįīĒæđæģãŦãĪããĶãŊã<2>ããĄããŪæįŦ 2>ããčͧãã ããã",
+ "Save": "äŋå",
+ "Tips": "ããģã",
+ "social tips": {
+ "p1": "įīäŧãããããĐããããĐãžã ãŪæĻčãŊãįŧéĒãŪåģäļé
ãŦčĄĻįĪšãããūãã",
+ "p2": "xLog ãŊ<2>ããĄããŪããĐããããĐãžã 2>ãĻįīäŧãåŊč―ã§ããïžįīäŧããŦæåãããĻčŠåįãŦããŪããīãĻãŠãģãŊãčĄĻįĪšãããūããïžããŪäŧãŪããĐããããĐãžã ãŪå īåããããĐãŦããŪããīãčĄĻįĪšãããūãããããĩããžãããĶæŽēããããĐããããĐãžã ããããūããããæŊé Issue ããããŊ PR ãä―æããĶãxLog ãããåŪæåšĶãŪéŦãããĐããããĐãžã ãŦãããūãããã",
+ "p3": "<2>xSync2>ãä―ŋįĻããĶãTwitterãTelegramãMediumãSubstack ãŠãĐãŪããĐããããĐãžã ãŦįīäŧããããģãģããģããčŠåįãŦåæããããĻãã§ããūããåæčĻåŪãčĄãĢãå īåããããŦãčĄĻįĪšãããūãã"
+ },
+ "Platform": "ããĐããããĐãžã ",
+ "Identity": "ãĒãĪããģããĢããĢ",
+ "Remove": "åéĪ",
+ "New Item": "æ°ãããĒãĪãã ",
+ "xLog provides some out-of-the-box built-in pages": "xLog ãŦãŊãããĪããä―ŋããããžãļããģããŽãžãããããūãã",
+ "Home page": "ããžã ããžãļ",
+ "Archives page": "ãĒãžãŦãĪãããžãļ",
+ "Tag page": "ãŋã°ããžãļ",
+ "NFT Showcase page": "NFT ã·ã§ãžãąãžãđããžãļ",
+ "Label": "ãĐããŦ",
+ "URL": "URL",
+ "subdomain": "ãĩãããĄãĪãģ",
+ "Custom Domain": "ãŦãđãŋã ããĄãĪãģ",
+ "Set the following record on your DNS provider to active your custom domain": "ãŦãđãŋã ããĄãĪãģãæåđãŦãããŦãŊãDNS ããããĪããžãŦæŽĄãŪãŽãģãžããčĻåŪããĶãã ããã",
+ "Scope: These styles will be applied to your entire blog, including this dashboard.": "éĐįĻįŊåēïžããããŪãđãŋãĪãŦãŊãããŪããã·ãĨããžããåŦãããã°å
Ļä―ãŦéĐįĻãããūãã",
+ "Support": "ãĩããžã",
+ "CSS variables: xLog provides some built-in CSS variables": "CSS åĪæ°ïžxLog ãŊãããĪããŪããŦããĪãģ CSS åĪæ°ãæäūããĶããūãã",
+ "Address": "ä―æ",
+ "Character": "ããĢãĐãŊãŋãž",
+ "New operator": "æ°ãããŠããŽãžãŋãž",
+ "Operator Address": "ãŠããŽãžãŋãžãĒããŽãđ",
+ "Operator Character Check": "ãŠããŽãžãŋãžããĢãĐãŊãŋãžãã§ããŊ",
+ "Warning": "čĶå",
+ "Operators have permissions to enter your dashboard, change your settings(excluding xLog subdomain) and post, modify, delete contents on your site.": "ã·ãĢããžčŠčĻžã§ãŊãããã·ãĨããžããļãŪãĒãŊãŧãđãčĻåŪãŪåĪæīïžxLog ãĩãããĄãĪãģãéĪãïžããĩãĪããŪãģãģããģããŪå
ŽéãŧäŋŪæĢãŧåéĪãåŊč―ã§ãã",
+ "Add": "æ°čĶ",
+ "delete_confirmation_post": "ããŪčĻäšãåéĪããĶãããããã§ããïž",
+ "delete_confirmation_page": "ããŪããžãļãåéĪããĶãããããã§ããïž"
+}
\ No newline at end of file
diff --git a/src/lib/i18n/locales/ja/index.json b/src/lib/i18n/locales/ja/index.json
new file mode 100644
index 00000000..057af974
--- /dev/null
+++ b/src/lib/i18n/locales/ja/index.json
@@ -0,0 +1,26 @@
+{
+ "Blog Free": "čŠįąãŦåĩä―ãã",
+ "description": "<0>xLog0>ãŊã芰ã§ããåĐįĻã§ãã<3>ããããŊãã§ãžãģ3>äļãŪ<7>ãŠãžããģã―ãžãđ7>ãŪããã°ãģããĨãããĢã§ãã",
+ "Get my xLog in 5 minutes": "5åã§čŠåãŪxLogãæãŦå
Ĩããã",
+ "Features": "įđåūī",
+ "Showcase": "ã·ã§ãžãąãžãđ",
+ "Integration": "įĩąå",
+ "Source Code": "ã―ãžãđãģãžã",
+ "Visit": "čĻŠå",
+ "Easy": "į°Ąå",
+ "Easy description": "
Web3ãĶãĐãŽãããūããŊ
ãĄãžãŦãĒããŽãđã§æĨįķãããŦãđãŋã ããĄãĪãģããĩããđãŊãŠãã·ã§ãģããããããģãĄãģããNFTéģé ãRSSãŠãĐãŪæĐč―ãåããåäššãŪãĶã§ããĩãĪããį°ĄåãŦæ§įŊã§ããūããæéãåķéãŊäļåãããūããã",
+ "Safe": "åŪå
Ļ",
+ "Safe description": "æ§æãčĻäšããĩããđãŊãŠãã·ã§ãģããģãĄãģããŠãĐãããđãĶãŪããã°ããžãŋãŊãããŠãčŠčšŦãį―ēåããĶããããŊãã§ãžãģãŦåŪå
ĻãŦäŋåãããūããããŠãäŧĨåĪãŪ芰ãåĪæīã§ããŠããããŦãŠãĢãĶããūãã",
+ "Fast": "éŦé",
+ "Fast description": "ããããŊãã§ãžãģãä―éãæåģããããã§ãŊãããūãããxLogãŊãéŦéãŠããĢãã·ãĨãĄãŦããšã ãĻåĪæ°ãŪæéĐåæŠį―ŪãæĄįĻããæĨĩéãŪãããĐãžããģãđãįšæŪããūãã",
+ "Customizable": "ãŦãđãŋããĪãšåŊč―",
+ "Customizable description": "čŠåãŪ
ããĄãĪãģãä―ŋįĻãããĶã§ããĩãĪããŪ
ãđãŋãĪãŦããŦãđãŋããĪãšããåŪå
ĻãŦčŠįąãŦãĶã§ããĩãĪããä―æã§ããūãããããŊããŠããŪãĶã§ããĩãĪãã§ãããä―ãŪåķéããããūããã",
+ "Open": "ãŠãžããģ",
+ "Open description": "æĻæšįãŠ
Markdownæ§æãæĄįĻãããĪãģããžã/ãĻãŊãđããžãããžãŦãĻčąåŊãŠAPIãæäūããããĻã§ãį§ŧčĄãį°ĄåãŦãŠããūãããããđãĶãŪãģãžããŊGitHubã§
ãŠãžããģã―ãžãđã§ãããããđãĶãŪ
ããžãŋãéæãŦããããŊãã§ãžãģãŦäŋåãããūãã",
+ "Discover these awesome teams and creators on xLog (sorted by update time)": "xLogã§ããããŪįī æīãããããžã ããŊãžãŦãŠäššããĄãįščĶããūãããïžæīæ°æéé ïž",
+ "Follow All!": "ããđãĶãããĐããžããïž",
+ "Show more": "ããĢãĻčĄĻįĪšãã",
+ "Submit yours": "čŠåãŪãæįĻŋãã",
+ "xLog's open design allows it to integrate with many other open protocols and applications without friction.": "xLogãŪãŠãžããģãŠčĻčĻãŦãããäŧãŪåĪããŪãŠãžããģããããģãŦããĒããŠãąãžã·ã§ãģãĻãđã ãžãšãŦįĩąåããããĻãã§ããūãã",
+ "Dashboard": "ããã·ãĨããžã"
+}
\ No newline at end of file
diff --git a/src/lib/i18n/locales/ja/site.json b/src/lib/i18n/locales/ja/site.json
new file mode 100644
index 00000000..626c40f4
--- /dev/null
+++ b/src/lib/i18n/locales/ja/site.json
@@ -0,0 +1,10 @@
+{
+ "Home": "ããžã ",
+ "Archives": "ãĒãžãŦãĪã",
+ "About": "įīđäŧ",
+ "Tags": "ãŋã°",
+ "load more": "ãūã {{count}}äŧķãŪ{{name}}ããããūãã",
+ "signed and stored on the blockchain": "{{name}}ãŊãåĩä―č
ãŦããĢãĶį―ēåãããããããŊãã§ãžãģãŦåŪå
ĻãŦäŋåãããĶããūãã",
+ "Write a comment on the blockchain": "ããããŊãã§ãžãģã§ãģãĄãģããã",
+ "powered by": "
ããŪãĩãĪããŊã ãŦããĢãĶä―åããūã"
+}
diff --git a/src/lib/i18n/locales/zh-TW/common.json b/src/lib/i18n/locales/zh-TW/common.json
new file mode 100644
index 00000000..37245f2e
--- /dev/null
+++ b/src/lib/i18n/locales/zh-TW/common.json
@@ -0,0 +1,87 @@
+{
+ "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 Scan2> äļæĨį",
+ "mint stored": "ä― å·ēå°éįŊæįŦ įčæ NFTïžåŊäŧĨåĻ <2>xChar2> æ <6>Crossbell Scan6> äļæĨį",
+ "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"
+}
\ No newline at end of file
diff --git a/src/lib/i18n/locales/zh-TW/dashboard.json b/src/lib/i18n/locales/zh-TW/dashboard.json
new file mode 100644
index 00000000..514b3dc3
--- /dev/null
+++ b/src/lib/i18n/locales/zh-TW/dashboard.json
@@ -0,0 +1,198 @@
+{
+ "Dashboard": "æīåŊ",
+ "Posts": "æįŦ ",
+ "Pages": "é éĒ",
+ "Notifications": "éįĨ",
+ "Unread notifications": "æŠčŪéįĨ",
+ "Settings": "čĻåŪ",
+ "Events": "æīŧå",
+ "New Events": "æ°æīŧå",
+ "Site Stats": "įķēįŦįĩąčĻ",
+ "Published posts": "æįŦ ",
+ "Received comments": "įčĻ",
+ "Followers": "įēįĩē",
+ "Viewed": "įčĶ―é",
+ "Received tips": "Donate",
+ "Site Duration": "Blog čŠį",
+ "days": "åĪĐ",
+ "Deleted!": "åŠæäšåĶïž",
+ "Fail to Deleted.": "å éĪåĪąčīĨ",
+ "Converted!": "č―ææåïž",
+ "Failed to convert.": "č―æåĪąæã",
+ "All Posts": "æææįŦ ",
+ "All Pages": "ææé éĒ",
+ "Published": "å·ēįžä―",
+ "published": "å·ēįžä―",
+ "Draft": "čįĻŋ",
+ "draft": "čįĻŋ",
+ "Scheduled": "åŪæįžä―",
+ "scheduled": "åŪæįžä―",
+ "published and local modified": "å·ēįžä―äļæå°æŠæīæ°įå
§åŪđ",
+ "link post-vs-page": "https://wordpress.com/zh-tw/support/post-vs-page/",
+ "posts description": "æįŦ æŊææéååšåĻä― įįķēįŦäļååšįæĒįŪãåŊäŧĨå°åŪåįä―æŊæīæ°ïžäŧĨæäūæ°å
§åŪđįĩĶčŪč
ã<2>æįŦ čé éĒ2>ã",
+ "pages description": "é éĒæŊéæ
įïžäļåæĨæå―ąéŋãåŪåæīåæŊä― įķēįŦäļįæ°ļäđ
å
įī ïžæŊåĶâéæžæåâãâčŊįđŦæåâįã <2>æįŦ čé éĒ2>ã",
+ "pages add": "åĩåŧšé éĒåūïžä― åŊäŧĨå°å
ķ<2>æ·ŧå å°ä― įķēįŦįå°čŠčåŪäļ2>ïžäŧĨäūŋįčĶ―č
åŊäŧĨæūå°åŪã",
+ "New Post": "æ°åŧšæįŦ ",
+ "New Page": "æ°åŧšé éĒ",
+ "Import": "åŊå
Ĩ",
+ "Import markdown file with front matter supported": "åŊå
Ĩ Markdown æäŧķ(æŊæ Front Matter)",
+ "View Site": "æĨįįķēįŦ",
+ "hello": {
+ "welcome": "
ð ä― åĨ―åïž
æĄčŋä―ŋįĻ xLogïž
äŧĨäļæŊäļäšæįĻįéĢįĩïžåđŦåĐä― éå§ä―ŋįĻ xLogïž
",
+ "hidden": "ð ä― åĨ―å",
+ "community": "
å å
ĨæåįįĪūåïžčŠčæ°æåæįĩĶäš xLog ååĐïž
"
+ },
+ "Edit": "į·ĻčžŊ ",
+ "Convert to Page": "č―æįšé éĒ",
+ "Convert to Post": "č―æįšæįŦ ",
+ "Select All": "å
ĻéĻéļæ",
+ "Deselect All": "åæķéļæ",
+ "Delete": "åŠéĪ",
+ "Heading": "æĻéĄ",
+ "Bold": "įēéŦ",
+ "Italic": "æéŦ",
+ "Strikethrough": "åŠéĪį·",
+ "Underline": "åšį·",
+ "Quote": "åžįĻ",
+ "Inline Code": "čĄå
§įĻåžįĒž",
+ "Code Block": "įĻåžįĒžååĄ",
+ "Unordered List": "įĄåšåčĄĻ",
+ "Ordered List": "æåšåčĄĻ",
+ "Link": "éĢįĩ",
+ "Image": "åį",
+ "Upload Image": "äļåģåį",
+ "Help: xLog Flavored Markdown": "åđŦåĐïžxLog éĒĻæ žį Markdown",
+ "Preview": "é čĶ―",
+ "Publish": "įžåļ",
+ "Update": "æīæ°",
+ "Discard Changes": "æĻæĢčŪæī",
+ "Publish at": "įžåļæé",
+ "This post will be accessible from this time": "æĪæįŦ å°åūæĪæééå§åŊįčĶ―",
+ "This page will be accessible from this time": "æĪé éĒå°åūæĪæééå§åŊįčĶ―",
+ "Post slug": "æįŦ įéĢįĩ",
+ "Page slug": "é éĒįéĢįĩ",
+ "This post will be accessible at": "æĪæįŦ å°åŊééäŧĨäļéĢįĩįčĶ―",
+ "This page will be accessible at": "æĪé éĒå°åŊééäŧĨäļéĢįĩįčĶ―",
+ "Tags": "æĻįąĪ",
+ "Separate multiple tags with English commas": "åĪåæĻįąĪčŦįĻčąæéčåé",
+ "Excerpt": "æčĶ",
+ "Leave it blank to use auto-generated excerpt": "įįĐšåä―ŋįĻčŠåįæįæčĶ",
+ "General": "äļčŽ",
+ "Social Platforms": "įĪūäšĪåŠéŦ",
+ "Navigation": "å°čŠ",
+ "Domains": "įķēå",
+ "Custom CSS": "čŠåŪįūĐ CSS",
+ "Operators": "čšŦäŧ―ææŽ",
+ "Export data": "åŊåščģæ",
+ "Site Settings": "įķēįŦčĻį―Ū",
+ "Icon": "åæĻ",
+ "Banner": "æĐŦåđ
",
+ "Supports both pictures and videos.": "æŊæåįåå―ąįã",
+ "Name": "åįĻą",
+ "Description": "čŠæäŧįīđ",
+ "Integrate Google Analytics": "å° Google Analytics æīåčģä― įįķēįŦäļãčŦæį
§<2>éčĢĄ2>į芊ææĨæūä― į Measurement IDã",
+ "Integrate Umami Cloud Analytics": "å° Umami Cloud Analytics æīåčģä― įįķēįŦäļãčŦæį
§<2>éčĢĄ2>į芊ææĨæūä― į Website IDã",
+ "Save": "åēå",
+ "Tips": "æįĪš",
+ "social tips": {
+ "p1": "éäšįĪūäšĪåđģå°æéĄŊįĪšåĻä― į xLog įåģäļč§ã",
+ "p2": "æåæŊæī<2>éäšåđģå°2>ïžčŠåéĄŊįĪšå
ķæĻčŠåéĢįĩãå°æžå
ķäŧåđģå°ïžå°éĄŊįĪšé čĻæĻčŠãåĶæä― æå
ķäŧéæąïžčŦéĻæåæåæäšĪåéĄæPRïžäŧĨæŊæīæīåĪįåđģå°ã",
+ "p3": "ä― éåŊäŧĨåĻ <2>xSync2> äļéĢæĨå° TwitterãTelegram é ŧéãMediumãSubstack įåđģå°ïžäļĶčŠååæĨå
§åŪđãįķä― åĻéĢčĢĄčĻį―ŪåæĨåūïžåŪäđæéĄŊįĪšåĻéčĢĄã"
+ },
+ "Platform": "åđģå°",
+ "Identity": "čšŦäŧ―",
+ "Remove": "į§ŧéĪ",
+ "New Item": "æ°åŧšé
įŪ",
+ "xLog provides some out-of-the-box built-in pages": "xLog æäūäšäļäšéįŪąåģįĻįå
§åŧšé éĒ",
+ "Home page": "éĶé ",
+ "Archives page": "å°åé éĒ",
+ "Tag page": "æĻįąĪé éĒ",
+ "NFT Showcase page": "NFT åąįĪšé éĒ",
+ "Label": "æĻįąĪ",
+ "URL": "éĢįĩ",
+ "subdomain": "åįķēå",
+ "Custom Domain": "čŠčĻįķēå",
+ "Set the following record on your DNS provider to active your custom domain": "åĻä― į DNS æäūåäļčĻåŪäŧĨäļįīéäŧĨåįĻčŠčĻįķēå",
+ "Scope: These styles will be applied to your entire blog, including this dashboard.": "įŊåïžéäšæĻĢåžå°åĨįĻčģä― įæīåéĻč―æ žïžå
æŽæĪåčĄĻæŋã",
+ "Support": "æŊæī",
+ "CSS variables: xLog provides some built-in CSS variables": "CSS čŪæļïžxLog æäūäšäļäšå
§åŧšį CSS čŪæļ",
+ "Address": "å°å",
+ "Character": "čšŦäŧ―",
+ "New operator": "æ°åŧšææŽ",
+ "Operator Address": "ææŽå°å",
+ "Operator Character Check": "čšŦäŧ―ææŽæŠĒæĨ",
+ "Warning": "čĶå",
+ "Operators have permissions to enter your dashboard, change your settings(excluding xLog subdomain) and post, modify, delete contents on your site.": "ææŽč
åŊäŧĨéēå
Ĩä― įåčĄĻæŋïžæīæđä― įčĻåŪïžäļå
æŽ xLog åįķēåïžäļĶåĻä― įįķēįŦäļįžä―ãäŋŪæđãåŠéĪå
§åŪđã",
+ "Add": "æ°åĒ",
+ "xLog News": "xLog æ°č",
+ "More": "æīåĪ",
+ "Meet New Friends": "čŠčæ°æå",
+ "Need help?": "éčĶåđŦåĐïž",
+ "Your post has been securely stored on the blockchain. Now you may want to": "ä― įæįŦ å·ēįķåŪå
Ļå°ååēåĻååĄéäļãįūåĻä― åŊč―æģčĶ",
+ "View the post": "æĨįæįŦ ",
+ "View the transaction": "æĨįäšĪæ",
+ "Share to Twitter": "åäšŦå° Twitter",
+ "Got it, thanks!": "įĨéäšïžčŽčŽïž",
+ "Published!": "å·ēįžä―ïž",
+ "Title goes here...": "æĻéĄåĻéčĢĄ...",
+ "Start writing...": "éå§åŊŦä―...",
+ "Slug can only contain letters, numbers, hyphens, and underscores.": "įįķēååŠč―å
åŦåæŊãæļåãéĢåįŽĶååšį·ã",
+ "Date": "æĨæ",
+ "Prize": "įé",
+ "Winners": "įēįč
",
+ "Learn more": "éēäļæĨäšč§Ģ",
+ "Ended": "å·ēįĩæ",
+ "Upcoming": "åģå°éå§",
+ "Ongoing": "éēčĄäļ",
+ "Import from Markdown files": "åū Markdown æŠæĄåŊå
Ĩ",
+ "Import from Mirror.xyz": "åū Mirror.xyz åŊå
Ĩ",
+ "Select Markdown Files": "éļæ Markdown æŠæĄ",
+ "No files chosen": "æŠé￿пĄ",
+ "No entries": "įĄé
įŪ",
+ "Preview your Mirror.xyz entries": "é čĶ―ä― į Mirror.xyz é
įŪ",
+ "Please select .md files, multiple files are supported, and front matter is supported.": "čŦéļæ .md æŠæĄïžæŊæīåĪåæŠæĄïžäļĶæŊæī front matterã",
+ "Achievements": "æå°ą",
+ "Tokens": "čæŽčēĻåđĢ",
+ "$MIRA is a valuable token in the Crossbell world, and can be easily exchanged on the Crosschain Bridge and Uniswap.": "$MIRA æŊ Crossbell äļįäļįäļįĻŪæįčēīįäŧĢåđĢïžåŊåĻč·ĻéæĐå Uniswap äļčžéŽå
æã",
+ "In the early stage, xLog will use $MIRA to motivate creators.": "åĻæĐæéæŪĩïžxLog å°ä―ŋįĻ $MIRA æŋåĩåĩä―č
ã",
+ "You can obtain $MIRA through the following ways:": "ä― åŊäŧĨééäŧĨäļæđåžįēåū $MIRAïž",
+ "Creator incentive program.": "åĩä―č
æŋåĩčĻįŦã",
+ "Participate in events.": "åå æīŧåã",
+ "Swap from USDC.": "åū USDC å
æã",
+ "Received tips and sponsorships from readers.": "åūčŪč
æķå°æčģåčīåĐã",
+ "Swap to USDC": "å
ææ USDC",
+ "Free Claim": "å
čēŧé å",
+ "$XLOG is related to xLog DAO, but it's too early now, please stay tuned.": "$XLOG č xLog DAO įļéïžä―įūåĻéįšæå°æĐïžæŽčŦæåū
ã",
+ "Stay tuned": "æŽčŦæåū
",
+ "This is a token used for interaction on the Crossbell blockchain, which can be claimed for free from the faucet, so there's no need to worry about its balance.": "éæŊäļåįĻæž Crossbell ååĄéäļäšåįčæŽčēĻåđĢïžåŊäŧĨåūæ°īéūé å
čēŧé åïžå æĪäļįĻæåŋå
ķéĪéĄã",
+ "Balance": "éĪéĄ",
+ "Swap Tutorial": "å
ææåļ",
+ "DNS Checking": "DNS æŠĒæĨäļ",
+ "DNS check passed.": "DNS æŠĒæĨééã",
+ "DNS check failed.": "DNS æŠĒæĨåĪąæã",
+ "Recheck": "éæ°æŠĒæĨ",
+ "Hottest": "įąé",
+ "Latest": "ææ°",
+ "Following": "čŋ―čđĪ",
+ "Create a Post": "æ°åĒæįŦ ",
+ "Change Site Icon or domain": "čŪæīįķēįŦåįĪšæåå",
+ "Join xLog's Discord channel": "å å
Ĩ xLog į Discord é ŧé",
+ "Participate in the development of xLog": "åč xLog įįžåą",
+ "Follow xLog's Twitter": "čŋ―čđĪ xLog į Twitter",
+ "Check out the updates of other bloggers": "įįå
ķäŧ Blogger įæīæ°",
+ "You have already imported them, please enter the post page to create a new post!": "å·ēįķåŊå
ĨåŪæäšåĶïžčŦå°æįŦ é éĒæ°åĒæ°įå
§åŪđïž",
+ "Today": "äŧåĪĐ",
+ "This month": "æŽæ",
+ "This week": "æŽåĻ",
+ "All time": "å
ĻéĻ",
+ "delete_confirmation_post": "ä― įĒščŠčĶåŠéĪéįŊæįŦ åïž",
+ "delete_confirmation_page": "ä― įĒščŠčĶåŠéĪéåé éĒåïž",
+ "Confirm": "įĒščŠ",
+ "Cancel": "åæķ",
+ "Published a new post on my blockchain blog: {{title}}. Check it out now!": "æåĻæįååĄééĻč―æ žįžä―äšäļįæ°įæįŦ ã{{title}}ãïžåŋŦäūįįå§ïž",
+ "Comments": "įčĻ",
+ "You can subscribe to comments through an RSS reader to receive timely reminders.": "ä― åŊäŧĨéé RSS éąčŪåĻčĻéąįčĻïžäŧĨäūŋåææķå°éįĨã",
+ "Subscription address:": "čĻéąå°åïž",
+ "comment on your": "åčĶäšä― į {{type}}ã
{{toTitle}}ã",
+ "{{newCount}} new comments": "{{newCount}} æĒæ°įčĻ"
+}
\ No newline at end of file
diff --git a/src/lib/i18n/locales/zh-TW/index.json b/src/lib/i18n/locales/zh-TW/index.json
new file mode 100644
index 00000000..ea31bccc
--- /dev/null
+++ b/src/lib/i18n/locales/zh-TW/index.json
@@ -0,0 +1,61 @@
+{
+ "Write": "Write",
+ "Own": "Own",
+ "Earn": "Earn",
+ ".": ".",
+ "Look at others'": "įįåĨäšš",
+ "description": "<0>xLog0>ãæä―ģ <7>éæš7> <3>ååĄé3> éĻč―æ žįĪūįūĪã",
+ "Get my xLog in 5 minutes": "äšåéæ°åŊŦčŠæ",
+ "Explore the xLog way": "æĒįīĒ xLog äđč·Ŋ",
+ "features": {
+ "Write": {
+ "subtitle": "éæäūčĻïžįĒįĒæä―",
+ "description": "éæūåĩé åïžæįšæéįŪĄįåΧåļŦïžčŪåŊŦä―æīé æĒïžå°æģĻåĩé æŠäūã"
+ },
+ "Own": {
+ "subtitle": "éäļå
§åŪđïžåŪå
ĻįĄæ",
+ "description": "éé Crossbell ååĄéįžä―ä― įå
§åŪđïžåŪå
Ļææ§ä― įæŽåĐčåŪå
Ļã
xLog äļååēäŧŧä―čģæïžįĄæģååĨŠæäŋŪæđä― įå
§åŪđåæŽåĐïžåģä―ŋ xLog æģéæĻĢåã
äšč§Ģ Crossbell æŊåĶä―å·Ĩä―įã",
+ "extra": {
+ "title": "äļčĶčžäŋĄïžå
æéĐč",
+ "description": "äļčžäŋĄäŧŧä―äššïžå
æŽ xLogãåŧšč°æŠĒæĨäšĪææ·åēčååĄéåįīäŧĢįĒžïžéĐčäšåŊĶæŊåĶįŽĶå xLog æčēæã",
+ "button": "æé xLog įįĨį§éĒįī"
+ }
+ },
+ "Earn": {
+ "subtitle": "čģšåčæŽčēĻåđĢåæŽį",
+ "description": "ååĄéįšåŠį§įå
§åŪđåĩä―č
æäūéæå
ŽæĢįįåĩæĐæïžxLog DAO æīéēäļæĨäŋéēįĪūįūĪįžåąã"
+ }
+ },
+ "Home": "éĶé ",
+ "Activities": "æįŦ æĩ",
+ "Source Code": "åå§įĒž",
+ "Features": "įđčē",
+ "Showcase": "åąįĪšæŦ",
+ "Integration": "æīå",
+ "Visit": "čĻŠå",
+ "Easy to get started": "čŪåŊŦä―æīį°ĄåŪ",
+ "Easy to get started text": "xLog æŊæ
Web3 éĒå
å
éŧåéĩäŧķ éĢæĨïžčŪä― čžéŽåĩåŧšåŪåķåéĻč―æ žïžäšŦæčŠåŪįūĐååãčĻéąãįčĻãNFT éé ãRSS čĻéąå AI åĒåž·įåč―ãåŠé 5 åéåģåŊåŪæïžįĄéįģčŦæčēŧįĻã",
+ "Elegant experience": "åŠé
æ°ļäļéæ",
+ "Elegant experience text": "éæŽį·ĻčžŊåĻïž
åŊĶæé čĶ―ïžåĩä―įĄåĢåïžä―ŋįϿϿšį
Markdown čŠæģ ïžæŊæ
HTML ã
éģčĶé ŧ å
æļåļå
Žåžãé éĒčĻčĻåŠé
ïžįščŪč
æäūčéĐįéąčŪéŦéĐã",
+ "Fast": "äļåįēūå―Đįčåū",
+ "Fast text": "ååĄéä―æïžäļïžéŦæį·ĐåæĐåķååĪįĻŪåŠåæđæĄïžæŋįžååĄéį
æĨĩčīæ§č― ïžåææäū
PWA čŪæĻčžéŽæŽå°åŪčĢåä―ŋįĻïžå éåŊĶįūįŪæĻïž",
+ "Safe": "čģåŪïžææåĻčĄ",
+ "Safe text": "ææčģæïžå
æŽé
į―ŪãæįŦ ãčĻéąãįčĻįïžé―čĒŦåŪå
Ļå°ååēåĻ
ååĄé äļãåŠææĻææį
į§é° æč―ææ§åčĻŠåéäščģæïžįĒšäŋæĻįéąį§åūå°æåΧįĻåšĶįäŋč·ã",
+ "Customizable": "åĩæïžčŠįąæŪį",
+ "Customizable text": "æĻåŊäŧĨčŠįąå°ä―ŋįĻ
čŠå·ąįåå ïžåĻčŠå·ąåæįéĒĻæ žäļčŠåŪįūĐįķēįŦïžxLog æäūčąåŊįäļŧéĄåæäŧķįģŧįĩąïžčŪæĻ
éĻåŋææŽē å°æé åąŽæžčŠå·ąįåæ§åįķēįŦãåąįūæĻįįĻįđéĒĻéïž",
+ "Open": "éæūïžéįĻčŠåĶ",
+ "Open text": "xLog æäūå°å
Ĩåå°åšå·Ĩå
·ïžčąåŊį
API å
įŽŽäļæđ æīåïžčۿϿīå čŠįąå°įŪĄįåä―ŋįĻįķēįŦãææäŧĢįĒžé―åĻ GitHub äļ
éæš ïžææčģæé―åĻååĄéäļ
éæååē ïžæēæäŧŧä―éąįã",
+ "Creator Incentives": "åĩä―č
æŋåĩ",
+ "Creator Incentives text": "åĻæĐæéæŪĩïžxLog äŧĨ
MIRA æäū
æŋåĩãæåæĢįĐæĨĩæ§åŧšäŧĢåđĢįķæŋæĻĄåïžåæąčŪ xLog æįšįžåļéŦčģŠéå
§åŪđįæä―ģåđģå°ãæååļæįšæĻæäūæåĨ―įéŦéĐåæåΧįåđåžåå ąïžčŪæĻåĻåĩä―ãåäšŦåįĪūäšĪäļæå°æīå čŠįąååŋŦæĻã",
+ "DAO": "åŧäļåŋåčŠæēŧįĩįđ",
+ "DAO text": "æåå°åŧšįŦäļå xLog DAOïžčŪåĩä―č
åŊäŧĨä―ŋįĻ
čæŽčēĻåđĢ éēčĄæįĨĻãæåįļäŋĄïžéå°ä―ŋæåįįĪūåæīå éæūãå
Žåđģåæ°äļŧåïžčŪæŊåäššé―č―įžæŪčŠå·ąįå―ąéŋååčēĒįŧåđåžãæåæåū
čæĻæææé äļåæīå įđæĶŪåææīŧåįįĪūåïž",
+ "Discover these awesome teams and creators on xLog (sorted by update time)": "åĻ xLog äļįžįūéäščķ
æĢįåéååĩä―č
åïžææīæ°æéæåšïž",
+ "Follow All!": "čŋ―čđĪææäššïž",
+ "Already Followed All!": "å·ēčŋ―čđĪææäššïž",
+ "Show more": "éĄŊįĪšæīåĪ",
+ "Submit yours": "įįä― įð",
+ "xLog's open design allows it to integrate with many other open protocols and applications without friction.": "æåįļäŋĄïžåŠæåĻéæūãäščŊãå
ąäšŦįåšįĪäļæč―ææīåΧįåđåžååĩæ°ã",
+ "Dashboard": "äļŧæ§čš",
+ "Suggested creators for you": "æĻčĶįĩĶä― äļäšåŠčģŠåĩä―č
",
+ "Hot Topics": "æĩčĄčĐąéĄ"
+}
diff --git a/src/lib/i18n/locales/zh-TW/site.json b/src/lib/i18n/locales/zh-TW/site.json
new file mode 100644
index 00000000..c2710677
--- /dev/null
+++ b/src/lib/i18n/locales/zh-TW/site.json
@@ -0,0 +1,18 @@
+{
+ "Home": "éĶé ",
+ "Archives": "å°å",
+ "About": "éæž",
+ "Tags": "æĻįąĪ",
+ "load more": "éæ{{count}}įŊ{{name}}ïžéŧéļčžå
ĨæīåĪæįŦ ",
+ "signed and stored on the blockchain": "æĪ{{name}}æļææææŽįąååĄéå åŊæčĄåæšč―åįīäŋéå
æļåĩä―č
ææã",
+ "Write a comment on the blockchain": "åĻååĄéäļįčĻã",
+ "powered by": "
įą æäūæŊæī",
+ "This address is in local editing preview mode and cannot be viewed by the public.": "æĪå°åčæžæŽå°į·ĻčžŊé čĶ―æĻĄåžïžå°æŠå
Žéã",
+ "View on xChar": "åĻ xChar äļæŠĒčĶ",
+ "View on xFeed": "åĻ xFeed äļæŠĒčĶ",
+ "View on Hoot It": "åĻ Hoot It äļæŠĒčĶ",
+ "View on Crossbell Scan": "åĻ Crossbell Scan äļæŠĒčĶ",
+ "Subscribe to JSON Feed": "čĻéą JSON Feed",
+ "Subscribe to RSS": "čĻéą RSS",
+ "Search on this site": "åĻéåįķēįŦäļæå°"
+}
\ No newline at end of file
diff --git a/src/lib/i18n/locales/zh/common.json b/src/lib/i18n/locales/zh/common.json
new file mode 100644
index 00000000..2918aace
--- /dev/null
+++ b/src/lib/i18n/locales/zh/common.json
@@ -0,0 +1,87 @@
+{
+ "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 Scan2> äļæĨį",
+ "mint stored": "ä― å·ēå°æĪįŊæįŦ įčæNFT, åŊäŧĨåĻ <2>xChar2> æ <6>Crossbell Scan6> äļæĨį",
+ "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": "åŊéļé ",
+ "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"
+}
diff --git a/src/lib/i18n/locales/zh/dashboard.json b/src/lib/i18n/locales/zh/dashboard.json
new file mode 100644
index 00000000..be107594
--- /dev/null
+++ b/src/lib/i18n/locales/zh/dashboard.json
@@ -0,0 +1,204 @@
+{
+ "Dashboard": "äŧŠčĄĻį",
+ "Posts": "æįŦ ",
+ "Pages": "éĄĩéĒ",
+ "Notifications": "éįĨ",
+ "Unread notifications": "æŠčŊŧéįĨ",
+ "Settings": "čŪūį―Ū",
+ "Events": "æīŧåĻ",
+ "New Events": "æ°æīŧåĻ",
+ "Site Stats": "įŦįđįŧčŪĄ",
+ "Published posts": "ååļįæįŦ ",
+ "Received comments": "æķå°įčŊčŪš",
+ "Followers": "å
ģæģĻč
",
+ "Viewed": "æĩč§é",
+ "Received tips": "æķå°įæčĩ",
+ "Site Duration": "įŦįđčŋčĄæķéī",
+ "days": "åĪĐ",
+ "Deleted!": "å·ēå éĪïž",
+ "Fail to Deleted.": "å éĪåĪąčīĨ",
+ "Converted!": "å·ēč―ŽæĒïž",
+ "Failed to convert.": "č―ŽæĒåĪąčīĨã",
+ "All Posts": "æææįŦ ",
+ "All Pages": "ææéĄĩéĒ",
+ "Published": "å·ēååļ",
+ "published": "å·ēååļ",
+ "Draft": "čįĻŋ",
+ "draft": "čįĻŋ",
+ "Scheduled": "åŪæķååļ",
+ "scheduled": "åŪæķååļ",
+ "published and local modified": "å·ēååļåđķæŽå°äŋŪæđ",
+ "link post-vs-page": "https://wordpress.com/zh-cn/support/post-vs-page/",
+ "posts description": "æįŦ æŊææķéīååšåĻä― įį―įŦäļååšįæĄįŪãåŊäŧĨå°åŪäŧŽįä―æŊæīæ°ïžäŧĨæäūæ°å
åŪđįŧčŊŧč
ã<2>æįŦ äļéĄĩéĒ2>ã",
+ "pages description": "éĄĩéĒæŊéæįïžäļåæĨæå―ąåãåŪäŧŽæīåæŊä― į―įŦäļįæ°ļäđ
å
įī ïžæŊåĶâå
ģäšæäŧŽâãâčįģŧæäŧŽâįã<2>æįŦ äļéĄĩéĒ2>ã",
+ "pages add": "ååŧšéĄĩéĒåïžä― åŊäŧĨå°å
ķ<2>æ·ŧå å°ä― į―įŦįåŊžčŠčåäļ2>ïžäŧĨäūŋčŪŋéŪč
åŊäŧĨæūå°åŪã",
+ "New Post": "æ°åŧšæįŦ ",
+ "New Page": "æ°åŧšéĄĩéĒ",
+ "Import": "åŊžå
Ĩ",
+ "Import markdown file with front matter supported": "åŊžå
Ĩ Markdown æäŧķ(æŊæ Front Matter)",
+ "View Site": "æĨįįŦįđ",
+ "hello": {
+ "welcome": "
ð ä― åĨ―åïž
æŽĒčŋä―ŋįĻ xLogïž
äŧĨäļæŊäļäšæįĻįéūæĨïžåļŪåĐä― åžå§ä―ŋįĻ xLogïž
",
+ "hidden": "ð ä― åĨ―å",
+ "community": "
å å
ĨæäŧŽįįĪūåšïžčŪĪčŊæ°æåæå
ąååŧščŪū xLogïž
"
+ },
+ "Edit": "įžčū",
+ "Convert to Page": "č―ŽæĒäļšéĄĩéĒ",
+ "Convert to Post": "č―ŽæĒäļšæįŦ ",
+ "Select All": "å
ĻéĻéæĐ",
+ "Deselect All": "åæķéæĐ",
+ "Delete": "å éĪ",
+ "Heading": "æ éĒ",
+ "Bold": "įēä―",
+ "Italic": "æä―",
+ "Strikethrough": "å éĪįšŋ",
+ "Underline": "äļåįšŋ",
+ "Quote": "åžįĻ",
+ "Inline Code": "čĄå
äŧĢį ",
+ "Code Block": "äŧĢį å",
+ "Unordered List": "æ åšåčĄĻ",
+ "Ordered List": "æåšåčĄĻ",
+ "Link": "éūæĨ",
+ "Image": "åūį",
+ "Upload Image": "äļäž åūį",
+ "Mention": "æå",
+ "Tip: xLog Flavored Markdown": "åļŪåĐïžxLog éĢæ žį Markdown",
+ "Preview": "éĒč§",
+ "Publish": "ååļ",
+ "Update": "æīæ°",
+ "Discard Changes": "æūåžåæī",
+ "Publish at": "ååļæķéī",
+ "This post will be accessible from this time": "æĪæįŦ å°äŧæĪæķéīåžå§åŊčŪŋéŪ",
+ "This page will be accessible from this time": "æĪéĄĩéĒå°äŧæĪæķéīåžå§åŊčŪŋéŪ",
+ "Post slug": "æįŦ įéūæĨ",
+ "Page slug": "éĄĩéĒįéūæĨ",
+ "This post will be accessible at": "æĪæįŦ å°åŊéčŋäŧĨäļéūæĨčŪŋéŪ",
+ "This page will be accessible at": "æĪéĄĩéĒå°åŊéčŋäŧĨäļéūæĨčŪŋéŪ",
+ "Tags": "æ įū",
+ "Separate multiple tags with English commas": "åĪäļŠæ įūčŊ·įĻčąæéå·åé",
+ "Excerpt": "æčĶ",
+ "Leave it blank to use auto-generated excerpt": "įįĐšåä―ŋįĻčŠåĻįæįæčĶ",
+ "General": "åļļč§",
+ "Social Platforms": "įĪūäšĪåđģå°",
+ "Navigation": "åŊžčŠ",
+ "Domains": "åå",
+ "Custom CSS": "čŠåŪäđ CSS",
+ "Operators": "å―ąåææ",
+ "Export data": "åŊžåšæ°æŪ",
+ "Site Settings": "įŦįđčŪūį―Ū",
+ "Icon": "åūæ ",
+ "Banner": "æĻŠåđ
",
+ "Supports both pictures and videos.": "æŊæåūįåč§éĒã",
+ "Name": "åį§°",
+ "Description": "æčŋ°",
+ "Integrate Google Analytics": "å° Google Analytics éæå°ä― įįŦįđäļãä― åŊäŧĨæį
§<2>čŋé2>įčŊīææĨæūä― į Measurement IDã",
+ "Integrate Umami Cloud Analytics": "å° Umami Cloud Analytics éæå°ä― įįŦįđäļãä― åŊäŧĨæį
§<2>čŋé2>įčŊīææĨæūä― į Website IDã",
+ "Save": "äŋå",
+ "Tips": "æįĪš",
+ "social tips": {
+ "p1": "čŋäšįĪūäšĪåđģå°å°æūįĪšåĻä― į xLog įåģäļč§ã",
+ "p2": "æäŧŽæŊæ<2>čŋäšåđģå°2>ïžčŠåĻæūįĪšå
ķæ åŋåéūæĨãåŊđäšå
ķäŧåđģå°ïžå°æūįĪšéŧčŪĪæ åŋãåĶæä― æå
ķäŧéæąïžčŊ·éæķåæäŧŽæäšĪéŪéĒæPRïžäŧĨæŊææīåĪįåđģå°ã",
+ "p3": "ä― čŋåŊäŧĨåĻ <2>xSync2> äļčŋæĨå° TwitterãTelegram éĒéãMediumãSubstack įåđģå°ïžåđķčŠåĻåæĨå
åŪđãå―ä― åĻéĢéčŪūį―ŪåæĨåïžåŪäđäžæūįĪšåĻčŋéã"
+ },
+ "Platform": "åđģå°",
+ "Identity": "čšŦäŧ―",
+ "Remove": "į§ŧéĪ",
+ "New Item": "æ°åŧšéĄđįŪ",
+ "xLog provides some out-of-the-box built-in pages": "xLog æäūäšäļäšåžįŪąåģįĻįå
į―ŪéĄĩéĒ",
+ "Home page": "éĶéĄĩ",
+ "Archives page": "å―æĄĢéĄĩéĒ",
+ "Tag page": "æ įūéĄĩéĒ",
+ "NFT Showcase page": "NFT åąįĪšéĄĩéĒ",
+ "Label": "æ įū",
+ "URL": "éūæĨ",
+ "subdomain": "ååå",
+ "Custom Domain": "čŠåŪäđåå",
+ "Set the following record on your DNS provider to active your custom domain": "åĻä― į DNS æäūåäļčŪūį―ŪäŧĨäļčŪ°å―äŧĨæŋæīŧčŠåŪäđåå",
+ "Scope: These styles will be applied to your entire blog, including this dashboard.": "čåīïžčŋäšæ ·åžå°åšįĻäšä― įæīäļŠååŪĒïžå
æŽčŋäļŠäŧŠčĄĻįã",
+ "Support": "æŊæ",
+ "CSS variables: xLog provides some built-in CSS variables": "CSS åéïžxLog æäūäšäļäšå
į―Ūį CSS åé",
+ "Using a browser plugin that can modify page styles, such as Stylebot, can help with debugging.": "ä―ŋįĻåŊäŧĨäŋŪæđéĄĩéĒæ ·åžįæĩč§åĻæäŧķïžäūåĶ StylebotïžåŊäŧĨåļŪåĐč°čŊã",
+ "Address": "å°å",
+ "Character": "č§čē",
+ "New operator": "æ°åŧšææ",
+ "Operator Address": "ææå°å",
+ "Operator Character Check": "ææč§čēæĢæĨ",
+ "Warning": "čĶå",
+ "Operators have permissions to enter your dashboard, change your settings(excluding xLog subdomain) and post, modify, delete contents on your site.": "å―ąåææåŊäŧĨčŋå
Ĩä― įäŧŠčĄĻįïžæīæđä― įčŪūį―Ūïžäļå
æŽ xLog åååïžåđķåĻä― įįŦįđäļååļãäŋŪæđãå éĪå
åŪđã",
+ "Add": "æ·ŧå ",
+ "xLog News": "xLog æ°éēäš",
+ "More": "æīåĪ",
+ "Meet New Friends": "čŪĪčŊæ°æå",
+ "Need help?": "éčĶåļŪåĐïž",
+ "Your post has been securely stored on the blockchain. Now you may want to": "ä― įæįŦ å·ēįŧåŪå
Ļå°ååĻåĻåšåéūäļãį°åĻä― åŊč―æģčĶ",
+ "View the post": "æĨįæįŦ ",
+ "View the transaction": "æĨįäšĪæ",
+ "Share to Twitter": "åäšŦå° Twitter",
+ "Got it, thanks!": "åĨ―įïžč°Ēč°Ēïž",
+ "Published!": "å·ēååļïž",
+ "Title goes here...": "æ éĒåĻčŋé...",
+ "Start writing...": "åžå§åä―...",
+ "Slug can only contain letters, numbers, hyphens, and underscores.": "įéūæĨåŠč―å
åŦåæŊãæ°åãčŋåįŽĶåäļåįšŋã",
+ "Date": "æĨæ",
+ "Prize": "åĨé",
+ "Winners": "č·åĨč
",
+ "Learn more": "äšč§ĢæīåĪ",
+ "Ended": "å·ēįŧæ",
+ "Upcoming": "åģå°åžå§",
+ "Ongoing": "čŋčĄäļ",
+ "Import from Markdown files": "äŧ Markdown æäŧķåŊžå
Ĩ",
+ "Import from Mirror.xyz": "äŧ Mirror.xyz åŊžå
Ĩ",
+ "Select Markdown Files": "éæĐ Markdown æäŧķ",
+ "No files chosen": "æŠéæĐæäŧķ",
+ "No entries": "æ æĄįŪ",
+ "Preview your Mirror.xyz entries": "éĒč§ä― į Mirror.xyz æĄįŪ",
+ "Please select .md files, multiple files are supported, and front matter is supported.": "čŊ·éæĐ .md æäŧķïžæŊæåĪäļŠæäŧķïžæŊæ front matterã",
+ "Achievements": "æå°ą",
+ "Tokens": "äŧĢåļ",
+ "$MIRA is a valuable token in the Crossbell world, and can be easily exchanged on the Crosschain Bridge and Uniswap.": "$MIRA æŊ Crossbell äļįäļįäļį§įčīĩįäŧĢåļïžåŊäŧĨč―ŧæūå°åĻ Crosschain Bridge å Uniswap äļå
æĒã",
+ "In the early stage, xLog will use $MIRA to motivate creators.": "åĻæĐæéķæŪĩïžxLog å°ä―ŋįĻ $MIRA æŋåąåä―č
ã",
+ "You can obtain $MIRA through the following ways:": "ä― åŊäŧĨéčŋäŧĨäļæđåžč·åū $MIRAïž",
+ "Creator incentive program.": "åä―č
æŋåąčŪĄåã",
+ "Participate in events.": "åå æīŧåĻã",
+ "Swap from USDC.": "äŧ USDC å
æĒã",
+ "Received tips and sponsorships from readers.": "äŧčŊŧč
æķå°æčĩåčĩåĐã",
+ "Swap to USDC": "å
æĒå° USDC",
+ "Free Claim": "å
čīđéĒå",
+ "$XLOG is related to xLog DAO, but it's too early now, please stay tuned.": "$XLOG äļ xLog DAO įļå
ģïžä―į°åĻčŋäļšæķå°æĐïžæŽčŊ·æåū
ã",
+ "Stay tuned": "æŽčŊ·æåū
",
+ "This is a token used for interaction on the Crossbell blockchain, which can be claimed for free from the faucet, so there's no need to worry about its balance.": "čŋæŊäļäļŠįĻäš Crossbell åšåéūäļäšĪäšįäŧĢåļïžåŊäŧĨäŧæ°īéūåĪīå
čīđéĒåïžæäŧĨäļįĻæ
åŋåŪįä―éĒã",
+ "Balance": "ä―éĒ",
+ "Swap Tutorial": "å
æĒæįĻ",
+ "DNS Checking": "DNS æĢæĨäļ",
+ "DNS check passed.": "DNS æĢæĨéčŋã",
+ "DNS check failed.": "DNS æĢæĨåĪąčīĨã",
+ "Subdomain Checking": "åååæĢæĨäļ",
+ "Subdomain Available.": "ååååŊįĻã",
+ "Subdomain Unavailable.": "åååäļåŊįĻã",
+ "Recheck": "éæ°æĢæĨ",
+ "Hottest": "æį",
+ "Latest": "ææ°",
+ "Following": "å
ģæģĻ",
+ "Create a Post": "ååŧšæįŦ ",
+ "Change Site Icon or domain": "æīæđįŦįđåūæ æåå",
+ "Join xLog's Discord channel": "å å
Ĩ xLog į Discord éĒé",
+ "Participate in the development of xLog": "åäļ xLog įåžå",
+ "Follow xLog's Twitter": "å
ģæģĻ xLog į Twitter",
+ "Check out the updates of other bloggers": "æĨįå
ķäŧåäļŧįæīæ°",
+ "You have already imported them, please enter the post page to create a new post!": "æĻå·ēįŧåŊžå
ĨäšåŪäŧŽïžčŊ·čŋå
ĨæįŦ éĄĩéĒååŧšæ°įæįŦ å§ã",
+ "Today": "äŧåĪĐ",
+ "This month": "æŽæ",
+ "This week": "æŽåĻ",
+ "All time": "å
ĻéĻ",
+ "Confirm Deletion?": "įĄŪčŪĪå éĪïž",
+ "delete_confirmation_post": "ä― įĄŪåŪčĶå éĪčŋįŊæįŦ åïž",
+ "delete_confirmation_page": "ä― įĄŪåŪčĶå éĪčŋäļŠéĄĩéĒåïž",
+ "Confirm": "įĄŪčŪĪ",
+ "Cancel": "åæķ",
+ "Published a new post on my blockchain blog: {{title}}. Check it out now!": "åĻæįåšåéūååŪĒååļäšäļįŊæ°æįŦ ã{{title}}ãïžåŋŦæĨįįå§ïž",
+ "Comments": "čŊčŪš",
+ "You can subscribe to comments through an RSS reader to receive timely reminders.": "ä― åŊäŧĨéčŋ RSS é
čŊŧåĻčŪĒé
čŊčŪšïžäŧĨäūŋåæķæķå°æéã",
+ "Subscription address:": "čŪĒé
å°åïž",
+ "comment on your" : "čŊčŪšäšä― į{{type}}ã
{{toTitle}}ã",
+ "{{newCount}} new comments" : "{{newCount}} æĄæ°čŊčŪš"
+}
\ No newline at end of file
diff --git a/src/lib/i18n/locales/zh/index.json b/src/lib/i18n/locales/zh/index.json
new file mode 100644
index 00000000..a7048a85
--- /dev/null
+++ b/src/lib/i18n/locales/zh/index.json
@@ -0,0 +1,61 @@
+{
+ "Write": "åä―",
+ "Own": "æĨæ",
+ "Earn": "čĩå",
+ ".": "ã",
+ "Look at others'": "įįå
ķäŧäššį",
+ "description": "<0>xLog0> æŊéĒåææäššįææĢ <3>éūäļ3> <7>åžæš7> ååŪĒįĪūåšã",
+ "Get my xLog in 5 minutes": "5 åéæĨæčŠå·ąį xLog",
+ "Explore the xLog way": "æĒįīĒ xLog äđč·Ŋ",
+ "features": {
+ "Write": {
+ "subtitle": "å―įĩææĨäļīæķåä―",
+ "description": "čŪĐåä―č
äŧŽæčąčæķãäļåŋ
čĶįæĩįĻïžåå°åä―čŋįĻäļįéŧįĒïžčŪĐä― ååĒéč―åĪäļæģĻäšåé ã"
+ },
+ "Own": {
+ "subtitle": "åĻåšåéūäļæĨæčŠå·ąįå
åŪđ",
+ "description": "éčŋåĻ Crossbell åšåéūäļååļæĨæĨæčŠå·ąįå
åŪđã xLog äļäžååĻäŧŧä―æ°æŪïžäđæ æģåĨåĪšæäŋŪæđä― įæåĐåå
åŪđïžåģä―ŋ xLog æģčŋæ ·åã
äšč§Ģ Crossbell æŊåĶä―å·Ĩä―įã",
+ "extra": {
+ "title": "äļčĶįļäŋĄïžåŧéŠčŊ",
+ "description": "åĻäščį―äļäļč―ŧäŋĄäŧŧä―äššæŊäļäļŠåĨ―äđ æŊïžå
æŽ xLogãå æĪïžæäŧŽåžšįåŧščŪŪä― æĩč§ xLog äļįäŧŧä―įŦįđïžæĢæĨéĄĩéĒåšéĻįäšĪæååēïžé
čŊŧåšåéūåįšĶäŧĢį ïžįįåŪæ
æŊåĶäļ xLog æå̰᧰įäļčīã",
+ "button": "åŧéŠčŊ"
+ }
+ },
+ "Earn": {
+ "subtitle": "čĩåäŧĢåļåæåĐ",
+ "description": "äžį§įå
åŪđåščŊĨåūå°åĨåąïžčåšåéūäļšåä―č
æäūäšéæå
ŽæĢįåĨåąæšäžãxLog DAO å°čŋäļæĨäŋčŋæäŧŽįĪūåšįååąã"
+ }
+ },
+ "Features": "įđčē",
+ "Showcase": "åąįĪšæ",
+ "Integration": "éæ",
+ "Home": "éĶéĄĩ",
+ "Activities": "åĻæ",
+ "Source Code": "æšį ",
+ "Visit": "čŪŋéŪ",
+ "Easy to get started": "įŪåäļæ",
+ "Easy to get started text": "xLog æŊæ
web3 éąå
å
įĩåéŪäŧķčŋæĨïžä―ŋä― č―åĪåŋŦéååŧšäļäļŠåŪåķįååŪĒïžå
·æčŠåŪäđååãčŪĒé
ãčŊčŪšãNFT éļé ãRSS čŪĒé
å AI åĒåžšįåč―ïžäŧ
é 5 åéåģåŊåŪæïžæ éįģčŊ·æčīđįĻã",
+ "Safe": "åŪå
Ļ",
+ "Safe text": "ææååŪĒæ°æŪïžå
æŽé
į―ŪãæįŦ ãčŪĒé
ãčŊčŪšįïžé―įąä― čŠå·ąįūååđķåŪå
Ļå°ååĻåĻ
åšåéūäļïžåŠæä― ææį
į§éĨæč―čŋčĄæ§åķåčŪŋéŪãå
æŽ xLog åĻå
įäŧŧä―äššé―æ æģčŋčĄæīæđã",
+ "Fast": "åŋŦé",
+ "Fast text": "åšåéūåđķäļæŧæåģįä―æįãxLogéčŋå
ķéŦæįįžåæšåķåäžåĪäžåïžåŊäŧĨčūūå°æä―ģæ§č―ãįŦįđčŋæŊæ
PWAįĻäšæŽå°åŪčĢ
åä―ŋįĻã",
+ "Customizable": "åŊåŪåķ",
+ "Customizable text": "ä― åŊäŧĨčŠįąå°ä―ŋįĻ
čŠå·ąįååãčŠåŪäđį―įŦåđķæčŠå·ąįååĨ―čŋčĄ
čŪūčŪĄãčŋæŊä― įį―įŦïžæēĄæäŧŧä―éåķãxLog éžåąåđķå°æäūäļ°åŊį
äļŧéĒåæäŧķįģŧįŧïžäŧĨåļŪåĐä― čŋčĄčŠåŪäđã",
+ "Open": "åžæū",
+ "Open text": "xLogæäūåŊžå
ĨååŊžåšå·Ĩå
·ïžäŧĨåäļ°åŊį
API åįŽŽäļæđ
éæãææäŧĢį é―åĻ GitHub äļ
åžæšïžæææ°æŪé―åĻåšåéūäļ
éæãxLog æēĄæäŧŧä―éįã",
+ "Elegant experience": "äžé
įä―éŠ",
+ "Elegant experience text": "xLog æäūåæ įžčūåĻïžå
·æ
åŪæķéĒč§åč―ïžæäūäžį§įåä―ä―éŠïžä―ŋįĻæ åį
Markdown čŊæģïžæŊæ
HTMLã
éģč§éĒåæ°åĶčĄĻčūūåžãéĄĩéĒčŪūčŪĄäžé
ïžäļščŊŧč
æäūčéįé
čŊŧä―éŠã",
+ "Creator Incentives": "åä―č
æŋåą",
+ "Creator Incentives text": "åĻæĐæéķæŪĩïžxLogäŧĨ
MIRAæäū
æŋåąãæäŧŽæĢåĻį§ŊææåŧšäŧĢåļįŧæĩæĻĄåïžäŧĨįĄŪäŋxLogæäļšååļéŦčīĻéå
åŪđįæä―ģåđģå°ã",
+ "DAO": "åŧäļåŋåčŠæēŧįŧįŧ",
+ "DAO text": "å°åŧšįŦäļäļŠxLog DAOïžåĻå
ķäļåä―č
ä―ŋįĻ
äŧĢåļčŋčĄæįĨĻã",
+ "Discover these awesome teams and creators on xLog (sorted by update time)": "åĻ xLog äļåį°čŋäščķ
æĢįåĒéååä―č
äŧŽïžææīæ°æķéīæåšïž",
+ "Follow All!": "å
ģæģĻææïž",
+ "Already Followed All!" : "å·ēįŧå
ģæģĻææïž",
+ "Show more": "æūįĪšæīåĪ",
+ "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": "äļšä― æĻčįåä―č
",
+ "Hot Topics": "įéĻčŊéĒ"
+}
\ No newline at end of file
diff --git a/src/lib/i18n/locales/zh/site.json b/src/lib/i18n/locales/zh/site.json
new file mode 100644
index 00000000..1926e4a1
--- /dev/null
+++ b/src/lib/i18n/locales/zh/site.json
@@ -0,0 +1,19 @@
+{
+ "Home": "äļŧéĄĩ",
+ "Archives": "å―æĄĢ",
+ "About": "å
ģäš",
+ "Tags": "æ įū",
+ "load more": "čŋæ {{count}} įŊ{{name}}ïžįđåŧå č――æīåĪ",
+ "signed and stored on the blockchain": "æĪ{{name}}æ°æŪæææįąåšåéūå åŊææŊåæšč―åįšĶäŋéäŧ
å―åä―č
ææã",
+ "Write a comment on the blockchain": "åĻåšåéūäļåäļä― įčŊčŪš",
+ "powered by": "
įą æäūæŊæ",
+ "This address is in local editing preview mode and cannot be viewed by the public.":
+ "æĪå°ååĪäšæŽå°įžčūéĒč§æĻĄåžïžæ æģčĒŦå
ŽäžæĨįã",
+ "View on xChar": "åĻ xChar äļæĨį",
+ "View on xFeed": "åĻ xFeed äļæĨį",
+ "View on Hoot It": "åĻ Hoot It äļæĨį",
+ "View on Crossbell Scan": "åĻ Crossbell Scan äļæĨį",
+ "Subscribe to JSON Feed": "čŪĒé
JSON Feed",
+ "Subscribe to RSS": "čŪĒé
RSS",
+ "Search on this site": "åĻæŽįŦæįīĒ"
+}
\ No newline at end of file
diff --git a/src/lib/i18n/settings.ts b/src/lib/i18n/settings.ts
new file mode 100644
index 00000000..4da7f705
--- /dev/null
+++ b/src/lib/i18n/settings.ts
@@ -0,0 +1,15 @@
+export const fallbackLng = "en"
+export const languages = ["en", "zh", "zh-TW", "ja"]
+export const defaultNS = "common"
+
+export function getOptions(lng = fallbackLng, ns = defaultNS) {
+ return {
+ // debug: true,
+ supportedLngs: languages,
+ fallbackLng,
+ lng,
+ fallbackNS: defaultNS,
+ defaultNS,
+ ns,
+ }
+}
diff --git a/src/lib/ipfs-parser.ts b/src/lib/ipfs-parser.ts
index cbcc389e..97504f2b 100644
--- a/src/lib/ipfs-parser.ts
+++ b/src/lib/ipfs-parser.ts
@@ -7,8 +7,13 @@ export type ToGatewayConfig = {
forceFallback?: boolean
}
-export const toGateway = (url: string) => {
- const ipfsUrl = toIPFS(url)
+export const toGateway = (url: string | URL) => {
+ let ipfsUrl
+ if (typeof url === "string") {
+ ipfsUrl = toIPFS(url)
+ } else {
+ ipfsUrl = toIPFS(url.toString())
+ }
return ipfsUrl?.replaceAll(IPFS_PREFIX, IPFS_GATEWAY)
}
diff --git a/src/lib/json-feed.ts b/src/lib/json-feed.ts
index c10a1b69..b9a7ddcf 100644
--- a/src/lib/json-feed.ts
+++ b/src/lib/json-feed.ts
@@ -1,77 +1,74 @@
-import type { GetServerSidePropsContext } from "next"
-
-import { QueryClient } from "@tanstack/react-query"
-
-import { getSiteLink } from "~/lib/helpers"
-import { ExpandedNote, PageVisibilityEnum } from "~/lib/types"
-import { renderPageContent } from "~/markdown"
-import { fetchGetPagesBySite } from "~/queries/page.server"
-import { fetchGetSite } from "~/queries/site.server"
+// import { QueryClient } from "@tanstack/react-query"
+// import { getSiteLink } from "~/lib/helpers"
+import { ExpandedNote } from "~/lib/types"
+// import { renderPageContent } from "~/markdown"
+// import { fetchGetPagesBySite } from "~/queries/page.server"
+// import { fetchGetSite } from "~/queries/site.server"
import { SITE_URL } from "./env"
-export const getJsonFeed = async (domainOrSubdomain: string, path: string) => {
- const queryClient = new QueryClient()
+// export const getJsonFeed = async (domainOrSubdomain: string, path: string) => {
+// const queryClient = new QueryClient()
- const site = await fetchGetSite(domainOrSubdomain, queryClient)
- const pages = await fetchGetPagesBySite(
- {
- characterId: site?.characterId,
- type: "post",
- visibility: PageVisibilityEnum.Published,
- keepBody: true,
- },
- queryClient,
- )
+// const site = await fetchGetSite(domainOrSubdomain, queryClient)
+// const pages = await fetchGetPagesBySite(
+// {
+// characterId: site?.characterId,
+// type: "post",
+// visibility: PageVisibilityEnum.Published,
+// keepBody: true,
+// },
+// queryClient,
+// )
- const hasAudio = pages.list?.find((page) => page.metadata?.content?.audio)
+// const hasAudio = pages.list?.find((page) => page.metadata?.content?.audio)
- const link = getSiteLink({
- subdomain: site?.handle || "",
- })
- return {
- version: "https://jsonfeed.org/version/1",
- title: site?.metadata?.content?.name,
- description: site?.metadata?.content?.bio,
- icon: site?.metadata?.content?.avatars?.[0],
- home_page_url: link,
- feed_url: `${link}${path}`,
- ...(hasAudio && {
- _itunes: {
- image: site?.metadata?.content?.avatars?.[0],
- author: site?.metadata?.content?.name,
- summary: site?.metadata?.content?.bio,
- },
- }),
- items: pages.list?.map((page) => ({
- id: page.characterId + "-" + page.noteId,
- title: page.metadata?.content?.title,
- content_html:
- page.metadata?.content?.content &&
- renderPageContent(page.metadata?.content?.content, true).contentHTML,
- summary: page.metadata?.content?.summary,
- url: `/api/redirection?characterId=${page.characterId}¬eId=${page.noteId}`,
- image: page.metadata?.content?.cover,
- date_published: page.metadata?.content?.date_published,
- date_modified: page.updatedAt,
- tags: page.metadata?.content?.tags,
- author: site?.metadata?.content?.name,
- ...(page.metadata?.content?.audio && {
- _itunes: {
- image: page.metadata?.content?.cover,
- summary: page.metadata?.content?.summary,
- },
- attachments: [
- {
- url: page.metadata?.content?.audio,
- mime_type: "audio/mpeg",
- title: page.metadata?.content?.title,
- },
- ],
- }),
- })),
- }
-}
+// const link = getSiteLink({
+// subdomain: site?.handle || "",
+// })
+// return {
+// version: "https://jsonfeed.org/version/1",
+// title: site?.metadata?.content?.name,
+// description: site?.metadata?.content?.bio,
+// icon: site?.metadata?.content?.avatars?.[0],
+// home_page_url: link,
+// feed_url: `${link}${path}`,
+// ...(hasAudio && {
+// _itunes: {
+// image: site?.metadata?.content?.avatars?.[0],
+// author: site?.metadata?.content?.name,
+// summary: site?.metadata?.content?.bio,
+// },
+// }),
+// items: pages.list?.map((page) => ({
+// id: page.characterId + "-" + page.noteId,
+// title: page.metadata?.content?.title,
+// content_html:
+// page.metadata?.content?.content &&
+// renderPageContent(page.metadata?.content?.content, true).contentHTML,
+// summary: page.metadata?.content?.summary,
+// url: `/api/redirection?characterId=${page.characterId}¬eId=${page.noteId}`,
+// image: page.metadata?.content?.cover,
+// date_published: page.metadata?.content?.date_published,
+// date_modified: page.updatedAt,
+// tags: page.metadata?.content?.tags,
+// author: site?.metadata?.content?.name,
+// ...(page.metadata?.content?.audio && {
+// _itunes: {
+// image: page.metadata?.content?.cover,
+// summary: page.metadata?.content?.summary,
+// },
+// attachments: [
+// {
+// url: page.metadata?.content?.audio,
+// mime_type: "audio/mpeg",
+// title: page.metadata?.content?.title,
+// },
+// ],
+// }),
+// })),
+// }
+// }
export const parsePost = (post: ExpandedNote, withTwitter?: boolean) => {
let twitter
@@ -98,15 +95,3 @@ export const parsePost = (post: ExpandedNote, withTwitter?: boolean) => {
},
}
}
-
-export const setHeader = (ctx: GetServerSidePropsContext, isXml?: boolean) => {
- ctx.res.setHeader(
- "Content-Type",
- ctx.query.format === "xml" || isXml
- ? "application/xml; charset=utf-8"
- : "application/feed+json; charset=utf-8",
- )
- ctx.res.setHeader("Access-Control-Allow-Methods", "GET")
- ctx.res.setHeader("Access-Control-Allow-Origin", "*")
- ctx.res.setHeader("Cache-Control", "public, max-age=1800")
-}
diff --git a/src/lib/query-client.ts b/src/lib/query-client.ts
new file mode 100644
index 00000000..a4a512ac
--- /dev/null
+++ b/src/lib/query-client.ts
@@ -0,0 +1,6 @@
+import { cache } from "react"
+
+import { QueryClient } from "@tanstack/react-query"
+
+const getQueryClient = cache(() => new QueryClient())
+export default getQueryClient
diff --git a/src/lib/server-helper.ts b/src/lib/server-helper.ts
new file mode 100644
index 00000000..8dc094bf
--- /dev/null
+++ b/src/lib/server-helper.ts
@@ -0,0 +1,75 @@
+// @ts-ignore
+import jsonfeedToRSS from "jsonfeed-to-rss"
+
+export const getQuery = (req: Request) => {
+ const url = new URL(req.url)
+ const searchParams = url.searchParams
+ const obj = {} as Record
+
+ for (const [key, value] of searchParams.entries()) {
+ obj[key] = value
+ }
+
+ return obj
+}
+
+export class NextServerResponse {
+ #status: number = 200
+ constructor() {}
+
+ status(status: number) {
+ this.#status = status
+ return this
+ }
+
+ json(data: any) {
+ const nextData = JSON.stringify(data)
+
+ return new Response(nextData, {
+ status: this.#status,
+ headers: {
+ "Content-Type": "application/json",
+ },
+ })
+ }
+
+ send(data: any) {
+ // if (data instanceof Stream) {
+ // return
+ // }
+
+ if (typeof data === "object" || typeof data === "undefined") {
+ return this.json(data)
+ }
+
+ return new Response(data, { status: this.#status })
+ }
+
+ end() {
+ return new Response("", { status: this.#status })
+ }
+
+ rss(data: any, format = "json") {
+ if (format === "xml") {
+ return new Response(jsonfeedToRSS(data), {
+ status: this.#status,
+ headers: {
+ "Content-Type": "application/xml; charset=utf-8",
+ "Access-Control-Allow-Methods": "GET",
+ "Access-Control-Allow-Origin": "*",
+ "Cache-Control": "public, max-age=1800",
+ },
+ })
+ } else {
+ return new Response(JSON.stringify(data), {
+ status: this.#status,
+ headers: {
+ "Content-Type": "application/feed+json; charset=utf-8",
+ "Access-Control-Allow-Methods": "GET",
+ "Access-Control-Allow-Origin": "*",
+ "Cache-Control": "public, max-age=1800",
+ },
+ })
+ }
+ }
+}
diff --git a/src/lib/types.ts b/src/lib/types.ts
index 0b9b6e7f..6a6104f8 100644
--- a/src/lib/types.ts
+++ b/src/lib/types.ts
@@ -1,4 +1,4 @@
-import { CharacterEntity, NoteEntity } from "crossbell.js"
+import type { CharacterEntity, NoteEntity } from "crossbell.js"
export type Site = {
id: string
diff --git a/src/lib/user-contents.ts b/src/lib/user-contents.ts
deleted file mode 100644
index 1750576b..00000000
--- a/src/lib/user-contents.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-export function getUserContentsUrl(filename: string): string
-export function getUserContentsUrl(filename: undefined | null): undefined
-export function getUserContentsUrl(
- filename: T,
-): T
-export function getUserContentsUrl(filename: string | undefined | null) {
- if (!filename) return undefined
- return filename
-}
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 3a4eac70..e100112a 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -39,3 +39,12 @@ export const throttle = (func: Function, limit: number) => {
}
}
}
+
+export const calculateElementTop = (el: HTMLElement) => {
+ let top = 0
+ while (el) {
+ top += el.offsetTop
+ el = el.offsetParent as HTMLElement
+ }
+ return top
+}
diff --git a/src/markdown/index.ts b/src/markdown/index.ts
index 6239e634..1b898923 100644
--- a/src/markdown/index.ts
+++ b/src/markdown/index.ts
@@ -3,7 +3,6 @@ import type { Root } from "mdast"
import { Result as TocResult, toc } from "mdast-util-toc"
import { ReactElement, createElement } from "react"
import { toast } from "react-hot-toast"
-import { Element } from "react-scroll"
import { refractor } from "refractor"
import jsx from "refractor/lang/jsx"
import solidity from "refractor/lang/solidity"
@@ -158,14 +157,6 @@ export const renderPageContent = (
},
children: [],
},
- {
- type: "element",
- tagName: "anchor",
- properties: {
- name: node.properties?.id,
- },
- children: [],
- },
]
},
})
@@ -219,7 +210,6 @@ export const renderPageContent = (
createElement: createElement,
components: {
img: ZoomedImage,
- anchor: Element,
mention: Mention,
mermaid: Mermaid,
audio: APlayer,
diff --git a/src/markdown/rehype-image.ts b/src/markdown/rehype-image.ts
index 1b336097..4c8a7f71 100644
--- a/src/markdown/rehype-image.ts
+++ b/src/markdown/rehype-image.ts
@@ -4,7 +4,6 @@ import { visit } from "unist-util-visit"
import { IS_PROD } from "~/lib/constants"
import { toGateway } from "~/lib/ipfs-parser"
-import { getUserContentsUrl } from "~/lib/user-contents"
import { MarkdownEnv } from "."
@@ -41,7 +40,7 @@ export const rehypeImage: Plugin, Root> = ({
return
}
- node.properties.src = getUserContentsUrl(url)
+ node.properties.src = url
})
}
}
diff --git a/src/middleware.ts b/src/middleware.ts
index 58263ca3..efc06eec 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -53,7 +53,9 @@ export default async function middleware(req: NextRequest) {
pathname.startsWith("/locales/") ||
pathname.match(/^\/(workbox|worker|fallback)-\w+\.js(\.map)?$/) ||
pathname === "/sw.js" ||
- pathname === "/sw.js.map"
+ pathname === "/sw.js.map" ||
+ pathname === "/robots.txt" ||
+ pathname === "/sitemap.xml"
) {
return NextResponse.next()
}
@@ -78,7 +80,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)
}
diff --git a/src/models/home.model.ts b/src/models/home.model.ts
index ffc9c9bd..5437958f 100644
--- a/src/models/home.model.ts
+++ b/src/models/home.model.ts
@@ -330,7 +330,7 @@ export const getShowcase = async () => {
.toPromise()
result.data?.characters?.forEach((site: any) => {
- if (site.metadata.content) {
+ if (site.metadata?.content) {
site.metadata.content.name = site.metadata?.content?.name || site.handle
} else {
site.metadata.content = {
diff --git a/src/pages/404.tsx b/src/pages/404.tsx
deleted file mode 100644
index b1766644..00000000
--- a/src/pages/404.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import { useState } from "react"
-
-import { SiteLayout } from "~/components/site/SiteLayout"
-import { SitePage } from "~/components/site/SitePage"
-import { SITE_URL } from "~/lib/env"
-
-export default function Custom404() {
- const [siteId, setSiteId] = useState("")
-
- try {
- fetch(`/api/host2handle?host=${window.location.host}`)
- .then((res) => res.json())
- .then((tenant) => {
- if (tenant.subdomain) {
- setSiteId(tenant.subdomain)
- }
- })
- } catch (error) {}
-
- return (
-
-
-
- )
-}
diff --git a/src/pages/_site/[site]/[page].tsx b/src/pages/_site/[site]/[page].tsx
deleted file mode 100644
index bc5ec2cf..00000000
--- a/src/pages/_site/[site]/[page].tsx
+++ /dev/null
@@ -1,64 +0,0 @@
-import { GetServerSideProps } from "next"
-import { ReactElement } from "react"
-
-import { QueryClient } from "@tanstack/react-query"
-
-import { SiteLayout } from "~/components/site/SiteLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/site/SiteLayout.server"
-import { SitePage } from "~/components/site/SitePage"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
-import { useGetPage } from "~/queries/page"
-import { useGetSite } from "~/queries/site"
-
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const queryClient = new QueryClient()
- const domainOrSubdomain = ctx.params!.site as string
- const pageSlug = ctx.params!.page as string
-
- const { props: layoutProps } = await getLayoutServerSideProps(
- ctx,
- queryClient,
- {
- useStat: true,
- },
- )
-
- return {
- props: {
- ...layoutProps,
- domainOrSubdomain,
- pageSlug,
- },
- }
- },
-)
-
-function SitePagePage({
- domainOrSubdomain,
- pageSlug,
-}: {
- domainOrSubdomain: string
- pageSlug: string
-}) {
- const site = useGetSite(domainOrSubdomain)
- const page = useGetPage({
- characterId: site.data?.characterId,
- slug: pageSlug,
- useStat: true,
- })
-
- return (
-
- )
-}
-
-SitePagePage.getLayout = (page: ReactElement) => {
- return (
-
- {page}
-
- )
-}
-
-export default SitePagePage
diff --git a/src/pages/_site/[site]/archives.tsx b/src/pages/_site/[site]/archives.tsx
deleted file mode 100644
index 087e8258..00000000
--- a/src/pages/_site/[site]/archives.tsx
+++ /dev/null
@@ -1,67 +0,0 @@
-import { GetServerSideProps } from "next"
-import type { ReactElement } from "react"
-
-import { QueryClient } from "@tanstack/react-query"
-
-import { SiteArchives } from "~/components/site/SiteArchives"
-import { SiteLayout } from "~/components/site/SiteLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/site/SiteLayout.server"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
-import { PageVisibilityEnum } from "~/lib/types"
-import { useGetPagesBySiteLite } from "~/queries/page"
-import { useGetSite } from "~/queries/site"
-
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const queryClient = new QueryClient()
- const domainOrSubdomain = ctx.params!.site as string
- const { props: layoutProps } = await getLayoutServerSideProps(
- ctx,
- queryClient,
- {
- limit: 100,
- },
- )
-
- return {
- props: {
- ...layoutProps,
- domainOrSubdomain,
- },
- }
- },
-)
-
-function SiteArchivesPage({
- domainOrSubdomain,
-}: {
- domainOrSubdomain: string
-}) {
- const site = useGetSite(domainOrSubdomain)
- const posts = useGetPagesBySiteLite({
- characterId: site.data?.characterId,
- limit: 100,
- type: "post",
- visibility: PageVisibilityEnum.Published,
- })
-
- return (
-
- )
-}
-
-SiteArchivesPage.getLayout = (page: ReactElement) => {
- return (
-
- {page}
-
- )
-}
-
-export default SiteArchivesPage
diff --git a/src/pages/_site/[site]/feed/comments.tsx b/src/pages/_site/[site]/feed/comments.tsx
deleted file mode 100644
index e8371c66..00000000
--- a/src/pages/_site/[site]/feed/comments.tsx
+++ /dev/null
@@ -1,83 +0,0 @@
-// @ts-ignore
-import jsonfeedToRSS from "jsonfeed-to-rss"
-import { GetServerSideProps } from "next"
-
-import { QueryClient } from "@tanstack/react-query"
-
-import { getSiteLink } from "~/lib/helpers"
-import { setHeader } from "~/lib/json-feed"
-import { renderPageContent } from "~/markdown"
-import { fetchGetComments, fetchGetSite } from "~/queries/site.server"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- const queryClient = new QueryClient()
- setHeader(ctx)
- const domainOrSubdomain = ctx.params!.site as string
-
- const site = await fetchGetSite(domainOrSubdomain, queryClient)
- const comments = await fetchGetComments(
- {
- characterId: site?.characterId,
- },
- queryClient,
- )
-
- const link = getSiteLink({
- subdomain: site?.handle || "",
- })
-
- const data = {
- version: "https://jsonfeed.org/version/1",
- title: "Comments on " + site?.metadata?.content?.name,
- description: site?.metadata?.content?.bio,
- icon: site?.metadata?.content?.avatars?.[0],
- home_page_url: link,
- feed_url: `${link}/feed/notifications`,
- items: comments?.list?.map((comment) => {
- const type = comment.toNote?.metadata?.content?.tags?.[0]
- let toTitle
- if (type === "post" || type === "page") {
- toTitle = comment.toNote?.metadata?.content?.title
- } else {
- if ((comment.toNote?.metadata?.content?.content?.length || 0) > 30) {
- toTitle =
- comment.toNote?.metadata?.content?.content?.slice(0, 30) + "..."
- } else {
- toTitle = comment.toNote?.metadata?.content?.content
- }
- }
- const name =
- comment?.character?.metadata?.content?.name ||
- `@${comment?.character?.handle}`
-
- return {
- id: comment.characterId + "-" + comment.noteId,
- title: `${name}: ${comment.metadata?.content?.content}`,
- content_html: `${name} commented on ${type} ${toTitle}: ${
- renderPageContent(comment.metadata?.content?.content || "")
- .contentHTML
- }`,
- url: `${link}/${
- comment.toNote?.metadata?.content?.attributes?.find(
- (attribute: any) => attribute.trait_type === "xlog_slug",
- )?.value || comment.toNote?.characterId + "-" + comment.toNote?.noteId
- }`,
- date_published: comment.createdAt,
- date_modified: comment.updatedAt,
- }
- }),
- }
-
- ctx.res.write(
- ctx.query.format === "xml" ? jsonfeedToRSS(data) : JSON.stringify(data),
- )
- ctx.res.end()
-
- return {
- props: {},
- }
-}
-
-const SiteFeed: React.FC = () => null
-
-export default SiteFeed
diff --git a/src/pages/_site/[site]/feed/index.tsx b/src/pages/_site/[site]/feed/index.tsx
deleted file mode 100644
index 6e39838b..00000000
--- a/src/pages/_site/[site]/feed/index.tsx
+++ /dev/null
@@ -1,25 +0,0 @@
-// @ts-ignore
-import jsonfeedToRSS from "jsonfeed-to-rss"
-import { GetServerSideProps } from "next"
-
-import { getJsonFeed, setHeader } from "~/lib/json-feed"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- setHeader(ctx)
- const domainOrSubdomain = ctx.params!.site as string
-
- const data = await getJsonFeed(domainOrSubdomain, "/feed")
-
- ctx.res.write(
- ctx.query.format === "xml" ? jsonfeedToRSS(data) : JSON.stringify(data),
- )
- ctx.res.end()
-
- return {
- props: {},
- }
-}
-
-const SiteFeed: React.FC = () => null
-
-export default SiteFeed
diff --git a/src/pages/_site/[site]/index.tsx b/src/pages/_site/[site]/index.tsx
deleted file mode 100644
index 0d56f358..00000000
--- a/src/pages/_site/[site]/index.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { GetServerSideProps } from "next"
-import type { ReactElement } from "react"
-
-import { QueryClient } from "@tanstack/react-query"
-
-import { SiteHome } from "~/components/site/SiteHome"
-import { SiteLayout } from "~/components/site/SiteLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/site/SiteLayout.server"
-import { PageVisibilityEnum } from "~/lib/types"
-import { useGetPagesBySiteLite } from "~/queries/page"
-import { useGetSite } from "~/queries/site"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- const queryClient = new QueryClient()
- const domainOrSubdomain = ctx.params!.site as string
- const { props: layoutProps } = await getLayoutServerSideProps(
- ctx,
- queryClient,
- {
- useStat: true,
- },
- )
-
- return {
- props: {
- ...layoutProps,
- domainOrSubdomain,
- },
- }
-}
-
-function SiteIndexPage({ domainOrSubdomain }: { domainOrSubdomain: string }) {
- const site = useGetSite(domainOrSubdomain)
- const posts = useGetPagesBySiteLite({
- characterId: site.data?.characterId,
- type: "post",
- visibility: PageVisibilityEnum.Published,
- useStat: true,
- })
-
- return (
-
- )
-}
-
-SiteIndexPage.getLayout = (page: ReactElement) => {
- return (
-
- {page}
-
- )
-}
-
-export default SiteIndexPage
diff --git a/src/pages/_site/[site]/manifest.json.tsx b/src/pages/_site/[site]/manifest.json.tsx
deleted file mode 100644
index cf3f59eb..00000000
--- a/src/pages/_site/[site]/manifest.json.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import { GetServerSideProps } from "next"
-
-import { QueryClient } from "@tanstack/react-query"
-
-import { fetchGetSite } from "~/queries/site.server"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- ctx.res.setHeader("Content-Type", "application/json")
-
- const queryClient = new QueryClient()
- const domainOrSubdomain = ctx.params!.site as string
- const site = await fetchGetSite(domainOrSubdomain, queryClient)
-
- ctx.res.write(
- JSON.stringify({
- name: site?.metadata?.content?.name,
- short_name: site?.metadata?.content?.name,
- description: site?.metadata?.content?.bio,
- icons: [
- {
- src: site?.metadata?.content?.avatars?.[0] || "assets/logo.png",
- type: "image/png",
- sizes: "any",
- },
- ],
- theme_color: "#ffffff",
- background_color: "#ffffff",
- start_url: "/",
- display: "standalone",
- orientation: "portrait",
- }),
- )
- ctx.res.end()
-
- return {
- props: {},
- }
-}
-
-const ManifestJson: React.FC = () => null
-
-export default ManifestJson
diff --git a/src/pages/_site/[site]/nft.tsx b/src/pages/_site/[site]/nft.tsx
deleted file mode 100644
index d2b66c3b..00000000
--- a/src/pages/_site/[site]/nft.tsx
+++ /dev/null
@@ -1,91 +0,0 @@
-import { GetServerSideProps } from "next"
-import { useTranslation } from "next-i18next"
-import Script from "next/script"
-import { ReactElement, useEffect, useState } from "react"
-import type { Asset } from "unidata.js"
-
-import { QueryClient } from "@tanstack/react-query"
-
-import { SiteLayout } from "~/components/site/SiteLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/site/SiteLayout.server"
-import { UniLink } from "~/components/ui/UniLink"
-import { UniMedia } from "~/components/ui/UniMedia"
-import { useGetNFTs, useGetSite } from "~/queries/site"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- const queryClient = new QueryClient()
- const domainOrSubdomain = ctx.params!.site as string
- const { props: layoutProps } = await getLayoutServerSideProps(
- ctx,
- queryClient,
- {
- skipPages: true,
- },
- )
-
- return {
- props: {
- ...layoutProps,
- domainOrSubdomain,
- },
- }
-}
-
-function SiteNFTPage({ domainOrSubdomain }: { domainOrSubdomain: string }) {
- const site = useGetSite(domainOrSubdomain)
- const { t } = useTranslation(["common", "site"])
-
- const nftsOrigin = useGetNFTs(site.data?.owner)
-
- const [nfts, setNfts] = useState([])
- useEffect(() => {
- if (nftsOrigin?.data?.list && !nfts.length) {
- setNfts(nftsOrigin.data.list)
- }
- }, [nftsOrigin.data, nfts])
-
- return (
- <>
-
- NFT {t("Showcase")}
-
- {nftsOrigin.isLoading ? (
-
{t("Loading")}...
- ) : (
-
- {nfts
- .filter((nft) => nft.items?.[0]?.address)
- .map((nft: Asset) => (
-
-
-
- {nft.name}
-
-
- ))}
-
- )}
-
- >
- )
-}
-
-SiteNFTPage.getLayout = (page: ReactElement) => {
- return {page}
-}
-
-export default SiteNFTPage
diff --git a/src/pages/_site/[site]/preview/[page].tsx b/src/pages/_site/[site]/preview/[page].tsx
deleted file mode 100644
index 6c9f191b..00000000
--- a/src/pages/_site/[site]/preview/[page].tsx
+++ /dev/null
@@ -1,73 +0,0 @@
-import { GetServerSideProps } from "next"
-import { useRouter } from "next/router"
-import type { ReactElement } from "react"
-
-import { QueryClient } from "@tanstack/react-query"
-
-import { SiteLayout } from "~/components/site/SiteLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/site/SiteLayout.server"
-import { SitePage } from "~/components/site/SitePage"
-import { useUserRole } from "~/hooks/useUserRole"
-import { getSiteLink } from "~/lib/helpers"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
-import { useGetPage } from "~/queries/page"
-import { useGetSite } from "~/queries/site"
-
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const queryClient = new QueryClient()
- const domainOrSubdomain = ctx.params!.site as string
-
- const { props: layoutProps } = await getLayoutServerSideProps(
- ctx,
- queryClient,
- {
- preview: true,
- },
- )
-
- return {
- props: {
- ...layoutProps,
- domainOrSubdomain,
- },
- }
- },
-)
-
-function SitePagePage() {
- const router = useRouter()
- const domainOrSubdomain = router.query.site as string
- const pageSlug = router.query.page as string
- const userRole = useUserRole(domainOrSubdomain)
-
- const site = useGetSite(domainOrSubdomain)
-
- const page = useGetPage({
- characterId: site.data?.characterId,
- slug: pageSlug,
- handle: domainOrSubdomain,
- })
-
- if (userRole.isSuccess && !userRole.data && page.isSuccess) {
- router.push(
- getSiteLink({
- subdomain: domainOrSubdomain,
- }),
- )
- }
-
- return (
-
- )
-}
-
-SitePagePage.getLayout = (page: ReactElement) => {
- return {page}
-}
-
-export default SitePagePage
diff --git a/src/pages/_site/[site]/robots.txt.tsx b/src/pages/_site/[site]/robots.txt.tsx
deleted file mode 100644
index e71b848a..00000000
--- a/src/pages/_site/[site]/robots.txt.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { GetServerSideProps } from "next"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- ctx.res.setHeader("Content-Type", "text/plain")
- ctx.res.setHeader("Access-Control-Allow-Methods", "GET")
- ctx.res.setHeader("Access-Control-Allow-Origin", "*")
-
- ctx.res.write(`User-agent: *
-Disallow: /dashboard/
-Disallow: /preview/
-
-Sitemap: https://${ctx.req.headers.host}/sitemap.xml`)
- ctx.res.end()
-
- return {
- props: {},
- }
-}
-
-const RobotsTxt: React.FC = () => null
-
-export default RobotsTxt
diff --git a/src/pages/_site/[site]/search.tsx b/src/pages/_site/[site]/search.tsx
deleted file mode 100644
index 1cb4d148..00000000
--- a/src/pages/_site/[site]/search.tsx
+++ /dev/null
@@ -1,76 +0,0 @@
-import { GetServerSideProps } from "next"
-import { useTranslation } from "next-i18next"
-import { useRouter } from "next/router"
-import type { ReactElement } from "react"
-
-import { QueryClient } from "@tanstack/react-query"
-
-import { SearchInput } from "~/components/common/SearchInput"
-import { SiteLayout } from "~/components/site/SiteLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/site/SiteLayout.server"
-import { SiteSearch } from "~/components/site/SiteSearch"
-import { useGetSearchPagesBySite } from "~/queries/page"
-import { useGetSite } from "~/queries/site"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- const queryClient = new QueryClient()
- const domainOrSubdomain = ctx.params!.site as string
- const { props: layoutProps } = await getLayoutServerSideProps(
- ctx,
- queryClient,
- {
- skipPages: true,
- },
- )
-
- return {
- props: {
- ...layoutProps,
- domainOrSubdomain,
- },
- }
-}
-
-function SiteSearchPage({ domainOrSubdomain }: { domainOrSubdomain: string }) {
- const site = useGetSite(domainOrSubdomain)
- const router = useRouter()
- const keyword = router.query.q as string
- const { t } = useTranslation(["common"])
-
- const posts = useGetSearchPagesBySite({
- characterId: site.data?.characterId,
- keyword,
- })
-
- return (
- <>
-
-
-
-
- {posts.data?.pages?.[0].count || "0"} {t("results")}
-
- {posts.isLoading ? (
- <>{t("Loading")}...>
- ) : (
-
- )}
- >
- )
-}
-
-SiteSearchPage.getLayout = (page: ReactElement) => {
- return (
-
- {page}
-
- )
-}
-
-export default SiteSearchPage
diff --git a/src/pages/_site/[site]/sitemap.xml.tsx b/src/pages/_site/[site]/sitemap.xml.tsx
deleted file mode 100644
index 683be892..00000000
--- a/src/pages/_site/[site]/sitemap.xml.tsx
+++ /dev/null
@@ -1,52 +0,0 @@
-import dayjs from "dayjs"
-import { GetServerSideProps } from "next"
-
-import { QueryClient } from "@tanstack/react-query"
-
-import { getSiteLink } from "~/lib/helpers"
-import { PageVisibilityEnum } from "~/lib/types"
-import { fetchGetPagesBySite } from "~/queries/page.server"
-import { fetchGetSite } from "~/queries/site.server"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- const queryClient = new QueryClient()
- ctx.res.setHeader("Content-Type", "text/xml")
- ctx.res.setHeader("Access-Control-Allow-Methods", "GET")
- ctx.res.setHeader("Access-Control-Allow-Origin", "*")
- const domainOrSubdomain = ctx.params!.site as string
-
- const site = await fetchGetSite(domainOrSubdomain, queryClient)
- const pages = await fetchGetPagesBySite(
- {
- characterId: site?.characterId,
- type: "post",
- visibility: PageVisibilityEnum.Published,
- limit: 1000,
- },
- queryClient,
- )
-
- const link = getSiteLink({
- domain: site?.metadata?.content?.custom_domain,
- subdomain: site?.handle || "",
- })
- ctx.res.write(`
-
-${pages.list?.map(
- (page: any) => `
- ${link}/${page.metadata.content.slug}
- ${dayjs(page.date_updated).format("YYYY-MM-DD")}
- `,
-).join(`
-`)}
-`)
- ctx.res.end()
-
- return {
- props: {},
- }
-}
-
-const SiteFeed: React.FC = () => null
-
-export default SiteFeed
diff --git a/src/pages/_site/[site]/tag/[tag].tsx b/src/pages/_site/[site]/tag/[tag].tsx
deleted file mode 100644
index 52409b90..00000000
--- a/src/pages/_site/[site]/tag/[tag].tsx
+++ /dev/null
@@ -1,69 +0,0 @@
-import { GetServerSideProps } from "next"
-import type { ReactElement } from "react"
-
-import { QueryClient } from "@tanstack/react-query"
-
-import { SiteArchives } from "~/components/site/SiteArchives"
-import { SiteLayout } from "~/components/site/SiteLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/site/SiteLayout.server"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
-import { PageVisibilityEnum } from "~/lib/types"
-import { useGetPagesBySiteLite } from "~/queries/page"
-import { useGetSite } from "~/queries/site"
-
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const queryClient = new QueryClient()
- const domainOrSubdomain = ctx.params!.site as string
- const tag = ctx.params!.tag as string
-
- const { props: layoutProps } = await getLayoutServerSideProps(
- ctx,
- queryClient,
- {
- limit: 100,
- },
- )
-
- return {
- props: {
- ...layoutProps,
- domainOrSubdomain,
- tag,
- },
- }
- },
-)
-
-function SiteTagPage({
- domainOrSubdomain,
- tag,
-}: {
- domainOrSubdomain: string
- tag: string
-}) {
- const site = useGetSite(domainOrSubdomain)
- const posts = useGetPagesBySiteLite({
- characterId: site.data?.characterId,
- limit: 100,
- type: "post",
- visibility: PageVisibilityEnum.Published,
- tags: [tag],
- })
-
- return (
-
- )
-}
-
-SiteTagPage.getLayout = (page: ReactElement) => {
- return {page}
-}
-
-export default SiteTagPage
diff --git a/src/pages/activities.tsx b/src/pages/activities.tsx
deleted file mode 100644
index 9ccdccf5..00000000
--- a/src/pages/activities.tsx
+++ /dev/null
@@ -1,86 +0,0 @@
-import { GetServerSideProps } from "next"
-import { serverSideTranslations } from "next-i18next/serverSideTranslations"
-import { ReactElement, useState } from "react"
-
-import { useAccountState, useConnectModal } from "@crossbell/connect-kit"
-import { QueryClient, dehydrate } from "@tanstack/react-query"
-
-import { MainFeed } from "~/components/main/MainFeed"
-import { MainLayout } from "~/components/main/MainLayout"
-import { MainSidebar } from "~/components/main/MainSidebar"
-import { Tabs } from "~/components/ui/Tabs"
-import { languageDetector } from "~/lib/language-detector"
-import type { FeedType } from "~/models/home.model"
-import { prefetchGetFeed, prefetchGetShowcase } from "~/queries/home.server"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- const queryClient = new QueryClient()
- await prefetchGetShowcase(queryClient)
- await prefetchGetFeed(
- {
- type: "latest",
- },
- queryClient,
- )
-
- return {
- props: {
- ...(await serverSideTranslations(languageDetector(ctx), [
- "common",
- "index",
- "dashboard",
- ])),
- dehydratedState: JSON.parse(JSON.stringify(dehydrate(queryClient))),
- },
- }
-}
-
-function Activities() {
- const currentCharacterId = useAccountState(
- (s) => s.computed.account?.characterId,
- )
- const connectModal = useConnectModal()
-
- const [feedType, setFeedType] = useState("latest")
- const tabs = [
- {
- text: "Latest",
- onClick: () => setFeedType("latest"),
- active: feedType === "latest",
- },
- {
- text: "Hottest",
- onClick: () => setFeedType("hot"),
- active: feedType === "hot",
- },
- {
- text: "Following",
- onClick: () => {
- if (!currentCharacterId) {
- connectModal.show()
- } else {
- setFeedType("following")
- }
- },
- active: feedType === "following",
- },
- ]
-
- return (
-
- )
-}
-
-Activities.getLayout = (page: ReactElement) => {
- return {page}
-}
-
-export default Activities
diff --git a/src/pages/api/check-domain.tsx b/src/pages/api/check-domain.tsx
deleted file mode 100644
index 64229d33..00000000
--- a/src/pages/api/check-domain.tsx
+++ /dev/null
@@ -1,19 +0,0 @@
-import { NextApiRequest, NextApiResponse } from "next"
-
-import { checkDomainServer } from "~/models/site.model"
-
-export default async function handler(
- req: NextApiRequest,
- res: NextApiResponse,
-) {
- const { handle, domain } = req.query
-
- if (!handle || !domain) {
- res.status(400).json({ error: "Missing characterId or domain" })
- return
- }
-
- res.status(200).json({
- data: await checkDomainServer(domain as string, handle as string),
- })
-}
diff --git a/src/pages/api/healthcheck.ts b/src/pages/api/healthcheck.ts
deleted file mode 100644
index 52f5b68c..00000000
--- a/src/pages/api/healthcheck.ts
+++ /dev/null
@@ -1,10 +0,0 @@
-import { NextApiRequest, NextApiResponse } from "next"
-
-export default async function healthcheck(
- req: NextApiRequest,
- res: NextApiResponse,
-) {
- res.status(200).json({
- ok: true,
- })
-}
diff --git a/src/pages/api/nfts.ts b/src/pages/api/nfts.ts
deleted file mode 100644
index 3b77bc63..00000000
--- a/src/pages/api/nfts.ts
+++ /dev/null
@@ -1,67 +0,0 @@
-import { NextApiRequest, NextApiResponse } from "next"
-import { Asset } from "unidata.js"
-
-import { IPFS_GATEWAY } from "~/lib/env"
-import { cacheGet, getRedis } from "~/lib/redis.server"
-import { getNFTs } from "~/models/site.model"
-
-export default async function handler(
- req: NextApiRequest,
- res: NextApiResponse,
-) {
- const query = req.query
-
- if (!query.address) {
- res.status(400).end()
- return
- }
-
- const redis = await getRedis()
- const redisKey = `nfts/${query.address}`
-
- let cache
- try {
- cache = await redis?.get(redisKey)
- } catch (error) {}
- if (cache) {
- res.status(200).json(JSON.parse(cache))
- } else {
- const result = await getNFTs(query.address as string)
- await Promise.all(
- result.list.map(async (nft: Asset) => {
- if (!nft.items?.[0].mime_type && nft.items?.[0]?.address) {
- try {
- new URL(nft.items[0].address)
- } catch (error) {
- return nft
- }
- try {
- const mime_type = await cacheGet({
- key: `nft-mimetype/${nft.items[0].address}`,
- getValueFun: async () => {
- const head = await fetch(
- `${nft.items![0].address!.replace(
- IPFS_GATEWAY,
- "https://gateway.ipfs.io/ipfs/",
- )}`,
- {
- method: "HEAD",
- },
- )
- return head.headers.get("content-type")
- },
- })
- nft.items[0].mime_type = mime_type
- } catch (error) {
- console.warn(error)
- }
- }
- return nft
- }),
- )
- redis?.set(redisKey, JSON.stringify(result), "EX", 60 * 60 * 24)
- res.status(200).json({
- list: [],
- })
- }
-}
diff --git a/src/pages/api/slug2id.ts b/src/pages/api/slug2id.ts
deleted file mode 100644
index 51fb49be..00000000
--- a/src/pages/api/slug2id.ts
+++ /dev/null
@@ -1,69 +0,0 @@
-import { NextApiRequest, NextApiResponse } from "next"
-
-import { getNoteSlug } from "~/lib/helpers"
-import { cacheDelete, cacheGet } from "~/lib/redis.server"
-
-export async function getIdBySlug(slug: string, characterId: string | number) {
- slug = (slug as string)?.toLowerCase?.()
-
- const result = (await cacheGet({
- key: ["slug2id", characterId, slug],
- getValueFun: async () => {
- let note
- let cursor = ""
-
- do {
- const response = await (
- await fetch(
- `https://indexer.crossbell.io/v1/notes?characterId=${characterId}&sources=xlog&cursor=${cursor}&limit=100`,
- )
- ).json()
- cursor = response.cursor
- note = response?.list?.find(
- (item: any) =>
- slug === getNoteSlug(item) ||
- slug === `${characterId}-${item.noteId}`,
- )
- } while (!note && cursor)
-
- return {
- noteId: note?.noteId,
- }
- },
- noUpdate: true,
- })) as {
- noteId: number
- }
-
- // revalidate
- if (result) {
- const noteIdMatch = slug.match(`^${characterId}-(\\d+)$`)
- if (!noteIdMatch?.[1]) {
- fetch(
- `https://indexer.crossbell.io/v1/characters/${characterId}/notes/${result.noteId}`,
- )
- .then((res) => res.json())
- .then((note) => {
- if ((note && getNoteSlug(note) !== slug) || note.deleted) {
- cacheDelete(["slug2id", characterId + "", slug])
- }
- })
- }
- }
-
- return result
-}
-
-export default async function handler(
- req: NextApiRequest,
- res: NextApiResponse,
-) {
- let { characterId, slug } = req.query
-
- if (!slug || !characterId) {
- res.status(400).send("Bad Request")
- return
- }
-
- res.status(200).send(await getIdBySlug(slug as string, characterId as string))
-}
diff --git a/src/pages/dashboard/[subdomain]/pages.tsx b/src/pages/dashboard/[subdomain]/pages.tsx
deleted file mode 100644
index 8012a44a..00000000
--- a/src/pages/dashboard/[subdomain]/pages.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { GetServerSideProps } from "next"
-import type { ReactElement } from "react"
-
-import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
-import { PagesManager } from "~/components/dashboard/PagesManager"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
-
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const { props: layoutProps } = await getLayoutServerSideProps(ctx)
-
- return {
- props: {
- ...layoutProps,
- },
- }
- },
-)
-
-export default function SubdomainPages() {
- return
-}
-
-SubdomainPages.getLayout = (page: ReactElement) => {
- return {page}
-}
diff --git a/src/pages/dashboard/[subdomain]/posts.tsx b/src/pages/dashboard/[subdomain]/posts.tsx
deleted file mode 100644
index ae09d358..00000000
--- a/src/pages/dashboard/[subdomain]/posts.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-import { GetServerSideProps } from "next"
-import type { ReactElement } from "react"
-
-import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
-import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
-import { PagesManager } from "~/components/dashboard/PagesManager"
-import { serverSidePropsHandler } from "~/lib/server-side-props"
-
-export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
- async (ctx) => {
- const { props: layoutProps } = await getLayoutServerSideProps(ctx)
-
- return {
- props: {
- ...layoutProps,
- },
- }
- },
-)
-
-export default function SubdomainPosts() {
- return
-}
-
-SubdomainPosts.getLayout = (page: ReactElement) => {
- return {page}
-}
diff --git a/src/pages/feed/hottest/0.tsx b/src/pages/feed/hottest/0.tsx
deleted file mode 100644
index 4c3dd571..00000000
--- a/src/pages/feed/hottest/0.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-// @ts-ignore
-import jsonfeedToRSS from "jsonfeed-to-rss"
-import { GetServerSideProps } from "next"
-
-import { SITE_URL } from "~/lib/env"
-import { parsePost, setHeader } from "~/lib/json-feed"
-import { ExpandedNote } from "~/lib/types"
-import { getFeed } from "~/models/home.model"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- setHeader(ctx)
-
- const feed = await getFeed({
- type: "hot",
- daysInterval: 0,
- })
-
- const data = {
- version: "https://jsonfeed.org/version/1",
- title: "xLog Hot",
- icon: "https://ipfs.4everland.xyz/ipfs/bafkreigxdnr5lvtjxqin5upquomrti2s77hlgtjy5zaeu43uhpny75rbga",
- home_page_url: `${SITE_URL}/activities`,
- feed_url: `${SITE_URL}/feed/hottest`,
- items: feed?.list?.map((post: ExpandedNote) =>
- parsePost(post, !!ctx.query.withTwitter),
- ),
- }
-
- ctx.res.write(
- ctx.query.format === "xml" ? jsonfeedToRSS(data) : JSON.stringify(data),
- )
- ctx.res.end()
-
- return {
- props: {},
- }
-}
-
-const LatestFeed: React.FC = () => null
-
-export default LatestFeed
diff --git a/src/pages/feed/hottest/1.tsx b/src/pages/feed/hottest/1.tsx
deleted file mode 100644
index 25c92dae..00000000
--- a/src/pages/feed/hottest/1.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-// @ts-ignore
-import jsonfeedToRSS from "jsonfeed-to-rss"
-import { GetServerSideProps } from "next"
-
-import { SITE_URL } from "~/lib/env"
-import { parsePost, setHeader } from "~/lib/json-feed"
-import { ExpandedNote } from "~/lib/types"
-import { getFeed } from "~/models/home.model"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- setHeader(ctx)
-
- const feed = await getFeed({
- type: "hot",
- daysInterval: 1,
- })
-
- const data = {
- version: "https://jsonfeed.org/version/1",
- title: "xLog Daily Hot",
- icon: "https://ipfs.4everland.xyz/ipfs/bafkreigxdnr5lvtjxqin5upquomrti2s77hlgtjy5zaeu43uhpny75rbga",
- home_page_url: `${SITE_URL}/activities`,
- feed_url: `${SITE_URL}/feed/hottest`,
- items: feed?.list?.map((post: ExpandedNote) =>
- parsePost(post, !!ctx.query.withTwitter),
- ),
- }
-
- ctx.res.write(
- ctx.query.format === "xml" ? jsonfeedToRSS(data) : JSON.stringify(data),
- )
- ctx.res.end()
-
- return {
- props: {},
- }
-}
-
-const LatestFeed: React.FC = () => null
-
-export default LatestFeed
diff --git a/src/pages/feed/hottest/30.tsx b/src/pages/feed/hottest/30.tsx
deleted file mode 100644
index bd383b50..00000000
--- a/src/pages/feed/hottest/30.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-// @ts-ignore
-import jsonfeedToRSS from "jsonfeed-to-rss"
-import { GetServerSideProps } from "next"
-
-import { SITE_URL } from "~/lib/env"
-import { parsePost, setHeader } from "~/lib/json-feed"
-import { ExpandedNote } from "~/lib/types"
-import { getFeed } from "~/models/home.model"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- setHeader(ctx)
-
- const feed = await getFeed({
- type: "hot",
- daysInterval: 30,
- })
-
- const data = {
- version: "https://jsonfeed.org/version/1",
- title: "xLog Monthly Hot",
- icon: "https://ipfs.4everland.xyz/ipfs/bafkreigxdnr5lvtjxqin5upquomrti2s77hlgtjy5zaeu43uhpny75rbga",
- home_page_url: `${SITE_URL}/activities`,
- feed_url: `${SITE_URL}/feed/hottest`,
- items: feed?.list?.map((post: ExpandedNote) =>
- parsePost(post, !!ctx.query.withTwitter),
- ),
- }
-
- ctx.res.write(
- ctx.query.format === "xml" ? jsonfeedToRSS(data) : JSON.stringify(data),
- )
- ctx.res.end()
-
- return {
- props: {},
- }
-}
-
-const LatestFeed: React.FC = () => null
-
-export default LatestFeed
diff --git a/src/pages/feed/hottest/7.tsx b/src/pages/feed/hottest/7.tsx
deleted file mode 100644
index 7364af4b..00000000
--- a/src/pages/feed/hottest/7.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-// @ts-ignore
-import jsonfeedToRSS from "jsonfeed-to-rss"
-import { GetServerSideProps } from "next"
-
-import { SITE_URL } from "~/lib/env"
-import { parsePost, setHeader } from "~/lib/json-feed"
-import { ExpandedNote } from "~/lib/types"
-import { getFeed } from "~/models/home.model"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- setHeader(ctx)
-
- const feed = await getFeed({
- type: "hot",
- daysInterval: 7,
- })
-
- const data = {
- version: "https://jsonfeed.org/version/1",
- title: "xLog Weekly Hot",
- icon: "https://ipfs.4everland.xyz/ipfs/bafkreigxdnr5lvtjxqin5upquomrti2s77hlgtjy5zaeu43uhpny75rbga",
- home_page_url: `${SITE_URL}/activities`,
- feed_url: `${SITE_URL}/feed/hottest`,
- items: feed?.list?.map((post: ExpandedNote) =>
- parsePost(post, !!ctx.query.withTwitter),
- ),
- }
-
- ctx.res.write(
- ctx.query.format === "xml" ? jsonfeedToRSS(data) : JSON.stringify(data),
- )
- ctx.res.end()
-
- return {
- props: {},
- }
-}
-
-const LatestFeed: React.FC = () => null
-
-export default LatestFeed
diff --git a/src/pages/index.tsx b/src/pages/index.tsx
deleted file mode 100644
index 8b0b356a..00000000
--- a/src/pages/index.tsx
+++ /dev/null
@@ -1,575 +0,0 @@
-import { GetServerSideProps } from "next"
-import { Trans, useTranslation } from "next-i18next"
-import { serverSideTranslations } from "next-i18next/serverSideTranslations"
-import { useRouter } from "next/router"
-import { ReactElement, useState } from "react"
-import { Element, Link } from "react-scroll"
-
-import { useAccountState } from "@crossbell/connect-kit"
-import {
- CrossbellChainLogo,
- XCharLogo,
- XFeedLogo,
- XShopLogo,
- XSyncLogo,
-} from "@crossbell/ui"
-import { RssIcon } from "@heroicons/react/24/outline"
-import { QueryClient, dehydrate } from "@tanstack/react-query"
-
-import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
-import { FollowAllButton } from "~/components/common/FollowAllButton"
-import { Logo } from "~/components/common/Logo"
-import { MainLayout } from "~/components/main/MainLayout"
-import { Button } from "~/components/ui/Button"
-import { Image } from "~/components/ui/Image"
-import { Tooltip } from "~/components/ui/Tooltip"
-import { UniLink } from "~/components/ui/UniLink"
-import { CSB_SCAN, GITHUB_LINK } from "~/lib/env"
-import { getSiteLink } from "~/lib/helpers"
-import { languageDetector } from "~/lib/language-detector"
-import { useGetShowcase } from "~/queries/home"
-import { prefetchGetShowcase } from "~/queries/home.server"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- const queryClient = new QueryClient()
- await prefetchGetShowcase(queryClient)
-
- return {
- props: {
- ...(await serverSideTranslations(languageDetector(ctx), [
- "common",
- "index",
- ])),
- dehydratedState: dehydrate(queryClient),
- },
- }
-}
-
-function Home() {
- const isConnected = useAccountState((s) => !!s.computed.account)
- const router = useRouter()
- const showcaseSites = useGetShowcase()
- const { t } = useTranslation("index")
-
- const tryNow = () => {
- router.push("/dashboard")
- }
-
- const features: {
- title: string
- subfeatures: {
- screenshot?: {
- src: string
- }
- icon: string
- title: string
- }[]
- extra?: boolean
- }[] = [
- {
- title: "Write",
- subfeatures: [
- {
- screenshot: {
- src: "/assets/easy.png",
- },
- icon: "ðĪŠ",
- title: "Easy to get started",
- },
- {
- screenshot: {
- src: "/assets/experience.png",
- },
- icon: "ð",
- title: "Elegant experience",
- },
- {
- screenshot: {
- src: "/assets/fast.png",
- },
- icon: "ð",
- title: "Fast",
- },
- ],
- },
- {
- title: "Own",
- subfeatures: [
- {
- screenshot: {
- src: "/assets/safe.png",
- },
- icon: "ð",
- title: "Safe",
- },
- {
- screenshot: {
- src: "/assets/customizable.png",
- },
- icon: "ðĻ",
- title: "Customizable",
- },
- {
- screenshot: {
- src: "/assets/open.png",
- },
- icon: "ð",
- title: "Open",
- },
- ],
- extra: true,
- },
- {
- title: "Earn",
- subfeatures: [
- {
- icon: "ðŠ",
- title: "Creator Incentives",
- },
- {
- icon: "ðïļ",
- title: "DAO",
- },
- ],
- },
- ]
-
- const integrations = [
- {
- name: "RSS",
- icon: ,
- url:
- getSiteLink({
- subdomain: "xlog",
- }) + "/feed?format=xml",
- },
- {
- name: "JSON Feed",
- icon: ,
- url:
- getSiteLink({
- subdomain: "xlog",
- }) + "/feed",
- },
- {
- name: "xChar",
- icon: ,
- url: "https://xchar.app/",
- },
- {
- name: "xFeed",
- icon: ,
- url: "https://crossbell.io/feed",
- },
- {
- name: "xSync",
- icon: ,
- url: "https://xsync.app/",
- },
- {
- name: "xShop",
- icon: ,
- text: "Coming soon",
- },
- {
- name: "Crossbell Scan",
- icon: ,
- url: "https://scan.crossbell.io/",
- },
- {
- name: "Crossbell Faucet",
- icon: ,
- url: "https://faucet.crossbell.io/",
- },
- {
- name: "Crossbell Export",
- icon: ,
- url: "https://export.crossbell.io/",
- },
- {
- name: "Crossbell SDK",
- icon: ,
- url: "https://crossbell-box.github.io/crossbell.js/",
- },
- {
- name: "RSS3",
- icon: (
-
- ),
- url: "https://rss3.io/",
- },
- {
- name: "Hoot It",
- icon: (
-
- ),
- url: "https://hoot.it/search/xLog",
- },
- {
- name: "Raycast",
- icon: ,
- url: "https://www.raycast.com/Songkeys/crossbell",
- },
- {
- name: "Obsidian",
- icon: (
-
- ),
- text: "Coming soon",
- },
- ]
-
- const [showcaseMore, setShowcaseMore] = useState(false)
-
- return (
-
-
-
- {features.map((feature) => (
-
- {t(feature.title)}
- {t(".")}
-
- ))}
-
-
-
- xLog is the best{" "}
-
- on-chain
- {" "}
- and{" "}
-
- open-source
- {" "}
- blogging community for everyone.
-
-
-
-
-
-
-
- {t("Explore the xLog way")}
-
-
-
-
-
- {features.map((feature, index) => (
-
-
-
-
- {index + 1}
-
-
- {t(feature.title)}
-
-
- {t(`features.${feature.title}.subtitle`)}
-
-
-
- .
-
- ),
- }}
- ns="index"
- />
-
-
-
- {feature.subfeatures.map((item) => (
- -
- {item.screenshot?.src && (
-
-
-
- )}
-
- {item.icon}
-
- {t(item.title)}
-
-
-
- ,
- axfm: (
-
- .
-
- ),
- aincentive: (
-
- .
-
- ),
- }}
- ns="index"
- />
-
-
- ))}
-
- {feature.extra && (
-
-
- ðĪŦ{" "}
-
- {t(`features.${feature.title}.extra.title`)}
-
-
-
{t(`features.${feature.title}.extra.description`)}
-
-
- )}
-
- ))}
-
-
-
- {t("Showcase")}
-
-
-
- {t(
- "Discover these awesome teams and creators on xLog (sorted by update time)",
- )}
-
-
s.characterId)
- .filter(Boolean)
- .map(Number)}
- siteIds={showcaseSites.data?.map(
- (s: { handle: string }) => s.handle,
- )}
- />
-
-
-
-
-
- {t("Integration")}
-
-
-
- {t(
- "xLog's open design allows it to integrate with many other open protocols and applications without friction.",
- )}
-
-
- {integrations.map((item, index) => (
- -
- {item.url ? (
-
-
- {item.icon}
-
-
- {item.name}
-
-
- ) : (
-
-
-
- {item.icon}
-
-
- {item.name}
-
-
-
- )}
-
- ))}
- -
-
-
-
-
-
-
-
-
-
-
- {isConnected ? (
-
-
-
- ) : (
-
- )}
-
-
- )
-}
-
-Home.getLayout = (page: ReactElement) => {
- return {page}
-}
-
-export default Home
diff --git a/src/pages/search.tsx b/src/pages/search.tsx
deleted file mode 100644
index c3940f77..00000000
--- a/src/pages/search.tsx
+++ /dev/null
@@ -1,54 +0,0 @@
-import { GetServerSideProps } from "next"
-import { serverSideTranslations } from "next-i18next/serverSideTranslations"
-import { useRouter } from "next/router"
-import { ReactElement } from "react"
-
-import { QueryClient, dehydrate } from "@tanstack/react-query"
-
-import { SearchInput } from "~/components/common/SearchInput"
-import { MainFeed } from "~/components/main/MainFeed"
-import { MainLayout } from "~/components/main/MainLayout"
-import { MainSidebar } from "~/components/main/MainSidebar"
-import { languageDetector } from "~/lib/language-detector"
-import { prefetchGetShowcase } from "~/queries/home.server"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- const queryClient = new QueryClient()
- await prefetchGetShowcase(queryClient)
-
- return {
- props: {
- ...(await serverSideTranslations(languageDetector(ctx), [
- "common",
- "index",
- "dashboard",
- ])),
- dehydratedState: dehydrate(queryClient),
- },
- }
-}
-
-function Search() {
- const router = useRouter()
- const keyword = router.query.q as string
-
- return (
-
- )
-}
-
-Search.getLayout = (page: ReactElement) => {
- return {page}
-}
-
-export default Search
diff --git a/src/pages/topic/[topic].tsx b/src/pages/topic/[topic].tsx
deleted file mode 100644
index db3124ca..00000000
--- a/src/pages/topic/[topic].tsx
+++ /dev/null
@@ -1,58 +0,0 @@
-import { GetServerSideProps } from "next"
-import { serverSideTranslations } from "next-i18next/serverSideTranslations"
-import { useRouter } from "next/router"
-import { ReactElement } from "react"
-
-import { QueryClient, dehydrate } from "@tanstack/react-query"
-
-import { MainFeed } from "~/components/main/MainFeed"
-import { MainLayout } from "~/components/main/MainLayout"
-import { MainSidebar } from "~/components/main/MainSidebar"
-import { languageDetector } from "~/lib/language-detector"
-import { prefetchGetShowcase } from "~/queries/home.server"
-
-import topics from "../../../data/topics.json"
-
-export const getServerSideProps: GetServerSideProps = async (ctx) => {
- const queryClient = new QueryClient()
- await prefetchGetShowcase(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 (
-
-
-
-
Topic: {topic}
-
{info?.description}
-
-
-
-
-
-
-
- )
-}
-
-Topic.getLayout = (page: ReactElement) => {
- return {page}
-}
-
-export default Topic
diff --git a/src/providers/LangProvider.tsx b/src/providers/LangProvider.tsx
new file mode 100644
index 00000000..738a9deb
--- /dev/null
+++ b/src/providers/LangProvider.tsx
@@ -0,0 +1,18 @@
+import { ReactNode, createContext } from "react"
+
+export interface LangContextType {
+ lang: string
+}
+
+export const LangContext = createContext(undefined)
+
+interface LangProviderProps {
+ children: ReactNode
+ lang: string
+}
+
+export function LangProvider({ children, lang }: LangProviderProps) {
+ return (
+ {children}
+ )
+}
diff --git a/src/queries/home.ts b/src/queries/home.ts
index 041ac416..480ff5fb 100644
--- a/src/queries/home.ts
+++ b/src/queries/home.ts
@@ -1,3 +1,5 @@
+"use client"
+
import { useInfiniteQuery, useQuery } from "@tanstack/react-query"
import * as homeModel from "~/models/home.model"
diff --git a/src/queries/page.server.ts b/src/queries/page.server.ts
index 7ba9558e..9df5c231 100644
--- a/src/queries/page.server.ts
+++ b/src/queries/page.server.ts
@@ -1,12 +1,62 @@
import { QueryClient } from "@tanstack/react-query"
-import { cacheGet } from "~/lib/redis.server"
+import { getNoteSlug } from "~/lib/helpers"
+import { cacheDelete, cacheGet } from "~/lib/redis.server"
import * as pageModel from "~/models/page.model"
-import { getIdBySlug } from "~/pages/api/slug2id"
-import { getSummary } from "~/pages/api/summary"
+
+export async function getIdBySlug(slug: string, characterId: string | number) {
+ slug = (slug as string)?.toLowerCase?.()
+
+ const result = (await cacheGet({
+ key: ["slug2id", characterId, slug],
+ getValueFun: async () => {
+ let note
+ let cursor = ""
+
+ do {
+ const response = await (
+ await fetch(
+ `https://indexer.crossbell.io/v1/notes?characterId=${characterId}&sources=xlog&cursor=${cursor}&limit=100`,
+ )
+ ).json()
+ cursor = response.cursor
+ note = response?.list?.find(
+ (item: any) =>
+ slug === getNoteSlug(item) ||
+ slug === `${characterId}-${item.noteId}`,
+ )
+ } while (!note && cursor)
+
+ return {
+ noteId: note?.noteId,
+ }
+ },
+ noUpdate: true,
+ })) as {
+ noteId: number
+ }
+
+ // revalidate
+ if (result) {
+ const noteIdMatch = slug.match(`^${characterId}-(\\d+)$`)
+ if (!noteIdMatch?.[1]) {
+ fetch(
+ `https://indexer.crossbell.io/v1/characters/${characterId}/notes/${result.noteId}`,
+ )
+ .then((res) => res.json())
+ .then((note) => {
+ if ((note && getNoteSlug(note) !== slug) || note.deleted) {
+ cacheDelete(["slug2id", characterId + "", slug])
+ }
+ })
+ }
+ }
+
+ return result
+}
export const fetchGetPage = async (
- input: Parameters[0],
+ input: Partial[0]>,
queryClient: QueryClient,
) => {
const key = ["getPage", input.characterId, input]
@@ -23,7 +73,14 @@ export const fetchGetPage = async (
}
return cacheGet({
key,
- getValueFun: () => pageModel.getPage(input),
+ getValueFun: () =>
+ pageModel.getPage({
+ slug: input.slug,
+ characterId: input.characterId!,
+ useStat: input.useStat,
+ noteId: input.noteId,
+ handle: input.handle,
+ }),
}) as Promise>
})
}
@@ -61,16 +118,3 @@ export const fetchGetPagesBySite = async (
}) as Promise>
})
}
-
-export const prefetchGetSummary = async (
- input: { cid?: string; lang?: string },
- queryClient: QueryClient,
-) => {
- const key = ["getSummary", input.cid, input.lang]
- await queryClient.fetchQuery(key, async () => {
- if (!input.cid || !input.lang) {
- return
- }
- return getSummary(input.cid, input.lang)
- })
-}
diff --git a/src/queries/page.ts b/src/queries/page.ts
index 0e54db32..4cebad29 100644
--- a/src/queries/page.ts
+++ b/src/queries/page.ts
@@ -1,3 +1,5 @@
+"use client"
+
import {
useAccountState,
useIsNoteLiked,
@@ -371,8 +373,6 @@ 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")
-
if (!input.address) {
return null
}
@@ -385,18 +385,7 @@ export function useGetMirrorXyz(input: { address?: string }) {
)
).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 {
+ return response?.data as {
title: string
type: string
size: number
diff --git a/src/queries/site.server.ts b/src/queries/site.server.ts
index c971ad51..9f2ec786 100644
--- a/src/queries/site.server.ts
+++ b/src/queries/site.server.ts
@@ -1,6 +1,9 @@
+import { Asset } from "unidata.js"
+
import { QueryClient } from "@tanstack/react-query"
-import { cacheGet } from "~/lib/redis.server"
+import { IPFS_GATEWAY } from "~/lib/env"
+import { cacheGet, getRedis } from "~/lib/redis.server"
import * as siteModel from "~/models/site.model"
export const prefetchGetSite = async (
@@ -88,3 +91,52 @@ export const fetchGetComments = async (
}) as Promise>
})
}
+
+export const getNFTs = async (address?: string) => {
+ const redis = await getRedis()
+ const redisKey = `nfts/${address}`
+
+ let cache
+ try {
+ cache = await redis?.get(redisKey)
+ } catch (error) {}
+ if (cache) {
+ return JSON.parse(cache)
+ } else {
+ const result = await siteModel.getNFTs(address as string)
+ await Promise.all(
+ result.list.map(async (nft: Asset) => {
+ if (!nft.items?.[0].mime_type && nft.items?.[0]?.address) {
+ try {
+ new URL(nft.items[0].address)
+ } catch (error) {
+ return nft
+ }
+ try {
+ const mime_type = await cacheGet({
+ key: `nft-mimetype/${nft.items[0].address}`,
+ getValueFun: async () => {
+ const head = await fetch(
+ `${nft.items![0].address!.replace(
+ IPFS_GATEWAY,
+ "https://gateway.ipfs.io/ipfs/",
+ )}`,
+ {
+ method: "HEAD",
+ },
+ )
+ return head.headers.get("content-type")
+ },
+ })
+ nft.items[0].mime_type = mime_type
+ } catch (error) {
+ console.warn(error)
+ }
+ }
+ return nft
+ }),
+ )
+ redis?.set(redisKey, JSON.stringify(result), "EX", 60 * 60 * 24)
+ return result
+ }
+}
diff --git a/src/queries/site.ts b/src/queries/site.ts
index c9292444..6bd4fd42 100644
--- a/src/queries/site.ts
+++ b/src/queries/site.ts
@@ -1,3 +1,5 @@
+"use client"
+
import {
useAccountState,
useFollowCharacter,
@@ -278,22 +280,6 @@ export function useRemoveOperator() {
)
}
-export const useGetNFTs = (address?: string) => {
- return useQuery(["getNFTs", address], async () => {
- if (!address) {
- return null
- }
- return await (
- await fetch(
- "/api/nfts?" +
- new URLSearchParams({
- address,
- } as any),
- )
- ).json()
- })
-}
-
export const useGetStat = (
data: Partial[0]>,
) => {
diff --git a/src/queries/unidata.ts b/src/queries/unidata.ts
index d131c9a0..4250f09c 100644
--- a/src/queries/unidata.ts
+++ b/src/queries/unidata.ts
@@ -1,3 +1,5 @@
+"use client"
+
import { useEffect, useState } from "react"
import type Unidata from "unidata.js"
import { useAccount } from "wagmi"
diff --git a/tsconfig.json b/tsconfig.json
index b9c9ccd1..d8c0da05 100644
--- a/tsconfig.json
+++ b/tsconfig.json
@@ -1,6 +1,10 @@
{
"compilerOptions": {
- "lib": ["DOM", "DOM.Iterable", "esnext"],
+ "lib": [
+ "DOM",
+ "DOM.Iterable",
+ "esnext"
+ ],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "preserve",
@@ -10,7 +14,9 @@
"strict": true,
"baseUrl": ".",
"paths": {
- "~/*": ["./src/*"]
+ "~/*": [
+ "./src/*"
+ ]
},
"noEmit": true,
"forceConsistentCasingInFileNames": true,
@@ -18,8 +24,22 @@
"skipLibCheck": true,
"incremental": true,
"module": "esnext",
- "typeRoots": ["./types"]
+ "typeRoots": [
+ "./types"
+ ],
+ "plugins": [
+ {
+ "name": "next"
+ }
+ ]
},
- "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
- "exclude": ["node_modules"]
+ "include": [
+ "next-env.d.ts",
+ "**/*.ts",
+ "**/*.tsx",
+ ".next/types/**/*.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
}