Remove dependency Unidata (#473)

This commit is contained in:
DIYgod 2023-05-02 00:32:25 +01:00 committed by GitHub
parent f7068304de
commit 2fe77a563f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
75 changed files with 1636 additions and 1713 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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>&copy; </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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}&noteId=${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,
},
],
}),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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}&noteId=${item.noteId}`
}
>
{t("Learn more")}{" "}

View File

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

View File

@ -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)}`,
],
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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