feat: server side rendering site header

This commit is contained in:
DIYgod 2023-06-23 17:00:34 +01:00
parent 1d8531c8e5
commit b436189f2f
No known key found for this signature in database
8 changed files with 250 additions and 211 deletions

View File

@ -1,3 +1,5 @@
"use client"
import { useEffect } from "react"
import { toast } from "react-hot-toast"

View File

@ -1,3 +1,5 @@
"use client"
import { useState } from "react"
import { CharacterList } from "~/components/common/CharacterList"

View File

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

View File

@ -1,3 +1,5 @@
"use client"
import { useState } from "react"
import { PatronModal } from "~/components/common/PatronModal"

View File

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

View File

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

View File

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

View File

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