Remove dependency Unidata (#473)
This commit is contained in:
parent
f7068304de
commit
2fe77a563f
|
|
@ -38,7 +38,7 @@ export const AchievementItem: React.FC<{
|
|||
group: AchievementSection["groups"][number]
|
||||
layoutId: string
|
||||
size?: number
|
||||
characterId?: string
|
||||
characterId?: number
|
||||
isOwner: boolean
|
||||
}> = ({ group, layoutId, size, characterId, isOwner }) => {
|
||||
const date = useDate()
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ export const AchievementModal: React.FC<{
|
|||
group: AchievementSection["groups"][number]
|
||||
layoutId: string
|
||||
isOwner: boolean
|
||||
characterId?: string
|
||||
characterId?: number
|
||||
}> = ({ opened, setOpened, group, layoutId, isOwner, characterId }) => {
|
||||
const date = useDate()
|
||||
const { t } = useTranslation("common")
|
||||
|
|
|
|||
|
|
@ -3,24 +3,26 @@ import { useTranslation } from "next-i18next"
|
|||
import { Disclosure } from "@headlessui/react"
|
||||
|
||||
import { BlockchainIcon } from "~/components/icons/BlockchainIcon"
|
||||
import { CSB_SCAN, IPFS_GATEWAY } from "~/lib/env"
|
||||
import { CSB_SCAN } from "~/lib/env"
|
||||
import { toCid, toGateway, toIPFS } from "~/lib/ipfs-parser"
|
||||
import { Note, Profile } from "~/lib/types"
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { useGetGreenfieldId } from "~/queries/site"
|
||||
|
||||
export const BlockchainInfo: React.FC<{
|
||||
site?: Profile | null
|
||||
page?: Note | null
|
||||
site?: ExpandedCharacter
|
||||
page?: ExpandedNote
|
||||
}> = ({ site, page }) => {
|
||||
const { t } = useTranslation(["common", "site"])
|
||||
|
||||
const ipfs = page
|
||||
? page.related_urls?.filter((url) => url.startsWith(IPFS_GATEWAY))?.[0]
|
||||
: site?.metadata?.uri
|
||||
const ipfs = (page ? page.metadata?.uri : site?.metadata?.uri) || ""
|
||||
const greenfieldId = useGetGreenfieldId(toCid(ipfs))
|
||||
|
||||
const type = page ? (page?.tags?.includes("post") ? "post" : "page") : "blog"
|
||||
const type = page
|
||||
? page?.metadata?.content?.tags?.includes("post")
|
||||
? "post"
|
||||
: "page"
|
||||
: "blog"
|
||||
|
||||
return (
|
||||
<div className="text-sm">
|
||||
|
|
@ -54,46 +56,53 @@ export const BlockchainInfo: React.FC<{
|
|||
<div className="font-medium">{t("Owner")}</div>
|
||||
<div>
|
||||
<BlockchainInfoLink
|
||||
href={`${CSB_SCAN}/address/${
|
||||
page?.metadata?.owner || site?.metadata?.owner
|
||||
}`}
|
||||
key={page?.metadata?.owner || site?.metadata?.owner}
|
||||
href={`${CSB_SCAN}/address/${page?.owner || site?.owner}`}
|
||||
key={page?.owner || site?.owner}
|
||||
>
|
||||
{page?.metadata?.owner || site?.metadata?.owner}
|
||||
{page?.owner || site?.owner}
|
||||
</BlockchainInfoLink>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div className="font-medium">{t("Transaction Hash")}</div>
|
||||
<div>
|
||||
{page
|
||||
? page?.related_urls
|
||||
?.filter((url) => url.startsWith(CSB_SCAN + "/tx/"))
|
||||
.map((url, index) => {
|
||||
return (
|
||||
<BlockchainInfoLink href={url} key={url}>
|
||||
{t(index === 0 ? "Creation" : "Last Update")}{" "}
|
||||
{url
|
||||
.replace(CSB_SCAN + "/tx/", "")
|
||||
.slice(0, 10)}
|
||||
...
|
||||
{url.replace(CSB_SCAN + "/tx/", "").slice(-10)}
|
||||
</BlockchainInfoLink>
|
||||
)
|
||||
})
|
||||
: site?.metadata?.transactions.map(
|
||||
(hash: string, index: number) => {
|
||||
return (
|
||||
<BlockchainInfoLink
|
||||
href={`${CSB_SCAN}/tx/${hash}`}
|
||||
key={hash}
|
||||
>
|
||||
{t(index === 0 ? "Creation" : "Last Update")}{" "}
|
||||
{hash.slice(0, 10)}...{hash.slice(-10)}
|
||||
</BlockchainInfoLink>
|
||||
)
|
||||
},
|
||||
)}
|
||||
{page ? (
|
||||
<>
|
||||
<BlockchainInfoLink
|
||||
href={`${CSB_SCAN}/tx/${page?.transactionHash}`}
|
||||
key={page?.transactionHash}
|
||||
>
|
||||
{t("Creation")} {page?.transactionHash.slice(0, 10)}
|
||||
...{page?.transactionHash.slice(-10)}
|
||||
</BlockchainInfoLink>
|
||||
<BlockchainInfoLink
|
||||
href={`${CSB_SCAN}/tx/${page?.updatedTransactionHash}`}
|
||||
key={page?.updatedTransactionHash}
|
||||
>
|
||||
{t("Last Update")}{" "}
|
||||
{page?.updatedTransactionHash.slice(0, 10)}...
|
||||
{page?.updatedTransactionHash.slice(-10)}
|
||||
</BlockchainInfoLink>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<BlockchainInfoLink
|
||||
href={`${CSB_SCAN}/tx/${site?.transactionHash}`}
|
||||
key={site?.transactionHash}
|
||||
>
|
||||
{t("Creation")} {site?.transactionHash.slice(0, 10)}
|
||||
...{site?.transactionHash.slice(-10)}
|
||||
</BlockchainInfoLink>
|
||||
<BlockchainInfoLink
|
||||
href={`${CSB_SCAN}/tx/${site?.updatedTransactionHash}`}
|
||||
key={site?.updatedTransactionHash}
|
||||
>
|
||||
{t("Last Update")}{" "}
|
||||
{site?.updatedTransactionHash.slice(0, 10)}...
|
||||
{site?.updatedTransactionHash.slice(-10)}
|
||||
</BlockchainInfoLink>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { FollowingCount } from "~/components/common/FollowingCount"
|
|||
import { Titles } from "~/components/common/Titles"
|
||||
import { Avatar } from "~/components/ui/Avatar"
|
||||
import { useDate } from "~/hooks/useDate"
|
||||
import type { Profile } from "~/lib/types"
|
||||
import type { ExpandedCharacter } from "~/lib/types"
|
||||
import { cn } from "~/lib/utils"
|
||||
import * as siteModel from "~/models/site.model"
|
||||
|
||||
|
|
@ -28,7 +28,7 @@ export const CharacterCard: React.FC<{
|
|||
style,
|
||||
}) => {
|
||||
const [firstOpen, setFirstOpen] = useState("")
|
||||
const [site, setSite] = useState<Profile>()
|
||||
const [site, setSite] = useState<ExpandedCharacter>()
|
||||
const date = useDate()
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
|
|
@ -39,9 +39,7 @@ export const CharacterCard: React.FC<{
|
|||
if (siteId) {
|
||||
siteModel.getSite(siteId).then((site) => setSite(site))
|
||||
} else if (address) {
|
||||
siteModel
|
||||
.getUserSites({ address })
|
||||
.then((sites) => setSite(sites?.[0]))
|
||||
siteModel.getSiteByAddress(address).then((site) => setSite(site))
|
||||
}
|
||||
} else {
|
||||
setSite(undefined)
|
||||
|
|
@ -64,7 +62,11 @@ export const CharacterCard: React.FC<{
|
|||
{site ? (
|
||||
<>
|
||||
<span className="flex items-center justify-between">
|
||||
<Avatar images={site?.avatars || []} name={site?.name} size={45} />
|
||||
<Avatar
|
||||
images={site?.metadata?.content?.avatars || []}
|
||||
name={site?.metadata?.content?.name}
|
||||
size={45}
|
||||
/>
|
||||
{!hideFollowButton && (
|
||||
<FollowingButton
|
||||
site={site}
|
||||
|
|
@ -75,30 +77,31 @@ export const CharacterCard: React.FC<{
|
|||
</span>
|
||||
<span className="flex items-center space-x-1">
|
||||
<span className="font-bold text-base text-zinc-800">
|
||||
{site?.name}
|
||||
{site?.metadata?.content?.name}
|
||||
</span>
|
||||
<Titles characterId={+(site.metadata?.proof || "")} />
|
||||
<span className="text-gray-600">@{site?.username}</span>
|
||||
<Titles characterId={+(site?.characterId || "")} />
|
||||
<span className="text-gray-600">@{site?.handle}</span>
|
||||
</span>
|
||||
{site?.description && (
|
||||
{site?.metadata?.content?.bio && (
|
||||
<span className="text-gray-600 line-clamp-4">
|
||||
{site?.description}
|
||||
{site?.metadata?.content?.bio}
|
||||
</span>
|
||||
)}
|
||||
{!simple && (
|
||||
<span className="block">
|
||||
<FollowingCount siteId={site.username} disableList={true} />
|
||||
<FollowingCount
|
||||
characterId={site.characterId}
|
||||
disableList={true}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
{!simple && site?.date_created && (
|
||||
{!simple && site?.createdAt && (
|
||||
<span className="block text-gray-500">
|
||||
<time dateTime={date.formatToISO(site.date_created)}>
|
||||
<time dateTime={date.formatToISO(site.createdAt)}>
|
||||
{t("joined ago", {
|
||||
time: date.dayjs
|
||||
.duration(
|
||||
date
|
||||
.dayjs(site.date_created)
|
||||
.diff(date.dayjs(), "minute"),
|
||||
date.dayjs(site.createdAt).diff(date.dayjs(), "minute"),
|
||||
"minute",
|
||||
)
|
||||
.humanize(),
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import React, { useCallback, useState } from "react"
|
|||
import { Virtuoso } from "react-virtuoso"
|
||||
|
||||
import { Modal } from "~/components/ui/Modal"
|
||||
import { ExpandedCharacter } from "~/lib/types"
|
||||
|
||||
import { Button } from "../ui/Button"
|
||||
import CharacterListItem from "./CharacterListItem"
|
||||
|
|
@ -53,7 +54,8 @@ export const CharacterList: React.FC<{
|
|||
}}
|
||||
data={flattenList}
|
||||
itemContent={(index, sub) => {
|
||||
const character = sub?.character || sub?.fromCharacter
|
||||
const character: ExpandedCharacter =
|
||||
sub?.character || sub?.fromCharacter || sub?.toCharacter
|
||||
return (
|
||||
<CharacterListItem
|
||||
key={index}
|
||||
|
|
|
|||
|
|
@ -5,13 +5,14 @@ import { BlockchainIcon } from "~/components/icons/BlockchainIcon"
|
|||
import { CSB_SCAN } from "~/lib/env"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { noopArr } from "~/lib/noop"
|
||||
import { ExpandedCharacter } from "~/lib/types"
|
||||
|
||||
import { Avatar } from "../ui/Avatar"
|
||||
import { UniLink } from "../ui/UniLink"
|
||||
import { FollowingButton } from "./FollowingButton"
|
||||
|
||||
const CharacterListItem: React.FC<{
|
||||
character: any
|
||||
character: ExpandedCharacter
|
||||
sub: any
|
||||
}> = ({ character, sub }) => {
|
||||
return (
|
||||
|
|
@ -43,17 +44,7 @@ const CharacterListItem: React.FC<{
|
|||
<BlockchainIcon />
|
||||
</UniLink>
|
||||
</div>
|
||||
<FollowingButton
|
||||
site={
|
||||
{
|
||||
username: character?.handle,
|
||||
metadata: {
|
||||
proof: character?.characterId,
|
||||
},
|
||||
} as any
|
||||
}
|
||||
size="sm"
|
||||
/>
|
||||
<FollowingButton site={character} size="sm" />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,16 +3,17 @@ import { Virtuoso } from "react-virtuoso"
|
|||
|
||||
import { CommentInput } from "~/components/common/CommentInput"
|
||||
import { CommentItem } from "~/components/common/CommentItem"
|
||||
import { Note } from "~/lib/types"
|
||||
import { ExpandedNote } from "~/lib/types"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { useGetComments } from "~/queries/page"
|
||||
|
||||
export const Comment: React.FC<{
|
||||
page?: Note | null
|
||||
page?: ExpandedNote
|
||||
className?: string
|
||||
}> = ({ page, className }) => {
|
||||
const comments = useGetComments({
|
||||
pageId: page?.id,
|
||||
characterId: page?.characterId,
|
||||
noteId: page?.noteId,
|
||||
})
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
|
|
@ -37,7 +38,7 @@ export const Comment: React.FC<{
|
|||
)}
|
||||
</span>
|
||||
</div>
|
||||
<CommentInput pageId={page?.id} />
|
||||
<CommentInput characterId={page?.characterId} noteId={page?.noteId} />
|
||||
|
||||
<Virtuoso
|
||||
className="xlog-comment-list"
|
||||
|
|
@ -51,7 +52,8 @@ export const Comment: React.FC<{
|
|||
p?.list?.map((comment, idx) => (
|
||||
<CommentItem
|
||||
className="mt-6"
|
||||
originalId={page?.id}
|
||||
originalCharacterId={page?.characterId}
|
||||
originalNoteId={page?.noteId}
|
||||
comment={comment}
|
||||
key={comment.transactionHash}
|
||||
depth={0}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
import { CharacterEntity, NoteEntity } from "crossbell.js"
|
||||
import { useTranslation } from "next-i18next"
|
||||
import { useRouter } from "next/router"
|
||||
import { useEffect, useState } from "react"
|
||||
import { useEffect } from "react"
|
||||
import { useForm } from "react-hook-form"
|
||||
|
||||
import { useAccountState } from "@crossbell/connect-kit"
|
||||
|
|
@ -10,34 +9,32 @@ import { Popover } from "@headlessui/react"
|
|||
import { Avatar } from "~/components/ui/Avatar"
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { Input } from "~/components/ui/Input"
|
||||
import { Profile } from "~/lib/types"
|
||||
import { useCommentPage, useUpdateComment } from "~/queries/page"
|
||||
import { useAccountSites } from "~/queries/site"
|
||||
|
||||
import { EmojiPicker } from "./EmojiPicker"
|
||||
|
||||
export const CommentInput: React.FC<{
|
||||
pageId?: string
|
||||
originalId?: string
|
||||
characterId?: number
|
||||
noteId?: number
|
||||
originalCharacterId?: number
|
||||
originalNoteId?: number
|
||||
onSubmitted?: () => void
|
||||
comment?: NoteEntity & {
|
||||
character?: CharacterEntity | null
|
||||
}
|
||||
}> = ({ pageId, originalId, onSubmitted, comment }) => {
|
||||
}> = ({
|
||||
characterId,
|
||||
noteId,
|
||||
originalCharacterId,
|
||||
originalNoteId,
|
||||
onSubmitted,
|
||||
comment,
|
||||
}) => {
|
||||
const account = useAccountState((s) => s.computed.account)
|
||||
const userSites = useAccountSites()
|
||||
const commentPage = useCommentPage()
|
||||
const updateComment = useUpdateComment()
|
||||
const router = useRouter()
|
||||
const [viewer, setViewer] = useState<Profile | null>(null)
|
||||
const { t } = useTranslation(["common", "site"])
|
||||
|
||||
useEffect(() => {
|
||||
if (userSites.isSuccess && userSites.data?.length) {
|
||||
setViewer(userSites.data[0])
|
||||
}
|
||||
}, [userSites, router])
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: {
|
||||
content: comment?.metadata?.content?.content || "",
|
||||
|
|
@ -45,24 +42,26 @@ export const CommentInput: React.FC<{
|
|||
})
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
if (pageId) {
|
||||
if (characterId && noteId) {
|
||||
if (comment) {
|
||||
if (values.content) {
|
||||
updateComment.mutate({
|
||||
pageId,
|
||||
content: values.content,
|
||||
externalUrl: window.location.href,
|
||||
originalId,
|
||||
characterId: comment.characterId,
|
||||
noteId: comment.noteId,
|
||||
originalCharacterId,
|
||||
originalNoteId,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
commentPage.mutate({
|
||||
pageId: pageId,
|
||||
characterId,
|
||||
noteId,
|
||||
content: values.content,
|
||||
externalUrl: window.location.href,
|
||||
originalId: originalId,
|
||||
originalCharacterId,
|
||||
originalNoteId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -79,8 +78,8 @@ export const CommentInput: React.FC<{
|
|||
<div className="xlog-comment-input flex">
|
||||
<Avatar
|
||||
className="align-middle mr-3"
|
||||
images={viewer?.avatars || []}
|
||||
name={viewer?.name}
|
||||
images={account?.character?.metadata?.content?.avatars || []}
|
||||
name={account?.character?.metadata?.content?.name}
|
||||
size={45}
|
||||
/>
|
||||
<form className="w-full" onSubmit={handleSubmit}>
|
||||
|
|
@ -88,12 +87,8 @@ export const CommentInput: React.FC<{
|
|||
<Input
|
||||
id="content"
|
||||
isBlock
|
||||
required={
|
||||
!!account && userSites.isSuccess && !!userSites.data?.length
|
||||
}
|
||||
disabled={
|
||||
!account || !userSites.isSuccess || !userSites.data?.length
|
||||
}
|
||||
required={!!account?.character}
|
||||
disabled={!account?.character}
|
||||
multiline
|
||||
maxLength={600}
|
||||
className="mb-2"
|
||||
|
|
@ -125,21 +120,14 @@ export const CommentInput: React.FC<{
|
|||
</Popover>
|
||||
<Button
|
||||
type="submit"
|
||||
isLoading={
|
||||
userSites.isLoading ||
|
||||
commentPage.isLoading ||
|
||||
updateComment.isLoading
|
||||
}
|
||||
isLoading={commentPage.isLoading || updateComment.isLoading}
|
||||
isDisabled={
|
||||
!!account &&
|
||||
userSites.isSuccess &&
|
||||
!!userSites.data?.length &&
|
||||
form.watch("content").trim().length === 0
|
||||
!!account?.character && form.watch("content").trim().length === 0
|
||||
}
|
||||
>
|
||||
{t(
|
||||
account
|
||||
? userSites.isSuccess && !userSites.data?.length
|
||||
? !account.character
|
||||
? "Create Character"
|
||||
: comment
|
||||
? "Confirm Modification"
|
||||
|
|
|
|||
|
|
@ -22,10 +22,11 @@ export const CommentItem: React.FC<{
|
|||
comment: NoteEntity & {
|
||||
character?: CharacterEntity | null
|
||||
}
|
||||
originalId?: string
|
||||
originalCharacterId?: number
|
||||
originalNoteId?: number
|
||||
depth: number
|
||||
className?: string
|
||||
}> = ({ comment, originalId, depth, className }) => {
|
||||
}> = ({ comment, originalCharacterId, originalNoteId, depth, className }) => {
|
||||
const [replyOpen, setReplyOpen] = useState(false)
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
|
||||
|
|
@ -108,7 +109,8 @@ export const CommentItem: React.FC<{
|
|||
>
|
||||
<ReactionLike
|
||||
size="sm"
|
||||
pageId={`${comment.characterId}-${comment.noteId}`}
|
||||
characterId={comment.characterId}
|
||||
noteId={comment.noteId}
|
||||
/>
|
||||
</div>
|
||||
{depth < 2 && (
|
||||
|
|
@ -137,8 +139,10 @@ export const CommentItem: React.FC<{
|
|||
{replyOpen && (
|
||||
<div className="pt-6">
|
||||
<CommentInput
|
||||
originalId={originalId}
|
||||
pageId={`${comment.characterId}-${comment.noteId}`}
|
||||
originalCharacterId={originalCharacterId}
|
||||
originalNoteId={originalNoteId}
|
||||
characterId={comment.characterId}
|
||||
noteId={comment.noteId}
|
||||
onSubmitted={() => setReplyOpen(false)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -146,8 +150,10 @@ export const CommentItem: React.FC<{
|
|||
{editOpen && (
|
||||
<div className="pt-6">
|
||||
<CommentInput
|
||||
originalId={originalId}
|
||||
pageId={`${comment.characterId}-${comment.noteId}`}
|
||||
originalCharacterId={originalCharacterId}
|
||||
originalNoteId={originalNoteId}
|
||||
characterId={comment.characterId}
|
||||
noteId={comment.noteId}
|
||||
onSubmitted={() => setEditOpen(false)}
|
||||
comment={comment}
|
||||
/>
|
||||
|
|
@ -164,7 +170,8 @@ export const CommentItem: React.FC<{
|
|||
},
|
||||
) => (
|
||||
<CommentItem
|
||||
originalId={originalId}
|
||||
originalCharacterId={originalCharacterId}
|
||||
originalNoteId={originalNoteId}
|
||||
comment={subcomment}
|
||||
key={subcomment.transactionHash}
|
||||
depth={depth + 1}
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ import { Menu } from "~/components/ui/Menu"
|
|||
import { SITE_URL } from "~/lib/env"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { useAccountSites } from "~/queries/site"
|
||||
|
||||
import { UniLink } from "../ui/UniLink"
|
||||
|
||||
|
|
@ -90,8 +89,6 @@ export const ConnectButton: React.FC<{
|
|||
const selectCharactersModal = useSelectCharactersModal()
|
||||
const walletMintNewCharacterModal = useWalletMintNewCharacterModal()
|
||||
|
||||
const userSites = useAccountSites()
|
||||
|
||||
const showNotificationModal = useShowNotificationModal()
|
||||
const { isAllRead } = useNotifications()
|
||||
|
||||
|
|
@ -112,12 +109,12 @@ export const ConnectButton: React.FC<{
|
|||
}, [balance])
|
||||
|
||||
const dropdownLinks: HeaderLinkType[] = [
|
||||
userSites.data?.[0]?.username
|
||||
account?.character
|
||||
? {
|
||||
icon: "icon-[mingcute--home-1-line]",
|
||||
label: t("My xLog") || "",
|
||||
url: getSiteLink({
|
||||
subdomain: userSites.data?.[0]?.username,
|
||||
subdomain: account?.character?.handle || "",
|
||||
}),
|
||||
}
|
||||
: {
|
||||
|
|
@ -219,115 +216,107 @@ export const ConnectButton: React.FC<{
|
|||
className="relative flex items-center -mr-2"
|
||||
style={{ height: avatarSize + "px" }}
|
||||
>
|
||||
{userSites.isSuccess ? (
|
||||
{!hideNotification && (
|
||||
<>
|
||||
{!hideNotification && (
|
||||
<>
|
||||
{isAllRead ? (
|
||||
<BellIcon
|
||||
className={`${
|
||||
size === "base" ? "w-6 h-6" : "w-5 h-5"
|
||||
} text-zinc-500 cursor-pointer sm:hover:animate-buzz-out`}
|
||||
onClick={showNotificationModal}
|
||||
/>
|
||||
) : (
|
||||
<BellAlertIcon
|
||||
className={`${
|
||||
size === "base" ? "w-6 h-6" : "w-5 h-5"
|
||||
} text-accent cursor-pointer sm:hover:animate-buzz-out`}
|
||||
onClick={showNotificationModal}
|
||||
/>
|
||||
)}
|
||||
<div className="h-full w-[2px] py-1 ml-3">
|
||||
<div className="w-full h-full bg-zinc-200 rounded-full"></div>
|
||||
</div>
|
||||
</>
|
||||
{isAllRead ? (
|
||||
<BellIcon
|
||||
className={`${
|
||||
size === "base" ? "w-6 h-6" : "w-5 h-5"
|
||||
} text-zinc-500 cursor-pointer sm:hover:animate-buzz-out`}
|
||||
onClick={showNotificationModal}
|
||||
/>
|
||||
) : (
|
||||
<BellAlertIcon
|
||||
className={`${
|
||||
size === "base" ? "w-6 h-6" : "w-5 h-5"
|
||||
} text-accent cursor-pointer sm:hover:animate-buzz-out`}
|
||||
onClick={showNotificationModal}
|
||||
/>
|
||||
)}
|
||||
<Menu
|
||||
placement="bottom-end"
|
||||
target={
|
||||
<button
|
||||
className="flex items-center w-full hover:bg-hover transition-colors py-1 px-2 rounded-lg ml-1"
|
||||
type="button"
|
||||
aria-label="connector"
|
||||
>
|
||||
<Avatar
|
||||
className="align-middle"
|
||||
images={userSites.data?.[0]?.avatars || []}
|
||||
name={userSites.data?.[0]?.name}
|
||||
size={avatarSize}
|
||||
/>
|
||||
{!hideName && (
|
||||
<>
|
||||
<div
|
||||
className={`flex-1 flex-col min-w-0 ml-2 max-w-[100px] ${
|
||||
mobileSimplification ? "hidden sm:flex" : "flex"
|
||||
<div className="h-full w-[2px] py-1 ml-3">
|
||||
<div className="w-full h-full bg-zinc-200 rounded-full"></div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
<Menu
|
||||
placement="bottom-end"
|
||||
target={
|
||||
<button
|
||||
className="flex items-center w-full hover:bg-hover transition-colors py-1 px-2 rounded-lg ml-1"
|
||||
type="button"
|
||||
aria-label="connector"
|
||||
>
|
||||
<Avatar
|
||||
className="align-middle"
|
||||
images={account.character?.metadata?.content?.avatars || []}
|
||||
name={account.character?.metadata?.content?.name}
|
||||
size={avatarSize}
|
||||
/>
|
||||
{!hideName && (
|
||||
<>
|
||||
<div
|
||||
className={`flex-1 flex-col min-w-0 ml-2 max-w-[100px] ${
|
||||
mobileSimplification ? "hidden sm:flex" : "flex"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`text-left leading-none font-medium truncate ${
|
||||
InsufficientBalance
|
||||
? "text-red-600"
|
||||
: "text-gray-600"
|
||||
} ${size === "base" ? "text-base" : "text-sm"}`}
|
||||
style={{ marginBottom: "0.15rem" }}
|
||||
>
|
||||
{account.character?.metadata?.content?.name ||
|
||||
getAccountDisplayName(account)}
|
||||
</span>
|
||||
{account.character?.handle && (
|
||||
<span
|
||||
className={`text-left leading-none ${
|
||||
sizeDecrease === "sm" ? "text-sm" : "text-xs"
|
||||
} truncate ${
|
||||
InsufficientBalance
|
||||
? "text-red-400"
|
||||
: "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`text-left leading-none font-medium truncate ${
|
||||
InsufficientBalance
|
||||
? "text-red-600"
|
||||
: "text-gray-600"
|
||||
} ${size === "base" ? "text-base" : "text-sm"}`}
|
||||
style={{ marginBottom: "0.15rem" }}
|
||||
>
|
||||
{userSites.data?.[0]?.name ||
|
||||
getAccountDisplayName(account)}
|
||||
</span>
|
||||
{userSites.data?.[0]?.username && (
|
||||
<span
|
||||
className={`text-left leading-none ${
|
||||
sizeDecrease === "sm" ? "text-sm" : "text-xs"
|
||||
} truncate ${
|
||||
InsufficientBalance
|
||||
? "text-red-400"
|
||||
: "text-gray-400"
|
||||
}`}
|
||||
>
|
||||
{"@" + userSites.data?.[0]?.username ||
|
||||
getAccountDisplayName(account)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<i className="icon-[mingcute--down-line] text-xl ml-[2px]" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
dropdown={
|
||||
<div
|
||||
className={`text-gray-600 bg-white rounded-lg ring-1 ring-border min-w-[140px] shadow-md py-2 ${
|
||||
size === "base" ? "text-base" : "text-sm"
|
||||
} mt-1`}
|
||||
>
|
||||
{dropdownLinks.map((link, i) => {
|
||||
return (
|
||||
<UniLink
|
||||
key={i}
|
||||
href={link.url}
|
||||
onClick={link.onClick}
|
||||
className={`${
|
||||
size === "base"
|
||||
? "pl-5 pr-6 h-11"
|
||||
: "pl-4 pr-5 h-9"
|
||||
} flex items-center w-full whitespace-nowrap hover:bg-hover`}
|
||||
aria-label={link.label}
|
||||
>
|
||||
<span className="mr-2 flex justify-center">
|
||||
<i className={cn(link.icon, "text-base")} />
|
||||
</span>
|
||||
{link.label}
|
||||
</UniLink>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
{"@" + account.character?.handle ||
|
||||
getAccountDisplayName(account)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<i className="icon-[mingcute--down-line] text-xl ml-[2px]" />
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
}
|
||||
dropdown={
|
||||
<div
|
||||
className={`text-gray-600 bg-white rounded-lg ring-1 ring-border min-w-[140px] shadow-md py-2 ${
|
||||
size === "base" ? "text-base" : "text-sm"
|
||||
} mt-1`}
|
||||
>
|
||||
{dropdownLinks.map((link, i) => {
|
||||
return (
|
||||
<UniLink
|
||||
key={i}
|
||||
href={link.url}
|
||||
onClick={link.onClick}
|
||||
className={`${
|
||||
size === "base" ? "pl-5 pr-6 h-11" : "pl-4 pr-5 h-9"
|
||||
} flex items-center w-full whitespace-nowrap hover:bg-hover`}
|
||||
aria-label={link.label}
|
||||
>
|
||||
<span className="mr-2 flex justify-center">
|
||||
<i className={cn(link.icon, "text-base")} />
|
||||
</span>
|
||||
{link.label}
|
||||
</UniLink>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})()}
|
||||
|
|
|
|||
|
|
@ -6,17 +6,16 @@ import { Button } from "~/components/ui/Button"
|
|||
import type { Variant } from "~/components/ui/Button"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { SITE_URL } from "~/lib/env"
|
||||
import { Profile } from "~/lib/types"
|
||||
import { ExpandedCharacter } from "~/lib/types"
|
||||
import { cn } from "~/lib/utils"
|
||||
import {
|
||||
useAccountSites,
|
||||
useGetSubscription,
|
||||
useSubscribeToSite,
|
||||
useUnsubscribeFromSite,
|
||||
} from "~/queries/site"
|
||||
|
||||
export const FollowingButton: React.FC<{
|
||||
site: Profile | undefined | null
|
||||
site?: ExpandedCharacter
|
||||
variant?: Variant
|
||||
className?: string
|
||||
size?: "sm" | "xl"
|
||||
|
|
@ -24,27 +23,25 @@ export const FollowingButton: React.FC<{
|
|||
}> = ({ site, variant, className, size, loadingStatusChange }) => {
|
||||
const subscribeToSite = useSubscribeToSite()
|
||||
const unsubscribeFromSite = useUnsubscribeFromSite()
|
||||
const userSite = useAccountSites()
|
||||
const characterId = site?.metadata?.proof ? Number(site.metadata.proof) : null
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
const handleClickSubscribe = () => {
|
||||
if (characterId) {
|
||||
if (site?.characterId) {
|
||||
if (subscription.data) {
|
||||
unsubscribeFromSite.mutate({
|
||||
characterId,
|
||||
siteId: site?.username,
|
||||
characterId: site?.characterId,
|
||||
siteId: site?.handle,
|
||||
} as any)
|
||||
} else {
|
||||
subscribeToSite.mutate({
|
||||
characterId,
|
||||
siteId: site?.username,
|
||||
characterId: site?.characterId,
|
||||
siteId: site?.handle,
|
||||
} as any)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const subscription = useGetSubscription(site?.username)
|
||||
const subscription = useGetSubscription(site?.characterId)
|
||||
|
||||
useEffect(() => {
|
||||
if (subscribeToSite.isError) {
|
||||
|
|
@ -99,8 +96,7 @@ export const FollowingButton: React.FC<{
|
|||
isLoading={
|
||||
subscription.data
|
||||
? unsubscribeFromSite.isLoading || subscribeToSite.isLoading
|
||||
: userSite.isLoading ||
|
||||
unsubscribeFromSite.isLoading ||
|
||||
: unsubscribeFromSite.isLoading ||
|
||||
subscribeToSite.isLoading ||
|
||||
subscription.isLoading
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,18 +9,18 @@ import {
|
|||
} from "~/queries/site"
|
||||
|
||||
export const FollowingCount: React.FC<{
|
||||
siteId?: string
|
||||
characterId?: number
|
||||
disableList?: boolean
|
||||
}> = ({ siteId, disableList }) => {
|
||||
}> = ({ characterId, disableList }) => {
|
||||
let [isFollowListOpen, setIsFollowListOpen] = useState(false)
|
||||
let [isToFollowListOpen, setIsToFollowListOpen] = useState(false)
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
const subscriptions = useGetSiteSubscriptions({
|
||||
siteId: siteId || "",
|
||||
characterId,
|
||||
})
|
||||
const toSubscriptions = useGetSiteToSubscriptions({
|
||||
siteId: siteId || "",
|
||||
characterId,
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
@ -34,7 +34,7 @@ export const FollowingCount: React.FC<{
|
|||
onClick={() => setIsFollowListOpen(true)}
|
||||
>
|
||||
<span className="font-medium text-zinc-700 pr-[2px]">
|
||||
{subscriptions.data?.pages?.[0]?.total || 0}
|
||||
{subscriptions.data?.pages?.[0]?.count || 0}
|
||||
</span>{" "}
|
||||
{t("Followers")}
|
||||
</Button>
|
||||
|
|
@ -47,7 +47,7 @@ export const FollowingCount: React.FC<{
|
|||
onClick={() => setIsToFollowListOpen(true)}
|
||||
>
|
||||
<span className="font-medium text-zinc-700 pr-[2px]">
|
||||
{toSubscriptions.data?.pages?.[0]?.total || 0}
|
||||
{toSubscriptions.data?.pages?.[0]?.count || 0}
|
||||
</span>{" "}
|
||||
{t("Followings")}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@ import { useState } from "react"
|
|||
|
||||
import { PatronModal } from "~/components/common/PatronModal"
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { Profile } from "~/lib/types"
|
||||
import { ExpandedCharacter } from "~/lib/types"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
export const PatronButton: React.FC<{
|
||||
site: Profile | undefined | null
|
||||
site?: ExpandedCharacter
|
||||
className?: string
|
||||
size?: "sm" | "xl"
|
||||
loadingStatusChange?: (status: boolean) => void
|
||||
|
|
|
|||
|
|
@ -11,8 +11,7 @@ import { Button } from "~/components/ui/Button"
|
|||
import { Modal } from "~/components/ui/Modal"
|
||||
import { CSB_SCAN, MIRA_LINK } from "~/lib/env"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { Note, Profile } from "~/lib/types"
|
||||
import { parsePageId } from "~/models/page.model"
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
import { useGetTips, useTipCharacter } from "~/queries/site"
|
||||
|
||||
import { Tabs } from "../ui/Tabs"
|
||||
|
|
@ -20,17 +19,23 @@ import { UniLink } from "../ui/UniLink"
|
|||
import { CharacterFloatCard } from "./CharacterFloatCard"
|
||||
|
||||
export const PatronModal: React.FC<{
|
||||
site: Profile | undefined | null
|
||||
page?: Note | null
|
||||
site?: ExpandedCharacter
|
||||
page?: ExpandedNote
|
||||
open: boolean
|
||||
setOpen: (open: boolean) => void
|
||||
}> = ({ site, page, open, setOpen }) => {
|
||||
const { t } = useTranslation("common")
|
||||
const tipCharacter = useTipCharacter()
|
||||
const tips = useGetTips({
|
||||
toCharacterId: site?.metadata?.proof,
|
||||
toNoteId: parsePageId(page?.id || "").noteId,
|
||||
})
|
||||
const tips = useGetTips(
|
||||
page
|
||||
? {
|
||||
toCharacterId: page?.characterId,
|
||||
toNoteId: page?.noteId,
|
||||
}
|
||||
: {
|
||||
toCharacterId: site?.characterId,
|
||||
},
|
||||
)
|
||||
const connectModal = useConnectModal()
|
||||
|
||||
const radios = [
|
||||
|
|
@ -66,12 +71,12 @@ export const PatronModal: React.FC<{
|
|||
)
|
||||
|
||||
const submit = () => {
|
||||
if (currentCharacterId && site?.metadata?.proof && parseInt(value)) {
|
||||
if (currentCharacterId && site?.characterId && parseInt(value)) {
|
||||
tipCharacter.mutate({
|
||||
fromCharacterId: currentCharacterId,
|
||||
toCharacterId: site?.metadata?.proof,
|
||||
toCharacterId: site?.characterId,
|
||||
amount: parseInt(value),
|
||||
noteId: parsePageId(page?.id || "").noteId,
|
||||
noteId: page?.noteId,
|
||||
})
|
||||
} else {
|
||||
setOpen(false)
|
||||
|
|
@ -100,7 +105,7 @@ export const PatronModal: React.FC<{
|
|||
tipCharacter.reset()
|
||||
}
|
||||
}
|
||||
}, [tipCharacter.isSuccess, t, site?.name])
|
||||
}, [tipCharacter.isSuccess, t])
|
||||
|
||||
useEffect(() => {
|
||||
if (tipCharacter.isError) {
|
||||
|
|
@ -112,10 +117,10 @@ export const PatronModal: React.FC<{
|
|||
const title =
|
||||
(page
|
||||
? t("Tip the post: {{name}}", {
|
||||
name: page.title,
|
||||
name: page.metadata?.content?.title,
|
||||
})
|
||||
: t("Become a patron of {{name}}", {
|
||||
name: site?.name,
|
||||
name: site?.metadata?.content?.name,
|
||||
})) || ""
|
||||
|
||||
return (
|
||||
|
|
@ -135,17 +140,21 @@ export const PatronModal: React.FC<{
|
|||
<div className="px-5 py-4 space-y-4 text-center">
|
||||
<div className="space-y-1">
|
||||
<span className="flex items-center justify-center">
|
||||
<Avatar images={site?.avatars || []} name={site?.name} size={100} />
|
||||
<Avatar
|
||||
images={site?.metadata?.content?.avatars || []}
|
||||
name={site?.metadata?.content?.name}
|
||||
size={100}
|
||||
/>
|
||||
</span>
|
||||
<span className="block">
|
||||
<span className="font-bold text-lg text-zinc-800">
|
||||
{site?.name}
|
||||
{site?.metadata?.content?.name}
|
||||
</span>
|
||||
<span className="ml-1 text-gray-600">@{site?.username}</span>
|
||||
<span className="ml-1 text-gray-600">@{site?.handle}</span>
|
||||
</span>
|
||||
{site?.description && (
|
||||
{site?.metadata?.content?.bio && (
|
||||
<span className="text-gray-600 text-sm line-clamp-4">
|
||||
{site?.description}
|
||||
{site?.metadata?.content?.bio}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import { Tooltip } from "~/components/ui/Tooltip"
|
|||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { CSB_SCAN } from "~/lib/env"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { parsePageId } from "~/models/page.model"
|
||||
import {
|
||||
useCheckLike,
|
||||
useGetLikeCounts,
|
||||
|
|
@ -21,8 +20,9 @@ import { Button } from "../ui/Button"
|
|||
|
||||
export const ReactionLike: React.FC<{
|
||||
size?: "sm" | "base"
|
||||
pageId?: string
|
||||
}> = ({ size, pageId }) => {
|
||||
characterId?: number
|
||||
noteId?: number
|
||||
}> = ({ size, characterId, noteId }) => {
|
||||
const toggleLikePage = useToggleLikePage()
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
|
|
@ -30,29 +30,44 @@ export const ReactionLike: React.FC<{
|
|||
const [isLikeListOpen, setIsLikeListOpen] = useState(false)
|
||||
const likeRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const [likes, likesMutation] = useGetLikes({ pageId })
|
||||
const [likeStatus] = useCheckLike({
|
||||
pageId,
|
||||
const [likes, likesMutation] = useGetLikes({
|
||||
characterId,
|
||||
noteId,
|
||||
})
|
||||
const [likeStatus] = useCheckLike({
|
||||
characterId,
|
||||
noteId,
|
||||
})
|
||||
const { data: likeCount = 0 } = useGetLikeCounts({
|
||||
characterId,
|
||||
noteId,
|
||||
})
|
||||
const { data: likeCount = 0 } = useGetLikeCounts({ pageId })
|
||||
|
||||
const [isUnlikeOpen, setIsUnlikeOpen] = useState(false)
|
||||
|
||||
const like = () => {
|
||||
if (pageId) {
|
||||
if (characterId && noteId) {
|
||||
if (likeStatus.isLiked) {
|
||||
setIsLikeOpen(true)
|
||||
} else {
|
||||
toggleLikePage.mutate({ ...parsePageId(pageId), action: "link" })
|
||||
toggleLikePage.mutate({
|
||||
characterId,
|
||||
noteId,
|
||||
action: "link",
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const unlike = () => {
|
||||
if (pageId) {
|
||||
if (characterId && noteId) {
|
||||
setIsUnlikeOpen(false)
|
||||
if (likeStatus.isLiked) {
|
||||
toggleLikePage.mutate({ ...parsePageId(pageId), action: "unlink" })
|
||||
toggleLikePage.mutate({
|
||||
noteId,
|
||||
characterId,
|
||||
action: "unlink",
|
||||
})
|
||||
} // else do nothing
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import { UniLink } from "~/components/ui/UniLink"
|
|||
import { CSB_SCAN, CSB_XCHAR } from "~/lib/env"
|
||||
import { noopArr } from "~/lib/noop"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { parsePageId } from "~/models/page.model"
|
||||
import { useCheckMint, useGetMints, useMintPage } from "~/queries/page"
|
||||
|
||||
import { AvatarStack } from "../ui/AvatarStack"
|
||||
|
|
@ -20,8 +19,9 @@ import { Button } from "../ui/Button"
|
|||
|
||||
export const ReactionMint: React.FC<{
|
||||
size?: "sm" | "base"
|
||||
pageId?: string
|
||||
}> = ({ size, pageId }) => {
|
||||
noteId?: number
|
||||
characterId?: number
|
||||
}> = ({ size, noteId, characterId }) => {
|
||||
const mintPage = useMintPage()
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
|
|
@ -32,17 +32,24 @@ export const ReactionMint: React.FC<{
|
|||
const mintRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const mints = useGetMints({
|
||||
pageId: pageId,
|
||||
characterId,
|
||||
noteId,
|
||||
includeCharacter: size !== "sm",
|
||||
})
|
||||
const isMint = useCheckMint(pageId)
|
||||
const isMint = useCheckMint({
|
||||
characterId,
|
||||
noteId,
|
||||
})
|
||||
|
||||
const mint = () => {
|
||||
if (pageId) {
|
||||
if (characterId && noteId) {
|
||||
if (isMint.data?.count) {
|
||||
setIsMintOpen(true)
|
||||
} else {
|
||||
mintPage.mutate(parsePageId(pageId))
|
||||
mintPage.mutate({
|
||||
characterId,
|
||||
noteId,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -77,11 +84,11 @@ export const ReactionMint: React.FC<{
|
|||
const avatars = useMemo(
|
||||
() =>
|
||||
mints.data?.pages?.[0]?.list
|
||||
?.sort((a, b: any) =>
|
||||
?.sort((a, b) =>
|
||||
b.character?.metadata?.content?.avatars?.[0] ? 1 : -1,
|
||||
)
|
||||
.slice(0, 3)
|
||||
.map((mint: any) => ({
|
||||
.map((mint) => ({
|
||||
images: mint.character?.metadata?.content?.avatars,
|
||||
name: mint.character?.metadata?.content?.name,
|
||||
})) || noopArr,
|
||||
|
|
|
|||
|
|
@ -6,18 +6,18 @@ import { useAccountState } from "@crossbell/connect-kit"
|
|||
import { PatronModal } from "~/components/common/PatronModal"
|
||||
import { Tooltip } from "~/components/ui/Tooltip"
|
||||
import { noopArr } from "~/lib/noop"
|
||||
import { Note, Profile } from "~/lib/types"
|
||||
import { parsePageId } from "~/models/page.model"
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
import { useGetTips } from "~/queries/site"
|
||||
|
||||
import { AvatarStack } from "../ui/AvatarStack"
|
||||
import { Button } from "../ui/Button"
|
||||
|
||||
export const ReactionTip: React.FC<{
|
||||
pageId?: string
|
||||
site?: Profile | null
|
||||
page?: Note | null
|
||||
}> = ({ pageId, site, page }) => {
|
||||
characterId?: number
|
||||
noteId?: number
|
||||
site?: ExpandedCharacter
|
||||
page?: ExpandedNote
|
||||
}> = ({ characterId, noteId, site, page }) => {
|
||||
const { t } = useTranslation("common")
|
||||
|
||||
const account = useAccountState((s) => s.computed.account)
|
||||
|
|
@ -25,7 +25,6 @@ export const ReactionTip: React.FC<{
|
|||
const [isTipOpen, setIsTipOpen] = useState(false)
|
||||
const tipRef = useRef<HTMLButtonElement>(null)
|
||||
|
||||
const { characterId, noteId } = parsePageId(pageId || "")
|
||||
const tips = useGetTips({
|
||||
toCharacterId: characterId,
|
||||
toNoteId: noteId,
|
||||
|
|
@ -37,7 +36,7 @@ export const ReactionTip: React.FC<{
|
|||
})
|
||||
|
||||
const tip = () => {
|
||||
if (pageId) {
|
||||
if (characterId && noteId) {
|
||||
setIsTipOpen(true)
|
||||
}
|
||||
}
|
||||
|
|
@ -45,11 +44,11 @@ export const ReactionTip: React.FC<{
|
|||
const avatars = useMemo(
|
||||
() =>
|
||||
tips.data?.pages?.[0]?.list
|
||||
?.sort((a, b: any) =>
|
||||
?.sort((a, b) =>
|
||||
b.character?.metadata?.content?.avatars?.[0] ? 1 : -1,
|
||||
)
|
||||
.slice(0, 3)
|
||||
.map((mint: any) => ({
|
||||
.map((mint) => ({
|
||||
images: mint.character?.metadata?.content?.avatars,
|
||||
name: mint.character?.metadata?.content?.name,
|
||||
})) || noopArr,
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import { toGateway } from "~/lib/ipfs-parser"
|
|||
import { getStorage } from "~/lib/storage"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { useGetPagesBySite } from "~/queries/page"
|
||||
import { useAccountSites, useGetSite } from "~/queries/site"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
|
||||
import { SEOHead } from "../common/SEOHead"
|
||||
import { UniLink } from "../ui/UniLink"
|
||||
|
|
@ -36,12 +36,11 @@ export function DashboardLayout({
|
|||
const router = useRouter()
|
||||
const subdomain = router.query.subdomain as string
|
||||
const site = useGetSite(subdomain)
|
||||
const userSite = useAccountSites()
|
||||
|
||||
const userRole = useUserRole(subdomain)
|
||||
const [ssrReady, isConnected] = useAccountState(({ ssrReady, computed }) => [
|
||||
const [ssrReady, account] = useAccountState(({ ssrReady, computed }) => [
|
||||
ssrReady,
|
||||
!!computed.account,
|
||||
computed.account,
|
||||
])
|
||||
const connectModal = useConnectModal()
|
||||
const [ready, setReady] = React.useState(false)
|
||||
|
|
@ -52,7 +51,7 @@ export function DashboardLayout({
|
|||
|
||||
useEffect(() => {
|
||||
if (ssrReady) {
|
||||
if (!isConnected) {
|
||||
if (!account) {
|
||||
setReady(false)
|
||||
setHasPermission(false)
|
||||
connectModal.show()
|
||||
|
|
@ -65,22 +64,22 @@ export function DashboardLayout({
|
|||
}
|
||||
}
|
||||
}
|
||||
}, [ssrReady, userRole.isSuccess, userRole.data, isConnected, connectModal])
|
||||
}, [ssrReady, userRole.isSuccess, userRole.data, account, connectModal])
|
||||
|
||||
const showNotificationModal = useShowNotificationModal()
|
||||
const { isAllRead } = useNotifications()
|
||||
|
||||
const pages = useGetPagesBySite({
|
||||
type: "post",
|
||||
site: "xlog-events",
|
||||
take: 1,
|
||||
characterId: 50153,
|
||||
limit: 1,
|
||||
})
|
||||
const [isEventsAllRead, setIsEventsAllRead] = React.useState(true)
|
||||
const latestEventRead = getStorage("latestEventRead")?.value || 0
|
||||
useEffect(() => {
|
||||
if (pages.isSuccess) {
|
||||
if (
|
||||
new Date(pages.data.pages[0].list?.[0].date_created) >
|
||||
new Date(pages.data.pages[0].list?.[0].createdAt) >
|
||||
new Date(latestEventRead)
|
||||
) {
|
||||
setIsEventsAllRead(false)
|
||||
|
|
@ -169,13 +168,15 @@ export function DashboardLayout({
|
|||
hasPermission ? (
|
||||
<>
|
||||
<SEOHead title={t(title) || ""} siteName={APP_NAME} />
|
||||
{site?.data?.css && (
|
||||
{site?.data?.metadata?.content?.css && (
|
||||
<link
|
||||
type="text/css"
|
||||
rel="stylesheet"
|
||||
href={
|
||||
"data:text/css;base64," +
|
||||
Buffer.from(toGateway(site.data.css)).toString("base64")
|
||||
Buffer.from(toGateway(site.data.metadata?.content?.css)).toString(
|
||||
"base64",
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
|
@ -274,23 +275,25 @@ export function DashboardLayout({
|
|||
</div>
|
||||
{isOpen && "xLog"}
|
||||
</div>
|
||||
{userSite.data?.[0]?.username &&
|
||||
{account?.character?.handle &&
|
||||
subdomain &&
|
||||
userSite.data[0].username !== subdomain && (
|
||||
account?.character?.handle !== subdomain && (
|
||||
<div className="mb-2 px-5 pt-3 pb-2 bg-orange-50 text-center">
|
||||
<div className="mb-2">
|
||||
{isOpen && "You are operating"}
|
||||
</div>
|
||||
<Avatar
|
||||
images={site.data?.avatars || []}
|
||||
images={site.data?.metadata?.content?.avatars || []}
|
||||
size={isOpen ? 60 : 40}
|
||||
name={site.data?.name}
|
||||
name={site.data?.metadata?.content?.name}
|
||||
/>
|
||||
{isOpen && (
|
||||
<span className="flex flex-col justify-center">
|
||||
<span className="block">{site.data?.name}</span>
|
||||
<span className="block">
|
||||
{site.data?.metadata?.content?.name}
|
||||
</span>
|
||||
<span className="block text-sm text-zinc-400">
|
||||
@{site.data?.username}
|
||||
@{site.data?.handle}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ import { useDate } from "~/hooks/useDate"
|
|||
import { getPageVisibility } from "~/lib/page-helpers"
|
||||
import { readFiles } from "~/lib/read-files"
|
||||
import { setStorage } from "~/lib/storage"
|
||||
import { PageVisibilityEnum } from "~/lib/types"
|
||||
import { ExpandedNote, PageVisibilityEnum } from "~/lib/types"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { useGetPagesBySite } from "~/queries/page"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
|
||||
import { Button } from "../ui/Button"
|
||||
import { EmptyState } from "../ui/EmptyState"
|
||||
|
|
@ -29,6 +30,7 @@ export const PagesManager: React.FC<{
|
|||
}> = ({ isPost }) => {
|
||||
const router = useRouter()
|
||||
const subdomain = router.query.subdomain as string
|
||||
const site = useGetSite(subdomain)
|
||||
|
||||
const visibility = useMemo<PageVisibilityEnum>(
|
||||
() =>
|
||||
|
|
@ -43,13 +45,13 @@ export const PagesManager: React.FC<{
|
|||
|
||||
const pages = useGetPagesBySite({
|
||||
type: isPost ? "post" : "page",
|
||||
site: subdomain!,
|
||||
take: 100,
|
||||
characterId: site.data?.characterId,
|
||||
limit: 100,
|
||||
visibility,
|
||||
})
|
||||
|
||||
// Batch selections
|
||||
const [batchSelected, setBatchSelected] = useState<string[]>([])
|
||||
const [batchSelected, setBatchSelected] = useState<(string | number)[]>([])
|
||||
|
||||
const tabItems: TabItem[] = [
|
||||
{
|
||||
|
|
@ -68,10 +70,6 @@ export const PagesManager: React.FC<{
|
|||
value: PageVisibilityEnum.Scheduled,
|
||||
text: "Scheduled",
|
||||
},
|
||||
{
|
||||
value: PageVisibilityEnum.Crossbell,
|
||||
text: "Others on Crossbell",
|
||||
},
|
||||
].map((item) => ({
|
||||
text: item.text,
|
||||
onClick: () => {
|
||||
|
|
@ -90,10 +88,10 @@ export const PagesManager: React.FC<{
|
|||
active: item.value === visibility,
|
||||
}))
|
||||
|
||||
const getPageEditLink = (page: { id: string }) => {
|
||||
return `/dashboard/${subdomain}/editor?id=${page.id}&type=${
|
||||
isPost ? "post" : "page"
|
||||
}`
|
||||
const getPageEditLink = (page: ExpandedNote) => {
|
||||
return `/dashboard/${subdomain}/editor?id=${
|
||||
page.noteId || page.draftKey
|
||||
}&type=${isPost ? "post" : "page"}`
|
||||
}
|
||||
|
||||
const queryClient = useQueryClient()
|
||||
|
|
@ -106,7 +104,7 @@ export const PagesManager: React.FC<{
|
|||
const file = (await readFiles(e.target?.files))?.[0]
|
||||
if (file) {
|
||||
const id = nanoid()
|
||||
const key = `draft-${subdomain}-local-${id}`
|
||||
const key = `draft-${site.data?.characterId}-local-${id}`
|
||||
setStorage(key, {
|
||||
date: +new Date(),
|
||||
values: {
|
||||
|
|
@ -119,7 +117,10 @@ export const PagesManager: React.FC<{
|
|||
},
|
||||
isPost: isPost,
|
||||
})
|
||||
queryClient.invalidateQueries(["getPagesBySite", subdomain])
|
||||
queryClient.invalidateQueries([
|
||||
"getPagesBySite",
|
||||
site.data?.characterId,
|
||||
])
|
||||
router.push(
|
||||
`/dashboard/${subdomain}/editor?id=local-${id}&type=${
|
||||
isPost ? "post" : "page"
|
||||
|
|
@ -220,7 +221,7 @@ export const PagesManager: React.FC<{
|
|||
!!tabItems.find((item) => item.text === "Others on Crossbell") // Not sure if there are better ways
|
||||
?.active
|
||||
}
|
||||
pages={pages}
|
||||
pages={pages.data}
|
||||
batchSelected={batchSelected}
|
||||
setBatchSelected={setBatchSelected}
|
||||
/>
|
||||
|
|
@ -229,7 +230,7 @@ export const PagesManager: React.FC<{
|
|||
)}
|
||||
|
||||
<div className="-mt-3">
|
||||
{!pages.data?.pages?.[0].total && (
|
||||
{!pages.data?.pages?.[0].count && (
|
||||
<EmptyState resource={isPost ? "posts" : "pages"} />
|
||||
)}
|
||||
|
||||
|
|
@ -238,7 +239,7 @@ export const PagesManager: React.FC<{
|
|||
currentLength++
|
||||
return (
|
||||
<Link
|
||||
key={page.id}
|
||||
key={page.transactionHash}
|
||||
href={getPageEditLink(page)}
|
||||
className="group relative hover:bg-zinc-100 rounded-lg py-3 px-3 transition-colors -mx-3 flex"
|
||||
>
|
||||
|
|
@ -246,27 +247,39 @@ export const PagesManager: React.FC<{
|
|||
<button
|
||||
className={cn(
|
||||
`text-gray-400 relative z-10 w-8 h-8 rounded inline-flex group-hover:visible justify-center items-center`,
|
||||
batchSelected.includes(page.id)
|
||||
batchSelected.includes(page.noteId || page.draftKey || 0)
|
||||
? "bg-gray-200"
|
||||
: `hover:bg-gray-200`,
|
||||
)}
|
||||
onClick={(e) => {
|
||||
e.preventDefault()
|
||||
// Toggle selection
|
||||
if (batchSelected.includes(page.id)) {
|
||||
if (
|
||||
batchSelected.includes(
|
||||
page.noteId || page.draftKey || 0,
|
||||
)
|
||||
) {
|
||||
// Deselect
|
||||
setBatchSelected(
|
||||
batchSelected.filter((pageId) => pageId !== page.id),
|
||||
batchSelected.filter(
|
||||
(pageId) =>
|
||||
pageId !== page.noteId || page.draftKey || 0,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
// Do select
|
||||
setBatchSelected([...batchSelected, page.id])
|
||||
setBatchSelected([
|
||||
...batchSelected,
|
||||
page.noteId || page.draftKey || 0,
|
||||
])
|
||||
}
|
||||
}}
|
||||
>
|
||||
<i
|
||||
className={`${
|
||||
batchSelected.includes(page.id)
|
||||
batchSelected.includes(
|
||||
page.noteId || page.draftKey || 0,
|
||||
)
|
||||
? "icon-[mingcute--check-line]"
|
||||
: isPost
|
||||
? "icon-[mingcute--news-line]"
|
||||
|
|
@ -276,13 +289,13 @@ export const PagesManager: React.FC<{
|
|||
</button>
|
||||
</div>
|
||||
<div className="min-w-0">
|
||||
{page.title ? (
|
||||
{page.metadata?.content?.title ? (
|
||||
<div className="flex items-center">
|
||||
<span>{page.title}</span>
|
||||
<span>{page.metadata?.content?.title}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-zinc-500 text-xs mt-1 truncate">
|
||||
<span>{page.summary?.content}</span>
|
||||
<span>{page.metadata?.content?.summary}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-zinc-400 text-xs mt-1">
|
||||
|
|
@ -292,8 +305,10 @@ export const PagesManager: React.FC<{
|
|||
<span className="mx-2">·</span>
|
||||
<span>
|
||||
{getPageVisibility(page) === PageVisibilityEnum.Draft
|
||||
? date.formatDate(page.date_updated)
|
||||
: date.formatDate(page.date_published)}
|
||||
? date.formatDate(page.updatedAt)
|
||||
: date.formatDate(
|
||||
page.metadata?.content?.date_published || "",
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -341,11 +356,11 @@ export const PagesManager: React.FC<{
|
|||
isPost
|
||||
? "post"
|
||||
: "page" +
|
||||
((pages.data?.pages?.[0].total || 0) - currentLength > 1
|
||||
((pages.data?.pages?.[0].count || 0) - currentLength > 1
|
||||
? "s"
|
||||
: ""),
|
||||
),
|
||||
count: (pages.data?.pages?.[0].total || 0) - currentLength,
|
||||
count: (pages.data?.pages?.[0].count || 0) - currentLength,
|
||||
ns: "site",
|
||||
})}
|
||||
</Button>
|
||||
|
|
|
|||
|
|
@ -2,14 +2,14 @@ import { useTranslation } from "next-i18next"
|
|||
import { useRouter } from "next/router"
|
||||
import React, { useState } from "react"
|
||||
import toast from "react-hot-toast"
|
||||
import type { Note, Notes } from "unidata.js"
|
||||
|
||||
import type { UseInfiniteQueryResult } from "@tanstack/react-query"
|
||||
import type { InfiniteData } from "@tanstack/react-query"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
|
||||
import { type TabItem, Tabs } from "~/components/ui/Tabs"
|
||||
import { APP_NAME } from "~/lib/env"
|
||||
import { delStorage, getStorage, setStorage } from "~/lib/storage"
|
||||
import { ExpandedNote } from "~/lib/types"
|
||||
import { useCreateOrUpdatePage, useDeletePage } from "~/queries/page"
|
||||
|
||||
import { DeleteConfirmationModal } from "./DeleteConfirmationModal"
|
||||
|
|
@ -17,8 +17,10 @@ import { DeleteConfirmationModal } from "./DeleteConfirmationModal"
|
|||
export const PagesManagerBatchSelectActionTab: React.FC<{
|
||||
isPost: boolean
|
||||
isNotxLogContent: boolean
|
||||
pages: UseInfiniteQueryResult<Notes, unknown>
|
||||
batchSelected: string[]
|
||||
pages?: InfiniteData<{
|
||||
list: ExpandedNote[]
|
||||
}>
|
||||
batchSelected: (string | number)[]
|
||||
setBatchSelected: (selected: string[]) => void
|
||||
}> = ({ isPost, isNotxLogContent, pages, batchSelected, setBatchSelected }) => {
|
||||
const { t } = useTranslation(["dashboard", "site"])
|
||||
|
|
@ -38,9 +40,9 @@ export const PagesManagerBatchSelectActionTab: React.FC<{
|
|||
onClick: () => {
|
||||
// Get all page IDs
|
||||
const allIDs: string[] = []
|
||||
pages.data?.pages.map((page) =>
|
||||
pages?.pages.map((page) =>
|
||||
page.list?.map((page) => {
|
||||
allIDs.push(page.id)
|
||||
allIDs.push(page.metadata?.content?.slug || "")
|
||||
}),
|
||||
)
|
||||
setBatchSelected(allIDs)
|
||||
|
|
@ -65,10 +67,10 @@ export const PagesManagerBatchSelectActionTab: React.FC<{
|
|||
const toastId = toast.loading("Converting...")
|
||||
|
||||
// Find all selected
|
||||
const selectedPages: Note[] = []
|
||||
pages.data?.pages.map((page) =>
|
||||
const selectedPages: ExpandedNote[] = []
|
||||
pages?.pages.map((page) =>
|
||||
page.list?.map((page) => {
|
||||
if (batchSelected.includes(page.id)) {
|
||||
if (batchSelected.includes(page.metadata?.content?.slug || "")) {
|
||||
selectedPages.push(page)
|
||||
}
|
||||
}),
|
||||
|
|
@ -79,34 +81,38 @@ export const PagesManagerBatchSelectActionTab: React.FC<{
|
|||
await Promise.all(
|
||||
selectedPages.map((page) => {
|
||||
// Check again to ensure it's not
|
||||
const isNotxLogContent = !page.applications?.includes("xlog")
|
||||
const isNotxLogContent =
|
||||
!page.metadata?.content?.sources?.includes("xlog")
|
||||
|
||||
const targetNoteBase = {
|
||||
published: true,
|
||||
pageId: page.id,
|
||||
pageId: `${page.characterId}-${page.noteId}`,
|
||||
siteId: subdomain,
|
||||
tags: page.tags
|
||||
tags: page.metadata?.content?.tags
|
||||
?.filter((tag) => tag !== "post" && tag !== "page")
|
||||
?.join(", "),
|
||||
applications: page.applications,
|
||||
applications: page.metadata?.content?.sources,
|
||||
}
|
||||
|
||||
if (isNotxLogContent) {
|
||||
return createOrUpdatePage.mutateAsync({
|
||||
...targetNoteBase,
|
||||
isPost: isPost, // Convert to xLog content
|
||||
characterId: page.characterId,
|
||||
})
|
||||
} else {
|
||||
if (!page.metadata) {
|
||||
if (!page.noteId) {
|
||||
// Is draft
|
||||
const data = getStorage(`draft-${subdomain}-${page.id}`)
|
||||
const key = `draft-${page?.characterId}-${page.draftKey}`
|
||||
const data = getStorage(key)
|
||||
data.isPost = !isPost
|
||||
setStorage(`draft-${subdomain}-${page.id}`, data)
|
||||
setStorage(key, data)
|
||||
} else {
|
||||
// IsNote
|
||||
return createOrUpdatePage.mutateAsync({
|
||||
...targetNoteBase,
|
||||
isPost: !isPost, // Change type
|
||||
characterId: page.characterId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -125,9 +131,12 @@ export const PagesManagerBatchSelectActionTab: React.FC<{
|
|||
|
||||
// Invalidate site data refresh
|
||||
await Promise.all([
|
||||
queryClient.invalidateQueries(["getPagesBySite", subdomain]),
|
||||
queryClient.invalidateQueries([
|
||||
"getPagesBySite",
|
||||
selectedPages[0]?.characterId,
|
||||
]),
|
||||
...selectedPages.map((page) =>
|
||||
queryClient.invalidateQueries(["getPage", page.id]),
|
||||
queryClient.invalidateQueries(["getPage", page?.characterId]),
|
||||
),
|
||||
])
|
||||
|
||||
|
|
@ -150,10 +159,10 @@ export const PagesManagerBatchSelectActionTab: React.FC<{
|
|||
const toastId = toast.loading("Deleting...")
|
||||
|
||||
// Find all selected
|
||||
const selectedPages: Note[] = []
|
||||
pages.data?.pages.map((page) =>
|
||||
const selectedPages: ExpandedNote[] = []
|
||||
pages?.pages.map((page) =>
|
||||
page.list?.map((page) => {
|
||||
if (batchSelected.includes(page.id)) {
|
||||
if (batchSelected.includes(page.noteId || page.draftKey || 0)) {
|
||||
selectedPages.push(page)
|
||||
}
|
||||
}),
|
||||
|
|
@ -163,14 +172,15 @@ export const PagesManagerBatchSelectActionTab: React.FC<{
|
|||
try {
|
||||
await Promise.all(
|
||||
selectedPages.map((page) => {
|
||||
if (!page.metadata) {
|
||||
if (!page.noteId) {
|
||||
// Is draft
|
||||
delStorage(`draft-${subdomain}-${page.id}`)
|
||||
delStorage(`draft-${page?.characterId}-${page.draftKey}`)
|
||||
} else {
|
||||
// Is Note
|
||||
return deletePage.mutateAsync({
|
||||
site: subdomain,
|
||||
id: page.id,
|
||||
id: `${page.characterId}-${page.noteId}`,
|
||||
characterId: page.characterId,
|
||||
})
|
||||
}
|
||||
}),
|
||||
|
|
@ -188,9 +198,12 @@ export const PagesManagerBatchSelectActionTab: React.FC<{
|
|||
|
||||
// Refresh site data
|
||||
await Promise.all([
|
||||
queryClient.refetchQueries(["getPagesBySite", subdomain]),
|
||||
queryClient.refetchQueries([
|
||||
"getPagesBySite",
|
||||
selectedPages[0]?.characterId,
|
||||
]),
|
||||
...selectedPages.map((page) =>
|
||||
queryClient.refetchQueries(["getPage", page.id]),
|
||||
queryClient.refetchQueries(["getPage", page.characterId]),
|
||||
),
|
||||
])
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ import { useTranslation } from "next-i18next"
|
|||
import { useRouter } from "next/router"
|
||||
import { FC, useEffect, useState } from "react"
|
||||
import toast from "react-hot-toast"
|
||||
import type { Note } from "unidata.js"
|
||||
|
||||
import { Menu } from "@headlessui/react"
|
||||
import { useQueryClient } from "@tanstack/react-query"
|
||||
|
|
@ -11,16 +10,17 @@ import { useGetState } from "~/hooks/useGetState"
|
|||
import { APP_NAME } from "~/lib/env"
|
||||
import { getNoteSlugFromNote, getTwitterShareUrl } from "~/lib/helpers"
|
||||
import { delStorage, getStorage, setStorage } from "~/lib/storage"
|
||||
import { ExpandedNote } from "~/lib/types"
|
||||
import { useCreateOrUpdatePage, useDeletePage } from "~/queries/page"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
|
||||
import { DeleteConfirmationModal } from "./DeleteConfirmationModal"
|
||||
|
||||
const usePageEditLink = (page: { id: string }, isPost: boolean) => {
|
||||
const usePageEditLink = (page: ExpandedNote, isPost: boolean) => {
|
||||
const router = useRouter()
|
||||
const subdomain = router.query.subdomain as string
|
||||
|
||||
return `/dashboard/${subdomain}/editor?id=${page.id}&type=${
|
||||
return `/dashboard/${subdomain}/editor?id=${page.noteId}&type=${
|
||||
isPost ? "post" : "page"
|
||||
}`
|
||||
}
|
||||
|
|
@ -32,12 +32,12 @@ interface Item {
|
|||
}
|
||||
export const PagesManagerMenu: FC<{
|
||||
isPost: boolean
|
||||
page: Note
|
||||
page: ExpandedNote
|
||||
onClick: () => void
|
||||
}> = ({ isPost, page, onClick: onClose }) => {
|
||||
const { t } = useTranslation(["dashboard", "site"])
|
||||
|
||||
const isCrossbell = !page.applications?.includes("xlog")
|
||||
const isCrossbell = !page.metadata?.content?.sources?.includes("xlog")
|
||||
const router = useRouter()
|
||||
const createOrUpdatePage = useCreateOrUpdatePage()
|
||||
|
||||
|
|
@ -110,21 +110,27 @@ export const PagesManagerMenu: FC<{
|
|||
setConvertToastId(toastId)
|
||||
createOrUpdatePage.mutate({
|
||||
published: true,
|
||||
pageId: page.id,
|
||||
pageId: `${page.characterId}-${page.noteId}`,
|
||||
siteId: subdomain,
|
||||
tags: page.tags
|
||||
tags: page.metadata?.content?.tags
|
||||
?.filter((tag) => tag !== "post" && tag !== "page")
|
||||
?.join(", "),
|
||||
isPost: isPost,
|
||||
applications: page.applications,
|
||||
applications: page.metadata?.content?.sources,
|
||||
characterId: page.characterId,
|
||||
})
|
||||
} else {
|
||||
if (!page.metadata) {
|
||||
const data = getStorage(`draft-${subdomain}-${page.id}`)
|
||||
if (!page.noteId) {
|
||||
const data = getStorage(
|
||||
`draft-${site.data?.characterId}-${page.draftKey}`,
|
||||
)
|
||||
data.isPost = !isPost
|
||||
setStorage(`draft-${subdomain}-${page.id}`, data)
|
||||
queryClient.invalidateQueries(["getPagesBySite", subdomain])
|
||||
queryClient.invalidateQueries(["getPage", page.id])
|
||||
setStorage(`draft-${site.data?.characterId}-${page.draftKey}`, data)
|
||||
queryClient.invalidateQueries([
|
||||
"getPagesBySite",
|
||||
site.data?.characterId,
|
||||
])
|
||||
queryClient.invalidateQueries(["getPage", page.characterId])
|
||||
toast.success("Converted!", {
|
||||
id: toastId,
|
||||
})
|
||||
|
|
@ -132,13 +138,14 @@ export const PagesManagerMenu: FC<{
|
|||
setConvertToastId(toastId)
|
||||
createOrUpdatePage.mutate({
|
||||
published: true,
|
||||
pageId: page.id,
|
||||
pageId: `${page.characterId}-${page.noteId}`,
|
||||
siteId: subdomain,
|
||||
tags: page.tags
|
||||
tags: page.metadata?.content?.tags
|
||||
?.filter((tag) => tag !== "post" && tag !== "page")
|
||||
?.join(", "),
|
||||
isPost: !isPost,
|
||||
applications: page.applications,
|
||||
applications: page.metadata?.content?.sources,
|
||||
characterId: page.characterId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -193,12 +200,12 @@ export const PagesManagerMenu: FC<{
|
|||
const [deleteConfirmModalOpen, setDeleteConfirmModalOpen] =
|
||||
useState<boolean>(false)
|
||||
const onDelete = () => {
|
||||
if (!page.metadata) {
|
||||
if (!page.noteId) {
|
||||
const toastId = toast.loading("Deleting...")
|
||||
delStorage(`draft-${subdomain}-${page.id}`)
|
||||
delStorage(`draft-${site.data?.characterId}-${page.draftKey}`)
|
||||
Promise.all([
|
||||
queryClient.refetchQueries(["getPagesBySite", subdomain]),
|
||||
queryClient.refetchQueries(["getPage", page.id]),
|
||||
queryClient.refetchQueries(["getPagesBySite", site.data?.characterId]),
|
||||
queryClient.refetchQueries(["getPage", page.characterId]),
|
||||
]).then(() => {
|
||||
toast.success("Deleted!", {
|
||||
id: toastId,
|
||||
|
|
@ -208,7 +215,8 @@ export const PagesManagerMenu: FC<{
|
|||
setDeleteToastId(toast.loading("Deleting..."))
|
||||
deletePage.mutate({
|
||||
site: subdomain,
|
||||
id: page.id,
|
||||
id: `${page.characterId}-${page.noteId}`,
|
||||
characterId: page.characterId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,42 +10,37 @@ import { DashboardMain } from "./DashboardMain"
|
|||
export const SettingsLayout: React.FC<{
|
||||
title: string
|
||||
children: React.ReactNode
|
||||
type: "site" | "account"
|
||||
}> = ({ title, children, type }) => {
|
||||
}> = ({ title, children }) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation("dashboard")
|
||||
const xSettingsModal = useXSettingsModal()
|
||||
|
||||
const subdomain = router.query.subdomain as string
|
||||
const tabItems: TabItem[] = (
|
||||
type === "site"
|
||||
? [
|
||||
{ text: "General", href: `/dashboard/${subdomain}/settings/general` },
|
||||
{
|
||||
text: "Social Platforms",
|
||||
href: `/dashboard/${subdomain}/settings/social-platforms`,
|
||||
},
|
||||
{
|
||||
text: "Navigation",
|
||||
href: `/dashboard/${subdomain}/settings/navigation`,
|
||||
},
|
||||
{ text: "Domains", href: `/dashboard/${subdomain}/settings/domains` },
|
||||
{ text: "Custom CSS", href: `/dashboard/${subdomain}/settings/css` },
|
||||
{
|
||||
text: "Operators",
|
||||
href: `/dashboard/${subdomain}/settings/operator`,
|
||||
},
|
||||
{
|
||||
text: "xSettings",
|
||||
onClick: () => xSettingsModal.show(),
|
||||
},
|
||||
{
|
||||
text: "Export data",
|
||||
href: `https://export.crossbell.io/?handle=${subdomain}`,
|
||||
},
|
||||
]
|
||||
: [{ text: "Profile", href: `/dashboard/${subdomain}/account/profile` }]
|
||||
).map((item) => ({ ...item, active: router.asPath === item.href }))
|
||||
const tabItems: TabItem[] = [
|
||||
{ text: "General", href: `/dashboard/${subdomain}/settings/general` },
|
||||
{
|
||||
text: "Social Platforms",
|
||||
href: `/dashboard/${subdomain}/settings/social-platforms`,
|
||||
},
|
||||
{
|
||||
text: "Navigation",
|
||||
href: `/dashboard/${subdomain}/settings/navigation`,
|
||||
},
|
||||
{ text: "Domains", href: `/dashboard/${subdomain}/settings/domains` },
|
||||
{ text: "Custom CSS", href: `/dashboard/${subdomain}/settings/css` },
|
||||
{
|
||||
text: "Operators",
|
||||
href: `/dashboard/${subdomain}/settings/operator`,
|
||||
},
|
||||
{
|
||||
text: "xSettings",
|
||||
onClick: () => xSettingsModal.show(),
|
||||
},
|
||||
{
|
||||
text: "Export data",
|
||||
href: `https://export.crossbell.io/?handle=${subdomain}`,
|
||||
},
|
||||
].map((item) => ({ ...item, active: router.asPath === item.href }))
|
||||
|
||||
return (
|
||||
<DashboardMain>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export function MainSidebar({ hideSearch }: { hideSearch?: boolean }) {
|
|||
>
|
||||
{t("Show more")}
|
||||
</div>
|
||||
{showcaseSites.data?.map((site: any) => (
|
||||
{showcaseSites.data?.map((site) => (
|
||||
<li className="flex align-middle" key={site.handle}>
|
||||
<UniLink
|
||||
href={getSiteLink({
|
||||
|
|
@ -78,7 +78,7 @@ export function MainSidebar({ hideSearch }: { hideSearch?: boolean }) {
|
|||
<Image
|
||||
className="rounded-full"
|
||||
src={
|
||||
site.metadata.content?.avatars?.[0] ||
|
||||
site?.metadata?.content?.avatars?.[0] ||
|
||||
"ipfs://bafkreiabgixxp63pg64moxnsydz7hewmpdkxxi3kdsa4oqv4pb6qvwnmxa"
|
||||
}
|
||||
alt={site.handle}
|
||||
|
|
@ -89,9 +89,9 @@ export function MainSidebar({ hideSearch }: { hideSearch?: boolean }) {
|
|||
</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}
|
||||
{site?.metadata?.content?.name}
|
||||
</span>
|
||||
{site.metadata.content?.bio && (
|
||||
{site?.metadata?.content?.bio && (
|
||||
<span className="text-gray-500 text-xs truncate w-full inline-block mt-1">
|
||||
{site.metadata.content?.bio}
|
||||
</span>
|
||||
|
|
|
|||
|
|
@ -2,11 +2,11 @@ import { Comment } from "~/components/common/Comment"
|
|||
import { ReactionLike } from "~/components/common/ReactionLike"
|
||||
import { ReactionMint } from "~/components/common/ReactionMint"
|
||||
import { ReactionTip } from "~/components/common/ReactionTip"
|
||||
import { Note, Profile } from "~/lib/types"
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
|
||||
export const PostFooter: React.FC<{
|
||||
page?: Note | null
|
||||
site?: Profile | null
|
||||
page?: ExpandedNote
|
||||
site?: ExpandedCharacter
|
||||
}> = ({ page, site }) => {
|
||||
return (
|
||||
<>
|
||||
|
|
@ -14,9 +14,14 @@ export const PostFooter: React.FC<{
|
|||
className="xlog-reactions flex fill-gray-400 text-gray-500 sm:items-center space-x-6 sm:space-x-10 mt-14 mb-12"
|
||||
data-hide-print
|
||||
>
|
||||
<ReactionLike pageId={page?.id} />
|
||||
<ReactionMint pageId={page?.id} />
|
||||
<ReactionTip pageId={page?.id} site={site} page={page} />
|
||||
<ReactionLike characterId={page?.characterId} noteId={page?.noteId} />
|
||||
<ReactionMint characterId={page?.characterId} noteId={page?.noteId} />
|
||||
<ReactionTip
|
||||
characterId={page?.characterId}
|
||||
noteId={page?.noteId}
|
||||
site={site}
|
||||
page={page}
|
||||
/>
|
||||
</div>
|
||||
<Comment page={page} />
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -1,29 +1,25 @@
|
|||
import { useTranslation } from "next-i18next"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import { CharacterFloatCard } from "~/components/common/CharacterFloatCard"
|
||||
import { BlockchainIcon } from "~/components/icons/BlockchainIcon"
|
||||
import { Avatar } from "~/components/ui/Avatar"
|
||||
import { UniLink } from "~/components/ui/UniLink"
|
||||
import { useDate } from "~/hooks/useDate"
|
||||
import { useUserRole } from "~/hooks/useUserRole"
|
||||
import { CSB_SCAN, SITE_URL } from "~/lib/env"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { toCid } from "~/lib/ipfs-parser"
|
||||
import { Note, Profile } from "~/lib/types"
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
import { useGetSummary } from "~/queries/page"
|
||||
|
||||
export const PostMeta: React.FC<{
|
||||
page: Note
|
||||
site?: Profile | null
|
||||
author?: Profile | null
|
||||
}> = ({ page, site, author }) => {
|
||||
page: ExpandedNote
|
||||
site?: ExpandedCharacter
|
||||
}> = ({ page, site }) => {
|
||||
const { t } = useTranslation("common")
|
||||
const date = useDate()
|
||||
const [isMounted, setIsMounted] = useState(false)
|
||||
const { i18n } = useTranslation()
|
||||
const summary = useGetSummary({
|
||||
cid: toCid(page.related_urls?.[0] || ""),
|
||||
cid: toCid(page.metadata?.uri || ""),
|
||||
lang: i18n.resolvedLanguage,
|
||||
})
|
||||
|
||||
|
|
@ -32,7 +28,7 @@ export const PostMeta: React.FC<{
|
|||
}, [])
|
||||
|
||||
const [showEdit, setShowEdit] = useState(false)
|
||||
const userRole = useUserRole(site?.username)
|
||||
const userRole = useUserRole(site?.handle)
|
||||
useEffect(() => {
|
||||
if (userRole.isSuccess && userRole.data) {
|
||||
setShowEdit(true)
|
||||
|
|
@ -43,19 +39,23 @@ export const PostMeta: React.FC<{
|
|||
<div className="xlog-post-meta">
|
||||
<div className="text-zinc-400 mt-4 space-x-5 flex items-center">
|
||||
<time
|
||||
dateTime={date.formatToISO(page.date_published)}
|
||||
dateTime={date.formatToISO(
|
||||
page?.metadata?.content?.date_published || "",
|
||||
)}
|
||||
className="xlog-post-date whitespace-nowrap"
|
||||
>
|
||||
{date.formatDate(
|
||||
page.date_published,
|
||||
page.metadata?.content?.date_published || "",
|
||||
undefined,
|
||||
isMounted ? undefined : "America/Los_Angeles",
|
||||
)}
|
||||
</time>
|
||||
{page.tags?.filter((tag) => tag !== "post" && tag !== "page").length ? (
|
||||
{page.metadata?.content?.tags?.filter(
|
||||
(tag) => tag !== "post" && tag !== "page",
|
||||
).length ? (
|
||||
<>
|
||||
<span className="xlog-post-tags space-x-1 truncate min-w-0">
|
||||
{page.tags
|
||||
{page.metadata?.content?.tags
|
||||
?.filter((tag) => tag !== "post" && tag !== "page")
|
||||
.map((tag) => (
|
||||
<UniLink
|
||||
|
|
@ -71,46 +71,22 @@ export const PostMeta: React.FC<{
|
|||
) : null}
|
||||
<span className="xlog-post-views inline-flex items-center">
|
||||
<i className="icon-[mingcute--eye-line] mr-[2px]" />
|
||||
<span>{page.views}</span>
|
||||
<span>{page.metadata?.content?.views}</span>
|
||||
</span>
|
||||
{author?.username && site?.username !== author?.username && (
|
||||
<>
|
||||
<span className="inline-flex items-center">
|
||||
<CharacterFloatCard siteId={author?.username}>
|
||||
<UniLink
|
||||
href={getSiteLink({
|
||||
subdomain: author?.username,
|
||||
})}
|
||||
className="cursor-pointer hover:text-zinc-600 inline-flex items-center"
|
||||
>
|
||||
<Avatar
|
||||
className="mr-1"
|
||||
images={author?.avatars || []}
|
||||
size={19}
|
||||
name={author?.name}
|
||||
/>
|
||||
<span>{author?.name}</span>
|
||||
</UniLink>
|
||||
</CharacterFloatCard>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
<UniLink
|
||||
className="xlog-post-blockchain inline-flex items-center"
|
||||
href={
|
||||
page.related_urls?.filter((url) =>
|
||||
url.startsWith(CSB_SCAN + "/tx/"),
|
||||
)?.[0]
|
||||
}
|
||||
href={`${CSB_SCAN}/tx/${page.updatedTransactionHash}`}
|
||||
>
|
||||
<BlockchainIcon className="fill-zinc-500 ml-1" />
|
||||
</UniLink>
|
||||
{showEdit && (
|
||||
<UniLink
|
||||
className="xlog-post-editor inline-flex items-center"
|
||||
href={`${SITE_URL}/dashboard/${site?.username}/editor?id=${
|
||||
page.id
|
||||
}&type=${page.tags?.includes("post") ? "post" : "page"}`}
|
||||
href={`${SITE_URL}/dashboard/${site?.handle}/editor?id=${
|
||||
page.noteId
|
||||
}&type=${
|
||||
page.metadata?.content?.tags?.includes("post") ? "post" : "page"
|
||||
}`}
|
||||
>
|
||||
<i className="icon-[mingcute--edit-line] mx-1" /> Edit
|
||||
</UniLink>
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@ import { useTranslation } from "next-i18next"
|
|||
import Link from "next/link"
|
||||
import { useMemo } from "react"
|
||||
|
||||
import type { InfiniteData } from "@tanstack/react-query"
|
||||
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { useDate } from "~/hooks/useDate"
|
||||
import { Note, Notes } from "~/lib/types"
|
||||
import { ExpandedNote } from "~/lib/types"
|
||||
|
||||
import { EmptyState } from "../ui/EmptyState"
|
||||
import { UniLink } from "../ui/UniLink"
|
||||
|
|
@ -12,14 +14,17 @@ import { UniLink } from "../ui/UniLink"
|
|||
export const SiteArchives: React.FC<{
|
||||
title?: string
|
||||
showTags?: boolean
|
||||
postPages?: Notes[]
|
||||
posts?: InfiniteData<{
|
||||
list: ExpandedNote[]
|
||||
count: number
|
||||
}>
|
||||
fetchNextPage: () => void
|
||||
hasNextPage?: boolean
|
||||
isFetchingNextPage?: boolean
|
||||
}> = ({
|
||||
title,
|
||||
showTags,
|
||||
postPages,
|
||||
posts,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
|
|
@ -28,13 +33,16 @@ export const SiteArchives: React.FC<{
|
|||
const date = useDate()
|
||||
const { t } = useTranslation(["common", "site"])
|
||||
|
||||
const groupedByYear = useMemo<Map<string, Note[]>>(() => {
|
||||
const groupedByYear = useMemo<Map<string, ExpandedNote[]>>(() => {
|
||||
const map = new Map()
|
||||
|
||||
if (postPages?.length) {
|
||||
for (const posts of postPages) {
|
||||
for (const post of posts.list) {
|
||||
const year = date.formatDate(post.date_published, "YYYY")
|
||||
if (posts?.pages?.length) {
|
||||
for (const page of posts.pages) {
|
||||
for (const post of page.list) {
|
||||
const year = date.formatDate(
|
||||
post.metadata?.content?.date_published || "",
|
||||
"YYYY",
|
||||
)
|
||||
const items = map.get(year) || []
|
||||
items.push(post)
|
||||
map.set(year, items)
|
||||
|
|
@ -43,15 +51,15 @@ export const SiteArchives: React.FC<{
|
|||
}
|
||||
|
||||
return map
|
||||
}, [postPages, date])
|
||||
}, [posts?.pages, date])
|
||||
|
||||
const tags = useMemo<Map<string, number>>(() => {
|
||||
const result = new Map()
|
||||
|
||||
if (postPages?.length) {
|
||||
for (const posts of postPages) {
|
||||
for (const post of posts.list) {
|
||||
post.tags?.forEach((tag) => {
|
||||
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") {
|
||||
if (result.has(tag)) {
|
||||
result.set(tag, result.get(tag) + 1)
|
||||
|
|
@ -65,21 +73,21 @@ export const SiteArchives: React.FC<{
|
|||
}
|
||||
|
||||
return result
|
||||
}, [postPages])
|
||||
}, [posts?.pages])
|
||||
|
||||
if (!postPages?.length) return null
|
||||
if (!posts?.pages?.length) return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="text-xl font-bold page-title">
|
||||
{title || t("Archives", { ns: "site" })}
|
||||
</h2>
|
||||
{!postPages[0].total && (
|
||||
{!posts?.pages[0].count && (
|
||||
<div className="mt-5">
|
||||
<EmptyState />
|
||||
</div>
|
||||
)}
|
||||
{!!postPages[0].total && (
|
||||
{!!posts?.pages[0].count && (
|
||||
<>
|
||||
{showTags && tags.size > 0 && (
|
||||
<div className="mt-5">
|
||||
|
|
@ -100,7 +108,6 @@ export const SiteArchives: React.FC<{
|
|||
)}
|
||||
<div className="mt-5 space-y-5">
|
||||
{[...groupedByYear.keys()].map((year) => {
|
||||
currentLength++
|
||||
const posts = groupedByYear.get(year)!
|
||||
return (
|
||||
<div key={year}>
|
||||
|
|
@ -108,16 +115,21 @@ export const SiteArchives: React.FC<{
|
|||
{year}
|
||||
</h3>
|
||||
{posts.map((post) => {
|
||||
currentLength++
|
||||
return (
|
||||
<Link
|
||||
key={post.id}
|
||||
href={`/${post.slug || post.id}`}
|
||||
key={post.transactionHash}
|
||||
href={`/${post.metadata?.content?.slug}`}
|
||||
className="flex justify-between items-center p-2 rounded-lg -mx-2 hover:bg-hover"
|
||||
>
|
||||
<span className="text-zinc-700">{post.title}</span>
|
||||
<span className="text-zinc-700">
|
||||
{post.metadata?.content?.title}
|
||||
</span>
|
||||
<span className="text-zinc-400 mr-3 whitespace-nowrap">
|
||||
{t("intlDateTime", {
|
||||
val: new Date(post.date_published),
|
||||
val: new Date(
|
||||
post.metadata?.content?.date_published || "",
|
||||
),
|
||||
formatParams: {
|
||||
val: {
|
||||
month: "short",
|
||||
|
|
@ -140,8 +152,8 @@ export const SiteArchives: React.FC<{
|
|||
isLoading={isFetchingNextPage}
|
||||
aria-label="load more"
|
||||
>
|
||||
There are {postPages[0].total - currentLength} more post
|
||||
{postPages[0].total - currentLength > 1 ? "s" : ""}, click to
|
||||
There are {posts?.pages[0].count - currentLength} more post
|
||||
{posts?.pages[0].count - currentLength > 1 ? "s" : ""}, click to
|
||||
load more
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -5,15 +5,14 @@ import { useEffect, useState } from "react"
|
|||
import { Logo } from "~/components/common/Logo"
|
||||
import { Platform } from "~/components/site/Platform"
|
||||
import { SITE_URL } from "~/lib/env"
|
||||
import { Note, Profile } from "~/lib/types"
|
||||
import { ExpandedCharacter } from "~/lib/types"
|
||||
|
||||
import { DarkModeSwitch } from "../common/DarkModeSwitch"
|
||||
import { UniLink } from "../ui/UniLink"
|
||||
|
||||
export const SiteFooter: React.FC<{
|
||||
site?: Profile | null
|
||||
page?: Note | null
|
||||
}> = ({ site, page }) => {
|
||||
site?: ExpandedCharacter
|
||||
}> = ({ site }) => {
|
||||
const [logoType, setLogoType] = useState<"svg" | "png" | "lottie">("svg")
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -38,7 +37,7 @@ export const SiteFooter: React.FC<{
|
|||
<div className="font-medium text-base">
|
||||
<span>© </span>
|
||||
<UniLink href="/" className="hover:text-accent">
|
||||
<span>{site?.name}</span>
|
||||
<span>{site?.metadata?.content?.name}</span>
|
||||
</UniLink>
|
||||
<span> · </span>
|
||||
<Trans
|
||||
|
|
@ -51,25 +50,32 @@ export const SiteFooter: React.FC<{
|
|||
ns="site"
|
||||
/>
|
||||
</div>
|
||||
{site?.connected_accounts && (
|
||||
{site?.metadata?.content?.connected_accounts && (
|
||||
<div className="sm:-mr-5 sm:block inline-block align-middle mr-4">
|
||||
{site?.connected_accounts.map((account, index) => (
|
||||
<Platform
|
||||
key={index}
|
||||
platform={account.platform}
|
||||
username={account.identity}
|
||||
className="mr-2 sm:mr-5"
|
||||
></Platform>
|
||||
))}
|
||||
{site?.metadata?.content?.connected_accounts.map(
|
||||
(account, index) => {
|
||||
const match = account.match(/:\/\/account:(.*)@(.*)/)
|
||||
if (match) {
|
||||
return (
|
||||
<Platform
|
||||
key={account}
|
||||
platform={match[2]}
|
||||
username={match[1]}
|
||||
className="mr-2 sm:mr-5"
|
||||
></Platform>
|
||||
)
|
||||
}
|
||||
},
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<DarkModeSwitch />
|
||||
</div>
|
||||
</footer>
|
||||
{site?.ga && (
|
||||
{site?.metadata?.content?.ga && (
|
||||
<div className="xlog-google-analytics">
|
||||
<Script
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=G-${site.ga}`}
|
||||
src={`https://www.googletagmanager.com/gtag/js?id=G-${site.metadata?.content?.ga}`}
|
||||
strategy="afterInteractive"
|
||||
/>
|
||||
<Script id="google-analytics" strategy="afterInteractive">
|
||||
|
|
@ -78,7 +84,7 @@ export const SiteFooter: React.FC<{
|
|||
function gtag(){window.dataLayer.push(arguments);}
|
||||
gtag('js', new Date());
|
||||
|
||||
gtag('config', 'G-${site.ga}');
|
||||
gtag('config', 'G-${site.metadata?.content?.ga}');
|
||||
`}
|
||||
</Script>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@ import { Modal } from "~/components/ui/Modal"
|
|||
import { Tooltip } from "~/components/ui/Tooltip"
|
||||
import { useIsDark } from "~/hooks/useDarkMode"
|
||||
import { CSB_IO, CSB_SCAN, CSB_XCHAR } from "~/lib/env"
|
||||
import { Profile } from "~/lib/types"
|
||||
import { ExpandedCharacter } from "~/lib/types"
|
||||
import { getUserContentsUrl } from "~/lib/user-contents"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
|
|
@ -58,25 +58,28 @@ const HeaderLink: React.FC<{ link: HeaderLinkType }> = ({ link }) => {
|
|||
}
|
||||
|
||||
export const SiteHeader: React.FC<{
|
||||
site?: Profile | undefined | null
|
||||
site?: ExpandedCharacter
|
||||
}> = ({ site }) => {
|
||||
const { t } = useTranslation("site")
|
||||
const leftLinks: HeaderLinkType[] = site?.navigation?.find(
|
||||
const leftLinks: HeaderLinkType[] = site?.metadata?.content?.navigation?.find(
|
||||
(nav) => nav.url === "/",
|
||||
)
|
||||
? site.navigation
|
||||
: [{ label: "Home", url: "/" }, ...(site?.navigation || [])]
|
||||
? site.metadata?.content?.navigation
|
||||
: [
|
||||
{ label: "Home", url: "/" },
|
||||
...(site?.metadata?.content?.navigation || []),
|
||||
]
|
||||
|
||||
const moreMenuItems = [
|
||||
{
|
||||
text: "View on xChar",
|
||||
icon: <XCharLogo className="w-full h-full" />,
|
||||
url: `${CSB_XCHAR}/${site?.username}`,
|
||||
url: `${CSB_XCHAR}/${site?.handle}`,
|
||||
},
|
||||
{
|
||||
text: "View on xFeed",
|
||||
icon: <XFeedLogo className="w-full h-full" />,
|
||||
url: `${CSB_IO}/@${site?.username}`,
|
||||
url: `${CSB_IO}/@${site?.handle}`,
|
||||
},
|
||||
{
|
||||
text: "View on Hoot It",
|
||||
|
|
@ -91,12 +94,12 @@ export const SiteHeader: React.FC<{
|
|||
/>
|
||||
</div>
|
||||
),
|
||||
url: `https://hoot.it/search/${site?.username}.csb/activities`,
|
||||
url: `https://hoot.it/search/${site?.handle}.csb/activities`,
|
||||
},
|
||||
{
|
||||
text: "View on Crossbell Scan",
|
||||
icon: <BlockchainIcon className="fill-[#c09526] w-full h-full" />,
|
||||
url: `${CSB_SCAN}/address/${site?.metadata?.owner}`,
|
||||
url: `${CSB_SCAN}/address/${site?.owner}`,
|
||||
},
|
||||
{
|
||||
text: "Subscribe to JSON Feed",
|
||||
|
|
@ -196,12 +199,14 @@ export const SiteHeader: React.FC<{
|
|||
}}
|
||||
>
|
||||
{(() => {
|
||||
switch (site?.banners?.[0]?.mime_type?.split("/")[0]) {
|
||||
switch (
|
||||
site?.metadata?.content?.banners?.[0]?.mime_type?.split("/")[0]
|
||||
) {
|
||||
case "image":
|
||||
return (
|
||||
<Image
|
||||
className="max-w-screen-md mx-auto object-cover"
|
||||
src={site?.banners?.[0]?.address}
|
||||
src={site?.metadata?.content?.banners?.[0]?.address}
|
||||
alt="banner"
|
||||
fill
|
||||
imageRef={bannerRef as MutableRefObject<HTMLImageElement>}
|
||||
|
|
@ -211,7 +216,7 @@ export const SiteHeader: React.FC<{
|
|||
return (
|
||||
<video
|
||||
className="max-w-screen-md mx-auto object-cover h-full w-full"
|
||||
src={site?.banners?.[0]?.address}
|
||||
src={site?.metadata?.content?.banners?.[0]?.address}
|
||||
autoPlay
|
||||
muted
|
||||
playsInline
|
||||
|
|
@ -230,24 +235,26 @@ export const SiteHeader: React.FC<{
|
|||
<div
|
||||
className={cn(
|
||||
"xlog-site-info flex space-x-6 items-center w-full",
|
||||
site?.banners?.[0]?.address
|
||||
site?.metadata?.content?.banners?.[0]?.address
|
||||
? "bg-white bg-opacity-50 backdrop-blur-sm rounded-xl p-4 sm:p-8 z-[1] border"
|
||||
: "",
|
||||
)}
|
||||
>
|
||||
{site?.avatars?.[0] && (
|
||||
{site?.metadata?.content?.avatars?.[0] && (
|
||||
<Avatar
|
||||
className="xlog-site-icon max-w-[80px] max-h-[80px] sm:max-w-none sm:max-h-none"
|
||||
images={[getUserContentsUrl(site?.avatars?.[0])]}
|
||||
images={[
|
||||
getUserContentsUrl(site?.metadata?.content?.avatars?.[0]),
|
||||
]}
|
||||
size={120}
|
||||
name={site?.name}
|
||||
name={site?.metadata?.content?.name}
|
||||
imageRef={avatarRef as MutableRefObject<HTMLImageElement>}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0 relative">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="xlog-site-name text-2xl sm:text-3xl font-bold text-zinc-900 leading-snug break-words min-w-0">
|
||||
{site?.name}
|
||||
{site?.metadata?.content?.name}
|
||||
</div>
|
||||
<div className="ml-0 sm:ml-8 space-x-3 sm:space-x-4 flex items-center sm:static absolute -bottom-0 right-0">
|
||||
<div className="xlog-site-more-menu relative inline-block align-middle">
|
||||
|
|
@ -318,14 +325,14 @@ export const SiteHeader: React.FC<{
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{site?.description && (
|
||||
{site?.metadata?.content?.bio && (
|
||||
<div className="xlog-site-description text-gray-500 leading-snug my-2 sm:my-3 text-sm sm:text-base line-clamp-4 whitespace-pre-wrap">
|
||||
{site?.description}
|
||||
{site?.metadata?.content?.bio}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex space-x-0 sm:space-x-5 space-y-2 sm:space-y-0 flex-col sm:flex-row text-sm sm:text-base">
|
||||
<span className="xlog-site-follow-count block sm:inline-block whitespace-nowrap">
|
||||
<FollowingCount siteId={site?.username} />
|
||||
<FollowingCount characterId={site?.characterId} />
|
||||
</span>
|
||||
<span className="xlog-site-patron">
|
||||
<PatronButton site={site} />
|
||||
|
|
|
|||
|
|
@ -3,20 +3,25 @@ import Link from "next/link"
|
|||
import { useRouter } from "next/router"
|
||||
import { useEffect, useState } from "react"
|
||||
|
||||
import type { InfiniteData } from "@tanstack/react-query"
|
||||
|
||||
import { Button } from "~/components/ui/Button"
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { useDate } from "~/hooks/useDate"
|
||||
import { getSlugUrl } from "~/lib/helpers"
|
||||
import { Notes } from "~/lib/types"
|
||||
import { ExpandedNote } from "~/lib/types"
|
||||
|
||||
import { EmptyState } from "../ui/EmptyState"
|
||||
|
||||
export const SiteHome: React.FC<{
|
||||
postPages?: Notes[]
|
||||
posts?: InfiniteData<{
|
||||
list: ExpandedNote[]
|
||||
count: number
|
||||
}>
|
||||
fetchNextPage: () => void
|
||||
hasNextPage?: boolean
|
||||
isFetchingNextPage?: boolean
|
||||
}> = ({ postPages, fetchNextPage, hasNextPage, isFetchingNextPage }) => {
|
||||
}> = ({ posts, fetchNextPage, hasNextPage, isFetchingNextPage }) => {
|
||||
const router = useRouter()
|
||||
const { t } = useTranslation(["common", "site"])
|
||||
const date = useDate()
|
||||
|
|
@ -27,45 +32,47 @@ export const SiteHome: React.FC<{
|
|||
setIsMounted(true)
|
||||
}, [])
|
||||
|
||||
if (!postPages?.length) return null
|
||||
if (!posts?.pages?.length) return null
|
||||
|
||||
let currentLength = 0
|
||||
|
||||
return (
|
||||
<>
|
||||
{!postPages[0].total && <EmptyState />}
|
||||
{!!postPages[0].total && (
|
||||
{!posts.pages[0].count && <EmptyState />}
|
||||
{!!posts.pages[0].count && (
|
||||
<div className="xlog-posts space-y-8">
|
||||
{postPages.map((posts) =>
|
||||
{posts.pages.map((posts) =>
|
||||
posts.list.map((post) => {
|
||||
currentLength++
|
||||
return (
|
||||
<Link
|
||||
key={post.id}
|
||||
href={getSlugUrl(`/${post.slug || post.id}`)}
|
||||
key={post.transactionHash}
|
||||
href={getSlugUrl(`/${post.metadata?.content?.slug}`)}
|
||||
className="xlog-post sm:hover:bg-hover bg-white transition-all px-5 py-7 -mx-5 first:-mt-5 sm:rounded-xl flex flex-col sm:flex-row items-center"
|
||||
suppressHydrationWarning
|
||||
>
|
||||
<div className="flex-1 flex justify-center flex-col w-full min-w-0">
|
||||
<h3 className="xlog-post-title text-2xl font-bold text-zinc-700">
|
||||
{post.title}
|
||||
{post.metadata?.content?.title}
|
||||
</h3>
|
||||
<div className="xlog-post-meta text-sm text-zinc-400 mt-1 space-x-4 flex items-center mr-8">
|
||||
<time
|
||||
dateTime={date.formatToISO(post.date_published)}
|
||||
dateTime={date.formatToISO(
|
||||
post.metadata?.content?.date_published || "",
|
||||
)}
|
||||
className="xlog-post-date whitespace-nowrap"
|
||||
>
|
||||
{date.formatDate(
|
||||
post.date_published,
|
||||
post.metadata?.content?.date_published || "",
|
||||
undefined,
|
||||
isMounted ? undefined : "America/Los_Angeles",
|
||||
)}
|
||||
</time>
|
||||
{!!post.tags?.filter(
|
||||
{!!post.metadata?.content?.tags?.filter(
|
||||
(tag) => tag !== "post" && tag !== "page",
|
||||
).length && (
|
||||
<span className="xlog-post-tags space-x-1 truncate min-w-0">
|
||||
{post.tags
|
||||
{post.metadata?.content?.tags
|
||||
?.filter((tag) => tag !== "post" && tag !== "page")
|
||||
.map((tag) => (
|
||||
<span
|
||||
|
|
@ -83,7 +90,7 @@ export const SiteHome: React.FC<{
|
|||
)}
|
||||
<span className="xlog-post-views inline-flex items-center">
|
||||
<i className="icon-[mingcute--eye-line] mr-[2px]" />
|
||||
<span>{post.views}</span>
|
||||
<span>{post.metadata?.content?.views}</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
|
|
@ -92,17 +99,17 @@ export const SiteHome: React.FC<{
|
|||
wordBreak: "break-word",
|
||||
}}
|
||||
>
|
||||
{post.summary?.content}
|
||||
{post.summary?.content && "..."}
|
||||
{post.metadata?.content?.summary}
|
||||
{post.metadata?.content?.summary && "..."}
|
||||
</div>
|
||||
</div>
|
||||
{post.cover && (
|
||||
{post.metadata?.content?.cover && (
|
||||
<div className="xlog-post-cover flex items-center relative w-full sm:w-24 h-40 sm:h-24 mt-2 sm:ml-4 sm:mt-0">
|
||||
<Image
|
||||
className="object-cover rounded"
|
||||
alt="cover"
|
||||
fill={true}
|
||||
src={post.cover}
|
||||
src={post.metadata?.content?.cover}
|
||||
></Image>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -123,9 +130,9 @@ export const SiteHome: React.FC<{
|
|||
{t("load more", {
|
||||
ns: "site",
|
||||
name: t(
|
||||
"post" + (postPages[0].total - currentLength > 1 ? "s" : ""),
|
||||
"post" + (posts?.pages[0].count - currentLength > 1 ? "s" : ""),
|
||||
),
|
||||
count: postPages[0].total - currentLength,
|
||||
count: posts?.pages[0].count - currentLength,
|
||||
})}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { notFound } from "~/lib/server-side-props"
|
|||
import { PageVisibilityEnum } from "~/lib/types"
|
||||
import { fetchGetPage, prefetchGetPagesBySite } from "~/queries/page.server"
|
||||
import {
|
||||
prefetchGetSite,
|
||||
fetchGetSite,
|
||||
prefetchGetSiteSubscriptions,
|
||||
prefetchGetSiteToSubscriptions,
|
||||
} from "~/queries/site.server"
|
||||
|
|
@ -16,7 +16,7 @@ export const getServerSideProps = async (
|
|||
ctx: any,
|
||||
queryClient: QueryClient,
|
||||
options?: {
|
||||
take?: number
|
||||
limit?: number
|
||||
useStat?: boolean
|
||||
skipPages?: boolean
|
||||
preview?: boolean
|
||||
|
|
@ -25,67 +25,69 @@ export const getServerSideProps = async (
|
|||
const domainOrSubdomain = ctx.params!.site as string
|
||||
const pageSlug = ctx.params!.page as string
|
||||
const tag = ctx.params!.tag as string
|
||||
const site = await fetchGetSite(domainOrSubdomain, queryClient)
|
||||
|
||||
await Promise.all([
|
||||
prefetchGetSite(domainOrSubdomain, queryClient),
|
||||
prefetchGetSiteSubscriptions(
|
||||
{
|
||||
siteId: domainOrSubdomain,
|
||||
},
|
||||
queryClient,
|
||||
),
|
||||
prefetchGetSiteToSubscriptions(
|
||||
{
|
||||
siteId: domainOrSubdomain,
|
||||
},
|
||||
queryClient,
|
||||
),
|
||||
new Promise(async (resolve, reject) => {
|
||||
if (options?.preview) {
|
||||
// do nothing
|
||||
} else if (pageSlug) {
|
||||
try {
|
||||
const page = await fetchGetPage(
|
||||
{
|
||||
site: domainOrSubdomain,
|
||||
page: pageSlug,
|
||||
...(options?.useStat && {
|
||||
useStat: true,
|
||||
}),
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
if (site?.characterId) {
|
||||
await Promise.all([
|
||||
prefetchGetSiteSubscriptions(
|
||||
{
|
||||
characterId: site.characterId,
|
||||
},
|
||||
queryClient,
|
||||
),
|
||||
prefetchGetSiteToSubscriptions(
|
||||
{
|
||||
characterId: site.characterId,
|
||||
},
|
||||
queryClient,
|
||||
),
|
||||
new Promise(async (resolve, reject) => {
|
||||
if (options?.preview) {
|
||||
// do nothing
|
||||
} else if (pageSlug) {
|
||||
try {
|
||||
const page = await fetchGetPage(
|
||||
{
|
||||
characterId: site.characterId,
|
||||
slug: pageSlug,
|
||||
...(options?.useStat && {
|
||||
useStat: true,
|
||||
}),
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
|
||||
if (!page || new Date(page!.date_published) > new Date()) {
|
||||
reject(notFound())
|
||||
if (
|
||||
!page ||
|
||||
new Date(page!.metadata?.content?.date_published || "") >
|
||||
new Date()
|
||||
) {
|
||||
reject(notFound())
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
} else {
|
||||
if (!options?.skipPages) {
|
||||
await prefetchGetPagesBySite(
|
||||
{
|
||||
characterId: site.characterId,
|
||||
...(options?.limit && { limit: options.limit }),
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
...(tag && { tags: [tag] }),
|
||||
...(options?.useStat && {
|
||||
useStat: true,
|
||||
}),
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
}
|
||||
|
||||
// if (page?.authors[0]) {
|
||||
// await prefetchGetUserSites(page?.authors[0], queryClient)
|
||||
// }
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
} else {
|
||||
if (!options?.skipPages) {
|
||||
await prefetchGetPagesBySite(
|
||||
{
|
||||
site: domainOrSubdomain,
|
||||
...(options?.take && { take: options.take }),
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
...(tag && { tags: [tag] }),
|
||||
...(options?.useStat && {
|
||||
useStat: true,
|
||||
}),
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
}
|
||||
}
|
||||
resolve(null)
|
||||
}),
|
||||
])
|
||||
resolve(null)
|
||||
}),
|
||||
])
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
|
|
|
|||
|
|
@ -39,27 +39,33 @@ export const SiteLayout: React.FC<SiteLayoutProps> = ({
|
|||
const pageSlug = router.query.page as string
|
||||
const tag = router.query.tag as string
|
||||
|
||||
const site = useGetSite(domainOrSubdomain)
|
||||
|
||||
const page = useGetPage({
|
||||
site: domainOrSubdomain,
|
||||
page: pageSlug,
|
||||
characterId: site.data?.characterId,
|
||||
slug: pageSlug,
|
||||
...(useStat && {
|
||||
useStat: true,
|
||||
}),
|
||||
})
|
||||
|
||||
const site = useGetSite(domainOrSubdomain)
|
||||
|
||||
const isConnected = useAccountState((s) => !!s.computed.account)
|
||||
const userRole = useUserRole(domainOrSubdomain)
|
||||
const subscription = useGetSubscription(domainOrSubdomain)
|
||||
const [{ isLiked }] = useCheckLike({ pageId: page.data?.id })
|
||||
const isMint = useCheckMint(page.data?.id)
|
||||
const subscription = useGetSubscription(site.data?.characterId)
|
||||
const [{ isLiked }] = useCheckLike({
|
||||
characterId: page.data?.characterId,
|
||||
noteId: page.data?.noteId,
|
||||
})
|
||||
const isMint = useCheckMint({
|
||||
characterId: page.data?.characterId,
|
||||
noteId: page.data?.noteId,
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (site.data) {
|
||||
if (
|
||||
window.location.host.split(".").slice(-2).join(".") !== OUR_DOMAIN &&
|
||||
window.location.host !== site.data?.custom_domain &&
|
||||
window.location.host !== site.data?.metadata?.content?.custom_domain &&
|
||||
IS_PROD
|
||||
) {
|
||||
window.location.href = SITE_URL
|
||||
|
|
@ -83,32 +89,37 @@ export const SiteLayout: React.FC<SiteLayoutProps> = ({
|
|||
)}
|
||||
>
|
||||
<SEOHead
|
||||
title={title || tag || page.data?.title || ""}
|
||||
siteName={site.data?.name || ""}
|
||||
title={title || tag || page.data?.metadata?.content?.title || ""}
|
||||
siteName={site.data?.metadata?.content?.name || ""}
|
||||
description={
|
||||
page.data?.summary?.content ??
|
||||
site.data?.description?.replace(/<[^>]*>/g, "")
|
||||
page.data?.metadata?.content?.summary ??
|
||||
site.data?.metadata?.content?.bio?.replace(/<[^>]*>/g, "")
|
||||
}
|
||||
image={page.data?.cover || getUserContentsUrl(site.data?.avatars?.[0])}
|
||||
icon={getUserContentsUrl(site.data?.avatars?.[0])}
|
||||
image={
|
||||
page.data?.metadata?.content?.cover ||
|
||||
getUserContentsUrl(site.data?.metadata?.content?.avatars?.[0])
|
||||
}
|
||||
icon={getUserContentsUrl(site.data?.metadata?.content?.avatars?.[0])}
|
||||
site={domainOrSubdomain}
|
||||
/>
|
||||
<Style content={site.data?.css} />
|
||||
<Style content={site.data?.metadata?.content?.css} />
|
||||
{site.data && <SiteHeader site={site.data} />}
|
||||
<div
|
||||
className={cn(
|
||||
`xlog-post-id-${page.data?.id} max-w-screen-md mx-auto px-5 pt-12 relative`,
|
||||
page.data?.tags?.map((tag) => `xlog-post-tag-${tag}`),
|
||||
`xlog-post-id-${page.data?.characterId}-${page.data?.noteId} max-w-screen-md mx-auto px-5 pt-12 relative`,
|
||||
page.data?.metadata?.content?.tags?.map(
|
||||
(tag) => `xlog-post-tag-${tag}`,
|
||||
),
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
{site.data && (
|
||||
<div className="max-w-screen-md mx-auto pt-12 pb-10">
|
||||
<BlockchainInfo site={site.data} page={page.data} />
|
||||
<BlockchainInfo site={site.data} page={page.data || undefined} />
|
||||
</div>
|
||||
)}
|
||||
<SiteFooter site={site.data} page={page.data} />
|
||||
<SiteFooter site={site.data || undefined} />
|
||||
|
||||
<FABContainer>
|
||||
<BackToTopFAB />
|
||||
|
|
|
|||
|
|
@ -3,17 +3,17 @@ import Head from "next/head"
|
|||
import serialize from "serialize-javascript"
|
||||
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { Note, Profile } from "~/lib/types"
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
|
||||
import { PageContent } from "../common/PageContent"
|
||||
import { PostFooter } from "./PostFooter"
|
||||
import { PostMeta } from "./PostMeta"
|
||||
|
||||
export const SitePage: React.FC<{
|
||||
page?: Note | null
|
||||
site?: Profile | null
|
||||
}> = ({ page, site }) => {
|
||||
// const author = useGetUserSites(page?.authors?.[0])
|
||||
page?: ExpandedNote
|
||||
site?: ExpandedCharacter
|
||||
preview?: boolean
|
||||
}> = ({ page, site, preview }) => {
|
||||
const { t } = useTranslation("site")
|
||||
|
||||
function addPageJsonLd() {
|
||||
|
|
@ -21,18 +21,18 @@ export const SitePage: React.FC<{
|
|||
__html: serialize({
|
||||
"@context": "https://schema.org",
|
||||
"@type": "BlogPosting",
|
||||
headline: page?.title,
|
||||
...(page?.cover && {
|
||||
image: [page?.cover],
|
||||
headline: page?.metadata?.content?.title,
|
||||
...(page?.metadata?.content?.cover && {
|
||||
image: [page?.metadata?.content?.cover],
|
||||
}),
|
||||
datePublished: page?.date_published,
|
||||
dateModified: page?.date_updated,
|
||||
datePublished: page?.metadata?.content?.date_published,
|
||||
dateModified: page?.updatedAt,
|
||||
author: [
|
||||
{
|
||||
"@type": "Person",
|
||||
name: site?.name,
|
||||
name: site?.metadata?.content?.name,
|
||||
url: getSiteLink({
|
||||
subdomain: site?.username || "",
|
||||
subdomain: site?.handle || "",
|
||||
}),
|
||||
},
|
||||
],
|
||||
|
|
@ -48,7 +48,7 @@ export const SitePage: React.FC<{
|
|||
dangerouslySetInnerHTML={addPageJsonLd()}
|
||||
/>
|
||||
</Head>
|
||||
{page?.preview && (
|
||||
{preview && (
|
||||
<div className="fixed top-0 left-0 w-full text-center text-red-500 bg-gray-100 py-2 opacity-80 text-sm z-10">
|
||||
{t(
|
||||
"This address is in local editing preview mode and cannot be viewed by the public.",
|
||||
|
|
@ -57,20 +57,22 @@ export const SitePage: React.FC<{
|
|||
)}
|
||||
<article>
|
||||
<div>
|
||||
{page?.tags?.includes("post") ? (
|
||||
<h2 className="xlog-post-title text-4xl font-bold">{page.title}</h2>
|
||||
{page?.metadata?.content?.tags?.includes("post") ? (
|
||||
<h2 className="xlog-post-title text-4xl font-bold">
|
||||
{page.metadata?.content?.title}
|
||||
</h2>
|
||||
) : (
|
||||
<h2 className="xlog-post-title text-xl font-bold page-title">
|
||||
{page?.title}
|
||||
{page?.metadata?.content?.title}
|
||||
</h2>
|
||||
)}
|
||||
{page?.tags?.includes("post") && !page?.preview && (
|
||||
{page?.metadata?.content?.tags?.includes("post") && !preview && (
|
||||
<PostMeta page={page} site={site} />
|
||||
)}
|
||||
</div>
|
||||
<PageContent
|
||||
className="mt-10"
|
||||
content={page?.body?.content}
|
||||
content={page?.metadata?.content?.content}
|
||||
toc={true}
|
||||
></PageContent>
|
||||
</article>
|
||||
|
|
|
|||
|
|
@ -1,18 +1,17 @@
|
|||
import { useAccountState } from "@crossbell/connect-kit"
|
||||
|
||||
import { useAccountSites, useGetSite, useIsOperators } from "~/queries/site"
|
||||
import { useGetSite, useIsOperators } from "~/queries/site"
|
||||
|
||||
export function useUserRole(subdomain?: string) {
|
||||
const site = useGetSite(subdomain)
|
||||
|
||||
const userSite = useAccountSites()
|
||||
const [ssrReady, account] = useAccountState(({ ssrReady, computed }) => [
|
||||
ssrReady,
|
||||
computed.account,
|
||||
])
|
||||
|
||||
const isOperator = useIsOperators({
|
||||
characterId: +(site.data?.metadata?.proof || 0),
|
||||
characterId: site.data?.characterId,
|
||||
operator: account?.address,
|
||||
})
|
||||
|
||||
|
|
@ -25,15 +24,14 @@ export function useUserRole(subdomain?: string) {
|
|||
}
|
||||
} else {
|
||||
if (account?.address) {
|
||||
if (userSite.data?.find((site) => site.username === subdomain)) {
|
||||
if (account.character?.handle === subdomain) {
|
||||
role = "owner"
|
||||
} else if (isOperator.data) {
|
||||
role = "operator"
|
||||
}
|
||||
|
||||
return {
|
||||
isSuccess:
|
||||
site.isSuccess && userSite.isSuccess && isOperator.isSuccess,
|
||||
isSuccess: site.isSuccess && ssrReady,
|
||||
data: role,
|
||||
}
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -1,123 +1,51 @@
|
|||
import { CharacterEntity, NoteEntity } from "crossbell.js"
|
||||
import { nanoid } from "nanoid"
|
||||
|
||||
import { SCORE_API_DOMAIN, SITE_URL } from "~/lib/env"
|
||||
import { toCid, toGateway } from "~/lib/ipfs-parser"
|
||||
import { ExpandedNote, Note, Profile } from "~/lib/types"
|
||||
|
||||
export const expandUnidataNote = async (page: Note, useStat?: boolean) => {
|
||||
if (page.body?.content && page.body?.mime_type === "text/markdown") {
|
||||
const { renderPageContent } = await import("~/markdown")
|
||||
const rendered = renderPageContent(page.body.content, true)
|
||||
page.body = {
|
||||
content: page.body.content,
|
||||
mime_type: "text/markdown",
|
||||
}
|
||||
if (!page.summary) {
|
||||
page.summary = {
|
||||
content: rendered.excerpt,
|
||||
mime_type: "text/html",
|
||||
}
|
||||
}
|
||||
page.cover = rendered.cover
|
||||
page.audio = rendered.audio
|
||||
if (page.metadata) {
|
||||
page.metadata.frontMatter = rendered.frontMatter
|
||||
}
|
||||
}
|
||||
page.slug = encodeURIComponent(
|
||||
page.attributes?.find((a) => a.trait_type === "xlog_slug")?.value ||
|
||||
page.metadata?.raw?._xlog_slug ||
|
||||
page.metadata?.raw?._crosslog_slug ||
|
||||
"",
|
||||
)
|
||||
delete page.metadata?.raw
|
||||
|
||||
if (useStat) {
|
||||
const stat = await (
|
||||
await fetch(
|
||||
`https://indexer.crossbell.io/v1/stat/notes/${page.id.replace(
|
||||
"-",
|
||||
"/",
|
||||
)}`,
|
||||
)
|
||||
).json()
|
||||
page.views = stat.viewDetailCount
|
||||
}
|
||||
|
||||
return page
|
||||
}
|
||||
|
||||
export const expandUnidataProfile = (site: Profile) => {
|
||||
site.navigation = JSON.parse(
|
||||
site.metadata?.raw?.attributes?.find(
|
||||
(a: any) => a.trait_type === "xlog_navigation",
|
||||
)?.value || "null",
|
||||
) ||
|
||||
site.metadata?.raw?.["_xlog_navigation"] ||
|
||||
site.metadata?.raw?.["_crosslog_navigation"] || [
|
||||
{ id: nanoid(), label: "Archives", url: "/archives" },
|
||||
]
|
||||
site.css =
|
||||
site.metadata?.raw?.attributes?.find(
|
||||
(a: any) => a.trait_type === "xlog_css",
|
||||
)?.value ||
|
||||
site.metadata?.raw?.["_xlog_css"] ||
|
||||
site.metadata?.raw?.["_crosslog_css"] ||
|
||||
""
|
||||
site.ga =
|
||||
site.metadata?.raw?.attributes?.find((a: any) => a.trait_type === "xlog_ga")
|
||||
?.value || ""
|
||||
site.custom_domain =
|
||||
site.metadata?.raw?.attributes?.find(
|
||||
(a: any) => a.trait_type === "xlog_custom_domain",
|
||||
)?.value || ""
|
||||
site.name = site.name || site.username
|
||||
site.description = site.bio
|
||||
|
||||
if (site.avatars) {
|
||||
site.avatars = site.avatars.map((avatar) => toGateway(avatar))
|
||||
}
|
||||
if (site.banners) {
|
||||
site.banners.map((banner) => {
|
||||
banner.address = toGateway(banner.address)
|
||||
return banner
|
||||
})
|
||||
}
|
||||
delete site.metadata?.raw
|
||||
|
||||
return site
|
||||
}
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
|
||||
export const expandCrossbellNote = async (
|
||||
page: ExpandedNote,
|
||||
note: NoteEntity,
|
||||
useStat?: boolean,
|
||||
useScore?: boolean,
|
||||
keyword?: string,
|
||||
) => {
|
||||
if (page.metadata?.content) {
|
||||
if (page.metadata?.content?.content) {
|
||||
const expandedNote: ExpandedNote = Object.assign(
|
||||
{
|
||||
metadata: {
|
||||
content: {},
|
||||
},
|
||||
},
|
||||
note,
|
||||
)
|
||||
|
||||
if (expandedNote.metadata?.content) {
|
||||
if (expandedNote.metadata?.content?.content) {
|
||||
const { renderPageContent } = await import("~/markdown")
|
||||
const rendered = renderPageContent(page.metadata.content.content, true)
|
||||
const rendered = renderPageContent(
|
||||
expandedNote.metadata.content.content,
|
||||
true,
|
||||
)
|
||||
if (keyword) {
|
||||
const position = page.metadata.content.content
|
||||
const position = expandedNote.metadata.content.content
|
||||
.toLowerCase()
|
||||
.indexOf(keyword.toLowerCase())
|
||||
page.metadata.content.summary = `...${page.metadata.content.content.slice(
|
||||
expandedNote.metadata.content.summary = `...${expandedNote.metadata.content.content.slice(
|
||||
position - 10,
|
||||
position + 100,
|
||||
)}`
|
||||
} else {
|
||||
if (!page.metadata.content.summary) {
|
||||
page.metadata.content.summary = rendered.excerpt
|
||||
if (!expandedNote.metadata.content.summary) {
|
||||
expandedNote.metadata.content.summary = rendered.excerpt
|
||||
}
|
||||
}
|
||||
page.metadata.content.cover = rendered.cover
|
||||
if (page.metadata) {
|
||||
page.metadata.content.frontMatter = rendered.frontMatter
|
||||
}
|
||||
expandedNote.metadata.content.cover = rendered.cover
|
||||
expandedNote.metadata.content.audio = rendered.audio
|
||||
expandedNote.metadata.content.frontMatter = rendered.frontMatter
|
||||
}
|
||||
page.metadata.content.slug = encodeURIComponent(
|
||||
page.metadata.content.attributes?.find(
|
||||
expandedNote.metadata.content.slug = encodeURIComponent(
|
||||
expandedNote.metadata.content.attributes?.find(
|
||||
(a) => a.trait_type === "xlog_slug",
|
||||
)?.value || "",
|
||||
)
|
||||
|
|
@ -125,10 +53,10 @@ export const expandCrossbellNote = async (
|
|||
if (useStat) {
|
||||
const stat = await (
|
||||
await fetch(
|
||||
`https://indexer.crossbell.io/v1/stat/notes/${page.characterId}/${page.noteId}`,
|
||||
`https://indexer.crossbell.io/v1/stat/notes/${expandedNote.characterId}/${expandedNote.noteId}`,
|
||||
)
|
||||
).json()
|
||||
page.metadata.content.views = stat.viewDetailCount
|
||||
expandedNote.metadata.content.views = stat.viewDetailCount
|
||||
}
|
||||
|
||||
if (useScore) {
|
||||
|
|
@ -137,17 +65,63 @@ export const expandCrossbellNote = async (
|
|||
await (
|
||||
await fetch(
|
||||
`${SCORE_API_DOMAIN || SITE_URL}/api/score?cid=${toCid(
|
||||
page.metadata?.uri || "",
|
||||
expandedNote.metadata?.uri || "",
|
||||
)}`,
|
||||
)
|
||||
).json()
|
||||
).data
|
||||
page.metadata.content.score = score
|
||||
expandedNote.metadata.content.score = score
|
||||
} catch (e) {
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return page
|
||||
return expandedNote
|
||||
}
|
||||
|
||||
export const expandCrossbellCharacter = (site: CharacterEntity) => {
|
||||
const expandedCharacter: ExpandedCharacter = Object.assign(
|
||||
{
|
||||
metadata: {
|
||||
content: {},
|
||||
},
|
||||
},
|
||||
site,
|
||||
)
|
||||
|
||||
expandedCharacter.metadata.content.navigation = JSON.parse(
|
||||
(expandedCharacter.metadata?.content?.attributes?.find(
|
||||
(a: any) => a.trait_type === "xlog_navigation",
|
||||
)?.value as string) || "null",
|
||||
) || [{ id: nanoid(), label: "Archives", url: "/archives" }]
|
||||
expandedCharacter.metadata.content.css =
|
||||
expandedCharacter.metadata?.content?.attributes?.find(
|
||||
(a: any) => a.trait_type === "xlog_css",
|
||||
)?.value as string
|
||||
expandedCharacter.metadata.content.ga =
|
||||
(expandedCharacter.metadata?.content?.attributes?.find(
|
||||
(a: any) => a.trait_type === "xlog_ga",
|
||||
)?.value as string) || ""
|
||||
expandedCharacter.metadata.content.custom_domain =
|
||||
(expandedCharacter.metadata?.content?.attributes?.find(
|
||||
(a: any) => a.trait_type === "xlog_custom_domain",
|
||||
)?.value as string) || ""
|
||||
expandedCharacter.metadata.content.name =
|
||||
expandedCharacter.metadata.content.name || expandedCharacter.handle
|
||||
|
||||
if (expandedCharacter.metadata.content.avatars) {
|
||||
expandedCharacter.metadata.content.avatars =
|
||||
expandedCharacter.metadata.content.avatars.map((avatar) =>
|
||||
toGateway(avatar),
|
||||
)
|
||||
}
|
||||
if (expandedCharacter.metadata.content.banners) {
|
||||
expandedCharacter.metadata.content.banners.map((banner) => {
|
||||
banner.address = toGateway(banner.address)
|
||||
return banner
|
||||
})
|
||||
}
|
||||
|
||||
return expandedCharacter
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { NoteEntity } from "crossbell.js"
|
||||
|
||||
import { Note, Profile } from "~/lib/types"
|
||||
import { ExpandedCharacter, ExpandedNote } from "~/lib/types"
|
||||
|
||||
import { IS_PROD, IS_VERCEL_PREVIEW } from "./constants"
|
||||
import { OUR_DOMAIN } from "./env"
|
||||
|
|
@ -51,8 +51,10 @@ export const getNoteSlug = (note: NoteEntity) => {
|
|||
)?.toLowerCase?.()
|
||||
}
|
||||
|
||||
export const getNoteSlugFromNote = (page: Note) => {
|
||||
return page.attributes?.find(($) => $.trait_type === "xlog_slug")?.value
|
||||
export const getNoteSlugFromNote = (page: ExpandedNote) => {
|
||||
return page.metadata?.content?.attributes?.find(
|
||||
($) => $.trait_type === "xlog_slug",
|
||||
)?.value
|
||||
}
|
||||
|
||||
export const getTwitterShareUrl = ({
|
||||
|
|
@ -60,8 +62,8 @@ export const getTwitterShareUrl = ({
|
|||
site,
|
||||
t,
|
||||
}: {
|
||||
page: Note
|
||||
site: Profile
|
||||
page: ExpandedNote
|
||||
site: ExpandedCharacter
|
||||
t: (key: string, options?: any) => string
|
||||
}) => {
|
||||
const slug = getNoteSlugFromNote(page)
|
||||
|
|
@ -71,13 +73,13 @@ export const getTwitterShareUrl = ({
|
|||
}
|
||||
|
||||
return `https://twitter.com/intent/tweet?url=${getSiteLink({
|
||||
subdomain: site.username!,
|
||||
domain: site.custom_domain,
|
||||
subdomain: site.handle!,
|
||||
domain: site.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(slug)}&via=_xLog&text=${encodeURIComponent(
|
||||
t(
|
||||
`Published a new post on my blockchain blog: {{title}}. Check it out now!`,
|
||||
{
|
||||
title: page.title,
|
||||
title: page.metadata?.content?.title,
|
||||
},
|
||||
),
|
||||
)}`
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ export const getJsonFeed = async (domainOrSubdomain: string, path: string) => {
|
|||
const site = await fetchGetSite(domainOrSubdomain, queryClient)
|
||||
const pages = await fetchGetPagesBySite(
|
||||
{
|
||||
site: domainOrSubdomain,
|
||||
characterId: site?.characterId,
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
keepBody: true,
|
||||
|
|
@ -24,48 +24,48 @@ export const getJsonFeed = async (domainOrSubdomain: string, path: string) => {
|
|||
queryClient,
|
||||
)
|
||||
|
||||
const hasAudio = pages.list?.find((page: any) => page.audio)
|
||||
const hasAudio = pages.list?.find((page) => page.metadata?.content?.audio)
|
||||
|
||||
const link = getSiteLink({
|
||||
subdomain: site.username || "",
|
||||
subdomain: site?.handle || "",
|
||||
})
|
||||
return {
|
||||
version: "https://jsonfeed.org/version/1",
|
||||
title: site.name,
|
||||
description: site.description,
|
||||
icon: site.avatars?.[0],
|
||||
title: site?.metadata?.content?.name,
|
||||
description: site?.metadata?.content?.bio,
|
||||
icon: site?.metadata?.content?.avatars?.[0],
|
||||
home_page_url: link,
|
||||
feed_url: `${link}${path}`,
|
||||
...(hasAudio && {
|
||||
_itunes: {
|
||||
image: site.avatars?.[0],
|
||||
author: site.name,
|
||||
summary: site.description,
|
||||
image: site?.metadata?.content?.avatars?.[0],
|
||||
author: site?.metadata?.content?.name,
|
||||
summary: site?.metadata?.content?.bio,
|
||||
},
|
||||
}),
|
||||
items: pages.list?.map((page: any) => ({
|
||||
id: page.id,
|
||||
title: page.title,
|
||||
items: pages.list?.map((page) => ({
|
||||
id: page.characterId + "-" + page.noteId,
|
||||
title: page.metadata?.content?.title,
|
||||
content_html:
|
||||
page.body?.content &&
|
||||
renderPageContent(page.body?.content, true).contentHTML,
|
||||
summary: page.summary?.content,
|
||||
url: `${link}/${page.slug || page.id}`,
|
||||
image: page.cover,
|
||||
date_published: page.date_published,
|
||||
date_modified: page.date_updated,
|
||||
tags: page.tags,
|
||||
author: site.name,
|
||||
...(page.audio && {
|
||||
page.metadata?.content?.content &&
|
||||
renderPageContent(page.metadata?.content?.content, true).contentHTML,
|
||||
summary: page.metadata?.content?.summary,
|
||||
url: `/api/redirection?characterId=${page.characterId}¬eId=${page.noteId}`,
|
||||
image: page.metadata?.content?.cover,
|
||||
date_published: page.metadata?.content?.date_published,
|
||||
date_modified: page.updatedAt,
|
||||
tags: page.metadata?.content?.tags,
|
||||
author: site?.metadata?.content?.name,
|
||||
...(page.metadata?.content?.audio && {
|
||||
_itunes: {
|
||||
image: page.cover,
|
||||
summary: page.summary?.content,
|
||||
image: page.metadata?.content?.cover,
|
||||
summary: page.metadata?.content?.summary,
|
||||
},
|
||||
attachments: [
|
||||
{
|
||||
url: page.audio,
|
||||
url: page.metadata?.content?.audio,
|
||||
mime_type: "audio/mpeg",
|
||||
title: page.title,
|
||||
title: page.metadata?.content?.title,
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,20 +1,14 @@
|
|||
import dayjs from "dayjs"
|
||||
|
||||
import { PageVisibilityEnum } from "./types"
|
||||
import { ExpandedNote, PageVisibilityEnum } from "./types"
|
||||
|
||||
export const getPageVisibility = ({
|
||||
date_published,
|
||||
metadata,
|
||||
preview,
|
||||
}: {
|
||||
date_published?: string
|
||||
metadata?: Object
|
||||
preview?: boolean
|
||||
}) => {
|
||||
if (!metadata) {
|
||||
export const getPageVisibility = (note?: ExpandedNote) => {
|
||||
if (!note?.noteId) {
|
||||
return PageVisibilityEnum.Draft
|
||||
} else if (dayjs(date_published).isBefore(new Date())) {
|
||||
if (preview) {
|
||||
} else if (
|
||||
dayjs(note.metadata?.content?.date_published).isBefore(new Date())
|
||||
) {
|
||||
if (note.local) {
|
||||
return PageVisibilityEnum.Modified
|
||||
} else {
|
||||
return PageVisibilityEnum.Published
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ let redisPromise: Promise<Redis | null> = new Promise((resolve, reject) => {
|
|||
export const getRedis = () => redisPromise
|
||||
|
||||
export async function cacheGet(options: {
|
||||
key: string | (Record<string, any> | string | undefined)[]
|
||||
key: string | (Record<string, any> | string | undefined | number)[]
|
||||
getValueFun: () => Promise<any>
|
||||
noUpdate?: boolean
|
||||
}) {
|
||||
|
|
@ -59,7 +59,7 @@ export async function cacheGet(options: {
|
|||
} else {
|
||||
const value = await options.getValueFun()
|
||||
if (options.noUpdate) {
|
||||
redis.set(redisKey, JSON.stringify(value))
|
||||
await redis.set(redisKey, JSON.stringify(value))
|
||||
} else {
|
||||
redis.set(redisKey, JSON.stringify(value), "EX", REDIS_EXPIRE)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
import { NoteEntity } from "crossbell.js"
|
||||
import { ReactElement } from "react"
|
||||
import type { Note as UniNote, Profile as UniProfile } from "unidata.js"
|
||||
import { CharacterEntity, NoteEntity } from "crossbell.js"
|
||||
|
||||
export type Site = {
|
||||
id: string
|
||||
|
|
@ -26,7 +24,6 @@ export enum PageVisibilityEnum {
|
|||
Published = "published",
|
||||
Scheduled = "scheduled",
|
||||
Draft = "draft",
|
||||
Crossbell = "crossbell",
|
||||
Modified = "published and local modified",
|
||||
}
|
||||
|
||||
|
|
@ -75,22 +72,8 @@ export type SiteNavigationItem = {
|
|||
url: string
|
||||
}
|
||||
|
||||
export type Note = UniNote & {
|
||||
slug?: string
|
||||
character?: Profile
|
||||
cover?: string
|
||||
audio?: string
|
||||
body?: {
|
||||
content?: string
|
||||
address?: string
|
||||
mime_type?: string
|
||||
element?: ReactElement
|
||||
}
|
||||
preview?: boolean
|
||||
views?: number
|
||||
}
|
||||
|
||||
export type ExpandedNote = NoteEntity & {
|
||||
draftKey?: string
|
||||
metadata: {
|
||||
content: {
|
||||
summary?: string
|
||||
|
|
@ -98,6 +81,7 @@ export type ExpandedNote = NoteEntity & {
|
|||
frontMatter?: Record<string, any>
|
||||
slug?: string
|
||||
views?: number
|
||||
audio?: string
|
||||
score?: {
|
||||
number?: number
|
||||
reason?: string
|
||||
|
|
@ -108,23 +92,16 @@ export type ExpandedNote = NoteEntity & {
|
|||
viewDetailCount: number
|
||||
hotScore?: number
|
||||
}
|
||||
local?: boolean
|
||||
}
|
||||
|
||||
export type Notes = {
|
||||
total: number
|
||||
list: Note[]
|
||||
cursor?: string
|
||||
}
|
||||
|
||||
export type Profile = UniProfile & {
|
||||
navigation?: SiteNavigationItem[]
|
||||
css?: string
|
||||
ga?: string
|
||||
custom_domain?: string
|
||||
description?: string
|
||||
}
|
||||
|
||||
export type Profiles = {
|
||||
total: number
|
||||
list: Note[]
|
||||
export type ExpandedCharacter = CharacterEntity & {
|
||||
metadata: {
|
||||
content: {
|
||||
navigation?: SiteNavigationItem[]
|
||||
css?: string
|
||||
ga?: string
|
||||
custom_domain?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,38 +1,39 @@
|
|||
import { NoteMetadata } from "crossbell.js"
|
||||
import type { Contract } from "crossbell.js"
|
||||
import type {
|
||||
CharacterEntity,
|
||||
Contract,
|
||||
ListResponse,
|
||||
MintedNoteEntity,
|
||||
NoteEntity,
|
||||
} from "crossbell.js"
|
||||
import type Unidata from "unidata.js"
|
||||
|
||||
import { GeneralAccount } from "@crossbell/connect-kit"
|
||||
import { indexer } from "@crossbell/indexer"
|
||||
|
||||
import { expandCrossbellNote, expandUnidataNote } from "~/lib/expand-unit"
|
||||
import { expandCrossbellNote } from "~/lib/expand-unit"
|
||||
import { notFound } from "~/lib/server-side-props"
|
||||
import { checkSlugReservedWords } from "~/lib/slug-reserved-words"
|
||||
import { getKeys, getStorage } from "~/lib/storage"
|
||||
import { ExpandedNote, Note, Notes, PageVisibilityEnum } from "~/lib/types"
|
||||
import unidata from "~/queries/unidata.server"
|
||||
import { ExpandedNote, PageVisibilityEnum } from "~/lib/types"
|
||||
|
||||
export async function checkPageSlug(
|
||||
input: {
|
||||
slug: string
|
||||
site: string
|
||||
pageId?: string
|
||||
},
|
||||
customUnidata?: Unidata,
|
||||
) {
|
||||
export async function checkPageSlug(input: {
|
||||
slug: string
|
||||
characterId?: number
|
||||
}) {
|
||||
if (!input.characterId) {
|
||||
return "Character not found"
|
||||
}
|
||||
const reserved = checkSlugReservedWords(input.slug)
|
||||
if (reserved) {
|
||||
return reserved
|
||||
} else {
|
||||
try {
|
||||
const page = await getPage(
|
||||
{
|
||||
page: input.slug,
|
||||
site: input.site,
|
||||
},
|
||||
customUnidata,
|
||||
)
|
||||
if (page && page.id !== input.pageId) {
|
||||
const page = await getPage({
|
||||
slug: input.slug,
|
||||
characterId: input.characterId,
|
||||
})
|
||||
if (page) {
|
||||
return `Slug "${input.slug}" has already been used`
|
||||
}
|
||||
} catch (error) {}
|
||||
|
|
@ -58,6 +59,8 @@ export async function createOrUpdatePage(
|
|||
customUnidata?: Unidata,
|
||||
newbieToken?: string,
|
||||
) {
|
||||
const { default: unidata } = await import("~/queries/unidata.server")
|
||||
|
||||
if (!input.published) {
|
||||
return await (customUnidata || unidata).notes.set(
|
||||
{
|
||||
|
|
@ -129,7 +132,7 @@ export async function createOrUpdatePage(
|
|||
export async function postNotes(
|
||||
input: {
|
||||
siteId: string
|
||||
characterId: string
|
||||
characterId: number
|
||||
notes: NoteMetadata[]
|
||||
},
|
||||
contract?: Contract,
|
||||
|
|
@ -142,163 +145,172 @@ export async function postNotes(
|
|||
)
|
||||
}
|
||||
|
||||
const getLocalPages = (input: { site: string; isPost?: boolean }) => {
|
||||
const pages: Note[] = []
|
||||
getKeys(`draft-${input.site}-`).forEach((key) => {
|
||||
const getLocalPages = (input: { characterId: number; isPost?: boolean }) => {
|
||||
const pages: ExpandedNote[] = []
|
||||
getKeys(`draft-${input.characterId}-`).forEach((key) => {
|
||||
const page = getStorage(key)
|
||||
if (input.isPost === undefined || page.isPost === input.isPost) {
|
||||
pages.push({
|
||||
id: key.replace(`draft-${input.site}-`, ""),
|
||||
title: page.values?.title,
|
||||
body: {
|
||||
content: page.values?.content,
|
||||
mime_type: "text/markdown",
|
||||
const note: ExpandedNote = {
|
||||
characterId: input.characterId,
|
||||
noteId: 0,
|
||||
draftKey: key.replace(`draft-${input.characterId}-`, ""),
|
||||
linkItemType: null,
|
||||
linkKey: "",
|
||||
toCharacterId: null,
|
||||
toAddress: null,
|
||||
toNoteId: null,
|
||||
toHeadCharacterId: null,
|
||||
toHeadNoteId: null,
|
||||
toContractAddress: null,
|
||||
toTokenId: null,
|
||||
toLinklistId: null,
|
||||
toUri: null,
|
||||
deleted: false,
|
||||
locked: false,
|
||||
contractAddress: null,
|
||||
uri: null,
|
||||
operator: "",
|
||||
owner: "",
|
||||
createdAt: new Date(page.date).toISOString(),
|
||||
updatedAt: new Date(page.date).toISOString(),
|
||||
deletedAt: null,
|
||||
transactionHash: "",
|
||||
blockNumber: 0,
|
||||
logIndex: 0,
|
||||
updatedTransactionHash: "",
|
||||
updatedBlockNumber: 0,
|
||||
updatedLogIndex: 0,
|
||||
metadata: {
|
||||
content: {
|
||||
title: page.values?.title,
|
||||
content: page.values?.content,
|
||||
date_published: page.values?.publishedAt,
|
||||
summary: page.values?.excerpt,
|
||||
tags: [
|
||||
page.isPost ? "post" : "page",
|
||||
...(page.values?.tags
|
||||
?.split(",")
|
||||
.map((tag: string) => tag.trim())
|
||||
.filter((tag: string) => tag) || []),
|
||||
],
|
||||
sources: ["xlog"],
|
||||
},
|
||||
},
|
||||
date_updated: new Date(page.date).toISOString(),
|
||||
date_published: page.values?.publishedAt,
|
||||
summary: {
|
||||
content: page.values?.excerpt,
|
||||
mime_type: "text/markdown",
|
||||
},
|
||||
tags: [
|
||||
page.isPost ? "post" : "page",
|
||||
...(page.values?.tags
|
||||
?.split(",")
|
||||
.map((tag: string) => tag.trim())
|
||||
.filter((tag: string) => tag) || []),
|
||||
],
|
||||
applications: ["xlog"],
|
||||
...(page.values?.slug && {
|
||||
attributes: [
|
||||
{
|
||||
trait_type: "xlog_slug",
|
||||
value: page.values?.slug,
|
||||
},
|
||||
],
|
||||
}),
|
||||
preview: true,
|
||||
})
|
||||
local: true,
|
||||
}
|
||||
pages.push(note)
|
||||
}
|
||||
})
|
||||
return pages
|
||||
}
|
||||
|
||||
export async function getPagesBySite(
|
||||
input: {
|
||||
site?: string
|
||||
type: "post" | "page"
|
||||
visibility?: PageVisibilityEnum | null
|
||||
take?: number | null
|
||||
cursor?: string | null
|
||||
tags?: string[]
|
||||
useStat?: boolean
|
||||
keepBody?: boolean
|
||||
},
|
||||
customUnidata?: Unidata,
|
||||
) {
|
||||
if (!input.site) {
|
||||
export async function getPagesBySite(input: {
|
||||
characterId?: number
|
||||
type: "post" | "page"
|
||||
visibility?: PageVisibilityEnum
|
||||
limit?: number
|
||||
cursor?: string
|
||||
tags?: string[]
|
||||
useStat?: boolean
|
||||
keepBody?: boolean
|
||||
}) {
|
||||
if (!input.characterId) {
|
||||
return {
|
||||
total: 0,
|
||||
count: 0,
|
||||
list: [],
|
||||
cursor: null,
|
||||
}
|
||||
}
|
||||
|
||||
const visibility = input.visibility || PageVisibilityEnum.All
|
||||
const crossbell = visibility === PageVisibilityEnum.Crossbell
|
||||
|
||||
const options = {
|
||||
source: "Crossbell Note",
|
||||
identity: input.site,
|
||||
platform: "Crossbell",
|
||||
limit: input.take || 10,
|
||||
order_by: "date_published" as "date_published",
|
||||
filter: {
|
||||
tags: [...(input.tags || []), ...(crossbell ? [] : [input.type])],
|
||||
applications: [...(crossbell ? [] : ["xlog"])],
|
||||
},
|
||||
...(input.cursor && { cursor: input.cursor }),
|
||||
}
|
||||
const notes = await indexer.getNotes({
|
||||
characterId: input.characterId,
|
||||
limit: input.limit || 10,
|
||||
cursor: input.cursor,
|
||||
orderBy: "publishedAt",
|
||||
tags: [...(input.tags || []), input.type],
|
||||
sources: "xlog",
|
||||
})
|
||||
|
||||
let pages: Notes = {
|
||||
total: 0,
|
||||
list: [],
|
||||
}
|
||||
|
||||
pages = (await (customUnidata || unidata).notes.get(options)) || {
|
||||
total: 0,
|
||||
list: [],
|
||||
}
|
||||
|
||||
if (!crossbell) {
|
||||
const local = getLocalPages({
|
||||
site: input.site,
|
||||
isPost: input.type === "post",
|
||||
})
|
||||
local.forEach((localPage) => {
|
||||
const index = pages.list.findIndex((page) => page.id === localPage.id)
|
||||
if (
|
||||
index >= 0 &&
|
||||
new Date(localPage.date_updated) >
|
||||
new Date(pages.list[index].date_updated)
|
||||
) {
|
||||
localPage.metadata = pages.list[index].metadata
|
||||
pages.list[index] = localPage
|
||||
} else {
|
||||
pages.list.push(localPage)
|
||||
pages.total++
|
||||
const list = await Promise.all(
|
||||
notes?.list.map(async (note) => {
|
||||
const expanded = await expandCrossbellNote(note, input.useStat)
|
||||
if (!input.keepBody) {
|
||||
delete expanded.metadata?.content?.content
|
||||
}
|
||||
})
|
||||
pages.list = pages.list.sort(
|
||||
(a, b) => +new Date(b.date_published) - +new Date(a.date_published),
|
||||
)
|
||||
}
|
||||
return expanded
|
||||
}),
|
||||
)
|
||||
|
||||
if (pages?.list) {
|
||||
switch (visibility) {
|
||||
case PageVisibilityEnum.Published:
|
||||
pages.list = pages.list.filter(
|
||||
(page) =>
|
||||
+new Date(page.date_published) <= +new Date() && page.metadata,
|
||||
)
|
||||
break
|
||||
case PageVisibilityEnum.Draft:
|
||||
pages.list = pages.list.filter((page) => !page.metadata)
|
||||
break
|
||||
case PageVisibilityEnum.Scheduled:
|
||||
pages.list = pages.list.filter(
|
||||
(page) => +new Date(page.date_published) > +new Date(),
|
||||
)
|
||||
break
|
||||
case PageVisibilityEnum.Crossbell:
|
||||
pages.list = pages.list.filter(
|
||||
(page) => !page.applications?.includes("xlog"),
|
||||
)
|
||||
break
|
||||
const expandedNotes: {
|
||||
list: ExpandedNote[]
|
||||
count: number
|
||||
cursor: string | null
|
||||
} = Object.assign(notes, {
|
||||
list,
|
||||
})
|
||||
|
||||
const local = getLocalPages({
|
||||
characterId: input.characterId,
|
||||
isPost: input.type === "post",
|
||||
})
|
||||
|
||||
local.forEach((localPage) => {
|
||||
const index = expandedNotes.list.findIndex(
|
||||
(page) => localPage.draftKey === page.noteId + "",
|
||||
)
|
||||
if (
|
||||
index !== -1 &&
|
||||
new Date(localPage.updatedAt) >
|
||||
new Date(expandedNotes.list[index].updatedAt)
|
||||
) {
|
||||
expandedNotes.list[index] = {
|
||||
...expandedNotes.list[index],
|
||||
metadata: {
|
||||
content: localPage.metadata?.content,
|
||||
},
|
||||
local: true,
|
||||
}
|
||||
} else {
|
||||
expandedNotes.list.push(localPage)
|
||||
expandedNotes.count++
|
||||
}
|
||||
const allLength = pages.list.length
|
||||
pages.list = pages.list.filter(
|
||||
(page) => page.date_published !== new Date("9999-01-01").toISOString(),
|
||||
)
|
||||
pages.total = pages.total - (allLength - pages.list.length)
|
||||
})
|
||||
|
||||
await Promise.all(
|
||||
pages?.list.map(async (page) => {
|
||||
await expandUnidataNote(page, input.useStat)
|
||||
|
||||
if (!input.keepBody) {
|
||||
delete page.body
|
||||
}
|
||||
|
||||
return page
|
||||
}),
|
||||
)
|
||||
switch (visibility) {
|
||||
case PageVisibilityEnum.Published:
|
||||
expandedNotes.list = expandedNotes.list.filter(
|
||||
(page) =>
|
||||
(!page.metadata?.content?.date_published ||
|
||||
+new Date(page.metadata?.content?.date_published) <= +new Date()) &&
|
||||
page.noteId,
|
||||
)
|
||||
break
|
||||
case PageVisibilityEnum.Draft:
|
||||
expandedNotes.list = expandedNotes.list.filter((page) => !page.noteId)
|
||||
break
|
||||
case PageVisibilityEnum.Scheduled:
|
||||
expandedNotes.list = expandedNotes.list.filter(
|
||||
(page) =>
|
||||
page.metadata?.content?.date_published &&
|
||||
+new Date(page.metadata?.content?.date_published) > +new Date(),
|
||||
)
|
||||
break
|
||||
}
|
||||
|
||||
return pages
|
||||
expandedNotes.list = expandedNotes.list.sort((a, b) =>
|
||||
a.metadata?.content?.date_published && b.metadata?.content?.date_published
|
||||
? +new Date(b.metadata?.content?.date_published) -
|
||||
+new Date(a.metadata?.content?.date_published)
|
||||
: 0,
|
||||
)
|
||||
|
||||
return expandedNotes
|
||||
}
|
||||
|
||||
export async function getSearchPagesBySite(input: {
|
||||
characterId?: string
|
||||
characterId?: number
|
||||
keyword?: string
|
||||
cursor?: string
|
||||
}) {
|
||||
|
|
@ -323,80 +335,66 @@ export async function getSearchPagesBySite(input: {
|
|||
}
|
||||
}
|
||||
|
||||
export async function getPage<TRender extends boolean = false>(
|
||||
input: {
|
||||
/** page slug or id, `site` is needed when `page` is a slug */
|
||||
page?: string
|
||||
pageId?: string
|
||||
site?: string
|
||||
useStat?: boolean
|
||||
},
|
||||
customUnidata?: Unidata,
|
||||
) {
|
||||
if (!input.site || !(input.page || input.pageId)) {
|
||||
return null
|
||||
}
|
||||
export async function getPage<TRender extends boolean = false>(input: {
|
||||
slug?: string
|
||||
characterId: number
|
||||
useStat?: boolean
|
||||
noteId?: number
|
||||
}) {
|
||||
const mustLocal = input.slug?.startsWith("local-")
|
||||
|
||||
const mustLocal = input.pageId?.startsWith("local-")
|
||||
|
||||
let page: Note | null = null
|
||||
let page: NoteEntity | null = null
|
||||
|
||||
if (!mustLocal) {
|
||||
// on-chain page
|
||||
if (!input.pageId) {
|
||||
if (!input.site || !input.page) return null
|
||||
if (!input.noteId) {
|
||||
const response = await fetch(
|
||||
`/api/slug2id?${new URLSearchParams({
|
||||
handle: input.site,
|
||||
slug: input.page,
|
||||
characterId: input.characterId + "",
|
||||
slug: input.slug!,
|
||||
}).toString()}`,
|
||||
)
|
||||
const slug2Id = await response.json()
|
||||
if (!slug2Id?.noteId) {
|
||||
return null
|
||||
}
|
||||
input.pageId = `${slug2Id.characterId}-${slug2Id.noteId}`
|
||||
input.noteId = (await response.json())?.noteId
|
||||
}
|
||||
if (input.noteId) {
|
||||
page = await indexer.getNote(input.characterId, input.noteId)
|
||||
}
|
||||
|
||||
const pages = await (customUnidata || unidata).notes.get({
|
||||
source: "Crossbell Note",
|
||||
identity: input.site,
|
||||
platform: "Crossbell",
|
||||
filter: {
|
||||
id: input.pageId,
|
||||
},
|
||||
})
|
||||
page = pages?.list[0] || null
|
||||
}
|
||||
|
||||
// local page
|
||||
const local = getLocalPages({
|
||||
site: input.site,
|
||||
characterId: input.characterId,
|
||||
})
|
||||
const localPage = local.find(
|
||||
(page) => page.id === input.page || page.id === input.pageId,
|
||||
(page) =>
|
||||
page.draftKey === input.noteId + "" || page.draftKey === input.slug,
|
||||
)
|
||||
|
||||
let expandedNote: ExpandedNote | undefined
|
||||
if (page) {
|
||||
expandedNote = await expandCrossbellNote(page, input.useStat)
|
||||
}
|
||||
|
||||
if (localPage) {
|
||||
if (page) {
|
||||
if (new Date(localPage.date_updated) > new Date(page.date_updated)) {
|
||||
localPage.metadata = page.metadata
|
||||
page = localPage
|
||||
if (expandedNote) {
|
||||
if (new Date(localPage.updatedAt) > new Date(expandedNote.updatedAt)) {
|
||||
expandedNote = {
|
||||
...expandedNote,
|
||||
metadata: {
|
||||
content: localPage.metadata?.content,
|
||||
},
|
||||
local: true,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
page = localPage
|
||||
expandedNote = localPage
|
||||
}
|
||||
}
|
||||
|
||||
if (!page && !mustLocal) {
|
||||
throw notFound(`page ${input.page} not found`)
|
||||
if (!expandedNote && !mustLocal) {
|
||||
throw notFound(`page ${input.slug} not found`)
|
||||
}
|
||||
|
||||
if (page) {
|
||||
await expandUnidataNote(page, input.useStat)
|
||||
}
|
||||
|
||||
return page
|
||||
return expandedNote
|
||||
}
|
||||
|
||||
export async function deletePage(
|
||||
|
|
@ -404,6 +402,8 @@ export async function deletePage(
|
|||
customUnidata?: Unidata,
|
||||
newbieToken?: string,
|
||||
) {
|
||||
const { default: unidata } = await import("~/queries/unidata.server")
|
||||
|
||||
return await (customUnidata || unidata).notes.set(
|
||||
{
|
||||
source: "Crossbell Note",
|
||||
|
|
@ -421,22 +421,20 @@ export async function deletePage(
|
|||
}
|
||||
|
||||
export async function getLikes({
|
||||
pageId,
|
||||
characterId,
|
||||
noteId,
|
||||
cursor,
|
||||
includeCharacter,
|
||||
}: {
|
||||
pageId: string
|
||||
characterId: number
|
||||
noteId: number
|
||||
cursor?: string
|
||||
includeCharacter?: boolean
|
||||
}) {
|
||||
const res = await indexer.getBacklinksOfNote(
|
||||
pageId.split("-")[0],
|
||||
pageId.split("-")[1],
|
||||
{
|
||||
linkType: "like",
|
||||
cursor,
|
||||
},
|
||||
)
|
||||
const res = await indexer.getBacklinksOfNote(characterId, noteId, {
|
||||
linkType: "like",
|
||||
cursor,
|
||||
})
|
||||
if (includeCharacter) {
|
||||
res.list?.forEach((item) => {
|
||||
;(item as any).character = item.fromCharacter
|
||||
|
|
@ -448,18 +446,20 @@ export async function getLikes({
|
|||
|
||||
export async function checkLike({
|
||||
account,
|
||||
pageId,
|
||||
characterId,
|
||||
noteId,
|
||||
}: {
|
||||
account: GeneralAccount
|
||||
pageId: string
|
||||
characterId: number
|
||||
noteId: number
|
||||
}) {
|
||||
if (!account.characterId) {
|
||||
throw notFound(`character not found`)
|
||||
} else {
|
||||
return indexer.getLinks(account.characterId, {
|
||||
linkType: "like",
|
||||
toCharacterId: pageId.split("-")[0],
|
||||
toNoteId: pageId.split("-")[1],
|
||||
toCharacterId: characterId,
|
||||
toNoteId: noteId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -467,33 +467,33 @@ export async function checkLike({
|
|||
export async function mintPage(
|
||||
{
|
||||
address,
|
||||
pageId,
|
||||
characterId,
|
||||
noteId,
|
||||
}: {
|
||||
address: string
|
||||
pageId: string
|
||||
characterId: number
|
||||
noteId: number
|
||||
},
|
||||
contract?: Contract,
|
||||
) {
|
||||
return contract?.mintNote(pageId.split("-")[0], pageId.split("-")[1], address)
|
||||
return contract?.mintNote(characterId, noteId, address)
|
||||
}
|
||||
|
||||
export async function getMints({
|
||||
pageId,
|
||||
characterId,
|
||||
noteId,
|
||||
cursor,
|
||||
includeCharacter,
|
||||
}: {
|
||||
pageId: string
|
||||
characterId: number
|
||||
noteId: number
|
||||
cursor?: string
|
||||
includeCharacter?: boolean
|
||||
}) {
|
||||
const data = await indexer.getMintedNotesOfNote(
|
||||
pageId.split("-")[0],
|
||||
pageId.split("-")[1],
|
||||
{
|
||||
cursor,
|
||||
limit: 5,
|
||||
},
|
||||
)
|
||||
const data = await indexer.getMintedNotesOfNote(characterId, noteId, {
|
||||
cursor,
|
||||
limit: 5,
|
||||
})
|
||||
|
||||
if (includeCharacter) {
|
||||
await Promise.all(
|
||||
|
|
@ -505,32 +505,40 @@ export async function getMints({
|
|||
)
|
||||
}
|
||||
|
||||
return data
|
||||
return data as ListResponse<
|
||||
MintedNoteEntity & {
|
||||
character: CharacterEntity
|
||||
}
|
||||
>
|
||||
}
|
||||
|
||||
export async function checkMint({
|
||||
address,
|
||||
pageId,
|
||||
noteCharacterId,
|
||||
noteId,
|
||||
}: {
|
||||
address: string
|
||||
pageId: string
|
||||
noteCharacterId: number
|
||||
noteId: number
|
||||
}) {
|
||||
return indexer.getMintedNotesOfAddress(address, {
|
||||
noteCharacterId: pageId.split("-")[0],
|
||||
noteId: pageId.split("-")[1],
|
||||
noteCharacterId: noteCharacterId,
|
||||
noteId: noteId,
|
||||
})
|
||||
}
|
||||
|
||||
export async function getComments({
|
||||
pageId,
|
||||
characterId,
|
||||
noteId,
|
||||
cursor,
|
||||
}: {
|
||||
pageId: string
|
||||
characterId: number
|
||||
noteId: number
|
||||
cursor?: string
|
||||
}) {
|
||||
const options = {
|
||||
toCharacterId: pageId.split("-")[0],
|
||||
toNoteId: pageId.split("-")[1],
|
||||
toCharacterId: characterId,
|
||||
toNoteId: noteId,
|
||||
cursor,
|
||||
includeCharacter: true,
|
||||
includeNestedNotes: true,
|
||||
|
|
@ -549,17 +557,13 @@ export async function getComments({
|
|||
|
||||
export async function updateComment(
|
||||
{
|
||||
pageId,
|
||||
content,
|
||||
externalUrl,
|
||||
originalId,
|
||||
characterId,
|
||||
noteId,
|
||||
}: {
|
||||
pageId: string
|
||||
content: string
|
||||
externalUrl: string
|
||||
originalId?: string
|
||||
characterId: number
|
||||
noteId: number
|
||||
},
|
||||
|
|
@ -573,19 +577,6 @@ export async function updateComment(
|
|||
})
|
||||
}
|
||||
|
||||
export function parsePageId(pageId: string) {
|
||||
const [characterId, noteId] = pageId.split("-").map(Number)
|
||||
|
||||
return { characterId, noteId }
|
||||
}
|
||||
|
||||
export function toPageId({
|
||||
characterId,
|
||||
noteId,
|
||||
}: ReturnType<typeof parsePageId>) {
|
||||
return `${characterId}-${noteId}`
|
||||
}
|
||||
|
||||
export async function getSummary({
|
||||
cid,
|
||||
lang,
|
||||
|
|
@ -598,10 +589,10 @@ export async function getSummary({
|
|||
).data
|
||||
}
|
||||
|
||||
export async function checkMirror(characterId: string) {
|
||||
export async function checkMirror(characterId: number) {
|
||||
const notes = await indexer.getNotes({
|
||||
characterId,
|
||||
sources: ["xlog"],
|
||||
sources: "xlog",
|
||||
tags: ["post", "Mirror.xyz"],
|
||||
limit: 0,
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,93 +1,31 @@
|
|||
import { CharacterOperatorPermission, Indexer } from "crossbell.js"
|
||||
import { nanoid } from "nanoid"
|
||||
import type Unidata from "unidata.js"
|
||||
import type { Profiles as UniProfiles } from "unidata.js"
|
||||
|
||||
import type { useContract } from "@crossbell/contract"
|
||||
import { cacheExchange, createClient, fetchExchange } from "@urql/core"
|
||||
|
||||
import { expandUnidataProfile } from "~/lib/expand-unit"
|
||||
import { Profile, SiteNavigationItem } from "~/lib/types"
|
||||
import unidata from "~/queries/unidata.server"
|
||||
import { expandCrossbellCharacter } from "~/lib/expand-unit"
|
||||
import { SiteNavigationItem } from "~/lib/types"
|
||||
|
||||
type Contract = ReturnType<typeof useContract>
|
||||
|
||||
const indexer = new Indexer()
|
||||
|
||||
export type GetUserSitesParams =
|
||||
| {
|
||||
address: string
|
||||
unidata?: Unidata
|
||||
}
|
||||
| {
|
||||
handle: string
|
||||
unidata?: Unidata
|
||||
}
|
||||
|
||||
export const getUserSites = async (params: GetUserSitesParams) => {
|
||||
let profiles: UniProfiles | null = null
|
||||
|
||||
try {
|
||||
const source = "Crossbell Profile"
|
||||
const filter = { primary: true }
|
||||
|
||||
if ("address" in params) {
|
||||
profiles = await (params.unidata || unidata).profiles.get({
|
||||
source,
|
||||
filter,
|
||||
identity: params.address,
|
||||
platform: "Ethereum",
|
||||
})
|
||||
}
|
||||
|
||||
if ("handle" in params) {
|
||||
profiles = await (params.unidata || unidata).profiles.get({
|
||||
source,
|
||||
filter,
|
||||
identity: params.handle,
|
||||
platform: "Crossbell",
|
||||
})
|
||||
}
|
||||
} catch (error) {
|
||||
return null
|
||||
export const getSite = async (input: string) => {
|
||||
const result = await indexer.getCharacterByHandle(input)
|
||||
if (result) {
|
||||
return expandCrossbellCharacter(result)
|
||||
}
|
||||
|
||||
const sites: Profile[] =
|
||||
profiles?.list?.map((profile) => {
|
||||
expandUnidataProfile(profile)
|
||||
return profile
|
||||
}) ?? []
|
||||
|
||||
return sites.length > 0 ? sites : null
|
||||
}
|
||||
|
||||
export type GetAccountSitesParams = {
|
||||
handle: string
|
||||
unidata?: Unidata
|
||||
}
|
||||
|
||||
export const getAccountSites = (
|
||||
params: GetAccountSitesParams,
|
||||
): Promise<Profile[] | null> => {
|
||||
return getUserSites({
|
||||
handle: params.handle,
|
||||
unidata: params.unidata,
|
||||
})
|
||||
}
|
||||
|
||||
export const getSite = async (input: string, customUnidata?: Unidata) => {
|
||||
const profiles = await (customUnidata || unidata).profiles.get({
|
||||
source: "Crossbell Profile",
|
||||
identity: input,
|
||||
platform: "Crossbell",
|
||||
export const getSiteByAddress = async (input: string) => {
|
||||
const result = await indexer.getCharacters(input, {
|
||||
primary: true,
|
||||
})
|
||||
|
||||
const site: Profile = profiles.list[0]
|
||||
if (site) {
|
||||
expandUnidataProfile(site)
|
||||
if (result?.list?.[0]) {
|
||||
return expandCrossbellCharacter(result.list[0])
|
||||
}
|
||||
|
||||
return site
|
||||
}
|
||||
|
||||
export const getSubscriptionsFromList = async (
|
||||
|
|
@ -124,64 +62,38 @@ export const getSubscriptionsFromList = async (
|
|||
return response.data?.links.map((link: any) => link.toCharacterId)
|
||||
}
|
||||
|
||||
export const getSubscription = async (
|
||||
siteId: string,
|
||||
handle: string,
|
||||
customUnidata?: Unidata,
|
||||
) => {
|
||||
const links = await (customUnidata || unidata).links.get({
|
||||
source: "Crossbell Link",
|
||||
identity: handle,
|
||||
platform: "Crossbell",
|
||||
filter: { to: siteId },
|
||||
export const getSubscription = async (input: {
|
||||
toCharacterId: number
|
||||
characterId: number
|
||||
}) => {
|
||||
const result = await indexer.getLinks(input.characterId, {
|
||||
linkType: "follow",
|
||||
toCharacterId: input.toCharacterId,
|
||||
})
|
||||
|
||||
return !!links?.list?.length
|
||||
return !!result?.list?.length
|
||||
}
|
||||
|
||||
export const getSiteSubscriptions = async (
|
||||
data: {
|
||||
siteId: string
|
||||
cursor?: string
|
||||
limit?: number
|
||||
},
|
||||
customUnidata?: Unidata,
|
||||
) => {
|
||||
const links = await (customUnidata || unidata).links.get({
|
||||
source: "Crossbell Link",
|
||||
identity: data.siteId,
|
||||
platform: "Crossbell",
|
||||
reversed: true,
|
||||
export const getSiteSubscriptions = async (data: {
|
||||
characterId: number
|
||||
cursor?: string
|
||||
limit?: number
|
||||
}) => {
|
||||
return indexer.getBacklinksOfCharacter(data.characterId, {
|
||||
linkType: "follow",
|
||||
cursor: data.cursor,
|
||||
limit: data.limit,
|
||||
})
|
||||
|
||||
links?.list.map(async (item: any) => {
|
||||
item.character = item.metadata.from_raw
|
||||
}) || []
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
export const getSiteToSubscriptions = async (
|
||||
data: {
|
||||
siteId: string
|
||||
cursor?: string
|
||||
},
|
||||
customUnidata?: Unidata,
|
||||
) => {
|
||||
const links = await (customUnidata || unidata).links.get({
|
||||
source: "Crossbell Link",
|
||||
identity: data.siteId,
|
||||
platform: "Crossbell",
|
||||
export const getSiteToSubscriptions = async (data: {
|
||||
characterId: number
|
||||
cursor?: string
|
||||
}) => {
|
||||
return indexer.getLinks(data.characterId, {
|
||||
linkType: "follow",
|
||||
cursor: data.cursor,
|
||||
})
|
||||
|
||||
links?.list.map(async (item: any) => {
|
||||
item.character = item.metadata.to_raw
|
||||
}) || []
|
||||
|
||||
return links
|
||||
}
|
||||
|
||||
export async function updateSite(
|
||||
|
|
@ -199,11 +111,17 @@ export async function updateSite(
|
|||
address: string
|
||||
mime_type: string
|
||||
}
|
||||
connected_accounts?: Profile["connected_accounts"]
|
||||
connected_accounts?: {
|
||||
identity: string
|
||||
platform: string
|
||||
url?: string | undefined
|
||||
}[]
|
||||
},
|
||||
customUnidata?: Unidata,
|
||||
newbieToken?: string,
|
||||
) {
|
||||
const { default: unidata } = await import("~/queries/unidata.server")
|
||||
|
||||
return await (customUnidata || unidata).profiles.set(
|
||||
{
|
||||
source: "Crossbell Profile",
|
||||
|
|
@ -266,56 +184,8 @@ export async function updateSite(
|
|||
)
|
||||
}
|
||||
|
||||
export async function createSite(
|
||||
address: string,
|
||||
payload: { name: string; subdomain: string },
|
||||
customUnidata?: Unidata,
|
||||
) {
|
||||
return await (customUnidata || unidata).profiles.set(
|
||||
{
|
||||
source: "Crossbell Profile",
|
||||
identity: address,
|
||||
platform: "Ethereum",
|
||||
action: "add",
|
||||
},
|
||||
{
|
||||
username: payload.subdomain,
|
||||
name: payload.name,
|
||||
tags: [
|
||||
"navigation:" +
|
||||
JSON.stringify([
|
||||
{
|
||||
id: nanoid(),
|
||||
label: "Archives",
|
||||
url: "/archives",
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export async function subscribeToSites(
|
||||
input: {
|
||||
user: Profile
|
||||
sites: {
|
||||
characterId: string
|
||||
}[]
|
||||
},
|
||||
contract?: Contract,
|
||||
) {
|
||||
if (input.user.metadata?.proof) {
|
||||
return contract?.linkCharactersInBatch(
|
||||
input.user.metadata.proof,
|
||||
input.sites.map((s) => s.characterId).filter((c) => c) as any,
|
||||
[],
|
||||
"follow",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getCommentsBySite(input: {
|
||||
characterId: string
|
||||
characterId?: number
|
||||
cursor?: string
|
||||
}) {
|
||||
const notes = await indexer.getNotes({
|
||||
|
|
@ -344,7 +214,7 @@ const xLogOperatorPermissions: CharacterOperatorPermission[] = [
|
|||
|
||||
export async function addOperator(
|
||||
input: {
|
||||
characterId: number
|
||||
characterId?: number
|
||||
operator: string
|
||||
},
|
||||
contract?: Contract,
|
||||
|
|
@ -416,15 +286,17 @@ export async function removeOperator(
|
|||
}
|
||||
}
|
||||
|
||||
export async function getNFTs(address: string, customUnidata?: Unidata) {
|
||||
const assets = await (customUnidata || unidata).assets.get({
|
||||
export async function getNFTs(address: string) {
|
||||
const { default: unidata } = await import("~/queries/unidata.server")
|
||||
|
||||
const assets = await unidata.assets.get({
|
||||
source: "Ethereum NFT",
|
||||
identity: address,
|
||||
})
|
||||
return assets
|
||||
}
|
||||
|
||||
export async function getStat({ characterId }: { characterId: string }) {
|
||||
export async function getStat({ characterId }: { characterId: number }) {
|
||||
if (characterId) {
|
||||
const [stat, site, subscriptions, comments, notes] = await Promise.all([
|
||||
(
|
||||
|
|
@ -571,7 +443,7 @@ export type AchievementSection = {
|
|||
}[]
|
||||
}
|
||||
|
||||
export async function getAchievements(characterId: string) {
|
||||
export async function getAchievements(characterId: number) {
|
||||
const crossbellAchievements = (await indexer.getAchievements(characterId))
|
||||
?.list as AchievementSection[] | undefined
|
||||
const xLogAchievements: AchievementSection[] = [
|
||||
|
|
@ -647,13 +519,13 @@ export async function getAchievements(characterId: string) {
|
|||
}
|
||||
|
||||
export async function mintAchievement(input: {
|
||||
characterId: string
|
||||
characterId: number
|
||||
achievementId: number
|
||||
}) {
|
||||
return indexer.mintAchievement(input.characterId, input.achievementId)
|
||||
}
|
||||
|
||||
export async function getMiraBalance(characterId: string, contract: Contract) {
|
||||
export async function getMiraBalance(characterId: number, contract: Contract) {
|
||||
const decimals = await getMiraTokenDecimals(contract)
|
||||
const result = await contract.getMiraBalanceOfCharacter(characterId)
|
||||
result.data = (
|
||||
|
|
|
|||
|
|
@ -41,14 +41,16 @@ function SitePagePage({
|
|||
domainOrSubdomain: string
|
||||
pageSlug: string
|
||||
}) {
|
||||
const site = useGetSite(domainOrSubdomain)
|
||||
const page = useGetPage({
|
||||
site: domainOrSubdomain,
|
||||
page: pageSlug,
|
||||
characterId: site.data?.characterId,
|
||||
slug: pageSlug,
|
||||
useStat: true,
|
||||
})
|
||||
const site = useGetSite(domainOrSubdomain)
|
||||
|
||||
return <SitePage page={page.data} site={site.data} />
|
||||
return (
|
||||
<SitePage page={page.data || undefined} site={site.data || undefined} />
|
||||
)
|
||||
}
|
||||
|
||||
SitePagePage.getLayout = (page: ReactElement) => {
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ 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 { Notes, PageVisibilityEnum, Profile } from "~/lib/types"
|
||||
import { PageVisibilityEnum } from "~/lib/types"
|
||||
import { useGetPagesBySiteLite } from "~/queries/page"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
||||
async (ctx) => {
|
||||
|
|
@ -18,7 +19,7 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
|||
ctx,
|
||||
queryClient,
|
||||
{
|
||||
take: 100,
|
||||
limit: 100,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -34,20 +35,19 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
|||
function SiteArchivesPage({
|
||||
domainOrSubdomain,
|
||||
}: {
|
||||
site: Profile
|
||||
posts: Notes
|
||||
domainOrSubdomain: string
|
||||
}) {
|
||||
const site = useGetSite(domainOrSubdomain)
|
||||
const posts = useGetPagesBySiteLite({
|
||||
site: domainOrSubdomain,
|
||||
take: 100,
|
||||
characterId: site.data?.characterId,
|
||||
limit: 100,
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
})
|
||||
|
||||
return (
|
||||
<SiteArchives
|
||||
postPages={posts.data?.pages}
|
||||
posts={posts.data}
|
||||
fetchNextPage={posts.fetchNextPage}
|
||||
hasNextPage={posts.hasNextPage}
|
||||
isFetchingNextPage={posts.isFetchingNextPage}
|
||||
|
|
|
|||
|
|
@ -17,20 +17,20 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||
const site = await fetchGetSite(domainOrSubdomain, queryClient)
|
||||
const comments = await fetchGetComments(
|
||||
{
|
||||
characterId: site?.metadata?.proof,
|
||||
characterId: site?.characterId,
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
|
||||
const link = getSiteLink({
|
||||
subdomain: site.username || "",
|
||||
subdomain: site?.handle || "",
|
||||
})
|
||||
|
||||
const data = {
|
||||
version: "https://jsonfeed.org/version/1",
|
||||
title: "Comments on " + site.name,
|
||||
description: site.description,
|
||||
icon: site.avatars?.[0],
|
||||
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) => {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ 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()
|
||||
|
|
@ -29,8 +30,9 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||
}
|
||||
|
||||
function SiteIndexPage({ domainOrSubdomain }: { domainOrSubdomain: string }) {
|
||||
const site = useGetSite(domainOrSubdomain)
|
||||
const posts = useGetPagesBySiteLite({
|
||||
site: domainOrSubdomain,
|
||||
characterId: site.data?.characterId,
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
useStat: true,
|
||||
|
|
@ -38,7 +40,7 @@ function SiteIndexPage({ domainOrSubdomain }: { domainOrSubdomain: string }) {
|
|||
|
||||
return (
|
||||
<SiteHome
|
||||
postPages={posts.data?.pages}
|
||||
posts={posts.data}
|
||||
fetchNextPage={posts.fetchNextPage}
|
||||
hasNextPage={posts.hasNextPage}
|
||||
isFetchingNextPage={posts.isFetchingNextPage}
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||
|
||||
ctx.res.write(
|
||||
JSON.stringify({
|
||||
name: site.name,
|
||||
short_name: site.name,
|
||||
description: site.description,
|
||||
name: site?.metadata?.content?.name,
|
||||
short_name: site?.metadata?.content?.name,
|
||||
description: site?.metadata?.content?.bio,
|
||||
icons: [
|
||||
{
|
||||
src: site.avatars?.[0] || "assets/logo.png",
|
||||
src: site?.metadata?.content?.avatars?.[0] || "assets/logo.png",
|
||||
type: "image/png",
|
||||
sizes: "any",
|
||||
},
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ function SiteNFTPage({ domainOrSubdomain }: { domainOrSubdomain: string }) {
|
|||
const site = useGetSite(domainOrSubdomain)
|
||||
const { t } = useTranslation(["common", "site"])
|
||||
|
||||
const nftsOrigin = useGetNFTs(site.data?.metadata?.owner)
|
||||
const nftsOrigin = useGetNFTs(site.data?.owner)
|
||||
|
||||
const [nfts, setNfts] = useState<Asset[]>([])
|
||||
useEffect(() => {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ 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 { getDefaultSlug } from "~/lib/default-slug"
|
||||
import { getSiteLink } from "~/lib/helpers"
|
||||
import { serverSidePropsHandler } from "~/lib/server-side-props"
|
||||
import { useGetPage } from "~/queries/page"
|
||||
|
|
@ -42,35 +41,26 @@ function SitePagePage() {
|
|||
const pageSlug = router.query.page as string
|
||||
const userRole = useUserRole(domainOrSubdomain)
|
||||
|
||||
const page = useGetPage({
|
||||
site: domainOrSubdomain,
|
||||
pageId: pageSlug,
|
||||
})
|
||||
|
||||
const site = useGetSite(domainOrSubdomain)
|
||||
|
||||
const page = useGetPage({
|
||||
characterId: site.data?.characterId,
|
||||
slug: pageSlug,
|
||||
})
|
||||
|
||||
if (userRole.isSuccess && !userRole.data && page.isSuccess) {
|
||||
router.push(
|
||||
`${getSiteLink({
|
||||
getSiteLink({
|
||||
subdomain: domainOrSubdomain,
|
||||
})}/${
|
||||
page.data?.slug ||
|
||||
getDefaultSlug(page.data?.title || "", page.data?.id || "")
|
||||
}`,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<SitePage
|
||||
page={
|
||||
page.data
|
||||
? {
|
||||
...page.data,
|
||||
preview: true,
|
||||
}
|
||||
: null
|
||||
}
|
||||
site={site.data}
|
||||
preview={true}
|
||||
page={page.data || undefined}
|
||||
site={site.data || undefined}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ function SiteSearchPage({ domainOrSubdomain }: { domainOrSubdomain: string }) {
|
|||
const { t } = useTranslation(["common"])
|
||||
|
||||
const posts = useGetSearchPagesBySite({
|
||||
characterId: site.data?.metadata?.proof,
|
||||
characterId: site.data?.characterId,
|
||||
keyword,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -18,17 +18,17 @@ export const getServerSideProps: GetServerSideProps = async (ctx) => {
|
|||
const site = await fetchGetSite(domainOrSubdomain, queryClient)
|
||||
const pages = await fetchGetPagesBySite(
|
||||
{
|
||||
site: domainOrSubdomain,
|
||||
characterId: site?.characterId,
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
take: 1000,
|
||||
limit: 1000,
|
||||
},
|
||||
queryClient,
|
||||
)
|
||||
|
||||
const link = getSiteLink({
|
||||
domain: site.custom_domain,
|
||||
subdomain: site.username || "",
|
||||
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">
|
||||
|
|
|
|||
|
|
@ -7,8 +7,9 @@ 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 { Notes, PageVisibilityEnum, Profile } from "~/lib/types"
|
||||
import { PageVisibilityEnum } from "~/lib/types"
|
||||
import { useGetPagesBySiteLite } from "~/queries/page"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
||||
async (ctx) => {
|
||||
|
|
@ -20,7 +21,7 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
|||
ctx,
|
||||
queryClient,
|
||||
{
|
||||
take: 100,
|
||||
limit: 100,
|
||||
},
|
||||
)
|
||||
|
||||
|
|
@ -38,14 +39,13 @@ function SiteTagPage({
|
|||
domainOrSubdomain,
|
||||
tag,
|
||||
}: {
|
||||
site: Profile
|
||||
posts: Notes
|
||||
domainOrSubdomain: string
|
||||
tag: string
|
||||
}) {
|
||||
const site = useGetSite(domainOrSubdomain)
|
||||
const posts = useGetPagesBySiteLite({
|
||||
site: domainOrSubdomain,
|
||||
take: 100,
|
||||
characterId: site.data?.characterId,
|
||||
limit: 100,
|
||||
type: "post",
|
||||
visibility: PageVisibilityEnum.Published,
|
||||
tags: [tag],
|
||||
|
|
@ -53,7 +53,7 @@ function SiteTagPage({
|
|||
|
||||
return (
|
||||
<SiteArchives
|
||||
postPages={posts.data?.pages}
|
||||
posts={posts.data}
|
||||
fetchNextPage={posts.fetchNextPage}
|
||||
hasNextPage={posts.hasNextPage}
|
||||
isFetchingNextPage={posts.isFetchingNextPage}
|
||||
|
|
|
|||
|
|
@ -10,10 +10,10 @@ export default async function handler(
|
|||
const query = req.query
|
||||
|
||||
const result = await getPagesBySite({
|
||||
site: query.site as string,
|
||||
characterId: +(query.characterId || 0) as number,
|
||||
type: query.type as "post" | "page",
|
||||
visibility: query.visibility as PageVisibilityEnum,
|
||||
take: query.take ? parseInt(query.take as string) : undefined,
|
||||
limit: query.take ? parseInt(query.take as string) : undefined,
|
||||
cursor: query.cursor as string,
|
||||
useStat: true,
|
||||
...(query.tags && {
|
||||
|
|
|
|||
|
|
@ -3,71 +3,49 @@ import { NextApiRequest, NextApiResponse } from "next"
|
|||
import { getNoteSlug } from "~/lib/helpers"
|
||||
import { cacheDelete, cacheGet } from "~/lib/redis.server"
|
||||
|
||||
export async function getIdBySlug(slug: string, handle: string) {
|
||||
export async function getIdBySlug(slug: string, characterId: string | number) {
|
||||
slug = (slug as string)?.toLowerCase?.()
|
||||
|
||||
const result = await cacheGet({
|
||||
key: ["slug2id", handle, slug],
|
||||
const result = (await cacheGet({
|
||||
key: ["slug2id", characterId, slug],
|
||||
getValueFun: async () => {
|
||||
let note
|
||||
let cursor = ""
|
||||
|
||||
const characterRes = await (
|
||||
await fetch(
|
||||
`https://indexer.crossbell.io/v1/handles/${handle}/character`,
|
||||
)
|
||||
).json()
|
||||
const cid = characterRes?.characterId
|
||||
|
||||
const noteIdMatch = slug.match(`^${cid}-(\\d+)$`)
|
||||
if (noteIdMatch?.[1]) {
|
||||
return {
|
||||
noteId: noteIdMatch?.[1],
|
||||
characterId: cid,
|
||||
}
|
||||
}
|
||||
|
||||
do {
|
||||
const response = await (
|
||||
await fetch(
|
||||
`https://indexer.crossbell.io/v1/notes?characterId=${cid}&sources=xlog&cursor=${cursor}&limit=100`,
|
||||
`https://indexer.crossbell.io/v1/notes?characterId=${characterId}&sources=xlog&cursor=${cursor}&limit=100`,
|
||||
)
|
||||
).json()
|
||||
cursor = response.cursor
|
||||
note = response?.list?.find(
|
||||
(item: any) =>
|
||||
slug === getNoteSlug(item) || slug === `${cid}-${item.noteId}`,
|
||||
slug === getNoteSlug(item) ||
|
||||
slug === `${characterId}-${item.noteId}`,
|
||||
)
|
||||
} while (!note && cursor)
|
||||
|
||||
if (note?.noteId) {
|
||||
return {
|
||||
noteId: note?.noteId,
|
||||
characterId: cid,
|
||||
}
|
||||
return {
|
||||
noteId: note?.noteId,
|
||||
}
|
||||
},
|
||||
noUpdate: true,
|
||||
})
|
||||
})) as {
|
||||
noteId: number
|
||||
}
|
||||
|
||||
// revalidate
|
||||
if (result) {
|
||||
const noteIdMatch = slug.match(`^${result.characterId}-(\\d+)$`)
|
||||
const noteIdMatch = slug.match(`^${characterId}-(\\d+)$`)
|
||||
if (!noteIdMatch?.[1]) {
|
||||
fetch(
|
||||
`https://indexer.crossbell.io/v1/characters/${result.characterId}/notes/${result.noteId}`,
|
||||
`https://indexer.crossbell.io/v1/characters/${characterId}/notes/${result.noteId}`,
|
||||
)
|
||||
.then((res) => res.json())
|
||||
.then((note) => {
|
||||
if ((note && getNoteSlug(note) !== slug) || note.deleted) {
|
||||
cacheDelete(["slug2id", handle, slug])
|
||||
}
|
||||
})
|
||||
fetch(`https://indexer.crossbell.io/v1/characters/${result.characterId}`)
|
||||
.then((res) => res.json())
|
||||
.then((character) => {
|
||||
if (character && character.handle !== handle) {
|
||||
cacheDelete(["slug2id", handle, slug])
|
||||
cacheDelete(["slug2id", characterId + "", slug])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
|
@ -80,12 +58,12 @@ export default async function handler(
|
|||
req: NextApiRequest,
|
||||
res: NextApiResponse,
|
||||
) {
|
||||
let { handle, slug } = req.query
|
||||
let { characterId, slug } = req.query
|
||||
|
||||
if (!slug || !handle) {
|
||||
if (!slug || !characterId) {
|
||||
res.status(400).send("Bad Request")
|
||||
return
|
||||
}
|
||||
|
||||
res.status(200).send(await getIdBySlug(slug as string, handle as string))
|
||||
res.status(200).send(await getIdBySlug(slug as string, characterId as string))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ export default function AchievementsPage() {
|
|||
const subdomain = router.query.subdomain as string
|
||||
const site = useGetSite(subdomain)
|
||||
|
||||
const achievement = useGetAchievements(site.data?.metadata?.proof)
|
||||
const achievement = useGetAchievements(site.data?.characterId)
|
||||
|
||||
return (
|
||||
<DashboardMain title="Achievements">
|
||||
|
|
@ -49,7 +49,7 @@ export default function AchievementsPage() {
|
|||
key={group.info.name}
|
||||
layoutId="achievements"
|
||||
size={80}
|
||||
characterId={site.data?.metadata?.proof}
|
||||
characterId={site.data?.characterId}
|
||||
isOwner={true}
|
||||
/>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,7 @@ export default function CommentsPage() {
|
|||
const site = useGetSite(subdomain)
|
||||
|
||||
const comments = useGetCommentsBySite({
|
||||
characterId: site.data?.metadata?.proof,
|
||||
characterId: site.data?.characterId,
|
||||
})
|
||||
|
||||
const feedUrl =
|
||||
|
|
|
|||
|
|
@ -101,42 +101,46 @@ export default function SubdomainEditor() {
|
|||
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-${subdomain}-local-${randomId}`
|
||||
key = `draft-${site.data?.characterId}-local-${randomId}`
|
||||
setDraftKey(key)
|
||||
queryClient.invalidateQueries(["getPagesBySite", subdomain])
|
||||
queryClient.invalidateQueries([
|
||||
"getPagesBySite",
|
||||
site.data?.characterId,
|
||||
])
|
||||
router.replace(
|
||||
`/dashboard/${subdomain}/editor?id=local-${randomId}&type=${router.query.type}`,
|
||||
)
|
||||
} else {
|
||||
key = `draft-${subdomain}-${pageId}`
|
||||
key = `draft-${site.data?.characterId}-${pageId}`
|
||||
}
|
||||
setDraftKey(key)
|
||||
setDefaultSlug(
|
||||
key
|
||||
.replace(`draft-${subdomain}-local-`, "")
|
||||
.replace(`draft-${subdomain}-`, ""),
|
||||
.replace(`draft-${site.data?.characterId}-local-`, "")
|
||||
.replace(`draft-${site.data?.characterId}-`, ""),
|
||||
)
|
||||
}
|
||||
}, [subdomain, pageId, queryClient, router])
|
||||
|
||||
const site = useGetSite(subdomain)
|
||||
}, [subdomain, pageId, queryClient, router, site.data?.characterId])
|
||||
|
||||
const page = useGetPage({
|
||||
site: subdomain!,
|
||||
pageId: pageId || draftKey.replace(`draft-${subdomain}-`, ""),
|
||||
characterId: site.data?.characterId,
|
||||
noteId: pageId && /\d+/.test(pageId) ? +pageId : undefined,
|
||||
slug: pageId || draftKey.replace(`draft-${site.data?.characterId}-`, ""),
|
||||
})
|
||||
|
||||
const [visibility, setVisibility] = useState<PageVisibilityEnum>()
|
||||
|
||||
useEffect(() => {
|
||||
if (page.isSuccess) {
|
||||
setVisibility(getPageVisibility(page.data || {}))
|
||||
setVisibility(getPageVisibility(page.data || undefined))
|
||||
}
|
||||
}, [page.isSuccess, page.data])
|
||||
|
||||
|
|
@ -167,7 +171,7 @@ export default function SubdomainEditor() {
|
|||
setDefaultSlug(
|
||||
getDefaultSlug(
|
||||
value as string,
|
||||
draftKey.replace(`draft-${subdomain}-`, ""),
|
||||
draftKey.replace(`draft-${site.data?.characterId}-`, ""),
|
||||
),
|
||||
)
|
||||
}
|
||||
|
|
@ -186,7 +190,10 @@ export default function SubdomainEditor() {
|
|||
values: newValues,
|
||||
isPost: isPost,
|
||||
})
|
||||
queryClient.invalidateQueries(["getPagesBySite", subdomain])
|
||||
queryClient.invalidateQueries([
|
||||
"getPagesBySite",
|
||||
site.data?.characterId,
|
||||
])
|
||||
}
|
||||
useEditorState.setState(newValues)
|
||||
},
|
||||
|
|
@ -201,8 +208,7 @@ export default function SubdomainEditor() {
|
|||
const savePage = async (published: boolean) => {
|
||||
const check = await checkPageSlug({
|
||||
slug: values.slug || defaultSlug,
|
||||
site: subdomain,
|
||||
pageId: pageId,
|
||||
characterId: site.data?.characterId,
|
||||
})
|
||||
if (check) {
|
||||
toast.error(check)
|
||||
|
|
@ -220,9 +226,10 @@ export default function SubdomainEditor() {
|
|||
(values.slug || defaultSlug) &&
|
||||
`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.custom_domain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(values.slug || defaultSlug)}`,
|
||||
applications: page.data?.applications,
|
||||
applications: page.data?.metadata?.content?.sources,
|
||||
characterId: site.data?.characterId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -234,10 +241,13 @@ export default function SubdomainEditor() {
|
|||
if (createOrUpdatePage.data?.code === 0) {
|
||||
if (draftKey) {
|
||||
delStorage(draftKey)
|
||||
queryClient.invalidateQueries(["getPagesBySite", subdomain])
|
||||
queryClient.invalidateQueries([
|
||||
"getPagesBySite",
|
||||
site.data?.characterId,
|
||||
])
|
||||
queryClient.invalidateQueries([
|
||||
"getPage",
|
||||
draftKey.replace(`draft-${subdomain}-`, ""),
|
||||
draftKey.replace(`draft-${site.data?.characterId}-`, ""),
|
||||
])
|
||||
} else {
|
||||
queryClient.invalidateQueries(["getPage", pageId])
|
||||
|
|
@ -245,7 +255,7 @@ export default function SubdomainEditor() {
|
|||
|
||||
if (createOrUpdatePage.data.data) {
|
||||
router.replace(
|
||||
`/dashboard/${subdomain}/editor?id=${site.data?.metadata?.proof}-${createOrUpdatePage.data.data}&type=${router.query.type}`,
|
||||
`/dashboard/${subdomain}/editor?id=${site.data?.characterId}-${createOrUpdatePage.data.data}&type=${router.query.type}`,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -259,26 +269,26 @@ export default function SubdomainEditor() {
|
|||
|
||||
useEffect(() => {
|
||||
if (!page.data || !draftKey) return
|
||||
setInitialContent(page.data.body?.content || "")
|
||||
setInitialContent(page.data.metadata?.content?.content || "")
|
||||
useEditorState.setState({
|
||||
title: page.data.title || "",
|
||||
publishedAt: page.data.date_published,
|
||||
published: !!page.data.id,
|
||||
excerpt: page.data.summary?.content || "",
|
||||
slug: page.data.slug || "",
|
||||
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.tags
|
||||
page.data.metadata?.content?.tags
|
||||
?.filter((tag) => tag !== "post" && tag !== "page")
|
||||
?.join(", ") || "",
|
||||
content: page.data.body?.content || "",
|
||||
content: page.data.metadata?.content?.content || "",
|
||||
})
|
||||
setDefaultSlug(
|
||||
getDefaultSlug(
|
||||
page.data.title || "",
|
||||
draftKey.replace(`draft-${subdomain}-`, ""),
|
||||
page.data.metadata?.content?.title || "",
|
||||
draftKey.replace(`draft-${site.data?.characterId}-`, ""),
|
||||
),
|
||||
)
|
||||
}, [page.data, subdomain, draftKey])
|
||||
}, [page.data, subdomain, draftKey, site.data?.characterId])
|
||||
|
||||
const [currentScrollArea, setCurrentScrollArea] = useState<string>("")
|
||||
const [view, setView] = useState<EditorView>()
|
||||
|
|
@ -481,11 +491,11 @@ export default function SubdomainEditor() {
|
|||
const onPreviewButtonClick = useCallback(() => {
|
||||
window.open(
|
||||
`/_site/${subdomain}/preview/${draftKey.replace(
|
||||
`draft-${subdomain}-`,
|
||||
`draft-${site.data?.characterId}-`,
|
||||
"",
|
||||
)}`,
|
||||
)
|
||||
}, [draftKey, subdomain])
|
||||
}, [draftKey, subdomain, site.data?.characterId])
|
||||
const extraProperties = (
|
||||
<EditorExtraProperties
|
||||
defaultSlug={defaultSlug}
|
||||
|
|
@ -675,7 +685,7 @@ export default function SubdomainEditor() {
|
|||
className="text-accent"
|
||||
href={`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.custom_domain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(values.slug || defaultSlug)}`}
|
||||
>
|
||||
{t("View the post")}
|
||||
|
|
@ -684,13 +694,10 @@ export default function SubdomainEditor() {
|
|||
<li>
|
||||
<UniLink
|
||||
className="text-accent"
|
||||
href={
|
||||
page.data?.metadata?.transactions &&
|
||||
`${CSB_SCAN}/tx/${
|
||||
page.data?.metadata?.transactions[1] ||
|
||||
page.data?.metadata?.transactions[0]
|
||||
}`
|
||||
}
|
||||
href={`${CSB_SCAN}/tx/${
|
||||
page.data?.updatedTransactionHash ||
|
||||
page.data?.transactionHash
|
||||
}`}
|
||||
>
|
||||
{t("View the transaction")}
|
||||
</UniLink>
|
||||
|
|
@ -784,13 +791,13 @@ const EditorExtraProperties: FC<{
|
|||
<UniLink
|
||||
href={`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.custom_domain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(values.slug || defaultSlug)}`}
|
||||
className="hover:underline"
|
||||
>
|
||||
{getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.custom_domain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
noProtocol: true,
|
||||
})}
|
||||
/{encodeURIComponent(values.slug || defaultSlug)}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,8 @@ function SiteAvatar({ siteId }: { siteId: string }) {
|
|||
})}
|
||||
>
|
||||
<Avatar
|
||||
images={site.data?.avatars || []}
|
||||
name={site.data?.name || ""}
|
||||
images={site.data?.metadata?.content?.avatars || []}
|
||||
name={site.data?.metadata?.content?.name || ""}
|
||||
size={40}
|
||||
/>
|
||||
</UniLink>
|
||||
|
|
@ -56,8 +56,8 @@ export default function EventsPage() {
|
|||
const { t } = useTranslation("dashboard")
|
||||
const pages = useGetPagesBySite({
|
||||
type: "post",
|
||||
site: "xlog-events",
|
||||
take: 100,
|
||||
characterId: 50153,
|
||||
limit: 100,
|
||||
})
|
||||
|
||||
const [latestEventRead, setLatestEventRead] = useState<Date>()
|
||||
|
|
@ -73,7 +73,7 @@ export default function EventsPage() {
|
|||
}, [])
|
||||
|
||||
pages.data?.pages[0]?.list.forEach((item) => {
|
||||
item.metadata?.frontMatter?.Winners
|
||||
item.metadata?.content?.frontMatter?.Winners
|
||||
})
|
||||
|
||||
return (
|
||||
|
|
@ -82,19 +82,18 @@ export default function EventsPage() {
|
|||
<div className="grid gap-4 grid-cols-1 sm:grid-cols-2">
|
||||
{pages.data?.pages[0]?.list.map((item) => {
|
||||
let status
|
||||
if (item.metadata?.frontMatter?.EndTime < new Date()) {
|
||||
if (item.metadata?.content?.frontMatter?.EndTime < new Date()) {
|
||||
status = "Ended"
|
||||
} else if (item.metadata?.frontMatter?.StartTime > new Date()) {
|
||||
} else if (
|
||||
item.metadata?.content?.frontMatter?.StartTime > new Date()
|
||||
) {
|
||||
status = "Upcoming"
|
||||
} else {
|
||||
status = "Ongoing"
|
||||
}
|
||||
|
||||
let isUnread = false
|
||||
if (
|
||||
latestEventRead &&
|
||||
new Date(item.date_created) > latestEventRead
|
||||
) {
|
||||
if (latestEventRead && new Date(item.createdAt) > latestEventRead) {
|
||||
isUnread = true
|
||||
}
|
||||
return (
|
||||
|
|
@ -102,7 +101,7 @@ export default function EventsPage() {
|
|||
className={cn("bg-slate-100 rounded-lg relative", {
|
||||
"opacity-70": status === "Ended",
|
||||
})}
|
||||
key={item.id}
|
||||
key={item.transactionHash}
|
||||
>
|
||||
{isUnread && (
|
||||
<div>
|
||||
|
|
@ -125,44 +124,44 @@ export default function EventsPage() {
|
|||
{t(status)}
|
||||
</span>
|
||||
</div>
|
||||
{item.cover && (
|
||||
{item.metadata?.content?.cover && (
|
||||
<div className="w-full h-24 mb-4">
|
||||
<Image
|
||||
className="object-cover rounded"
|
||||
alt="cover"
|
||||
fill={true}
|
||||
src={item.cover}
|
||||
src={item.metadata?.content?.cover}
|
||||
></Image>
|
||||
</div>
|
||||
)}
|
||||
<div className="font-bold text-xl text-zinc-800 leading-tight">
|
||||
{item.title}
|
||||
{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?.frontMatter?.StartTime,
|
||||
item.metadata?.content?.frontMatter?.StartTime,
|
||||
"lll",
|
||||
isMounted ? undefined : "America/Los_Angeles",
|
||||
)}{" "}
|
||||
-{" "}
|
||||
{date.formatDate(
|
||||
item.metadata?.frontMatter?.EndTime,
|
||||
item.metadata?.content?.frontMatter?.EndTime,
|
||||
"lll",
|
||||
isMounted ? undefined : "America/Los_Angeles",
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<span className="font-bold">{t("Prize")}:</span>{" "}
|
||||
{item.metadata?.frontMatter?.Prize}
|
||||
{item.metadata?.content?.frontMatter?.Prize}
|
||||
</div>
|
||||
{item.metadata?.frontMatter?.Winners?.map && (
|
||||
{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?.frontMatter?.Winners?.map?.(
|
||||
{item.metadata?.content?.frontMatter?.Winners?.map?.(
|
||||
(winner: string) => (
|
||||
<SiteAvatar key={winner} siteId={winner} />
|
||||
),
|
||||
|
|
@ -174,8 +173,8 @@ export default function EventsPage() {
|
|||
<UniLink
|
||||
className="mt-6 font-bold flex items-center leading-none"
|
||||
href={
|
||||
item.metadata?.frontMatter?.ExtraLink ||
|
||||
item.related_urls?.[0]
|
||||
item.metadata?.content?.frontMatter?.ExtraLink ||
|
||||
`/api/redirection?characterId=${item.characterId}¬eId=${item.noteId}`
|
||||
}
|
||||
>
|
||||
{t("Learn more")}{" "}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ 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"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
||||
async (ctx) => {
|
||||
|
|
@ -25,9 +24,6 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
|||
|
||||
export default function ImportPage() {
|
||||
const router = useRouter()
|
||||
const subdomain = router.query.subdomain as string
|
||||
const site = useGetSite(subdomain)
|
||||
const characterId = site.data?.metadata?.proof
|
||||
const { t } = useTranslation("dashboard")
|
||||
|
||||
const options = [
|
||||
|
|
|
|||
|
|
@ -46,10 +46,10 @@ export default function ImportMarkdownPage() {
|
|||
const [notes, setNotes] = useState<NoteMetadata[]>()
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
if (notes?.length && site.data?.username && site.data.metadata?.proof) {
|
||||
if (notes?.length && site.data?.handle && site.data.characterId) {
|
||||
postNotes.mutate({
|
||||
siteId: site.data.username,
|
||||
characterId: site.data.metadata.proof,
|
||||
siteId: site.data.handle,
|
||||
characterId: site.data.characterId,
|
||||
notes,
|
||||
})
|
||||
}
|
||||
|
|
@ -74,7 +74,7 @@ export default function ImportMarkdownPage() {
|
|||
external_urls: [
|
||||
`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.custom_domain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(file.slug)}`,
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -40,9 +40,9 @@ export default function ImportMarkdownPage() {
|
|||
})
|
||||
const postNotes = usePostNotes()
|
||||
const mirrorXyz = useGetMirrorXyz({
|
||||
address: site.data?.metadata?.owner,
|
||||
address: site.data?.owner,
|
||||
})
|
||||
const checkMirror = useCheckMirror(site.data?.metadata?.proof)
|
||||
const checkMirror = useCheckMirror(site.data?.characterId)
|
||||
|
||||
const notes = mirrorXyz?.data?.map((note) => ({
|
||||
title: note.title,
|
||||
|
|
@ -59,17 +59,17 @@ export default function ImportMarkdownPage() {
|
|||
external_urls: [
|
||||
`${getSiteLink({
|
||||
subdomain,
|
||||
domain: site.data?.custom_domain,
|
||||
domain: site.data?.metadata?.content?.custom_domain,
|
||||
})}/${encodeURIComponent(note.slug)}`,
|
||||
...note.external_urls,
|
||||
],
|
||||
}))
|
||||
|
||||
const handleSubmit = form.handleSubmit(async (values) => {
|
||||
if (notes?.length && site.data?.username && site.data.metadata?.proof) {
|
||||
if (notes?.length && site.data?.handle && site.data.characterId) {
|
||||
postNotes.mutate({
|
||||
siteId: site.data.username,
|
||||
characterId: site.data.metadata.proof,
|
||||
siteId: site.data.handle,
|
||||
characterId: site.data.characterId,
|
||||
notes: notes,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ export default function SubdomainIndex() {
|
|||
const router = useRouter()
|
||||
const subdomain = router.query.subdomain as string
|
||||
const site = useGetSite(subdomain)
|
||||
const characterId = site.data?.metadata?.proof
|
||||
const characterId = site.data?.characterId
|
||||
const stat = useGetStat({
|
||||
characterId,
|
||||
})
|
||||
|
|
@ -97,8 +97,8 @@ export default function SubdomainIndex() {
|
|||
|
||||
const pages = useGetPagesBySite({
|
||||
type: "post",
|
||||
site: "xlog",
|
||||
take: 4,
|
||||
characterId: 32022,
|
||||
limit: 4,
|
||||
})
|
||||
|
||||
const showcaseSites = useGetShowcase()
|
||||
|
|
@ -210,22 +210,24 @@ export default function SubdomainIndex() {
|
|||
<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.slug}`}
|
||||
key={item.id}
|
||||
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.cover && (
|
||||
{item.metadata?.content?.cover && (
|
||||
<div className="w-full h-24">
|
||||
<Image
|
||||
className="object-cover rounded"
|
||||
alt="cover"
|
||||
fill={true}
|
||||
src={item.cover}
|
||||
src={item.metadata?.content?.cover}
|
||||
></Image>
|
||||
</div>
|
||||
)}
|
||||
<span className="font-bold text-sm text-zinc-800 leading-tight mt-4">
|
||||
{item.title}
|
||||
{item.metadata?.content?.title}
|
||||
</span>
|
||||
</UniLink>
|
||||
))}
|
||||
|
|
@ -236,7 +238,7 @@ export default function SubdomainIndex() {
|
|||
{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: any) => (
|
||||
{showcaseSites.data?.slice(0, 6)?.map((site) => (
|
||||
<li className="inline-flex align-middle" key={site.handle}>
|
||||
<UniLink
|
||||
href={getSiteLink({
|
||||
|
|
|
|||
|
|
@ -58,13 +58,13 @@ export default function SettingsCSSPage() {
|
|||
const [hasSet, setHasSet] = useState(false)
|
||||
useEffect(() => {
|
||||
if (site.isSuccess && site.data && !css && !hasSet) {
|
||||
setCss(site.data.css || "")
|
||||
setCss(site.data.metadata?.content?.css || "")
|
||||
setHasSet(true)
|
||||
}
|
||||
}, [site.data, site.isSuccess, css, hasSet])
|
||||
|
||||
return (
|
||||
<SettingsLayout title={"Site Settings"} type="site">
|
||||
<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">
|
||||
|
|
|
|||
|
|
@ -61,7 +61,8 @@ export default function SettingsDomainsPage() {
|
|||
updateSite.mutate({
|
||||
site: subdomain,
|
||||
...(subdomain !== values.subdomain && { subdomain: values.subdomain }),
|
||||
...(site.data?.custom_domain !== values.custom_domain && {
|
||||
...(site.data?.metadata?.content?.custom_domain !==
|
||||
values.custom_domain && {
|
||||
custom_domain: values.custom_domain,
|
||||
}),
|
||||
})
|
||||
|
|
@ -149,7 +150,7 @@ export default function SettingsDomainsPage() {
|
|||
const [subdomainChanged, setSubdomainChanged] = useState(false)
|
||||
form.register("subdomain", {
|
||||
onChange: (e) => {
|
||||
if (e.target.value !== site.data?.username) {
|
||||
if (e.target.value !== site.data?.handle) {
|
||||
toCheckSubdomain(e.target.value)
|
||||
setSubdomainChanged(true)
|
||||
} else {
|
||||
|
|
@ -162,15 +163,18 @@ export default function SettingsDomainsPage() {
|
|||
useEffect(() => {
|
||||
if (site.isSuccess && site.data && !hasSet) {
|
||||
setHasSet(true)
|
||||
form.setValue("subdomain", site.data.username || "")
|
||||
form.setValue("custom_domain", site.data.custom_domain || "")
|
||||
setCustomDomain(site.data.custom_domain || "")
|
||||
toCheckDomain(site.data.custom_domain || "")
|
||||
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"} type="site">
|
||||
<SettingsLayout title={"Site Settings"}>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<Input
|
||||
|
|
|
|||
|
|
@ -83,21 +83,28 @@ export default function SiteSettingsGeneralPage() {
|
|||
useEffect(() => {
|
||||
if (site.data) {
|
||||
!form.getValues("icon") &&
|
||||
form.setValue("icon", toIPFS(site.data?.avatars?.[0] || ""))
|
||||
form.setValue(
|
||||
"icon",
|
||||
toIPFS(site.data?.metadata?.content?.avatars?.[0] || ""),
|
||||
)
|
||||
!form.getValues("banner") &&
|
||||
form.setValue(
|
||||
"banner",
|
||||
site.data?.banners?.[0]
|
||||
site.data?.metadata?.content?.banners?.[0]
|
||||
? {
|
||||
address: toIPFS(site.data?.banners?.[0].address || ""),
|
||||
mime_type: site.data?.banners?.[0].mime_type,
|
||||
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.name || "")
|
||||
!form.getValues("name") &&
|
||||
form.setValue("name", site.data.metadata?.content?.name || "")
|
||||
!form.getValues("description") &&
|
||||
form.setValue("description", site.data.bio || "")
|
||||
!form.getValues("ga") && form.setValue("ga", site.data.ga || "")
|
||||
form.setValue("description", site.data.metadata?.content?.bio || "")
|
||||
!form.getValues("ga") &&
|
||||
form.setValue("ga", site.data.metadata?.content?.ga || "")
|
||||
}
|
||||
}, [site.data, form])
|
||||
|
||||
|
|
@ -105,7 +112,7 @@ export default function SiteSettingsGeneralPage() {
|
|||
const [bannerUploading, setBannerUploading] = useState(false)
|
||||
|
||||
return (
|
||||
<SettingsLayout title="Site Settings" type="site">
|
||||
<SettingsLayout title="Site Settings">
|
||||
<form onSubmit={handleSubmit}>
|
||||
<div className="mt-5">
|
||||
<label htmlFor="icon" className="form-label">
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@ export default function SiteSettingsNavigationPage() {
|
|||
|
||||
const itemsModified = useMemo(() => {
|
||||
if (!site.isSuccess) return false
|
||||
return !equal(items, site.data?.navigation)
|
||||
return !equal(items, site.data?.metadata?.content?.navigation)
|
||||
}, [items, site.data, site.isSuccess])
|
||||
|
||||
const updateItem: UpdateItem = (id, newItem) => {
|
||||
|
|
@ -112,10 +112,12 @@ export default function SiteSettingsNavigationPage() {
|
|||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
updateSite.mutate({
|
||||
site: site.data?.username!,
|
||||
navigation: items,
|
||||
})
|
||||
if (site.data?.handle) {
|
||||
updateSite.mutate({
|
||||
site: site.data?.handle,
|
||||
navigation: items,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -136,14 +138,14 @@ export default function SiteSettingsNavigationPage() {
|
|||
|
||||
const [hasSet, setHasSet] = useState(false)
|
||||
useEffect(() => {
|
||||
if (site.data?.navigation && !hasSet) {
|
||||
if (site.data?.metadata?.content?.navigation && !hasSet) {
|
||||
setHasSet(true)
|
||||
setItems(site.data?.navigation)
|
||||
setItems(site.data?.metadata?.content?.navigation)
|
||||
}
|
||||
}, [site.data?.navigation, hasSet])
|
||||
}, [site.data?.metadata?.content?.navigation, hasSet])
|
||||
|
||||
return (
|
||||
<SettingsLayout title="Site Settings" type="site">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export default function SettingsOperatorPage() {
|
|||
const removeOperator = useRemoveOperator()
|
||||
const site = useGetSite(subdomain)
|
||||
const operators = useGetOperators({
|
||||
characterId: +(site.data?.metadata?.proof || 0),
|
||||
characterId: site.data?.characterId,
|
||||
})
|
||||
const isEmailAccount = useAccountState(
|
||||
(s) => s.computed.account?.type === "email",
|
||||
|
|
@ -121,14 +121,14 @@ export default function SettingsOperatorPage() {
|
|||
|
||||
const removeItem: RemoveItem = (operator) => {
|
||||
removeOperator.mutate({
|
||||
characterId: +(site.data?.metadata?.proof || 0),
|
||||
characterId: site.data?.characterId,
|
||||
operator: operator,
|
||||
})
|
||||
}
|
||||
const addItem = () => {
|
||||
if (address) {
|
||||
addOperator.mutate({
|
||||
characterId: +(site.data?.metadata?.proof || 0),
|
||||
characterId: site.data?.characterId,
|
||||
operator: address,
|
||||
})
|
||||
}
|
||||
|
|
@ -142,7 +142,7 @@ export default function SettingsOperatorPage() {
|
|||
const [address, setAddress] = useState("")
|
||||
|
||||
return (
|
||||
<SettingsLayout title="Site Settings" type="site">
|
||||
<SettingsLayout title="Site Settings">
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
onClose={() => setIsOpen(false)}
|
||||
|
|
|
|||
|
|
@ -16,7 +16,6 @@ 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 { Profile } from "~/lib/types"
|
||||
import { useGetSite, useUpdateSite } from "~/queries/site"
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
||||
|
|
@ -31,7 +30,11 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
|
|||
},
|
||||
)
|
||||
|
||||
type Item = Required<Profile>["connected_accounts"][number] & {
|
||||
type Item = {
|
||||
identity: string
|
||||
platform: string
|
||||
url?: string | undefined
|
||||
} & {
|
||||
id: string
|
||||
}
|
||||
|
||||
|
|
@ -99,7 +102,7 @@ export default function SiteSettingsNavigationPage() {
|
|||
|
||||
const itemsModified = useMemo(() => {
|
||||
if (!site.isSuccess) return false
|
||||
return !equal(items, site.data?.connected_accounts)
|
||||
return !equal(items, site.data?.metadata?.content?.connected_accounts)
|
||||
}, [items, site.data, site.isSuccess])
|
||||
|
||||
const updateItem: UpdateItem = (id, newItem) => {
|
||||
|
|
@ -122,10 +125,12 @@ export default function SiteSettingsNavigationPage() {
|
|||
|
||||
const handleSubmit = (e: FormEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
updateSite.mutate({
|
||||
site: site.data?.username!,
|
||||
connected_accounts: items.map(({ id, ...item }) => item),
|
||||
})
|
||||
if (site.data?.handle) {
|
||||
updateSite.mutate({
|
||||
site: site.data?.handle,
|
||||
connected_accounts: items.map(({ id, ...item }) => item),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -146,19 +151,31 @@ export default function SiteSettingsNavigationPage() {
|
|||
|
||||
const [hasSet, setHasSet] = useState(false)
|
||||
useEffect(() => {
|
||||
if (site.data?.connected_accounts && !hasSet) {
|
||||
if (site.data?.metadata?.content?.connected_accounts && !hasSet) {
|
||||
setHasSet(true)
|
||||
setItems(
|
||||
site.data?.connected_accounts.map((item) => ({
|
||||
id: nanoid(),
|
||||
...item,
|
||||
})),
|
||||
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?.connected_accounts, hasSet])
|
||||
}, [site.data?.metadata?.content?.connected_accounts, hasSet])
|
||||
|
||||
return (
|
||||
<SettingsLayout title="Site Settings" type="site">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -37,7 +37,7 @@ export default function TokensPage() {
|
|||
const subdomain = router.query.subdomain as string
|
||||
const site = useGetSite(subdomain)
|
||||
|
||||
const miraBalance = useGetMiraBalance(site.data?.metadata?.proof)
|
||||
const miraBalance = useGetMiraBalance(site.data?.characterId)
|
||||
const csbBalance = useAccountBalance()
|
||||
const claimCSBStatus = useClaimCSBStatus()
|
||||
const claimCSBModal = useWalletClaimCSBModal()
|
||||
|
|
|
|||
|
|
@ -7,15 +7,12 @@ import {
|
|||
useWalletMintNewCharacterModal,
|
||||
} from "@crossbell/connect-kit"
|
||||
|
||||
import { useAccountSites } from "~/queries/site"
|
||||
|
||||
export default function Dashboard() {
|
||||
const router = useRouter()
|
||||
const userSites = useAccountSites()
|
||||
const walletMintNewCharacterModal = useWalletMintNewCharacterModal()
|
||||
const [ssrReady, isConnected] = useAccountState(({ ssrReady, computed }) => [
|
||||
const [ssrReady, account] = useAccountState(({ ssrReady, computed }) => [
|
||||
ssrReady,
|
||||
!!computed.account,
|
||||
computed.account,
|
||||
])
|
||||
const connectModal = useConnectModal()
|
||||
|
||||
|
|
@ -25,7 +22,7 @@ export default function Dashboard() {
|
|||
useEffect(() => {
|
||||
if (ssrReady) {
|
||||
// Wait till SSR is ready
|
||||
if (!isConnected) {
|
||||
if (!account) {
|
||||
// Wallet not connected
|
||||
if (!isConnectModalShown.current) {
|
||||
// Not shown
|
||||
|
|
@ -35,11 +32,11 @@ export default function Dashboard() {
|
|||
// Shown, but closed by user
|
||||
router.push("/") // Go back home
|
||||
}
|
||||
} else if (userSites.isSuccess) {
|
||||
} else {
|
||||
// Wallet is connected, wait till site is ready
|
||||
// Reset connect wallet status to prevent unexpected redirect
|
||||
isConnectModalShown.current = false
|
||||
if (!userSites.data?.length) {
|
||||
if (!account.character) {
|
||||
// No character found, prompt to mint one
|
||||
if (!isMintCharacterModalShown.current) {
|
||||
// Not shown
|
||||
|
|
@ -51,18 +48,11 @@ export default function Dashboard() {
|
|||
}
|
||||
} else {
|
||||
// Already have characters, redirect to primary
|
||||
router.push(`/dashboard/${userSites.data[0].username}`)
|
||||
router.push(`/dashboard/${account.character.handle}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
ssrReady,
|
||||
userSites,
|
||||
router,
|
||||
walletMintNewCharacterModal,
|
||||
isConnected,
|
||||
connectModal,
|
||||
])
|
||||
}, [ssrReady, router, walletMintNewCharacterModal, account, connectModal])
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-center w-full h-60">
|
||||
|
|
|
|||
|
|
@ -205,11 +205,6 @@ function Home() {
|
|||
),
|
||||
url: "https://hoot.it/search/xLog",
|
||||
},
|
||||
{
|
||||
name: "Unidata",
|
||||
icon: <Image src="/assets/unidata.png" alt="Unidata" />,
|
||||
url: "https://unidata.app/",
|
||||
},
|
||||
{
|
||||
name: "Raycast",
|
||||
icon: <Image src="/assets/raycast.png" alt="Raycast" />,
|
||||
|
|
@ -463,7 +458,7 @@ function Home() {
|
|||
>
|
||||
{t("Show more")}
|
||||
</div>
|
||||
{showcaseSites.data?.map((site: any) => (
|
||||
{showcaseSites.data?.map((site) => (
|
||||
<li className="inline-flex align-middle" key={site.handle}>
|
||||
<UniLink
|
||||
href={getSiteLink({
|
||||
|
|
|
|||
|
|
@ -9,23 +9,22 @@ export const fetchGetPage = async (
|
|||
input: Parameters<typeof pageModel.getPage>[0],
|
||||
queryClient: QueryClient,
|
||||
) => {
|
||||
const key = ["getPage", input.page, input]
|
||||
const key = ["getPage", input.characterId, input]
|
||||
return await queryClient.fetchQuery(key, async () => {
|
||||
if (!input.pageId) {
|
||||
if (!input.page || !input.site) {
|
||||
return null
|
||||
}
|
||||
const slug2Id = await getIdBySlug(input.page, input.site)
|
||||
if (!input.characterId || !input.slug) {
|
||||
return null
|
||||
}
|
||||
if (!input.noteId) {
|
||||
const slug2Id = await getIdBySlug(input.slug, input.characterId)
|
||||
if (!slug2Id?.noteId) {
|
||||
return null
|
||||
}
|
||||
input.pageId = `${slug2Id.characterId}-${slug2Id.noteId}`
|
||||
input.noteId = slug2Id.noteId
|
||||
}
|
||||
delete input.page
|
||||
return cacheGet({
|
||||
key,
|
||||
getValueFun: () => pageModel.getPage(input),
|
||||
})
|
||||
}) as Promise<ReturnType<typeof pageModel.getPage>>
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -33,7 +32,7 @@ export const prefetchGetPagesBySite = async (
|
|||
input: Parameters<typeof pageModel.getPagesBySite>[0],
|
||||
queryClient: QueryClient,
|
||||
) => {
|
||||
const key = ["getPagesBySite", input.site, input]
|
||||
const key = ["getPagesBySite", input.characterId, input]
|
||||
await queryClient.prefetchInfiniteQuery({
|
||||
queryKey: key,
|
||||
queryFn: async ({ pageParam }) => {
|
||||
|
|
@ -54,12 +53,12 @@ export const fetchGetPagesBySite = async (
|
|||
input: Parameters<typeof pageModel.getPagesBySite>[0],
|
||||
queryClient: QueryClient,
|
||||
) => {
|
||||
const key = ["getPagesBySite", input.site, input]
|
||||
const key = ["getPagesBySite", input.characterId, input]
|
||||
return await queryClient.fetchQuery(key, async () => {
|
||||
return cacheGet({
|
||||
key,
|
||||
getValueFun: () => pageModel.getPagesBySite(input),
|
||||
})
|
||||
}) as Promise<ReturnType<typeof pageModel.getPagesBySite>>
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ export const useGetPagesBySiteLite = (
|
|||
input: Parameters<typeof pageModel.getPagesBySite>[0],
|
||||
) => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["getPagesBySite", input.site, input],
|
||||
queryKey: ["getPagesBySite", input.characterId, input],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
const result: ReturnType<typeof pageModel.getPagesBySite> = await (
|
||||
await fetch(
|
||||
|
|
@ -44,17 +44,13 @@ export const useGetPagesBySiteLite = (
|
|||
export const useGetPagesBySite = (
|
||||
input: Parameters<typeof pageModel.getPagesBySite>[0],
|
||||
) => {
|
||||
const unidata = useUnidata()
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["getPagesBySite", input.site, input],
|
||||
queryKey: ["getPagesBySite", input.characterId, input],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
return pageModel.getPagesBySite(
|
||||
{
|
||||
...input,
|
||||
cursor: pageParam,
|
||||
},
|
||||
unidata,
|
||||
)
|
||||
return pageModel.getPagesBySite({
|
||||
...input,
|
||||
cursor: pageParam,
|
||||
})
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage.cursor || undefined,
|
||||
})
|
||||
|
|
@ -75,36 +71,70 @@ export const useGetSearchPagesBySite = (
|
|||
})
|
||||
}
|
||||
|
||||
export const useGetPage = (input: Parameters<typeof pageModel.getPage>[0]) => {
|
||||
const unidata = useUnidata()
|
||||
return useQuery(["getPage", input.page || input.pageId, input], async () => {
|
||||
if (!input.site || !(input.page || input.pageId)) {
|
||||
export const useGetPage = (
|
||||
input: Partial<Parameters<typeof pageModel.getPage>[0]>,
|
||||
) => {
|
||||
return useQuery(["getPage", input.characterId, input], async () => {
|
||||
if (!input.characterId || (!input.slug && !input.noteId)) {
|
||||
return null
|
||||
}
|
||||
return pageModel.getPage(input, unidata)
|
||||
return pageModel.getPage({
|
||||
characterId: input.characterId,
|
||||
slug: input.slug,
|
||||
noteId: input.noteId,
|
||||
useStat: input.useStat,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetLikeCounts = ({ pageId = "" }: { pageId?: string }) => {
|
||||
return useNoteLikeCount(pageModel.parsePageId(pageId))
|
||||
export const useGetLikeCounts = ({
|
||||
characterId,
|
||||
noteId,
|
||||
}: {
|
||||
characterId?: number
|
||||
noteId?: number
|
||||
}) => {
|
||||
return useNoteLikeCount({
|
||||
characterId: characterId || 0,
|
||||
noteId: noteId || 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetLikes = ({ pageId = "" }: { pageId?: string }) => {
|
||||
return useNoteLikeList(pageModel.parsePageId(pageId))
|
||||
export const useGetLikes = ({
|
||||
characterId,
|
||||
noteId,
|
||||
}: {
|
||||
characterId?: number
|
||||
noteId?: number
|
||||
}) => {
|
||||
return useNoteLikeList({
|
||||
characterId: characterId || 0,
|
||||
noteId: noteId || 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const useCheckLike = ({ pageId = "" }: { pageId?: string }) => {
|
||||
return useIsNoteLiked(pageModel.parsePageId(pageId))
|
||||
export const useCheckLike = ({
|
||||
characterId,
|
||||
noteId,
|
||||
}: {
|
||||
characterId?: number
|
||||
noteId?: number
|
||||
}) => {
|
||||
return useIsNoteLiked({
|
||||
characterId: characterId || 0,
|
||||
noteId: noteId || 0,
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetMints = (input: {
|
||||
pageId?: string
|
||||
characterId?: number
|
||||
noteId?: number
|
||||
includeCharacter?: boolean
|
||||
}) => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["getMints", input.pageId, input],
|
||||
queryKey: ["getMints", input.characterId, input.noteId, input],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!input.pageId) {
|
||||
if (!input.characterId || !input.noteId) {
|
||||
return {
|
||||
count: 0,
|
||||
list: [],
|
||||
|
|
@ -112,7 +142,8 @@ export const useGetMints = (input: {
|
|||
}
|
||||
}
|
||||
return pageModel.getMints({
|
||||
pageId: input.pageId,
|
||||
characterId: input.characterId,
|
||||
noteId: input.noteId,
|
||||
includeCharacter: input.includeCharacter,
|
||||
cursor: pageParam,
|
||||
})
|
||||
|
|
@ -121,15 +152,25 @@ export const useGetMints = (input: {
|
|||
})
|
||||
}
|
||||
|
||||
export const useCheckMint = (pageId: string | undefined) => {
|
||||
export const useCheckMint = ({
|
||||
characterId,
|
||||
noteId,
|
||||
}: {
|
||||
characterId?: number
|
||||
noteId?: number
|
||||
}) => {
|
||||
const address = useAccountState((s) => s.wallet?.address)
|
||||
|
||||
return useQuery(["checkMint", pageId, address], async () => {
|
||||
if (!pageId || !address) {
|
||||
return useQuery(["checkMint", characterId, noteId, address], async () => {
|
||||
if (!characterId || !noteId || !address) {
|
||||
return { count: 0, list: [] }
|
||||
}
|
||||
|
||||
return pageModel.checkMint({ pageId, address })
|
||||
return pageModel.checkMint({
|
||||
noteCharacterId: characterId,
|
||||
noteId: noteId,
|
||||
address,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -138,13 +179,17 @@ export function useCreateOrUpdatePage() {
|
|||
const unidata = useUnidata()
|
||||
const queryClient = useQueryClient()
|
||||
const mutation = useMutation(
|
||||
async (payload: Parameters<typeof pageModel.createOrUpdatePage>[0]) => {
|
||||
async (
|
||||
payload: Parameters<typeof pageModel.createOrUpdatePage>[0] & {
|
||||
characterId?: number
|
||||
},
|
||||
) => {
|
||||
return pageModel.createOrUpdatePage(payload, unidata, newbieToken)
|
||||
},
|
||||
{
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries(["getPagesBySite", variables.siteId])
|
||||
queryClient.invalidateQueries(["getPage", variables.pageId])
|
||||
queryClient.invalidateQueries(["getPagesBySite", variables.characterId])
|
||||
queryClient.invalidateQueries(["getPage", variables.characterId])
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -160,7 +205,7 @@ export function usePostNotes() {
|
|||
},
|
||||
{
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries(["getPagesBySite", variables.siteId])
|
||||
queryClient.invalidateQueries(["getPagesBySite", variables.characterId])
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -172,13 +217,17 @@ export function useDeletePage() {
|
|||
const unidata = useUnidata()
|
||||
const queryClient = useQueryClient()
|
||||
return useMutation(
|
||||
async (input: Parameters<typeof pageModel.deletePage>[0]) => {
|
||||
async (
|
||||
input: Parameters<typeof pageModel.deletePage>[0] & {
|
||||
characterId?: number
|
||||
},
|
||||
) => {
|
||||
return pageModel.deletePage(input, unidata, newbieToken)
|
||||
},
|
||||
{
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries(["getPagesBySite", variables.site])
|
||||
queryClient.invalidateQueries(["getPage", variables.id])
|
||||
queryClient.invalidateQueries(["getPagesBySite", variables.characterId])
|
||||
queryClient.invalidateQueries(["getPage", variables.characterId])
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -192,11 +241,18 @@ export function useMintPage() {
|
|||
|
||||
return useMintNote({
|
||||
onSuccess: (_, variables) => {
|
||||
const pageId = pageModel.toPageId(variables)
|
||||
|
||||
return Promise.all([
|
||||
queryClient.invalidateQueries(["checkMint", pageId, address]),
|
||||
queryClient.invalidateQueries(["getMints", pageId]),
|
||||
queryClient.invalidateQueries([
|
||||
"checkMint",
|
||||
variables.characterId,
|
||||
variables.noteId,
|
||||
address,
|
||||
]),
|
||||
queryClient.invalidateQueries([
|
||||
"getMints",
|
||||
variables.characterId,
|
||||
variables.noteId,
|
||||
]),
|
||||
])
|
||||
},
|
||||
})
|
||||
|
|
@ -210,19 +266,26 @@ export function useCommentPage() {
|
|||
|
||||
const mutate = useRefCallback(
|
||||
({
|
||||
pageId,
|
||||
characterId,
|
||||
noteId,
|
||||
content,
|
||||
externalUrl,
|
||||
originalId,
|
||||
originalCharacterId,
|
||||
originalNoteId,
|
||||
}: {
|
||||
pageId: string
|
||||
characterId: number
|
||||
noteId: number
|
||||
content: string
|
||||
externalUrl: string
|
||||
originalId?: string
|
||||
originalCharacterId?: number
|
||||
originalNoteId?: number
|
||||
}) => {
|
||||
return postNoteForNote.mutate(
|
||||
{
|
||||
note: pageModel.parsePageId(pageId),
|
||||
note: {
|
||||
characterId,
|
||||
noteId,
|
||||
},
|
||||
metadata: {
|
||||
content,
|
||||
external_urls: [externalUrl],
|
||||
|
|
@ -231,8 +294,12 @@ export function useCommentPage() {
|
|||
},
|
||||
},
|
||||
{
|
||||
onSuccess() {
|
||||
queryClient.invalidateQueries(["getComments", originalId || pageId])
|
||||
onSuccess(data, variables) {
|
||||
queryClient.invalidateQueries([
|
||||
"getComments",
|
||||
originalCharacterId || characterId,
|
||||
originalNoteId || noteId,
|
||||
])
|
||||
},
|
||||
},
|
||||
)
|
||||
|
|
@ -250,29 +317,38 @@ export function useUpdateComment() {
|
|||
const contract = useContract()
|
||||
|
||||
return useMutation(
|
||||
async (payload: Parameters<typeof pageModel.updateComment>[0]) => {
|
||||
async (
|
||||
payload: Parameters<typeof pageModel.updateComment>[0] & {
|
||||
originalNoteId?: number
|
||||
originalCharacterId?: number
|
||||
},
|
||||
) => {
|
||||
return pageModel.updateComment(payload, contract)
|
||||
},
|
||||
{
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries([
|
||||
"getComments",
|
||||
variables.originalId || variables.pageId,
|
||||
variables.originalCharacterId || variables.characterId,
|
||||
variables.originalNoteId || variables.noteId,
|
||||
])
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function useGetComments(input: { pageId?: string }) {
|
||||
export function useGetComments(
|
||||
input: Partial<Parameters<typeof pageModel.getComments>[0]>,
|
||||
) {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["getComments", input.pageId],
|
||||
queryKey: ["getComments", input.characterId, input.noteId],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!input.pageId) {
|
||||
return
|
||||
if (!input.characterId || !input.noteId) {
|
||||
return null
|
||||
}
|
||||
return pageModel.getComments({
|
||||
pageId: input.pageId,
|
||||
characterId: input.characterId,
|
||||
noteId: input.noteId,
|
||||
cursor: pageParam,
|
||||
})
|
||||
},
|
||||
|
|
@ -292,7 +368,7 @@ export function useGetSummary(input: { cid?: string; lang?: string }) {
|
|||
})
|
||||
}
|
||||
|
||||
export function useGetMirrorXyz(input: { address: string }) {
|
||||
export function useGetMirrorXyz(input: { address?: string }) {
|
||||
return useQuery(["getMirror", input.address], async () => {
|
||||
const { getDefaultSlug } = await import("~/lib/default-slug")
|
||||
|
||||
|
|
@ -332,7 +408,7 @@ export function useGetMirrorXyz(input: { address: string }) {
|
|||
})
|
||||
}
|
||||
|
||||
export function useCheckMirror(characterId?: string) {
|
||||
export function useCheckMirror(characterId?: number) {
|
||||
return useQuery(["checkMirror", characterId], async () => {
|
||||
if (!characterId) {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ export const fetchGetSite = async (input: string, queryClient: QueryClient) => {
|
|||
return cacheGet({
|
||||
key,
|
||||
getValueFun: () => siteModel.getSite(input),
|
||||
})
|
||||
}) as Promise<ReturnType<typeof siteModel.getSite>>
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -38,7 +38,7 @@ export const prefetchGetSiteSubscriptions = async (
|
|||
key,
|
||||
getValueFun: () => {
|
||||
return siteModel.getSiteSubscriptions({
|
||||
...input,
|
||||
characterId: input.characterId,
|
||||
cursor: pageParam,
|
||||
})
|
||||
},
|
||||
|
|
|
|||
|
|
@ -16,89 +16,68 @@ import * as siteModel from "~/models/site.model"
|
|||
|
||||
import { useUnidata } from "./unidata"
|
||||
|
||||
export const useAccountSites = () => {
|
||||
const unidata = useUnidata()
|
||||
const account = useAccountState((s) => s.computed.account)
|
||||
const handle =
|
||||
account?.type === "email" ? account.character?.handle : account?.handle
|
||||
|
||||
return useQuery(["getUserSites", handle], async () => {
|
||||
if (!account || !handle) {
|
||||
return []
|
||||
}
|
||||
|
||||
return siteModel.getAccountSites({ handle, unidata })
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetSite = (input?: string) => {
|
||||
const unidata = useUnidata()
|
||||
return useQuery(["getSite", input], async () => {
|
||||
if (!input) {
|
||||
return null
|
||||
}
|
||||
return siteModel.getSite(input, unidata)
|
||||
return siteModel.getSite(input)
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetSubscription = (siteId: string | undefined) => {
|
||||
export const useGetSubscription = (toCharacterId?: number) => {
|
||||
const account = useAccountState((s) => s.computed.account)
|
||||
const handle =
|
||||
account?.type === "email" ? account.character?.handle : account?.handle
|
||||
const unidata = useUnidata()
|
||||
|
||||
return useQuery(["getSubscription", siteId, handle], async () => {
|
||||
if (!handle || !siteId) {
|
||||
return false
|
||||
}
|
||||
return useQuery(
|
||||
["getSubscription", toCharacterId, account?.characterId],
|
||||
async () => {
|
||||
if (!account?.characterId || !toCharacterId) {
|
||||
return false
|
||||
}
|
||||
|
||||
return siteModel.getSubscription(siteId, handle, unidata)
|
||||
})
|
||||
return siteModel.getSubscription({
|
||||
characterId: account?.characterId,
|
||||
toCharacterId: toCharacterId,
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export const useGetSiteSubscriptions = (data: { siteId: string }) => {
|
||||
const unidata = useUnidata()
|
||||
export const useGetSiteSubscriptions = (data: { characterId?: number }) => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["getSiteSubscriptions", data],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!data.siteId) {
|
||||
if (!data.characterId) {
|
||||
return {
|
||||
total: 0,
|
||||
count: 0,
|
||||
list: [],
|
||||
cursor: undefined,
|
||||
}
|
||||
}
|
||||
return siteModel.getSiteSubscriptions(
|
||||
{
|
||||
...data,
|
||||
cursor: pageParam,
|
||||
},
|
||||
unidata,
|
||||
)
|
||||
return siteModel.getSiteSubscriptions({
|
||||
characterId: data.characterId,
|
||||
cursor: pageParam,
|
||||
})
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage?.cursor || undefined,
|
||||
})
|
||||
}
|
||||
|
||||
export const useGetSiteToSubscriptions = (data: { siteId: string }) => {
|
||||
const unidata = useUnidata()
|
||||
export const useGetSiteToSubscriptions = (data: { characterId?: number }) => {
|
||||
return useInfiniteQuery({
|
||||
queryKey: ["getSiteToSubscriptions", data],
|
||||
queryFn: async ({ pageParam }) => {
|
||||
if (!data.siteId) {
|
||||
if (!data.characterId) {
|
||||
return {
|
||||
total: 0,
|
||||
count: 0,
|
||||
list: [],
|
||||
cursor: undefined,
|
||||
}
|
||||
}
|
||||
return siteModel.getSiteToSubscriptions(
|
||||
{
|
||||
...data,
|
||||
cursor: pageParam,
|
||||
},
|
||||
unidata,
|
||||
)
|
||||
return siteModel.getSiteToSubscriptions({
|
||||
characterId: data.characterId,
|
||||
cursor: pageParam,
|
||||
})
|
||||
},
|
||||
getNextPageParam: (lastPage) => lastPage?.cursor || undefined,
|
||||
})
|
||||
|
|
@ -114,7 +93,6 @@ export function useUpdateSite() {
|
|||
},
|
||||
{
|
||||
onSuccess: (data, variables) => {
|
||||
queryClient.invalidateQueries(["getUserSites"])
|
||||
queryClient.invalidateQueries(["getSite"])
|
||||
},
|
||||
},
|
||||
|
|
@ -122,27 +100,6 @@ export function useUpdateSite() {
|
|||
return mutation
|
||||
}
|
||||
|
||||
export function useCreateSite() {
|
||||
const unidata = useUnidata()
|
||||
const queryClient = useQueryClient()
|
||||
const account = useAccountState((s) => s.computed.account)
|
||||
const address = account?.type === "email" ? account.email : account?.address
|
||||
|
||||
return useMutation(
|
||||
async (payload: { name: string; subdomain: string }) => {
|
||||
if (address) {
|
||||
// FIXME: - Support email users
|
||||
return siteModel.createSite(address, payload, unidata)
|
||||
}
|
||||
},
|
||||
{
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["getUserSites", address])
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
export function useSubscribeToSite() {
|
||||
const queryClient = useQueryClient()
|
||||
const account = useAccountState((s) => s.computed.account)
|
||||
|
|
@ -153,13 +110,13 @@ export function useSubscribeToSite() {
|
|||
queryClient.invalidateQueries([
|
||||
"getSiteSubscriptions",
|
||||
{
|
||||
siteId: variables.siteId,
|
||||
characterId: variables.characterId,
|
||||
},
|
||||
]),
|
||||
|
||||
queryClient.invalidateQueries([
|
||||
"getSubscription",
|
||||
variables.siteId,
|
||||
variables.characterId,
|
||||
account?.type === "email"
|
||||
? account?.character?.handle
|
||||
: account?.handle,
|
||||
|
|
@ -178,18 +135,18 @@ export function useSubscribeToSites() {
|
|||
return useFollowCharacters({
|
||||
onSuccess: (_, variables: any) =>
|
||||
Promise.all(
|
||||
variables.siteIds.flatMap((siteId: string) => {
|
||||
variables.siteIds.flatMap((characterId: number) => {
|
||||
return [
|
||||
queryClient.invalidateQueries([
|
||||
"getSiteSubscriptions",
|
||||
{
|
||||
siteId,
|
||||
characterId,
|
||||
},
|
||||
]),
|
||||
|
||||
queryClient.invalidateQueries([
|
||||
"getSubscription",
|
||||
siteId,
|
||||
characterId,
|
||||
currentCharacterId,
|
||||
]),
|
||||
]
|
||||
|
|
@ -208,12 +165,12 @@ export function useUnsubscribeFromSite() {
|
|||
queryClient.invalidateQueries([
|
||||
"getSiteSubscriptions",
|
||||
{
|
||||
siteId: variables.siteId,
|
||||
siteId: variables.characterId,
|
||||
},
|
||||
]),
|
||||
queryClient.invalidateQueries([
|
||||
"getSubscription",
|
||||
variables.siteId,
|
||||
variables.characterId,
|
||||
account?.type === "email"
|
||||
? account?.character?.handle
|
||||
: account?.handle,
|
||||
|
|
@ -321,7 +278,7 @@ export function useRemoveOperator() {
|
|||
)
|
||||
}
|
||||
|
||||
export const useGetNFTs = (address: string) => {
|
||||
export const useGetNFTs = (address?: string) => {
|
||||
return useQuery(["getNFTs", address], async () => {
|
||||
if (!address) {
|
||||
return null
|
||||
|
|
@ -398,7 +355,7 @@ export const useGetTips = (
|
|||
})
|
||||
}
|
||||
|
||||
export const useGetAchievements = (characterId?: string) => {
|
||||
export const useGetAchievements = (characterId?: number) => {
|
||||
return useQuery(["getAchievements", characterId], async () => {
|
||||
if (!characterId) {
|
||||
return null
|
||||
|
|
@ -424,7 +381,7 @@ export const useMintAchievement = () => {
|
|||
)
|
||||
}
|
||||
|
||||
export const useGetMiraBalance = (characterId?: string) => {
|
||||
export const useGetMiraBalance = (characterId?: number) => {
|
||||
const contract = useContract()
|
||||
return useQuery(["getMiraBalance", characterId], async () => {
|
||||
if (!characterId) {
|
||||
|
|
|
|||
|
|
@ -1,24 +1,37 @@
|
|||
import Unidata from "unidata.js"
|
||||
import { useEffect, useState } from "react"
|
||||
import type Unidata from "unidata.js"
|
||||
import { useAccount } from "wagmi"
|
||||
|
||||
import { IPFS_GATEWAY } from "../lib/env"
|
||||
|
||||
let unidata: Unidata
|
||||
|
||||
export const useUnidata = () => {
|
||||
const { connector, isConnected } = useAccount()
|
||||
if (isConnected && connector) {
|
||||
connector?.getProvider().then((provider) => {
|
||||
unidata = new Unidata({
|
||||
ethereumProvider: provider,
|
||||
ipfsGateway: IPFS_GATEWAY,
|
||||
ipfsRelay: "https://ipfs-relay.crossbell.io/json?gnfd=t",
|
||||
})
|
||||
|
||||
const [unidata, setUnidata] = useState<Unidata>()
|
||||
|
||||
useEffect(() => {
|
||||
import("unidata.js").then(({ default: Unidata }) => {
|
||||
if (isConnected && connector) {
|
||||
connector?.getProvider().then((provider) => {
|
||||
console.log("provider", provider)
|
||||
setUnidata(
|
||||
new Unidata({
|
||||
ethereumProvider: provider,
|
||||
ipfsGateway: IPFS_GATEWAY,
|
||||
ipfsRelay: "https://ipfs-relay.crossbell.io/json?gnfd=t",
|
||||
}),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
setUnidata(
|
||||
new Unidata({
|
||||
ipfsGateway: IPFS_GATEWAY,
|
||||
ipfsRelay: "https://ipfs-relay.crossbell.io/json?gnfd=t",
|
||||
}),
|
||||
)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
unidata = new Unidata({
|
||||
ipfsGateway: IPFS_GATEWAY,
|
||||
})
|
||||
}
|
||||
}, [isConnected, connector])
|
||||
|
||||
return unidata
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue