310 lines
11 KiB
TypeScript
310 lines
11 KiB
TypeScript
import { cn } from "~/lib/utils"
|
|
import { useRouter } from "next/router"
|
|
import { CSB_SCAN, CSB_IO, CSB_XCHAR } from "~/lib/env"
|
|
import { getUserContentsUrl } from "~/lib/user-contents"
|
|
import { Avatar } from "../ui/Avatar"
|
|
import { Button } from "../ui/Button"
|
|
import { UniLink } from "../ui/UniLink"
|
|
import { Profile } from "~/lib/types"
|
|
import { ConnectButton } from "../common/ConnectButton"
|
|
import { BlockchainIcon } from "~/components/icons/BlockchainIcon"
|
|
import { Image } from "~/components/ui/Image"
|
|
import { FollowingButton } from "~/components/common/FollowingButton"
|
|
import { FollowingCount } from "~/components/common/FollowingCount"
|
|
import { RssIcon } from "@heroicons/react/24/solid"
|
|
import { XCharLogo, XFeedLogo } from "@crossbell/ui"
|
|
import { FastAverageColor } from "fast-average-color"
|
|
import { useState, useRef, useEffect, RefObject } from "react"
|
|
import chroma from "chroma-js"
|
|
import { Menu } from "~/components/ui/Menu"
|
|
import { useTranslation } from "next-i18next"
|
|
import { Tooltip } from "~/components/ui/Tooltip"
|
|
import { PatronButton } from "~/components/common/PatronButton"
|
|
|
|
type HeaderLinkType = {
|
|
icon?: React.ReactNode
|
|
label: string
|
|
url?: string
|
|
onClick?: () => void
|
|
}
|
|
|
|
const fac = new FastAverageColor()
|
|
|
|
const HeaderLink: React.FC<{ link: HeaderLinkType }> = ({ link }) => {
|
|
const router = useRouter()
|
|
const { t } = useTranslation("site")
|
|
const active = router.asPath === link.url
|
|
return (
|
|
<UniLink
|
|
href={link.url}
|
|
onClick={link.onClick}
|
|
className={cn(
|
|
`xlog-site-navigation-item h-10 flex items-center space-x-1 transition-colors relative after:content-[''] hover:after:w-full hover:after:left-0 after:transition-[width,left] after:h-[2px] after:block after:absolute after:bottom-0`,
|
|
active
|
|
? `text-accent after:w-full after:left-0 after:bg-accent`
|
|
: `hover:text-gray-700 after:w-0 after:left-1/2 after:bg-gray-700`,
|
|
)}
|
|
>
|
|
{link.icon && <span>{link.icon}</span>}
|
|
<span className="whitespace-nowrap">{t(link.label)}</span>
|
|
</UniLink>
|
|
)
|
|
}
|
|
|
|
export const SiteHeader: React.FC<{
|
|
site?: Profile | undefined | null
|
|
}> = ({ site }) => {
|
|
const { t } = useTranslation("site")
|
|
const leftLinks: HeaderLinkType[] = site?.navigation?.find(
|
|
(nav) => nav.url === "/",
|
|
)
|
|
? site.navigation
|
|
: [{ label: "Home", url: "/" }, ...(site?.navigation || [])]
|
|
|
|
const moreMenuItems = [
|
|
{
|
|
text: "View on xChar",
|
|
icon: <XCharLogo className="w-full h-full" />,
|
|
url: `${CSB_XCHAR}/${site?.username}`,
|
|
},
|
|
{
|
|
text: "View on xFeed",
|
|
icon: <XFeedLogo className="w-full h-full" />,
|
|
url: `${CSB_IO}/@${site?.username}`,
|
|
},
|
|
{
|
|
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?.username}.csb/activities`,
|
|
},
|
|
{
|
|
text: "View on Crossbell Scan",
|
|
icon: <BlockchainIcon className="fill-[#c09526] w-full h-full" />,
|
|
url: `${CSB_SCAN}/address/${site?.metadata?.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/xml`,
|
|
out: true,
|
|
},
|
|
]
|
|
|
|
const avatarRef = useRef<HTMLImageElement>(null)
|
|
const bannerRef = useRef<HTMLImageElement | HTMLVideoElement>(null)
|
|
|
|
const [averageColor, setAverageColor] = useState<string>()
|
|
const [autoHoverColor, setAutoHoverColor] = useState<string>()
|
|
const [autoThemeColor, setAutoThemeColor] = useState<string>()
|
|
|
|
useEffect(() => {
|
|
if (bannerRef?.current) {
|
|
fac
|
|
.getColorAsync(bannerRef.current)
|
|
.then((color) => {
|
|
setAverageColor(chroma(color.hex).hex())
|
|
setAutoHoverColor(chroma(color.hex).luminance(0.8).hex())
|
|
setAutoThemeColor(chroma(color.hex).saturate(3).luminance(0.3).hex())
|
|
})
|
|
.catch((e) => {
|
|
console.warn(e)
|
|
})
|
|
} else if (avatarRef?.current) {
|
|
fac
|
|
.getColorAsync(avatarRef.current)
|
|
.then((color) => {
|
|
setAverageColor(chroma(color.hex).luminance(0.95).hex())
|
|
setAutoHoverColor(chroma(color.hex).luminance(0.8).hex())
|
|
setAutoThemeColor(chroma(color.hex).saturate(3).luminance(0.3).hex())
|
|
})
|
|
.catch((e) => {
|
|
console.warn(e)
|
|
})
|
|
}
|
|
}, [bannerRef, avatarRef])
|
|
|
|
return (
|
|
<header className="xlog-header border-b border-zinc-100 relative">
|
|
{averageColor && (
|
|
<style jsx global>{`
|
|
:root {
|
|
--auto-hover-color: ${autoHoverColor};
|
|
--auto-theme-color: ${autoThemeColor};
|
|
}
|
|
`}</style>
|
|
)}
|
|
<div
|
|
className="xlog-banner absolute top-0 bottom-0 left-0 right-0 -z-10 overflow-hidden"
|
|
style={{
|
|
backgroundColor: averageColor
|
|
? `var(--banner-bg-color, ${averageColor})`
|
|
: "var(--banner-bg-color)",
|
|
}}
|
|
>
|
|
{(() => {
|
|
switch (site?.banners?.[0]?.mime_type.split("/")[0]) {
|
|
case "image":
|
|
return (
|
|
<Image
|
|
className="max-w-screen-md mx-auto object-cover"
|
|
src={site?.banners?.[0]?.address}
|
|
alt="banner"
|
|
fill
|
|
imageRef={bannerRef as RefObject<HTMLImageElement>}
|
|
/>
|
|
)
|
|
case "video":
|
|
return (
|
|
<video
|
|
className="max-w-screen-md mx-auto object-cover h-full w-full"
|
|
src={site?.banners?.[0]?.address}
|
|
autoPlay
|
|
muted
|
|
playsInline
|
|
ref={bannerRef as RefObject<HTMLVideoElement>}
|
|
crossOrigin="anonymous"
|
|
/>
|
|
)
|
|
default:
|
|
return null
|
|
}
|
|
})()}
|
|
</div>
|
|
<div className="px-5 max-w-screen-md mx-auto h-full relative flex items-center flex-col">
|
|
<div className="mb-auto"></div>
|
|
<div className="flex py-12 w-full">
|
|
<div className="xlog-site-info flex space-x-6 items-center w-full">
|
|
{site?.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])]}
|
|
size={120}
|
|
name={site?.name}
|
|
imageRef={avatarRef}
|
|
/>
|
|
)}
|
|
<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}
|
|
</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">
|
|
<Menu
|
|
target={
|
|
<Button
|
|
variant="text"
|
|
aria-label="more"
|
|
className="-mx-2 text-zinc-600"
|
|
>
|
|
<i className="i-mingcute:more-1-line text-2xl" />
|
|
</Button>
|
|
}
|
|
dropdown={
|
|
<div className="text-gray-600 bg-white rounded-lg ring-1 ring-zinc-100 shadow-md py-2 text-sm">
|
|
{moreMenuItems.map((item) => {
|
|
return (
|
|
<UniLink
|
|
key={item.text}
|
|
href={item.url}
|
|
className="h-10 flex w-full space-x-2 items-center px-3 hover:bg-hover"
|
|
>
|
|
<span className="fill-gray-500 flex w-4 h-4">
|
|
{item.icon}
|
|
</span>
|
|
<span>{t(item.text)}</span>
|
|
</UniLink>
|
|
)
|
|
})}
|
|
</div>
|
|
}
|
|
/>
|
|
</div>
|
|
<div className="xlog-site-more-out hidden sm:block">
|
|
{moreMenuItems.map((item) => {
|
|
if (item.out) {
|
|
return (
|
|
<Tooltip
|
|
label={t(item.text)}
|
|
key={item.text}
|
|
placement="bottom"
|
|
>
|
|
<Button
|
|
variant="text"
|
|
aria-label={item.text}
|
|
className="-mx-2"
|
|
onClick={() => window.open(item.url, "_blank")}
|
|
>
|
|
<span className="fill-gray-500 flex w-6 h-6">
|
|
{item.icon}
|
|
</span>
|
|
</Button>
|
|
</Tooltip>
|
|
)
|
|
} else {
|
|
return null
|
|
}
|
|
})}
|
|
</div>
|
|
<div className="xlog-site-follow-button">
|
|
<FollowingButton site={site} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{site?.bio && (
|
|
<div
|
|
className="xlog-site-description text-gray-500 leading-snug my-2 sm:my-3 text-sm sm:text-base"
|
|
dangerouslySetInnerHTML={{ __html: site?.description || "" }}
|
|
></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} />
|
|
</span>
|
|
<span className="xlog-site-patron">
|
|
<PatronButton site={site} />
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div className="text-gray-500 flex items-center justify-between w-full mt-auto">
|
|
<div className="xlog-site-navigation flex items-center space-x-5 min-w-0 overflow-x-auto text-sm sm:text-base">
|
|
{leftLinks.map((link, i) => {
|
|
return <HeaderLink link={link} key={`${link.label}${i}`} />
|
|
})}
|
|
</div>
|
|
<div className="pl-1">
|
|
<ConnectButton variant="text" mobileSimplification={true} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</header>
|
|
)
|
|
}
|