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.
Files changed (2) hide show
  1. package/dist/index.js +188 -15
  2. 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 { LiveStats } from "@/components/live-stats";
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
- <LiveStats />
1890
- <div className="mt-6 rounded-2xl border border-hairline bg-surface p-6">
1891
- <div className="mb-4 font-semibold">Recent activity</div>
1892
- <div className="space-y-3">
1893
- {[1, 2, 3, 4].map((i) => (
1894
- <div key={i} className="flex items-center justify-between border-b border-hairline pb-3 text-sm">
1895
- <span className="text-muted">Event #{i}</span>
1896
- <span className="text-faint">just now</span>
1897
- </div>
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&rsquo;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") f["app/topics/page.tsx"] = topicsPage();
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.11.0",
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": {