create-nextly-app 0.0.2-alpha.42 → 0.0.2-alpha.43
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/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/cli.mjs.map +1 -1
- package/package.json +4 -4
- package/templates/base/.env.example +16 -0
- package/templates/base/README.md +30 -0
- package/templates/base/eslint.config.mjs +18 -0
- package/templates/base/next.config.ts +15 -0
- package/templates/base/postcss.config.mjs +7 -0
- package/templates/base/public/file.svg +1 -0
- package/templates/base/public/globe.svg +1 -0
- package/templates/base/public/next.svg +1 -0
- package/templates/base/public/vercel.svg +1 -0
- package/templates/base/public/window.svg +1 -0
- package/templates/base/src/app/admin/[[...params]]/layout.tsx +20 -0
- package/templates/base/src/app/admin/[[...params]]/page.tsx +18 -0
- package/templates/base/src/app/admin/api/[[...params]]/route.ts +12 -0
- package/templates/base/src/app/admin/api/media/[[...path]]/route.ts +26 -0
- package/templates/base/src/app/api/health/route.ts +7 -0
- package/templates/base/src/app/api/media/[[...path]]/route.ts +34 -0
- package/templates/base/src/app/favicon.ico +0 -0
- package/templates/base/src/app/globals.css +55 -0
- package/templates/base/src/app/layout.tsx +43 -0
- package/templates/base/src/app/page.tsx +65 -0
- package/templates/base/src/types/generated/.gitkeep +0 -0
- package/templates/base/src/types/generated/nextly-types.ts +12 -0
- package/templates/base/tsconfig.json +36 -0
- package/templates/blank/.env.example +8 -0
- package/templates/blank/README.md +63 -0
- package/templates/blank/nextly.config.ts +14 -0
- package/templates/blank/src/app/(frontend)/components/README.md +10 -0
- package/templates/blank/src/app/(frontend)/components/ThemeToggle.tsx +113 -0
- package/templates/blank/src/app/(frontend)/page.tsx +176 -0
- package/templates/blank/src/app/globals.css +79 -0
- package/templates/blank/src/app/layout.tsx +46 -0
- package/templates/blank/template.json +14 -0
- package/templates/plugin/README.md +44 -0
- package/templates/plugin/dev/.env.example +8 -0
- package/templates/plugin/dev/instrumentation.ts +10 -0
- package/templates/plugin/dev/next.config.ts +30 -0
- package/templates/plugin/dev/nextly.config.ts +17 -0
- package/templates/plugin/dev/src/app/admin/[[...params]]/page.tsx +9 -0
- package/templates/plugin/dev/src/app/admin/api/[[...params]]/route.ts +19 -0
- package/templates/plugin/dev/src/app/layout.tsx +13 -0
- package/templates/plugin/dev/tsconfig.json +22 -0
- package/templates/plugin/eslint.config.mjs +8 -0
- package/templates/plugin/src/admin/SettingsPage.tsx +13 -0
- package/templates/plugin/src/admin/index.ts +21 -0
- package/templates/plugin/src/collections/example.ts +13 -0
- package/templates/plugin/src/index.ts +6 -0
- package/templates/plugin/src/plugin.test.ts +29 -0
- package/templates/plugin/src/plugin.ts +48 -0
- package/templates/plugin/src/types.ts +17 -0
- package/templates/plugin/template.json +14 -0
- package/templates/plugin/tsconfig.json +18 -0
- package/templates/plugin/tsup.config.ts +22 -0
- package/templates/plugin/vitest.config.ts +8 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
4
|
+
|
|
5
|
+
type Theme = "light" | "dark" | "system";
|
|
6
|
+
|
|
7
|
+
export function ThemeToggle() {
|
|
8
|
+
const [theme, setTheme] = useState<Theme>("system");
|
|
9
|
+
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
const savedTheme = localStorage.getItem("theme") as Theme | null;
|
|
12
|
+
if (savedTheme) {
|
|
13
|
+
setTheme(savedTheme);
|
|
14
|
+
applyTheme(savedTheme);
|
|
15
|
+
} else {
|
|
16
|
+
applyTheme("system");
|
|
17
|
+
}
|
|
18
|
+
}, []);
|
|
19
|
+
|
|
20
|
+
const applyTheme = (newTheme: Theme) => {
|
|
21
|
+
const isDark =
|
|
22
|
+
newTheme === "dark" ||
|
|
23
|
+
(newTheme === "system" &&
|
|
24
|
+
window.matchMedia("(prefers-color-scheme: dark)").matches);
|
|
25
|
+
|
|
26
|
+
document.documentElement.classList.toggle("dark", isDark);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const handleThemeChange = (newTheme: Theme) => {
|
|
30
|
+
setTheme(newTheme);
|
|
31
|
+
localStorage.setItem("theme", newTheme);
|
|
32
|
+
applyTheme(newTheme);
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<div className="fixed top-6 right-6 flex items-center border border-foreground/10 bg-background p-1 z-50">
|
|
37
|
+
<button
|
|
38
|
+
onClick={() => handleThemeChange("light")}
|
|
39
|
+
className={`p-1.5 transition-colors cursor-pointer ${
|
|
40
|
+
theme === "light"
|
|
41
|
+
? "text-foreground bg-foreground/5"
|
|
42
|
+
: "text-slate-400 hover:text-foreground"
|
|
43
|
+
}`}
|
|
44
|
+
aria-label="Light theme"
|
|
45
|
+
>
|
|
46
|
+
<svg
|
|
47
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
48
|
+
fill="none"
|
|
49
|
+
viewBox="0 0 24 24"
|
|
50
|
+
strokeWidth={2}
|
|
51
|
+
stroke="currentColor"
|
|
52
|
+
className="w-4 h-4"
|
|
53
|
+
>
|
|
54
|
+
<path
|
|
55
|
+
strokeLinecap="round"
|
|
56
|
+
strokeLinejoin="round"
|
|
57
|
+
d="M12 3v2.25m6.364.386l-1.591 1.591M21 12h-2.25m-.386 6.364l-1.591-1.591M12 18.75V21m-4.773-4.227l-1.591 1.591M3 12h2.25m.386-6.364l1.591 1.591M15.75 12a3.75 3.75 0 11-7.5 0 3.75 3.75 0 017.5 0z"
|
|
58
|
+
/>
|
|
59
|
+
</svg>
|
|
60
|
+
</button>
|
|
61
|
+
|
|
62
|
+
<button
|
|
63
|
+
onClick={() => handleThemeChange("system")}
|
|
64
|
+
className={`p-1.5 transition-colors cursor-pointer ${
|
|
65
|
+
theme === "system"
|
|
66
|
+
? "text-foreground bg-foreground/5"
|
|
67
|
+
: "text-slate-400 hover:text-foreground"
|
|
68
|
+
}`}
|
|
69
|
+
aria-label="System theme"
|
|
70
|
+
>
|
|
71
|
+
<svg
|
|
72
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
73
|
+
fill="none"
|
|
74
|
+
viewBox="0 0 24 24"
|
|
75
|
+
strokeWidth={2}
|
|
76
|
+
stroke="currentColor"
|
|
77
|
+
className="w-4 h-4"
|
|
78
|
+
>
|
|
79
|
+
<path
|
|
80
|
+
strokeLinecap="round"
|
|
81
|
+
strokeLinejoin="round"
|
|
82
|
+
d="M9 17.25v1.007a3 3 0 01-.879 2.122L7.5 21h9l-.621-.621A3 3 0 0115 18.257V17.25m6-12V15a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 15V5.25m18 0A2.25 2.25 0 0018.75 3H5.25A2.25 2.25 0 003 5.25m18 0V12a2.25 2.25 0 01-2.25 2.25H5.25A2.25 2.25 0 013 12V5.25"
|
|
83
|
+
/>
|
|
84
|
+
</svg>
|
|
85
|
+
</button>
|
|
86
|
+
|
|
87
|
+
<button
|
|
88
|
+
onClick={() => handleThemeChange("dark")}
|
|
89
|
+
className={`p-1.5 transition-colors cursor-pointer ${
|
|
90
|
+
theme === "dark"
|
|
91
|
+
? "text-foreground bg-foreground/5"
|
|
92
|
+
: "text-slate-400 hover:text-foreground"
|
|
93
|
+
}`}
|
|
94
|
+
aria-label="Dark theme"
|
|
95
|
+
>
|
|
96
|
+
<svg
|
|
97
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
98
|
+
fill="none"
|
|
99
|
+
viewBox="0 0 24 24"
|
|
100
|
+
strokeWidth={2}
|
|
101
|
+
stroke="currentColor"
|
|
102
|
+
className="w-4 h-4"
|
|
103
|
+
>
|
|
104
|
+
<path
|
|
105
|
+
strokeLinecap="round"
|
|
106
|
+
strokeLinejoin="round"
|
|
107
|
+
d="M21.752 15.002A9.718 9.718 0 0118 15.75c-5.385 0-9.75-4.365-9.75-9.75 0-1.33.266-2.597.748-3.752A9.753 9.753 0 003 11.25C3 16.635 7.365 21 12.75 21a9.753 9.753 0 009.002-5.998z"
|
|
108
|
+
/>
|
|
109
|
+
</svg>
|
|
110
|
+
</button>
|
|
111
|
+
</div>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
import { ThemeToggle } from "@/components/ThemeToggle";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Blank-template landing page (public route via the (frontend) route group).
|
|
7
|
+
*
|
|
8
|
+
* Replaces the inherited Next.js placeholder at `/` with a minimal,
|
|
9
|
+
* brutalist-edge welcome page that points the developer at the admin
|
|
10
|
+
* panel and the docs. Server Component — fetches setup-status so the
|
|
11
|
+
* primary button label flips between "Set up admin" (no super-admin
|
|
12
|
+
* yet) and "Open admin" (super-admin exists).
|
|
13
|
+
*
|
|
14
|
+
* If the auth API fails for any reason (DB not migrated, network),
|
|
15
|
+
* the button defaults to "Open admin" / /admin since /admin already
|
|
16
|
+
* redirects to /admin/setup when no super-admin exists — the link
|
|
17
|
+
* works either way.
|
|
18
|
+
*
|
|
19
|
+
* Edit this file in src/app/(frontend)/page.tsx after scaffolding to customise.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
interface SetupStatus {
|
|
23
|
+
isSetup: boolean;
|
|
24
|
+
requiresInitialUser: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchSetupStatus(): Promise<SetupStatus | null> {
|
|
28
|
+
try {
|
|
29
|
+
const baseUrl = process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000";
|
|
30
|
+
const res = await fetch(`${baseUrl}/api/auth/setup-status`, {
|
|
31
|
+
cache: "no-store",
|
|
32
|
+
});
|
|
33
|
+
if (!res.ok) return null;
|
|
34
|
+
return (await res.json()) as SetupStatus;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export default async function HomePage() {
|
|
41
|
+
const status = await fetchSetupStatus();
|
|
42
|
+
const needsSetup = status?.requiresInitialUser ?? false;
|
|
43
|
+
const adminLabel = needsSetup ? "Set up admin" : "Open admin";
|
|
44
|
+
const adminHref = needsSetup ? "/admin/setup" : "/admin";
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<main className="relative min-h-screen flex flex-col items-center justify-center p-6 bg-background text-foreground font-display selection:bg-foreground/10">
|
|
48
|
+
<ThemeToggle />
|
|
49
|
+
<div className="max-w-[640px] w-full text-center">
|
|
50
|
+
<div className="flex items-center justify-center mb-8">
|
|
51
|
+
<svg
|
|
52
|
+
width="180"
|
|
53
|
+
height="48"
|
|
54
|
+
viewBox="0 0 235 63"
|
|
55
|
+
fill="none"
|
|
56
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
57
|
+
className="text-foreground"
|
|
58
|
+
>
|
|
59
|
+
<rect width="62" height="63" fill="currentColor" />
|
|
60
|
+
<path
|
|
61
|
+
d="M18 46.7034L24.6253 43.6455V26.6564L32.9494 30.5448V23.26L18 16.21V46.7034Z"
|
|
62
|
+
fill="var(--color-background)"
|
|
63
|
+
/>
|
|
64
|
+
<path
|
|
65
|
+
d="M44 16.2102L37.3747 19.268V36.2905L29.035 32.4021L29.0506 39.6536L44 46.7036V16.2102Z"
|
|
66
|
+
fill="var(--color-background)"
|
|
67
|
+
/>
|
|
68
|
+
<path
|
|
69
|
+
d="M116.233 11.0908V45.9999H109.858L94.6705 24.0283H94.4148V45.9999H87.0341V11.0908H93.5114L108.58 33.0454H108.886V11.0908H116.233ZM134.126 46.5113C131.433 46.5113 129.115 45.9658 127.172 44.8749C125.24 43.7726 123.751 42.2158 122.706 40.2044C121.661 38.1817 121.138 35.7897 121.138 33.0283C121.138 30.3351 121.661 27.9715 122.706 25.9374C123.751 23.9033 125.223 22.3181 127.121 21.1817C129.03 20.0454 131.268 19.4772 133.837 19.4772C135.564 19.4772 137.172 19.7556 138.661 20.3124C140.161 20.8579 141.467 21.6817 142.581 22.784C143.706 23.8863 144.581 25.2726 145.206 26.9431C145.831 28.6022 146.143 30.5454 146.143 32.7726V34.7669H124.036V30.2669H139.308C139.308 29.2215 139.081 28.2954 138.626 27.4885C138.172 26.6817 137.541 26.051 136.734 25.5965C135.939 25.1306 135.013 24.8976 133.956 24.8976C132.854 24.8976 131.876 25.1533 131.024 25.6647C130.183 26.1647 129.524 26.8408 129.047 27.6931C128.57 28.534 128.325 29.4715 128.314 30.5056V34.784C128.314 36.0794 128.553 37.1988 129.03 38.1419C129.518 39.0851 130.206 39.8124 131.092 40.3238C131.979 40.8351 133.03 41.0908 134.246 41.0908C135.053 41.0908 135.791 40.9772 136.462 40.7499C137.132 40.5226 137.706 40.1817 138.183 39.7272C138.661 39.2726 139.024 38.7158 139.274 38.0567L145.99 38.4999C145.649 40.1135 144.95 41.5226 143.893 42.7272C142.848 43.9204 141.496 44.8522 139.837 45.5226C138.189 46.1817 136.286 46.5113 134.126 46.5113ZM156.482 19.8181L161.288 28.9715L166.214 19.8181H173.663L166.078 32.909L173.868 45.9999H166.453L161.288 36.9488L156.209 45.9999H148.709L156.482 32.909L148.982 19.8181H156.482ZM191.902 19.8181V25.2726H176.135V19.8181H191.902ZM179.714 13.5454H186.976V37.9544C186.976 38.6249 187.078 39.1476 187.283 39.5226C187.487 39.8863 187.771 40.1419 188.135 40.2897C188.51 40.4374 188.942 40.5113 189.43 40.5113C189.771 40.5113 190.112 40.4829 190.453 40.426C190.794 40.3579 191.055 40.3067 191.237 40.2726L192.379 45.676C192.016 45.7897 191.504 45.9204 190.845 46.0681C190.186 46.2272 189.385 46.3238 188.442 46.3579C186.692 46.426 185.158 46.1931 183.839 45.659C182.533 45.1249 181.516 44.2954 180.788 43.1704C180.061 42.0454 179.703 40.6249 179.714 38.909V13.5454ZM204.456 11.0908V45.9999H197.195V11.0908H204.456ZM214.756 55.8181C213.835 55.8181 212.972 55.7442 212.165 55.5965C211.369 55.4601 210.71 55.284 210.188 55.0681L211.824 49.6476C212.676 49.909 213.443 50.051 214.125 50.0738C214.818 50.0965 215.415 49.9374 215.915 49.5965C216.426 49.2556 216.841 48.676 217.159 47.8579L217.585 46.7499L208.193 19.8181H215.83L221.25 39.0454H221.523L226.994 19.8181H234.682L224.506 48.8294C224.017 50.2385 223.352 51.4658 222.511 52.5113C221.682 53.5681 220.631 54.3806 219.358 54.9488C218.085 55.5283 216.551 55.8181 214.756 55.8181Z"
|
|
70
|
+
fill="currentColor"
|
|
71
|
+
/>
|
|
72
|
+
</svg>
|
|
73
|
+
<span className="ml-4 font-mono text-[10px] font-semibold tracking-wide uppercase px-2 py-0.5 bg-foreground/5 text-slate-500">
|
|
74
|
+
Alpha
|
|
75
|
+
</span>
|
|
76
|
+
</div>
|
|
77
|
+
|
|
78
|
+
<h1 className="mx-auto font-semibold tracking-[-0.03em] leading-[1.1] text-[clamp(24px,5vw,32px)]">
|
|
79
|
+
Welcome to your new Nextly project.
|
|
80
|
+
</h1>
|
|
81
|
+
|
|
82
|
+
<p className="mt-6 mx-auto text-slate-500 dark:text-slate-400 max-w-[42ch] text-[clamp(16px,1.8vw,18px)] leading-[1.6]">
|
|
83
|
+
This is the blank template. Open the admin to create your first
|
|
84
|
+
collection or read the docs to see what Nextly can do.
|
|
85
|
+
</p>
|
|
86
|
+
|
|
87
|
+
<div className="mt-10 flex flex-wrap items-center justify-center gap-4">
|
|
88
|
+
<Link href={adminHref} className="btn-primary">
|
|
89
|
+
<span>{adminLabel}</span>
|
|
90
|
+
<svg
|
|
91
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
92
|
+
width="18"
|
|
93
|
+
height="18"
|
|
94
|
+
viewBox="0 0 24 24"
|
|
95
|
+
fill="none"
|
|
96
|
+
stroke="currentColor"
|
|
97
|
+
strokeWidth="2"
|
|
98
|
+
strokeLinecap="round"
|
|
99
|
+
strokeLinejoin="round"
|
|
100
|
+
className="lucide lucide-arrow-right"
|
|
101
|
+
>
|
|
102
|
+
<path d="M5 12h14" />
|
|
103
|
+
<path d="m12 5 7 7-7 7" />
|
|
104
|
+
</svg>
|
|
105
|
+
</Link>
|
|
106
|
+
<Link href="https://nextlyhq.com/docs" className="btn-secondary">
|
|
107
|
+
<span>Read documentation</span>
|
|
108
|
+
<svg
|
|
109
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
110
|
+
width="18"
|
|
111
|
+
height="18"
|
|
112
|
+
viewBox="0 0 24 24"
|
|
113
|
+
fill="none"
|
|
114
|
+
stroke="currentColor"
|
|
115
|
+
strokeWidth="2"
|
|
116
|
+
strokeLinecap="round"
|
|
117
|
+
strokeLinejoin="round"
|
|
118
|
+
className="lucide lucide-arrow-up-right"
|
|
119
|
+
>
|
|
120
|
+
<path d="M7 7h10v10" />
|
|
121
|
+
<path d="M7 17 17 7" />
|
|
122
|
+
</svg>
|
|
123
|
+
</Link>
|
|
124
|
+
</div>
|
|
125
|
+
|
|
126
|
+
<div className="mt-16 pt-8 border-t border-foreground/5 flex flex-col sm:flex-row items-center justify-between gap-6">
|
|
127
|
+
<p className="font-mono text-[11px] tracking-tight text-slate-500">
|
|
128
|
+
Edit in{" "}
|
|
129
|
+
<span className="text-foreground">src/app/(frontend)/page.tsx</span>
|
|
130
|
+
</p>
|
|
131
|
+
<div className="flex gap-6">
|
|
132
|
+
<Link
|
|
133
|
+
href="https://github.com/nextlyhq/nextly"
|
|
134
|
+
className="flex items-center gap-2 text-[12px] font-medium text-slate-500 hover:text-foreground transition-colors"
|
|
135
|
+
>
|
|
136
|
+
<svg
|
|
137
|
+
aria-hidden="true"
|
|
138
|
+
focusable="false"
|
|
139
|
+
viewBox="0 0 24 24"
|
|
140
|
+
width="16"
|
|
141
|
+
height="16"
|
|
142
|
+
fill="currentColor"
|
|
143
|
+
style={{ verticalAlign: "text-bottom" }}
|
|
144
|
+
>
|
|
145
|
+
<path d="M10.226 17.284c-2.965-.36-5.054-2.493-5.054-5.256 0-1.123.404-2.336 1.078-3.144-.292-.741-.247-2.314.09-2.965.898-.112 2.111.36 2.83 1.01.853-.269 1.752-.404 2.853-.404 1.1 0 1.999.135 2.807.382.696-.629 1.932-1.1 2.83-.988.315.606.36 2.179.067 2.942.72.854 1.101 2 1.101 3.167 0 2.763-2.089 4.852-5.098 5.234.763.494 1.28 1.572 1.28 2.807v2.336c0 .674.561 1.056 1.235.786 4.066-1.55 7.255-5.615 7.255-10.646C23.5 6.188 18.334 1 11.978 1 5.62 1 .5 6.188.5 12.545c0 4.986 3.167 9.12 7.435 10.669.606.225 1.19-.18 1.19-.786V20.63a2.9 2.9 0 0 1-1.078.224c-1.483 0-2.359-.808-2.987-2.313-.247-.607-.517-.966-1.034-1.033-.27-.023-.359-.135-.359-.27 0-.27.45-.471.898-.471.652 0 1.213.404 1.797 1.235.45.651.921.943 1.483.943.561 0 .92-.202 1.437-.719.382-.381.674-.718.944-.943"></path>
|
|
146
|
+
</svg>
|
|
147
|
+
<span>GitHub</span>
|
|
148
|
+
</Link>
|
|
149
|
+
<Link
|
|
150
|
+
href="https://nextlyhq.com"
|
|
151
|
+
className="flex items-center gap-2 text-[12px] font-medium text-slate-500 hover:text-foreground transition-colors"
|
|
152
|
+
>
|
|
153
|
+
<svg
|
|
154
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
155
|
+
width="16"
|
|
156
|
+
height="16"
|
|
157
|
+
viewBox="0 0 24 24"
|
|
158
|
+
fill="none"
|
|
159
|
+
stroke="currentColor"
|
|
160
|
+
strokeWidth="2"
|
|
161
|
+
strokeLinecap="round"
|
|
162
|
+
strokeLinejoin="round"
|
|
163
|
+
className="lucide lucide-globe"
|
|
164
|
+
>
|
|
165
|
+
<circle cx="12" cy="12" r="10" />
|
|
166
|
+
<path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20" />
|
|
167
|
+
<path d="M2 12h20" />
|
|
168
|
+
</svg>
|
|
169
|
+
<span>Website</span>
|
|
170
|
+
</Link>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
</main>
|
|
175
|
+
);
|
|
176
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Blank-template global styles.
|
|
5
|
+
*
|
|
6
|
+
* Overrides templates/base/src/app/globals.css so the branded landing
|
|
7
|
+
* page renders with its supporting decorations: a subtle 56px grid
|
|
8
|
+
* backdrop, the corner brackets, the pulsing "Localhost / Development"
|
|
9
|
+
* dot, and a hover-invert helper for the secondary button.
|
|
10
|
+
*
|
|
11
|
+
* Tailwind base tokens still apply — these rules are additive.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
:root {
|
|
15
|
+
--color-background: #f9fafb;
|
|
16
|
+
--color-foreground: #0f172a;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@media (prefers-color-scheme: dark) {
|
|
20
|
+
:root {
|
|
21
|
+
--color-background: #0b0f19;
|
|
22
|
+
--color-foreground: #f9fafb;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.dark {
|
|
27
|
+
--color-background: #0b0f19;
|
|
28
|
+
--color-foreground: #f9fafb;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@theme {
|
|
32
|
+
--color-background: var(--color-background);
|
|
33
|
+
--color-foreground: var(--color-foreground);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
body {
|
|
37
|
+
background-color: var(--color-background);
|
|
38
|
+
color: var(--color-foreground);
|
|
39
|
+
overflow-x: hidden;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.pulse-dot {
|
|
43
|
+
display: inline-block;
|
|
44
|
+
width: 6px;
|
|
45
|
+
height: 6px;
|
|
46
|
+
margin-right: 6px;
|
|
47
|
+
background: #10b981;
|
|
48
|
+
border-radius: 50%;
|
|
49
|
+
box-shadow: 0 0 8px #10b981;
|
|
50
|
+
animation: nextly-pulse 1.6s ease-in-out infinite;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@keyframes nextly-pulse {
|
|
54
|
+
0%,
|
|
55
|
+
100% {
|
|
56
|
+
opacity: 1;
|
|
57
|
+
}
|
|
58
|
+
50% {
|
|
59
|
+
opacity: 0.4;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
@media (prefers-reduced-motion: reduce) {
|
|
64
|
+
.pulse-dot {
|
|
65
|
+
animation: none;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.font-display {
|
|
70
|
+
font-family: var(--font-display), ui-sans-serif, system-ui, sans-serif;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
.btn-primary {
|
|
74
|
+
@apply inline-flex items-center gap-2.5 rounded-none bg-foreground px-6 py-3.5 text-sm font-semibold text-background transition-all hover:opacity-90 active:scale-95;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.btn-secondary {
|
|
78
|
+
@apply inline-flex items-center gap-2.5 rounded-none border border-foreground/10 bg-transparent px-6 py-3.5 text-sm font-medium text-foreground transition-all hover:bg-foreground/5 active:scale-95;
|
|
79
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { Inter, JetBrains_Mono } from "next/font/google";
|
|
3
|
+
|
|
4
|
+
import "./globals.css";
|
|
5
|
+
|
|
6
|
+
const display = Inter({
|
|
7
|
+
variable: "--font-display",
|
|
8
|
+
subsets: ["latin"],
|
|
9
|
+
display: "swap",
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
const mono = JetBrains_Mono({
|
|
13
|
+
variable: "--font-mono",
|
|
14
|
+
subsets: ["latin"],
|
|
15
|
+
display: "swap",
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Blank-template root layout.
|
|
20
|
+
*
|
|
21
|
+
* Overrides templates/base/src/app/layout.tsx so the landing page can
|
|
22
|
+
* use a distinctive font pairing (Bricolage Grotesque for display,
|
|
23
|
+
* JetBrains Mono for tech accents). `metadataBase` is set so future
|
|
24
|
+
* blank-template pages can use relative OG image URLs without breaking.
|
|
25
|
+
*/
|
|
26
|
+
export const metadata: Metadata = {
|
|
27
|
+
metadataBase: new URL(
|
|
28
|
+
process.env.NEXT_PUBLIC_SITE_URL ?? "http://localhost:3000"
|
|
29
|
+
),
|
|
30
|
+
title: { default: "Nextly", template: "%s — Nextly" },
|
|
31
|
+
description: "A Nextly project.",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export default function RootLayout({
|
|
35
|
+
children,
|
|
36
|
+
}: Readonly<{
|
|
37
|
+
children: React.ReactNode;
|
|
38
|
+
}>) {
|
|
39
|
+
return (
|
|
40
|
+
<html lang="en" suppressHydrationWarning>
|
|
41
|
+
<body className={`${display.variable} ${mono.variable} antialiased`}>
|
|
42
|
+
{children}
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "blank",
|
|
3
|
+
"label": "Blank",
|
|
4
|
+
"description": "Start fresh with an empty config",
|
|
5
|
+
"hint": "Empty project, define your own schemas",
|
|
6
|
+
"approaches": [],
|
|
7
|
+
"defaultApproach": null,
|
|
8
|
+
"collections": [],
|
|
9
|
+
"singles": [],
|
|
10
|
+
"hasDemoData": false,
|
|
11
|
+
"hasFrontendPages": true,
|
|
12
|
+
"recommendedDatabase": "any",
|
|
13
|
+
"release": "alpha"
|
|
14
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# {{pluginName}}
|
|
2
|
+
|
|
3
|
+
A [Nextly](https://nextlyhq.com) plugin.
|
|
4
|
+
|
|
5
|
+
## Develop
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm install
|
|
9
|
+
pnpm dev # runs the embedded /dev playground → open http://localhost:3000/admin
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Your plugin lives in `src/`. The `dev/` folder is a minimal Nextly app on SQLite
|
|
13
|
+
that registers this plugin so you can exercise it in a real admin with hot-reload.
|
|
14
|
+
Editing files under `src/` reloads the playground. **`dev/` is never published.**
|
|
15
|
+
|
|
16
|
+
## Test
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm test # boots a real Nextly on in-memory SQLite via createTestNextly
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Build & publish
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pnpm build # tsup → dist/
|
|
26
|
+
npm publish
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
Only `dist/` ships (see `files` in `package.json`). The `nextly-plugin` keyword
|
|
30
|
+
makes this package discoverable.
|
|
31
|
+
|
|
32
|
+
## Use in an app
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
// nextly.config.ts
|
|
36
|
+
import { defineConfig } from "nextly";
|
|
37
|
+
import { myPlugin } from "{{pluginName}}";
|
|
38
|
+
|
|
39
|
+
export default defineConfig({
|
|
40
|
+
plugins: [myPlugin()],
|
|
41
|
+
});
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
See the [Nextly plugin author guide](https://nextlyhq.com/docs/plugins/author-guide).
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Dev playground env. Copy to dev/.env and run `pnpm dev`.
|
|
2
|
+
DB_DIALECT=sqlite
|
|
3
|
+
DATABASE_URL=file:./nextly-dev.db
|
|
4
|
+
|
|
5
|
+
# Any non-empty secret works for local dev (sign/verify session cookies).
|
|
6
|
+
NEXTLY_SECRET=dev-secret-change-me
|
|
7
|
+
|
|
8
|
+
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Warm up Nextly once per worker at startup (Node.js runtime only) so admin/API
|
|
2
|
+
// handlers never run before init and schema boot-apply runs once, not per request.
|
|
3
|
+
// Imported dynamically + guarded so nextly stays out of the Edge bundle.
|
|
4
|
+
export async function register(): Promise<void> {
|
|
5
|
+
if (process.env.NEXT_RUNTIME !== "nodejs") return;
|
|
6
|
+
|
|
7
|
+
const { createRegister } = await import("nextly");
|
|
8
|
+
const { default: config } = await import("./nextly.config");
|
|
9
|
+
await createRegister(config)();
|
|
10
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
// Dev playground for the plugin. The plugin's own source (../src) is aliased and
|
|
4
|
+
// transpiled so editing it hot-reloads. `nextly` + the admin come from
|
|
5
|
+
// node_modules (real deps). Node-only / dynamically-imported packages stay
|
|
6
|
+
// external (Turbopack can't bundle them).
|
|
7
|
+
const nextConfig: NextConfig = {
|
|
8
|
+
typescript: { ignoreBuildErrors: true },
|
|
9
|
+
// Transpile the aliased plugin source so HMR fires on src/ edits.
|
|
10
|
+
transpilePackages: ["{{pluginName}}"],
|
|
11
|
+
serverExternalPackages: [
|
|
12
|
+
"better-sqlite3",
|
|
13
|
+
"drizzle-orm",
|
|
14
|
+
"drizzle-kit",
|
|
15
|
+
"esbuild",
|
|
16
|
+
"bundle-require",
|
|
17
|
+
"sharp",
|
|
18
|
+
"@nextlyhq/adapter-drizzle",
|
|
19
|
+
"@nextlyhq/adapter-sqlite",
|
|
20
|
+
],
|
|
21
|
+
turbopack: {
|
|
22
|
+
resolveAlias: {
|
|
23
|
+
// Map the package name to its source so HMR works without a build step.
|
|
24
|
+
"{{pluginName}}": ["./../src/index.ts"],
|
|
25
|
+
"{{pluginName}}/admin": ["./../src/admin/index.ts"],
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export default nextConfig;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dev playground config — a minimal Nextly app that registers THIS plugin so you
|
|
3
|
+
* can exercise it in a real admin. SQLite, dev auto-login. Never published.
|
|
4
|
+
*/
|
|
5
|
+
import { defineConfig } from "nextly/config";
|
|
6
|
+
import { myPlugin } from "{{pluginName}}";
|
|
7
|
+
|
|
8
|
+
export default defineConfig({
|
|
9
|
+
admin: {
|
|
10
|
+
// Land on /admin already logged in. Hard-blocked in production by Nextly.
|
|
11
|
+
devAutoLogin: {
|
|
12
|
+
email: "dev@nextly.local",
|
|
13
|
+
password: "DevPassword123!",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
plugins: [myPlugin()],
|
|
17
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any -- framework adapter passes through Next.js App Router context */
|
|
2
|
+
/**
|
|
3
|
+
* Admin API catch-all. Routes every /admin/api/* request (incl. your plugin's
|
|
4
|
+
* /api/plugins/<name>/… routes) through the handler built from nextly.config.ts.
|
|
5
|
+
*/
|
|
6
|
+
import { createDynamicHandlers } from "nextly/runtime";
|
|
7
|
+
|
|
8
|
+
import nextlyConfig from "../../../../../nextly.config";
|
|
9
|
+
|
|
10
|
+
const handlers = createDynamicHandlers({ config: nextlyConfig });
|
|
11
|
+
|
|
12
|
+
export const GET = (req: Request, context: any) => handlers.GET(req, context);
|
|
13
|
+
export const POST = (req: Request, context: any) => handlers.POST(req, context);
|
|
14
|
+
export const PUT = (req: Request, context: any) => handlers.PUT(req, context);
|
|
15
|
+
export const PATCH = (req: Request, context: any) =>
|
|
16
|
+
handlers.PATCH(req, context);
|
|
17
|
+
export const DELETE = (req: Request, context: any) =>
|
|
18
|
+
handlers.DELETE(req, context);
|
|
19
|
+
export const OPTIONS = (req: Request) => handlers.OPTIONS(req);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export const metadata = {
|
|
4
|
+
title: "{{pluginName}} — dev playground",
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export default function RootLayout({ children }: { children: ReactNode }) {
|
|
8
|
+
return (
|
|
9
|
+
<html lang="en">
|
|
10
|
+
<body>{children}</body>
|
|
11
|
+
</html>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
5
|
+
"module": "ESNext",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"jsx": "preserve",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"noEmit": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"plugins": [{ "name": "next" }],
|
|
15
|
+
"paths": {
|
|
16
|
+
"{{pluginName}}": ["../src/index.ts"],
|
|
17
|
+
"{{pluginName}}/admin": ["../src/admin/index.ts"]
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
21
|
+
"exclude": ["node_modules"]
|
|
22
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example plugin settings page, rendered at `/admin/plugins/<slug>` and inside
|
|
3
|
+
* a per-component error boundary (D53). Build your real settings UI here with
|
|
4
|
+
* `@nextlyhq/ui` components.
|
|
5
|
+
*/
|
|
6
|
+
export function SettingsPage() {
|
|
7
|
+
return (
|
|
8
|
+
<div style={{ padding: 24 }}>
|
|
9
|
+
<h1>Plugin Settings</h1>
|
|
10
|
+
<p>Settings UI goes here. Edit src/admin/SettingsPage.tsx.</p>
|
|
11
|
+
</div>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Admin component registration (D19/D60). Importing this module registers the
|
|
3
|
+
* React components referenced by `contributes.admin` string paths so the host
|
|
4
|
+
* admin can resolve them. The host's generated import map imports this for you;
|
|
5
|
+
* the eager + lazy registration below is the fallback.
|
|
6
|
+
*/
|
|
7
|
+
import { registerComponents, registerKnownPlugin } from "@nextlyhq/admin";
|
|
8
|
+
|
|
9
|
+
import { SettingsPage } from "./SettingsPage";
|
|
10
|
+
|
|
11
|
+
const SETTINGS_PATH = "{{pluginName}}/admin#SettingsPage";
|
|
12
|
+
|
|
13
|
+
// Eager: register on module load.
|
|
14
|
+
registerComponents({ [SETTINGS_PATH]: SettingsPage });
|
|
15
|
+
|
|
16
|
+
// Lazy fallback: the host can trigger registration on demand by package prefix.
|
|
17
|
+
registerKnownPlugin("{{pluginName}}", async () => {
|
|
18
|
+
registerComponents({ [SETTINGS_PATH]: SettingsPage });
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export { SettingsPage };
|