mirror of
https://github.com/penxio/penx.git
synced 2026-05-12 03:03:12 -04:00
feat: improve theme
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender/ContentRender'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { getSite } from '@/lib/fetchers'
|
||||
import { loadTheme } from '@/themes/theme-loader'
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Analytics } from '@/components/Analytics'
|
||||
import { getSite } from '@/lib/fetchers'
|
||||
import { loadTheme } from '@/themes/theme-loader'
|
||||
import { Providers } from '../providers'
|
||||
@@ -31,7 +32,7 @@ export default async function RootLayout({
|
||||
)}
|
||||
|
||||
{site.analytics?.gaMeasurementId && (
|
||||
<GoogleAnalytics trackPageViews gaMeasurementId="" />
|
||||
<Analytics gaMeasurementId={site.analytics?.gaMeasurementId} />
|
||||
)}
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
'use client'
|
||||
|
||||
import dynamic from 'next/dynamic'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import { SubscriptionInSession } from '@/lib/types'
|
||||
import useSession from '@/lib/useSession'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Post } from '@/server/db/schema'
|
||||
import dynamic from 'next/dynamic'
|
||||
import { GateCover } from './GateCover'
|
||||
|
||||
const PostDetail: any = dynamic(
|
||||
@@ -32,13 +33,14 @@ function checkMembership(subscriptions: SubscriptionInSession[]) {
|
||||
}
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
postId: string
|
||||
post: Post
|
||||
prev: Post
|
||||
next: Post
|
||||
}
|
||||
|
||||
export function PaidContent({ postId, post, next, prev }: Props) {
|
||||
export function PaidContent({ site, postId, post, next, prev }: Props) {
|
||||
const { data: session, status } = useSession()
|
||||
|
||||
if (status === 'loading') return null
|
||||
@@ -48,6 +50,7 @@ export function PaidContent({ postId, post, next, prev }: Props) {
|
||||
return (
|
||||
<div>
|
||||
<PostDetail
|
||||
site={site}
|
||||
post={{
|
||||
...post,
|
||||
content: getContent(post, true),
|
||||
@@ -71,6 +74,7 @@ export function PaidContent({ postId, post, next, prev }: Props) {
|
||||
return (
|
||||
<div className="">
|
||||
<PostDetail
|
||||
site={site}
|
||||
post={{
|
||||
...post,
|
||||
content: hasMembership ? getContent(post) : getContent(post, true),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
import { getPost, getPosts, getSite } from '@/lib/fetchers'
|
||||
import { GateType } from '@/lib/types'
|
||||
import { Post } from '@/server/db/schema'
|
||||
import { loadTheme } from '@/themes/theme-loader'
|
||||
import { Metadata } from 'next'
|
||||
import { notFound } from 'next/navigation'
|
||||
|
||||
// import { PaidContent } from './PaidContent'
|
||||
|
||||
function getContent(post: Post) {
|
||||
@@ -60,6 +61,7 @@ export default async function Page({
|
||||
// console.log('=====post:', post)
|
||||
return (
|
||||
<PostDetail
|
||||
site={site}
|
||||
post={{
|
||||
...post,
|
||||
content: getContent(post),
|
||||
|
||||
12
components/Analytics.tsx
Normal file
12
components/Analytics.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
'use client'
|
||||
|
||||
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
||||
import { GoogleAnalytics } from 'nextjs-google-analytics'
|
||||
|
||||
interface Props {
|
||||
gaMeasurementId: string
|
||||
}
|
||||
|
||||
export function Analytics({ gaMeasurementId }: Props) {
|
||||
return <GoogleAnalytics trackPageViews gaMeasurementId={gaMeasurementId} />
|
||||
}
|
||||
13
components/theme-ui/PostSubtitle.tsx
Normal file
13
components/theme-ui/PostSubtitle.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PostSubtitle({ children, className }: Props) {
|
||||
return (
|
||||
<p className={cn('text-lg text-foreground/50', className)}>{children}</p>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
'use client'
|
||||
|
||||
import { Mail } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { SubscribeNewsletterDialog } from './SubscribeNewsletterDialog'
|
||||
import { useSubscribeNewsletterDialog } from './useSubscribeNewsletterDialog'
|
||||
|
||||
interface Props {
|
||||
className?: string
|
||||
site: Site
|
||||
}
|
||||
|
||||
export function SubscribeNewsletterCard({ site, className }: Props) {
|
||||
const { setIsOpen } = useSubscribeNewsletterDialog()
|
||||
if (process.env.NEXT_PUBLIC_CAN_SUBSCRIBE !== 'yes') return null
|
||||
|
||||
return (
|
||||
<>
|
||||
<SubscribeNewsletterDialog site={site} />
|
||||
<div
|
||||
className={cn(
|
||||
'mx-auto flex flex-col items-center gap-4 mt-8',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
<div className="font-bold text-2xl">Subscribe to {site.name}</div>
|
||||
<Button
|
||||
className="w-40 flex items-center gap-2"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<Mail size={16} className="opacity-70" />
|
||||
<span>Subscribe</span>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
'use client'
|
||||
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import { useSubscribeNewsletterDialog } from './useSubscribeNewsletterDialog'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
}
|
||||
|
||||
export function SubscribeNewsletterDialog({ site }: Props) {
|
||||
const { isOpen, setIsOpen } = useSubscribeNewsletterDialog()
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={(v) => setIsOpen(v)}>
|
||||
<DialogContent className="sm:max-w-[520px] grid gap-4">
|
||||
<DialogHeader className="hidden">
|
||||
<DialogTitle className=""></DialogTitle>
|
||||
<DialogDescription></DialogDescription>
|
||||
</DialogHeader>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { atom, useAtom } from 'jotai'
|
||||
|
||||
const subscribeNewsletterDialogAtom = atom<boolean>(false)
|
||||
|
||||
export function useSubscribeNewsletterDialog() {
|
||||
const [isOpen, setIsOpen] = useAtom(subscribeNewsletterDialogAtom)
|
||||
return { isOpen, setIsOpen }
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { NavLink, NavLinkType } from '../theme.types'
|
||||
|
||||
export * from './element-type'
|
||||
export * from './theme.constants'
|
||||
export * from './defaultPostContent'
|
||||
|
||||
export const isServer = typeof window === 'undefined'
|
||||
|
||||
5
lib/constants/theme.constants.ts
Normal file
5
lib/constants/theme.constants.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
export const POSTS_PER_PAGE = Number(
|
||||
process.env.NEXT_PUBLIC_POSTS_PAGE_SIZE || 20,
|
||||
)
|
||||
|
||||
// export const POSTS_PER_PAGE = 2
|
||||
@@ -1,10 +1,9 @@
|
||||
import { ReactNode, Suspense } from 'react'
|
||||
import { Merienda } from 'next/font/google'
|
||||
import Link from 'next/link'
|
||||
import { Profile } from '@/components/Profile/Profile'
|
||||
import { Airdrop } from '@/components/theme-ui/Airdrop'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import Link from './Link'
|
||||
|
||||
const merienda = Merienda({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
@@ -12,26 +11,27 @@ const merienda = Merienda({
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
const headerNavLinksRight = [{ href: '/creator-fi/plans', title: 'CreatorFi' }]
|
||||
const headerNavLinks = [
|
||||
{ href: '/', title: 'Home' },
|
||||
{ href: '/posts', title: 'Blog' },
|
||||
{ href: '/tags', title: 'Tags' },
|
||||
{ href: '/about', title: 'About' },
|
||||
{ href: '/membership', title: 'Membership', isMembership: true },
|
||||
]
|
||||
|
||||
const headerNavLinksRight = [{ href: '/creator-fi', title: 'CreatorFi' }]
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
}
|
||||
|
||||
export const Header = ({ site }: Props) => {
|
||||
const links = [
|
||||
...site?.navLinks,
|
||||
{
|
||||
pathname: '/creator-fi/plans',
|
||||
title: 'CreatorFi',
|
||||
visible: true,
|
||||
},
|
||||
]
|
||||
const links = [...site?.navLinks]
|
||||
return (
|
||||
<header className={cn('flex items-center w-full py-4 h-16 z-40')}>
|
||||
<div className="flex-1 no-scrollbar hidden items-center space-x-4 overflow-x-auto sm:flex sm:space-x-6">
|
||||
{links.map((link) => {
|
||||
if (link.pathname === '/creator-fi/plans' && !site.spaceId) {
|
||||
if (link.pathname === '/creator-fi' && !site.spaceId) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export const Header = ({ site }: Props) => {
|
||||
<Link
|
||||
href="/membership"
|
||||
className={cn(
|
||||
'font-medium hover:text-brand-500 dark:hover:text-brand-400 text-foreground/90',
|
||||
'font-medium hover:text-brand-500 text-foreground/90',
|
||||
'border border-brand-500 text-brand-500 rounded-full px-2 py-1 hover:bg-brand-500 hover:text-background text-sm',
|
||||
)}
|
||||
>
|
||||
@@ -85,7 +85,7 @@ export const Header = ({ site }: Props) => {
|
||||
<Link
|
||||
key={link.title}
|
||||
href={link.href}
|
||||
className="font-medium hover:text-brand-500 dark:hover:text-brand-400 text-foreground/90"
|
||||
className="font-medium hover:text-brand-500 dark:hover:text-brand-400 text-foreground/90"
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
@@ -96,6 +96,7 @@ export const Header = ({ site }: Props) => {
|
||||
<div className="flex items-center">
|
||||
<Airdrop />
|
||||
</div>
|
||||
|
||||
<Profile></Profile>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
9
themes/card/components/Image.tsx
Normal file
9
themes/card/components/Image.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import NextImage, { ImageProps } from 'next/image'
|
||||
|
||||
const basePath = process.env.BASE_PATH
|
||||
|
||||
const Image = ({ src, ...rest }: ImageProps) => (
|
||||
<NextImage src={`${basePath || ''}${src}`} {...rest} />
|
||||
)
|
||||
|
||||
export default Image
|
||||
34
themes/card/components/Link.tsx
Normal file
34
themes/card/components/Link.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { AnchorHTMLAttributes } from 'react'
|
||||
import Link from 'next/link'
|
||||
import type { LinkProps } from 'next/link'
|
||||
|
||||
const CustomLink = ({
|
||||
href,
|
||||
...rest
|
||||
}: LinkProps & AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const isInternalLink = href && href.startsWith('/')
|
||||
const isAnchorLink = href && href.startsWith('#')
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
if (isInternalLink) {
|
||||
// if (isProd) href = href + '.html'
|
||||
return <Link className="break-words" href={href} {...rest} />
|
||||
}
|
||||
|
||||
if (isAnchorLink) {
|
||||
return <a className="break-words" href={href} {...rest} />
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className="break-words"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomLink
|
||||
@@ -1,6 +1,8 @@
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { Post } from '@/lib/theme.types'
|
||||
import Link from 'next/link'
|
||||
import { Node } from 'slate'
|
||||
import { Post, PostType } from '@/lib/theme.types'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
import Image from './Image'
|
||||
import Link from './Link'
|
||||
import Tag from './Tag'
|
||||
|
||||
interface PostItemProps {
|
||||
@@ -10,19 +12,71 @@ interface PostItemProps {
|
||||
export function PostItem({ post }: PostItemProps) {
|
||||
const { slug, title } = post
|
||||
|
||||
function getCardContent() {
|
||||
if (post.type === PostType.IMAGE) {
|
||||
return (
|
||||
<Image
|
||||
src={post.content}
|
||||
alt=""
|
||||
width={400}
|
||||
height={400}
|
||||
className="object-cover w-full h-52"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const getTextFromChildren = (children: any[]) => {
|
||||
return children.reduce((acc: string, child: any) => {
|
||||
return acc + Node.string(child)
|
||||
}, '')
|
||||
}
|
||||
|
||||
const text = JSON.parse(post.content)
|
||||
.map((element: any) => {
|
||||
if (Array.isArray(element.children)) {
|
||||
return getTextFromChildren(element.children)
|
||||
} else {
|
||||
return Node.string(element)
|
||||
}
|
||||
})
|
||||
.join('')
|
||||
|
||||
if (post.type === PostType.NOTE) {
|
||||
return (
|
||||
<span className="text-foreground/80 p-4 border border-foreground/5 h-full block">
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
if (post?.image) {
|
||||
return (
|
||||
<Image
|
||||
src={post.image || ''}
|
||||
alt=""
|
||||
width={400}
|
||||
height={400}
|
||||
className="object-cover w-full h-52"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<span className="text-foreground/80 p-4 border border-foreground/5 h-full block">
|
||||
{text}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<article key={slug} className="flex flex-col space-y-5">
|
||||
<Link
|
||||
href={`/posts/${slug}`}
|
||||
className="object-cover w-full h-52 bg-foreground/10 rounded-lg overflow-hidden hover:scale-105 transition-all"
|
||||
>
|
||||
{!!post?.image && (
|
||||
<img
|
||||
src={post.image || ''}
|
||||
alt=""
|
||||
className="object-cover w-full h-52"
|
||||
/>
|
||||
className={cn(
|
||||
'object-cover w-full h-52 overflow-hidden hover:scale-105 transition-all',
|
||||
)}
|
||||
>
|
||||
{getCardContent()}
|
||||
</Link>
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { Tag } from '@/lib/theme.types'
|
||||
import { slug } from 'github-slugger'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import Link from './Link'
|
||||
|
||||
interface PostListWithTagProps {
|
||||
tags: Tag[]
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './pages/HomePage'
|
||||
export * from './pages/TagDetailPage'
|
||||
export * from './pages/TagListPage'
|
||||
export * from './pages/BlogPage'
|
||||
export * from './pages/PageDetail'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import Image from '../components/Image'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
@@ -14,9 +15,11 @@ export function AboutPage({ site }: Props) {
|
||||
<div className="">
|
||||
<div className="flex flex-col items-center space-x-2 pt-8">
|
||||
{site.logo && (
|
||||
<img
|
||||
<Image
|
||||
src={site.logo}
|
||||
alt="avatar"
|
||||
width={192}
|
||||
height={192}
|
||||
className="h-48 w-48 rounded-full"
|
||||
/>
|
||||
)}
|
||||
@@ -24,6 +27,12 @@ export function AboutPage({ site }: Props) {
|
||||
{site.name}
|
||||
</h3>
|
||||
<div className="text-foreground/60">{site.description}</div>
|
||||
{/* <div className="flex space-x-3 pt-6">
|
||||
<SocialIcon kind="mail" href={`mailto:${email}`} />
|
||||
<SocialIcon kind="github" href={github} />
|
||||
<SocialIcon kind="linkedin" href={linkedin} />
|
||||
<SocialIcon kind="x" href={twitter} />
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="prose max-w-none pb-8 pt-8 dark:prose-invert xl:col-span-2 mx-auto lg:max-w-3xl">
|
||||
<ContentRender content={site.about} />
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { POSTS_PER_PAGE } from '@/lib/constants'
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import Link from 'next/link'
|
||||
import Image from '../components/Image'
|
||||
import Link from '../components/Link'
|
||||
import { PostItem } from '../components/PostItem'
|
||||
|
||||
const POSTS_PER_PAGE = Number(process.env.NEXT_PUBLIC_POSTS_PER_PAGE || 200)
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
posts: Post[]
|
||||
@@ -16,9 +16,11 @@ export function HomePage({ posts = [], site }: Props) {
|
||||
<div className="max-w-none mb-10 hover:text-foreground text-foreground/80">
|
||||
<div className="flex flex-col items-center flex-shrink-0">
|
||||
{site.logo && (
|
||||
<img
|
||||
<Image
|
||||
src={site.logo}
|
||||
alt="avatar"
|
||||
width={192}
|
||||
height={192}
|
||||
className="h-48 w-48 rounded-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
19
themes/card/pages/PageDetail.tsx
Normal file
19
themes/card/pages/PageDetail.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
|
||||
interface LayoutProps {
|
||||
page: any
|
||||
content: any
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PageDetail({ content, className }: LayoutProps) {
|
||||
return (
|
||||
<article className="mt-10 sm:mt-20 mx-auto w-full lg:max-w-3xl">
|
||||
<div className="prose max-w-none pb-8 dark:prose-invert">
|
||||
<ContentRender content={content} />
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,17 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { PostActions } from '@/components/theme-ui/PostActions'
|
||||
import { PostSubtitle } from '@/components/theme-ui/PostSubtitle'
|
||||
import { SubscribeNewsletterCard } from '@/components/theme-ui/SubscribeNewsletter/SubscribeNewsletterCard'
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { Post } from '@/lib/theme.types'
|
||||
import SectionContainer from '../components/SectionContainer'
|
||||
import Image from '../components/Image'
|
||||
import Link from '../components/Link'
|
||||
|
||||
interface LayoutProps {
|
||||
site: Site
|
||||
post: Post
|
||||
children: ReactNode
|
||||
className?: string
|
||||
@@ -16,82 +19,86 @@ interface LayoutProps {
|
||||
prev?: Post
|
||||
}
|
||||
|
||||
export function PostDetail({ post, className, next, prev }: LayoutProps) {
|
||||
export function PostDetail({ site, post, className, next, prev }: LayoutProps) {
|
||||
return (
|
||||
<SectionContainer className={className}>
|
||||
<article className="mt-20 mx-auto w-full lg:max-w-3xl">
|
||||
<header className="space-y-4 pb-4">
|
||||
<PageTitle>{post.title}</PageTitle>
|
||||
<div className="flex justify-between items-center">
|
||||
<dl className="flex items-center gap-2 text-foreground/50">
|
||||
<dd className="text-base font-medium leading-6">
|
||||
<time>{formatDate(post.updatedAt)}</time>
|
||||
</dd>
|
||||
<dd>·</dd>
|
||||
<dd className="text-base font-medium leading-6">
|
||||
{post.readingTime.text}
|
||||
</dd>
|
||||
</dl>
|
||||
<article className="mt-20 mx-auto w-full lg:max-w-3xl">
|
||||
<header className="space-y-4 pb-4">
|
||||
<div className="mb-4">
|
||||
<PageTitle className="mb-2">{post.title}</PageTitle>
|
||||
{post.description && <PostSubtitle>{post.description}</PostSubtitle>}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<dl className="flex items-center gap-2 text-foreground/50">
|
||||
<dd className="text-base font-medium leading-6">
|
||||
<time>{formatDate(post.updatedAt)}</time>
|
||||
</dd>
|
||||
<dd>·</dd>
|
||||
<dd className="text-base font-medium leading-6">
|
||||
{post.readingTime.text}
|
||||
</dd>
|
||||
</dl>
|
||||
</div>
|
||||
|
||||
<PostActions post={post} />
|
||||
</header>
|
||||
|
||||
{!!post.image && (
|
||||
<Image
|
||||
src={post.image || ''}
|
||||
alt=""
|
||||
width={1000}
|
||||
height={800}
|
||||
className="object-cover w-full max-h-96 rounded-2xl"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid-rows-[auto_1fr]">
|
||||
<div className="prose max-w-none pb-8 dark:prose-invert">
|
||||
<ContentRender content={post.content} />
|
||||
<SubscribeNewsletterCard site={site} />
|
||||
</div>
|
||||
|
||||
{post.cid && (
|
||||
<div className="text-foreground/60 text-xs rounded-md py-2 md:flex items-center gap-2 hidden">
|
||||
<span className="text-foreground/80">IPFS CID:</span>
|
||||
<span>{post.cid}</span>
|
||||
<a
|
||||
className="inline-flex"
|
||||
href={`https://ipfs-gateway.spaceprotocol.xyz/ipfs/${post.cid}`}
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink className="cursor-pointer" size={12} />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<PostActions post={post} />
|
||||
</header>
|
||||
|
||||
{!!post.image && (
|
||||
<img
|
||||
src={post.image || ''}
|
||||
alt=""
|
||||
className="object-cover w-full max-h-96 rounded-2xl"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid-rows-[auto_1fr]">
|
||||
<div className="prose max-w-none pb-8 dark:prose-invert">
|
||||
<ContentRender content={post.content} />
|
||||
<footer>
|
||||
<div className="flex flex-col text-sm font-medium sm:flex-row sm:justify-between sm:text-base">
|
||||
{prev && prev?.slug && (
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href={`/posts/${prev.slug}`}
|
||||
className="text-brand-500 hover:text-brand-600 dark:hover:text-brand-400"
|
||||
aria-label={`Previous post: ${prev.title}`}
|
||||
>
|
||||
← {prev.title}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{next && next?.slug && (
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href={`/posts/${next.slug}`}
|
||||
className="text-brand-500 hover:text-brand-600 dark:hover:text-brand-400"
|
||||
aria-label={`Next post: ${next.title}`}
|
||||
>
|
||||
{next.title} →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{post.cid && (
|
||||
<div className="text-foreground/60 text-xs rounded-md py-2 md:flex items-center gap-2 hidden">
|
||||
<span className="text-foreground/80">IPFS CID:</span>
|
||||
<span>{post.cid}</span>
|
||||
<a
|
||||
className="inline-flex"
|
||||
href={`https://ipfs-gateway.spaceprotocol.xyz/ipfs/${post.cid}`}
|
||||
target="_blank"
|
||||
>
|
||||
<ExternalLink className="cursor-pointer" size={12} />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<footer>
|
||||
<div className="flex flex-col text-sm font-medium sm:flex-row sm:justify-between sm:text-base">
|
||||
{prev && prev?.slug && (
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href={`/posts/${prev.slug}`}
|
||||
className="text-brand-500 hover:text-brand-600 dark:hover:text-brand-400"
|
||||
aria-label={`Previous post: ${prev.title}`}
|
||||
>
|
||||
← {prev.title}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{next && next?.slug && (
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href={`/posts/${next.slug}`}
|
||||
className="text-brand-500 hover:text-brand-600 dark:hover:text-brand-400"
|
||||
aria-label={`Next post: ${next.title}`}
|
||||
>
|
||||
{next.title} →
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
</SectionContainer>
|
||||
</footer>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 898 KiB |
@@ -1,9 +1,9 @@
|
||||
import { Lobster } from 'next/font/google'
|
||||
import Link from 'next/link'
|
||||
import { Profile } from '@/components/Profile/Profile'
|
||||
import { Airdrop } from '@/components/theme-ui/Airdrop'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Lobster } from 'next/font/google'
|
||||
import Link from './Link'
|
||||
import { PostTypeNav } from './PostTypeNav'
|
||||
|
||||
const lobster = Lobster({
|
||||
@@ -20,7 +20,7 @@ export const Header = ({ site }: Props) => {
|
||||
const links = [
|
||||
...site?.navLinks,
|
||||
{
|
||||
pathname: '/creator-fi/plans',
|
||||
pathname: '/creator-fi',
|
||||
title: 'CreatorFi',
|
||||
visible: true,
|
||||
},
|
||||
@@ -31,17 +31,18 @@ export const Header = ({ site }: Props) => {
|
||||
<div className="lg:flex items-center space-x-4 leading-5 sm:space-x-6 hidden flex-1">
|
||||
<div className="flex items-center space-x-4">
|
||||
{links.map((link) => {
|
||||
if (link.pathname === '/creator-fi/plans' && !site.spaceId) {
|
||||
if (link.pathname === '/creator-fi' && !site.spaceId) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!link.visible) return null
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={link.pathname}
|
||||
href={link.pathname}
|
||||
className={cn(
|
||||
'font-medium hover:text-brand-500 dark:hover:text-brand-400 text-xs text-foreground/60',
|
||||
'font-medium hover:text-brand-500 dark:hover:text-brand-400 text-foreground/90',
|
||||
)}
|
||||
>
|
||||
{link.title}
|
||||
@@ -53,7 +54,7 @@ export const Header = ({ site }: Props) => {
|
||||
<Link
|
||||
href="/membership"
|
||||
className={cn(
|
||||
'font-medium hover:text-brand-500 dark:hover:text-brand-400 text-xs text-foreground/60',
|
||||
'font-medium hover:text-brand-500 text-foreground/90',
|
||||
'border border-brand-500 text-brand-500 rounded-full px-2 py-1 hover:bg-brand-500 hover:text-background text-sm',
|
||||
)}
|
||||
>
|
||||
@@ -85,10 +86,16 @@ export const Header = ({ site }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex item-center justify-end gap-3 flex-1">
|
||||
<Link
|
||||
href="/about"
|
||||
className="font-medium flex items-center hover:text-brand-500 text-foreground/60 text-xs hover:scale-105 transition-all sm:hidden"
|
||||
>
|
||||
About
|
||||
</Link>
|
||||
<div className="flex items-center">
|
||||
<Airdrop />
|
||||
</div>
|
||||
<Profile />
|
||||
<Profile></Profile>
|
||||
</div>
|
||||
</div>
|
||||
<PostTypeNav className="flex md:hidden" />
|
||||
|
||||
9
themes/garden/components/Image.tsx
Normal file
9
themes/garden/components/Image.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import NextImage, { ImageProps } from 'next/image'
|
||||
|
||||
const basePath = process.env.BASE_PATH
|
||||
|
||||
const Image = ({ src, ...rest }: ImageProps) => (
|
||||
<NextImage src={`${basePath || ''}${src}`} {...rest} />
|
||||
)
|
||||
|
||||
export default Image
|
||||
34
themes/garden/components/Link.tsx
Normal file
34
themes/garden/components/Link.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { AnchorHTMLAttributes } from 'react'
|
||||
import Link from 'next/link'
|
||||
import type { LinkProps } from 'next/link'
|
||||
|
||||
const CustomLink = ({
|
||||
href,
|
||||
...rest
|
||||
}: LinkProps & AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const isInternalLink = href && href.startsWith('/')
|
||||
const isAnchorLink = href && href.startsWith('#')
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
if (isInternalLink) {
|
||||
// if (isProd) href = href + '.html'
|
||||
return <Link className="break-words" href={href} {...rest} />
|
||||
}
|
||||
|
||||
if (isAnchorLink) {
|
||||
return <a className="break-words" href={href} {...rest} />
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className="break-words"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomLink
|
||||
@@ -1,25 +1,20 @@
|
||||
'use client'
|
||||
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { isAddress } from '@/lib/utils'
|
||||
import { Post, PostType, User } from '@/lib/theme.types'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { Node } from 'slate'
|
||||
import { PlateEditor } from '@/components/editor/plate-editor'
|
||||
import { PostActions } from '@/components/theme-ui/PostActions/PostActions'
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Post, PostType, User } from '@/lib/theme.types'
|
||||
import { cn, formatDate, isAddress } from '@/lib/utils'
|
||||
|
||||
interface PostItemProps {
|
||||
post: Post
|
||||
receivers?: string[]
|
||||
ContentRender?: (props: { content: any[]; className?: string }) => JSX.Element
|
||||
PostActions?: (props: {
|
||||
post: Post
|
||||
receivers?: string[]
|
||||
className?: string
|
||||
}) => JSX.Element
|
||||
}
|
||||
|
||||
export function PostItem({ post, PostActions, receivers = [] }: PostItemProps) {
|
||||
export function PostItem({ post, receivers = [] }: PostItemProps) {
|
||||
const { slug, title } = post
|
||||
|
||||
const name = getUserName(post.user)
|
||||
@@ -43,12 +38,20 @@ export function PostItem({ post, PostActions, receivers = [] }: PostItemProps) {
|
||||
const getContent = () => {
|
||||
if (post.type === PostType.IMAGE) {
|
||||
return (
|
||||
<img src={post.image!} alt="" className="w-full h-auto rounded-lg" />
|
||||
<img src={post.content} alt="" className="w-full h-auto rounded-lg" />
|
||||
)
|
||||
}
|
||||
|
||||
if (post.type === PostType.NOTE) {
|
||||
return <div className="text-foreground/80">{post.title}</div>
|
||||
return (
|
||||
<div className="text-foreground/80">
|
||||
<PlateEditor
|
||||
value={JSON.parse(post.content)}
|
||||
readonly
|
||||
className="px-0 py-0"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const nodes: any[] =
|
||||
@@ -57,12 +60,12 @@ export function PostItem({ post, PostActions, receivers = [] }: PostItemProps) {
|
||||
|
||||
return (
|
||||
<Link href={`/posts/${slug}`} className="space-y-2">
|
||||
<div className="text-2xl font-bold hover:scale-105 transition-all origin-left">
|
||||
<h2 className="text-2xl font-bold hover:scale-105 transition-all origin-left block">
|
||||
{post.title}
|
||||
</div>
|
||||
<div className="text-foreground/80 hover:text-foreground transition-all hover:scale-105 break-words break-all">
|
||||
{str?.slice(0, 260)}...
|
||||
</div>
|
||||
</h2>
|
||||
<p className="text-foreground/70 hover:text-foreground transition-all hover:scale-105 line-clamp-2">
|
||||
{post.description || str?.slice(0, 200)}
|
||||
</p>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
@@ -103,7 +106,7 @@ export function PostItem({ post, PostActions, receivers = [] }: PostItemProps) {
|
||||
|
||||
{getContent()}
|
||||
|
||||
{PostActions && <PostActions post={post} receivers={receivers} />}
|
||||
<PostActions post={post} receivers={receivers} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ export function PostListWithTag({
|
||||
initialDisplayPosts.length > 0 ? initialDisplayPosts : posts
|
||||
|
||||
return (
|
||||
<div className="flex flex-col mx-auto max-w-3xl">
|
||||
<div className="flex flex-col justify-center items-center mx-auto max-w-3xl">
|
||||
<PageTitle>Tags</PageTitle>
|
||||
<TagList tags={tags} />
|
||||
<div className="mt-10">
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { Tag } from '@/lib/theme.types'
|
||||
import { slug } from 'github-slugger'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import Link from './Link'
|
||||
|
||||
interface PostListWithTagProps {
|
||||
tags: Tag[]
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './pages/HomePage'
|
||||
export * from './pages/TagDetailPage'
|
||||
export * from './pages/TagListPage'
|
||||
export * from './pages/BlogPage'
|
||||
export * from './pages/PageDetail'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import Image from '../components/Image'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
@@ -13,9 +14,11 @@ export function AboutPage({ site }: Props) {
|
||||
<div className="">
|
||||
<div className="flex flex-col items-center space-x-2 pt-8">
|
||||
{site.logo && (
|
||||
<img
|
||||
<Image
|
||||
src={site.logo}
|
||||
alt="avatar"
|
||||
width={192}
|
||||
height={192}
|
||||
className="h-48 w-48 rounded-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function BlogPage({
|
||||
initialDisplayPosts,
|
||||
}: Props) {
|
||||
return (
|
||||
<div className="space-y-6 mx-auto max-w-3xl">
|
||||
<div className="space-y-6 mx-auto sm:max-w-xl">
|
||||
<PageTitle>Blog</PageTitle>
|
||||
<PostList
|
||||
posts={posts}
|
||||
|
||||
19
themes/garden/pages/PageDetail.tsx
Normal file
19
themes/garden/pages/PageDetail.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
|
||||
interface LayoutProps {
|
||||
page: any
|
||||
content: any
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PageDetail({ content, className }: LayoutProps) {
|
||||
return (
|
||||
<article className="mt-10 sm:mt-20 mx-auto w-full lg:max-w-3xl">
|
||||
<div className="prose max-w-none pb-8 dark:prose-invert">
|
||||
<ContentRender content={content} />
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,16 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { PostActions } from '@/components/theme-ui/PostActions'
|
||||
import { PostSubtitle } from '@/components/theme-ui/PostSubtitle'
|
||||
import { SubscribeNewsletterCard } from '@/components/theme-ui/SubscribeNewsletter/SubscribeNewsletterCard'
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
import { Post } from '@/lib/theme.types'
|
||||
import SectionContainer from '../components/SectionContainer'
|
||||
import Link from '../components/Link'
|
||||
|
||||
interface LayoutProps {
|
||||
site: Site
|
||||
post: Post
|
||||
children: ReactNode
|
||||
className?: string
|
||||
@@ -16,11 +18,14 @@ interface LayoutProps {
|
||||
prev?: { path: string; title: string }
|
||||
}
|
||||
|
||||
export function PostDetail({ post, next, prev, className }: LayoutProps) {
|
||||
export function PostDetail({ site, post, next, prev, className }: LayoutProps) {
|
||||
return (
|
||||
<article className={cn('mt-20 mx-auto w-full lg:max-w-3xl', className)}>
|
||||
<header className="space-y-4 pb-4">
|
||||
<PageTitle className="mb-0">{post.title}</PageTitle>
|
||||
<div className="mb-4">
|
||||
<PageTitle className="mb-2">{post.title}</PageTitle>
|
||||
{post.description && <PostSubtitle>{post.description}</PostSubtitle>}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<dl className="flex items-center gap-2 text-foreground/50">
|
||||
<dt className="sr-only">Published on</dt>
|
||||
@@ -39,6 +44,7 @@ export function PostDetail({ post, next, prev, className }: LayoutProps) {
|
||||
<div className="grid-rows-[auto_1fr]">
|
||||
<div className="prose max-w-none pb-8 dark:prose-invert">
|
||||
<ContentRender content={post.content} />
|
||||
<SubscribeNewsletterCard site={site} />
|
||||
</div>
|
||||
{post.cid && (
|
||||
<div className="text-foreground/60 text-xs rounded-md py-2 md:flex items-center gap-2 hidden">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { Tag } from '@/lib/theme.types'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { TagList } from '../components/TagList'
|
||||
|
||||
interface Props {
|
||||
@@ -8,7 +8,7 @@ interface Props {
|
||||
|
||||
export function TagListPage({ tags }: Props) {
|
||||
return (
|
||||
<div className="flex flex-col mx-auto max-w-3xl">
|
||||
<div className="flex flex-col justify-center items-center mx-auto max-w-3xl">
|
||||
<PageTitle>Tags</PageTitle>
|
||||
<div className="grid gap-y-3">
|
||||
{tags.length === 0 && 'No tags found.'}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import Link from 'next/link'
|
||||
import { Profile } from '@/components/Profile/Profile'
|
||||
import { Airdrop } from '@/components/theme-ui/Airdrop'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import Link from './Link'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
@@ -12,7 +12,7 @@ export const Header = ({ site }: Props) => {
|
||||
const links = [
|
||||
...site?.navLinks,
|
||||
{
|
||||
pathname: '/creator-fi/plans',
|
||||
pathname: '/creator-fi',
|
||||
title: 'CreatorFi',
|
||||
visible: true,
|
||||
},
|
||||
@@ -24,7 +24,7 @@ export const Header = ({ site }: Props) => {
|
||||
<div className="flex items-center space-x-4 leading-5 sm:space-x-6">
|
||||
<div className="no-scrollbar hidden items-center space-x-4 overflow-x-auto sm:flex sm:space-x-6">
|
||||
{links.map((link) => {
|
||||
if (link.pathname === '/creator-fi/plans' && !site.spaceId) {
|
||||
if (link.pathname === '/creator-fi' && !site.spaceId) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -41,26 +41,27 @@ export const Header = ({ site }: Props) => {
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
||||
{site.spaceId && (
|
||||
<Link
|
||||
href="/membership"
|
||||
className={cn(
|
||||
'font-medium hover:text-brand-500 dark:hover:text-brand-400 text-foreground/90',
|
||||
'border border-brand-500 text-brand-500 rounded-full px-2 py-1 hover:bg-brand-500 hover:text-background text-sm',
|
||||
)}
|
||||
>
|
||||
Membership
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{site.spaceId && (
|
||||
<Link
|
||||
href="/membership"
|
||||
className={cn(
|
||||
'font-medium hover:text-brand-500 text-foreground/90',
|
||||
'border border-brand-500 text-brand-500 rounded-full px-2 py-1 hover:bg-brand-500 hover:text-background text-sm',
|
||||
)}
|
||||
>
|
||||
Membership
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex item-center gap-2">
|
||||
<div className="flex items-center">
|
||||
<Airdrop />
|
||||
</div>
|
||||
<Profile />
|
||||
|
||||
<Profile></Profile>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
9
themes/micro/components/Image.tsx
Normal file
9
themes/micro/components/Image.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import NextImage, { ImageProps } from 'next/image'
|
||||
|
||||
const basePath = process.env.BASE_PATH
|
||||
|
||||
const Image = ({ src, ...rest }: ImageProps) => (
|
||||
<NextImage src={`${basePath || ''}${src}`} {...rest} />
|
||||
)
|
||||
|
||||
export default Image
|
||||
34
themes/micro/components/Link.tsx
Normal file
34
themes/micro/components/Link.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { AnchorHTMLAttributes } from 'react'
|
||||
import Link from 'next/link'
|
||||
import type { LinkProps } from 'next/link'
|
||||
|
||||
const CustomLink = ({
|
||||
href,
|
||||
...rest
|
||||
}: LinkProps & AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const isInternalLink = href && href.startsWith('/')
|
||||
const isAnchorLink = href && href.startsWith('#')
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
if (isInternalLink) {
|
||||
// if (isProd) href = href + '.html'
|
||||
return <Link className="break-words" href={href} {...rest} />
|
||||
}
|
||||
|
||||
if (isAnchorLink) {
|
||||
return <a className="break-words" href={href} {...rest} />
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className="break-words"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomLink
|
||||
@@ -1,6 +1,8 @@
|
||||
import Image from 'next/image'
|
||||
import { PlateEditor } from '@/components/editor/plate-editor'
|
||||
import { Post, PostType } from '@/lib/theme.types'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { Post } from '@/lib/theme.types'
|
||||
import Link from 'next/link'
|
||||
import Link from './Link'
|
||||
import Tag from './Tag'
|
||||
|
||||
interface PostItemProps {
|
||||
@@ -10,6 +12,37 @@ interface PostItemProps {
|
||||
export function PostItem({ post }: PostItemProps) {
|
||||
const { slug, title } = post
|
||||
|
||||
const getContent = () => {
|
||||
if (post.type === PostType.IMAGE) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-base font-bold">{post.title || 'Untitled'}</div>
|
||||
<Image
|
||||
src={post.content}
|
||||
alt=""
|
||||
width={100}
|
||||
height={100}
|
||||
className="w-10 h-10 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (post.type === PostType.NOTE) {
|
||||
return (
|
||||
<div className="text-foreground/80">
|
||||
<PlateEditor
|
||||
value={JSON.parse(post.content)}
|
||||
readonly
|
||||
className="px-0 py-0"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className="text-lg">{title}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Link
|
||||
@@ -17,7 +50,7 @@ export function PostItem({ post }: PostItemProps) {
|
||||
href={`/posts/${slug}`}
|
||||
className="hover:text-foreground flex items-center justify-between gap-6 text-foreground/80"
|
||||
>
|
||||
<div className="text-lg">{title}</div>
|
||||
{getContent()}
|
||||
</Link>
|
||||
|
||||
<div className="flex items-center text-sm gap-3">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Post, Tag } from '@/lib/theme.types'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { Post, Tag } from '@/lib/theme.types'
|
||||
import { PostList } from './PostList'
|
||||
import { TagList } from './TagList'
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { Tag } from '@/lib/theme.types'
|
||||
import { slug } from 'github-slugger'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import Link from './Link'
|
||||
|
||||
interface PostListWithTagProps {
|
||||
tags: Tag[]
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './pages/HomePage'
|
||||
export * from './pages/TagDetailPage'
|
||||
export * from './pages/TagListPage'
|
||||
export * from './pages/BlogPage'
|
||||
export * from './pages/PageDetail'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import Image from '../components/Image'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
@@ -14,9 +15,11 @@ export function AboutPage({ site }: Props) {
|
||||
<div className="">
|
||||
<div className="flex flex-col items-center space-x-2 pt-8">
|
||||
{site.logo && (
|
||||
<img
|
||||
<Image
|
||||
src={site.logo}
|
||||
alt="avatar"
|
||||
width={192}
|
||||
height={192}
|
||||
className="h-48 w-48 rounded-full"
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { POSTS_PER_PAGE } from '@/lib/constants'
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import Link from 'next/link'
|
||||
import Link from '../components/Link'
|
||||
import { PostItem } from '../components/PostItem'
|
||||
|
||||
const POSTS_PER_PAGE = Number(process.env.NEXT_PUBLIC_POSTS_PER_PAGE || 200)
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
posts: Post[]
|
||||
|
||||
19
themes/micro/pages/PageDetail.tsx
Normal file
19
themes/micro/pages/PageDetail.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
|
||||
interface LayoutProps {
|
||||
page: any
|
||||
content: any
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PageDetail({ content, className }: LayoutProps) {
|
||||
return (
|
||||
<article className="mt-10 sm:mt-20 mx-auto w-full lg:max-w-3xl">
|
||||
<div className="prose max-w-none pb-8 dark:prose-invert">
|
||||
<ContentRender content={content} />
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -2,12 +2,15 @@ import { ReactNode } from 'react'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { PostActions } from '@/components/theme-ui/PostActions'
|
||||
import { PostSubtitle } from '@/components/theme-ui/PostSubtitle'
|
||||
import { SubscribeNewsletterCard } from '@/components/theme-ui/SubscribeNewsletter/SubscribeNewsletterCard'
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
import { Post } from '@/lib/theme.types'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import Link from '../components/Link'
|
||||
|
||||
interface LayoutProps {
|
||||
site: Site
|
||||
post: Post
|
||||
children: ReactNode
|
||||
className?: string
|
||||
@@ -16,6 +19,7 @@ interface LayoutProps {
|
||||
}
|
||||
|
||||
export function PostDetail({
|
||||
site,
|
||||
post,
|
||||
next,
|
||||
prev,
|
||||
@@ -25,7 +29,10 @@ export function PostDetail({
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<header className="space-y-4 pb-4">
|
||||
<PageTitle className="mb-0">{post.title}</PageTitle>
|
||||
<div className="mb-4">
|
||||
<PageTitle className="mb-2">{post.title}</PageTitle>
|
||||
{post.description && <PostSubtitle>{post.description}</PostSubtitle>}
|
||||
</div>
|
||||
<dl className="flex items-center gap-2">
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6 text-foreground/60">
|
||||
@@ -41,6 +48,7 @@ export function PostDetail({
|
||||
<div className="grid-rows-[auto_1fr]">
|
||||
<div className="prose max-w-none pb-8 dark:prose-invert">
|
||||
<ContentRender content={post.content} />
|
||||
<SubscribeNewsletterCard site={site} />
|
||||
</div>
|
||||
|
||||
{post.cid && (
|
||||
|
||||
18
themes/minimal/components/ClientOnly.tsx
Normal file
18
themes/minimal/components/ClientOnly.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
||||
|
||||
export function ClientOnly({ children }: PropsWithChildren) {
|
||||
// State / Props
|
||||
const [hasMounted, setHasMounted] = useState(false)
|
||||
|
||||
// Hooks
|
||||
useEffect(() => {
|
||||
setHasMounted(true)
|
||||
}, [])
|
||||
|
||||
// Render
|
||||
if (!hasMounted) return null
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
@@ -1,8 +1,8 @@
|
||||
import Link from 'next/link'
|
||||
import { Profile } from '@/components/Profile/Profile'
|
||||
import { Airdrop } from '@/components/theme-ui/Airdrop'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import Link from './Link'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
@@ -12,7 +12,7 @@ export const Header = ({ site }: Props) => {
|
||||
const links = [
|
||||
...site?.navLinks,
|
||||
{
|
||||
pathname: '/creator-fi/plans',
|
||||
pathname: '/creator-fi',
|
||||
title: 'CreatorFi',
|
||||
visible: true,
|
||||
},
|
||||
@@ -24,11 +24,12 @@ export const Header = ({ site }: Props) => {
|
||||
<div className="flex items-center space-x-4 leading-5 sm:space-x-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
{links.map((link) => {
|
||||
if (link.pathname === '/creator-fi/plans' && !site.spaceId) {
|
||||
if (link.pathname === '/creator-fi' && !site.spaceId) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (!link.visible) return null
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={link.pathname}
|
||||
@@ -46,7 +47,7 @@ export const Header = ({ site }: Props) => {
|
||||
<Link
|
||||
href="/membership"
|
||||
className={cn(
|
||||
'font-medium hover:text-brand-500 dark:hover:text-brand-400 text-foreground/90',
|
||||
'font-medium hover:text-brand-500 text-foreground/90',
|
||||
'border border-brand-500 text-brand-500 rounded-full px-2 py-1 hover:bg-brand-500 hover:text-background text-sm',
|
||||
)}
|
||||
>
|
||||
@@ -59,7 +60,7 @@ export const Header = ({ site }: Props) => {
|
||||
<div className="flex items-center">
|
||||
<Airdrop />
|
||||
</div>
|
||||
<Profile />
|
||||
<Profile></Profile>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
||||
9
themes/minimal/components/Image.tsx
Normal file
9
themes/minimal/components/Image.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import NextImage, { ImageProps } from 'next/image'
|
||||
|
||||
const basePath = process.env.BASE_PATH
|
||||
|
||||
const Image = ({ src, ...rest }: ImageProps) => (
|
||||
<NextImage src={`${basePath || ''}${src}`} {...rest} />
|
||||
)
|
||||
|
||||
export default Image
|
||||
34
themes/minimal/components/Link.tsx
Normal file
34
themes/minimal/components/Link.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { AnchorHTMLAttributes } from 'react'
|
||||
import Link from 'next/link'
|
||||
import type { LinkProps } from 'next/link'
|
||||
|
||||
const CustomLink = ({
|
||||
href,
|
||||
...rest
|
||||
}: LinkProps & AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const isInternalLink = href && href.startsWith('/')
|
||||
const isAnchorLink = href && href.startsWith('#')
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
if (isInternalLink) {
|
||||
// if (isProd) href = href + '.html'
|
||||
return <Link className="break-words" href={href} {...rest} />
|
||||
}
|
||||
|
||||
if (isAnchorLink) {
|
||||
return <a className="break-words" href={href} {...rest} />
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className="break-words"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomLink
|
||||
@@ -1,6 +1,8 @@
|
||||
import Image from 'next/image'
|
||||
import { PlateEditor } from '@/components/editor/plate-editor'
|
||||
import { Post, PostType } from '@/lib/theme.types'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import { Post } from '@/lib/theme.types'
|
||||
import Link from 'next/link'
|
||||
import Link from './Link'
|
||||
|
||||
interface PostItemProps {
|
||||
post: Post
|
||||
@@ -9,13 +11,44 @@ interface PostItemProps {
|
||||
export function PostItem({ post }: PostItemProps) {
|
||||
const { slug, title } = post
|
||||
|
||||
const getContent = () => {
|
||||
if (post.type === PostType.IMAGE) {
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="text-base font-bold">{post.title || 'Untitled'}</div>
|
||||
<Image
|
||||
src={post.content}
|
||||
alt=""
|
||||
width={100}
|
||||
height={100}
|
||||
className="w-10 h-10 rounded-lg"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (post.type === PostType.NOTE) {
|
||||
return (
|
||||
<div className="text-foreground/80">
|
||||
<PlateEditor
|
||||
value={JSON.parse(post.content)}
|
||||
readonly
|
||||
className="px-0 py-0"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return <div className="text-lg">{title}</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
key={slug}
|
||||
href={`/posts/${slug}`}
|
||||
className="hover:text-foreground flex items-center justify-between gap-6 text-foreground/80"
|
||||
>
|
||||
<div className="text-lg">{title}</div>
|
||||
{getContent()}
|
||||
<time className="text-sm text-foreground/50">
|
||||
{formatDate(post.updatedAt)}
|
||||
</time>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Post, Tag } from '@/lib/theme.types'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { Post, Tag } from '@/lib/theme.types'
|
||||
import { PostList } from './PostList'
|
||||
import { TagList } from './TagList'
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { Tag } from '@/lib/theme.types'
|
||||
import { slug } from 'github-slugger'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import Link from './Link'
|
||||
|
||||
interface PostListWithTagProps {
|
||||
tags: Tag[]
|
||||
|
||||
@@ -5,3 +5,4 @@ export * from './pages/HomePage'
|
||||
export * from './pages/TagDetailPage'
|
||||
export * from './pages/TagListPage'
|
||||
export * from './pages/BlogPage'
|
||||
export * from './pages/PageDetail'
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import Image from '../components/Image'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
@@ -14,9 +15,11 @@ export function AboutPage({ site }: Props) {
|
||||
<div className="">
|
||||
<div className="flex flex-col items-center space-x-2 pt-8">
|
||||
{site.logo && (
|
||||
<img
|
||||
<Image
|
||||
src={site.logo}
|
||||
alt="avatar"
|
||||
width={192}
|
||||
height={192}
|
||||
className="h-48 w-48 rounded-full"
|
||||
/>
|
||||
)}
|
||||
@@ -24,6 +27,12 @@ export function AboutPage({ site }: Props) {
|
||||
{site.name}
|
||||
</h3>
|
||||
<div className="text-foreground/60">{site.description}</div>
|
||||
{/* <div className="flex space-x-3 pt-6">
|
||||
<SocialIcon kind="mail" href={`mailto:${email}`} />
|
||||
<SocialIcon kind="github" href={github} />
|
||||
<SocialIcon kind="linkedin" href={linkedin} />
|
||||
<SocialIcon kind="x" href={twitter} />
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="prose max-w-none pb-8 pt-8 dark:prose-invert xl:col-span-2 mx-auto lg:max-w-3xl">
|
||||
<ContentRender content={site.about} />
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { POSTS_PER_PAGE } from '@/lib/constants'
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import Link from 'next/link'
|
||||
import Link from '../components/Link'
|
||||
import { PostItem } from '../components/PostItem'
|
||||
|
||||
const POSTS_PER_PAGE = Number(process.env.NEXT_PUBLIC_POSTS_PER_PAGE || 200)
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
posts: Post[]
|
||||
|
||||
19
themes/minimal/pages/PageDetail.tsx
Normal file
19
themes/minimal/pages/PageDetail.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
|
||||
interface LayoutProps {
|
||||
page: any
|
||||
content: any
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PageDetail({ content, className }: LayoutProps) {
|
||||
return (
|
||||
<article className="mt-10 sm:mt-20 mx-auto w-full lg:max-w-3xl">
|
||||
<div className="prose max-w-none pb-8 dark:prose-invert">
|
||||
<ContentRender content={content} />
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -2,12 +2,15 @@ import { ReactNode } from 'react'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { PostActions } from '@/components/theme-ui/PostActions'
|
||||
import { SubscribeNewsletterCard } from '@/components/theme-ui/SubscribeNewsletter/SubscribeNewsletterCard'
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
import { Post } from '@/lib/theme.types'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import Link from '../components/Link'
|
||||
import { PostSubtitle } from '@/components/theme-ui/PostSubtitle'
|
||||
|
||||
interface LayoutProps {
|
||||
site: Site
|
||||
post: Post
|
||||
children: ReactNode
|
||||
className?: string
|
||||
@@ -15,11 +18,14 @@ interface LayoutProps {
|
||||
prev?: { path: string; title: string }
|
||||
}
|
||||
|
||||
export function PostDetail({ post, next, prev, className }: LayoutProps) {
|
||||
export function PostDetail({ site, post, next, prev, className }: LayoutProps) {
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<header className="space-y-4 pb-4">
|
||||
<PageTitle className="mb-0">{post.title}</PageTitle>
|
||||
<div className="mb-4">
|
||||
<PageTitle className="mb-2">{post.title}</PageTitle>
|
||||
{post.description && <PostSubtitle>{post.description}</PostSubtitle>}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<dl className="flex items-center gap-2 text-foreground/50">
|
||||
<dt className="sr-only">Published on</dt>
|
||||
@@ -38,6 +44,7 @@ export function PostDetail({ post, next, prev, className }: LayoutProps) {
|
||||
<div className="grid-rows-[auto_1fr]">
|
||||
<div className="prose max-w-none pb-8 dark:prose-invert">
|
||||
<ContentRender content={post.content} />
|
||||
<SubscribeNewsletterCard site={site} />
|
||||
</div>
|
||||
{post.cid && (
|
||||
<div className="text-foreground/60 text-xs rounded-md py-2 md:flex items-center gap-2 hidden">
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
import Link from 'next/link'
|
||||
import { Profile } from '@/components/Profile/Profile'
|
||||
import { Airdrop } from '@/components/theme-ui/Airdrop'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
|
||||
const headerNavLinks = [
|
||||
{ href: '/', title: 'Home' },
|
||||
// { href: '/posts', title: 'Blog' },
|
||||
// { href: '/tags', title: 'Tags' },
|
||||
{ href: '/about', title: 'About' },
|
||||
{ href: '/creator-fi/plans', title: 'CreatorFi' },
|
||||
{ href: '/membership', title: 'Membership', isMembership: true },
|
||||
]
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
}
|
||||
|
||||
export const Header = ({ site }: Props) => {
|
||||
return (
|
||||
<header
|
||||
className={cn(
|
||||
'flex items-center w-full justify-between py-4 h-16 z-40 sticky top-0',
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center space-x-4 leading-5 sm:space-x-6">
|
||||
<div className="flex items-center space-x-4">
|
||||
{headerNavLinks.map((link) => {
|
||||
if (link.href === '/creator-fi' && !site.spaceId) {
|
||||
return null
|
||||
}
|
||||
|
||||
if (link.href === '/membership' && !site.spaceId) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
key={link.title}
|
||||
href={link.href}
|
||||
className={cn(
|
||||
'font-medium hover:text-brand-500 dark:hover:text-brand-400 text-foreground/90',
|
||||
link.isMembership &&
|
||||
'border border-brand-500 text-brand-500 rounded-full px-2 py-1 hover:bg-brand-500 hover:text-background text-sm',
|
||||
)}
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
{/* {MobileNav && <MobileNav />} */}
|
||||
</div>
|
||||
<div className="flex item-center gap-2">
|
||||
<div className="flex items-center">
|
||||
<Airdrop />
|
||||
</div>
|
||||
<Profile />
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { isAddress } from '@/lib/utils'
|
||||
import { Post, User } from '@/lib/theme.types'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
|
||||
interface PostItemProps {
|
||||
post: Post
|
||||
receivers?: string[]
|
||||
PostActions?: (props: {
|
||||
post: Post
|
||||
receivers?: string[]
|
||||
className?: string
|
||||
}) => JSX.Element
|
||||
}
|
||||
|
||||
export function PostItem({ post, PostActions, receivers = [] }: PostItemProps) {
|
||||
const { slug, title } = post
|
||||
const name = getUserName(post.user)
|
||||
|
||||
const getAvatar = () => {
|
||||
if (post.user.image) {
|
||||
return (
|
||||
<Avatar className="h-6 w-6">
|
||||
<AvatarImage src={post.user.image || ''} />
|
||||
<AvatarFallback>{post.user.displayName}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-red-300 h-6 w-6 rounded-full flex-shrink-0',
|
||||
generateGradient(post.user.displayName || post.user.name),
|
||||
)}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
{getAvatar()}
|
||||
<div className="font-medium">{name}</div>
|
||||
<div className="text-foreground/50 text-sm">posted</div>
|
||||
<div className="">{title}</div>p
|
||||
</div>
|
||||
<time className="text-xs text-foreground/50">
|
||||
{formatDate(post.updatedAt)}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
{/* <Link key={slug} href={`/posts/${slug}`} className="flex"></Link> */}
|
||||
|
||||
<img src={post.image!} alt="" className="w-full h-auto rounded-lg" />
|
||||
|
||||
{PostActions && <PostActions post={post} receivers={receivers} />}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function hashCode(str: string) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i)
|
||||
hash = (hash << 5) - hash + char
|
||||
hash = hash & hash // Convert to 32bit integer
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
function getFromColor(i: number) {
|
||||
const colors = [
|
||||
'from-red-500',
|
||||
'from-yellow-500',
|
||||
'from-green-500',
|
||||
'from-blue-500',
|
||||
'from-indigo-500',
|
||||
'from-purple-500',
|
||||
'from-pink-500',
|
||||
'from-red-600',
|
||||
'from-yellow-600',
|
||||
'from-green-600',
|
||||
'from-blue-600',
|
||||
'from-indigo-600',
|
||||
'from-purple-600',
|
||||
'from-pink-600',
|
||||
]
|
||||
return colors[Math.abs(i) % colors.length]
|
||||
}
|
||||
|
||||
function getToColor(i: number) {
|
||||
const colors = [
|
||||
'to-red-500',
|
||||
'to-yellow-500',
|
||||
'to-green-500',
|
||||
'to-blue-500',
|
||||
'to-indigo-500',
|
||||
'to-purple-500',
|
||||
'to-pink-500',
|
||||
'to-red-600',
|
||||
'to-yellow-600',
|
||||
'to-green-600',
|
||||
'to-blue-600',
|
||||
'to-indigo-600',
|
||||
'to-purple-600',
|
||||
'to-pink-600',
|
||||
]
|
||||
return colors[Math.abs(i) % colors.length]
|
||||
}
|
||||
|
||||
function generateGradient(walletAddress: string) {
|
||||
if (!walletAddress) return `bg-gradient-to-r to-pink-500 to-purple-500`
|
||||
const hash = hashCode(walletAddress)
|
||||
const from = getFromColor(hash)
|
||||
const to = getToColor(hash >> 8)
|
||||
return `bg-gradient-to-r ${from} ${to}`
|
||||
}
|
||||
|
||||
function getUserName(user: User) {
|
||||
const { displayName = '', name } = user
|
||||
|
||||
if (displayName) {
|
||||
if (isAddress(displayName)) {
|
||||
return displayName.slice(0, 3) + '...' + displayName.slice(-4)
|
||||
}
|
||||
return user.displayName || user.name
|
||||
}
|
||||
|
||||
if (isAddress(name)) {
|
||||
return name.slice(0, 3) + '...' + name.slice(-4)
|
||||
}
|
||||
return user.displayName || user.name
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { ReactNode } from 'react'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
export default function SectionContainer({ children }: Props) {
|
||||
return (
|
||||
<section className="px-4 sm:px-6 xl:px-4 min-h-screen flex flex-col">
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { Tag } from '@/lib/theme.types'
|
||||
import { slug } from 'github-slugger'
|
||||
import Link from 'next/link'
|
||||
import { usePathname } from 'next/navigation'
|
||||
|
||||
interface PostListWithTagProps {
|
||||
tags: Tag[]
|
||||
}
|
||||
|
||||
export function TagList({ tags = [] }: PostListWithTagProps) {
|
||||
const pathname = usePathname()!
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<ul className="flex flex-wrap gap-x-5">
|
||||
{tags.map((t) => {
|
||||
return (
|
||||
<li key={t.id} className="my-3">
|
||||
{decodeURI(pathname.split('/tags/')[1]) === slug(t.name) ? (
|
||||
<h3 className="inline py-2 text-brand-500">#{`${t.name}`}</h3>
|
||||
) : (
|
||||
<Link
|
||||
href={`/tags/${slug(t.name)}`}
|
||||
className="py-2 text-foreground/60 hover:text-brand-500 dark:hover:text-brand-500 rounded-full"
|
||||
aria-label={`View posts tagged ${t.name}`}
|
||||
>
|
||||
#{`${t.name}`}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"name": "photo",
|
||||
"title": "Photo",
|
||||
"author": "0xZio",
|
||||
"description": "",
|
||||
"previewUrl": "https://demo3.penx.io"
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
}
|
||||
|
||||
export function AboutPage({ site }: Props) {
|
||||
return (
|
||||
<div className="mx-auto max-w-3xl">
|
||||
<PageTitle>About</PageTitle>
|
||||
<div className="">
|
||||
<div className="flex flex-col items-center space-x-2 pt-8">
|
||||
{site.logo && (
|
||||
<img
|
||||
src={site.logo}
|
||||
alt="avatar"
|
||||
className="h-48 w-48 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<h3 className="pb-2 pt-4 text-2xl font-bold leading-8 tracking-tight">
|
||||
{site.name}
|
||||
</h3>
|
||||
<div className="text-foreground/60">{site.description}</div>
|
||||
{/* <div className="flex space-x-3 pt-6">
|
||||
<SocialIcon kind="mail" href={`mailto:${email}`} />
|
||||
<SocialIcon kind="github" href={github} />
|
||||
<SocialIcon kind="linkedin" href={linkedin} />
|
||||
<SocialIcon kind="x" href={twitter} />
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="prose max-w-none pb-8 pt-8 dark:prose-invert xl:col-span-2 mx-auto lg:max-w-3xl">
|
||||
<ContentRender content={site.about} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,46 +0,0 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Post, PostType, Site } from '@/lib/theme.types'
|
||||
import { Lobster, Merienda } from 'next/font/google'
|
||||
import { PostItem } from '../components/PostItem'
|
||||
|
||||
const merienda = Lobster({
|
||||
weight: ['400'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
posts: Post[]
|
||||
}
|
||||
|
||||
export function HomePage({ posts = [], site }: Props) {
|
||||
const creations = posts.filter((post) => post.type === PostType.IMAGE)
|
||||
|
||||
const addresses = posts.reduce((acc, { user }) => {
|
||||
const { accounts = [] } = user
|
||||
for (const a of accounts) {
|
||||
if (a.providerType === 'WALLET') acc.push(a.providerAccountId)
|
||||
}
|
||||
return acc
|
||||
}, [] as string[])
|
||||
const receivers = Array.from(new Set(addresses))
|
||||
|
||||
return (
|
||||
<div className="mx-auto sm:max-w-xl flex flex-col gap-10 -mt-16">
|
||||
<div className="flex items-center gap-2 sticky top-0 bg-background/40 py-4 z-40 backdrop-blur-sm">
|
||||
{site.logo && (
|
||||
<img src={site.logo} alt="" className="w-10 h-10 rounded-full" />
|
||||
)}
|
||||
<div className={cn('font-normal text-3xl', merienda.className)}>
|
||||
{site.name}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-12">
|
||||
{creations.map((post) => {
|
||||
return <PostItem key={post.slug} post={post} receivers={receivers} />
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 898 KiB |
56
themes/publication/components/AboutCard.tsx
Normal file
56
themes/publication/components/AboutCard.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
'use client'
|
||||
|
||||
import Image from 'next/image'
|
||||
import { useAccount } from 'wagmi'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import useSession from '@/lib/useSession'
|
||||
import { useConnectModal } from '@rainbow-me/rainbowkit'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
}
|
||||
|
||||
export const AboutCard = ({ site }: Props) => {
|
||||
const { openConnectModal } = useConnectModal()
|
||||
const { isConnected } = useAccount()
|
||||
const { data } = useSession()
|
||||
return (
|
||||
<div className="mb-10 hover:text-foreground text-foreground/80">
|
||||
<div className="flex flex-col flex-shrink-0">
|
||||
{site.logo && (
|
||||
<Image
|
||||
src={site.logo}
|
||||
alt="avatar"
|
||||
width={192}
|
||||
height={192}
|
||||
className="h-20 w-20 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<h3 className="pb-2 pt-4 text-2xl font-bold leading-8 tracking-tight">
|
||||
{site.name}
|
||||
</h3>
|
||||
<div className="text-foreground/60">{site.description}</div>
|
||||
</div>
|
||||
<div className="prose max-w-none dark:prose-invert xl:col-span-2">
|
||||
<ContentRender content={site.about} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="w-full rounded-xl"
|
||||
onClick={() => {
|
||||
if (!isConnected) {
|
||||
openConnectModal?.()
|
||||
}
|
||||
if (data) {
|
||||
location.href = `/membership`
|
||||
}
|
||||
}}
|
||||
>
|
||||
Become a member
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
87
themes/publication/components/AuthorAvatar.tsx
Normal file
87
themes/publication/components/AuthorAvatar.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
|
||||
import { Post } from '@/lib/theme.types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props {
|
||||
post: Post
|
||||
className?: string
|
||||
}
|
||||
|
||||
export const AuthorAvatar = ({ post, className }: Props) => {
|
||||
if (post.user.image) {
|
||||
return (
|
||||
<Avatar className={cn('h-6 w-6', className)}>
|
||||
<AvatarImage src={post.user.image || ''} />
|
||||
<AvatarFallback>{post.user.displayName}</AvatarFallback>
|
||||
</Avatar>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
'bg-red-300 h-6 w-6 rounded-full flex-shrink-0',
|
||||
generateGradient(post.user.displayName || post.user.name),
|
||||
className,
|
||||
)}
|
||||
></div>
|
||||
)
|
||||
}
|
||||
|
||||
function hashCode(str: string) {
|
||||
let hash = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
const char = str.charCodeAt(i)
|
||||
hash = (hash << 5) - hash + char
|
||||
hash = hash & hash // Convert to 32bit integer
|
||||
}
|
||||
return hash
|
||||
}
|
||||
|
||||
function getFromColor(i: number) {
|
||||
const colors = [
|
||||
'from-red-500',
|
||||
'from-yellow-500',
|
||||
'from-green-500',
|
||||
'from-blue-500',
|
||||
'from-indigo-500',
|
||||
'from-purple-500',
|
||||
'from-pink-500',
|
||||
'from-red-600',
|
||||
'from-yellow-600',
|
||||
'from-green-600',
|
||||
'from-blue-600',
|
||||
'from-indigo-600',
|
||||
'from-purple-600',
|
||||
'from-pink-600',
|
||||
]
|
||||
return colors[Math.abs(i) % colors.length]
|
||||
}
|
||||
|
||||
function getToColor(i: number) {
|
||||
const colors = [
|
||||
'to-red-500',
|
||||
'to-yellow-500',
|
||||
'to-green-500',
|
||||
'to-blue-500',
|
||||
'to-indigo-500',
|
||||
'to-purple-500',
|
||||
'to-pink-500',
|
||||
'to-red-600',
|
||||
'to-yellow-600',
|
||||
'to-green-600',
|
||||
'to-blue-600',
|
||||
'to-indigo-600',
|
||||
'to-purple-600',
|
||||
'to-pink-600',
|
||||
]
|
||||
return colors[Math.abs(i) % colors.length]
|
||||
}
|
||||
|
||||
function generateGradient(walletAddress: string) {
|
||||
if (!walletAddress) return `bg-gradient-to-r to-pink-500 to-purple-500`
|
||||
const hash = hashCode(walletAddress)
|
||||
const from = getFromColor(hash)
|
||||
const to = getToColor(hash >> 8)
|
||||
return `bg-gradient-to-r ${from} ${to}`
|
||||
}
|
||||
18
themes/publication/components/ClientOnly.tsx
Normal file
18
themes/publication/components/ClientOnly.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
'use client'
|
||||
|
||||
import React, { PropsWithChildren, useEffect, useState } from 'react'
|
||||
|
||||
export function ClientOnly({ children }: PropsWithChildren) {
|
||||
// State / Props
|
||||
const [hasMounted, setHasMounted] = useState(false)
|
||||
|
||||
// Hooks
|
||||
useEffect(() => {
|
||||
setHasMounted(true)
|
||||
}, [])
|
||||
|
||||
// Render
|
||||
if (!hasMounted) return null
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
34
themes/publication/components/FeaturedPost.tsx
Normal file
34
themes/publication/components/FeaturedPost.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { PostActions } from '@/components/theme-ui/PostActions'
|
||||
import { Post } from '@/lib/theme.types'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
|
||||
interface Props {
|
||||
post: Post
|
||||
}
|
||||
|
||||
export default function FeaturedPost({ post }: Props) {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3 mt-2">
|
||||
<Link href={`/posts/${post.slug}`}>
|
||||
<Image
|
||||
src={post.image || ''}
|
||||
className="w-full h-full transition-all hover:scale-105"
|
||||
width={1000}
|
||||
height={1000}
|
||||
alt=""
|
||||
/>
|
||||
</Link>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<h2 className="font-bold text-2xl hover:scale-105 transition-all origin-left">
|
||||
<Link href={`/posts/${post.slug}`}>{post.title}</Link>
|
||||
</h2>
|
||||
<time className="text-sm text-foreground/50">
|
||||
{formatDate(post.updatedAt)}
|
||||
</time>
|
||||
</div>
|
||||
<PostActions post={post} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
69
themes/publication/components/Header.tsx
Normal file
69
themes/publication/components/Header.tsx
Normal file
@@ -0,0 +1,69 @@
|
||||
import { Profile } from '@/components/Profile/Profile'
|
||||
import { Airdrop } from '@/components/theme-ui/Airdrop'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { Merienda } from 'next/font/google'
|
||||
import Link from './Link'
|
||||
import { Nav } from './Nav'
|
||||
|
||||
const merienda = Merienda({
|
||||
weight: ['400', '500', '600', '700'],
|
||||
subsets: ['latin'],
|
||||
display: 'swap',
|
||||
})
|
||||
|
||||
const headerNavLinksRight = [{ href: '/creator-fi', title: 'CreatorFi' }]
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
}
|
||||
|
||||
export const Header = ({ site }: Props) => {
|
||||
return (
|
||||
<header className="z-40">
|
||||
<div
|
||||
className={cn(
|
||||
'flex justify-center items-center w-full py-4 h-16 px-0 sm:px-4',
|
||||
)}
|
||||
>
|
||||
<div className="flex-1 hidden md:block"></div>
|
||||
<div className="flex-1 flex items-center justify-start md:justify-center">
|
||||
<Link href="/" aria-label={site.name}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div
|
||||
className={cn('h-6 text-2xl font-semibold', merienda.className)}
|
||||
>
|
||||
{site.name}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end flex-1 gap-4">
|
||||
<div className="no-scrollbar hidden items-center space-x-4 overflow-x-auto sm:flex sm:space-x-6">
|
||||
{headerNavLinksRight.map((link) => {
|
||||
if (link.href === '/creator-fi' && !site.spaceId) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<Link
|
||||
key={link.title}
|
||||
href={link.href}
|
||||
className="font-medium hover:text-brand-500 text-foreground/90"
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center">
|
||||
<Airdrop />
|
||||
</div>
|
||||
<Profile></Profile>
|
||||
</div>
|
||||
</div>
|
||||
<Nav site={site} />
|
||||
</header>
|
||||
)
|
||||
}
|
||||
9
themes/publication/components/Image.tsx
Normal file
9
themes/publication/components/Image.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import NextImage, { ImageProps } from 'next/image'
|
||||
|
||||
const basePath = process.env.BASE_PATH
|
||||
|
||||
const Image = ({ src, ...rest }: ImageProps) => (
|
||||
<NextImage src={`${basePath || ''}${src}`} {...rest} />
|
||||
)
|
||||
|
||||
export default Image
|
||||
34
themes/publication/components/Link.tsx
Normal file
34
themes/publication/components/Link.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { AnchorHTMLAttributes } from 'react'
|
||||
import Link from 'next/link'
|
||||
import type { LinkProps } from 'next/link'
|
||||
|
||||
const CustomLink = ({
|
||||
href,
|
||||
...rest
|
||||
}: LinkProps & AnchorHTMLAttributes<HTMLAnchorElement>) => {
|
||||
const isInternalLink = href && href.startsWith('/')
|
||||
const isAnchorLink = href && href.startsWith('#')
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production'
|
||||
|
||||
if (isInternalLink) {
|
||||
// if (isProd) href = href + '.html'
|
||||
return <Link className="break-words" href={href} {...rest} />
|
||||
}
|
||||
|
||||
if (isAnchorLink) {
|
||||
return <a className="break-words" href={href} {...rest} />
|
||||
}
|
||||
|
||||
return (
|
||||
<a
|
||||
className="break-words"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={href}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomLink
|
||||
65
themes/publication/components/MostPopular.tsx
Normal file
65
themes/publication/components/MostPopular.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
import { format } from 'date-fns'
|
||||
import Image from 'next/image'
|
||||
import { getUserName } from '../lib/getUserName'
|
||||
import { AuthorAvatar } from './AuthorAvatar'
|
||||
import Link from './Link'
|
||||
|
||||
interface Props {
|
||||
posts: Post[]
|
||||
}
|
||||
|
||||
export const MostPopular = ({ posts }: Props) => {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="font-medium text-xl">Most Popular</div>
|
||||
<div className="grid gap-5">
|
||||
{posts.slice(0, 5).map((post) => (
|
||||
<div
|
||||
key={post.id}
|
||||
className="flex items-center justify-between gap-2"
|
||||
>
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link
|
||||
href={`/posts/${post.slug}`}
|
||||
key={post.slug}
|
||||
className="text-base leading-tight font-bold text-foreground/80 hover:text-foreground"
|
||||
>
|
||||
{post.title}
|
||||
</Link>
|
||||
<div className="flex items-center text-xs gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
<AuthorAvatar post={post} className="h-5 w-5" />
|
||||
<div className="font-medium">{getUserName(post.user)}</div>
|
||||
</div>
|
||||
<time className="text-xs text-foreground/50">
|
||||
{/* {format(new Date(post.updatedAt), 'MM/dd')} */}
|
||||
{formatDate(post.updatedAt)}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{post.image && (
|
||||
<div className="max-w-[80px] justify-between">
|
||||
<Link href={`/posts/${post.slug}`}>
|
||||
<Image
|
||||
src={post.image || ''}
|
||||
className="w-full h-auto rounded"
|
||||
style={{
|
||||
aspectRatio: '1.5/1',
|
||||
}}
|
||||
width={400}
|
||||
height={400}
|
||||
alt=""
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
40
themes/publication/components/Nav.tsx
Normal file
40
themes/publication/components/Nav.tsx
Normal file
@@ -0,0 +1,40 @@
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import Link from './Link'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
}
|
||||
|
||||
export const Nav = ({ site }: Props) => {
|
||||
return (
|
||||
<div className="flex justify-center items-center space-x-4 overflow-x-auto sm:flex sm:space-x-6 border-t border-b h-12 border-foreground/5">
|
||||
{site.navLinks.map((link) => {
|
||||
if (!link.visible) return null
|
||||
return (
|
||||
<Link
|
||||
key={link.title}
|
||||
href={link.pathname}
|
||||
className={cn(
|
||||
'font-medium hover:text-brand-500 dark:hover:text-brand-400 text-foreground/90',
|
||||
)}
|
||||
>
|
||||
{link.title}
|
||||
</Link>
|
||||
)
|
||||
})}
|
||||
|
||||
{site.spaceId && (
|
||||
<Link
|
||||
href="/membership"
|
||||
className={cn(
|
||||
'font-medium hover:text-brand-500 text-foreground/90',
|
||||
'border border-brand-500 text-brand-500 rounded-full px-2 py-1 hover:bg-brand-500 hover:text-background text-sm',
|
||||
)}
|
||||
>
|
||||
Membership
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
114
themes/publication/components/PostItem.tsx
Normal file
114
themes/publication/components/PostItem.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
'use client'
|
||||
|
||||
import Image from 'next/image'
|
||||
import Link from 'next/link'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import { Node } from 'slate'
|
||||
import { PlateEditor } from '@/components/editor/plate-editor'
|
||||
import { PostActions } from '@/components/theme-ui/PostActions'
|
||||
import { Post, PostType, User } from '@/lib/theme.types'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
import { getUserName } from '../lib/getUserName'
|
||||
import { AuthorAvatar } from './AuthorAvatar'
|
||||
|
||||
interface PostItemProps {
|
||||
post: Post
|
||||
receivers?: string[]
|
||||
className?: string
|
||||
ContentRender?: (props: { content: any[]; className?: string }) => JSX.Element
|
||||
}
|
||||
|
||||
export function PostItem({ post, receivers = [], className }: PostItemProps) {
|
||||
const { slug, title } = post
|
||||
const name = getUserName(post.user)
|
||||
const params = useSearchParams()!
|
||||
const type = params.get('type')
|
||||
|
||||
// console.log('========post:', post)
|
||||
|
||||
if (type === 'photos' && post.type !== PostType.IMAGE) return null
|
||||
if (type === 'notes' && post.type !== PostType.NOTE) return null
|
||||
if (type === 'articles' && post.type !== PostType.ARTICLE) return null
|
||||
|
||||
const getTitle = () => {
|
||||
if (post.type === PostType.IMAGE) return <div className="">{title}</div>
|
||||
if (post.type === PostType.NOTE) return <div className="">a note</div>
|
||||
if (post.type === PostType.ARTICLE) {
|
||||
return <div className="">an article</div>
|
||||
}
|
||||
return <div></div>
|
||||
}
|
||||
|
||||
const getContent = () => {
|
||||
if (post.type === PostType.IMAGE) {
|
||||
return (
|
||||
<img src={post.content} alt="" className="w-full h-auto rounded-lg" />
|
||||
)
|
||||
}
|
||||
|
||||
if (post.type === PostType.NOTE) {
|
||||
return (
|
||||
<div className="text-foreground/80">
|
||||
<PlateEditor
|
||||
value={JSON.parse(post.content)}
|
||||
readonly
|
||||
className="px-0 py-0"
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
const nodes: any[] =
|
||||
typeof post.content === 'string' ? JSON.parse(post.content) : post.content
|
||||
const str = nodes.map((node) => Node.string(node)).join('') || ''
|
||||
|
||||
return (
|
||||
<Link href={`/posts/${slug}`} className="space-y-2">
|
||||
<h2 className="text-2xl font-bold hover:scale-105 transition-all origin-left block">
|
||||
{post.title}
|
||||
</h2>
|
||||
<p className="text-foreground/70 hover:text-foreground transition-all hover:scale-105 line-clamp-2">
|
||||
{post.description || str?.slice(0, 200)}
|
||||
</p>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('flex justify-between items-center gap-10', className)}>
|
||||
<div className="flex flex-col gap-3 py-8 flex-1">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1 text-sm">
|
||||
<AuthorAvatar post={post} />
|
||||
<div className="font-medium">{name}</div>
|
||||
<div className="text-foreground/50 text-sm">posted</div>
|
||||
{getTitle()}
|
||||
</div>
|
||||
<time className="text-xs text-foreground/50">
|
||||
{formatDate(post.updatedAt)}
|
||||
</time>
|
||||
</div>
|
||||
|
||||
{getContent()}
|
||||
<PostActions post={post} receivers={receivers} />
|
||||
</div>
|
||||
|
||||
{post.image && (
|
||||
<div className="max-w-[160px]">
|
||||
<Link href={`/posts/${post.slug}`}>
|
||||
<Image
|
||||
src={post.image || ''}
|
||||
className="w-full h-auto rounded"
|
||||
style={{
|
||||
aspectRatio: '1.5/1',
|
||||
}}
|
||||
width={400}
|
||||
height={400}
|
||||
alt=""
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Pagination } from '@/components/theme-ui/Pagination'
|
||||
import { Post } from '@/lib/theme.types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { PostItem } from './PostItem'
|
||||
|
||||
interface PaginationProps {
|
||||
@@ -22,8 +23,8 @@ export function PostList({
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<div className="grid grid-cols-1 gap-3">
|
||||
{displayPosts.map((post) => {
|
||||
<div className="grid gap-2">
|
||||
{displayPosts.map((post, index) => {
|
||||
return <PostItem key={post.slug} post={post} />
|
||||
})}
|
||||
</div>
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Post, Tag } from '@/lib/theme.types'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { Post, Tag } from '@/lib/theme.types'
|
||||
import { PostList } from './PostList'
|
||||
import { TagList } from './TagList'
|
||||
|
||||
@@ -24,8 +24,8 @@ export function PostListWithTag({
|
||||
initialDisplayPosts.length > 0 ? initialDisplayPosts : posts
|
||||
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<PageTitle>Tags</PageTitle>
|
||||
<div className="flex flex-col mx-auto max-w-2xl">
|
||||
<PageTitle className="text-center">Tags</PageTitle>
|
||||
<TagList tags={tags} />
|
||||
<div className="mt-10">
|
||||
<PostList posts={displayPosts} />
|
||||
20
themes/publication/components/SectionContainer.tsx
Normal file
20
themes/publication/components/SectionContainer.tsx
Normal file
@@ -0,0 +1,20 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props {
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export default function SectionContainer({ children, className }: Props) {
|
||||
return (
|
||||
<section
|
||||
className={cn(
|
||||
'mx-auto px-4 sm:px-6 xl:px-0 w-full flex flex-col h-screen',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{children}
|
||||
</section>
|
||||
)
|
||||
}
|
||||
17
themes/publication/components/Sidebar.tsx
Normal file
17
themes/publication/components/Sidebar.tsx
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import { AboutCard } from './AboutCard'
|
||||
import { MostPopular } from './MostPopular'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
posts: Post[]
|
||||
}
|
||||
|
||||
export const Sidebar = ({ site, posts }: Props) => {
|
||||
return (
|
||||
<div className="sm:w-[340px] w-full flex-shrink-0 space-y-20">
|
||||
<MostPopular posts={posts} />
|
||||
<AboutCard site={site} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +1,23 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
import { slug } from 'github-slugger'
|
||||
import Link from 'next/link'
|
||||
import { PostTag } from '@/lib/theme.types'
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
interface Props {
|
||||
text: string
|
||||
postTag: PostTag
|
||||
className?: string
|
||||
}
|
||||
|
||||
const Tag = ({ text, className }: Props) => {
|
||||
const Tag = ({ postTag, className }: Props) => {
|
||||
return (
|
||||
<Link
|
||||
href={`/tags/${slug(text)}`}
|
||||
href={`/tags/${slug(postTag.tag.name)}`}
|
||||
className={cn(
|
||||
'mr-3 text-base font-medium text-brand-500 hover:text-brand-600 dark:hover:text-brand-400',
|
||||
className,
|
||||
)}
|
||||
>
|
||||
{text.split(' ').join('-')}
|
||||
{postTag.tag.name.split(' ').join('-')}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
36
themes/publication/components/TagList.tsx
Normal file
36
themes/publication/components/TagList.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
'use client'
|
||||
|
||||
import { Tag } from '@/lib/theme.types'
|
||||
import { slug } from 'github-slugger'
|
||||
import { usePathname } from 'next/navigation'
|
||||
import Link from './Link'
|
||||
|
||||
interface PostListWithTagProps {
|
||||
tags: Tag[]
|
||||
}
|
||||
|
||||
export function TagList({ tags = [] }: PostListWithTagProps) {
|
||||
const pathname = usePathname()!
|
||||
|
||||
return (
|
||||
<ul className="flex flex-wrap gap-x-5 w-full justify-center">
|
||||
{tags.map((t) => {
|
||||
return (
|
||||
<li key={t.id} className="my-3">
|
||||
{decodeURI(pathname.split('/tags/')[1]) === slug(t.name) ? (
|
||||
<h3 className="inline py-2 text-brand-500">#{`${t.name}`}</h3>
|
||||
) : (
|
||||
<Link
|
||||
href={`/tags/${slug(t.name)}`}
|
||||
className="py-2 text-foreground/60 hover:text-brand-500 dark:hover:text-brand-500 rounded-full"
|
||||
aria-label={`View posts tagged ${t.name}`}
|
||||
>
|
||||
#{`${t.name}`}
|
||||
</Link>
|
||||
)}
|
||||
</li>
|
||||
)
|
||||
})}
|
||||
</ul>
|
||||
)
|
||||
}
|
||||
@@ -5,3 +5,4 @@ export * from './pages/HomePage'
|
||||
export * from './pages/TagDetailPage'
|
||||
export * from './pages/TagListPage'
|
||||
export * from './pages/BlogPage'
|
||||
export * from './pages/PageDetail'
|
||||
@@ -1,16 +1,10 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { Footer } from '@/components/theme-ui/Footer'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import { Header } from '../components/Header'
|
||||
import SectionContainer from '../components/SectionContainer'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
Logo: () => ReactNode
|
||||
ModeToggle: () => ReactNode
|
||||
MobileNav: () => ReactNode
|
||||
ConnectButton: () => ReactNode
|
||||
Airdrop: () => ReactNode
|
||||
site: any
|
||||
children: ReactNode
|
||||
}
|
||||
|
||||
@@ -18,7 +12,9 @@ export function SiteLayout({ children, site }: Props) {
|
||||
return (
|
||||
<SectionContainer>
|
||||
<Header site={site} />
|
||||
<main className="mb-auto">{children}</main>
|
||||
<main className="mb-auto mx-auto px-4 md:px-6 w-full lg:max-w-6xl xl:px-0 flex flex-col">
|
||||
{children}
|
||||
</main>
|
||||
<Footer site={site} />
|
||||
</SectionContainer>
|
||||
)
|
||||
18
themes/publication/lib/getUserName.ts
Normal file
18
themes/publication/lib/getUserName.ts
Normal file
@@ -0,0 +1,18 @@
|
||||
import { isAddress } from '@/lib/utils'
|
||||
import { User } from '@/lib/theme.types'
|
||||
|
||||
export function getUserName(user: User) {
|
||||
const { displayName = '', name } = user
|
||||
|
||||
if (displayName) {
|
||||
if (isAddress(displayName)) {
|
||||
return displayName.slice(0, 3) + '...' + displayName.slice(-4)
|
||||
}
|
||||
return user.displayName || user.name
|
||||
}
|
||||
|
||||
if (isAddress(name)) {
|
||||
return name.slice(0, 3) + '...' + name.slice(-4)
|
||||
}
|
||||
return user.displayName || user.name
|
||||
}
|
||||
7
themes/publication/manifest.json
Normal file
7
themes/publication/manifest.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "publication",
|
||||
"title": "Publication",
|
||||
"author": "0xZio",
|
||||
"description": "",
|
||||
"previewUrl": "https://demo4.penx.io"
|
||||
}
|
||||
44
themes/publication/pages/AboutPage.tsx
Normal file
44
themes/publication/pages/AboutPage.tsx
Normal file
@@ -0,0 +1,44 @@
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { Site } from '@/lib/theme.types'
|
||||
import Image from '../components/Image'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
}
|
||||
|
||||
export function AboutPage({ site }: Props) {
|
||||
return (
|
||||
<>
|
||||
<div className="">
|
||||
<PageTitle className="text-center">About</PageTitle>
|
||||
<div className="">
|
||||
<div className="flex flex-col items-center space-x-2 pt-8">
|
||||
{site.logo && (
|
||||
<Image
|
||||
src={site.logo}
|
||||
alt="avatar"
|
||||
width={192}
|
||||
height={192}
|
||||
className="h-48 w-48 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<h3 className="pb-2 pt-4 text-2xl font-bold leading-8 tracking-tight">
|
||||
{site.name}
|
||||
</h3>
|
||||
<div className="text-foreground/60">{site.description}</div>
|
||||
{/* <div className="flex space-x-3 pt-6">
|
||||
<SocialIcon kind="mail" href={`mailto:${email}`} />
|
||||
<SocialIcon kind="github" href={github} />
|
||||
<SocialIcon kind="linkedin" href={linkedin} />
|
||||
<SocialIcon kind="x" href={twitter} />
|
||||
</div> */}
|
||||
</div>
|
||||
<div className="prose max-w-none pb-8 pt-8 dark:prose-invert xl:col-span-2 mx-auto lg:max-w-3xl">
|
||||
<ContentRender content={site.about} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -16,10 +16,9 @@ export function BlogPage({
|
||||
pagination,
|
||||
initialDisplayPosts,
|
||||
}: Props) {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PageTitle>Blog</PageTitle>
|
||||
<div className="space-y-6 mx-auto max-w-2xl">
|
||||
<PageTitle className="text-center">Blog</PageTitle>
|
||||
<PostList
|
||||
posts={posts}
|
||||
pagination={pagination}
|
||||
63
themes/publication/pages/HomePage.tsx
Normal file
63
themes/publication/pages/HomePage.tsx
Normal file
@@ -0,0 +1,63 @@
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import { cn } from '@/lib/utils'
|
||||
import FeaturedPost from '../components/FeaturedPost'
|
||||
import Link from '../components/Link'
|
||||
import { PostItem } from '../components/PostItem'
|
||||
import { Sidebar } from '../components/Sidebar'
|
||||
|
||||
interface Props {
|
||||
site: Site
|
||||
posts: Post[]
|
||||
}
|
||||
|
||||
export function HomePage({ posts = [], site }: Props) {
|
||||
const { popularPosts, featuredPost, commonPosts } = extractPosts(posts)
|
||||
|
||||
const displayedPosts = commonPosts.slice(0, 100)
|
||||
return (
|
||||
<div className="mt-12 flex flex-col gap-20 md:flex-row">
|
||||
<div className="flex-1">
|
||||
{featuredPost && <FeaturedPost post={featuredPost} />}
|
||||
|
||||
<div className="grid gap-2">
|
||||
{displayedPosts.map((post, index) => {
|
||||
return (
|
||||
<PostItem
|
||||
key={post.slug}
|
||||
post={post}
|
||||
className={cn(
|
||||
displayedPosts.length - 1 !== index &&
|
||||
'border-b border-foreground/10',
|
||||
)}
|
||||
/>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<Link
|
||||
href="/posts"
|
||||
className="text-brand-500 hover:text-brand-600 dark:hover:text-brand-400"
|
||||
>
|
||||
<Button variant="secondary">All posts →</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Sidebar site={site} posts={popularPosts}></Sidebar>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function extractPosts(posts: Post[]) {
|
||||
const popularPosts = posts.filter((post) => post.isPopular)
|
||||
const featuredPost = posts.find((post) => post.featured) || posts[0]
|
||||
const ids = popularPosts.map((post) => post.id)
|
||||
if (featuredPost) ids.push(featuredPost.id)
|
||||
const commonPosts = posts.filter((post) => !ids.includes(post.id))
|
||||
return {
|
||||
popularPosts,
|
||||
featuredPost,
|
||||
commonPosts,
|
||||
}
|
||||
}
|
||||
19
themes/publication/pages/PageDetail.tsx
Normal file
19
themes/publication/pages/PageDetail.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
|
||||
interface LayoutProps {
|
||||
page: any
|
||||
content: any
|
||||
children: ReactNode
|
||||
className?: string
|
||||
}
|
||||
|
||||
export function PageDetail({ content, className }: LayoutProps) {
|
||||
return (
|
||||
<article className="mt-10 sm:mt-20 mx-auto w-full lg:max-w-3xl">
|
||||
<div className="prose max-w-none pb-8 dark:prose-invert">
|
||||
<ContentRender content={content} />
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +1,34 @@
|
||||
import { ReactNode } from 'react'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import { ContentRender } from '@/components/theme-ui/ContentRender'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { PostActions } from '@/components/theme-ui/PostActions'
|
||||
import { cn, formatDate } from '@/lib/utils'
|
||||
import { Post } from '@/lib/theme.types'
|
||||
import { ExternalLink } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
import { PostSubtitle } from '@/components/theme-ui/PostSubtitle'
|
||||
import { SubscribeNewsletterCard } from '@/components/theme-ui/SubscribeNewsletter/SubscribeNewsletterCard'
|
||||
import { Post, Site } from '@/lib/theme.types'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
import Image from '../components/Image'
|
||||
import Link from '../components/Link'
|
||||
|
||||
interface LayoutProps {
|
||||
site: Site
|
||||
post: Post
|
||||
children: ReactNode
|
||||
className?: string
|
||||
next?: { path: string; title: string }
|
||||
prev?: { path: string; title: string }
|
||||
next?: Post
|
||||
prev?: Post
|
||||
}
|
||||
|
||||
export function PostDetail({ post, next, prev, className }: LayoutProps) {
|
||||
export function PostDetail({ site, post, className, next, prev }: LayoutProps) {
|
||||
return (
|
||||
<div className={cn(className)}>
|
||||
<article className="mt-20 mx-auto w-full lg:max-w-3xl">
|
||||
<header className="space-y-4 pb-4">
|
||||
<PageTitle className="mb-0">{post.title}</PageTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="mb-4">
|
||||
<PageTitle className="mb-2">{post.title}</PageTitle>
|
||||
{post.description && <PostSubtitle>{post.description}</PostSubtitle>}
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<dl className="flex items-center gap-2 text-foreground/50">
|
||||
<dt className="sr-only">Published on</dt>
|
||||
<dd className="text-base font-medium leading-6">
|
||||
<time>{formatDate(post.updatedAt)}</time>
|
||||
</dd>
|
||||
@@ -35,10 +41,23 @@ export function PostDetail({ post, next, prev, className }: LayoutProps) {
|
||||
|
||||
<PostActions post={post} />
|
||||
</header>
|
||||
|
||||
{!!post.image && (
|
||||
<Image
|
||||
src={post.image || ''}
|
||||
alt=""
|
||||
width={1000}
|
||||
height={800}
|
||||
className="object-cover w-full max-h-96 rounded-2xl"
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="grid-rows-[auto_1fr]">
|
||||
<div className="prose max-w-none pb-8 dark:prose-invert">
|
||||
<ContentRender content={post.content} />
|
||||
<SubscribeNewsletterCard site={site} />
|
||||
</div>
|
||||
|
||||
{post.cid && (
|
||||
<div className="text-foreground/60 text-xs rounded-md py-2 md:flex items-center gap-2 hidden">
|
||||
<span className="text-foreground/80">IPFS CID:</span>
|
||||
@@ -55,10 +74,10 @@ export function PostDetail({ post, next, prev, className }: LayoutProps) {
|
||||
|
||||
<footer>
|
||||
<div className="flex flex-col text-sm font-medium sm:flex-row sm:justify-between sm:text-base">
|
||||
{prev && prev.path && (
|
||||
{prev && prev?.slug && (
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href={`/${prev.path}`}
|
||||
href={`/posts/${prev.slug}`}
|
||||
className="text-brand-500 hover:text-brand-600 dark:hover:text-brand-400"
|
||||
aria-label={`Previous post: ${prev.title}`}
|
||||
>
|
||||
@@ -66,10 +85,10 @@ export function PostDetail({ post, next, prev, className }: LayoutProps) {
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{next && next.path && (
|
||||
{next && next?.slug && (
|
||||
<div className="pt-4 xl:pt-8">
|
||||
<Link
|
||||
href={`/${next.path}`}
|
||||
href={`/posts/${next.slug}`}
|
||||
className="text-brand-500 hover:text-brand-600 dark:hover:text-brand-400"
|
||||
aria-label={`Next post: ${next.title}`}
|
||||
>
|
||||
@@ -80,6 +99,6 @@ export function PostDetail({ post, next, prev, className }: LayoutProps) {
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { Tag } from '@/lib/theme.types'
|
||||
import { PageTitle } from '@/components/theme-ui/PageTitle'
|
||||
import { TagList } from '../components/TagList'
|
||||
|
||||
interface Props {
|
||||
@@ -9,7 +9,7 @@ interface Props {
|
||||
export function TagListPage({ tags }: Props) {
|
||||
return (
|
||||
<div className="flex flex-col">
|
||||
<PageTitle>Tags</PageTitle>
|
||||
<PageTitle className="text-center">Tags</PageTitle>
|
||||
<div className="grid gap-y-3">
|
||||
{tags.length === 0 && 'No tags found.'}
|
||||
{tags.length > 0 && <TagList tags={tags} />}
|
||||
Reference in New Issue
Block a user