) {
89 | return (
90 |
94 | )
95 | }
96 |
--------------------------------------------------------------------------------
/themes/card/components/social-icons/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Facebook,
3 | Github,
4 | Instagram,
5 | Linkedin,
6 | Mail,
7 | Mastodon,
8 | Threads,
9 | Twitter,
10 | X,
11 | Youtube,
12 | } from './icons'
13 |
14 | const components = {
15 | mail: Mail,
16 | github: Github,
17 | facebook: Facebook,
18 | youtube: Youtube,
19 | linkedin: Linkedin,
20 | twitter: Twitter,
21 | x: X,
22 | mastodon: Mastodon,
23 | threads: Threads,
24 | instagram: Instagram,
25 | }
26 |
27 | type SocialIconProps = {
28 | kind: keyof typeof components
29 | href: string | undefined
30 | size?: number
31 | }
32 |
33 | const SocialIcon = ({ kind, href, size = 8 }: SocialIconProps) => {
34 | if (
35 | !href ||
36 | (kind === 'mail' &&
37 | !/^mailto:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(href))
38 | )
39 | return null
40 |
41 | const SocialSvg = components[kind]
42 |
43 | return (
44 |
50 | {kind}
51 |
54 |
55 | )
56 | }
57 |
58 | export default SocialIcon
59 |
--------------------------------------------------------------------------------
/themes/card/externals/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | import { Site } from '@penxio/types'
2 | import Image from '../components/Image'
3 | import PageTitle from '../components/PageTitle'
4 |
5 | interface Props {
6 | site: Site
7 | ContentRender: (props: { content: any[]; className?: string }) => JSX.Element
8 | }
9 |
10 | export function AboutPage({ site, ContentRender }: Props) {
11 | return (
12 | <>
13 |
14 |
About
15 |
16 |
17 | {site.logo && (
18 |
25 | )}
26 |
27 | {site.name}
28 |
29 |
{site.description}
30 | {/*
31 |
32 |
33 |
34 |
35 |
*/}
36 |
37 |
38 |
39 |
40 |
41 |
42 | >
43 | )
44 | }
45 |
--------------------------------------------------------------------------------
/themes/card/externals/BlogPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post } from '@penxio/types'
2 | import PageTitle from '../components/PageTitle'
3 | import { PostList } from '../components/PostList'
4 |
5 | interface Props {
6 | posts: Post[]
7 | initialDisplayPosts: Post[]
8 | pagination: {
9 | currentPage: number
10 | totalPages: number
11 | }
12 | }
13 |
14 | export function BlogPage({
15 | posts = [],
16 | pagination,
17 | initialDisplayPosts,
18 | }: Props) {
19 | return (
20 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/themes/card/externals/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Site } from '@penxio/types'
2 | import Image from '../components/Image'
3 | import Link from '../components/Link'
4 | import { PostItem } from '../components/PostItem'
5 |
6 | const POSTS_PER_PAGE = Number(process.env.NEXT_PUBLIC_POSTS_PER_PAGE || 10)
7 |
8 | interface Props {
9 | site: Site
10 | posts: Post[]
11 | ContentRender: (props: { content: any[]; className?: string }) => JSX.Element
12 | }
13 |
14 | export function HomePage({ posts = [], site, ContentRender }: Props) {
15 | return (
16 |
17 |
18 |
19 | {site.logo && (
20 |
27 | )}
28 |
29 | {site.name}
30 |
31 |
{site.description}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 | Latest
42 |
43 |
44 | {posts.length > POSTS_PER_PAGE && (
45 |
49 | All posts →
50 |
51 | )}
52 |
53 |
54 |
55 | {!posts.length && 'No posts found.'}
56 | {posts.slice(0, POSTS_PER_PAGE).map((post) => {
57 | return
58 | })}
59 |
60 |
61 |
62 | )
63 | }
64 |
--------------------------------------------------------------------------------
/themes/card/externals/PostDetail.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { formatDate } from '@/lib/utils'
3 | import { Post } from '@penxio/types'
4 | import { ExternalLink } from 'lucide-react'
5 | import Image from '../components/Image'
6 | import Link from '../components/Link'
7 | import PageTitle from '../components/PageTitle'
8 | import SectionContainer from '../components/SectionContainer'
9 |
10 | interface LayoutProps {
11 | post: Post
12 | children: ReactNode
13 | className?: string
14 | next?: Post
15 | prev?: Post
16 | PostActions?: (props: { post: Post; className?: string }) => JSX.Element
17 | ContentRender?: (props: { content: any; className?: string }) => JSX.Element
18 | }
19 |
20 | export function PostDetail({
21 | post,
22 | PostActions,
23 | className,
24 | ContentRender,
25 | next,
26 | prev,
27 | }: LayoutProps) {
28 | return (
29 |
30 |
31 |
32 | {post.title}
33 |
34 |
35 | -
36 |
37 |
38 | - ·
39 | -
40 | {post.readingTime.text}
41 |
42 |
43 |
44 |
45 | {PostActions && }
46 |
47 |
48 | {!!post.image && (
49 |
56 | )}
57 |
58 |
59 |
60 | {ContentRender && }
61 |
62 |
63 | {post.cid && (
64 |
65 |
IPFS CID:
66 |
{post.cid}
67 |
72 |
73 |
74 |
75 | )}
76 |
77 |
103 |
104 |
105 |
106 | )
107 | }
108 |
--------------------------------------------------------------------------------
/themes/card/externals/SiteLayout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { Footer } from '../components/Footer'
3 | import { Header } from '../components/Header'
4 | import SectionContainer from '../components/SectionContainer'
5 |
6 | interface Props {
7 | site: any
8 | Logo: () => ReactNode
9 | ModeToggle: () => ReactNode
10 | MobileNav: () => ReactNode
11 | ConnectButton: () => ReactNode
12 | Airdrop: () => ReactNode
13 | children: ReactNode
14 | }
15 |
16 | export function SiteLayout({
17 | children,
18 | site,
19 | Logo,
20 | ModeToggle,
21 | MobileNav,
22 | ConnectButton,
23 | Airdrop,
24 | }: Props) {
25 | return (
26 |
27 |
35 | {children}
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/themes/card/externals/TagDetailPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Tag } from '@penxio/types'
2 | import { PostListWithTag } from '../components/PostListWithTag'
3 |
4 | interface Props {
5 | posts: Post[]
6 | tags: Tag[]
7 | }
8 |
9 | export function TagDetailPage({ posts = [], tags = [] }: Props) {
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/themes/card/externals/TagListPage.tsx:
--------------------------------------------------------------------------------
1 | import { Tag } from '@penxio/types'
2 | import PageTitle from '../components/PageTitle'
3 | import { TagList } from '../components/TagList'
4 |
5 | interface Props {
6 | tags: Tag[]
7 | }
8 |
9 | export function TagListPage({ tags }: Props) {
10 | return (
11 |
12 |
Tags
13 |
14 | {tags.length === 0 && 'No tags found.'}
15 | {tags.length > 0 && }
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/themes/card/externals/index.ts:
--------------------------------------------------------------------------------
1 | export * from './SiteLayout'
2 | export * from './PostDetail'
3 | export * from './AboutPage'
4 | export * from './HomePage'
5 | export * from './TagDetailPage'
6 | export * from './TagListPage'
7 | export * from './BlogPage'
8 |
--------------------------------------------------------------------------------
/themes/card/files.json:
--------------------------------------------------------------------------------
1 | [
2 | "components/Header.tsx",
3 | "components/PostItem.tsx",
4 | "components/PostList.tsx",
5 | "components/PostListWithTag.tsx",
6 | "components/SectionContainer.tsx",
7 | "components/Tag.tsx",
8 | "components/TagList.tsx",
9 | "index.ts",
10 | "layouts/SiteLayout.tsx",
11 | "manifest.json",
12 | "pages/AboutPage.tsx",
13 | "pages/BlogPage.tsx",
14 | "pages/HomePage.tsx",
15 | "pages/PostDetail.tsx",
16 | "pages/TagDetailPage.tsx",
17 | "pages/TagListPage.tsx",
18 | "screenshots/screenshot-1.png"
19 | ]
--------------------------------------------------------------------------------
/themes/card/index.ts:
--------------------------------------------------------------------------------
1 | export * from './layouts/SiteLayout'
2 | export * from './pages/PostDetail'
3 | export * from './pages/AboutPage'
4 | export * from './pages/HomePage'
5 | export * from './pages/TagDetailPage'
6 | export * from './pages/TagListPage'
7 | export * from './pages/BlogPage'
8 |
--------------------------------------------------------------------------------
/themes/card/layouts/SiteLayout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { Footer } from '@/components/theme-ui/Footer'
3 | import { Header } from '../components/Header'
4 | import SectionContainer from '../components/SectionContainer'
5 |
6 | interface Props {
7 | site: any
8 | children: ReactNode
9 | }
10 |
11 | export function SiteLayout({ children, site }: Props) {
12 | return (
13 |
14 |
15 | {children}
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/themes/card/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Card",
3 | "author": "0xZio",
4 | "description": "",
5 | "previewUrl": "https://demo2.penx.io"
6 | }
7 |
--------------------------------------------------------------------------------
/themes/card/pages/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | import { ContentRender } from '@/components/plate-ui/ContentRender'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { Site } from '@penxio/types'
4 |
5 | interface Props {
6 | site: Site
7 | }
8 |
9 | export function AboutPage({ site }: Props) {
10 | return (
11 | <>
12 |
13 |
About
14 |
15 |
16 | {site.logo && (
17 |

22 | )}
23 |
24 | {site.name}
25 |
26 |
{site.description}
27 |
28 |
29 |
30 |
31 |
32 |
33 | >
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/themes/card/pages/BlogPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { PostList } from '../components/PostList'
4 |
5 | interface Props {
6 | posts: Post[]
7 | initialDisplayPosts: Post[]
8 | pagination: {
9 | currentPage: number
10 | totalPages: number
11 | }
12 | }
13 |
14 | export function BlogPage({
15 | posts = [],
16 | pagination,
17 | initialDisplayPosts,
18 | }: Props) {
19 | return (
20 |
28 | )
29 | }
30 |
--------------------------------------------------------------------------------
/themes/card/pages/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { ContentRender } from '@/components/plate-ui/ContentRender'
2 | import { Post, Site } from '@penxio/types'
3 | import Link from 'next/link'
4 | import { PostItem } from '../components/PostItem'
5 |
6 | const POSTS_PER_PAGE = Number(process.env.NEXT_PUBLIC_POSTS_PER_PAGE || 10)
7 |
8 | interface Props {
9 | site: Site
10 | posts: Post[]
11 | }
12 |
13 | export function HomePage({ posts = [], site }: Props) {
14 | return (
15 |
16 |
17 |
18 | {site.logo && (
19 |

24 | )}
25 |
26 | {site.name}
27 |
28 |
{site.description}
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 | Latest
39 |
40 |
41 | {posts.length > POSTS_PER_PAGE && (
42 |
46 | All posts →
47 |
48 | )}
49 |
50 |
51 |
52 | {!posts.length && 'No posts found.'}
53 | {posts.slice(0, POSTS_PER_PAGE).map((post) => {
54 | return
55 | })}
56 |
57 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/themes/card/pages/PostDetail.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { ContentRender } from '@/components/plate-ui/ContentRender'
3 | import { PageTitle } from '@/components/theme-ui/PageTitle'
4 | import { PostActions } from '@/components/theme-ui/PostActions'
5 | import { formatDate } from '@/lib/utils'
6 | import { Post } from '@penxio/types'
7 | import { ExternalLink } from 'lucide-react'
8 | import Link from 'next/link'
9 | import SectionContainer from '../components/SectionContainer'
10 |
11 | interface LayoutProps {
12 | post: Post
13 | children: ReactNode
14 | className?: string
15 | next?: Post
16 | prev?: Post
17 | }
18 |
19 | export function PostDetail({ post, className, next, prev }: LayoutProps) {
20 | return (
21 |
22 |
23 |
24 | {post.title}
25 |
26 |
27 | -
28 |
29 |
30 | - ·
31 | -
32 | {post.readingTime.text}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 | {!!post.image && (
41 |
46 | )}
47 |
48 |
49 |
50 |
51 |
52 |
53 | {post.cid && (
54 |
55 |
IPFS CID:
56 |
{post.cid}
57 |
62 |
63 |
64 |
65 | )}
66 |
67 |
93 |
94 |
95 |
96 | )
97 | }
98 |
--------------------------------------------------------------------------------
/themes/card/pages/TagDetailPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Tag } from '@penxio/types'
2 | import { PostListWithTag } from '../components/PostListWithTag'
3 |
4 | interface Props {
5 | posts: Post[]
6 | tags: Tag[]
7 | }
8 |
9 | export function TagDetailPage({ posts = [], tags = [] }: Props) {
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/themes/card/pages/TagListPage.tsx:
--------------------------------------------------------------------------------
1 | import { Tag } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { TagList } from '../components/TagList'
4 |
5 | interface Props {
6 | tags: Tag[]
7 | }
8 |
9 | export function TagListPage({ tags }: Props) {
10 | return (
11 |
12 |
Tags
13 |
14 | {tags.length === 0 && 'No tags found.'}
15 | {tags.length > 0 && }
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/themes/card/screenshots/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/penxio/themes/c24d73088e62169d8eeccd073e16f6a8495aa31d/themes/card/screenshots/screenshot-1.png
--------------------------------------------------------------------------------
/themes/garden/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Profile } from '@/components/Profile/Profile'
2 | import { Airdrop } from '@/components/theme-ui/Airdrop'
3 | import { cn } from '@/lib/utils'
4 | import { Site } from '@penxio/types'
5 | import { Lobster } from 'next/font/google'
6 | import Link from 'next/link'
7 | import { PostTypeNav } from './PostTypeNav'
8 |
9 | const lobster = Lobster({
10 | weight: ['400'],
11 | subsets: ['latin'],
12 | display: 'swap',
13 | })
14 |
15 | const headerNavLinks = [
16 | { href: '/', title: 'Home' },
17 | // { href: '/posts', title: 'Blog' },
18 | // { href: '/tags', title: 'Tags' },
19 | { href: '/about', title: 'About' },
20 | { href: '/creator-fi', title: 'CreatorFi' },
21 | { href: '/membership', title: 'Membership', isMembership: true },
22 | ]
23 |
24 | interface Props {
25 | site: Site
26 | }
27 |
28 | export const Header = ({ site }: Props) => {
29 | return (
30 |
31 |
32 |
33 |
34 | {headerNavLinks.map((link) => {
35 | if (link.href === '/creator-fi' && !site.spaceId) {
36 | return null
37 | }
38 |
39 | if (link.href === '/membership' && !site.spaceId) {
40 | return null
41 | }
42 | return (
43 |
52 | {link.title}
53 |
54 | )
55 | })}
56 |
57 | {/* {MobileNav &&
} */}
58 |
59 |
60 |
61 |
62 |
66 | {site.logo && (
67 |

68 | )}
69 |
75 | {site.name}
76 |
77 |
78 |
79 |
80 |
81 |
82 |
86 | About
87 |
88 |
89 |
92 |
93 |
94 |
95 |
96 |
97 | )
98 | }
99 |
--------------------------------------------------------------------------------
/themes/garden/components/PostItem.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
4 | import { isAddress } from '@/lib/utils'
5 | import { Post, PostType, User } from '@penxio/types'
6 | import { cn, formatDate } from '@/lib/utils'
7 | import Link from 'next/link'
8 | import { useSearchParams } from 'next/navigation'
9 | import { Node } from 'slate'
10 |
11 | interface PostItemProps {
12 | post: Post
13 | receivers?: string[]
14 | ContentRender?: (props: { content: any[]; className?: string }) => JSX.Element
15 | PostActions?: (props: {
16 | post: Post
17 | receivers?: string[]
18 | className?: string
19 | }) => JSX.Element
20 | }
21 |
22 | export function PostItem({ post, PostActions, receivers = [] }: PostItemProps) {
23 | const { slug, title } = post
24 |
25 | const name = getUserName(post.user)
26 |
27 | const params = useSearchParams()!
28 | const type = params.get('type')
29 |
30 | if (type === 'photos' && post.type !== PostType.IMAGE) return null
31 | if (type === 'notes' && post.type !== PostType.NOTE) return null
32 | if (type === 'articles' && post.type !== PostType.ARTICLE) return null
33 |
34 | const getTitle = () => {
35 | if (post.type === PostType.IMAGE) return {title}
36 | if (post.type === PostType.NOTE) return a note
37 | if (post.type === PostType.ARTICLE) {
38 | return an article
39 | }
40 | return
41 | }
42 |
43 | const getContent = () => {
44 | if (post.type === PostType.IMAGE) {
45 | return (
46 |
47 | )
48 | }
49 |
50 | if (post.type === PostType.NOTE) {
51 | return {post.title}
52 | }
53 |
54 | const nodes: any[] =
55 | typeof post.content === 'string' ? JSON.parse(post.content) : post.content
56 | const str = nodes.map((node) => Node.string(node)).join('') || ''
57 |
58 | return (
59 |
60 |
61 | {post.title}
62 |
63 |
64 | {str?.slice(0, 260)}...
65 |
66 |
67 | )
68 | }
69 |
70 | const getAvatar = () => {
71 | if (post.user.image) {
72 | return (
73 |
74 |
75 | {post.user.displayName}
76 |
77 | )
78 | }
79 |
80 | return (
81 |
87 | )
88 | }
89 |
90 | return (
91 |
92 |
93 |
94 | {getAvatar()}
95 |
{name}
96 |
posted
97 | {getTitle()}
98 |
99 |
102 |
103 |
104 | {getContent()}
105 |
106 | {PostActions &&
}
107 |
108 | )
109 | }
110 |
111 | function hashCode(str: string) {
112 | let hash = 0
113 | for (let i = 0; i < str.length; i++) {
114 | const char = str.charCodeAt(i)
115 | hash = (hash << 5) - hash + char
116 | hash = hash & hash // Convert to 32bit integer
117 | }
118 | return hash
119 | }
120 |
121 | function getFromColor(i: number) {
122 | const colors = [
123 | 'from-red-500',
124 | 'from-yellow-500',
125 | 'from-green-500',
126 | 'from-blue-500',
127 | 'from-indigo-500',
128 | 'from-purple-500',
129 | 'from-pink-500',
130 | 'from-red-600',
131 | 'from-yellow-600',
132 | 'from-green-600',
133 | 'from-blue-600',
134 | 'from-indigo-600',
135 | 'from-purple-600',
136 | 'from-pink-600',
137 | ]
138 | return colors[Math.abs(i) % colors.length]
139 | }
140 |
141 | function getToColor(i: number) {
142 | const colors = [
143 | 'to-red-500',
144 | 'to-yellow-500',
145 | 'to-green-500',
146 | 'to-blue-500',
147 | 'to-indigo-500',
148 | 'to-purple-500',
149 | 'to-pink-500',
150 | 'to-red-600',
151 | 'to-yellow-600',
152 | 'to-green-600',
153 | 'to-blue-600',
154 | 'to-indigo-600',
155 | 'to-purple-600',
156 | 'to-pink-600',
157 | ]
158 | return colors[Math.abs(i) % colors.length]
159 | }
160 |
161 | function generateGradient(walletAddress: string) {
162 | if (!walletAddress) return `bg-gradient-to-r to-pink-500 to-purple-500`
163 | const hash = hashCode(walletAddress)
164 | const from = getFromColor(hash)
165 | const to = getToColor(hash >> 8)
166 | return `bg-gradient-to-r ${from} ${to}`
167 | }
168 |
169 | function getUserName(user: User) {
170 | const { displayName = '', name } = user
171 |
172 | if (displayName) {
173 | if (isAddress(displayName)) {
174 | return displayName.slice(0, 3) + '...' + displayName.slice(-4)
175 | }
176 | return user.displayName || user.name
177 | }
178 |
179 | if (isAddress(name)) {
180 | return name.slice(0, 3) + '...' + name.slice(-4)
181 | }
182 | return user.displayName || user.name
183 | }
184 |
--------------------------------------------------------------------------------
/themes/garden/components/PostList.tsx:
--------------------------------------------------------------------------------
1 | import { Pagination } from '@/components/theme-ui/Pagination'
2 | import { Post } from '@penxio/types'
3 | import { PostItem } from './PostItem'
4 |
5 | interface PaginationProps {
6 | totalPages: number
7 | currentPage: number
8 | }
9 | interface PostListProps {
10 | posts: Post[]
11 | initialDisplayPosts?: Post[]
12 | pagination?: PaginationProps
13 | }
14 |
15 | export function PostList({
16 | posts,
17 | initialDisplayPosts = [],
18 | pagination,
19 | }: PostListProps) {
20 | const displayPosts =
21 | initialDisplayPosts.length > 0 ? initialDisplayPosts : posts
22 |
23 | return (
24 |
25 |
26 | {displayPosts.map((post) => {
27 | return
28 | })}
29 |
30 | {pagination && pagination.totalPages > 1 && (
31 |
35 | )}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/themes/garden/components/PostListWithTag.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Tag } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { PostList } from './PostList'
4 | import { TagList } from './TagList'
5 |
6 | interface PaginationProps {
7 | totalPages: number
8 | currentPage: number
9 | }
10 | interface PostListWithTagProps {
11 | posts: Post[]
12 | tags: Tag[]
13 | initialDisplayPosts?: Post[]
14 | pagination?: PaginationProps
15 | }
16 |
17 | export function PostListWithTag({
18 | posts,
19 | tags = [],
20 | initialDisplayPosts = [],
21 | pagination,
22 | }: PostListWithTagProps) {
23 | const displayPosts =
24 | initialDisplayPosts.length > 0 ? initialDisplayPosts : posts
25 |
26 | return (
27 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/themes/garden/components/PostTypeNav.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { cn } from '@/lib/utils'
4 | import Link from 'next/link'
5 | import { useSearchParams } from 'next/navigation'
6 |
7 | interface Props {
8 | className?: string
9 | }
10 |
11 | export const PostTypeNav = ({ className }: Props) => {
12 | const param = useSearchParams()!
13 | const type = param.get('type')
14 |
15 | return (
16 |
22 |
23 | All
24 |
25 |
29 | Articles
30 |
31 |
35 | Notes
36 |
37 |
41 | Photos
42 |
43 |
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/themes/garden/components/SectionContainer.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { cn } from '@/lib/utils'
3 |
4 | interface Props {
5 | children: ReactNode
6 | className?: string
7 | }
8 |
9 | export default function SectionContainer({ children, className }: Props) {
10 | return (
11 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/themes/garden/components/Tag.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import { slug } from 'github-slugger'
3 | import Link from 'next/link'
4 |
5 | interface Props {
6 | text: string
7 | className?: string
8 | }
9 |
10 | const Tag = ({ text, className }: Props) => {
11 | return (
12 |
19 | {text.split(' ').join('-')}
20 |
21 | )
22 | }
23 |
24 | export default Tag
25 |
--------------------------------------------------------------------------------
/themes/garden/components/TagList.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Tag } from '@penxio/types'
4 | import { slug } from 'github-slugger'
5 | import Link from 'next/link'
6 | import { usePathname } from 'next/navigation'
7 |
8 | interface PostListWithTagProps {
9 | tags: Tag[]
10 | }
11 |
12 | export function TagList({ tags = [] }: PostListWithTagProps) {
13 | const pathname = usePathname()!
14 |
15 | return (
16 |
17 |
18 | {tags.map((t) => {
19 | return (
20 | -
21 | {decodeURI(pathname.split('/tags/')[1]) === slug(t.name) ? (
22 |
#{`${t.name}`}
23 | ) : (
24 |
29 | #{`${t.name}`}
30 |
31 | )}
32 |
33 | )
34 | })}
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/themes/garden/files.json:
--------------------------------------------------------------------------------
1 | [
2 | "components/Header.tsx",
3 | "components/PostItem.tsx",
4 | "components/PostList.tsx",
5 | "components/PostListWithTag.tsx",
6 | "components/PostTypeNav.tsx",
7 | "components/SectionContainer.tsx",
8 | "components/Tag.tsx",
9 | "components/TagList.tsx",
10 | "index.ts",
11 | "layouts/SiteLayout.tsx",
12 | "manifest.json",
13 | "pages/AboutPage.tsx",
14 | "pages/BlogPage.tsx",
15 | "pages/HomePage.tsx",
16 | "pages/PostDetail.tsx",
17 | "pages/TagDetailPage.tsx",
18 | "pages/TagListPage.tsx",
19 | "screenshots/screenshot-1.png"
20 | ]
--------------------------------------------------------------------------------
/themes/garden/index.ts:
--------------------------------------------------------------------------------
1 | export * from './layouts/SiteLayout'
2 | export * from './pages/PostDetail'
3 | export * from './pages/AboutPage'
4 | export * from './pages/HomePage'
5 | export * from './pages/TagDetailPage'
6 | export * from './pages/TagListPage'
7 | export * from './pages/BlogPage'
8 |
--------------------------------------------------------------------------------
/themes/garden/layouts/SiteLayout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { Footer } from '@/components/theme-ui/Footer'
3 | import { Site } from '@penxio/types'
4 | import { Header } from '../components/Header'
5 | import SectionContainer from '../components/SectionContainer'
6 |
7 | interface Props {
8 | site: Site
9 | children: ReactNode
10 | }
11 |
12 | export function SiteLayout({ children, site }: Props) {
13 | return (
14 |
15 |
16 | {children}
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/themes/garden/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "title": "Garden",
3 | "author": "0xZio",
4 | "description": "",
5 | "previewUrl": "https://www.0xz.io"
6 | }
7 |
--------------------------------------------------------------------------------
/themes/garden/pages/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | import { ContentRender } from '@/components/plate-ui/ContentRender'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { Site } from '@penxio/types'
4 |
5 | interface Props {
6 | site: Site
7 | }
8 |
9 | export function AboutPage({ site }: Props) {
10 | return (
11 |
12 |
About
13 |
14 |
15 | {site.logo && (
16 |

21 | )}
22 |
23 | {site.name}
24 |
25 |
{site.description}
26 | {/*
27 |
28 |
29 |
30 |
31 |
*/}
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/themes/garden/pages/BlogPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { PostList } from '../components/PostList'
4 |
5 | interface Props {
6 | posts: Post[]
7 | initialDisplayPosts: Post[]
8 | pagination: {
9 | currentPage: number
10 | totalPages: number
11 | }
12 | }
13 |
14 | export function BlogPage({
15 | posts = [],
16 | pagination,
17 | initialDisplayPosts,
18 | }: Props) {
19 |
20 | return (
21 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/themes/garden/pages/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Site } from '@penxio/types'
2 | import { Lobster, Merienda } from 'next/font/google'
3 | import { PostItem } from '../components/PostItem'
4 |
5 | const lobster = Lobster({
6 | weight: ['400'],
7 | subsets: ['latin'],
8 | display: 'swap',
9 | })
10 |
11 | interface Props {
12 | site: Site
13 | posts: Post[]
14 | }
15 |
16 | export function HomePage({ posts = [], site }: Props) {
17 | const addresses = posts.reduce((acc, { user }) => {
18 | const { accounts = [] } = user
19 | for (const a of accounts) {
20 | if (a.providerType === 'WALLET') acc.push(a.providerAccountId)
21 | }
22 | return acc
23 | }, [] as string[])
24 | const receivers = Array.from(new Set(addresses))
25 |
26 | return (
27 |
28 |
29 | {posts.map((post) => {
30 | return
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/themes/garden/pages/PostDetail.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { ContentRender } from '@/components/plate-ui/ContentRender'
3 | import { PageTitle } from '@/components/theme-ui/PageTitle'
4 | import { PostActions } from '@/components/theme-ui/PostActions'
5 | import { cn, formatDate } from '@/lib/utils'
6 | import { Post } from '@penxio/types'
7 | import { ExternalLink } from 'lucide-react'
8 | import Link from 'next/link'
9 | import SectionContainer from '../components/SectionContainer'
10 |
11 | interface LayoutProps {
12 | post: Post
13 | children: ReactNode
14 | className?: string
15 | next?: { path: string; title: string }
16 | prev?: { path: string; title: string }
17 | }
18 |
19 | export function PostDetail({ post, next, prev, className }: LayoutProps) {
20 | return (
21 |
22 |
23 | {post.title}
24 |
25 |
26 | - Published on
27 | -
28 |
29 |
30 | - ·
31 | -
32 | {post.readingTime.text}
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | {post.cid && (
44 |
45 |
IPFS CID:
46 |
{post.cid}
47 |
52 |
53 |
54 |
55 | )}
56 |
57 |
83 |
84 |
85 | )
86 | }
87 |
--------------------------------------------------------------------------------
/themes/garden/pages/TagDetailPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Tag } from '@penxio/types'
2 | import { PostListWithTag } from '../components/PostListWithTag'
3 |
4 | interface Props {
5 | posts: Post[]
6 | tags: Tag[]
7 | }
8 |
9 | export function TagDetailPage({ posts = [], tags = [] }: Props) {
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/themes/garden/pages/TagListPage.tsx:
--------------------------------------------------------------------------------
1 | import { Tag } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { TagList } from '../components/TagList'
4 |
5 | interface Props {
6 | tags: Tag[]
7 | }
8 |
9 | export function TagListPage({ tags }: Props) {
10 | return (
11 |
12 |
Tags
13 |
14 | {tags.length === 0 && 'No tags found.'}
15 | {tags.length > 0 && }
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/themes/garden/screenshots/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/penxio/themes/c24d73088e62169d8eeccd073e16f6a8495aa31d/themes/garden/screenshots/screenshot-1.png
--------------------------------------------------------------------------------
/themes/micro/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Profile } from '@/components/Profile/Profile'
2 | import { Airdrop } from '@/components/theme-ui/Airdrop'
3 | import { cn } from '@/lib/utils'
4 | import { Site } from '@penxio/types'
5 | import Link from 'next/link'
6 |
7 | const headerNavLinks = [
8 | { href: '/', title: 'Home' },
9 | { href: '/posts', title: 'Blog' },
10 | // { href: '/tags', title: 'Tags' },
11 | { href: '/about', title: 'About' },
12 | { href: '/creator-fi', title: 'CreatorFi' },
13 | { href: '/membership', title: 'Membership', isMembership: true },
14 | ]
15 |
16 | interface Props {
17 | site: Site
18 | }
19 |
20 | export const Header = ({ site }: Props) => {
21 | return (
22 |
56 | )
57 | }
58 |
--------------------------------------------------------------------------------
/themes/micro/components/PostItem.tsx:
--------------------------------------------------------------------------------
1 | import { formatDate } from '@/lib/utils'
2 | import { Post } from '@penxio/types'
3 | import Link from 'next/link'
4 | import Tag from './Tag'
5 |
6 | interface PostItemProps {
7 | post: Post
8 | }
9 |
10 | export function PostItem({ post }: PostItemProps) {
11 | const { slug, title } = post
12 |
13 | return (
14 |
15 |
20 |
{title}
21 |
22 |
23 |
24 |
25 | {formatDate(post.updatedAt)}
26 |
27 |
28 | {post.postTags?.map((item) => (
29 |
30 | ))}
31 |
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/themes/micro/components/PostList.tsx:
--------------------------------------------------------------------------------
1 | import { Pagination } from '@/components/theme-ui/Pagination'
2 | import { Post } from '@penxio/types'
3 | import { PostItem } from './PostItem'
4 |
5 | interface PaginationProps {
6 | totalPages: number
7 | currentPage: number
8 | }
9 | interface PostListProps {
10 | posts: Post[]
11 | initialDisplayPosts?: Post[]
12 | pagination?: PaginationProps
13 | }
14 |
15 | export function PostList({
16 | posts,
17 | initialDisplayPosts = [],
18 | pagination,
19 | }: PostListProps) {
20 | const displayPosts =
21 | initialDisplayPosts.length > 0 ? initialDisplayPosts : posts
22 |
23 | return (
24 |
25 |
26 | {displayPosts.map((post) => {
27 | return
28 | })}
29 |
30 | {pagination && pagination.totalPages > 1 && (
31 |
35 | )}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/themes/micro/components/PostListWithTag.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Tag } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { PostList } from './PostList'
4 | import { TagList } from './TagList'
5 |
6 | interface PaginationProps {
7 | totalPages: number
8 | currentPage: number
9 | }
10 | interface PostListWithTagProps {
11 | posts: Post[]
12 | tags: Tag[]
13 | initialDisplayPosts?: Post[]
14 | pagination?: PaginationProps
15 | }
16 |
17 | export function PostListWithTag({
18 | posts,
19 | tags = [],
20 | initialDisplayPosts = [],
21 | pagination,
22 | }: PostListWithTagProps) {
23 | const displayPosts =
24 | initialDisplayPosts.length > 0 ? initialDisplayPosts : posts
25 |
26 | return (
27 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/themes/micro/components/SectionContainer.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 |
3 | interface Props {
4 | children: ReactNode
5 | }
6 |
7 | export default function SectionContainer({ children }: Props) {
8 | return (
9 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/themes/micro/components/Tag.tsx:
--------------------------------------------------------------------------------
1 | import { slug } from 'github-slugger'
2 | import Link from 'next/link'
3 | import { PostTag } from '@penxio/types'
4 | import { cn } from '@/lib/utils'
5 |
6 | interface Props {
7 | postTag: PostTag
8 | className?: string
9 | }
10 |
11 | const Tag = ({ postTag, className }: Props) => {
12 | return (
13 |
20 | {postTag.tag.name.split(' ').join('-')}
21 |
22 | )
23 | }
24 |
25 | export default Tag
26 |
--------------------------------------------------------------------------------
/themes/micro/components/TagList.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Tag } from '@penxio/types'
4 | import { slug } from 'github-slugger'
5 | import Link from 'next/link'
6 | import { usePathname } from 'next/navigation'
7 |
8 | interface PostListWithTagProps {
9 | tags: Tag[]
10 | }
11 |
12 | export function TagList({ tags = [] }: PostListWithTagProps) {
13 | const pathname = usePathname()!
14 |
15 | return (
16 |
17 |
18 | {tags.map((t) => {
19 | return (
20 | -
21 | {decodeURI(pathname.split('/tags/')[1]) === slug(t.name) ? (
22 |
#{`${t.name}`}
23 | ) : (
24 |
29 | #{`${t.name}`}
30 |
31 | )}
32 |
33 | )
34 | })}
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/themes/micro/files.json:
--------------------------------------------------------------------------------
1 | [
2 | "components/Header.tsx",
3 | "components/PostItem.tsx",
4 | "components/PostList.tsx",
5 | "components/PostListWithTag.tsx",
6 | "components/SectionContainer.tsx",
7 | "components/Tag.tsx",
8 | "components/TagList.tsx",
9 | "index.ts",
10 | "layouts/SiteLayout.tsx",
11 | "manifest.json",
12 | "pages/AboutPage.tsx",
13 | "pages/BlogPage.tsx",
14 | "pages/HomePage.tsx",
15 | "pages/PostDetail.tsx",
16 | "pages/TagDetailPage.tsx",
17 | "pages/TagListPage.tsx",
18 | "screenshots/screenshot-1.png"
19 | ]
--------------------------------------------------------------------------------
/themes/micro/index.ts:
--------------------------------------------------------------------------------
1 | export * from './layouts/SiteLayout'
2 | export * from './pages/PostDetail'
3 | export * from './pages/AboutPage'
4 | export * from './pages/HomePage'
5 | export * from './pages/TagDetailPage'
6 | export * from './pages/TagListPage'
7 | export * from './pages/BlogPage'
8 |
--------------------------------------------------------------------------------
/themes/micro/layouts/SiteLayout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { Footer } from '@/components/theme-ui/Footer'
3 | import { Site } from '@penxio/types'
4 | import { Header } from '../components/Header'
5 | import SectionContainer from '../components/SectionContainer'
6 |
7 | interface Props {
8 | site: Site
9 | children: ReactNode
10 | }
11 |
12 | export function SiteLayout({ children, site }: Props) {
13 | return (
14 |
15 |
16 | {children}
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/themes/micro/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "micro",
3 | "title": "Micro",
4 | "author": "0xZio",
5 | "description": "",
6 | "previewUrl": "https://penx-cloudflare.pages.dev"
7 | }
8 |
--------------------------------------------------------------------------------
/themes/micro/pages/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | import { ContentRender } from '@/components/plate-ui/ContentRender'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { Site } from '@penxio/types'
4 |
5 | interface Props {
6 | site: Site
7 | }
8 |
9 | export function AboutPage({ site }: Props) {
10 | return (
11 | <>
12 |
13 |
About
14 |
15 |
16 | {site.logo && (
17 |

22 | )}
23 |
24 | {site.name}
25 |
26 |
{site.description}
27 | {/*
28 |
29 |
30 |
31 |
32 |
*/}
33 |
34 |
35 |
36 |
37 |
38 |
39 | >
40 | )
41 | }
42 |
--------------------------------------------------------------------------------
/themes/micro/pages/BlogPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { PostList } from '../components/PostList'
4 |
5 | interface Props {
6 | posts: Post[]
7 | initialDisplayPosts: Post[]
8 | pagination: {
9 | currentPage: number
10 | totalPages: number
11 | }
12 | }
13 |
14 | export function BlogPage({
15 | posts = [],
16 | pagination,
17 | initialDisplayPosts,
18 | }: Props) {
19 |
20 | return (
21 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/themes/micro/pages/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { ContentRender } from '@/components/plate-ui/ContentRender'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { Post, Site } from '@penxio/types'
4 | import Link from 'next/link'
5 | import { PostItem } from '../components/PostItem'
6 |
7 | const POSTS_PER_PAGE = Number(process.env.NEXT_PUBLIC_POSTS_PER_PAGE || 10)
8 |
9 | interface Props {
10 | site: Site
11 | posts: Post[]
12 | }
13 |
14 | export function HomePage({ posts = [], site }: Props) {
15 | return (
16 |
17 |
18 |
{site.name}
19 |
20 |
21 |
22 |
23 |
24 |
25 | Latest
26 |
27 |
28 | {posts.length > POSTS_PER_PAGE && (
29 |
33 | All posts →
34 |
35 | )}
36 |
37 |
38 | {!posts.length && 'No posts found.'}
39 | {posts.slice(0, POSTS_PER_PAGE).map((post) => {
40 | return
41 | })}
42 |
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/themes/micro/pages/PostDetail.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { ContentRender } from '@/components/plate-ui/ContentRender'
3 | import { PageTitle } from '@/components/theme-ui/PageTitle'
4 | import { PostActions } from '@/components/theme-ui/PostActions'
5 | import { cn, formatDate } from '@/lib/utils'
6 | import { Post } from '@penxio/types'
7 | import { ExternalLink } from 'lucide-react'
8 | import Link from 'next/link'
9 |
10 | interface LayoutProps {
11 | post: Post
12 | children: ReactNode
13 | className?: string
14 | next?: { path: string; title: string }
15 | prev?: { path: string; title: string }
16 | }
17 |
18 | export function PostDetail({
19 | post,
20 | next,
21 | prev,
22 | className,
23 | children,
24 | }: LayoutProps) {
25 | return (
26 |
27 |
28 | {post.title}
29 |
30 | - Published on
31 | -
32 |
33 |
34 | - ·
35 | -
36 | {post.readingTime.text}
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 | {post.cid && (
47 |
48 |
IPFS CID:
49 |
{post.cid}
50 |
55 |
56 |
57 |
58 | )}
59 |
60 |
86 |
87 |
88 | )
89 | }
90 |
--------------------------------------------------------------------------------
/themes/micro/pages/TagDetailPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Tag } from '@penxio/types'
2 | import { PostListWithTag } from '../components/PostListWithTag'
3 |
4 | interface Props {
5 | posts: Post[]
6 | tags: Tag[]
7 | }
8 |
9 | export function TagDetailPage({ posts = [], tags = [] }: Props) {
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/themes/micro/pages/TagListPage.tsx:
--------------------------------------------------------------------------------
1 | import { Tag } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { TagList } from '../components/TagList'
4 |
5 | interface Props {
6 | tags: Tag[]
7 | }
8 |
9 | export function TagListPage({ tags }: Props) {
10 | return (
11 |
12 |
Tags
13 |
14 | {tags.length === 0 && 'No tags found.'}
15 | {tags.length > 0 && }
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/themes/micro/screenshots/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/penxio/themes/c24d73088e62169d8eeccd073e16f6a8495aa31d/themes/micro/screenshots/screenshot-1.png
--------------------------------------------------------------------------------
/themes/minimal/components/ClientOnly.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React, { PropsWithChildren, useEffect, useState } from 'react'
4 |
5 | export function ClientOnly({ children }: PropsWithChildren) {
6 | // State / Props
7 | const [hasMounted, setHasMounted] = useState(false)
8 |
9 | // Hooks
10 | useEffect(() => {
11 | setHasMounted(true)
12 | }, [])
13 |
14 | // Render
15 | if (!hasMounted) return null
16 |
17 | return <>{children}>
18 | }
19 |
--------------------------------------------------------------------------------
/themes/minimal/components/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { Site } from '@penxio/types'
3 | import SocialIcon from './social-icons'
4 |
5 | interface Props {
6 | ModeToggle: () => ReactNode
7 | site: Site
8 | }
9 |
10 | export function Footer({ site, ModeToggle }: Props) {
11 | if (!site) return null
12 | const socials = site.socials
13 | return (
14 |
46 | )
47 | }
48 |
--------------------------------------------------------------------------------
/themes/minimal/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Profile } from '@/components/Profile/Profile'
2 | import { Airdrop } from '@/components/theme-ui/Airdrop'
3 | import { cn } from '@/lib/utils'
4 | import { Site } from '@penxio/types'
5 | import Link from 'next/link'
6 |
7 | const headerNavLinks = [
8 | { href: '/', title: 'Home' },
9 | { href: '/posts', title: 'Blog' },
10 | // { href: '/tags', title: 'Tags' },
11 | { href: '/about', title: 'About' },
12 | { href: '/creator-fi', title: 'CreatorFi' },
13 | { href: '/membership', title: 'Membership', isMembership: true },
14 | ]
15 |
16 | interface Props {
17 | site: Site
18 | }
19 |
20 | export const Header = ({ site }: Props) => {
21 | return (
22 |
58 | )
59 | }
60 |
--------------------------------------------------------------------------------
/themes/minimal/components/Image.tsx:
--------------------------------------------------------------------------------
1 | import NextImage, { ImageProps } from 'next/image'
2 |
3 | const basePath = process.env.BASE_PATH
4 |
5 | const Image = ({ src, ...rest }: ImageProps) => (
6 |
7 | )
8 |
9 | export default Image
10 |
--------------------------------------------------------------------------------
/themes/minimal/components/Link.tsx:
--------------------------------------------------------------------------------
1 | import { AnchorHTMLAttributes } from 'react'
2 | import Link from 'next/link'
3 | import type { LinkProps } from 'next/link'
4 |
5 | const CustomLink = ({
6 | href,
7 | ...rest
8 | }: LinkProps & AnchorHTMLAttributes) => {
9 | const isInternalLink = href && href.startsWith('/')
10 | const isAnchorLink = href && href.startsWith('#')
11 |
12 | const isProd = process.env.NODE_ENV === 'production'
13 |
14 | if (isInternalLink) {
15 | // if (isProd) href = href + '.html'
16 | return
17 | }
18 |
19 | if (isAnchorLink) {
20 | return
21 | }
22 |
23 | return (
24 |
31 | )
32 | }
33 |
34 | export default CustomLink
35 |
--------------------------------------------------------------------------------
/themes/minimal/components/PageTitle.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { cn } from '@/lib/utils'
3 |
4 | interface Props {
5 | children: ReactNode
6 | className?: string
7 | }
8 |
9 | export default function PageTitle({ children, className }: Props) {
10 | return (
11 |
17 | {children}
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/themes/minimal/components/Pagination.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/anchor-is-valid */
2 | 'use client'
3 |
4 | import { usePathname } from 'next/navigation'
5 | import Link from './Link'
6 |
7 | interface PaginationProps {
8 | totalPages: number
9 | currentPage: number
10 | }
11 |
12 | export function Pagination({ totalPages, currentPage }: PaginationProps) {
13 | const pathname = usePathname()!
14 | const basePath = pathname.split('/')[1]
15 | const prevPage = currentPage - 1 > 0
16 | const nextPage = currentPage + 1 <= totalPages
17 |
18 | return (
19 |
20 |
58 |
59 | )
60 | }
61 |
--------------------------------------------------------------------------------
/themes/minimal/components/PostItem.tsx:
--------------------------------------------------------------------------------
1 | import { formatDate } from '@/lib/utils'
2 | import { Post } from '@penxio/types'
3 | import Link from 'next/link'
4 |
5 | interface PostItemProps {
6 | post: Post
7 | }
8 |
9 | export function PostItem({ post }: PostItemProps) {
10 | const { slug, title } = post
11 |
12 | return (
13 |
18 | {title}
19 |
22 |
23 | )
24 | }
25 |
--------------------------------------------------------------------------------
/themes/minimal/components/PostList.tsx:
--------------------------------------------------------------------------------
1 | import { Pagination } from '@/components/theme-ui/Pagination'
2 | import { Post } from '@penxio/types'
3 | import { PostItem } from './PostItem'
4 |
5 | interface PaginationProps {
6 | totalPages: number
7 | currentPage: number
8 | }
9 | interface PostListProps {
10 | posts: Post[]
11 | initialDisplayPosts?: Post[]
12 | pagination?: PaginationProps
13 | }
14 |
15 | export function PostList({
16 | posts,
17 | initialDisplayPosts = [],
18 | pagination,
19 | }: PostListProps) {
20 | const displayPosts =
21 | initialDisplayPosts.length > 0 ? initialDisplayPosts : posts
22 |
23 | return (
24 |
25 |
26 | {displayPosts.map((post) => {
27 | return
28 | })}
29 |
30 | {pagination && pagination.totalPages > 1 && (
31 |
35 | )}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/themes/minimal/components/PostListWithTag.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Tag } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { PostList } from './PostList'
4 | import { TagList } from './TagList'
5 |
6 | interface PaginationProps {
7 | totalPages: number
8 | currentPage: number
9 | }
10 | interface PostListWithTagProps {
11 | posts: Post[]
12 | tags: Tag[]
13 | initialDisplayPosts?: Post[]
14 | pagination?: PaginationProps
15 | }
16 |
17 | export function PostListWithTag({
18 | posts,
19 | tags = [],
20 | initialDisplayPosts = [],
21 | pagination,
22 | }: PostListWithTagProps) {
23 | const displayPosts =
24 | initialDisplayPosts.length > 0 ? initialDisplayPosts : posts
25 |
26 | return (
27 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/themes/minimal/components/SectionContainer.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 |
3 | interface Props {
4 | children: ReactNode
5 | }
6 |
7 | export default function SectionContainer({ children }: Props) {
8 | return (
9 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/themes/minimal/components/Tag.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import { slug } from 'github-slugger'
3 | import Link from 'next/link'
4 |
5 | interface Props {
6 | text: string
7 | className?: string
8 | }
9 |
10 | const Tag = ({ text, className }: Props) => {
11 | return (
12 |
19 | {text.split(' ').join('-')}
20 |
21 | )
22 | }
23 |
24 | export default Tag
25 |
--------------------------------------------------------------------------------
/themes/minimal/components/TagList.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Tag } from '@penxio/types'
4 | import { slug } from 'github-slugger'
5 | import Link from 'next/link'
6 | import { usePathname } from 'next/navigation'
7 |
8 | interface PostListWithTagProps {
9 | tags: Tag[]
10 | }
11 |
12 | export function TagList({ tags = [] }: PostListWithTagProps) {
13 | const pathname = usePathname()!
14 |
15 | return (
16 |
17 |
18 | {tags.map((t) => {
19 | return (
20 | -
21 | {decodeURI(pathname.split('/tags/')[1]) === slug(t.name) ? (
22 |
#{`${t.name}`}
23 | ) : (
24 |
29 | #{`${t.name}`}
30 |
31 | )}
32 |
33 | )
34 | })}
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/themes/minimal/components/social-icons/icons.tsx:
--------------------------------------------------------------------------------
1 | import { SVGProps } from 'react'
2 |
3 | // Icons taken from: https://simpleicons.org/
4 | // To add a new icon, add a new function here and add it to components in social-icons/index.tsx
5 |
6 | export function Facebook(svgProps: SVGProps) {
7 | return (
8 |
12 | )
13 | }
14 |
15 | export function Github(svgProps: SVGProps) {
16 | return (
17 |
21 | )
22 | }
23 |
24 | export function Linkedin(svgProps: SVGProps) {
25 | return (
26 |
30 | )
31 | }
32 |
33 | export function Mail(svgProps: SVGProps) {
34 | return (
35 |
40 | )
41 | }
42 |
43 | export function Twitter(svgProps: SVGProps) {
44 | return (
45 |
49 | )
50 | }
51 |
52 | export function X(svgProps: SVGProps) {
53 | return (
54 |
58 | )
59 | }
60 |
61 | export function Youtube(svgProps: SVGProps) {
62 | return (
63 |
67 | )
68 | }
69 |
70 | export function Mastodon(svgProps: SVGProps) {
71 | return (
72 |
76 | )
77 | }
78 |
79 | export function Threads(svgProps: SVGProps) {
80 | return (
81 |
85 | )
86 | }
87 |
88 | export function Instagram(svgProps: SVGProps) {
89 | return (
90 |
94 | )
95 | }
96 |
--------------------------------------------------------------------------------
/themes/minimal/components/social-icons/index.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Facebook,
3 | Github,
4 | Instagram,
5 | Linkedin,
6 | Mail,
7 | Mastodon,
8 | Threads,
9 | Twitter,
10 | X,
11 | Youtube,
12 | } from './icons'
13 |
14 | const components = {
15 | mail: Mail,
16 | github: Github,
17 | facebook: Facebook,
18 | youtube: Youtube,
19 | linkedin: Linkedin,
20 | twitter: Twitter,
21 | x: X,
22 | mastodon: Mastodon,
23 | threads: Threads,
24 | instagram: Instagram,
25 | }
26 |
27 | type SocialIconProps = {
28 | kind: keyof typeof components
29 | href: string | undefined
30 | size?: number
31 | }
32 |
33 | const SocialIcon = ({ kind, href, size = 8 }: SocialIconProps) => {
34 | if (
35 | !href ||
36 | (kind === 'mail' &&
37 | !/^mailto:[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/.test(href))
38 | )
39 | return null
40 |
41 | const SocialSvg = components[kind]
42 |
43 | return (
44 |
50 | {kind}
51 |
54 |
55 | )
56 | }
57 |
58 | export default SocialIcon
59 |
--------------------------------------------------------------------------------
/themes/minimal/externals/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | import { Site } from '@penxio/types'
2 | import Image from '../components/Image'
3 | import PageTitle from '../components/PageTitle'
4 | import SocialIcon from '../components/social-icons'
5 |
6 | interface Props {
7 | site: Site
8 | ContentRender: (props: { content: any[]; className?: string }) => JSX.Element
9 | }
10 |
11 | export function AboutPage({ site, ContentRender }: Props) {
12 | return (
13 | <>
14 |
15 |
About
16 |
17 |
18 | {site.logo && (
19 |
26 | )}
27 |
28 | {site.name}
29 |
30 |
{site.description}
31 | {/*
32 |
33 |
34 |
35 |
36 |
*/}
37 |
38 |
39 |
40 |
41 |
42 |
43 | >
44 | )
45 | }
46 |
--------------------------------------------------------------------------------
/themes/minimal/externals/BlogPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post } from '@penxio/types'
2 | import PageTitle from '../components/PageTitle'
3 | import { PostList } from '../components/PostList'
4 |
5 | interface Props {
6 | posts: Post[]
7 | initialDisplayPosts: Post[]
8 | pagination: {
9 | currentPage: number
10 | totalPages: number
11 | }
12 | }
13 |
14 | export function BlogPage({
15 | posts = [],
16 | pagination,
17 | initialDisplayPosts,
18 | }: Props) {
19 |
20 | return (
21 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/themes/minimal/externals/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Site } from '@penxio/types'
2 | import Link from '../components/Link'
3 | import PageTitle from '../components/PageTitle'
4 | import { PostItem } from '../components/PostItem'
5 |
6 | const POSTS_PER_PAGE = Number(process.env.NEXT_PUBLIC_POSTS_PER_PAGE || 10)
7 |
8 | interface Props {
9 | site: Site
10 | posts: Post[]
11 | ContentRender: (props: { content: any[]; className?: string }) => JSX.Element
12 | }
13 |
14 | export function HomePage({ posts = [], site, ContentRender }: Props) {
15 | return (
16 |
17 |
18 |
{site.name}
19 |
20 |
21 |
22 |
23 |
24 |
25 | Latest
26 |
27 |
28 | {posts.length > POSTS_PER_PAGE && (
29 |
33 | All posts →
34 |
35 | )}
36 |
37 |
38 | {!posts.length && 'No posts found.'}
39 | {posts.slice(0, POSTS_PER_PAGE).map((post) => {
40 | return
41 | })}
42 |
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/themes/minimal/externals/PostDetail.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { ExternalLink } from 'lucide-react'
3 | import { Post } from '@penxio/types'
4 | import { cn, formatDate } from '@/lib/utils'
5 | import Link from '../components/Link'
6 | import PageTitle from '../components/PageTitle'
7 |
8 | interface LayoutProps {
9 | post: Post
10 | children: ReactNode
11 | className?: string
12 | next?: { path: string; title: string }
13 | prev?: { path: string; title: string }
14 | PostActions?: (props: { post: Post; className?: string }) => JSX.Element
15 | ContentRender?: (props: { content: any; className?: string }) => JSX.Element
16 | }
17 |
18 | export function PostDetail({
19 | post,
20 | next,
21 | prev,
22 | className,
23 | PostActions,
24 | ContentRender,
25 | }: LayoutProps) {
26 | return (
27 |
28 |
29 | {post.title}
30 |
31 |
32 | - Published on
33 | -
34 |
35 |
36 | - ·
37 | -
38 | {post.readingTime.text}
39 |
40 |
41 |
42 |
43 | {PostActions && }
44 |
45 |
46 |
47 | {ContentRender && }
48 |
49 | {post.cid && (
50 |
51 |
IPFS CID:
52 |
{post.cid}
53 |
58 |
59 |
60 |
61 | )}
62 |
63 |
89 |
90 |
91 | )
92 | }
93 |
--------------------------------------------------------------------------------
/themes/minimal/externals/SiteLayout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { Site } from '@penxio/types'
3 | import { Footer } from '../components/Footer'
4 | import { Header } from '../components/Header'
5 | import SectionContainer from '../components/SectionContainer'
6 |
7 | interface Props {
8 | site: Site
9 | Logo: () => ReactNode
10 | ModeToggle: () => ReactNode
11 | MobileNav: () => ReactNode
12 | ConnectButton: () => ReactNode
13 | Airdrop: () => ReactNode
14 | children: ReactNode
15 | }
16 |
17 | export function SiteLayout({
18 | children,
19 | site,
20 | Logo,
21 | ModeToggle,
22 | MobileNav,
23 | Airdrop,
24 | ConnectButton,
25 | }: Props) {
26 | return (
27 |
28 | {ModeToggle && (
29 |
30 |
31 |
32 | )}
33 |
34 |
42 | {children}
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/themes/minimal/externals/TagDetailPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Tag } from '@penxio/types'
2 | import { PostListWithTag } from '../components/PostListWithTag'
3 |
4 | interface Props {
5 | posts: Post[]
6 | tags: Tag[]
7 | }
8 |
9 | export function TagDetailPage({ posts = [], tags = [] }: Props) {
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/themes/minimal/externals/TagListPage.tsx:
--------------------------------------------------------------------------------
1 | import { Tag } from '@penxio/types'
2 | import PageTitle from '../components/PageTitle'
3 | import { TagList } from '../components/TagList'
4 |
5 | interface Props {
6 | tags: Tag[]
7 | }
8 |
9 | export function TagListPage({ tags }: Props) {
10 | return (
11 |
12 |
Tags
13 |
14 | {tags.length === 0 && 'No tags found.'}
15 | {tags.length > 0 && }
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/themes/minimal/externals/index.ts:
--------------------------------------------------------------------------------
1 | export * from './SiteLayout'
2 | export * from './PostDetail'
3 | export * from './AboutPage'
4 | export * from './HomePage'
5 | export * from './TagDetailPage'
6 | export * from './TagListPage'
7 | export * from './BlogPage'
8 |
--------------------------------------------------------------------------------
/themes/minimal/files.json:
--------------------------------------------------------------------------------
1 | [
2 | "components/Header.tsx",
3 | "components/PostItem.tsx",
4 | "components/PostList.tsx",
5 | "components/PostListWithTag.tsx",
6 | "components/SectionContainer.tsx",
7 | "components/Tag.tsx",
8 | "components/TagList.tsx",
9 | "index.ts",
10 | "layouts/SiteLayout.tsx",
11 | "manifest.json",
12 | "pages/AboutPage.tsx",
13 | "pages/BlogPage.tsx",
14 | "pages/HomePage.tsx",
15 | "pages/PostDetail.tsx",
16 | "pages/TagDetailPage.tsx",
17 | "pages/TagListPage.tsx",
18 | "screenshots/screenshot-1.png"
19 | ]
--------------------------------------------------------------------------------
/themes/minimal/index.ts:
--------------------------------------------------------------------------------
1 | export * from './layouts/SiteLayout'
2 | export * from './pages/PostDetail'
3 | export * from './pages/AboutPage'
4 | export * from './pages/HomePage'
5 | export * from './pages/TagDetailPage'
6 | export * from './pages/TagListPage'
7 | export * from './pages/BlogPage'
8 |
--------------------------------------------------------------------------------
/themes/minimal/layouts/SiteLayout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { Footer } from '@/components/theme-ui/Footer'
3 | import { Site } from '@penxio/types'
4 | import { Header } from '../components/Header'
5 | import SectionContainer from '../components/SectionContainer'
6 |
7 | interface Props {
8 | site: Site
9 | children: ReactNode
10 | }
11 |
12 | export function SiteLayout({ children, site }: Props) {
13 | return (
14 |
15 |
16 | {children}
17 |
18 |
19 | )
20 | }
21 |
--------------------------------------------------------------------------------
/themes/minimal/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "minimal",
3 | "title": "Minimal",
4 | "author": "0xZio",
5 | "description": "",
6 | "previewUrl": "https://demo4.penx.io"
7 | }
8 |
--------------------------------------------------------------------------------
/themes/minimal/pages/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | import { ContentRender } from '@/components/plate-ui/ContentRender'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { Site } from '@penxio/types'
4 |
5 | interface Props {
6 | site: Site
7 | }
8 |
9 | export function AboutPage({ site }: Props) {
10 | return (
11 | <>
12 |
13 |
About
14 |
15 |
16 | {site.logo && (
17 |

22 | )}
23 |
24 | {site.name}
25 |
26 |
{site.description}
27 |
28 |
29 |
30 |
31 |
32 |
33 | >
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/themes/minimal/pages/BlogPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { PostList } from '../components/PostList'
4 |
5 | interface Props {
6 | posts: Post[]
7 | initialDisplayPosts: Post[]
8 | pagination: {
9 | currentPage: number
10 | totalPages: number
11 | }
12 | }
13 |
14 | export function BlogPage({
15 | posts = [],
16 | pagination,
17 | initialDisplayPosts,
18 | }: Props) {
19 |
20 | return (
21 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/themes/minimal/pages/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { ContentRender } from '@/components/plate-ui/ContentRender'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { Post, Site } from '@penxio/types'
4 | import Link from 'next/link'
5 | import { PostItem } from '../components/PostItem'
6 |
7 | const POSTS_PER_PAGE = Number(process.env.NEXT_PUBLIC_POSTS_PER_PAGE || 10)
8 |
9 | interface Props {
10 | site: Site
11 | posts: Post[]
12 | }
13 |
14 | export function HomePage({ posts = [], site }: Props) {
15 | return (
16 |
17 |
18 |
{site.name}
19 |
20 |
21 |
22 |
23 |
24 |
25 | Latest
26 |
27 |
28 | {posts.length > POSTS_PER_PAGE && (
29 |
33 | All posts →
34 |
35 | )}
36 |
37 |
38 | {!posts.length && 'No posts found.'}
39 | {posts.slice(0, POSTS_PER_PAGE).map((post) => {
40 | return
41 | })}
42 |
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/themes/minimal/pages/PostDetail.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { ContentRender } from '@/components/plate-ui/ContentRender'
3 | import { PageTitle } from '@/components/theme-ui/PageTitle'
4 | import { PostActions } from '@/components/theme-ui/PostActions'
5 | import { cn, formatDate } from '@/lib/utils'
6 | import { Post } from '@penxio/types'
7 | import { ExternalLink } from 'lucide-react'
8 | import Link from 'next/link'
9 |
10 | interface LayoutProps {
11 | post: Post
12 | children: ReactNode
13 | className?: string
14 | next?: { path: string; title: string }
15 | prev?: { path: string; title: string }
16 | }
17 |
18 | export function PostDetail({ post, next, prev, className }: LayoutProps) {
19 | return (
20 |
21 |
22 | {post.title}
23 |
24 |
25 | - Published on
26 | -
27 |
28 |
29 | - ·
30 | -
31 | {post.readingTime.text}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {post.cid && (
43 |
44 |
IPFS CID:
45 |
{post.cid}
46 |
51 |
52 |
53 |
54 | )}
55 |
56 |
82 |
83 |
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/themes/minimal/pages/TagDetailPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Tag } from '@penxio/types'
2 | import { PostListWithTag } from '../components/PostListWithTag'
3 |
4 | interface Props {
5 | posts: Post[]
6 | tags: Tag[]
7 | }
8 |
9 | export function TagDetailPage({ posts = [], tags = [] }: Props) {
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/themes/minimal/pages/TagListPage.tsx:
--------------------------------------------------------------------------------
1 | import { Tag } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { TagList } from '../components/TagList'
4 |
5 | interface Props {
6 | tags: Tag[]
7 | }
8 |
9 | export function TagListPage({ tags }: Props) {
10 | return (
11 |
12 |
Tags
13 |
14 | {tags.length === 0 && 'No tags found.'}
15 | {tags.length > 0 && }
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/themes/minimal/screenshots/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/penxio/themes/c24d73088e62169d8eeccd073e16f6a8495aa31d/themes/minimal/screenshots/screenshot-1.png
--------------------------------------------------------------------------------
/themes/photo/components/Header.tsx:
--------------------------------------------------------------------------------
1 | import { Profile } from '@/components/Profile/Profile'
2 | import { Airdrop } from '@/components/theme-ui/Airdrop'
3 | import { cn } from '@/lib/utils'
4 | import { Site } from '@penxio/types'
5 | import Link from 'next/link'
6 |
7 | const headerNavLinks = [
8 | { href: '/', title: 'Home' },
9 | // { href: '/posts', title: 'Blog' },
10 | // { href: '/tags', title: 'Tags' },
11 | { href: '/about', title: 'About' },
12 | { href: '/creator-fi', title: 'CreatorFi' },
13 | { href: '/membership', title: 'Membership', isMembership: true },
14 | ]
15 |
16 | interface Props {
17 | site: Site
18 | }
19 |
20 | export const Header = ({ site }: Props) => {
21 | return (
22 |
61 | )
62 | }
63 |
--------------------------------------------------------------------------------
/themes/photo/components/PostItem.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
2 | import { isAddress } from '@/lib/utils'
3 | import { Post, User } from '@penxio/types'
4 | import { cn, formatDate } from '@/lib/utils'
5 |
6 | interface PostItemProps {
7 | post: Post
8 | receivers?: string[]
9 | PostActions?: (props: {
10 | post: Post
11 | receivers?: string[]
12 | className?: string
13 | }) => JSX.Element
14 | }
15 |
16 | export function PostItem({ post, PostActions, receivers = [] }: PostItemProps) {
17 | const { slug, title } = post
18 | const name = getUserName(post.user)
19 |
20 | const getAvatar = () => {
21 | if (post.user.image) {
22 | return (
23 |
24 |
25 | {post.user.displayName}
26 |
27 | )
28 | }
29 |
30 | return (
31 |
37 | )
38 | }
39 |
40 | return (
41 |
42 |
43 |
44 | {getAvatar()}
45 |
{name}
46 |
posted
47 |
{title}
p
48 |
49 |
52 |
53 |
54 | {/*
*/}
55 |
56 |

57 |
58 | {PostActions &&
}
59 |
60 | )
61 | }
62 |
63 | function hashCode(str: string) {
64 | let hash = 0
65 | for (let i = 0; i < str.length; i++) {
66 | const char = str.charCodeAt(i)
67 | hash = (hash << 5) - hash + char
68 | hash = hash & hash // Convert to 32bit integer
69 | }
70 | return hash
71 | }
72 |
73 | function getFromColor(i: number) {
74 | const colors = [
75 | 'from-red-500',
76 | 'from-yellow-500',
77 | 'from-green-500',
78 | 'from-blue-500',
79 | 'from-indigo-500',
80 | 'from-purple-500',
81 | 'from-pink-500',
82 | 'from-red-600',
83 | 'from-yellow-600',
84 | 'from-green-600',
85 | 'from-blue-600',
86 | 'from-indigo-600',
87 | 'from-purple-600',
88 | 'from-pink-600',
89 | ]
90 | return colors[Math.abs(i) % colors.length]
91 | }
92 |
93 | function getToColor(i: number) {
94 | const colors = [
95 | 'to-red-500',
96 | 'to-yellow-500',
97 | 'to-green-500',
98 | 'to-blue-500',
99 | 'to-indigo-500',
100 | 'to-purple-500',
101 | 'to-pink-500',
102 | 'to-red-600',
103 | 'to-yellow-600',
104 | 'to-green-600',
105 | 'to-blue-600',
106 | 'to-indigo-600',
107 | 'to-purple-600',
108 | 'to-pink-600',
109 | ]
110 | return colors[Math.abs(i) % colors.length]
111 | }
112 |
113 | function generateGradient(walletAddress: string) {
114 | if (!walletAddress) return `bg-gradient-to-r to-pink-500 to-purple-500`
115 | const hash = hashCode(walletAddress)
116 | const from = getFromColor(hash)
117 | const to = getToColor(hash >> 8)
118 | return `bg-gradient-to-r ${from} ${to}`
119 | }
120 |
121 | function getUserName(user: User) {
122 | const { displayName = '', name } = user
123 |
124 | if (displayName) {
125 | if (isAddress(displayName)) {
126 | return displayName.slice(0, 3) + '...' + displayName.slice(-4)
127 | }
128 | return user.displayName || user.name
129 | }
130 |
131 | if (isAddress(name)) {
132 | return name.slice(0, 3) + '...' + name.slice(-4)
133 | }
134 | return user.displayName || user.name
135 | }
136 |
--------------------------------------------------------------------------------
/themes/photo/components/PostList.tsx:
--------------------------------------------------------------------------------
1 | import { Pagination } from '@/components/theme-ui/Pagination'
2 | import { Post } from '@penxio/types'
3 | import { PostItem } from './PostItem'
4 |
5 | interface PaginationProps {
6 | totalPages: number
7 | currentPage: number
8 | }
9 | interface PostListProps {
10 | posts: Post[]
11 | initialDisplayPosts?: Post[]
12 | pagination?: PaginationProps
13 | }
14 |
15 | export function PostList({
16 | posts,
17 | initialDisplayPosts = [],
18 | pagination,
19 | }: PostListProps) {
20 | const displayPosts =
21 | initialDisplayPosts.length > 0 ? initialDisplayPosts : posts
22 |
23 | return (
24 |
25 |
26 | {displayPosts.map((post) => {
27 | return
28 | })}
29 |
30 | {pagination && pagination.totalPages > 1 && (
31 |
35 | )}
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/themes/photo/components/PostListWithTag.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Tag } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { PostList } from './PostList'
4 | import { TagList } from './TagList'
5 |
6 | interface PaginationProps {
7 | totalPages: number
8 | currentPage: number
9 | }
10 | interface PostListWithTagProps {
11 | posts: Post[]
12 | tags: Tag[]
13 | initialDisplayPosts?: Post[]
14 | pagination?: PaginationProps
15 | }
16 |
17 | export function PostListWithTag({
18 | posts,
19 | tags = [],
20 | initialDisplayPosts = [],
21 | pagination,
22 | }: PostListWithTagProps) {
23 | const displayPosts =
24 | initialDisplayPosts.length > 0 ? initialDisplayPosts : posts
25 |
26 | return (
27 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/themes/photo/components/SectionContainer.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 |
3 | interface Props {
4 | children: ReactNode
5 | }
6 |
7 | export default function SectionContainer({ children }: Props) {
8 | return (
9 |
12 | )
13 | }
14 |
--------------------------------------------------------------------------------
/themes/photo/components/Tag.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import { slug } from 'github-slugger'
3 | import Link from 'next/link'
4 |
5 | interface Props {
6 | text: string
7 | className?: string
8 | }
9 |
10 | const Tag = ({ text, className }: Props) => {
11 | return (
12 |
19 | {text.split(' ').join('-')}
20 |
21 | )
22 | }
23 |
24 | export default Tag
25 |
--------------------------------------------------------------------------------
/themes/photo/components/TagList.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import { Tag } from '@penxio/types'
4 | import { slug } from 'github-slugger'
5 | import Link from 'next/link'
6 | import { usePathname } from 'next/navigation'
7 |
8 | interface PostListWithTagProps {
9 | tags: Tag[]
10 | }
11 |
12 | export function TagList({ tags = [] }: PostListWithTagProps) {
13 | const pathname = usePathname()!
14 |
15 | return (
16 |
17 |
18 | {tags.map((t) => {
19 | return (
20 | -
21 | {decodeURI(pathname.split('/tags/')[1]) === slug(t.name) ? (
22 |
#{`${t.name}`}
23 | ) : (
24 |
29 | #{`${t.name}`}
30 |
31 | )}
32 |
33 | )
34 | })}
35 |
36 |
37 | )
38 | }
39 |
--------------------------------------------------------------------------------
/themes/photo/files.json:
--------------------------------------------------------------------------------
1 | [
2 | "components/Header.tsx",
3 | "components/PostItem.tsx",
4 | "components/PostList.tsx",
5 | "components/PostListWithTag.tsx",
6 | "components/SectionContainer.tsx",
7 | "components/Tag.tsx",
8 | "components/TagList.tsx",
9 | "index.ts",
10 | "layouts/SiteLayout.tsx",
11 | "manifest.json",
12 | "pages/AboutPage.tsx",
13 | "pages/BlogPage.tsx",
14 | "pages/HomePage.tsx",
15 | "pages/PostDetail.tsx",
16 | "pages/TagDetailPage.tsx",
17 | "pages/TagListPage.tsx",
18 | "screenshots/screenshot-1.png"
19 | ]
--------------------------------------------------------------------------------
/themes/photo/index.ts:
--------------------------------------------------------------------------------
1 | export * from './layouts/SiteLayout'
2 | export * from './pages/PostDetail'
3 | export * from './pages/AboutPage'
4 | export * from './pages/HomePage'
5 | export * from './pages/TagDetailPage'
6 | export * from './pages/TagListPage'
7 | export * from './pages/BlogPage'
8 |
--------------------------------------------------------------------------------
/themes/photo/layouts/SiteLayout.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { Footer } from '@/components/theme-ui/Footer'
3 | import { Site } from '@penxio/types'
4 | import { Header } from '../components/Header'
5 | import SectionContainer from '../components/SectionContainer'
6 |
7 | interface Props {
8 | site: Site
9 | Logo: () => ReactNode
10 | ModeToggle: () => ReactNode
11 | MobileNav: () => ReactNode
12 | ConnectButton: () => ReactNode
13 | Airdrop: () => ReactNode
14 | children: ReactNode
15 | }
16 |
17 | export function SiteLayout({ children, site }: Props) {
18 | return (
19 |
20 |
21 | {children}
22 |
23 |
24 | )
25 | }
26 |
--------------------------------------------------------------------------------
/themes/photo/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "photo",
3 | "title": "Photo",
4 | "author": "0xZio",
5 | "description": "",
6 | "previewUrl": "https://demo3.penx.io"
7 | }
8 |
--------------------------------------------------------------------------------
/themes/photo/pages/AboutPage.tsx:
--------------------------------------------------------------------------------
1 | import { ContentRender } from '@/components/plate-ui/ContentRender'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { Site } from '@penxio/types'
4 |
5 | interface Props {
6 | site: Site
7 | }
8 |
9 | export function AboutPage({ site }: Props) {
10 | return (
11 |
12 |
About
13 |
14 |
15 | {site.logo && (
16 |

21 | )}
22 |
23 | {site.name}
24 |
25 |
{site.description}
26 | {/*
27 |
28 |
29 |
30 |
31 |
*/}
32 |
33 |
34 |
35 |
36 |
37 |
38 | )
39 | }
40 |
--------------------------------------------------------------------------------
/themes/photo/pages/BlogPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { PostList } from '../components/PostList'
4 |
5 | interface Props {
6 | posts: Post[]
7 | initialDisplayPosts: Post[]
8 | pagination: {
9 | currentPage: number
10 | totalPages: number
11 | }
12 | }
13 |
14 | export function BlogPage({
15 | posts = [],
16 | pagination,
17 | initialDisplayPosts,
18 | }: Props) {
19 |
20 | return (
21 |
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/themes/photo/pages/HomePage.tsx:
--------------------------------------------------------------------------------
1 | import { cn } from '@/lib/utils'
2 | import { Post, PostType, Site } from '@penxio/types'
3 | import { Lobster, Merienda } from 'next/font/google'
4 | import { PostItem } from '../components/PostItem'
5 |
6 | const merienda = Lobster({
7 | weight: ['400'],
8 | subsets: ['latin'],
9 | display: 'swap',
10 | })
11 |
12 | interface Props {
13 | site: Site
14 | posts: Post[]
15 | }
16 |
17 | export function HomePage({ posts = [], site }: Props) {
18 | const creations = posts.filter((post) => post.type === PostType.IMAGE)
19 |
20 | const addresses = posts.reduce((acc, { user }) => {
21 | const { accounts = [] } = user
22 | for (const a of accounts) {
23 | if (a.providerType === 'WALLET') acc.push(a.providerAccountId)
24 | }
25 | return acc
26 | }, [] as string[])
27 | const receivers = Array.from(new Set(addresses))
28 |
29 | return (
30 |
31 |
32 | {site.logo && (
33 |

34 | )}
35 |
36 | {site.name}
37 |
38 |
39 |
40 | {creations.map((post) => {
41 | return
42 | })}
43 |
44 |
45 | )
46 | }
47 |
--------------------------------------------------------------------------------
/themes/photo/pages/PostDetail.tsx:
--------------------------------------------------------------------------------
1 | import { ReactNode } from 'react'
2 | import { ContentRender } from '@/components/plate-ui/ContentRender'
3 | import { PageTitle } from '@/components/theme-ui/PageTitle'
4 | import { PostActions } from '@/components/theme-ui/PostActions'
5 | import { cn, formatDate } from '@/lib/utils'
6 | import { Post } from '@penxio/types'
7 | import { ExternalLink } from 'lucide-react'
8 | import Link from 'next/link'
9 |
10 | interface LayoutProps {
11 | post: Post
12 | children: ReactNode
13 | className?: string
14 | next?: { path: string; title: string }
15 | prev?: { path: string; title: string }
16 | }
17 |
18 | export function PostDetail({ post, next, prev, className }: LayoutProps) {
19 | return (
20 |
21 |
22 | {post.title}
23 |
24 |
25 | - Published on
26 | -
27 |
28 |
29 | - ·
30 | -
31 | {post.readingTime.text}
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | {post.cid && (
43 |
44 |
IPFS CID:
45 |
{post.cid}
46 |
51 |
52 |
53 |
54 | )}
55 |
56 |
82 |
83 |
84 | )
85 | }
86 |
--------------------------------------------------------------------------------
/themes/photo/pages/TagDetailPage.tsx:
--------------------------------------------------------------------------------
1 | import { Post, Tag } from '@penxio/types'
2 | import { PostListWithTag } from '../components/PostListWithTag'
3 |
4 | interface Props {
5 | posts: Post[]
6 | tags: Tag[]
7 | }
8 |
9 | export function TagDetailPage({ posts = [], tags = [] }: Props) {
10 | return
11 | }
12 |
--------------------------------------------------------------------------------
/themes/photo/pages/TagListPage.tsx:
--------------------------------------------------------------------------------
1 | import { Tag } from '@penxio/types'
2 | import { PageTitle } from '@/components/theme-ui/PageTitle'
3 | import { TagList } from '../components/TagList'
4 |
5 | interface Props {
6 | tags: Tag[]
7 | }
8 |
9 | export function TagListPage({ tags }: Props) {
10 | return (
11 |
12 |
Tags
13 |
14 | {tags.length === 0 && 'No tags found.'}
15 | {tags.length > 0 && }
16 |
17 |
18 | )
19 | }
20 |
--------------------------------------------------------------------------------
/themes/photo/screenshots/screenshot-1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/penxio/themes/c24d73088e62169d8eeccd073e16f6a8495aa31d/themes/photo/screenshots/screenshot-1.png
--------------------------------------------------------------------------------