feat: server side rendering site header
This commit is contained in:
parent
1d8531c8e5
commit
b436189f2f
|
|
@ -1,3 +1,5 @@
|
|||
"use client"
|
||||
|
||||
import { useEffect } from "react"
|
||||
import { toast } from "react-hot-toast"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import { CharacterList } from "~/components/common/CharacterList"
|
||||
|
|
|
|||
|
|
@ -48,7 +48,11 @@ const PageContent = memo(function PageContent({
|
|||
onMouseEnter={onMouseEnter}
|
||||
>
|
||||
<>
|
||||
<div className="xlog-post-content prose" ref={inputRef}>
|
||||
<div
|
||||
className="xlog-post-content prose"
|
||||
ref={inputRef}
|
||||
suppressHydrationWarning
|
||||
>
|
||||
{inParsedContent?.element}
|
||||
</div>
|
||||
{toc && inParsedContent?.toc && <PostToc data={inParsedContent?.toc} />}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import { PatronModal } from "~/components/common/PatronModal"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export const SearchInput = ({
|
|||
})
|
||||
|
||||
return (
|
||||
<div className="xlog-comment-input flex">
|
||||
<div className="xlog-search-input flex">
|
||||
<form className="w-full relative" onSubmit={handleSubmit}>
|
||||
<div
|
||||
className="absolute left-0 top-1/2 -translate-y-1/2 text-2xl text-zinc-500 h-11 ml-4 flex items-center justify-center cursor-pointer"
|
||||
|
|
|
|||
|
|
@ -1,145 +1,42 @@
|
|||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
import { useState } from "react"
|
||||
|
||||
import { XCharLogo, XFeedLogo } from "@crossbell/ui"
|
||||
import { RssIcon } from "@heroicons/react/24/solid"
|
||||
import NextImage from "next/image"
|
||||
|
||||
import { FollowingButton } from "~/components/common/FollowingButton"
|
||||
import { FollowingCount } from "~/components/common/FollowingCount"
|
||||
import { PatronButton } from "~/components/common/PatronButton"
|
||||
import { SearchInput } from "~/components/common/SearchInput"
|
||||
import { BlockchainIcon } from "~/components/icons/BlockchainIcon"
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { Modal } from "~/components/ui/Modal"
|
||||
import { Tooltip } from "~/components/ui/Tooltip"
|
||||
import { CSB_SCAN, CSB_XCHAR, CSB_XFEED } from "~/lib/env"
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
import getQueryClient from "~/lib/query-client"
|
||||
import { cn } from "~/lib/utils"
|
||||
import { useGetSite } from "~/queries/site"
|
||||
import { fetchGetSite } from "~/queries/site.server"
|
||||
|
||||
import { ConnectButton } from "../common/ConnectButton"
|
||||
import { Avatar } from "../ui/Avatar"
|
||||
import { Button } from "../ui/Button"
|
||||
import { Menu } from "../ui/Menu"
|
||||
import { UniLink } from "../ui/UniLink"
|
||||
import { HeaderLink, type HeaderLinkType } from "./SiteHeaderLink"
|
||||
import { SiteHeaderMenu } from "./SiteHeaderMenu"
|
||||
|
||||
type HeaderLinkType = {
|
||||
icon?: React.ReactNode
|
||||
label: string
|
||||
url?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
export const SiteHeader = async ({ handle }: { handle: string }) => {
|
||||
const queryClient = getQueryClient()
|
||||
|
||||
const HeaderLink = ({ link }: { link: HeaderLinkType }) => {
|
||||
const pathname = usePathname()
|
||||
const { t } = useTranslation("site")
|
||||
|
||||
const active = pathname === link.url
|
||||
return (
|
||||
<UniLink
|
||||
href={link.url}
|
||||
onClick={link.onClick}
|
||||
className={cn("xlog-site-navigation-item", {
|
||||
"xlog-site-navigation-item-active": active,
|
||||
})}
|
||||
>
|
||||
{link.icon && <span>{link.icon}</span>}
|
||||
<span className="whitespace-nowrap">{t(link.label)}</span>
|
||||
</UniLink>
|
||||
const site = await fetchGetSite(handle, queryClient)
|
||||
const leftLinks: HeaderLinkType[] = site?.metadata?.content?.navigation?.find(
|
||||
(nav) => nav.url === "/",
|
||||
)
|
||||
}
|
||||
|
||||
export const SiteHeader = ({ handle }: { handle: string }) => {
|
||||
const site = useGetSite(handle)
|
||||
const { t } = useTranslation("site")
|
||||
const leftLinks: HeaderLinkType[] =
|
||||
site.data?.metadata?.content?.navigation?.find((nav) => nav.url === "/")
|
||||
? site.data.metadata?.content?.navigation
|
||||
: [
|
||||
{ label: "Home", url: "/" },
|
||||
...(site.data?.metadata?.content?.navigation || []),
|
||||
]
|
||||
|
||||
const moreMenuItems = [
|
||||
{
|
||||
text: "View on xChar",
|
||||
icon: <XCharLogo className="w-full h-full" />,
|
||||
url: `${CSB_XCHAR}/${site.data?.handle}`,
|
||||
},
|
||||
{
|
||||
text: "View on xFeed",
|
||||
icon: <XFeedLogo className="w-full h-full" />,
|
||||
url: `${CSB_XFEED}/u/${site.data?.handle}`,
|
||||
},
|
||||
{
|
||||
text: "View on Hoot It",
|
||||
icon: (
|
||||
<div className="w-full h-full">
|
||||
<Image
|
||||
alt="Hoot It"
|
||||
src="/assets/hoot.svg"
|
||||
className="rounded"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
url: `https://hoot.it/search/${site.data?.handle}.csb/activities`,
|
||||
},
|
||||
{
|
||||
text: "View on Crossbell Scan",
|
||||
icon: <BlockchainIcon className="fill-[#c09526] w-full h-full" />,
|
||||
url: `${CSB_SCAN}/address/${site.data?.owner}`,
|
||||
},
|
||||
{
|
||||
text: "Subscribe to JSON Feed",
|
||||
icon: (
|
||||
<div className="w-full h-full">
|
||||
<Image
|
||||
alt="JSON Feed"
|
||||
src="/assets/json-feed.png"
|
||||
className="rounded"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
url: `/feed`,
|
||||
},
|
||||
{
|
||||
text: "Subscribe to RSS",
|
||||
icon: <RssIcon className="w-full h-full text-[#ee832f]" />,
|
||||
url: `/feed?format=xml`,
|
||||
out: true,
|
||||
},
|
||||
{
|
||||
text: "Search on this site",
|
||||
icon: (
|
||||
<span className="text-stone-400">
|
||||
<i className="icon-[mingcute--search-line] block" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => setSearchOpen(true),
|
||||
out: true,
|
||||
},
|
||||
]
|
||||
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
? site.metadata?.content?.navigation
|
||||
: [
|
||||
{ label: "Home", url: "/" },
|
||||
...(site?.metadata?.content?.navigation || []),
|
||||
]
|
||||
|
||||
return (
|
||||
<header className="xlog-header border-b border-zinc-100 relative">
|
||||
<div className="xlog-banner absolute top-0 bottom-0 left-0 right-0 -z-10 overflow-hidden">
|
||||
{(() => {
|
||||
switch (
|
||||
site.data?.metadata?.content?.banners?.[0]?.mime_type?.split("/")[0]
|
||||
site?.metadata?.content?.banners?.[0]?.mime_type?.split("/")[0]
|
||||
) {
|
||||
case "image":
|
||||
return (
|
||||
<Image
|
||||
<NextImage
|
||||
className="max-w-screen-md mx-auto object-cover"
|
||||
src={site.data?.metadata?.content?.banners?.[0]?.address}
|
||||
src={site?.metadata?.content?.banners?.[0]?.address}
|
||||
alt="banner"
|
||||
fill
|
||||
priority={true}
|
||||
|
|
@ -149,7 +46,7 @@ export const SiteHeader = ({ handle }: { handle: string }) => {
|
|||
return (
|
||||
<video
|
||||
className="max-w-screen-md mx-auto object-cover h-full w-full"
|
||||
src={site.data?.metadata?.content?.banners?.[0]?.address}
|
||||
src={site?.metadata?.content?.banners?.[0]?.address}
|
||||
autoPlay
|
||||
muted
|
||||
playsInline
|
||||
|
|
@ -167,95 +64,44 @@ export const SiteHeader = ({ handle }: { handle: string }) => {
|
|||
<div
|
||||
className={cn(
|
||||
"xlog-site-info flex space-x-6 sm:space-x-8 items-center w-full",
|
||||
site.data?.metadata?.content?.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.data?.metadata?.content?.avatars?.[0] && (
|
||||
{site?.metadata?.content?.avatars?.[0] && (
|
||||
<Avatar
|
||||
className="xlog-site-icon max-w-[100px] max-h-[100px] sm:max-w-none sm:max-h-none"
|
||||
images={site.data?.metadata?.content?.avatars}
|
||||
images={site?.metadata?.content?.avatars}
|
||||
size={150}
|
||||
name={site.data?.metadata?.content?.name}
|
||||
name={site?.metadata?.content?.name}
|
||||
priority={true}
|
||||
/>
|
||||
)}
|
||||
<div className="flex-1 min-w-0 relative">
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="xlog-site-name text-3xl sm:text-4xl font-bold text-zinc-900 leading-snug break-words min-w-0">
|
||||
{site.data?.metadata?.content?.site_name ||
|
||||
site.data?.metadata?.content?.name}
|
||||
{site?.metadata?.content?.site_name ||
|
||||
site?.metadata?.content?.name}
|
||||
</h1>
|
||||
<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">
|
||||
<MoreActions>
|
||||
{moreMenuItems.map((item) => (
|
||||
<MoreActions.Item
|
||||
key={item.text}
|
||||
icon={item.icon}
|
||||
{...(item.onClick
|
||||
? {
|
||||
type: "button",
|
||||
onClick: item.onClick,
|
||||
}
|
||||
: {
|
||||
type: "link",
|
||||
href: item.url,
|
||||
})}
|
||||
>
|
||||
{t(item.text)}
|
||||
</MoreActions.Item>
|
||||
))}
|
||||
</MoreActions>
|
||||
</div>
|
||||
<div className="xlog-site-more-out hidden sm:block">
|
||||
<div className="-mx-2 flex">
|
||||
{moreMenuItems.map((item) => {
|
||||
if (item.out) {
|
||||
return (
|
||||
<Tooltip
|
||||
label={t(item.text)}
|
||||
key={item.text}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
aria-label={item.text}
|
||||
onClick={() =>
|
||||
item.url
|
||||
? window.open(item.url, "_blank")
|
||||
: item.onClick?.()
|
||||
}
|
||||
>
|
||||
<span className="fill-gray-500 flex w-6 h-6 text-2xl">
|
||||
{item.icon}
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<SiteHeaderMenu handle={site?.handle} owner={site?.owner} />
|
||||
<div className="xlog-site-follow-button">
|
||||
<FollowingButton site={site.data || undefined} />
|
||||
<FollowingButton site={site || undefined} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{site.data?.metadata?.content?.bio && (
|
||||
{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.data?.metadata?.content?.bio}
|
||||
{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 characterId={site.data?.characterId} />
|
||||
<FollowingCount characterId={site?.characterId} />
|
||||
</span>
|
||||
<span className="xlog-site-patron">
|
||||
<PatronButton site={site.data || undefined} />
|
||||
<PatronButton site={site || undefined} />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -272,30 +118,6 @@ export const SiteHeader = ({ handle }: { handle: string }) => {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Modal open={searchOpen} setOpen={setSearchOpen}>
|
||||
<div className="p-3">
|
||||
<SearchInput noBorder={true} onSubmit={() => setSearchOpen(false)} />
|
||||
</div>
|
||||
</Modal>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
function MoreActions({ children }: React.PropsWithChildren<{}>) {
|
||||
return (
|
||||
<Menu
|
||||
target={
|
||||
<Button
|
||||
variant="text"
|
||||
aria-label="more"
|
||||
className="-mx-2 text-zinc-600"
|
||||
>
|
||||
<i className="icon-[mingcute--more-1-line] text-2xl" />
|
||||
</Button>
|
||||
}
|
||||
dropdown={<div className="text-sm">{children}</div>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
MoreActions.Item = Menu.Item
|
||||
|
|
|
|||
|
|
@ -0,0 +1,34 @@
|
|||
"use client"
|
||||
|
||||
import { usePathname } from "next/navigation"
|
||||
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
import { cn } from "~/lib/utils"
|
||||
|
||||
import { UniLink } from "../ui/UniLink"
|
||||
|
||||
export type HeaderLinkType = {
|
||||
icon?: React.ReactNode
|
||||
label: string
|
||||
url?: string
|
||||
onClick?: () => void
|
||||
}
|
||||
|
||||
export const HeaderLink = ({ link }: { link: HeaderLinkType }) => {
|
||||
const pathname = usePathname()
|
||||
const { t } = useTranslation("site")
|
||||
|
||||
const active = pathname === link.url
|
||||
return (
|
||||
<UniLink
|
||||
href={link.url}
|
||||
onClick={link.onClick}
|
||||
className={cn("xlog-site-navigation-item", {
|
||||
"xlog-site-navigation-item-active": active,
|
||||
})}
|
||||
>
|
||||
{link.icon && <span>{link.icon}</span>}
|
||||
<span className="whitespace-nowrap">{t(link.label)}</span>
|
||||
</UniLink>
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
"use client"
|
||||
|
||||
import { useState } from "react"
|
||||
|
||||
import { XCharLogo, XFeedLogo } from "@crossbell/ui"
|
||||
import { RssIcon } from "@heroicons/react/24/solid"
|
||||
|
||||
import { SearchInput } from "~/components/common/SearchInput"
|
||||
import { BlockchainIcon } from "~/components/icons/BlockchainIcon"
|
||||
import { Image } from "~/components/ui/Image"
|
||||
import { Modal } from "~/components/ui/Modal"
|
||||
import { Tooltip } from "~/components/ui/Tooltip"
|
||||
import { CSB_SCAN, CSB_XCHAR, CSB_XFEED } from "~/lib/env"
|
||||
import { useTranslation } from "~/lib/i18n/client"
|
||||
|
||||
import { Button } from "../ui/Button"
|
||||
import { Menu } from "../ui/Menu"
|
||||
|
||||
function MoreActions({ children }: React.PropsWithChildren<{}>) {
|
||||
return (
|
||||
<Menu
|
||||
target={
|
||||
<Button
|
||||
variant="text"
|
||||
aria-label="more"
|
||||
className="-mx-2 text-zinc-600"
|
||||
>
|
||||
<i className="icon-[mingcute--more-1-line] text-2xl" />
|
||||
</Button>
|
||||
}
|
||||
dropdown={<div className="text-sm">{children}</div>}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export const SiteHeaderMenu = ({
|
||||
handle,
|
||||
owner,
|
||||
}: {
|
||||
handle?: string
|
||||
owner?: string
|
||||
}) => {
|
||||
const { t } = useTranslation("site")
|
||||
const [searchOpen, setSearchOpen] = useState(false)
|
||||
|
||||
const moreMenuItems = [
|
||||
{
|
||||
text: "View on xChar",
|
||||
icon: <XCharLogo className="w-full h-full" />,
|
||||
url: `${CSB_XCHAR}/${handle}`,
|
||||
},
|
||||
{
|
||||
text: "View on xFeed",
|
||||
icon: <XFeedLogo className="w-full h-full" />,
|
||||
url: `${CSB_XFEED}/u/${handle}`,
|
||||
},
|
||||
{
|
||||
text: "View on Hoot It",
|
||||
icon: (
|
||||
<div className="w-full h-full">
|
||||
<Image
|
||||
alt="Hoot It"
|
||||
src="/assets/hoot.svg"
|
||||
className="rounded"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
url: `https://hoot.it/search/${handle}.csb/activities`,
|
||||
},
|
||||
{
|
||||
text: "View on Crossbell Scan",
|
||||
icon: <BlockchainIcon className="fill-[#c09526] w-full h-full" />,
|
||||
url: `${CSB_SCAN}/address/${owner}`,
|
||||
},
|
||||
{
|
||||
text: "Subscribe to JSON Feed",
|
||||
icon: (
|
||||
<div className="w-full h-full">
|
||||
<Image
|
||||
alt="JSON Feed"
|
||||
src="/assets/json-feed.png"
|
||||
className="rounded"
|
||||
width={16}
|
||||
height={16}
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
url: `/feed`,
|
||||
},
|
||||
{
|
||||
text: "Subscribe to RSS",
|
||||
icon: <RssIcon className="w-full h-full text-[#ee832f]" />,
|
||||
url: `/feed?format=xml`,
|
||||
out: true,
|
||||
},
|
||||
{
|
||||
text: "Search on this site",
|
||||
icon: (
|
||||
<span className="text-stone-400">
|
||||
<i className="icon-[mingcute--search-line] block" />
|
||||
</span>
|
||||
),
|
||||
onClick: () => setSearchOpen(true),
|
||||
out: true,
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="xlog-site-more-menu relative inline-block align-middle">
|
||||
<MoreActions>
|
||||
{moreMenuItems.map((item) => (
|
||||
<MoreActions.Item
|
||||
key={item.text}
|
||||
icon={item.icon}
|
||||
{...(item.onClick
|
||||
? {
|
||||
type: "button",
|
||||
onClick: item.onClick,
|
||||
}
|
||||
: {
|
||||
type: "link",
|
||||
href: item.url,
|
||||
})}
|
||||
>
|
||||
{t(item.text)}
|
||||
</MoreActions.Item>
|
||||
))}
|
||||
</MoreActions>
|
||||
</div>
|
||||
<div className="xlog-site-more-out hidden sm:block">
|
||||
<div className="-mx-2 flex">
|
||||
{moreMenuItems.map((item) => {
|
||||
if (item.out) {
|
||||
return (
|
||||
<Tooltip
|
||||
label={t(item.text)}
|
||||
key={item.text}
|
||||
placement="bottom"
|
||||
>
|
||||
<Button
|
||||
variant="text"
|
||||
aria-label={item.text}
|
||||
onClick={() =>
|
||||
item.url
|
||||
? window.open(item.url, "_blank")
|
||||
: item.onClick?.()
|
||||
}
|
||||
>
|
||||
<span className="fill-gray-500 flex w-6 h-6 text-2xl">
|
||||
{item.icon}
|
||||
</span>
|
||||
</Button>
|
||||
</Tooltip>
|
||||
)
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
<Modal open={searchOpen} setOpen={setSearchOpen}>
|
||||
<div className="p-3">
|
||||
<SearchInput noBorder={true} onSubmit={() => setSearchOpen(false)} />
|
||||
</div>
|
||||
</Modal>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
MoreActions.Item = Menu.Item
|
||||
Loading…
Reference in New Issue