create-lacspace-app 1.11.0 → 1.12.1
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/index.js +190 -16
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -78,6 +78,7 @@ var tsconfig = () => JSON.stringify({
|
|
|
78
78
|
jsx: "preserve",
|
|
79
79
|
incremental: true,
|
|
80
80
|
plugins: [{ name: "next" }],
|
|
81
|
+
baseUrl: ".",
|
|
81
82
|
paths: { "@/*": ["./*"] }
|
|
82
83
|
},
|
|
83
84
|
include: ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
@@ -189,6 +190,8 @@ body {
|
|
|
189
190
|
animation: gradient-pan 6s ease infinite;
|
|
190
191
|
}
|
|
191
192
|
.marquee-track { display: flex; width: max-content; animation: marquee-x 32s linear infinite; }
|
|
193
|
+
@keyframes draw { to { stroke-dashoffset: 0; } }
|
|
194
|
+
.animate-draw { stroke-dasharray: 2000; stroke-dashoffset: 2000; animation: draw 1.8s ease forwards; }
|
|
192
195
|
.marquee-mask { -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); }
|
|
193
196
|
|
|
194
197
|
/* soft entrance for content */
|
|
@@ -513,7 +516,7 @@ export default function Home() {
|
|
|
513
516
|
<h3 className="font-semibold">{prod.n}</h3>
|
|
514
517
|
<span className="text-muted">{prod.p}</span>
|
|
515
518
|
</div>
|
|
516
|
-
<
|
|
519
|
+
<a href="/shop" className="mt-3 block w-full rounded-lg border border-hairline py-2 text-center text-sm font-medium hover:bg-surface">Add to cart</a>
|
|
517
520
|
</div>
|
|
518
521
|
))}
|
|
519
522
|
</div>
|
|
@@ -775,6 +778,27 @@ export async function submitContact(prev: ContactState, formData: FormData): Pro
|
|
|
775
778
|
console.log("New contact message:", r.data);
|
|
776
779
|
return { ok: true };
|
|
777
780
|
}
|
|
781
|
+
|
|
782
|
+
const newsletter = createForm({
|
|
783
|
+
schema: v.object({ email: v.string().email("Enter a valid email").toLowerCase() }),
|
|
784
|
+
honeypot: "website",
|
|
785
|
+
minSubmitMs: 500,
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
export type SubscribeState =
|
|
789
|
+
| { ok: true }
|
|
790
|
+
| { ok: false; errors: Record<string, string>; values: Record<string, unknown> }
|
|
791
|
+
| null;
|
|
792
|
+
|
|
793
|
+
export async function submitNewsletter(prev: SubscribeState, formData: FormData): Promise<SubscribeState> {
|
|
794
|
+
const r = newsletter.action(prev, formData);
|
|
795
|
+
if (!r.ok) return r;
|
|
796
|
+
|
|
797
|
+
// \u2705 r.data is fully typed: { email }
|
|
798
|
+
// TODO: add r.data.email to your list (Resend, Mailchimp, a database\u2026).
|
|
799
|
+
console.log("New subscriber:", r.data.email);
|
|
800
|
+
return { ok: true };
|
|
801
|
+
}
|
|
778
802
|
`;
|
|
779
803
|
var contactForm = () => `"use client";
|
|
780
804
|
import { useActionState } from "react";
|
|
@@ -1488,7 +1512,7 @@ function pagesFor(ctx) {
|
|
|
1488
1512
|
const k = ctx.template.key;
|
|
1489
1513
|
if (k === "dashboard") {
|
|
1490
1514
|
return [
|
|
1491
|
-
{ path: "/analytics", label: "Analytics", nav: true, group: "Product" },
|
|
1515
|
+
{ path: "/analytics", label: "Analytics", nav: true, group: "Product", real: true },
|
|
1492
1516
|
{ path: "/customers", label: "Customers", nav: true, group: "Product" },
|
|
1493
1517
|
{ path: "/billing", label: "Billing", nav: true, group: "Product" },
|
|
1494
1518
|
{ path: "/settings", label: "Settings", nav: true, group: "Product", real: true },
|
|
@@ -1530,7 +1554,7 @@ function pagesFor(ctx) {
|
|
|
1530
1554
|
blog: [
|
|
1531
1555
|
{ path: "/blog", label: "Articles", nav: true, group: "Product", real: true },
|
|
1532
1556
|
{ path: "/topics", label: "Topics", nav: true, group: "Product", real: true },
|
|
1533
|
-
{ path: "/newsletter", label: "Newsletter", group: "Resources" }
|
|
1557
|
+
{ path: "/newsletter", label: "Newsletter", group: "Resources", real: true }
|
|
1534
1558
|
],
|
|
1535
1559
|
docs: [
|
|
1536
1560
|
{ path: "/docs", label: "Docs", nav: true, group: "Product", real: true },
|
|
@@ -1879,23 +1903,37 @@ export function DashboardShell({ title, subtitle, children }: { title: string; s
|
|
|
1879
1903
|
};
|
|
1880
1904
|
var dashboardHome = (ctx) => `import { site } from "@/lib/site";
|
|
1881
1905
|
import { DashboardShell } from "@/components/dashboard-shell";
|
|
1882
|
-
import {
|
|
1906
|
+
import { StatCard, AreaChart } from "@/components/ui";
|
|
1883
1907
|
|
|
1884
1908
|
export const metadata = site.meta({ title: "Overview", path: "/" });
|
|
1885
1909
|
|
|
1886
1910
|
export default function Home() {
|
|
1887
1911
|
return (
|
|
1888
1912
|
<DashboardShell title="Overview" subtitle=${JSON.stringify(ctx.template.siteDescription)}>
|
|
1889
|
-
<
|
|
1890
|
-
|
|
1891
|
-
<
|
|
1892
|
-
<
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1913
|
+
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
1914
|
+
<StatCard label="Revenue" value="$48.2k" delta="+12%" />
|
|
1915
|
+
<StatCard label="Orders" value="1,204" delta="+8%" />
|
|
1916
|
+
<StatCard label="Customers" value="8,430" delta="+3.4%" />
|
|
1917
|
+
<StatCard label="Churn" value="1.2%" delta="-0.3%" />
|
|
1918
|
+
</div>
|
|
1919
|
+
<div className="mt-6 grid gap-6 lg:grid-cols-3">
|
|
1920
|
+
<div className="rounded-2xl border border-hairline bg-surface p-6 lg:col-span-2">
|
|
1921
|
+
<div className="mb-4 flex items-center justify-between">
|
|
1922
|
+
<h2 className="font-semibold">Revenue</h2>
|
|
1923
|
+
<span className="text-sm font-medium text-emerald-400">+12.4%</span>
|
|
1924
|
+
</div>
|
|
1925
|
+
<AreaChart data={[12, 18, 15, 22, 20, 28, 26, 34, 30, 38, 42, 48]} />
|
|
1926
|
+
</div>
|
|
1927
|
+
<div className="rounded-2xl border border-hairline bg-surface p-6">
|
|
1928
|
+
<div className="mb-4 font-semibold">Recent activity</div>
|
|
1929
|
+
<div className="space-y-3">
|
|
1930
|
+
{[1, 2, 3, 4].map((i) => (
|
|
1931
|
+
<div key={i} className="flex items-center justify-between border-b border-hairline pb-3 text-sm">
|
|
1932
|
+
<span className="text-muted">Event #{i}</span>
|
|
1933
|
+
<span className="text-faint">just now</span>
|
|
1934
|
+
</div>
|
|
1935
|
+
))}
|
|
1936
|
+
</div>
|
|
1899
1937
|
</div>
|
|
1900
1938
|
</div>
|
|
1901
1939
|
</DashboardShell>
|
|
@@ -1907,7 +1945,7 @@ var pageFile = (o) => `import type { Metadata } from "next";
|
|
|
1907
1945
|
import Link from "next/link";
|
|
1908
1946
|
import { site } from "@/lib/site";
|
|
1909
1947
|
import { Aurora } from "@/components/aurora";
|
|
1910
|
-
import { Section, Pill, StatCard, FeatureCard, Testimonial, Steps, CTABand } from "@/components/ui";
|
|
1948
|
+
import { Section, Pill, StatCard, FeatureCard, Testimonial, Steps, CTABand, Bento, AreaChart, Newsletter } from "@/components/ui";
|
|
1911
1949
|
|
|
1912
1950
|
export const metadata: Metadata = site.meta({ title: ${JSON.stringify(o.title)}, path: ${JSON.stringify(o.path)}, description: ${JSON.stringify(o.description)} });
|
|
1913
1951
|
|
|
@@ -2020,6 +2058,16 @@ var featuresPage = (ctx) => pageFile({
|
|
|
2020
2058
|
{ title: "Configure", desc: "Set it up your way in a few clicks \u2014 no code required." },
|
|
2021
2059
|
{ title: "Ship", desc: "Go live and watch the numbers in real time." },
|
|
2022
2060
|
]} />
|
|
2061
|
+
</section>
|
|
2062
|
+
<section className="mx-auto max-w-6xl px-6 py-8">
|
|
2063
|
+
<h2 className="mb-8 text-center text-2xl font-bold">One platform, everything included</h2>
|
|
2064
|
+
<Bento items={[
|
|
2065
|
+
{ title: "Realtime dashboard", desc: "Everything as it happens, in one view.", icon: "\u{1F4CA}", className: "sm:col-span-2 sm:row-span-2" },
|
|
2066
|
+
{ title: "Webhooks", desc: "Automate anything.", icon: "\u{1F50C}" },
|
|
2067
|
+
{ title: "SSO & SAML", desc: "Enterprise-ready.", icon: "\u{1F510}" },
|
|
2068
|
+
{ title: "Audit logs", desc: "Full history.", icon: "\u{1F9FE}" },
|
|
2069
|
+
{ title: "99.99% uptime", desc: "Rock solid.", icon: "\u26A1" },
|
|
2070
|
+
]} />
|
|
2023
2071
|
</section>`
|
|
2024
2072
|
});
|
|
2025
2073
|
var workPage = (ctx) => pageFile({
|
|
@@ -2137,6 +2185,51 @@ var guidesPage = (ctx) => cardGridPage({
|
|
|
2137
2185
|
{ title: "Troubleshooting", desc: "Fix the most common issues quickly." }
|
|
2138
2186
|
]
|
|
2139
2187
|
});
|
|
2188
|
+
var newsletterPage = (ctx) => pageFile({
|
|
2189
|
+
title: "Newsletter",
|
|
2190
|
+
path: "/newsletter",
|
|
2191
|
+
description: `Subscribe to ${ctx.template.siteName} \u2014 new posts straight to your inbox.`,
|
|
2192
|
+
body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
2193
|
+
<Pill>Newsletter</Pill>
|
|
2194
|
+
<h1 className="mt-4 text-4xl font-bold sm:text-5xl">Join the <span className="gradient-text">list</span></h1>
|
|
2195
|
+
<p className="mx-auto mt-4 max-w-xl text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
2196
|
+
</section>
|
|
2197
|
+
<section className="px-6 pb-24">
|
|
2198
|
+
<Newsletter title="Get new posts in your inbox" subtitle="A short email when we publish something new. Unsubscribe anytime." />
|
|
2199
|
+
</section>`
|
|
2200
|
+
});
|
|
2201
|
+
var analyticsPage = (ctx) => `import type { Metadata } from "next";
|
|
2202
|
+
import { site } from "@/lib/site";
|
|
2203
|
+
import { DashboardShell } from "@/components/dashboard-shell";
|
|
2204
|
+
import { LiveStats } from "@/components/live-stats";
|
|
2205
|
+
import { StatCard, AreaChart } from "@/components/ui";
|
|
2206
|
+
|
|
2207
|
+
export const metadata: Metadata = site.meta({ title: "Analytics", path: "/analytics", description: ${JSON.stringify(`Analytics for ${ctx.template.siteName}.`)} });
|
|
2208
|
+
|
|
2209
|
+
export default function Page() {
|
|
2210
|
+
return (
|
|
2211
|
+
<DashboardShell title="Analytics" subtitle="Traffic, signups and revenue at a glance.">
|
|
2212
|
+
<LiveStats />
|
|
2213
|
+
<div className="mt-6 grid gap-6 lg:grid-cols-2">
|
|
2214
|
+
<div className="rounded-2xl border border-hairline bg-surface p-6">
|
|
2215
|
+
<div className="mb-4 flex items-center justify-between"><h2 className="font-semibold">Traffic</h2><span className="text-sm font-medium text-emerald-400">+18%</span></div>
|
|
2216
|
+
<AreaChart gradientId="g-traffic" data={[8, 12, 10, 16, 14, 20, 22, 19, 26, 30, 28, 34]} />
|
|
2217
|
+
</div>
|
|
2218
|
+
<div className="rounded-2xl border border-hairline bg-surface p-6">
|
|
2219
|
+
<div className="mb-4 flex items-center justify-between"><h2 className="font-semibold">Signups</h2><span className="text-sm font-medium text-emerald-400">+9%</span></div>
|
|
2220
|
+
<AreaChart gradientId="g-signups" data={[3, 5, 4, 7, 9, 8, 11, 13, 12, 15, 18, 21]} />
|
|
2221
|
+
</div>
|
|
2222
|
+
</div>
|
|
2223
|
+
<div className="mt-6 grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
2224
|
+
<StatCard label="Sessions" value="48.2k" delta="+12%" />
|
|
2225
|
+
<StatCard label="Avg. time" value="3m 14s" delta="+4%" />
|
|
2226
|
+
<StatCard label="Bounce rate" value="38%" delta="-2%" />
|
|
2227
|
+
<StatCard label="Conversion" value="3.4%" delta="+0.6%" />
|
|
2228
|
+
</div>
|
|
2229
|
+
</DashboardShell>
|
|
2230
|
+
);
|
|
2231
|
+
}
|
|
2232
|
+
`;
|
|
2140
2233
|
var shopPage = (ctx) => `import type { Metadata } from "next";
|
|
2141
2234
|
import { site } from "@/lib/site";
|
|
2142
2235
|
import { ProductGrid } from "@/components/product-grid";
|
|
@@ -2392,6 +2485,80 @@ export function CTABand({ title, subtitle, ctaLabel = "Get started", ctaHref = "
|
|
|
2392
2485
|
</section>
|
|
2393
2486
|
);
|
|
2394
2487
|
}
|
|
2488
|
+
`,
|
|
2489
|
+
"components/ui/bento.tsx": `import type { ReactNode } from "react";
|
|
2490
|
+
|
|
2491
|
+
interface BentoItem { title: string; desc?: string; icon?: ReactNode; className?: string; }
|
|
2492
|
+
|
|
2493
|
+
/** A modern, asymmetric "bento" grid. Pass className like "sm:col-span-2" to span. */
|
|
2494
|
+
export function Bento({ items }: { items: BentoItem[] }) {
|
|
2495
|
+
return (
|
|
2496
|
+
<div className="grid auto-rows-[minmax(150px,auto)] grid-cols-2 gap-4 sm:grid-cols-3">
|
|
2497
|
+
{items.map((it) => (
|
|
2498
|
+
<div key={it.title} className={"relative overflow-hidden rounded-3xl border border-hairline bg-surface p-6 transition hover:-translate-y-1 " + (it.className ?? "")}>
|
|
2499
|
+
<div aria-hidden className="pointer-events-none absolute -right-8 -top-8 h-24 w-24 rounded-full gradient-bg opacity-20 blur-2xl" />
|
|
2500
|
+
{it.icon ? <div className="mb-3 text-2xl">{it.icon}</div> : null}
|
|
2501
|
+
<h3 className="relative font-semibold">{it.title}</h3>
|
|
2502
|
+
{it.desc ? <p className="relative mt-1 text-sm text-muted">{it.desc}</p> : null}
|
|
2503
|
+
</div>
|
|
2504
|
+
))}
|
|
2505
|
+
</div>
|
|
2506
|
+
);
|
|
2507
|
+
}
|
|
2508
|
+
`,
|
|
2509
|
+
"components/ui/area-chart.tsx": `/** A dependency-free, theme-aware area chart with an animated draw. */
|
|
2510
|
+
export function AreaChart({ data, height = 160, gradientId = "lac-area", className = "" }: { data: number[]; height?: number; gradientId?: string; className?: string }) {
|
|
2511
|
+
const w = 600;
|
|
2512
|
+
const max = Math.max(...data, 1);
|
|
2513
|
+
const min = Math.min(...data, 0);
|
|
2514
|
+
const range = max - min || 1;
|
|
2515
|
+
const step = data.length > 1 ? w / (data.length - 1) : w;
|
|
2516
|
+
const pts = data.map((d, i) => [i * step, height - ((d - min) / range) * (height - 24) - 12] as const);
|
|
2517
|
+
const line = pts.map((p, i) => (i === 0 ? "M" : "L") + p[0].toFixed(1) + " " + p[1].toFixed(1)).join(" ");
|
|
2518
|
+
const area = line + " L" + w + " " + height + " L0 " + height + " Z";
|
|
2519
|
+
return (
|
|
2520
|
+
<svg viewBox={"0 0 " + w + " " + height} preserveAspectRatio="none" className={"w-full " + className} style={{ height }} role="img" aria-label="Chart">
|
|
2521
|
+
<defs>
|
|
2522
|
+
<linearGradient id={gradientId} x1="0" y1="0" x2="0" y2="1">
|
|
2523
|
+
<stop offset="0%" stopColor="var(--accent-to)" stopOpacity="0.35" />
|
|
2524
|
+
<stop offset="100%" stopColor="var(--accent-to)" stopOpacity="0" />
|
|
2525
|
+
</linearGradient>
|
|
2526
|
+
</defs>
|
|
2527
|
+
<path d={area} fill={"url(#" + gradientId + ")"} />
|
|
2528
|
+
<path d={line} fill="none" stroke="var(--accent-to)" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" className="animate-draw" />
|
|
2529
|
+
</svg>
|
|
2530
|
+
);
|
|
2531
|
+
}
|
|
2532
|
+
`,
|
|
2533
|
+
"components/ui/newsletter.tsx": `"use client";
|
|
2534
|
+
|
|
2535
|
+
import { useActionState } from "react";
|
|
2536
|
+
import { honeypotProps, timestampValue } from "@lacspace/form";
|
|
2537
|
+
import { submitNewsletter, type SubscribeState } from "@/app/actions";
|
|
2538
|
+
|
|
2539
|
+
/** An email capture block \u2014 validated & spam-protected via @lacspace/form. */
|
|
2540
|
+
export function Newsletter({ title = "Stay in the loop", subtitle = "Occasional updates. No spam \u2014 unsubscribe anytime." }: { title?: string; subtitle?: string }) {
|
|
2541
|
+
const [state, action, pending] = useActionState<SubscribeState, FormData>(submitNewsletter, null);
|
|
2542
|
+
const err = state && !state.ok ? (state.errors.email ?? state.errors._form) : undefined;
|
|
2543
|
+
|
|
2544
|
+
return (
|
|
2545
|
+
<div className="mx-auto max-w-xl rounded-3xl border border-hairline bg-surface p-8 text-center">
|
|
2546
|
+
<h2 className="text-2xl font-bold">{title}</h2>
|
|
2547
|
+
<p className="mt-2 text-sm text-muted">{subtitle}</p>
|
|
2548
|
+
{state?.ok ? (
|
|
2549
|
+
<p className="mt-6 rounded-xl border border-hairline bg-app p-4 text-sm">You’re subscribed \u2014 thanks! \u2705</p>
|
|
2550
|
+
) : (
|
|
2551
|
+
<form action={action} className="mt-6 flex flex-col gap-3 sm:flex-row">
|
|
2552
|
+
<input name="email" type="email" placeholder="you@example.com" aria-label="Email" className="w-full flex-1 rounded-full border border-hairline bg-app px-5 py-3 outline-none focus:border-[color:var(--accent-to)]" />
|
|
2553
|
+
<input {...honeypotProps("website")} />
|
|
2554
|
+
<input type="hidden" name="_ts" defaultValue={timestampValue()} />
|
|
2555
|
+
<button disabled={pending} className="rounded-full gradient-bg px-6 py-3 font-semibold text-black transition hover:opacity-90 disabled:opacity-60">{pending ? "Joining\u2026" : "Subscribe"}</button>
|
|
2556
|
+
</form>
|
|
2557
|
+
)}
|
|
2558
|
+
{err ? <p className="mt-3 text-sm text-red-400">{err}</p> : null}
|
|
2559
|
+
</div>
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2395
2562
|
`,
|
|
2396
2563
|
"components/ui/index.ts": `export * from "./section";
|
|
2397
2564
|
export * from "./pill";
|
|
@@ -2400,6 +2567,9 @@ export * from "./feature-card";
|
|
|
2400
2567
|
export * from "./testimonial";
|
|
2401
2568
|
export * from "./steps";
|
|
2402
2569
|
export * from "./cta-band";
|
|
2570
|
+
export * from "./bento";
|
|
2571
|
+
export * from "./area-chart";
|
|
2572
|
+
export * from "./newsletter";
|
|
2403
2573
|
`
|
|
2404
2574
|
});
|
|
2405
2575
|
function realPageFiles(ctx) {
|
|
@@ -2420,12 +2590,16 @@ function realPageFiles(ctx) {
|
|
|
2420
2590
|
f["components/product-grid.tsx"] = productGrid();
|
|
2421
2591
|
f["components/cart-view.tsx"] = cartView();
|
|
2422
2592
|
}
|
|
2423
|
-
if (k === "blog")
|
|
2593
|
+
if (k === "blog") {
|
|
2594
|
+
f["app/topics/page.tsx"] = topicsPage();
|
|
2595
|
+
f["app/newsletter/page.tsx"] = newsletterPage(ctx);
|
|
2596
|
+
}
|
|
2424
2597
|
if (k === "docs") f["app/guides/page.tsx"] = guidesPage();
|
|
2425
2598
|
if (k === "restaurant") f["app/menu/page.tsx"] = menuPage(ctx);
|
|
2426
2599
|
if (k === "dashboard") {
|
|
2427
2600
|
f["app/settings/page.tsx"] = settingsPage(ctx);
|
|
2428
2601
|
f["components/settings-panel.tsx"] = settingsPanel();
|
|
2602
|
+
f["app/analytics/page.tsx"] = analyticsPage(ctx);
|
|
2429
2603
|
}
|
|
2430
2604
|
return f;
|
|
2431
2605
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-lacspace-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.12.1",
|
|
4
4
|
"description": "Scaffold a beautiful, production-ready Next.js app from a Lacspace template — portfolio, business, e-commerce, SaaS, blog, docs, dashboard or restaurant — pre-wired with SEO, security headers, sitemap and robots. Like create-next-app, but you start gorgeous.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|