chore: remove pages
This commit is contained in:
parent
778d685cb0
commit
7007375ba7
|
|
@ -1,5 +1,5 @@
|
|||
import { OpenAI } from "langchain"
|
||||
import { AnalyzeDocumentChain, loadSummarizationChain } from "langchain/chains"
|
||||
import { OpenAI } from "langchain/llms/openai"
|
||||
import { PromptTemplate } from "langchain/prompts"
|
||||
import removeMarkdown from "remove-markdown"
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
<SiteLayout siteId={siteId} type="404">
|
||||
<SitePage
|
||||
page={
|
||||
{
|
||||
title: "404 - Whoops, this page is gone.",
|
||||
body: {
|
||||
content: `
|
||||
- [Back to Home](/)
|
||||
- [All posts](/archives)
|
||||
|
||||

|
||||
`,
|
||||
},
|
||||
} as any
|
||||
}
|
||||
/>
|
||||
</SiteLayout>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
import "aplayer-react/dist/index.css"
|
||||
import { appWithTranslation } from "next-i18next"
|
||||
import NextNProgress from "nextjs-progressbar"
|
||||
import { Toaster } from "react-hot-toast"
|
||||
import { AppPropsWithLayout } from "types/next"
|
||||
import { WagmiConfig, createClient } from "wagmi"
|
||||
|
||||
import {
|
||||
ConnectKitProvider,
|
||||
getDefaultClientConfig,
|
||||
} from "@crossbell/connect-kit"
|
||||
import {
|
||||
NotificationModal,
|
||||
NotificationModalColorScheme,
|
||||
} from "@crossbell/notification"
|
||||
import { Hydrate, 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 } from "~/lib/env"
|
||||
import { toGateway } from "~/lib/ipfs-parser"
|
||||
import { createIDBPersister } from "~/lib/persister.client"
|
||||
import { urlComposer } from "~/lib/url-composer"
|
||||
|
||||
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 = {
|
||||
text: `rgb(var(--tw-colors-i-zinc-800))`,
|
||||
textSecondary: `rgb(var(--tw-colors-i-gray-600))`,
|
||||
background: `rgb(var(--tw-colors-i-white))`,
|
||||
border: `var(--border-color)`,
|
||||
}
|
||||
|
||||
function MyApp({ Component, pageProps }: AppPropsWithLayout) {
|
||||
const getLayout = Component.getLayout ?? ((page) => page)
|
||||
|
||||
useDarkMode()
|
||||
useMobileLayout()
|
||||
|
||||
return (
|
||||
<WagmiConfig client={wagmiClient}>
|
||||
<PersistQueryClientProvider
|
||||
client={queryClient}
|
||||
persistOptions={{
|
||||
persister,
|
||||
dehydrateOptions: {
|
||||
shouldDehydrateQuery: (query) => {
|
||||
const queryIsReadyForPersistance =
|
||||
query.state.status === "success"
|
||||
if (queryIsReadyForPersistance) {
|
||||
return !((query.state?.data as any)?.pages?.length > 1)
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
},
|
||||
},
|
||||
}}
|
||||
>
|
||||
<ConnectKitProvider
|
||||
ipfsLinkToHttpLink={toGateway}
|
||||
urlComposer={urlComposer}
|
||||
signInStrategy="simple"
|
||||
ignoreWalletDisconnectEvent={true}
|
||||
>
|
||||
<Hydrate state={pageProps.dehydratedState}>
|
||||
{/* <ReactQueryDevtools /> */}
|
||||
<NextNProgress
|
||||
options={{ easing: "linear", speed: 500, trickleSpeed: 100 }}
|
||||
/>
|
||||
{getLayout(<Component {...pageProps} />)}
|
||||
<Toaster />
|
||||
<NotificationModal colorScheme={colorScheme} />
|
||||
</Hydrate>
|
||||
</ConnectKitProvider>
|
||||
</PersistQueryClientProvider>
|
||||
</WagmiConfig>
|
||||
)
|
||||
}
|
||||
|
||||
// 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)
|
||||
|
|
@ -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 (
|
||||
<SitePage page={page.data || undefined} site={site.data || undefined} />
|
||||
)
|
||||
}
|
||||
|
||||
SitePagePage.getLayout = (page: ReactElement) => {
|
||||
return (
|
||||
<SiteLayout useStat={true} type="post">
|
||||
{page}
|
||||
</SiteLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default SitePagePage
|
||||
|
|
@ -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 (
|
||||
<SiteArchives
|
||||
posts={posts.data}
|
||||
fetchNextPage={posts.fetchNextPage}
|
||||
hasNextPage={posts.hasNextPage}
|
||||
isFetchingNextPage={posts.isFetchingNextPage}
|
||||
showTags={true}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
SiteArchivesPage.getLayout = (page: ReactElement) => {
|
||||
return (
|
||||
<SiteLayout title="Archives" type="archive">
|
||||
{page}
|
||||
</SiteLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default SiteArchivesPage
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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 (
|
||||
<SiteHome
|
||||
posts={posts.data}
|
||||
fetchNextPage={posts.fetchNextPage}
|
||||
hasNextPage={posts.hasNextPage}
|
||||
isFetchingNextPage={posts.isFetchingNextPage}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
SiteIndexPage.getLayout = (page: ReactElement) => {
|
||||
return (
|
||||
<SiteLayout useStat={true} type="index">
|
||||
{page}
|
||||
</SiteLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default SiteIndexPage
|
||||
|
|
@ -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
|
||||
|
|
@ -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<Asset[]>([])
|
||||
useEffect(() => {
|
||||
if (nftsOrigin?.data?.list && !nfts.length) {
|
||||
setNfts(nftsOrigin.data.list)
|
||||
}
|
||||
}, [nftsOrigin.data, nfts])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Script
|
||||
type="module"
|
||||
src="https://cdn.jsdelivr.net/npm/@google/model-viewer/dist/model-viewer.min.js"
|
||||
></Script>
|
||||
<h2 className="text-xl font-bold page-title">NFT {t("Showcase")}</h2>
|
||||
<div className="mt-8">
|
||||
{nftsOrigin.isLoading ? (
|
||||
<div>{t("Loading")}...</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-3 md:grid-cols-4 gap-10">
|
||||
{nfts
|
||||
.filter((nft) => nft.items?.[0]?.address)
|
||||
.map((nft: Asset) => (
|
||||
<UniLink
|
||||
key={nft.metadata?.proof}
|
||||
className="xlog-nft flex items-center flex-col"
|
||||
href={nft.related_urls?.[nft.related_urls.length - 1]}
|
||||
data-collection={nft.metadata?.collection_address}
|
||||
data-network={nft.metadata?.network}
|
||||
data-token-id={nft.metadata?.token_id}
|
||||
data-name={nft.name}
|
||||
>
|
||||
<UniMedia
|
||||
src={nft.items?.[0]?.address || ""}
|
||||
mime_type={nft.items?.[0].mime_type}
|
||||
/>
|
||||
<div className="text-xs mt-2 text-center font-medium">
|
||||
{nft.name}
|
||||
</div>
|
||||
</UniLink>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
SiteNFTPage.getLayout = (page: ReactElement) => {
|
||||
return <SiteLayout type="nft">{page}</SiteLayout>
|
||||
}
|
||||
|
||||
export default SiteNFTPage
|
||||
|
|
@ -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 (
|
||||
<SitePage
|
||||
preview={true}
|
||||
page={page.data || undefined}
|
||||
site={site.data || undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
SitePagePage.getLayout = (page: ReactElement) => {
|
||||
return <SiteLayout type="post">{page}</SiteLayout>
|
||||
}
|
||||
|
||||
export default SitePagePage
|
||||
|
|
@ -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
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<div className="sm:-mx-5">
|
||||
<SearchInput value={keyword} />
|
||||
</div>
|
||||
<h2 className="mb-8 mt-5 text-zinc-500">
|
||||
{posts.data?.pages?.[0].count || "0"} {t("results")}
|
||||
</h2>
|
||||
{posts.isLoading ? (
|
||||
<>{t("Loading")}...</>
|
||||
) : (
|
||||
<SiteSearch
|
||||
postPages={posts.data?.pages}
|
||||
fetchNextPage={posts.fetchNextPage}
|
||||
hasNextPage={posts.hasNextPage}
|
||||
isFetchingNextPage={posts.isFetchingNextPage}
|
||||
keyword={keyword}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
SiteSearchPage.getLayout = (page: ReactElement) => {
|
||||
return (
|
||||
<SiteLayout useStat={true} type="index">
|
||||
{page}
|
||||
</SiteLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default SiteSearchPage
|
||||
|
|
@ -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(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||
${pages.list?.map(
|
||||
(page: any) => ` <url>
|
||||
<loc>${link}/${page.slug || page.id}</loc>
|
||||
<lastmod>${dayjs(page.date_updated).format("YYYY-MM-DD")}</lastmod>
|
||||
</url>`,
|
||||
).join(`
|
||||
`)}
|
||||
</urlset>`)
|
||||
ctx.res.end()
|
||||
|
||||
return {
|
||||
props: {},
|
||||
}
|
||||
}
|
||||
|
||||
const SiteFeed: React.FC = () => null
|
||||
|
||||
export default SiteFeed
|
||||
|
|
@ -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 (
|
||||
<SiteArchives
|
||||
posts={posts.data}
|
||||
fetchNextPage={posts.fetchNextPage}
|
||||
hasNextPage={posts.hasNextPage}
|
||||
isFetchingNextPage={posts.isFetchingNextPage}
|
||||
title={tag}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
SiteTagPage.getLayout = (page: ReactElement) => {
|
||||
return <SiteLayout type="tag">{page}</SiteLayout>
|
||||
}
|
||||
|
||||
export default SiteTagPage
|
||||
|
|
@ -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/home/HomeFeed"
|
||||
import { MainSidebar } from "~/components/home/HomeSidebar"
|
||||
import { MainLayout } from "~/components/main/MainLayout"
|
||||
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<FeedType>("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 (
|
||||
<section className="pt-24">
|
||||
<div className="max-w-screen-lg px-5 mx-auto flex">
|
||||
<div className="flex-1 min-w-[300px]">
|
||||
<Tabs items={tabs} className="border-none text-lg"></Tabs>
|
||||
<MainFeed type={feedType} />
|
||||
</div>
|
||||
<MainSidebar />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Activities.getLayout = (page: ReactElement) => {
|
||||
return <MainLayout>{page}</MainLayout>
|
||||
}
|
||||
|
||||
export default Activities
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { useRouter } from "next/router"
|
||||
import type { ReactElement } from "react"
|
||||
|
||||
import { AchievementItem } from "~/components/common/AchievementItem"
|
||||
import { DashboardMain } from "~/components/dashboard/DashboardMain"
|
||||
import { serverSidePropsHandler } from "~/lib/server-side-props"
|
||||
import { useGetAchievements, useGetSite } from "~/queries/site"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
||||
async (ctx) => {
|
||||
const { props: layoutProps } = await getLayoutServerSideProps(ctx)
|
||||
|
||||
return {
|
||||
props: {
|
||||
...layoutProps,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default function AchievementsPage() {
|
||||
const router = useRouter()
|
||||
const subdomain = router.query.subdomain as string
|
||||
const site = useGetSite(subdomain)
|
||||
|
||||
const achievement = useGetAchievements(site.data?.characterId)
|
||||
|
||||
return (
|
||||
<DashboardMain title="Achievements">
|
||||
<div className="min-w-[270px] max-w-screen-lg flex flex-col space-y-8">
|
||||
<>
|
||||
{achievement.data?.list?.map((series) => {
|
||||
let length = series.groups?.length
|
||||
if (!length) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div key={series.info.name}>
|
||||
<div className="text-lg font-medium mb-4">
|
||||
{series.info.title}
|
||||
</div>
|
||||
<div className="grid grid-cols-3 sm:grid-cols-7 gap-x-2 gap-y-5">
|
||||
{series.groups?.map((group) => (
|
||||
<AchievementItem
|
||||
group={group}
|
||||
key={group.info.name}
|
||||
layoutId="achievements"
|
||||
size={80}
|
||||
characterId={site.data?.characterId}
|
||||
isOwner={true}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</>
|
||||
</div>
|
||||
</DashboardMain>
|
||||
)
|
||||
}
|
||||
|
||||
AchievementsPage.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Achievements">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -1,154 +0,0 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { Trans, useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
import type { ReactElement } from "react"
|
||||
import { Virtuoso } from "react-virtuoso"
|
||||
|
||||
import { CommentItem } from "~/components/common/CommentItem"
|
||||
import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
|
||||
import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
|
||||
import { DashboardMain } from "~/components/dashboard/DashboardMain"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { serverSidePropsHandler } from "~/lib/server-side-props"
|
||||
import { useGetCommentsBySite, useGetSite } from "~/queries/site"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
||||
async (ctx) => {
|
||||
const { props: layoutProps } = await getLayoutServerSideProps(ctx)
|
||||
|
||||
return {
|
||||
props: {
|
||||
...layoutProps,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default function CommentsPage() {
|
||||
const router = useRouter()
|
||||
const subdomain = router.query.subdomain as string
|
||||
const { t } = useTranslation(["dashboard", "common"])
|
||||
|
||||
const site = useGetSite(subdomain)
|
||||
|
||||
const comments = useGetCommentsBySite({
|
||||
characterId: site.data?.characterId,
|
||||
})
|
||||
|
||||
const feedUrl =
|
||||
getSiteLink({
|
||||
subdomain: subdomain,
|
||||
}) + "/feed/comments"
|
||||
|
||||
return (
|
||||
<DashboardMain title="Comments">
|
||||
<div className="min-w-[270px] max-w-screen-lg">
|
||||
<div className="text-sm text-zinc-500 leading-relaxed">
|
||||
<p>
|
||||
{t(
|
||||
"You can subscribe to comments through an RSS reader to receive timely reminders.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t("Subscription address:")}{" "}
|
||||
<UniLink className="text-accent" href={feedUrl} target="_blank">
|
||||
{feedUrl}
|
||||
</UniLink>
|
||||
</p>
|
||||
</div>
|
||||
<div className="xlog-comment">
|
||||
<div className="prose space-y-4 pt-4">
|
||||
<Virtuoso
|
||||
className="xlog-comment-list"
|
||||
useWindowScroll
|
||||
data={comments.data?.pages}
|
||||
endReached={() =>
|
||||
comments.hasNextPage && comments.fetchNextPage()
|
||||
}
|
||||
components={{
|
||||
Footer: comments.isLoading ? Loader : undefined,
|
||||
}}
|
||||
itemContent={(_, p) =>
|
||||
p?.list?.map((comment, idx) => {
|
||||
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 (
|
||||
<div key={comment.transactionHash} className="mt-6">
|
||||
<div>
|
||||
{name}{" "}
|
||||
<Trans
|
||||
i18nKey="comment on your"
|
||||
values={{
|
||||
type: t(type || "", {
|
||||
ns: "common",
|
||||
}),
|
||||
toTitle,
|
||||
}}
|
||||
defaults="commented on your {{type}} <tolink>{{toTitle}}</tolink>"
|
||||
components={{
|
||||
tolink: (
|
||||
<UniLink
|
||||
href={`/api/redirection?characterId=${comment.characterId}¬eId=${comment.noteId}`}
|
||||
target="_blank"
|
||||
>
|
||||
.
|
||||
</UniLink>
|
||||
),
|
||||
}}
|
||||
ns="dashboard"
|
||||
/>
|
||||
:
|
||||
</div>
|
||||
<CommentItem
|
||||
className="mt-6"
|
||||
comment={comment}
|
||||
depth={0}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
}
|
||||
></Virtuoso>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardMain>
|
||||
)
|
||||
}
|
||||
|
||||
CommentsPage.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Comments">{page}</DashboardLayout>
|
||||
}
|
||||
|
||||
const Loader = () => {
|
||||
const { t } = useTranslation("common")
|
||||
return (
|
||||
<div
|
||||
className="relative mt-4 w-full text-sm text-center py-4"
|
||||
key={"loading"}
|
||||
>
|
||||
{t("Loading")}...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,880 +0,0 @@
|
|||
import { useDebounceEffect } from "ahooks"
|
||||
import type { Root } from "mdast"
|
||||
import { nanoid } from "nanoid"
|
||||
import { GetServerSideProps } from "next"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
import NodeID3 from "node-id3"
|
||||
import {
|
||||
ChangeEvent,
|
||||
FC,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from "react"
|
||||
import type { ReactElement } from "react"
|
||||
import toast from "react-hot-toast"
|
||||
import { shallow } from "zustand/shallow"
|
||||
|
||||
import type { EditorView } from "@codemirror/view"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
import { PageContent } from "~/components/common/PageContent"
|
||||
import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
|
||||
import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
|
||||
import { DashboardMain } from "~/components/dashboard/DashboardMain"
|
||||
import { OptionsButton } from "~/components/dashboard/OptionsButton"
|
||||
import { PublishButton } from "~/components/dashboard/PublishButton"
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { CodeMirrorEditor } from "~/components/ui/CodeMirror"
|
||||
import { EditorToolbar } from "~/components/ui/EditorToolbar"
|
||||
import { Input } from "~/components/ui/Input"
|
||||
import { Modal } from "~/components/ui/Modal"
|
||||
import { TagInput } from "~/components/ui/TagInput"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { toolbars } from "~/editor"
|
||||
import { useDate } from "~/hooks/useDate"
|
||||
import {
|
||||
Values,
|
||||
initialEditorState,
|
||||
useEditorState,
|
||||
} from "~/hooks/useEdtiorState"
|
||||
import { useGetState } from "~/hooks/useGetState"
|
||||
import { useIsMobileLayout } from "~/hooks/useMobileLayout"
|
||||
import { useSyncOnce } from "~/hooks/useSyncOnce"
|
||||
import { useUploadFile } from "~/hooks/useUploadFile"
|
||||
import { showConfetti } from "~/lib/confetti"
|
||||
import { getDefaultSlug } from "~/lib/default-slug"
|
||||
import { CSB_SCAN } from "~/lib/env"
|
||||
import { getSiteLink, getTwitterShareUrl } from "~/lib/helpers"
|
||||
import { getPageVisibility } from "~/lib/page-helpers"
|
||||
import { serverSidePropsHandler } from "~/lib/server-side-props"
|
||||
import { delStorage, setStorage } from "~/lib/storage"
|
||||
import { PageVisibilityEnum } from "~/lib/types"
|
||||
import { cn, pick } from "~/lib/utils"
|
||||
import { Rendered, renderPageContent } from "~/markdown"
|
||||
import { checkPageSlug } from "~/models/page.model"
|
||||
import {
|
||||
useCreateOrUpdatePage,
|
||||
useGetPage,
|
||||
useGetPagesBySiteLite,
|
||||
} from "~/queries/page"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
||||
async (ctx) => {
|
||||
const { props: layoutProps } = await getLayoutServerSideProps(ctx)
|
||||
|
||||
return {
|
||||
props: {
|
||||
...layoutProps,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
const getInputDatetimeValue = (date: Date | string, dayjs: any) => {
|
||||
const str = dayjs(date).format()
|
||||
return str.substring(0, ((str.indexOf("T") | 0) + 6) | 0)
|
||||
}
|
||||
|
||||
export default function SubdomainEditor() {
|
||||
const router = useRouter()
|
||||
const queryClient = useQueryClient()
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
let pageId = router.query.id as string | undefined
|
||||
const subdomain = router.query.subdomain as string
|
||||
const isPost = router.query.type === "post"
|
||||
|
||||
const site = useGetSite(subdomain)
|
||||
|
||||
const [draftKey, setDraftKey] = useState<string>("")
|
||||
useEffect(() => {
|
||||
if (subdomain) {
|
||||
let key
|
||||
if (!pageId) {
|
||||
const randomId = nanoid()
|
||||
key = `draft-${site.data?.characterId}-local-${randomId}`
|
||||
setDraftKey(key)
|
||||
queryClient.invalidateQueries([
|
||||
"getPagesBySite",
|
||||
site.data?.characterId,
|
||||
])
|
||||
router.replace(
|
||||
`/dashboard/${subdomain}/editor?id=local-${randomId}&type=${router.query.type}`,
|
||||
)
|
||||
} else {
|
||||
key = `draft-${site.data?.characterId}-${pageId}`
|
||||
}
|
||||
setDraftKey(key)
|
||||
setDefaultSlug(
|
||||
key
|
||||
.replace(`draft-${site.data?.characterId}-local-`, "")
|
||||
.replace(`draft-${site.data?.characterId}-`, ""),
|
||||
)
|
||||
}
|
||||
}, [subdomain, pageId, queryClient, router, site.data?.characterId])
|
||||
|
||||
const page = useGetPage({
|
||||
characterId: site.data?.characterId,
|
||||
noteId: pageId && /\d+/.test(pageId) ? +pageId : undefined,
|
||||
slug: pageId || draftKey.replace(`draft-${site.data?.characterId}-`, ""),
|
||||
handle: subdomain,
|
||||
})
|
||||
|
||||
const { data: posts = { pages: [] } } = useGetPagesBySiteLite({
|
||||
characterId: site.data?.characterId,
|
||||
limit: 100,
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
})
|
||||
|
||||
const userTags = useMemo(() => {
|
||||
const result = new Set<string>()
|
||||
|
||||
if (posts?.pages?.length) {
|
||||
for (const page of posts.pages) {
|
||||
for (const post of page.list) {
|
||||
post.metadata?.content?.tags?.forEach((tag) => {
|
||||
if (tag !== "post" && tag !== "page") {
|
||||
result.add(tag)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(result)
|
||||
}, [posts.pages])
|
||||
|
||||
const [visibility, setVisibility] = useState<PageVisibilityEnum>()
|
||||
|
||||
useEffect(() => {
|
||||
if (page.isSuccess) {
|
||||
setVisibility(getPageVisibility(page.data || undefined))
|
||||
}
|
||||
}, [page.isSuccess, page.data])
|
||||
|
||||
const uploadFile = useUploadFile()
|
||||
|
||||
// reset editor state when page changes
|
||||
useSyncOnce(() => {
|
||||
useEditorState.setState(initialEditorState)
|
||||
})
|
||||
|
||||
const values = useEditorState()
|
||||
|
||||
const [initialContent, setInitialContent] = useState("")
|
||||
const [defaultSlug, setDefaultSlug] = useState("")
|
||||
|
||||
const getValues = useGetState(values)
|
||||
const getDraftKey = useGetState(draftKey)
|
||||
|
||||
const updateValue = useCallback(
|
||||
<K extends keyof Values>(key: K, value: Values[K]) => {
|
||||
if (visibility !== PageVisibilityEnum.Draft) {
|
||||
setVisibility(PageVisibilityEnum.Modified)
|
||||
}
|
||||
|
||||
const values = getValues()
|
||||
const draftKey = getDraftKey()
|
||||
if (key === "title") {
|
||||
setDefaultSlug(
|
||||
getDefaultSlug(
|
||||
value as string,
|
||||
draftKey.replace(`draft-${site.data?.characterId}-`, ""),
|
||||
),
|
||||
)
|
||||
}
|
||||
if (key === "slug" && !/^[a-zA-Z0-9\-_]*$/.test(value as string)) {
|
||||
toast.error(
|
||||
t(
|
||||
"Slug can only contain letters, numbers, hyphens, and underscores.",
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
const newValues = { ...values, [key]: value }
|
||||
if (draftKey) {
|
||||
setStorage(draftKey, {
|
||||
date: +new Date(),
|
||||
values: newValues,
|
||||
isPost: isPost,
|
||||
})
|
||||
queryClient.invalidateQueries([
|
||||
"getPagesBySite",
|
||||
site.data?.characterId,
|
||||
])
|
||||
}
|
||||
useEditorState.setState(newValues)
|
||||
},
|
||||
[isPost, queryClient, subdomain, visibility],
|
||||
)
|
||||
|
||||
const createOrUpdatePage = useCreateOrUpdatePage()
|
||||
|
||||
const isMobileLayout = useIsMobileLayout()
|
||||
const [isRendering, setIsRendering] = useState(false)
|
||||
|
||||
const savePage = async (published: boolean) => {
|
||||
const check = await checkPageSlug({
|
||||
slug: values.slug || defaultSlug,
|
||||
characterId: site.data?.characterId,
|
||||
noteId: page?.data?.noteId,
|
||||
})
|
||||
if (check) {
|
||||
toast.error(check)
|
||||
} else {
|
||||
const uniqueTags = Array.from(new Set(values.tags.split(","))).join(",")
|
||||
createOrUpdatePage.mutate({
|
||||
...values,
|
||||
tags: uniqueTags,
|
||||
slug: values.slug || defaultSlug,
|
||||
siteId: subdomain,
|
||||
...(visibility === PageVisibilityEnum.Draft
|
||||
? {}
|
||||
: { pageId: `${page?.data?.characterId}-${page?.data?.noteId}` }),
|
||||
isPost: isPost,
|
||||
published,
|
||||
externalUrl:
|
||||
(values.slug || defaultSlug) &&
|
||||
`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(values.slug || defaultSlug)}`,
|
||||
applications: page.data?.metadata?.content?.sources,
|
||||
characterId: site.data?.characterId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const [isCheersOpen, setIsCheersOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (createOrUpdatePage.isSuccess) {
|
||||
if (createOrUpdatePage.data?.code === 0) {
|
||||
if (draftKey) {
|
||||
delStorage(draftKey)
|
||||
queryClient.invalidateQueries([
|
||||
"getPagesBySite",
|
||||
site.data?.characterId,
|
||||
])
|
||||
queryClient.invalidateQueries([
|
||||
"getPage",
|
||||
draftKey.replace(`draft-${site.data?.characterId}-`, ""),
|
||||
])
|
||||
} else {
|
||||
queryClient.invalidateQueries(["getPage", pageId])
|
||||
}
|
||||
|
||||
if (createOrUpdatePage.data.data) {
|
||||
router.replace(
|
||||
`/dashboard/${subdomain}/editor?id=${site.data?.characterId}-${createOrUpdatePage.data.data}&type=${router.query.type}`,
|
||||
)
|
||||
}
|
||||
|
||||
setIsCheersOpen(true)
|
||||
showConfetti()
|
||||
} else {
|
||||
toast.error("Error: " + createOrUpdatePage.data?.message)
|
||||
}
|
||||
}
|
||||
}, [createOrUpdatePage.isSuccess])
|
||||
|
||||
useEffect(() => {
|
||||
if (!page.data || !draftKey) return
|
||||
setInitialContent(page.data.metadata?.content?.content || "")
|
||||
useEditorState.setState({
|
||||
title: page.data.metadata?.content?.title || "",
|
||||
publishedAt: page.data.metadata?.content?.date_published,
|
||||
published: !!page.data.noteId,
|
||||
excerpt: page.data.metadata?.content?.summary || "",
|
||||
slug: page.data.metadata?.content?.slug || "",
|
||||
tags:
|
||||
page.data.metadata?.content?.tags
|
||||
?.filter((tag) => tag !== "post" && tag !== "page")
|
||||
?.join(", ") || "",
|
||||
content: page.data.metadata?.content?.content || "",
|
||||
})
|
||||
setDefaultSlug(
|
||||
getDefaultSlug(
|
||||
page.data.metadata?.content?.title || "",
|
||||
draftKey.replace(`draft-${site.data?.characterId}-`, ""),
|
||||
),
|
||||
)
|
||||
}, [page.data, subdomain, draftKey, site.data?.characterId])
|
||||
|
||||
const [currentScrollArea, setCurrentScrollArea] = useState<string>("")
|
||||
const [view, setView] = useState<EditorView>()
|
||||
const [tree, setTree] = useState<Root | null>()
|
||||
|
||||
// preview
|
||||
|
||||
const [parsedContent, setParsedContent] = useState<Rendered | undefined>()
|
||||
|
||||
useDebounceEffect(
|
||||
() => {
|
||||
const result = renderPageContent(values.content)
|
||||
setTree(result.tree)
|
||||
setParsedContent(result)
|
||||
},
|
||||
[values.content],
|
||||
{
|
||||
wait: 500,
|
||||
},
|
||||
)
|
||||
|
||||
const previewRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
// editor
|
||||
const onCreateEditor = useCallback(
|
||||
(view: EditorView) => {
|
||||
setView?.(view)
|
||||
},
|
||||
[setView],
|
||||
)
|
||||
|
||||
const onChange = useCallback(
|
||||
(value: string) => {
|
||||
updateValue("content", value)
|
||||
},
|
||||
[updateValue],
|
||||
)
|
||||
|
||||
const handleDropFile = useCallback(
|
||||
async (file: File) => {
|
||||
const toastId = toast.loading("Uploading...")
|
||||
try {
|
||||
if (
|
||||
!file.type.startsWith("image/") &&
|
||||
!file.type.startsWith("audio/")
|
||||
) {
|
||||
throw new Error("You can only upload images and audios")
|
||||
}
|
||||
|
||||
const { key } = await uploadFile(file)
|
||||
toast.success("Uploaded!", {
|
||||
id: toastId,
|
||||
})
|
||||
if (file.type.startsWith("image/")) {
|
||||
view?.dispatch(
|
||||
view.state.replaceSelection(
|
||||
`\n\n`,
|
||||
),
|
||||
)
|
||||
} else if (file.type.startsWith("audio/")) {
|
||||
const fileArrayBuffer = await file.arrayBuffer()
|
||||
const fileBuffer = Buffer.from(fileArrayBuffer)
|
||||
const tags = NodeID3.read(fileBuffer)
|
||||
const name = tags.title ?? file.name
|
||||
const artist = tags.artist
|
||||
const cover = await (async () => {
|
||||
const image = tags.image
|
||||
if (!image || typeof image === "string") return image
|
||||
|
||||
const toastId = toast.loading("Uploading cover...")
|
||||
const { key } = await uploadFile(
|
||||
new Blob([image.imageBuffer], { type: image.type.name }),
|
||||
)
|
||||
toast.success("Uploaded cover!", {
|
||||
id: toastId,
|
||||
})
|
||||
return key
|
||||
})()
|
||||
view?.dispatch(
|
||||
view.state.replaceSelection(
|
||||
`\n<audio src="${key}" name="${name}" ${
|
||||
artist ? `artist="${artist}"` : ""
|
||||
} ${cover ? `cover="${cover}"` : ""}><audio>\n`,
|
||||
),
|
||||
)
|
||||
} else if (file.type === "text/plain") {
|
||||
view?.dispatch(view.state.replaceSelection(key))
|
||||
} else {
|
||||
throw new Error("Unknown upload file type")
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message, { id: toastId })
|
||||
}
|
||||
}
|
||||
},
|
||||
[uploadFile, view],
|
||||
)
|
||||
|
||||
const computedPosition = useCallback(() => {
|
||||
let previewChildNodes = previewRef.current?.childNodes[0]?.childNodes
|
||||
const editorElementList: number[] = []
|
||||
const previewElementList: number[] = []
|
||||
if (view?.state && previewChildNodes) {
|
||||
tree?.children.forEach((child, index) => {
|
||||
if (
|
||||
child.position &&
|
||||
previewChildNodes?.[index] &&
|
||||
(child as any).tagName !== "style"
|
||||
) {
|
||||
if (child.position.start.line > view.state.doc.lines) return
|
||||
const line = view.state?.doc.line(child.position.start.line)
|
||||
const block = view.lineBlockAt(line.from)
|
||||
if (block) {
|
||||
editorElementList.push(block.top)
|
||||
previewElementList.push(
|
||||
(previewChildNodes[index] as HTMLElement).offsetTop,
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
return {
|
||||
editorElementList,
|
||||
previewElementList,
|
||||
}
|
||||
}, [view, tree])
|
||||
|
||||
const onScroll = useCallback(
|
||||
(scrollTop: number, area: string) => {
|
||||
if (
|
||||
currentScrollArea === area &&
|
||||
previewRef.current?.parentElement &&
|
||||
view
|
||||
) {
|
||||
const position = computedPosition()
|
||||
|
||||
let selfElement
|
||||
let selfPosition
|
||||
let targetElement
|
||||
let targetPosition
|
||||
if (area === "preview") {
|
||||
selfElement = previewRef.current.parentElement
|
||||
selfPosition = position.previewElementList
|
||||
targetElement = view.scrollDOM
|
||||
targetPosition = position.editorElementList
|
||||
} else {
|
||||
selfElement = view.scrollDOM
|
||||
selfPosition = position.editorElementList
|
||||
targetElement = previewRef.current.parentElement
|
||||
targetPosition = position.previewElementList
|
||||
}
|
||||
|
||||
let scrollElementIndex = 0
|
||||
for (let i = 0; i < selfPosition.length; i++) {
|
||||
if (scrollTop < selfPosition[i]) {
|
||||
scrollElementIndex = i - 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// scroll to bottom
|
||||
if (scrollTop >= selfElement.scrollHeight - selfElement.clientHeight) {
|
||||
targetElement.scrollTop =
|
||||
targetElement.scrollHeight - targetElement.clientHeight
|
||||
return
|
||||
}
|
||||
|
||||
// scroll to position
|
||||
if (scrollElementIndex >= 0) {
|
||||
let ratio =
|
||||
(scrollTop - selfPosition[scrollElementIndex]) /
|
||||
(selfPosition[scrollElementIndex + 1] -
|
||||
selfPosition[scrollElementIndex])
|
||||
targetElement.scrollTop =
|
||||
ratio *
|
||||
(targetPosition[scrollElementIndex + 1] -
|
||||
targetPosition[scrollElementIndex]) +
|
||||
targetPosition[scrollElementIndex]
|
||||
}
|
||||
}
|
||||
},
|
||||
[view, computedPosition, currentScrollArea],
|
||||
)
|
||||
|
||||
const onEditorScroll = useCallback(
|
||||
(scrollTop: number) => {
|
||||
onScroll(scrollTop, "editor")
|
||||
},
|
||||
[onScroll],
|
||||
)
|
||||
|
||||
const onPreviewScroll = useCallback(
|
||||
(scrollTop: number) => {
|
||||
onScroll(scrollTop, "preview")
|
||||
},
|
||||
[onScroll],
|
||||
)
|
||||
|
||||
const onPreviewButtonClick = useCallback(() => {
|
||||
window.open(
|
||||
`/_site/${subdomain}/preview/${draftKey.replace(
|
||||
`draft-${site.data?.characterId}-`,
|
||||
"",
|
||||
)}`,
|
||||
)
|
||||
}, [draftKey, subdomain, site.data?.characterId])
|
||||
|
||||
const extraProperties = (
|
||||
<EditorExtraProperties
|
||||
defaultSlug={defaultSlug}
|
||||
updateValue={updateValue}
|
||||
isPost={isPost}
|
||||
subdomain={subdomain}
|
||||
userTags={userTags}
|
||||
/>
|
||||
)
|
||||
|
||||
const discardChanges = useCallback(() => {
|
||||
if (draftKey) {
|
||||
delStorage(draftKey)
|
||||
queryClient.invalidateQueries(["getPagesBySite", site.data?.characterId])
|
||||
page.remove()
|
||||
page.refetch()
|
||||
}
|
||||
}, [draftKey, site.data?.characterId])
|
||||
|
||||
return (
|
||||
<>
|
||||
<DashboardMain fullWidth>
|
||||
{page.isLoading ? (
|
||||
<div className="flex justify-center items-center min-h-[300px]">
|
||||
{t("Loading")}...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<header
|
||||
className={`flex justify-between absolute top-0 left-0 right-0 z-25 px-5 h-14 border-b items-center text-sm ${
|
||||
isMobileLayout ? "w-screen" : undefined
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`flex items-center overflow-x-auto scrollbar-hide ${
|
||||
isMobileLayout ? "flex-1" : undefined
|
||||
}`}
|
||||
>
|
||||
<EditorToolbar view={view} toolbars={toolbars}></EditorToolbar>
|
||||
</div>
|
||||
{isMobileLayout ? (
|
||||
<div className="flex items-center space-x-3 w-auto pl-5">
|
||||
<OptionsButton
|
||||
visibility={visibility}
|
||||
savePage={savePage}
|
||||
published={visibility !== PageVisibilityEnum.Draft}
|
||||
isRendering={isRendering}
|
||||
renderPage={setIsRendering}
|
||||
propertiesWidget={extraProperties}
|
||||
previewPage={onPreviewButtonClick}
|
||||
isPost={isPost}
|
||||
isModified={visibility === PageVisibilityEnum.Modified}
|
||||
discardChanges={discardChanges}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-3 flex-shrink-0">
|
||||
<span
|
||||
className={cn(
|
||||
`text-sm capitalize`,
|
||||
visibility === PageVisibilityEnum.Draft
|
||||
? `text-zinc-300`
|
||||
: visibility === PageVisibilityEnum.Modified
|
||||
? "text-orange-600"
|
||||
: "text-green-600",
|
||||
)}
|
||||
>
|
||||
{t(visibility as string)}
|
||||
</span>
|
||||
<Button isAutoWidth onClick={onPreviewButtonClick}>
|
||||
{t("Preview")}
|
||||
</Button>
|
||||
<PublishButton
|
||||
save={savePage}
|
||||
twitterShareUrl={
|
||||
page.data && site.data
|
||||
? getTwitterShareUrl({
|
||||
page: page.data,
|
||||
site: site.data,
|
||||
t,
|
||||
})
|
||||
: ""
|
||||
}
|
||||
published={visibility !== PageVisibilityEnum.Draft}
|
||||
isSaving={createOrUpdatePage.isLoading}
|
||||
isDisabled={
|
||||
visibility !== PageVisibilityEnum.Modified &&
|
||||
visibility !== PageVisibilityEnum.Draft
|
||||
}
|
||||
isPost={isPost}
|
||||
isModified={visibility === PageVisibilityEnum.Modified}
|
||||
discardChanges={discardChanges}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</header>
|
||||
<div
|
||||
className={`pt-14 flex w-full ${
|
||||
isMobileLayout
|
||||
? "w-screen h-[calc(100vh-4rem)]"
|
||||
: "min-w-[840px] h-screen "
|
||||
}`}
|
||||
>
|
||||
<div className="h-full overflow-auto w-full">
|
||||
<div className="h-full mx-auto pt-5 flex flex-col">
|
||||
<div className="px-5 h-12">
|
||||
<input
|
||||
type="text"
|
||||
name="title"
|
||||
value={values.title}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === "Enter" && !e.nativeEvent.isComposing) {
|
||||
view?.focus()
|
||||
}
|
||||
}}
|
||||
onChange={(e) => updateValue("title", e.target.value)}
|
||||
className="h-12 ml-1 inline-flex items-center border-none text-3xl font-bold w-full focus:outline-none bg-white"
|
||||
placeholder={t("Title goes here...") || ""}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5 flex-1 flex overflow-hidden">
|
||||
{isMobileLayout ? (
|
||||
!isRendering ? (
|
||||
<CodeMirrorEditor
|
||||
value={initialContent}
|
||||
onChange={onChange}
|
||||
handleDropFile={handleDropFile}
|
||||
onScroll={onEditorScroll}
|
||||
// onUpdate={onUpdate}
|
||||
onCreateEditor={onCreateEditor}
|
||||
onMouseEnter={() => {
|
||||
setCurrentScrollArea("editor")
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<PageContent
|
||||
className={`px-5 overflow-scroll pb-[200px] ${
|
||||
isMobileLayout ? "" : "w-1/2 "
|
||||
}`}
|
||||
parsedContent={parsedContent}
|
||||
inputRef={previewRef}
|
||||
onScroll={onPreviewScroll}
|
||||
onMouseEnter={() => {
|
||||
setCurrentScrollArea("preview")
|
||||
}}
|
||||
></PageContent>
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<CodeMirrorEditor
|
||||
value={initialContent}
|
||||
onChange={onChange}
|
||||
handleDropFile={handleDropFile}
|
||||
onScroll={onEditorScroll}
|
||||
// onUpdate={onUpdate}
|
||||
onCreateEditor={onCreateEditor}
|
||||
onMouseEnter={() => {
|
||||
setCurrentScrollArea("editor")
|
||||
}}
|
||||
/>
|
||||
<PageContent
|
||||
className={`px-5 overflow-scroll pb-[200px] ${
|
||||
isMobileLayout ? "" : "w-1/2 "
|
||||
}`}
|
||||
parsedContent={parsedContent}
|
||||
inputRef={previewRef}
|
||||
onScroll={onPreviewScroll}
|
||||
onMouseEnter={() => {
|
||||
setCurrentScrollArea("preview")
|
||||
}}
|
||||
></PageContent>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{!isMobileLayout && (
|
||||
<EditorExtraProperties
|
||||
defaultSlug={defaultSlug}
|
||||
updateValue={updateValue}
|
||||
isPost={isPost}
|
||||
userTags={userTags}
|
||||
subdomain={subdomain}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</DashboardMain>
|
||||
<Modal
|
||||
open={isCheersOpen}
|
||||
setOpen={setIsCheersOpen}
|
||||
title={`🎉 ${t("Published!")}`}
|
||||
>
|
||||
<div className="p-5">
|
||||
{t(
|
||||
"Your post has been securely stored on the blockchain. Now you may want to",
|
||||
)}
|
||||
<ul className="list-disc pl-5 mt-2 space-y-1">
|
||||
<li>
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(values.slug || defaultSlug)}`}
|
||||
>
|
||||
{t("View the post")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={`${CSB_SCAN}/tx/${
|
||||
page.data?.updatedTransactionHash ||
|
||||
page.data?.transactionHash
|
||||
}`}
|
||||
>
|
||||
{t("View the transaction")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={
|
||||
page.data && site.data
|
||||
? getTwitterShareUrl({
|
||||
page: page.data,
|
||||
site: site.data,
|
||||
t,
|
||||
})
|
||||
: ""
|
||||
}
|
||||
>
|
||||
{t("Share to Twitter")}
|
||||
</UniLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="h-16 border-t flex items-center px-5">
|
||||
<Button isBlock onClick={() => setIsCheersOpen(false)}>
|
||||
{t("Got it, thanks!")}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
SubdomainEditor.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Editor">{page}</DashboardLayout>
|
||||
}
|
||||
|
||||
const EditorExtraProperties: FC<{
|
||||
updateValue: <K extends keyof Values>(key: K, value: Values[K]) => void
|
||||
isPost: boolean
|
||||
|
||||
subdomain: string
|
||||
defaultSlug: string
|
||||
userTags: string[]
|
||||
}> = memo(({ isPost, updateValue, subdomain, defaultSlug, userTags }) => {
|
||||
const values = useEditorState(
|
||||
(state) => pick(state, ["publishedAt", "slug", "excerpt", "tags"]),
|
||||
shallow,
|
||||
)
|
||||
const date = useDate()
|
||||
const { t } = useTranslation("dashboard")
|
||||
const site = useGetSite(subdomain)
|
||||
|
||||
return (
|
||||
<div className="h-full overflow-auto flex-shrink-0 w-[280px] border-l bg-zinc-50 p-5 space-y-5">
|
||||
<div>
|
||||
<Input
|
||||
type="datetime-local"
|
||||
label={t("Publish at") || ""}
|
||||
isBlock
|
||||
name="publishAt"
|
||||
id="publishAt"
|
||||
value={getInputDatetimeValue(values.publishedAt, date.dayjs)}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => {
|
||||
try {
|
||||
const value = date.inLocalTimezone(e.target.value).toISOString()
|
||||
updateValue("publishedAt", value)
|
||||
} catch (error) {}
|
||||
}}
|
||||
help={t(
|
||||
`This ${
|
||||
isPost ? "post" : "page"
|
||||
} will be accessible from this time`,
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
name="slug"
|
||||
value={values.slug}
|
||||
placeholder={defaultSlug}
|
||||
label={t(`${isPost ? "Post" : "Page"} slug`) || ""}
|
||||
id="slug"
|
||||
isBlock
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
updateValue("slug", e.target.value)
|
||||
}
|
||||
help={
|
||||
<>
|
||||
{(values.slug || defaultSlug) && (
|
||||
<>
|
||||
{t(`This ${isPost ? "post" : "page"} will be accessible at`)}{" "}
|
||||
<UniLink
|
||||
href={`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(values.slug || defaultSlug)}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
noProtocol: true,
|
||||
})}
|
||||
/{encodeURIComponent(values.slug || defaultSlug)}
|
||||
</UniLink>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
name="tags"
|
||||
value={values.tags}
|
||||
label={t("Tags") || ""}
|
||||
id="tags"
|
||||
isBlock
|
||||
renderInput={(props) => (
|
||||
<TagInput
|
||||
{...props}
|
||||
userTags={userTags}
|
||||
onTagChange={(value: string) => updateValue("tags", value)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Input
|
||||
label={t("Excerpt") || ""}
|
||||
isBlock
|
||||
name="excerpt"
|
||||
id="excerpt"
|
||||
value={values.excerpt}
|
||||
multiline
|
||||
rows={5}
|
||||
onChange={(e: ChangeEvent<HTMLTextAreaElement>) => {
|
||||
updateValue("excerpt", e.target.value)
|
||||
}}
|
||||
help={t("Leave it blank to use auto-generated excerpt")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})
|
||||
|
||||
EditorExtraProperties.displayName = "EditorExtraProperties"
|
||||
|
|
@ -1,195 +0,0 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import type { ReactElement } from "react"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
|
||||
import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
|
||||
import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
|
||||
import { DashboardMain } from "~/components/dashboard/DashboardMain"
|
||||
import { Avatar } from "~/components/ui/Avatar"
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { useDate } from "~/hooks/useDate"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { serverSidePropsHandler } from "~/lib/server-side-props"
|
||||
import { getStorage, setStorage } from "~/lib/storage"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { useGetPagesBySite } from "~/queries/page"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
||||
async (ctx) => {
|
||||
const { props: layoutProps } = await getLayoutServerSideProps(ctx)
|
||||
|
||||
return {
|
||||
props: {
|
||||
...layoutProps,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
function SiteAvatar({ siteId }: { siteId: string }) {
|
||||
const site = useGetSite(siteId)
|
||||
return (
|
||||
<div className="inline-block w-10 h-10 relative">
|
||||
<CharacterFloatCard siteId={siteId}>
|
||||
<UniLink
|
||||
href={getSiteLink({
|
||||
subdomain: siteId,
|
||||
})}
|
||||
>
|
||||
<Avatar
|
||||
images={site.data?.metadata?.content?.avatars || []}
|
||||
name={site.data?.metadata?.content?.name || ""}
|
||||
size={40}
|
||||
/>
|
||||
</UniLink>
|
||||
</CharacterFloatCard>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function EventsPage() {
|
||||
const date = useDate()
|
||||
const { t } = useTranslation("dashboard")
|
||||
const pages = useGetPagesBySite({
|
||||
type: "post",
|
||||
characterId: 50153,
|
||||
limit: 100,
|
||||
})
|
||||
|
||||
const [latestEventRead, setLatestEventRead] = useState<Date>()
|
||||
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setIsMounted(true)
|
||||
setLatestEventRead(new Date(getStorage("latestEventRead")?.value || 0))
|
||||
setStorage("latestEventRead", {
|
||||
value: new Date().toISOString(),
|
||||
})
|
||||
}, [])
|
||||
|
||||
pages.data?.pages[0]?.list.forEach((item) => {
|
||||
item.metadata?.content?.frontMatter?.Winners
|
||||
})
|
||||
|
||||
return (
|
||||
<DashboardMain title="Events">
|
||||
<div className="min-w-[270px] max-w-screen-xl flex flex-col xl:flex-row space-y-8 xl:space-y-0">
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2">
|
||||
{pages.data?.pages[0]?.list.map((item) => {
|
||||
let status
|
||||
if (item.metadata?.content?.frontMatter?.EndTime < new Date()) {
|
||||
status = "Ended"
|
||||
} else if (
|
||||
item.metadata?.content?.frontMatter?.StartTime > new Date()
|
||||
) {
|
||||
status = "Upcoming"
|
||||
} else {
|
||||
status = "Ongoing"
|
||||
}
|
||||
|
||||
let isUnread = false
|
||||
if (latestEventRead && new Date(item.createdAt) > latestEventRead) {
|
||||
isUnread = true
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={cn("bg-slate-100 rounded-lg relative", {
|
||||
"opacity-70": status === "Ended",
|
||||
})}
|
||||
key={item.transactionHash}
|
||||
>
|
||||
{isUnread && (
|
||||
<div>
|
||||
<span className="absolute -left-2 -top-2 bg-red-500 rounded-full w-4 h-4"></span>
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-col p-7 justify-between h-full">
|
||||
<div>
|
||||
<div className="font-bold mb-4">
|
||||
<span
|
||||
className={cn(
|
||||
"border py-1 px-4 rounded-full text-white",
|
||||
{
|
||||
"bg-gray-500": status === "Ended",
|
||||
"bg-green-500": status === "Upcoming",
|
||||
"bg-yellow-500": status === "Ongoing",
|
||||
},
|
||||
)}
|
||||
>
|
||||
{t(status)}
|
||||
</span>
|
||||
</div>
|
||||
{item.metadata?.content?.cover && (
|
||||
<div className="w-full h-24 mb-4">
|
||||
<Image
|
||||
className="object-cover rounded"
|
||||
alt="cover"
|
||||
fill={true}
|
||||
src={item.metadata?.content?.cover}
|
||||
></Image>
|
||||
</div>
|
||||
)}
|
||||
<div className="font-bold text-xl text-zinc-800 leading-tight">
|
||||
{item.metadata?.content?.title}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 space-y-4 text-sm">
|
||||
<div>
|
||||
<span className="font-bold">{t("Date")}:</span>{" "}
|
||||
{date.formatDate(
|
||||
item.metadata?.content?.frontMatter?.StartTime,
|
||||
"lll",
|
||||
isMounted ? undefined : "America/Los_Angeles",
|
||||
)}{" "}
|
||||
-{" "}
|
||||
{date.formatDate(
|
||||
item.metadata?.content?.frontMatter?.EndTime,
|
||||
"lll",
|
||||
isMounted ? undefined : "America/Los_Angeles",
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold">{t("Prize")}:</span>{" "}
|
||||
{item.metadata?.content?.frontMatter?.Prize}
|
||||
</div>
|
||||
{item.metadata?.content?.frontMatter?.Winners?.map && (
|
||||
<div>
|
||||
<span className="font-bold">{t("Winners")}:</span>
|
||||
<div className="flex items-center space-x-2 mt-2">
|
||||
{item.metadata?.content?.frontMatter?.Winners?.map?.(
|
||||
(winner: string) => (
|
||||
<SiteAvatar key={winner} siteId={winner} />
|
||||
),
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<UniLink
|
||||
className="mt-6 font-bold flex items-center leading-none"
|
||||
href={
|
||||
item.metadata?.content?.frontMatter?.ExtraLink ||
|
||||
`/api/redirection?characterId=${item.characterId}¬eId=${item.noteId}`
|
||||
}
|
||||
>
|
||||
{t("Learn more")}{" "}
|
||||
<i className="icon-[mingcute--right-line] text-xl ml-1" />
|
||||
</UniLink>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</DashboardMain>
|
||||
)
|
||||
}
|
||||
|
||||
EventsPage.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Events">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -1,70 +0,0 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
import type { ReactElement } from "react"
|
||||
|
||||
import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
|
||||
import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
|
||||
import { DashboardMain } from "~/components/dashboard/DashboardMain"
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
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 ImportPage() {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
const options = [
|
||||
{
|
||||
name: "Markdown files",
|
||||
path: "/markdown",
|
||||
icon: "markdown.svg",
|
||||
},
|
||||
{
|
||||
name: "Mirror.xyz",
|
||||
path: "/mirror",
|
||||
icon: "mirror.xyz.svg",
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<DashboardMain title="Import">
|
||||
<div className="min-w-[270px] max-w-screen-lg flex flex-col space-y-4">
|
||||
{options.map((option) => (
|
||||
<UniLink
|
||||
className="prose p-6 bg-slate-100 rounded-lg relative flex items-center"
|
||||
key={option.name}
|
||||
href={router.asPath + option.path}
|
||||
>
|
||||
<span className="w-8 h-8 mr-4">
|
||||
<Image
|
||||
fill
|
||||
src={`/assets/${option.icon}`}
|
||||
alt={option.name}
|
||||
></Image>
|
||||
</span>
|
||||
<span className="font-medium">
|
||||
{t(`Import from ${option.name}`)}
|
||||
</span>
|
||||
</UniLink>
|
||||
))}
|
||||
</div>
|
||||
</DashboardMain>
|
||||
)
|
||||
}
|
||||
|
||||
ImportPage.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Import">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import type { NoteMetadata } from "crossbell.js"
|
||||
import { GetServerSideProps } from "next"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
import { ReactElement, useEffect, useState } from "react"
|
||||
import { 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 { DashboardMain } from "~/components/dashboard/DashboardMain"
|
||||
import { ImportPreview } from "~/components/dashboard/ImportPreview"
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { Input } from "~/components/ui/Input"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { readFiles } from "~/lib/read-files"
|
||||
import { serverSidePropsHandler } from "~/lib/server-side-props"
|
||||
import { usePostNotes } from "~/queries/page"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
||||
async (ctx) => {
|
||||
const { props: layoutProps } = await getLayoutServerSideProps(ctx)
|
||||
|
||||
return {
|
||||
props: {
|
||||
...layoutProps,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default function ImportMarkdownPage() {
|
||||
const router = useRouter()
|
||||
const subdomain = router.query.subdomain as string
|
||||
const site = useGetSite(subdomain)
|
||||
const { t } = useTranslation("dashboard")
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
type: "post",
|
||||
files: [],
|
||||
},
|
||||
})
|
||||
const postNotes = usePostNotes()
|
||||
|
||||
const [notes, setNotes] = useState<NoteMetadata[]>()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
if (notes?.length && site.data?.handle && site.data.characterId) {
|
||||
postNotes.mutate({
|
||||
siteId: site.data.handle,
|
||||
characterId: site.data.characterId,
|
||||
notes,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
form.register("files", {
|
||||
onChange: async (e) => {
|
||||
const files = await readFiles(e.target.files)
|
||||
const notes = files.map((file) => {
|
||||
return {
|
||||
title: file.title,
|
||||
content: file.content,
|
||||
tags: ["post", ...file.tags],
|
||||
sources: ["xlog"],
|
||||
attributes: [
|
||||
{
|
||||
trait_type: "xlog_slug",
|
||||
value: file.slug,
|
||||
},
|
||||
],
|
||||
date_published: file.date_published,
|
||||
external_urls: [
|
||||
`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(file.slug)}`,
|
||||
],
|
||||
}
|
||||
})
|
||||
setNotes(notes)
|
||||
},
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (postNotes.isSuccess) {
|
||||
form.reset()
|
||||
toast.success("Notes imported successfully")
|
||||
}
|
||||
}, [postNotes.isSuccess, form])
|
||||
|
||||
return (
|
||||
<DashboardMain title="Import from Markdown files">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="min-w-[270px] max-w-screen-lg flex flex-col space-y-4">
|
||||
<Input
|
||||
className="py-1"
|
||||
label={t(`Select Markdown Files`) || ""}
|
||||
id="notes"
|
||||
type="file"
|
||||
accept=".md"
|
||||
multiple={true}
|
||||
help={t(
|
||||
"Please select .md files, multiple files are supported, and front matter is supported.",
|
||||
)}
|
||||
{...form.register("files", {})}
|
||||
/>
|
||||
<div>
|
||||
<div className="form-label">{t("Preview")}</div>
|
||||
{notes?.length ? (
|
||||
notes?.map((note) => (
|
||||
<ImportPreview key={note.title} note={note} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-gray-500">{t("No files chosen")}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
isAutoWidth={true}
|
||||
type="submit"
|
||||
isLoading={site.isLoading || postNotes.isLoading}
|
||||
disabled={!notes?.length}
|
||||
>
|
||||
{t("Import")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</DashboardMain>
|
||||
)
|
||||
}
|
||||
|
||||
ImportMarkdownPage.getLayout = (page: ReactElement) => {
|
||||
return (
|
||||
<DashboardLayout title="Import from Markdown files">{page}</DashboardLayout>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,131 +0,0 @@
|
|||
import type { NoteMetadata } from "crossbell.js"
|
||||
import { GetServerSideProps } from "next"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
import { ReactElement, useEffect } from "react"
|
||||
import { 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 { DashboardMain } from "~/components/dashboard/DashboardMain"
|
||||
import { ImportPreview } from "~/components/dashboard/ImportPreview"
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { serverSidePropsHandler } from "~/lib/server-side-props"
|
||||
import { useCheckMirror, useGetMirrorXyz, usePostNotes } from "~/queries/page"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
||||
async (ctx) => {
|
||||
const { props: layoutProps } = await getLayoutServerSideProps(ctx)
|
||||
|
||||
return {
|
||||
props: {
|
||||
...layoutProps,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default function ImportMarkdownPage() {
|
||||
const router = useRouter()
|
||||
const subdomain = router.query.subdomain as string
|
||||
const site = useGetSite(subdomain)
|
||||
const { t } = useTranslation("dashboard")
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
type: "post",
|
||||
},
|
||||
})
|
||||
const postNotes = usePostNotes()
|
||||
const mirrorXyz = useGetMirrorXyz({
|
||||
address: site.data?.owner,
|
||||
})
|
||||
const checkMirror = useCheckMirror(site.data?.characterId)
|
||||
|
||||
const notes = mirrorXyz?.data?.map((note) => ({
|
||||
title: note.title,
|
||||
content: note.content,
|
||||
tags: ["post", ...note.tags],
|
||||
sources: ["xlog"],
|
||||
attributes: [
|
||||
{
|
||||
trait_type: "xlog_slug",
|
||||
value: note.slug,
|
||||
},
|
||||
],
|
||||
date_published: note.date_published,
|
||||
external_urls: [
|
||||
`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(note.slug)}`,
|
||||
...note.external_urls,
|
||||
],
|
||||
}))
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
if (notes?.length && site.data?.handle && site.data.characterId) {
|
||||
postNotes.mutate({
|
||||
siteId: site.data.handle,
|
||||
characterId: site.data.characterId,
|
||||
notes: notes,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (postNotes.isSuccess) {
|
||||
form.reset()
|
||||
toast.success("Notes imported successfully")
|
||||
}
|
||||
}, [postNotes.isSuccess, form])
|
||||
|
||||
return (
|
||||
<DashboardMain title="Import from Mirror.xyz">
|
||||
{checkMirror?.data ? (
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="min-w-[270px] max-w-screen-lg flex flex-col space-y-4">
|
||||
<div>
|
||||
<div className="form-label">
|
||||
{t("Preview your Mirror.xyz entries")}
|
||||
</div>
|
||||
{notes?.length ? (
|
||||
notes?.map((note: NoteMetadata) => (
|
||||
<ImportPreview key={note.title} note={note} />
|
||||
))
|
||||
) : (
|
||||
<div className="text-gray-500">{t("No entries")}</div>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
isAutoWidth={true}
|
||||
type="submit"
|
||||
isLoading={
|
||||
site.isLoading || mirrorXyz.isLoading || postNotes.isLoading
|
||||
}
|
||||
disabled={!notes?.length}
|
||||
>
|
||||
{t("Import")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
) : (
|
||||
<div className="text-gray-500">
|
||||
{t(
|
||||
"You have already imported them, please enter the post page to create a new post!",
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</DashboardMain>
|
||||
)
|
||||
}
|
||||
|
||||
ImportMarkdownPage.getLayout = (page: ReactElement) => {
|
||||
return (
|
||||
<DashboardLayout title="Import from Mirror.xyz">{page}</DashboardLayout>
|
||||
)
|
||||
}
|
||||
|
|
@ -1,286 +0,0 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { Trans, useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
import type { ReactElement } from "react"
|
||||
|
||||
import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
|
||||
import { DashboardLayout } from "~/components/dashboard/DashboardLayout"
|
||||
import { getServerSideProps as getLayoutServerSideProps } from "~/components/dashboard/DashboardLayout.server"
|
||||
import { DashboardMain } from "~/components/dashboard/DashboardMain"
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { useDate } from "~/hooks/useDate"
|
||||
import { CSB_SCAN, DISCORD_LINK, GITHUB_LINK, TWITTER_LINK } from "~/lib/env"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { serverSidePropsHandler } from "~/lib/server-side-props"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { useGetShowcase } from "~/queries/home"
|
||||
import { useGetPagesBySite } from "~/queries/page"
|
||||
import { useGetSite, useGetStat, useGetTips } from "~/queries/site"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
||||
async (ctx) => {
|
||||
const { props: layoutProps } = await getLayoutServerSideProps(ctx)
|
||||
|
||||
return {
|
||||
props: {
|
||||
...layoutProps,
|
||||
},
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
export default function SubdomainIndex() {
|
||||
const router = useRouter()
|
||||
const subdomain = router.query.subdomain as string
|
||||
const site = useGetSite(subdomain)
|
||||
const characterId = site.data?.characterId
|
||||
const stat = useGetStat({
|
||||
characterId,
|
||||
})
|
||||
const date = useDate()
|
||||
const { t } = useTranslation("dashboard")
|
||||
const tips = useGetTips({
|
||||
toCharacterId: characterId,
|
||||
limit: 1000,
|
||||
})
|
||||
|
||||
const statMap = [
|
||||
{
|
||||
icon: "icon-[mingcute--news-line]",
|
||||
name: "Published posts",
|
||||
value: stat.data?.notesCount,
|
||||
url: `/dashboard/${subdomain}/posts`,
|
||||
},
|
||||
{
|
||||
icon: "icon-[mingcute--comment-line]",
|
||||
name: "Received comments",
|
||||
value: stat.data?.commentsCount,
|
||||
url: `/dashboard/${subdomain}/comments`,
|
||||
},
|
||||
{
|
||||
icon: "icon-[mingcute--heart-line]",
|
||||
name: "Received tips",
|
||||
value: `${
|
||||
tips.data?.pages?.[0]?.list
|
||||
?.map((i) => +i.amount)
|
||||
.reduce((acr, cur) => acr + cur, 0) ?? 0
|
||||
} MIRA`,
|
||||
url: `/dashboard/${subdomain}/tokens`,
|
||||
},
|
||||
{
|
||||
icon: "icon-[mingcute--user-follow-line]",
|
||||
name: "Followers",
|
||||
value: stat.data?.subscriptionCount,
|
||||
url: getSiteLink({
|
||||
subdomain,
|
||||
}),
|
||||
},
|
||||
{
|
||||
icon: "icon-[mingcute--eye-line]",
|
||||
name: "Viewed",
|
||||
value: stat.data?.viewsCount,
|
||||
url: getSiteLink({
|
||||
subdomain,
|
||||
}),
|
||||
},
|
||||
{
|
||||
icon: "icon-[mingcute--history-line]",
|
||||
name: "Site Duration",
|
||||
value:
|
||||
date.dayjs().diff(date.dayjs(stat.data?.createdAt), "day") +
|
||||
" " +
|
||||
t("days"),
|
||||
url: `${CSB_SCAN}/tx/${stat.data?.createTx}`,
|
||||
},
|
||||
]
|
||||
|
||||
const pages = useGetPagesBySite({
|
||||
type: "post",
|
||||
characterId: 32022,
|
||||
limit: 4,
|
||||
})
|
||||
|
||||
const showcaseSites = useGetShowcase()
|
||||
|
||||
return (
|
||||
<DashboardMain title="Dashboard" className="max-w-screen-2xl">
|
||||
<div className="min-w-[270px] flex flex-col xl:flex-row space-y-8 xl:space-y-0">
|
||||
<div className="flex-1 space-y-8">
|
||||
<div className="grid gap-4 sm:grid-cols-3 grid-cols-2">
|
||||
{statMap.map((item) => (
|
||||
<UniLink
|
||||
href={item.url}
|
||||
key={item.name}
|
||||
className="bg-slate-100 rounded-lg flex justify-center flex-col py-4 px-6"
|
||||
>
|
||||
<span>
|
||||
<i
|
||||
className={cn(
|
||||
item.icon,
|
||||
"inline-block mr-1 text-lg align-middle",
|
||||
)}
|
||||
/>
|
||||
<span className="align-middle">{t(item.name)}</span>
|
||||
</span>
|
||||
<span className="font-bold text-2xl">{item.value}</span>
|
||||
</UniLink>
|
||||
))}
|
||||
</div>
|
||||
<div className="prose p-6 bg-slate-50 rounded-lg relative">
|
||||
<Trans
|
||||
i18nKey="hello.welcome"
|
||||
defaults="<p>👋 Hello there,</p><p>Welcome to use xLog!</p><p>Here are some useful links to get started:</p>"
|
||||
components={{
|
||||
p: <p />,
|
||||
}}
|
||||
ns="dashboard"
|
||||
/>
|
||||
<ul>
|
||||
<li>
|
||||
<UniLink
|
||||
href={getSiteLink({
|
||||
subdomain,
|
||||
})}
|
||||
>
|
||||
{t("View Site")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
<UniLink href={`/dashboard/${subdomain}/editor?type=post`}>
|
||||
{t("Create a Post")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
<UniLink href={`/dashboard/${subdomain}/settings/general`}>
|
||||
{t("Change Site Icon or domain")}
|
||||
</UniLink>
|
||||
</li>
|
||||
</ul>
|
||||
<Trans
|
||||
i18nKey="hello.community"
|
||||
defaults="<p>Join the community to meet friends or build xLog together:</p>"
|
||||
components={{
|
||||
p: <p />,
|
||||
}}
|
||||
ns="dashboard"
|
||||
/>
|
||||
<ul
|
||||
style={{
|
||||
marginBottom: 0,
|
||||
}}
|
||||
>
|
||||
<li>
|
||||
<UniLink href="/activities" target="_blank">
|
||||
{t("Check out the updates of other bloggers")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
<UniLink href={DISCORD_LINK}>
|
||||
{t("Join xLog's Discord channel")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
<UniLink href={GITHUB_LINK}>
|
||||
{t("Participate in the development of xLog")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
<UniLink href={TWITTER_LINK}>
|
||||
{t("Follow xLog's Twitter")}
|
||||
</UniLink>
|
||||
</li>
|
||||
</ul>
|
||||
<div></div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="w-full xl:w-[500px] xl:ml-10 space-y-7">
|
||||
<div className="p-6 bg-slate-50 rounded-lg relative">
|
||||
<UniLink
|
||||
href={getSiteLink({ subdomain: "xlog" })}
|
||||
className="absolute right-0 left-0 top-0 h-16 cursor-pointer"
|
||||
>
|
||||
<div className="absolute right-5 top-5 text-sm font-bold">
|
||||
{t("More")}
|
||||
</div>
|
||||
</UniLink>
|
||||
<h4 className="text-xl font-bold mb-4 leading-none">
|
||||
{t("xLog News")}
|
||||
</h4>
|
||||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2">
|
||||
{pages.data?.pages[0]?.list.map((item) => (
|
||||
<UniLink
|
||||
href={`${getSiteLink({ subdomain: "xlog" })}/${
|
||||
item.metadata?.content?.slug
|
||||
}`}
|
||||
key={item.transactionHash}
|
||||
className="bg-slate-100 rounded-lg flex flex-col py-4 px-6"
|
||||
>
|
||||
{item.metadata?.content?.cover && (
|
||||
<div className="w-full h-24">
|
||||
<Image
|
||||
className="object-cover rounded"
|
||||
alt="cover"
|
||||
fill={true}
|
||||
src={item.metadata?.content?.cover}
|
||||
></Image>
|
||||
</div>
|
||||
)}
|
||||
<span className="font-bold text-sm text-zinc-800 leading-tight mt-4">
|
||||
{item.metadata?.content?.title}
|
||||
</span>
|
||||
</UniLink>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-6 bg-slate-50 rounded-lg relative">
|
||||
<h4 className="text-xl font-bold mb-4 leading-none">
|
||||
{t("Meet New Friends")}
|
||||
</h4>
|
||||
<ul className="pt-2 grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6 relative">
|
||||
{showcaseSites.data?.slice(0, 6)?.map((site) => (
|
||||
<li className="inline-flex align-middle" key={site.handle}>
|
||||
<UniLink
|
||||
href={getSiteLink({
|
||||
subdomain: site.handle,
|
||||
})}
|
||||
className="inline-flex align-middle w-full"
|
||||
>
|
||||
<CharacterFloatCard siteId={site.handle}>
|
||||
<span className="w-14 h-14 inline-block">
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src={
|
||||
site.metadata.content?.avatars?.[0] ||
|
||||
"ipfs://bafkreiabgixxp63pg64moxnsydz7hewmpdkxxi3kdsa4oqv4pb6qvwnmxa"
|
||||
}
|
||||
alt={site.handle}
|
||||
width="56"
|
||||
height="56"
|
||||
></Image>
|
||||
</span>
|
||||
</CharacterFloatCard>
|
||||
<span className="ml-3 min-w-0 flex-1 justify-center inline-flex flex-col">
|
||||
<span className="truncate w-full inline-block font-medium">
|
||||
{site.metadata.content?.name}
|
||||
</span>
|
||||
{site.metadata.content?.bio && (
|
||||
<span className="text-gray-500 text-xs truncate w-full inline-block mt-1">
|
||||
{site.metadata.content?.bio}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</UniLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</DashboardMain>
|
||||
)
|
||||
}
|
||||
|
||||
SubdomainIndex.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Dashboard">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -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 <PagesManager isPost={false} />
|
||||
}
|
||||
|
||||
SubdomainPages.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Pages">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -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 <PagesManager isPost={true} />
|
||||
}
|
||||
|
||||
SubdomainPosts.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Posts">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -1,138 +0,0 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
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 { 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 updateSite = useUpdateSite()
|
||||
const site = useGetSite(subdomain)
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
const [css, setCss] = useState("")
|
||||
const handleSubmit = (e: any) => {
|
||||
e.preventDefault()
|
||||
updateSite.mutate({
|
||||
site: subdomain,
|
||||
css: css,
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateSite.isSuccess) {
|
||||
if (updateSite.data?.code === 0) {
|
||||
toast.success("Saved!")
|
||||
} else {
|
||||
toast.error("Failed to update site" + ": " + updateSite.data.message)
|
||||
}
|
||||
} else if (updateSite.isError) {
|
||||
toast.error("Failed to update site")
|
||||
}
|
||||
}, [updateSite.isSuccess, updateSite.isError])
|
||||
|
||||
const [hasSet, setHasSet] = useState(false)
|
||||
useEffect(() => {
|
||||
if (site.isSuccess && site.data && !css && !hasSet) {
|
||||
setCss(site.data.metadata?.content?.css || "")
|
||||
setHasSet(true)
|
||||
}
|
||||
}, [site.data, site.isSuccess, css, hasSet])
|
||||
|
||||
return (
|
||||
<SettingsLayout title={"Site Settings"}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="">
|
||||
<div className="p-5 text-zinc-500 bg-zinc-50 mb-5 rounded-lg text-xs space-y-2">
|
||||
<p className="text-zinc-800 text-sm font-bold">{t("Tips")}:</p>
|
||||
<p>
|
||||
{t(
|
||||
"Scope: These styles will be applied to your entire blog, including this dashboard.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t(
|
||||
"Using a browser plugin that can modify page styles, such as Stylebot, can help with debugging.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t("Support")} <code>ipfs://</code>
|
||||
</p>
|
||||
<p>
|
||||
{t("CSS variables: xLog provides some built-in CSS variables")}
|
||||
</p>
|
||||
<p className="pl-2">
|
||||
<span className="bg-zinc-200 rounded-lg px-2">
|
||||
--theme-color: #4f46e5;
|
||||
</span>
|
||||
</p>
|
||||
<p className="pl-2">
|
||||
<span className="bg-zinc-200 rounded-lg px-2">
|
||||
--header-height: auto;
|
||||
</span>
|
||||
</p>
|
||||
<p className="pl-2">
|
||||
<span className="bg-zinc-200 rounded-lg px-2">
|
||||
--banner-bg-color: #000;
|
||||
</span>
|
||||
</p>
|
||||
<p className="pl-2">
|
||||
<span className="bg-zinc-200 rounded-lg px-2">
|
||||
--font-fans: ui-sans-serif, system-ui, -apple-system,
|
||||
BlinkMacSystemFont, "Segoe UI", Roboto,
|
||||
"Helvetica Neue", Arial, "Noto Sans",
|
||||
sans-serif, "Apple Color Emoji", "Segoe UI
|
||||
Emoji", "Segoe UI Symbol", "Noto Color
|
||||
Emoji";
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<FieldLabel label={t("Custom CSS")} />
|
||||
<MonacoEditor
|
||||
className="w-full h-96 border outline-none py-3 rounded-lg inline-flex items-center overflow-hidden"
|
||||
defaultLanguage="css"
|
||||
defaultValue={css}
|
||||
onChange={(value) => setCss(value || "")}
|
||||
options={{
|
||||
fontSize: 14,
|
||||
minimap: { enabled: false },
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<Button type="submit" isLoading={updateSite.isLoading}>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
SettingsCSSPage.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Site Settings">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -1,318 +0,0 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useEffect, useState } from "react"
|
||||
import type { ReactElement } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
import toast from "react-hot-toast"
|
||||
|
||||
import {
|
||||
useAccountState,
|
||||
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"
|
||||
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 { 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 updateSite = useUpdateSite()
|
||||
const site = useGetSite(subdomain)
|
||||
const userRole = useUserRole(subdomain)
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
const isEmailAccount = useAccountState(
|
||||
(s) => s.computed.account?.type === "email",
|
||||
)
|
||||
const upgradeAccountModal = useUpgradeEmailAccountModal()
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
subdomain: "",
|
||||
custom_domain: "",
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit((values) => {
|
||||
updateSite.mutate({
|
||||
site: subdomain,
|
||||
...(subdomain !== values.subdomain && { subdomain: values.subdomain }),
|
||||
...(site.data?.metadata?.content?.custom_domain !==
|
||||
values.custom_domain && {
|
||||
custom_domain: values.custom_domain,
|
||||
}),
|
||||
})
|
||||
})
|
||||
|
||||
const [customDomain, setCustomDomain] = useState("")
|
||||
form.register("custom_domain", {
|
||||
onChange: (e) => {
|
||||
setCustomDomain(e.target.value)
|
||||
toCheckDomain(e.target.value)
|
||||
},
|
||||
})
|
||||
|
||||
const customSubdomain = customDomain?.split(".").slice(0, -2).join(".") || ""
|
||||
|
||||
useEffect(() => {
|
||||
if (updateSite.isSuccess) {
|
||||
if (updateSite.data?.code === 0) {
|
||||
toast.success("Saved!")
|
||||
router.replace(
|
||||
`/dashboard/${
|
||||
updateSite.variables?.subdomain || updateSite.variables?.site
|
||||
}/settings/domains`,
|
||||
)
|
||||
} else {
|
||||
toast.error("Failed to update site" + ": " + updateSite.data.message)
|
||||
}
|
||||
} else if (updateSite.isError) {
|
||||
toast.error("Failed to update site")
|
||||
}
|
||||
}, [updateSite.isSuccess])
|
||||
|
||||
const [domainCheckResult, setDomainCheckResult] = useState<{
|
||||
isLoading: boolean
|
||||
data?: boolean
|
||||
}>({
|
||||
isLoading: false,
|
||||
data: true,
|
||||
})
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const toCheckDomain = (custom: string) => {
|
||||
if (custom) {
|
||||
setDomainCheckResult({
|
||||
isLoading: true,
|
||||
})
|
||||
checkDomain(custom, form.getValues().subdomain).then((r) =>
|
||||
setDomainCheckResult({
|
||||
isLoading: false,
|
||||
data: r,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const [subdomainCheckResult, setSubdomainCheckResult] = useState<{
|
||||
isLoading: boolean
|
||||
data?: boolean
|
||||
}>({
|
||||
isLoading: false,
|
||||
data: true,
|
||||
})
|
||||
const toCheckSubdomain = (subdomain: string) => {
|
||||
if (subdomain) {
|
||||
setSubdomainCheckResult({
|
||||
isLoading: true,
|
||||
})
|
||||
const subdomainRegex = /^[a-z0-9]([a-z0-9\-]{0,61}[a-z0-9])?$/i
|
||||
if (!subdomainRegex.test(subdomain)) {
|
||||
setSubdomainCheckResult({
|
||||
isLoading: false,
|
||||
data: false,
|
||||
})
|
||||
} else {
|
||||
getSite(subdomain).then((r) =>
|
||||
setSubdomainCheckResult({
|
||||
isLoading: false,
|
||||
data: !r,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [subdomainChanged, setSubdomainChanged] = useState(false)
|
||||
form.register("subdomain", {
|
||||
onChange: (e) => {
|
||||
if (e.target.value !== site.data?.handle) {
|
||||
toCheckSubdomain(e.target.value)
|
||||
setSubdomainChanged(true)
|
||||
} else {
|
||||
setSubdomainChanged(false)
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
const [hasSet, setHasSet] = useState(false)
|
||||
useEffect(() => {
|
||||
if (site.isSuccess && site.data && !hasSet) {
|
||||
setHasSet(true)
|
||||
form.setValue("subdomain", site.data.handle || "")
|
||||
form.setValue(
|
||||
"custom_domain",
|
||||
site.data.metadata?.content?.custom_domain || "",
|
||||
)
|
||||
setCustomDomain(site.data.metadata?.content?.custom_domain || "")
|
||||
toCheckDomain(site.data.metadata?.content?.custom_domain || "")
|
||||
}
|
||||
}, [form, site.data, site.isSuccess, customDomain, hasSet, toCheckDomain])
|
||||
|
||||
return (
|
||||
<SettingsLayout title={"Site Settings"}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<Input
|
||||
id="subdomain"
|
||||
label={`xLog ${t("subdomain")}`}
|
||||
addon={`.${OUR_DOMAIN}`}
|
||||
className="w-28"
|
||||
{...form.register("subdomain")}
|
||||
disabled={isEmailAccount || userRole?.data === "operator"}
|
||||
/>
|
||||
{subdomainChanged && (
|
||||
<div className="text-sm mt-2">
|
||||
{subdomainCheckResult.isLoading ? (
|
||||
<span>{t("Subdomain Checking")}...</span>
|
||||
) : (
|
||||
<span
|
||||
className={
|
||||
subdomainCheckResult.data
|
||||
? "text-green-600"
|
||||
: "text-red-600"
|
||||
}
|
||||
>
|
||||
{t(
|
||||
subdomainCheckResult.data
|
||||
? "Subdomain Available."
|
||||
: "Subdomain Unavailable.",
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{isEmailAccount && (
|
||||
<div className="text-sm text-orange-400 mt-1">
|
||||
Email users cannot change subdomain/handle.{" "}
|
||||
<UniLink
|
||||
className="underline"
|
||||
href={
|
||||
getSiteLink({
|
||||
subdomain: "crossbell-blog",
|
||||
}) + "/newbie-villa"
|
||||
}
|
||||
>
|
||||
Learn more
|
||||
</UniLink>{" "}
|
||||
or{" "}
|
||||
<span
|
||||
className="underline cursor-pointer"
|
||||
onClick={upgradeAccountModal.show}
|
||||
>
|
||||
upgrade account
|
||||
</span>
|
||||
.
|
||||
</div>
|
||||
)}
|
||||
{userRole.data === "operator" && (
|
||||
<div className="text-sm text-orange-400 mt-1">
|
||||
Operators cannot change subdomain/handle. Please contact the site
|
||||
owner.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<Input
|
||||
id="custom_domain"
|
||||
label={t("Custom Domain") || ""}
|
||||
className="w-64"
|
||||
{...form.register("custom_domain")}
|
||||
/>
|
||||
{customDomain && (
|
||||
<div className="mt-2 text-xs space-y-2">
|
||||
<p>
|
||||
{t(
|
||||
"Set the following record on your DNS provider to active your custom domain",
|
||||
)}
|
||||
:
|
||||
</p>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr className="border-b">
|
||||
<th className="text-center p-3">Type</th>
|
||||
<th className="text-center p-3">Name</th>
|
||||
<th className="text-center p-3">Value</th>
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
<td className="text-center p-3">CNAME</td>
|
||||
<td className="text-center p-3">
|
||||
{customSubdomain || "@"}
|
||||
</td>
|
||||
<td className="text-center p-3">cname.{OUR_DOMAIN}</td>
|
||||
</tr>
|
||||
<tr className="border-b">
|
||||
<td className="text-center p-3">TXT</td>
|
||||
<td className="text-center p-3">{`_xlog-challenge${
|
||||
customSubdomain ? `.${customSubdomain}` : customSubdomain
|
||||
}`}</td>
|
||||
<td className="text-center p-3">{subdomain}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div className="text-sm">
|
||||
{domainCheckResult.isLoading ? (
|
||||
<span>{t("DNS Checking")}...</span>
|
||||
) : domainCheckResult.data ? (
|
||||
<span className="text-green-600">
|
||||
{t("DNS check passed.")}
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<span className="text-red-600">
|
||||
{t("DNS check failed.")}
|
||||
</span>
|
||||
<Button
|
||||
className="ml-4 font-media"
|
||||
variant="secondary"
|
||||
onClick={() => toCheckDomain(customDomain)}
|
||||
>
|
||||
{t("Recheck")}
|
||||
</Button>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={updateSite.isLoading || domainCheckResult.isLoading}
|
||||
isDisabled={domainCheckResult?.data === false && !!customDomain}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
SettingsDomainsPage.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Site Settings">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -1,265 +0,0 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { Trans, useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
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 { 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 updateSite = useUpdateSite()
|
||||
const site = useGetSite(subdomain)
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
icon: "",
|
||||
banner: undefined,
|
||||
name: "",
|
||||
description: "",
|
||||
ga: "",
|
||||
ua: "",
|
||||
} as {
|
||||
icon: string
|
||||
banner?: {
|
||||
address: string
|
||||
mime_type: string
|
||||
}
|
||||
name: string
|
||||
description: string
|
||||
ga: string
|
||||
ua: string
|
||||
},
|
||||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit((values) => {
|
||||
updateSite.mutate({
|
||||
icon: values.icon,
|
||||
banner: values.banner,
|
||||
site: subdomain,
|
||||
name: values.name,
|
||||
description: values.description,
|
||||
ga: values.ga,
|
||||
ua: values.ua,
|
||||
})
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (updateSite.isSuccess) {
|
||||
if (updateSite.data?.code === 0) {
|
||||
toast.success("Site updated")
|
||||
} else {
|
||||
toast.error("Failed to update site" + ": " + updateSite.data.message)
|
||||
}
|
||||
} else if (updateSite.isError) {
|
||||
toast.error("Failed to update site")
|
||||
}
|
||||
}, [updateSite.isSuccess, updateSite.isError])
|
||||
|
||||
useEffect(() => {
|
||||
if (site.data) {
|
||||
!form.getValues("icon") &&
|
||||
form.setValue(
|
||||
"icon",
|
||||
toIPFS(site.data?.metadata?.content?.avatars?.[0] || ""),
|
||||
)
|
||||
!form.getValues("banner") &&
|
||||
form.setValue(
|
||||
"banner",
|
||||
site.data?.metadata?.content?.banners?.[0]
|
||||
? {
|
||||
address: toIPFS(
|
||||
site.data?.metadata?.content?.banners?.[0].address || "",
|
||||
),
|
||||
mime_type: site.data?.metadata?.content?.banners?.[0].mime_type,
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
!form.getValues("name") &&
|
||||
form.setValue("name", site.data.metadata?.content?.name || "")
|
||||
!form.getValues("description") &&
|
||||
form.setValue("description", site.data.metadata?.content?.bio || "")
|
||||
!form.getValues("ga") &&
|
||||
form.setValue("ga", site.data.metadata?.content?.ga || "")
|
||||
!form.getValues("ua") &&
|
||||
form.setValue("ua", site.data.metadata?.content?.ua || "")
|
||||
}
|
||||
}, [site.data, form])
|
||||
|
||||
const [iconUploading, setIconUploading] = useState(false)
|
||||
const [bannerUploading, setBannerUploading] = useState(false)
|
||||
|
||||
return (
|
||||
<SettingsLayout title="Site Settings">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mt-5">
|
||||
<label htmlFor="icon" className="form-label">
|
||||
{t("Icon")}
|
||||
</label>
|
||||
<Controller
|
||||
name="icon"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<ImageUploader
|
||||
id="icon"
|
||||
className="w-24 h-24 rounded-full"
|
||||
uploadStart={() => {
|
||||
setIconUploading(true)
|
||||
}}
|
||||
uploadEnd={(key) => {
|
||||
form.setValue("icon", key as string)
|
||||
setIconUploading(false)
|
||||
}}
|
||||
{...field}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<label htmlFor="icon" className="form-label">
|
||||
{t("Banner")}
|
||||
</label>
|
||||
<Controller
|
||||
name="banner"
|
||||
control={form.control}
|
||||
render={({ field }) => (
|
||||
<ImageUploader
|
||||
id="banner"
|
||||
className="max-w-screen-md h-[220px]"
|
||||
uploadStart={() => {
|
||||
setBannerUploading(true)
|
||||
}}
|
||||
uploadEnd={(key) => {
|
||||
form.setValue(
|
||||
"banner",
|
||||
key as { address: string; mime_type: string },
|
||||
)
|
||||
setBannerUploading(false)
|
||||
}}
|
||||
withMimeType={true}
|
||||
hasClose={true}
|
||||
{...(field as any)}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<div className="text-xs text-gray-400 mt-1">
|
||||
{t("Supports both pictures and videos.")}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<Input
|
||||
required
|
||||
label={t("Name") || ""}
|
||||
id="name"
|
||||
{...form.register("name")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<label htmlFor="description" className="form-label">
|
||||
{t("Description")}
|
||||
</label>
|
||||
<Input
|
||||
multiline
|
||||
id="description"
|
||||
className="input is-block"
|
||||
rows={2}
|
||||
{...form.register("description")}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<Input
|
||||
id="ga"
|
||||
{...form.register("ga")}
|
||||
prefix="G-"
|
||||
label="Google Analytics"
|
||||
help={
|
||||
<p>
|
||||
<Trans i18nKey="Integrate Google Analytics" ns="dashboard">
|
||||
Integrate Google Analytics into your site. You can follow the
|
||||
instructions{" "}
|
||||
<UniLink
|
||||
className="underline"
|
||||
href="https://support.google.com/analytics/answer/9539598"
|
||||
>
|
||||
here
|
||||
</UniLink>{" "}
|
||||
to find your Measurement ID.
|
||||
</Trans>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<Input
|
||||
id="ua"
|
||||
{...form.register("ua")}
|
||||
label="Umami Cloud Analytics"
|
||||
help={
|
||||
<p>
|
||||
<Trans i18nKey="Integrate Umami Cloud Analytics" ns="dashboard">
|
||||
Integrate Umami Cloud Analytics into your site. You can follow
|
||||
the instructions{" "}
|
||||
<UniLink
|
||||
className="underline"
|
||||
href="https://umami.is/docs/collect-data"
|
||||
>
|
||||
here
|
||||
</UniLink>{" "}
|
||||
to find your Website ID.
|
||||
</Trans>
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={updateSite.isLoading}
|
||||
isDisabled={iconUploading || bannerUploading}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
{/* <div className="mt-14 border-t pt-8">
|
||||
<h3 className="text-red-500 text-lg mb-5">Danger Zone</h3>
|
||||
<form>
|
||||
<Button variantColor="red" type="submit">
|
||||
Delete Site
|
||||
</Button>
|
||||
</form>
|
||||
</div> */}
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
SiteSettingsGeneralPage.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Site Settings">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -1,217 +0,0 @@
|
|||
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 { 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 { 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<SiteNavigationItem>) => void
|
||||
|
||||
type RemoveItem = (id: string) => void
|
||||
|
||||
const SortableNavigationItem: React.FC<{
|
||||
item: SiteNavigationItem
|
||||
updateItem: UpdateItem
|
||||
removeItem: RemoveItem
|
||||
}> = ({ item, updateItem, removeItem }) => {
|
||||
const { t } = useTranslation("dashboard")
|
||||
return (
|
||||
<div className="flex space-x-5 border-b p-5 bg-zinc-50 last:border-0">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="drag-handle cursor-grab -mt-1 text-zinc-400 rounded-lg h-8 w-6 flex items-center justify-center hover:text-zinc-800 hover:bg-zinc-200"
|
||||
>
|
||||
<i className="icon-[mingcute--dot-grid-fill]" />
|
||||
</button>
|
||||
</div>
|
||||
<Input
|
||||
label={t("Label") || ""}
|
||||
required
|
||||
id={`${item.id}-label`}
|
||||
value={item.label}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
updateItem(item.id, { label: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
label={t("URL") || ""}
|
||||
required
|
||||
id={`${item.id}-url`}
|
||||
type="text"
|
||||
value={item.url}
|
||||
pattern="(https?://|/)(.*)?"
|
||||
title="URL must start with / or http:// or https://"
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
updateItem(item.id, { url: e.target.value })
|
||||
}
|
||||
/>
|
||||
<div className="flex items-end relative -top-[5px]">
|
||||
<Button onClick={() => removeItem(item.id)} variantColor="red">
|
||||
{t("Remove")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SiteSettingsNavigationPage() {
|
||||
const router = useRouter()
|
||||
|
||||
const subdomain = router.query.subdomain as string
|
||||
|
||||
const updateSite = useUpdateSite()
|
||||
const site = useGetSite(subdomain)
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
const [items, setItems] = useState<SiteNavigationItem[]>([])
|
||||
|
||||
const itemsModified = useMemo(() => {
|
||||
if (!site.isSuccess) return false
|
||||
return !equal(items, site.data?.metadata?.content?.navigation)
|
||||
}, [items, site.data, site.isSuccess])
|
||||
|
||||
const updateItem: UpdateItem = (id, newItem) => {
|
||||
setItems((items) => {
|
||||
return items.map((item) => {
|
||||
if (item.id === id) {
|
||||
return { ...item, ...newItem }
|
||||
}
|
||||
return item
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const newEmptyItem = () => {
|
||||
setItems((items) => [...items, { id: nanoid(), label: "", url: "" }])
|
||||
}
|
||||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
if (site.data?.handle) {
|
||||
updateSite.mutate({
|
||||
site: site.data?.handle,
|
||||
navigation: items,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateSite.isSuccess) {
|
||||
if (updateSite.data?.code === 0) {
|
||||
toast.success("Saved")
|
||||
} else {
|
||||
toast.error("Failed to save" + ": " + updateSite.data.message)
|
||||
}
|
||||
} else if (updateSite.isError) {
|
||||
toast.error("Failed to save")
|
||||
}
|
||||
}, [updateSite.isSuccess, updateSite.isError])
|
||||
|
||||
const removeItem: RemoveItem = (id) => {
|
||||
setItems((items) => items.filter((item) => item.id !== id))
|
||||
}
|
||||
|
||||
const [hasSet, setHasSet] = useState(false)
|
||||
useEffect(() => {
|
||||
if (site.data?.metadata?.content?.navigation && !hasSet) {
|
||||
setHasSet(true)
|
||||
setItems(site.data?.metadata?.content?.navigation)
|
||||
}
|
||||
}, [site.data?.metadata?.content?.navigation, hasSet])
|
||||
|
||||
return (
|
||||
<SettingsLayout title="Site Settings">
|
||||
<div className="p-5 text-zinc-500 bg-zinc-50 mb-5 rounded-lg text-xs space-y-2">
|
||||
<p className="text-zinc-800 text-sm font-bold">{t("Tips")}:</p>
|
||||
<p>
|
||||
<span className="text-zinc-800 font-medium">
|
||||
{t("xLog provides some out-of-the-box built-in pages")}:
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-zinc-800">- {t("Home page")}:</span>{" "}
|
||||
<span className="bg-zinc-200 rounded-lg px-2">/</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-zinc-800">- {t("Archives page")}:</span>{" "}
|
||||
<span className="bg-zinc-200 rounded-lg px-2">/archives</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-zinc-900">- {t("Tag page")}:</span>{" "}
|
||||
<span className="bg-zinc-200 rounded-lg px-2">/tag/[tag]</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-zinc-900">- {t("NFT Showcase page")}:</span>{" "}
|
||||
<span className="bg-zinc-200 rounded-lg px-2">/nft</span>
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="bg-zinc-50 rounded-lg overflow-hidden">
|
||||
{items.length === 0 && (
|
||||
<div className="text-center text-zinc-500 p-5">
|
||||
No navigation items yet
|
||||
</div>
|
||||
)}
|
||||
<ReactSortable list={items} setList={setItems} handle=".drag-handle">
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<SortableNavigationItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
updateItem={updateItem}
|
||||
removeItem={removeItem}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<style jsx global>{`
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
`}</style>
|
||||
</ReactSortable>
|
||||
</div>
|
||||
<div className="border-t pt-5 mt-10 space-x-3 flex items-center">
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={updateSite.isLoading}
|
||||
isDisabled={!itemsModified}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={newEmptyItem}>
|
||||
{t("New Item")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
SiteSettingsNavigationPage.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Site Settings">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -1,281 +0,0 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useEffect, useState } from "react"
|
||||
import type { ReactElement } from "react"
|
||||
import toast from "react-hot-toast"
|
||||
|
||||
import {
|
||||
useAccountState,
|
||||
useUpgradeEmailAccountModal,
|
||||
} from "@crossbell/connect-kit"
|
||||
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 {
|
||||
useAddOperator,
|
||||
useGetOperators,
|
||||
useGetSite,
|
||||
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<{
|
||||
item: string
|
||||
removeItem: RemoveItem
|
||||
isLoading: boolean
|
||||
disabled?: boolean
|
||||
}> = ({ item, removeItem, isLoading, disabled }) => {
|
||||
const { t } = useTranslation("dashboard")
|
||||
return (
|
||||
<div className="flex space-x-5 border-b p-5 bg-zinc-50 last:border-0 items-center">
|
||||
<div className="text-sm space-y-4">
|
||||
<div>
|
||||
{t("Address")}: {item}
|
||||
</div>
|
||||
<div>{t("Character")}:</div>
|
||||
<CharacterCard
|
||||
address={item}
|
||||
open={true}
|
||||
hideFollowButton={true}
|
||||
style="flat"
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-end relative -top-[5px]">
|
||||
<Button
|
||||
onClick={() => removeItem(item)}
|
||||
variantColor="red"
|
||||
isLoading={isLoading}
|
||||
isDisabled={disabled}
|
||||
>
|
||||
{t("Remove")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SettingsOperatorPage() {
|
||||
const router = useRouter()
|
||||
|
||||
const subdomain = router.query.subdomain as string
|
||||
|
||||
const addOperator = useAddOperator()
|
||||
const removeOperator = useRemoveOperator()
|
||||
const site = useGetSite(subdomain)
|
||||
const operators = useGetOperators({
|
||||
characterId: site.data?.characterId,
|
||||
})
|
||||
const isEmailAccount = useAccountState(
|
||||
(s) => s.computed.account?.type === "email",
|
||||
)
|
||||
const upgradeAccountModal = useUpgradeEmailAccountModal()
|
||||
const userRole = useUserRole(subdomain)
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
const [items, setItems] = useState<string[]>([])
|
||||
|
||||
const newEmptyItem = () => {
|
||||
setIsOpen(true)
|
||||
setAddress("")
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (addOperator.isSuccess) {
|
||||
toast.success("Operator added")
|
||||
} else if (addOperator.isError) {
|
||||
toast.error("Failed to add operator")
|
||||
}
|
||||
setIsOpen(false)
|
||||
}, [addOperator.isSuccess, addOperator.isError])
|
||||
|
||||
useEffect(() => {
|
||||
if (removeOperator.isSuccess) {
|
||||
toast.success("Operator removed")
|
||||
} else if (removeOperator.isError) {
|
||||
toast.error("Failed to remove operator")
|
||||
}
|
||||
}, [removeOperator.isSuccess, removeOperator.isError])
|
||||
|
||||
const removeItem: RemoveItem = (operator) => {
|
||||
removeOperator.mutate({
|
||||
characterId: site.data?.characterId,
|
||||
operator: operator,
|
||||
})
|
||||
}
|
||||
const addItem = () => {
|
||||
if (address) {
|
||||
addOperator.mutate({
|
||||
characterId: site.data?.characterId,
|
||||
operator: address,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setItems(operators.data?.list.map((o) => o.operator) || [])
|
||||
}, [operators.data])
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
const [address, setAddress] = useState("")
|
||||
|
||||
return (
|
||||
<SettingsLayout title="Site Settings">
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
className="fixed z-10 inset-0 overflow-y-auto"
|
||||
>
|
||||
<div className="flex items-center justify-center min-h-screen">
|
||||
<Dialog.Overlay className="fixed inset-0 bg-black opacity-30" />
|
||||
|
||||
<div className="relative bg-white rounded-lg max-w-md w-full mx-auto">
|
||||
<Dialog.Title className="px-5 h-12 flex items-center border-b">
|
||||
{t("New operator")}
|
||||
</Dialog.Title>
|
||||
|
||||
<div className="p-5">
|
||||
<Input
|
||||
className="w-full"
|
||||
label={t("Operator Address") || ""}
|
||||
required
|
||||
onChange={(e) => {
|
||||
setAddress(e.target.value)
|
||||
}}
|
||||
/>
|
||||
<div className="form-label mt-5">
|
||||
{t("Operator Character Check")}
|
||||
</div>
|
||||
<div>
|
||||
<CharacterCard
|
||||
address={address}
|
||||
open={true}
|
||||
hideFollowButton={true}
|
||||
style="flat"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="h-16 border-t flex items-center px-5">
|
||||
<Button
|
||||
isLoading={addOperator.isLoading}
|
||||
isDisabled={!address}
|
||||
onClick={addItem}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
<div className="p-5 text-zinc-500 bg-orange-50 mb-5 rounded-lg text-sm space-y-2">
|
||||
<p className="text-zinc-800 text-sm font-bold">⚠️ {t("Warning")}:</p>
|
||||
<p>
|
||||
<span className="text-zinc-800">
|
||||
{isEmailAccount && (
|
||||
<span>
|
||||
Email users cannot set operators.{" "}
|
||||
<UniLink
|
||||
className="underline"
|
||||
href={
|
||||
getSiteLink({
|
||||
subdomain: "crossbell-blog",
|
||||
}) + "/newbie-villa"
|
||||
}
|
||||
>
|
||||
Learn more
|
||||
</UniLink>{" "}
|
||||
or{" "}
|
||||
<span
|
||||
className="underline cursor-pointer"
|
||||
onClick={upgradeAccountModal.show}
|
||||
>
|
||||
upgrade account
|
||||
</span>
|
||||
.
|
||||
</span>
|
||||
)}
|
||||
{userRole.data === "operator" && (
|
||||
<span>
|
||||
Operators cannot set other operators. Please contact the site
|
||||
owner.
|
||||
</span>
|
||||
)}
|
||||
{!isEmailAccount && userRole.data !== "operator" && (
|
||||
<span>
|
||||
{t(
|
||||
"Operators have permissions to enter your dashboard, change your settings(excluding xLog subdomain) and post, modify, delete contents on your site.",
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
className={
|
||||
isEmailAccount || userRole.data === "operator"
|
||||
? `grayscale cursor-not-allowed`
|
||||
: ""
|
||||
}
|
||||
>
|
||||
<div className="bg-zinc-50 rounded-lg overflow-hidden">
|
||||
{items.length === 0 && (
|
||||
<div className="text-center text-zinc-500 p-5">
|
||||
No operators yet
|
||||
</div>
|
||||
)}
|
||||
{items.map((item, index) => {
|
||||
return (
|
||||
<SortableNavigationItem
|
||||
key={index}
|
||||
item={item}
|
||||
removeItem={removeItem}
|
||||
isLoading={removeOperator.isLoading}
|
||||
disabled={isEmailAccount || userRole.data === "operator"}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<style jsx global>{`
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
<div className="border-t pt-5 mt-10 space-x-3 flex items-center">
|
||||
<Button
|
||||
onClick={newEmptyItem}
|
||||
isDisabled={isEmailAccount || userRole.data === "operator"}
|
||||
>
|
||||
{t("Add")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
SettingsOperatorPage.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Site Settings">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -1,257 +0,0 @@
|
|||
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 { 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 { 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
|
||||
url?: string | undefined
|
||||
} & {
|
||||
id: string
|
||||
}
|
||||
|
||||
type UpdateItem = (id: string, newItem: Partial<Item>) => void
|
||||
|
||||
type RemoveItem = (id: string) => void
|
||||
|
||||
const SortableNavigationItem: React.FC<{
|
||||
item: Item
|
||||
updateItem: UpdateItem
|
||||
removeItem: RemoveItem
|
||||
}> = ({ item, updateItem, removeItem }) => {
|
||||
const { t } = useTranslation("dashboard")
|
||||
return (
|
||||
<div className="flex space-x-5 border-b p-5 bg-zinc-50 last:border-0">
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
className="drag-handle cursor-grab -mt-1 text-zinc-400 rounded-lg h-8 w-6 flex items-center justify-center hover:text-zinc-800 hover:bg-zinc-200"
|
||||
>
|
||||
<i className="icon-[mingcute--dot-grid-fill]" />
|
||||
</button>
|
||||
</div>
|
||||
<Input
|
||||
label={t("Platform") || ""}
|
||||
required
|
||||
id={`${item.id}-platform`}
|
||||
value={item.platform}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
updateItem(item.id, { platform: e.target.value })
|
||||
}
|
||||
/>
|
||||
<Input
|
||||
label="Identity"
|
||||
required
|
||||
id={`${item.id}-identity`}
|
||||
type="text"
|
||||
value={item.identity}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) =>
|
||||
updateItem(item.id, { identity: e.target.value })
|
||||
}
|
||||
/>
|
||||
<div className="flex items-end pb-2">
|
||||
<Platform platform={item.platform} username={item.identity}></Platform>
|
||||
</div>
|
||||
<div className="flex items-end relative -top-[5px]">
|
||||
<Button onClick={() => removeItem(item.id)} variantColor="red">
|
||||
{t("Remove")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default function SiteSettingsNavigationPage() {
|
||||
const router = useRouter()
|
||||
|
||||
const subdomain = router.query.subdomain as string
|
||||
|
||||
const updateSite = useUpdateSite()
|
||||
const site = useGetSite(subdomain)
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
const [items, setItems] = useState<Item[]>([])
|
||||
|
||||
const itemsModified = useMemo(() => {
|
||||
if (!site.isSuccess) return false
|
||||
return !equal(items, site.data?.metadata?.content?.connected_accounts)
|
||||
}, [items, site.data, site.isSuccess])
|
||||
|
||||
const updateItem: UpdateItem = (id, newItem) => {
|
||||
setItems((items) => {
|
||||
return items.map((item) => {
|
||||
if (item.id === id) {
|
||||
return { ...item, ...newItem }
|
||||
}
|
||||
return item
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const newEmptyItem = () => {
|
||||
setItems((items) => [
|
||||
...items,
|
||||
{ id: nanoid(), platform: "", identity: "" },
|
||||
])
|
||||
}
|
||||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
if (site.data?.handle) {
|
||||
updateSite.mutate({
|
||||
site: site.data?.handle,
|
||||
connected_accounts: items.map(({ id, ...item }) => item),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (updateSite.isSuccess) {
|
||||
if (updateSite.data?.code === 0) {
|
||||
toast.success("Saved")
|
||||
} else {
|
||||
toast.error("Failed to save" + ": " + updateSite.data.message)
|
||||
}
|
||||
} else if (updateSite.isError) {
|
||||
toast.error("Failed to save")
|
||||
}
|
||||
}, [updateSite.isSuccess, updateSite.isError])
|
||||
|
||||
const removeItem: RemoveItem = (id) => {
|
||||
setItems((items) => items.filter((item) => item.id !== id))
|
||||
}
|
||||
|
||||
const [hasSet, setHasSet] = useState(false)
|
||||
useEffect(() => {
|
||||
if (site.data?.metadata?.content?.connected_accounts && !hasSet) {
|
||||
setHasSet(true)
|
||||
setItems(
|
||||
site.data?.metadata?.content?.connected_accounts.map((item) => {
|
||||
const match = item.match(/:\/\/account:(.*)@(.*)/)
|
||||
if (match) {
|
||||
return {
|
||||
id: nanoid(),
|
||||
identity: match[1],
|
||||
platform: match[2],
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
id: nanoid(),
|
||||
identity: item,
|
||||
platform: "",
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
}
|
||||
}, [site.data?.metadata?.content?.connected_accounts, hasSet])
|
||||
|
||||
return (
|
||||
<SettingsLayout title="Site Settings">
|
||||
<div className="p-5 text-zinc-500 bg-zinc-50 mb-5 rounded-lg text-xs space-y-2">
|
||||
<p className="text-zinc-800 text-sm font-bold">{t("Tips")}:</p>
|
||||
<p>
|
||||
<span className="text-zinc-800">{t("social tips.p1")}</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-zinc-800">
|
||||
<Trans ns="dashboard" i18nKey="social tips.p2">
|
||||
We support{" "}
|
||||
<UniLink
|
||||
href="https://github.com/Crossbell-Box/xLog/blob/dev/src/components/site/Platform.tsx#L7"
|
||||
className="underline"
|
||||
>
|
||||
these platforms
|
||||
</UniLink>{" "}
|
||||
with automatic display of logos and links, other platforms will
|
||||
display a default logo. Please feel free to submit an issue or pr
|
||||
to us to support more platforms.
|
||||
</Trans>
|
||||
</span>
|
||||
</p>
|
||||
<p>
|
||||
<span className="text-zinc-800">
|
||||
<Trans ns="dashboard" i18nKey="social tips.p3">
|
||||
You can also connect to Twitter, Telegram Channel, Medium,
|
||||
Substack and more and automatically sync content on{" "}
|
||||
<UniLink href="https://xsync.app/" className="underline">
|
||||
xSync
|
||||
</UniLink>
|
||||
. When you have set up the sync there, it will also show here.
|
||||
</Trans>
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="bg-zinc-50 rounded-lg overflow-auto">
|
||||
{items.length === 0 && (
|
||||
<div className="text-center text-zinc-500 p-5">
|
||||
No navigation items yet
|
||||
</div>
|
||||
)}
|
||||
<ReactSortable list={items} setList={setItems} handle=".drag-handle">
|
||||
{items.map((item) => {
|
||||
return (
|
||||
<SortableNavigationItem
|
||||
key={item.id}
|
||||
item={item}
|
||||
updateItem={updateItem}
|
||||
removeItem={removeItem}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
<style jsx global>{`
|
||||
.sortable-ghost {
|
||||
opacity: 0.4;
|
||||
}
|
||||
`}</style>
|
||||
</ReactSortable>
|
||||
</div>
|
||||
<div className="border-t pt-5 mt-10 space-x-3 flex items-center">
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={updateSite.isLoading}
|
||||
isDisabled={!itemsModified}
|
||||
>
|
||||
{t("Save")}
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={newEmptyItem}>
|
||||
{t("New Item")}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</SettingsLayout>
|
||||
)
|
||||
}
|
||||
|
||||
SiteSettingsNavigationPage.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Site Settings">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -1,167 +0,0 @@
|
|||
import { GetServerSideProps } from "next"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
import type { ReactElement } from "react"
|
||||
|
||||
import {
|
||||
useAccountBalance,
|
||||
useClaimCSBStatus,
|
||||
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 { 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 site = useGetSite(subdomain)
|
||||
|
||||
const miraBalance = useGetMiraBalance(site.data?.characterId)
|
||||
const csbBalance = useAccountBalance()
|
||||
const claimCSBStatus = useClaimCSBStatus()
|
||||
const claimCSBModal = useWalletClaimCSBModal()
|
||||
|
||||
const tokens = [
|
||||
{
|
||||
name: "MIRA",
|
||||
balance: miraBalance.isLoading
|
||||
? "Loading..."
|
||||
: miraBalance.data?.data || 0,
|
||||
description: (
|
||||
<>
|
||||
<p>
|
||||
{t(
|
||||
"$MIRA is a valuable token in the Crossbell world, and can be easily exchanged on the Crosschain Bridge and Uniswap.",
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{t("In the early stage, xLog will use $MIRA to motivate creators.")}
|
||||
</p>
|
||||
<p>{t("You can obtain $MIRA through the following ways:")}</p>
|
||||
<ul className="ml-2">
|
||||
<li>
|
||||
1.{" "}
|
||||
<UniLink
|
||||
className="underline"
|
||||
href={`${getSiteLink({
|
||||
subdomain: "xlog",
|
||||
})}/creator-incentive-plan`}
|
||||
>
|
||||
{t("Creator incentive program.")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
2.{" "}
|
||||
<UniLink
|
||||
className="underline"
|
||||
href={`/dashboard/${subdomain}/events`}
|
||||
>
|
||||
{t("Participate in events.")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>
|
||||
3.{" "}
|
||||
<UniLink className="underline" href={MIRA_LINK}>
|
||||
{t("Swap from USDC.")}
|
||||
</UniLink>
|
||||
</li>
|
||||
<li>4. {t("Received tips and sponsorships from readers.")}</li>
|
||||
</ul>
|
||||
</>
|
||||
),
|
||||
buttons: (
|
||||
<div className="w-fit space-x-4">
|
||||
<Button onClick={() => window.open(MIRA_LINK)}>
|
||||
<>{t("Swap to USDC") || ""}</>
|
||||
</Button>
|
||||
<UniLink
|
||||
className="underline text-zinc-500"
|
||||
href={`${getSiteLink({ subdomain: "atlas" })}/swap-mira-for-usdc`}
|
||||
>
|
||||
{t("Swap Tutorial")}
|
||||
</UniLink>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "CSB",
|
||||
balance: csbBalance?.balance?.formatted || 0,
|
||||
description: (
|
||||
<>
|
||||
{t(
|
||||
"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.",
|
||||
)}
|
||||
</>
|
||||
),
|
||||
buttons: (
|
||||
<>
|
||||
<Button
|
||||
isDisabled={!claimCSBStatus.isEligibleToClaim}
|
||||
onClick={() => claimCSBModal.show()}
|
||||
>
|
||||
{claimCSBStatus.errorMsg || t("Free Claim")}
|
||||
</Button>
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
name: "XLOG",
|
||||
description: t(
|
||||
"$XLOG is related to xLog DAO, but it's too early now, please stay tuned.",
|
||||
),
|
||||
balance: t("Stay tuned"),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<DashboardMain title="Tokens">
|
||||
<div className="min-w-[270px] max-w-screen-lg flex flex-col space-y-8">
|
||||
<div className="text-sm text-zinc-500 leading-relaxed">
|
||||
{t("features.Earn.description", { ns: "index" })}
|
||||
</div>
|
||||
{tokens.map((token) => {
|
||||
return (
|
||||
<div key={token.name} className="space-y-4">
|
||||
<div className="text-2xl font-medium text-accent">
|
||||
${token.name}
|
||||
</div>
|
||||
<div className="text-zinc-500 text-sm space-y-1">
|
||||
{token.description}
|
||||
</div>
|
||||
<div className="text-lg">
|
||||
{t("Balance")}:{" "}
|
||||
<span className="font-bold">{token.balance}</span>
|
||||
</div>
|
||||
<div>{token.buttons}</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</DashboardMain>
|
||||
)
|
||||
}
|
||||
|
||||
TokensPage.getLayout = (page: ReactElement) => {
|
||||
return <DashboardLayout title="Tokens">{page}</DashboardLayout>
|
||||
}
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { useRouter } from "next/router"
|
||||
import { useEffect, useRef } from "react"
|
||||
|
||||
import {
|
||||
useAccountState,
|
||||
useConnectModal,
|
||||
useWalletMintNewCharacterModal,
|
||||
} from "@crossbell/connect-kit"
|
||||
|
||||
export default function Dashboard() {
|
||||
const router = useRouter()
|
||||
const walletMintNewCharacterModal = useWalletMintNewCharacterModal()
|
||||
const [ssrReady, account] = useAccountState(({ ssrReady, computed }) => [
|
||||
ssrReady,
|
||||
computed.account,
|
||||
])
|
||||
const connectModal = useConnectModal()
|
||||
|
||||
const isConnectModalShown = useRef(false)
|
||||
const isMintCharacterModalShown = useRef(false)
|
||||
|
||||
useEffect(() => {
|
||||
if (ssrReady) {
|
||||
// Wait till SSR is ready
|
||||
if (!account) {
|
||||
// Wallet not connected
|
||||
if (!isConnectModalShown.current) {
|
||||
// Not shown
|
||||
isConnectModalShown.current = true
|
||||
connectModal.show()
|
||||
} else if (!connectModal.isActive) {
|
||||
// Shown, but closed by user
|
||||
router.push("/") // Go back home
|
||||
}
|
||||
} else {
|
||||
// Wallet is connected, wait till site is ready
|
||||
// Reset connect wallet status to prevent unexpected redirect
|
||||
isConnectModalShown.current = false
|
||||
if (!account.character) {
|
||||
// No character found, prompt to mint one
|
||||
if (!isMintCharacterModalShown.current) {
|
||||
// Not shown
|
||||
isMintCharacterModalShown.current = true
|
||||
walletMintNewCharacterModal.show()
|
||||
} else if (!walletMintNewCharacterModal.isActive) {
|
||||
// Shown, but closed by user
|
||||
router.push("/") // Go back home
|
||||
}
|
||||
} else {
|
||||
// Already have characters, redirect to primary
|
||||
router.push(`/dashboard/${account.character.handle}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [ssrReady, router, walletMintNewCharacterModal, account, connectModal])
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-60">
|
||||
Loading...
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -1,38 +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: "latest",
|
||||
})
|
||||
|
||||
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)),
|
||||
}
|
||||
|
||||
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
|
||||
|
|
@ -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: <RssIcon className="w-full h-full text-orange-500" />,
|
||||
url:
|
||||
getSiteLink({
|
||||
subdomain: "xlog",
|
||||
}) + "/feed?format=xml",
|
||||
},
|
||||
{
|
||||
name: "JSON Feed",
|
||||
icon: <Image src="/assets/json-feed.png" alt="JSON Feed" />,
|
||||
url:
|
||||
getSiteLink({
|
||||
subdomain: "xlog",
|
||||
}) + "/feed",
|
||||
},
|
||||
{
|
||||
name: "xChar",
|
||||
icon: <XCharLogo className="w-full h-full" />,
|
||||
url: "https://xchar.app/",
|
||||
},
|
||||
{
|
||||
name: "xFeed",
|
||||
icon: <XFeedLogo className="w-full h-full" />,
|
||||
url: "https://crossbell.io/feed",
|
||||
},
|
||||
{
|
||||
name: "xSync",
|
||||
icon: <XSyncLogo className="w-full h-full" />,
|
||||
url: "https://xsync.app/",
|
||||
},
|
||||
{
|
||||
name: "xShop",
|
||||
icon: <XShopLogo className="w-full h-full" />,
|
||||
text: "Coming soon",
|
||||
},
|
||||
{
|
||||
name: "Crossbell Scan",
|
||||
icon: <CrossbellChainLogo className="w-full h-full text-[#E7B75B]" />,
|
||||
url: "https://scan.crossbell.io/",
|
||||
},
|
||||
{
|
||||
name: "Crossbell Faucet",
|
||||
icon: <CrossbellChainLogo className="w-full h-full text-[#E7B75B]" />,
|
||||
url: "https://faucet.crossbell.io/",
|
||||
},
|
||||
{
|
||||
name: "Crossbell Export",
|
||||
icon: <CrossbellChainLogo className="w-full h-full text-[#E7B75B]" />,
|
||||
url: "https://export.crossbell.io/",
|
||||
},
|
||||
{
|
||||
name: "Crossbell SDK",
|
||||
icon: <CrossbellChainLogo className="w-full h-full text-[#E7B75B]" />,
|
||||
url: "https://crossbell-box.github.io/crossbell.js/",
|
||||
},
|
||||
{
|
||||
name: "RSS3",
|
||||
icon: (
|
||||
<Image alt="RSS3" src="/assets/rss3.svg" className="rounded" fill />
|
||||
),
|
||||
url: "https://rss3.io/",
|
||||
},
|
||||
{
|
||||
name: "Hoot It",
|
||||
icon: (
|
||||
<Image alt="Hoot It" src="/assets/hoot.svg" className="rounded" fill />
|
||||
),
|
||||
url: "https://hoot.it/search/xLog",
|
||||
},
|
||||
{
|
||||
name: "Raycast",
|
||||
icon: <Image src="/assets/raycast.png" alt="Raycast" />,
|
||||
url: "https://www.raycast.com/Songkeys/crossbell",
|
||||
},
|
||||
{
|
||||
name: "Obsidian",
|
||||
icon: (
|
||||
<Image
|
||||
src="/assets/obsidian.svg"
|
||||
alt="Obsidian"
|
||||
className="rounded"
|
||||
fill
|
||||
/>
|
||||
),
|
||||
text: "Coming soon",
|
||||
},
|
||||
]
|
||||
|
||||
const [showcaseMore, setShowcaseMore] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="max-w-screen-lg px-5 mx-auto">
|
||||
<div className="h-screen w-full flex justify-center flex-col relative text-center">
|
||||
<h2 className="text-6xl sm:text-8xl font-bold mb-5">
|
||||
{features.map((feature) => (
|
||||
<span
|
||||
key={feature.title}
|
||||
className={`text-feature text-feature-${feature.title.toLocaleLowerCase()} block sm:inline-block sm:mr-1 mb-2`}
|
||||
>
|
||||
{t(feature.title)}
|
||||
{t(".")}
|
||||
</span>
|
||||
))}
|
||||
</h2>
|
||||
<h3 className="mt-3 sm:mt-5 text-zinc-800 text-2xl sm:text-4xl font-light">
|
||||
<Trans i18nKey="description" ns="index">
|
||||
<strong className="font-medium">xLog</strong> is the best{" "}
|
||||
<UniLink
|
||||
className="underline decoration-2 text-yellow-400 font-medium"
|
||||
href={CSB_SCAN}
|
||||
>
|
||||
on-chain
|
||||
</UniLink>{" "}
|
||||
and{" "}
|
||||
<UniLink
|
||||
className="underline decoration-2 text-green-400 font-medium"
|
||||
href={GITHUB_LINK}
|
||||
>
|
||||
open-source
|
||||
</UniLink>{" "}
|
||||
blogging community for everyone.
|
||||
</Trans>
|
||||
</h3>
|
||||
<div className="my-4 sm:my-12 flex items-center justify-center sm:flex-row flex-col">
|
||||
<Button
|
||||
className="text-accent h-10 mt-4 flex items-center"
|
||||
onClick={tryNow}
|
||||
size="2xl"
|
||||
variantColor="black"
|
||||
>
|
||||
{isConnected ? (
|
||||
<>
|
||||
<span className="icon-[mingcute--grid-line] text-xl mr-2 inline-block"></span>
|
||||
<span>{t("Dashboard")}</span>
|
||||
</>
|
||||
) : (
|
||||
t("Get my xLog in 5 minutes")
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
className="text-accent h-10 sm:ml-4 mt-4 flex"
|
||||
size="2xl"
|
||||
variant="outline"
|
||||
variantColor="gradient"
|
||||
>
|
||||
<span>
|
||||
<UniLink href="/activities">{t("Look at others'")}</UniLink>
|
||||
</span>
|
||||
</Button>
|
||||
</div>
|
||||
<div className="text-center absolute bottom-4 sm:bottom-14 w-full flex items-center justify-center flex-col">
|
||||
<span className="mb-1 sm:mb-3">{t("Explore the xLog way")}</span>
|
||||
<Link
|
||||
to="Features"
|
||||
spy={true}
|
||||
smooth={true}
|
||||
duration={500}
|
||||
className="cursor-pointer inline-block icon-[mingcute--down-line] text-3xl"
|
||||
></Link>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Element name="Features">
|
||||
{features.map((feature, index) => (
|
||||
<div className="text-center mt-28" key={feature.title}>
|
||||
<div className="">
|
||||
<div
|
||||
className={`h-20 w-[1px] bg-gradient-to-b mx-auto text-feature-${feature.title.toLocaleLowerCase()}`}
|
||||
style={
|
||||
{
|
||||
"--tw-gradient-from": "transparent",
|
||||
} as any
|
||||
}
|
||||
></div>
|
||||
<div
|
||||
className={`block rounded-full w-10 h-10 mx-auto p-2 text-white bg-gradient-to-br text-feature-${feature.title.toLocaleLowerCase()}`}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
<h3
|
||||
className={`mt-6 inline-block text-3xl font-bold text-feature text-feature-${feature.title.toLocaleLowerCase()}`}
|
||||
>
|
||||
{t(feature.title)}
|
||||
</h3>
|
||||
<h4 className="mt-6 text-5xl sm:text-6xl font-bold">
|
||||
{t(`features.${feature.title}.subtitle`)}
|
||||
</h4>
|
||||
<p className="mt-6 text-zinc-500">
|
||||
<Trans
|
||||
i18nKey={`features.${feature.title}.description`}
|
||||
components={{
|
||||
aown: (
|
||||
<UniLink
|
||||
className="underline"
|
||||
href="https://github.com/Crossbell-Box/Crossbell-Contracts/wiki"
|
||||
>
|
||||
.
|
||||
</UniLink>
|
||||
),
|
||||
}}
|
||||
ns="index"
|
||||
/>
|
||||
</p>
|
||||
</div>
|
||||
<ul className="pt-10 grid grid-cols-1 sm:grid-cols-3 gap-8">
|
||||
{feature.subfeatures.map((item) => (
|
||||
<li
|
||||
className="border rounded-xl overflow-hidden bg-white hover:shadow-md hover:scale-105 transition duration-300 cursor-default"
|
||||
key={item.title}
|
||||
>
|
||||
{item.screenshot?.src && (
|
||||
<div className="w-full h-44">
|
||||
<Image
|
||||
className="object-cover"
|
||||
src={item.screenshot.src}
|
||||
alt={item.title}
|
||||
fill
|
||||
></Image>
|
||||
</div>
|
||||
)}
|
||||
<p className="text-2xl font-bold mt-5 flex items-center px-5">
|
||||
<span className="mr-3">{item.icon}</span>
|
||||
<span
|
||||
className={`text-feature text-feature-${feature.title.toLocaleLowerCase()}`}
|
||||
>
|
||||
{t(item.title)}
|
||||
</span>
|
||||
</p>
|
||||
<p className="text-base font-light mt-3 mb-4 leading-normal px-5 text-left">
|
||||
<Trans
|
||||
i18nKey={`${item.title} text`}
|
||||
components={{
|
||||
strong: <strong className="font-bold" />,
|
||||
axfm: (
|
||||
<UniLink
|
||||
className="underline"
|
||||
href={`${getSiteLink({
|
||||
subdomain: "xlog",
|
||||
})}/xfm`}
|
||||
>
|
||||
.
|
||||
</UniLink>
|
||||
),
|
||||
aincentive: (
|
||||
<UniLink
|
||||
className="underline"
|
||||
href={`${getSiteLink({
|
||||
subdomain: "xlog",
|
||||
})}/creator-incentive-plan`}
|
||||
>
|
||||
.
|
||||
</UniLink>
|
||||
),
|
||||
}}
|
||||
ns="index"
|
||||
/>
|
||||
</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
{feature.extra && (
|
||||
<div className="border rounded-xl overflow-hidden bg-white hover:shadow-md hover:scale-105 transition duration-300 cursor-default mt-10 px-5 py-8 space-y-4">
|
||||
<div className="font-bold text-3xl">
|
||||
🤫{" "}
|
||||
<span
|
||||
className={`text-feature text-feature-${feature.title.toLocaleLowerCase()}`}
|
||||
>
|
||||
{t(`features.${feature.title}.extra.title`)}
|
||||
</span>
|
||||
</div>
|
||||
<div>{t(`features.${feature.title}.extra.description`)}</div>
|
||||
<Button
|
||||
variantColor="black"
|
||||
onClick={() =>
|
||||
window.open(
|
||||
getSiteLink({
|
||||
subdomain: "xlog",
|
||||
}),
|
||||
)
|
||||
}
|
||||
>
|
||||
{t(`features.${feature.title}.extra.button`)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Element>
|
||||
<Element name="Showcase" className="text-center">
|
||||
<div className="pt-32 text-5xl sm:text-6xl font-bold">
|
||||
{t("Showcase")}
|
||||
</div>
|
||||
<div className="my-10 text-zinc-700">
|
||||
<p className="text-lg">
|
||||
{t(
|
||||
"Discover these awesome teams and creators on xLog (sorted by update time)",
|
||||
)}
|
||||
</p>
|
||||
<FollowAllButton
|
||||
className="mt-5"
|
||||
size="xl"
|
||||
characterIds={showcaseSites.data
|
||||
?.map((s: { characterId?: string }) => s.characterId)
|
||||
.filter(Boolean)
|
||||
.map(Number)}
|
||||
siteIds={showcaseSites.data?.map(
|
||||
(s: { handle: string }) => s.handle,
|
||||
)}
|
||||
/>
|
||||
<ul
|
||||
className={`pt-10 grid grid-cols-2 md:grid-cols-3 gap-10 overflow-y-hidden relative text-left ${
|
||||
showcaseMore ? "" : "max-h-[540px]"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`absolute bottom-0 h-20 left-0 right-0 bg-gradient-to-t from-white via-white flex items-end justify-center font-bold cursor-pointer ${
|
||||
showcaseMore ? "hidden" : ""
|
||||
}`}
|
||||
onClick={() => setShowcaseMore(true)}
|
||||
>
|
||||
{t("Show more")}
|
||||
</div>
|
||||
{showcaseSites.data?.map((site) => (
|
||||
<li className="inline-flex align-middle" key={site.handle}>
|
||||
<UniLink
|
||||
href={getSiteLink({
|
||||
subdomain: site.handle,
|
||||
})}
|
||||
className="inline-flex align-middle w-full"
|
||||
>
|
||||
<CharacterFloatCard siteId={site.handle}>
|
||||
<span className="w-14 h-14 inline-block">
|
||||
<Image
|
||||
className="rounded-full"
|
||||
src={
|
||||
site.metadata.content?.avatars?.[0] ||
|
||||
"ipfs://bafkreiabgixxp63pg64moxnsydz7hewmpdkxxi3kdsa4oqv4pb6qvwnmxa"
|
||||
}
|
||||
alt={site.handle}
|
||||
width="56"
|
||||
height="56"
|
||||
></Image>
|
||||
</span>
|
||||
</CharacterFloatCard>
|
||||
<span className="ml-3 min-w-0 flex-1 justify-center inline-flex flex-col">
|
||||
<span className="truncate w-full inline-block font-medium">
|
||||
{site.metadata.content?.name}
|
||||
</span>
|
||||
{site.metadata.content?.bio && (
|
||||
<span className="text-gray-500 text-xs truncate w-full inline-block mt-1">
|
||||
{site.metadata.content?.bio}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
</UniLink>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</Element>
|
||||
<Element name="Integration" className="text-center">
|
||||
<div className="pt-32 text-5xl sm:text-6xl font-bold">
|
||||
{t("Integration")}
|
||||
</div>
|
||||
<div className="my-10 text-zinc-700">
|
||||
<p className="text-xl">
|
||||
{t(
|
||||
"xLog's open design allows it to integrate with many other open protocols and applications without friction.",
|
||||
)}
|
||||
</p>
|
||||
<ul className="mt-14 grid grid-cols-3 sm:grid-cols-5 gap-y-14 gap-x-2">
|
||||
{integrations.map((item, index) => (
|
||||
<li
|
||||
className="hover:scale-105 transition-transform duration-300"
|
||||
key={index}
|
||||
>
|
||||
{item.url ? (
|
||||
<UniLink
|
||||
href={item.url}
|
||||
className="w-full flex items-center flex-col justify-center"
|
||||
>
|
||||
<div className="w-12 h-12 rounded-md overflow-hidden">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div className="font-medium sm:text-lg mt-2 text-center">
|
||||
{item.name}
|
||||
</div>
|
||||
</UniLink>
|
||||
) : (
|
||||
<Tooltip label={item.text!}>
|
||||
<div className="w-full h-full flex items-center flex-col justify-center">
|
||||
<div className="w-12 h-12 rounded-md overflow-hidden">
|
||||
{item.icon}
|
||||
</div>
|
||||
<div className="font-medium sm:text-lg mt-2 text-center">
|
||||
{item.name}
|
||||
</div>
|
||||
</div>
|
||||
</Tooltip>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
<li className="flex items-center justify-center">
|
||||
<i className="icon-[mingcute--more-1-line] text-5xl" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Element>
|
||||
</div>
|
||||
<div className="my-20 text-center">
|
||||
<div className="w-[100px] h-[100px] mx-auto mb-10">
|
||||
<Logo type="lottie" width={100} height={100} loop={true} />
|
||||
</div>
|
||||
{isConnected ? (
|
||||
<UniLink
|
||||
href="/dashboard"
|
||||
className="text-accent inline-flex items-center space-x-2"
|
||||
>
|
||||
<Button size="2xl" variantColor="black">
|
||||
{t("Get my xLog in 5 minutes")}
|
||||
</Button>
|
||||
</UniLink>
|
||||
) : (
|
||||
<Button onClick={tryNow} size="2xl" variantColor="black">
|
||||
{t("Get my xLog in 5 minutes")}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
Home.getLayout = (page: ReactElement) => {
|
||||
return <MainLayout>{page}</MainLayout>
|
||||
}
|
||||
|
||||
export default Home
|
||||
|
|
@ -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/home/HomeFeed"
|
||||
import { MainSidebar } from "~/components/home/HomeSidebar"
|
||||
import { MainLayout } from "~/components/main/MainLayout"
|
||||
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 (
|
||||
<section className="pt-24">
|
||||
<div className="max-w-screen-lg px-5 mx-auto flex">
|
||||
<div className="flex-1 min-w-[300px]">
|
||||
<SearchInput value={keyword} />
|
||||
<div className="mt-10">
|
||||
<MainFeed type="search" keyword={keyword} />
|
||||
</div>
|
||||
</div>
|
||||
<MainSidebar hideSearch={true} />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Search.getLayout = (page: ReactElement) => {
|
||||
return <MainLayout>{page}</MainLayout>
|
||||
}
|
||||
|
||||
export default Search
|
||||
|
|
@ -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/home/HomeFeed"
|
||||
import { MainSidebar } from "~/components/home/HomeSidebar"
|
||||
import { MainLayout } from "~/components/main/MainLayout"
|
||||
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 (
|
||||
<section className="pt-24">
|
||||
<div className="max-w-screen-lg px-5 mx-auto flex">
|
||||
<div className="flex-1 min-w-[300px]">
|
||||
<h2 className="text-3xl font-bold">Topic: {topic}</h2>
|
||||
<p className="text-zinc-400 mt-4">{info?.description}</p>
|
||||
<div className="mt-10">
|
||||
<MainFeed type="topic" noteIds={info?.notes} />
|
||||
</div>
|
||||
</div>
|
||||
<MainSidebar />
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
||||
Topic.getLayout = (page: ReactElement) => {
|
||||
return <MainLayout>{page}</MainLayout>
|
||||
}
|
||||
|
||||
export default Topic
|
||||
Loading…
Reference in New Issue