@revealui/cli 0.0.1-pre.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +22 -202
- package/README.md +194 -0
- package/bin/create-revealui.js +6 -0
- package/bin/revealui.js +6 -0
- package/dist/cli.d.ts +16 -0
- package/dist/cli.js +1801 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1795 -124
- package/dist/index.js.map +1 -0
- package/package.json +49 -42
- package/templates/basic-blog/.env.example +36 -0
- package/templates/basic-blog/_gitignore +26 -0
- package/templates/basic-blog/next.config.mjs +10 -0
- package/templates/basic-blog/package.json +36 -0
- package/templates/basic-blog/postcss.config.mjs +5 -0
- package/templates/basic-blog/revealui.config.ts +19 -0
- package/templates/basic-blog/src/app/globals.css +6 -0
- package/templates/basic-blog/src/app/layout.tsx +15 -0
- package/templates/basic-blog/src/app/page.tsx +57 -0
- package/templates/basic-blog/src/app/posts/[slug]/page.tsx +66 -0
- package/templates/basic-blog/src/app/posts/page.tsx +61 -0
- package/templates/basic-blog/src/collections/Posts.ts +42 -0
- package/templates/basic-blog/src/seed.ts +73 -0
- package/templates/basic-blog/tsconfig.json +11 -0
- package/templates/e-commerce/.env.example +36 -0
- package/templates/e-commerce/_gitignore +26 -0
- package/templates/e-commerce/next.config.mjs +10 -0
- package/templates/e-commerce/package.json +36 -0
- package/templates/e-commerce/postcss.config.mjs +5 -0
- package/templates/e-commerce/revealui.config.ts +20 -0
- package/templates/e-commerce/src/app/globals.css +6 -0
- package/templates/e-commerce/src/app/layout.tsx +15 -0
- package/templates/e-commerce/src/app/page.tsx +82 -0
- package/templates/e-commerce/src/app/products/[slug]/page.tsx +80 -0
- package/templates/e-commerce/src/app/products/page.tsx +72 -0
- package/templates/e-commerce/src/collections/Orders.ts +63 -0
- package/templates/e-commerce/src/collections/Products.ts +50 -0
- package/templates/e-commerce/src/seed.ts +72 -0
- package/templates/e-commerce/tsconfig.json +11 -0
- package/templates/portfolio/.env.example +36 -0
- package/templates/portfolio/_gitignore +26 -0
- package/templates/portfolio/next.config.mjs +10 -0
- package/templates/portfolio/package.json +36 -0
- package/templates/portfolio/postcss.config.mjs +5 -0
- package/templates/portfolio/revealui.config.ts +19 -0
- package/templates/portfolio/src/app/globals.css +6 -0
- package/templates/portfolio/src/app/layout.tsx +15 -0
- package/templates/portfolio/src/app/page.tsx +60 -0
- package/templates/portfolio/src/app/projects/[slug]/page.tsx +95 -0
- package/templates/portfolio/src/app/projects/page.tsx +85 -0
- package/templates/portfolio/src/collections/Projects.ts +49 -0
- package/templates/portfolio/src/seed.ts +73 -0
- package/templates/portfolio/tsconfig.json +11 -0
- package/templates/starter/.env.example +36 -0
- package/templates/starter/_gitignore +26 -0
- package/templates/starter/next.config.mjs +10 -0
- package/templates/starter/package.json +36 -0
- package/templates/starter/postcss.config.mjs +5 -0
- package/templates/starter/revealui.config.ts +18 -0
- package/templates/starter/src/app/globals.css +6 -0
- package/templates/starter/src/app/layout.tsx +15 -0
- package/templates/starter/src/app/page.tsx +18 -0
- package/templates/starter/src/seed.ts +40 -0
- package/templates/starter/tsconfig.json +11 -0
- package/dist/commands/add.d.ts +0 -28
- package/dist/commands/add.d.ts.map +0 -1
- package/dist/commands/add.js +0 -115
- package/dist/commands/check.d.ts +0 -7
- package/dist/commands/check.d.ts.map +0 -1
- package/dist/commands/check.js +0 -34
- package/dist/commands/doctor/checks/build.d.ts +0 -10
- package/dist/commands/doctor/checks/build.d.ts.map +0 -1
- package/dist/commands/doctor/checks/build.js +0 -74
- package/dist/commands/doctor/checks/config.d.ts +0 -14
- package/dist/commands/doctor/checks/config.d.ts.map +0 -1
- package/dist/commands/doctor/checks/config.js +0 -116
- package/dist/commands/doctor/checks/dependencies.d.ts +0 -14
- package/dist/commands/doctor/checks/dependencies.d.ts.map +0 -1
- package/dist/commands/doctor/checks/dependencies.js +0 -126
- package/dist/commands/doctor/checks/practices.d.ts +0 -14
- package/dist/commands/doctor/checks/practices.d.ts.map +0 -1
- package/dist/commands/doctor/checks/practices.js +0 -142
- package/dist/commands/doctor/checks/structure.d.ts +0 -14
- package/dist/commands/doctor/checks/structure.d.ts.map +0 -1
- package/dist/commands/doctor/checks/structure.js +0 -107
- package/dist/commands/doctor/fixes/index.d.ts +0 -26
- package/dist/commands/doctor/fixes/index.d.ts.map +0 -1
- package/dist/commands/doctor/fixes/index.js +0 -108
- package/dist/commands/doctor/index.d.ts +0 -11
- package/dist/commands/doctor/index.d.ts.map +0 -1
- package/dist/commands/doctor/index.js +0 -37
- package/dist/commands/doctor/print.d.ts +0 -6
- package/dist/commands/doctor/print.d.ts.map +0 -1
- package/dist/commands/doctor/print.js +0 -31
- package/dist/commands/doctor/types.d.ts +0 -16
- package/dist/commands/doctor/types.d.ts.map +0 -1
- package/dist/commands/doctor/types.js +0 -1
- package/dist/commands/fix.d.ts +0 -5
- package/dist/commands/fix.d.ts.map +0 -1
- package/dist/commands/fix.js +0 -129
- package/dist/commands/init.d.ts +0 -35
- package/dist/commands/init.d.ts.map +0 -1
- package/dist/commands/init.js +0 -104
- package/dist/commands/upgrade.d.ts +0 -9
- package/dist/commands/upgrade.d.ts.map +0 -1
- package/dist/commands/upgrade.js +0 -85
- package/dist/index.d.ts.map +0 -1
- package/dist/onLoad.d.ts +0 -3
- package/dist/onLoad.d.ts.map +0 -1
- package/dist/onLoad.js +0 -5
- package/dist/utils.d.ts +0 -3
- package/dist/utils.d.ts.map +0 -1
- package/dist/utils.js +0 -6
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const API_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:4000';
|
|
2
|
+
|
|
3
|
+
interface Post {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
content: unknown;
|
|
8
|
+
status: string;
|
|
9
|
+
publishedAt: string | null;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function getPost(slug: string): Promise<Post | null> {
|
|
13
|
+
try {
|
|
14
|
+
const res = await fetch(`${API_URL}/api/posts?where[slug][equals]=${slug}&limit=1`, {
|
|
15
|
+
cache: 'no-store',
|
|
16
|
+
});
|
|
17
|
+
if (!res.ok) return null;
|
|
18
|
+
const data = await res.json();
|
|
19
|
+
return data.docs?.[0] ?? null;
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default async function PostPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
26
|
+
const { slug } = await params;
|
|
27
|
+
const post = await getPost(slug);
|
|
28
|
+
|
|
29
|
+
if (!post) {
|
|
30
|
+
return (
|
|
31
|
+
<main className="mx-auto max-w-2xl px-4 py-16">
|
|
32
|
+
<h1 className="text-2xl font-bold">Post not found</h1>
|
|
33
|
+
<p className="mt-4">
|
|
34
|
+
<a href="/posts" className="text-accent underline">
|
|
35
|
+
Back to blog
|
|
36
|
+
</a>
|
|
37
|
+
</p>
|
|
38
|
+
</main>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<main className="mx-auto max-w-2xl px-4 py-16">
|
|
44
|
+
<nav className="mb-8">
|
|
45
|
+
<a href="/posts" className="text-sm text-accent underline">
|
|
46
|
+
← Back to blog
|
|
47
|
+
</a>
|
|
48
|
+
</nav>
|
|
49
|
+
<article>
|
|
50
|
+
<h1 className="mb-2 text-3xl font-bold">{post.title}</h1>
|
|
51
|
+
{post.publishedAt && (
|
|
52
|
+
<time className="mb-8 block text-sm text-gray-500">
|
|
53
|
+
{new Date(post.publishedAt).toLocaleDateString()}
|
|
54
|
+
</time>
|
|
55
|
+
)}
|
|
56
|
+
<div className="prose">
|
|
57
|
+
{typeof post.content === 'string' ? (
|
|
58
|
+
<p>{post.content}</p>
|
|
59
|
+
) : (
|
|
60
|
+
<p className="text-gray-500">Rich text content will render here.</p>
|
|
61
|
+
)}
|
|
62
|
+
</div>
|
|
63
|
+
</article>
|
|
64
|
+
</main>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const API_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:4000';
|
|
2
|
+
|
|
3
|
+
interface Post {
|
|
4
|
+
id: string;
|
|
5
|
+
title: string;
|
|
6
|
+
slug: string;
|
|
7
|
+
status: string;
|
|
8
|
+
publishedAt: string | null;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function getPosts(): Promise<Post[]> {
|
|
12
|
+
try {
|
|
13
|
+
const res = await fetch(
|
|
14
|
+
`${API_URL}/api/posts?where[status][equals]=published&sort=-publishedAt`,
|
|
15
|
+
{
|
|
16
|
+
cache: 'no-store',
|
|
17
|
+
},
|
|
18
|
+
);
|
|
19
|
+
if (!res.ok) return [];
|
|
20
|
+
const data = await res.json();
|
|
21
|
+
return data.docs ?? [];
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default async function PostsPage() {
|
|
28
|
+
const posts = await getPosts();
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<main className="mx-auto max-w-2xl px-4 py-16">
|
|
32
|
+
<h1 className="mb-8 text-3xl font-bold">Blog</h1>
|
|
33
|
+
|
|
34
|
+
{posts.length === 0 ? (
|
|
35
|
+
<p className="text-gray-500">
|
|
36
|
+
No posts yet. Create your first post in the{' '}
|
|
37
|
+
<a href="/admin/collections/posts" className="text-accent underline">
|
|
38
|
+
admin panel
|
|
39
|
+
</a>
|
|
40
|
+
, or run <code className="rounded bg-gray-100 px-1">pnpm db:seed</code> to add sample
|
|
41
|
+
data.
|
|
42
|
+
</p>
|
|
43
|
+
) : (
|
|
44
|
+
<ul className="space-y-6">
|
|
45
|
+
{posts.map((post) => (
|
|
46
|
+
<li key={post.id}>
|
|
47
|
+
<a href={`/posts/${post.slug}`} className="group block">
|
|
48
|
+
<h2 className="text-xl font-semibold group-hover:text-accent">{post.title}</h2>
|
|
49
|
+
{post.publishedAt && (
|
|
50
|
+
<time className="text-sm text-gray-500">
|
|
51
|
+
{new Date(post.publishedAt).toLocaleDateString()}
|
|
52
|
+
</time>
|
|
53
|
+
)}
|
|
54
|
+
</a>
|
|
55
|
+
</li>
|
|
56
|
+
))}
|
|
57
|
+
</ul>
|
|
58
|
+
)}
|
|
59
|
+
</main>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { CollectionConfig } from '@revealui/contracts';
|
|
2
|
+
|
|
3
|
+
export const Posts: CollectionConfig = {
|
|
4
|
+
slug: 'posts',
|
|
5
|
+
labels: { singular: 'Post', plural: 'Posts' },
|
|
6
|
+
fields: [
|
|
7
|
+
{
|
|
8
|
+
name: 'title',
|
|
9
|
+
type: 'text',
|
|
10
|
+
required: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
name: 'slug',
|
|
14
|
+
type: 'text',
|
|
15
|
+
required: true,
|
|
16
|
+
unique: true,
|
|
17
|
+
admin: {
|
|
18
|
+
description: 'URL-friendly identifier (e.g. "my-first-post")',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
name: 'content',
|
|
23
|
+
type: 'richText',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: 'status',
|
|
27
|
+
type: 'select',
|
|
28
|
+
defaultValue: 'draft',
|
|
29
|
+
options: [
|
|
30
|
+
{ label: 'Draft', value: 'draft' },
|
|
31
|
+
{ label: 'Published', value: 'published' },
|
|
32
|
+
],
|
|
33
|
+
},
|
|
34
|
+
{
|
|
35
|
+
name: 'publishedAt',
|
|
36
|
+
type: 'date',
|
|
37
|
+
admin: {
|
|
38
|
+
description: 'When this post was published',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
],
|
|
42
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Seed script for the basic-blog template.
|
|
3
|
+
* Creates 3 sample blog posts via the RevealUI REST API.
|
|
4
|
+
*
|
|
5
|
+
* Usage: pnpm db:seed (requires the dev server to be running)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const API_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:4000';
|
|
9
|
+
|
|
10
|
+
interface SeedPost {
|
|
11
|
+
title: string;
|
|
12
|
+
slug: string;
|
|
13
|
+
content: string;
|
|
14
|
+
status: string;
|
|
15
|
+
publishedAt: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const posts: SeedPost[] = [
|
|
19
|
+
{
|
|
20
|
+
title: 'Getting Started with RevealUI',
|
|
21
|
+
slug: 'getting-started-with-revealui',
|
|
22
|
+
content:
|
|
23
|
+
'Welcome to your new RevealUI blog! This post was created by the seed script. Edit or delete it from the admin panel at /admin.',
|
|
24
|
+
status: 'published',
|
|
25
|
+
publishedAt: new Date().toISOString(),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
title: 'Customizing Your Blog',
|
|
29
|
+
slug: 'customizing-your-blog',
|
|
30
|
+
content:
|
|
31
|
+
'You can customize your blog by editing the Posts collection in src/collections/Posts.ts. Add new fields, change validation rules, or add hooks to run custom logic.',
|
|
32
|
+
status: 'published',
|
|
33
|
+
publishedAt: new Date(Date.now() - 86_400_000).toISOString(),
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
title: 'Draft Post Example',
|
|
37
|
+
slug: 'draft-post-example',
|
|
38
|
+
content:
|
|
39
|
+
'This is a draft post. It will not appear on the public blog until you change its status to "published" in the admin panel.',
|
|
40
|
+
status: 'draft',
|
|
41
|
+
publishedAt: '',
|
|
42
|
+
},
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
const log = (...args: unknown[]) => process.stdout.write(`${args.join(' ')}\n`);
|
|
46
|
+
const logErr = (...args: unknown[]) => process.stderr.write(`${args.join(' ')}\n`);
|
|
47
|
+
|
|
48
|
+
async function seed(): Promise<void> {
|
|
49
|
+
log(`Seeding blog posts to ${API_URL}...`);
|
|
50
|
+
|
|
51
|
+
for (const post of posts) {
|
|
52
|
+
try {
|
|
53
|
+
const res = await fetch(`${API_URL}/api/posts`, {
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: { 'Content-Type': 'application/json' },
|
|
56
|
+
body: JSON.stringify(post),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
if (res.ok) {
|
|
60
|
+
log(` Created: ${post.title}`);
|
|
61
|
+
} else {
|
|
62
|
+
const error = await res.text();
|
|
63
|
+
logErr(` Failed to create "${post.title}": ${error}`);
|
|
64
|
+
}
|
|
65
|
+
} catch (err) {
|
|
66
|
+
logErr(` Error creating "${post.title}":`, err);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
log('Seeding complete.');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
seed();
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# RevealUI Environment Variables
|
|
2
|
+
# Copy this file to .env.local and fill in your values before running `pnpm dev`
|
|
3
|
+
|
|
4
|
+
# ─── Core ────────────────────────────────────────────────────────────────────
|
|
5
|
+
# 32+ character secret used for signing sessions and tokens
|
|
6
|
+
REVEALUI_SECRET=change-me-to-a-long-random-secret-at-least-32-chars
|
|
7
|
+
|
|
8
|
+
# Public URL of your CMS server (must match NEXT_PUBLIC_SERVER_URL)
|
|
9
|
+
REVEALUI_PUBLIC_SERVER_URL=http://localhost:4000
|
|
10
|
+
NEXT_PUBLIC_SERVER_URL=http://localhost:4000
|
|
11
|
+
|
|
12
|
+
# ─── Database ────────────────────────────────────────────────────────────────
|
|
13
|
+
# PostgreSQL connection string (NeonDB, Supabase, or local Postgres)
|
|
14
|
+
POSTGRES_URL=postgresql://postgres:postgres@localhost:5432/revealui
|
|
15
|
+
|
|
16
|
+
# ─── Storage ─────────────────────────────────────────────────────────────────
|
|
17
|
+
# Vercel Blob token for file uploads (optional — leave placeholder for local dev)
|
|
18
|
+
BLOB_READ_WRITE_TOKEN=vercel_blob_rw_placeholder
|
|
19
|
+
|
|
20
|
+
# ─── Stripe (optional) ───────────────────────────────────────────────────────
|
|
21
|
+
# Use test keys during development: https://dashboard.stripe.com/test/apikeys
|
|
22
|
+
STRIPE_SECRET_KEY=sk_test_placeholder
|
|
23
|
+
STRIPE_WEBHOOK_SECRET=whsec_placeholder
|
|
24
|
+
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_placeholder
|
|
25
|
+
|
|
26
|
+
# ─── Admin Bootstrap ─────────────────────────────────────────────────────────
|
|
27
|
+
# Used on first run only to create the initial admin account
|
|
28
|
+
REVEALUI_ADMIN_EMAIL=admin@example.com
|
|
29
|
+
REVEALUI_ADMIN_PASSWORD=changeme-min-12-chars
|
|
30
|
+
|
|
31
|
+
# ─── Branding (Enterprise white-label) ───────────────────────────────────────
|
|
32
|
+
# Customize the admin UI for your brand. Enterprise license required for full white-label.
|
|
33
|
+
# REVEALUI_BRAND_NAME=My CMS
|
|
34
|
+
# REVEALUI_BRAND_LOGO_URL=https://example.com/logo.png
|
|
35
|
+
# REVEALUI_BRAND_PRIMARY_COLOR=#ea580c
|
|
36
|
+
# REVEALUI_SHOW_POWERED_BY=false
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Dependencies
|
|
2
|
+
node_modules/
|
|
3
|
+
.pnp
|
|
4
|
+
.pnp.js
|
|
5
|
+
|
|
6
|
+
# Next.js
|
|
7
|
+
.next/
|
|
8
|
+
out/
|
|
9
|
+
build/
|
|
10
|
+
|
|
11
|
+
# Environment variables
|
|
12
|
+
.env
|
|
13
|
+
.env.local
|
|
14
|
+
.env.development.local
|
|
15
|
+
.env.test.local
|
|
16
|
+
.env.production.local
|
|
17
|
+
|
|
18
|
+
# Turbo
|
|
19
|
+
.turbo/
|
|
20
|
+
|
|
21
|
+
# Misc
|
|
22
|
+
.DS_Store
|
|
23
|
+
*.pem
|
|
24
|
+
npm-debug.log*
|
|
25
|
+
yarn-debug.log*
|
|
26
|
+
yarn-error.log*
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev --port 4000",
|
|
8
|
+
"build": "next build",
|
|
9
|
+
"start": "next start --port 4000",
|
|
10
|
+
"lint": "biome check .",
|
|
11
|
+
"typecheck": "tsc --noEmit",
|
|
12
|
+
"test": "vitest run",
|
|
13
|
+
"db:init": "revealui cms",
|
|
14
|
+
"db:migrate": "drizzle-kit migrate",
|
|
15
|
+
"db:seed": "tsx src/seed.ts"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@revealui/core": "^0.2.0",
|
|
19
|
+
"@revealui/config": "^0.2.0",
|
|
20
|
+
"@revealui/db": "^0.2.0",
|
|
21
|
+
"@revealui/auth": "^0.2.0",
|
|
22
|
+
"@revealui/contracts": "^1.1.0",
|
|
23
|
+
"next": "^16.0.0",
|
|
24
|
+
"react": "^19.0.0",
|
|
25
|
+
"react-dom": "^19.0.0",
|
|
26
|
+
"sharp": "^0.34.0",
|
|
27
|
+
"zod": "^4.0.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@revealui/dev": "^0.0.1",
|
|
31
|
+
"@tailwindcss/postcss": "^4.1.0",
|
|
32
|
+
"tailwindcss": "^4.1.0",
|
|
33
|
+
"typescript": "^5.9.0",
|
|
34
|
+
"vitest": "^4.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import config from '@revealui/config';
|
|
2
|
+
import { buildConfig, universalPostgresAdapter } from '@revealui/core';
|
|
3
|
+
import sharp from 'sharp';
|
|
4
|
+
import { Orders } from './src/collections/Orders';
|
|
5
|
+
import { Products } from './src/collections/Products';
|
|
6
|
+
|
|
7
|
+
export default buildConfig({
|
|
8
|
+
serverURL: config.reveal.publicServerURL || 'http://localhost:4000',
|
|
9
|
+
secret: config.reveal.secret,
|
|
10
|
+
db: config.database.url
|
|
11
|
+
? universalPostgresAdapter({ connectionString: config.database.url })
|
|
12
|
+
: universalPostgresAdapter({ provider: 'electric' }),
|
|
13
|
+
admin: {
|
|
14
|
+
user: 'users',
|
|
15
|
+
},
|
|
16
|
+
collections: [Products, Orders],
|
|
17
|
+
globals: [],
|
|
18
|
+
plugins: [],
|
|
19
|
+
sharp,
|
|
20
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { Metadata } from 'next';
|
|
2
|
+
import './globals.css';
|
|
3
|
+
|
|
4
|
+
export const metadata: Metadata = {
|
|
5
|
+
title: 'RevealUI App',
|
|
6
|
+
description: 'Built with RevealUI',
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
10
|
+
return (
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<body>{children}</body>
|
|
13
|
+
</html>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import Link from 'next/link';
|
|
2
|
+
|
|
3
|
+
export default function HomePage() {
|
|
4
|
+
return (
|
|
5
|
+
<main className="mx-auto max-w-4xl px-4 py-16">
|
|
6
|
+
<div className="text-center">
|
|
7
|
+
<h1 className="text-4xl font-bold tracking-tight text-gray-900 sm:text-5xl">
|
|
8
|
+
Your store is live
|
|
9
|
+
</h1>
|
|
10
|
+
<p className="mx-auto mt-4 max-w-xl text-lg text-gray-600">
|
|
11
|
+
Built with{' '}
|
|
12
|
+
<a
|
|
13
|
+
href="https://revealui.com"
|
|
14
|
+
className="font-medium text-accent hover:text-accent-hover"
|
|
15
|
+
target="_blank"
|
|
16
|
+
rel="noopener noreferrer"
|
|
17
|
+
>
|
|
18
|
+
RevealUI
|
|
19
|
+
</a>
|
|
20
|
+
. Add products from the admin panel, configure Stripe for payments, and start selling.
|
|
21
|
+
</p>
|
|
22
|
+
|
|
23
|
+
<div className="mt-8 flex justify-center gap-3">
|
|
24
|
+
<Link
|
|
25
|
+
href="/products"
|
|
26
|
+
className="rounded-lg bg-accent px-5 py-2.5 text-sm font-medium text-white transition-colors hover:bg-accent-hover"
|
|
27
|
+
>
|
|
28
|
+
Browse products
|
|
29
|
+
</Link>
|
|
30
|
+
<a
|
|
31
|
+
href="/admin"
|
|
32
|
+
className="rounded-lg border border-gray-300 px-5 py-2.5 text-sm font-medium text-gray-700 transition-colors hover:bg-gray-50"
|
|
33
|
+
>
|
|
34
|
+
Admin panel
|
|
35
|
+
</a>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
|
|
39
|
+
<div className="mt-16 grid gap-6 sm:grid-cols-3">
|
|
40
|
+
<div className="rounded-lg border border-gray-200 p-5">
|
|
41
|
+
<h3 className="font-semibold text-gray-900">Products</h3>
|
|
42
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
43
|
+
Manage your catalog with custom fields, images, and pricing.
|
|
44
|
+
</p>
|
|
45
|
+
</div>
|
|
46
|
+
<div className="rounded-lg border border-gray-200 p-5">
|
|
47
|
+
<h3 className="font-semibold text-gray-900">Payments</h3>
|
|
48
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
49
|
+
Stripe integration for checkout, subscriptions, and webhooks.
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
<div className="rounded-lg border border-gray-200 p-5">
|
|
53
|
+
<h3 className="font-semibold text-gray-900">Orders</h3>
|
|
54
|
+
<p className="mt-1 text-sm text-gray-600">
|
|
55
|
+
Track and manage orders with status updates and fulfillment.
|
|
56
|
+
</p>
|
|
57
|
+
</div>
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<div className="mt-12 rounded-lg border border-gray-200 bg-gray-50 p-6">
|
|
61
|
+
<h2 className="text-sm font-semibold uppercase tracking-wide text-gray-500">Quick start</h2>
|
|
62
|
+
<ol className="mt-3 space-y-2 text-sm text-gray-700">
|
|
63
|
+
<li>
|
|
64
|
+
<code className="rounded bg-gray-200 px-1.5 py-0.5 text-xs">pnpm db:seed</code> — add
|
|
65
|
+
sample products
|
|
66
|
+
</li>
|
|
67
|
+
<li>
|
|
68
|
+
Add your Stripe keys to{' '}
|
|
69
|
+
<code className="rounded bg-gray-200 px-1.5 py-0.5 text-xs">.env.local</code>
|
|
70
|
+
</li>
|
|
71
|
+
<li>
|
|
72
|
+
Visit{' '}
|
|
73
|
+
<a href="/admin" className="text-accent hover:text-accent-hover">
|
|
74
|
+
/admin
|
|
75
|
+
</a>{' '}
|
|
76
|
+
— manage products and orders
|
|
77
|
+
</li>
|
|
78
|
+
</ol>
|
|
79
|
+
</div>
|
|
80
|
+
</main>
|
|
81
|
+
);
|
|
82
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import Image from 'next/image';
|
|
2
|
+
|
|
3
|
+
const API_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:4000';
|
|
4
|
+
|
|
5
|
+
interface Product {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
description: unknown;
|
|
10
|
+
price: number;
|
|
11
|
+
status: string;
|
|
12
|
+
image?: { url: string; alt?: string } | null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function formatPrice(cents: number): string {
|
|
16
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function getProduct(slug: string): Promise<Product | null> {
|
|
20
|
+
try {
|
|
21
|
+
const res = await fetch(`${API_URL}/api/products?where[slug][equals]=${slug}&limit=1`, {
|
|
22
|
+
cache: 'no-store',
|
|
23
|
+
});
|
|
24
|
+
if (!res.ok) return null;
|
|
25
|
+
const data = await res.json();
|
|
26
|
+
return data.docs?.[0] ?? null;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) {
|
|
33
|
+
const { slug } = await params;
|
|
34
|
+
const product = await getProduct(slug);
|
|
35
|
+
|
|
36
|
+
if (!product) {
|
|
37
|
+
return (
|
|
38
|
+
<main className="mx-auto max-w-2xl px-4 py-16">
|
|
39
|
+
<h1 className="text-2xl font-bold">Product not found</h1>
|
|
40
|
+
<p className="mt-4">
|
|
41
|
+
<a href="/products" className="text-accent underline">
|
|
42
|
+
Back to products
|
|
43
|
+
</a>
|
|
44
|
+
</p>
|
|
45
|
+
</main>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<main className="mx-auto max-w-2xl px-4 py-16">
|
|
51
|
+
<nav className="mb-8">
|
|
52
|
+
<a href="/products" className="text-sm text-accent underline">
|
|
53
|
+
← Back to products
|
|
54
|
+
</a>
|
|
55
|
+
</nav>
|
|
56
|
+
<div className="grid grid-cols-1 gap-8 md:grid-cols-2">
|
|
57
|
+
{product.image?.url && (
|
|
58
|
+
<Image
|
|
59
|
+
src={product.image.url}
|
|
60
|
+
alt={product.image.alt || product.name}
|
|
61
|
+
width={600}
|
|
62
|
+
height={600}
|
|
63
|
+
className="aspect-square w-full rounded-lg object-cover"
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
<div>
|
|
67
|
+
<h1 className="text-3xl font-bold">{product.name}</h1>
|
|
68
|
+
<p className="mt-2 text-2xl font-bold text-gray-900">{formatPrice(product.price)}</p>
|
|
69
|
+
<div className="prose mt-6">
|
|
70
|
+
{typeof product.description === 'string' ? (
|
|
71
|
+
<p>{product.description}</p>
|
|
72
|
+
) : (
|
|
73
|
+
<p className="text-gray-500">Product description will render here.</p>
|
|
74
|
+
)}
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
</main>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import Image from 'next/image';
|
|
2
|
+
|
|
3
|
+
const API_URL = process.env.NEXT_PUBLIC_SERVER_URL || 'http://localhost:4000';
|
|
4
|
+
|
|
5
|
+
interface Product {
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
slug: string;
|
|
9
|
+
price: number;
|
|
10
|
+
status: string;
|
|
11
|
+
image?: { url: string; alt?: string } | null;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function formatPrice(cents: number): string {
|
|
15
|
+
return `$${(cents / 100).toFixed(2)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function getProducts(): Promise<Product[]> {
|
|
19
|
+
try {
|
|
20
|
+
const res = await fetch(`${API_URL}/api/products?where[status][equals]=active&sort=name`, {
|
|
21
|
+
cache: 'no-store',
|
|
22
|
+
});
|
|
23
|
+
if (!res.ok) return [];
|
|
24
|
+
const data = await res.json();
|
|
25
|
+
return data.docs ?? [];
|
|
26
|
+
} catch {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export default async function ProductsPage() {
|
|
32
|
+
const products = await getProducts();
|
|
33
|
+
|
|
34
|
+
return (
|
|
35
|
+
<main className="mx-auto max-w-4xl px-4 py-16">
|
|
36
|
+
<h1 className="mb-8 text-3xl font-bold">Products</h1>
|
|
37
|
+
|
|
38
|
+
{products.length === 0 ? (
|
|
39
|
+
<p className="text-gray-500">
|
|
40
|
+
No products yet. Add products in the{' '}
|
|
41
|
+
<a href="/admin/collections/products" className="text-accent underline">
|
|
42
|
+
admin panel
|
|
43
|
+
</a>
|
|
44
|
+
, or run <code className="rounded bg-gray-100 px-1">pnpm db:seed</code> to add sample
|
|
45
|
+
data.
|
|
46
|
+
</p>
|
|
47
|
+
) : (
|
|
48
|
+
<div className="grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
49
|
+
{products.map((product) => (
|
|
50
|
+
<a
|
|
51
|
+
key={product.id}
|
|
52
|
+
href={`/products/${product.slug}`}
|
|
53
|
+
className="group rounded-lg border border-gray-200 p-4 transition-shadow hover:shadow-md"
|
|
54
|
+
>
|
|
55
|
+
{product.image?.url && (
|
|
56
|
+
<Image
|
|
57
|
+
src={product.image.url}
|
|
58
|
+
alt={product.image.alt || product.name}
|
|
59
|
+
width={400}
|
|
60
|
+
height={400}
|
|
61
|
+
className="mb-4 aspect-square w-full rounded object-cover"
|
|
62
|
+
/>
|
|
63
|
+
)}
|
|
64
|
+
<h2 className="font-semibold group-hover:text-accent">{product.name}</h2>
|
|
65
|
+
<p className="mt-1 text-lg font-bold text-gray-900">{formatPrice(product.price)}</p>
|
|
66
|
+
</a>
|
|
67
|
+
))}
|
|
68
|
+
</div>
|
|
69
|
+
)}
|
|
70
|
+
</main>
|
|
71
|
+
);
|
|
72
|
+
}
|