feat: improve theme

This commit is contained in:
0xzio
2025-02-17 23:38:17 +08:00
parent 6ac861810c
commit 8f22792be1
92 changed files with 1573 additions and 574 deletions

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

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

View 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

View 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

View File

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

View File

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

View File

@@ -5,3 +5,4 @@ export * from './pages/HomePage'
export * from './pages/TagDetailPage'
export * from './pages/TagListPage'
export * from './pages/BlogPage'
export * from './pages/PageDetail'

View File

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

View File

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

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

View File

@@ -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}`}
>
&larr; {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} &rarr;
</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}`}
>
&larr; {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} &rarr;
</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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

@@ -5,3 +5,4 @@ export * from './pages/HomePage'
export * from './pages/TagDetailPage'
export * from './pages/TagListPage'
export * from './pages/BlogPage'
export * from './pages/PageDetail'

View File

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

View File

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

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

View File

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

View File

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

@@ -5,3 +5,4 @@ export * from './pages/HomePage'
export * from './pages/TagDetailPage'
export * from './pages/TagListPage'
export * from './pages/BlogPage'
export * from './pages/PageDetail'

View File

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

View File

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

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

View File

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

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

View File

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

View 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

View 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

View File

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

View File

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

View File

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

View File

@@ -5,3 +5,4 @@ export * from './pages/HomePage'
export * from './pages/TagDetailPage'
export * from './pages/TagListPage'
export * from './pages/BlogPage'
export * from './pages/PageDetail'

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
{
"name": "photo",
"title": "Photo",
"author": "0xZio",
"description": "",
"previewUrl": "https://demo3.penx.io"
}

View File

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

View File

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

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

View 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}`
}

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

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

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

View 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

View 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

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

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

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

View File

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

View File

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

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

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

View File

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

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

View File

@@ -5,3 +5,4 @@ export * from './pages/HomePage'
export * from './pages/TagDetailPage'
export * from './pages/TagListPage'
export * from './pages/BlogPage'
export * from './pages/PageDetail'

View File

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

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

View File

@@ -0,0 +1,7 @@
{
"name": "publication",
"title": "Publication",
"author": "0xZio",
"description": "",
"previewUrl": "https://demo4.penx.io"
}

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

View File

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

View 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 &rarr;</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,
}
}

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

View File

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

View File

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