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