create-lacspace-app 1.10.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 +321 -47
- 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";
|
|
@@ -1463,6 +1486,7 @@ function buildFiles(ctx) {
|
|
|
1463
1486
|
for (const page of stubPages(ctx)) {
|
|
1464
1487
|
files[`app${page.path}/page.tsx`] = isDash ? dashboardStubPage(page) : underDevPage(page);
|
|
1465
1488
|
}
|
|
1489
|
+
Object.assign(files, uiKitFiles());
|
|
1466
1490
|
Object.assign(files, realPageFiles(ctx));
|
|
1467
1491
|
if (isBlog) {
|
|
1468
1492
|
files["lib/posts.ts"] = postsLib();
|
|
@@ -1487,7 +1511,7 @@ function pagesFor(ctx) {
|
|
|
1487
1511
|
const k = ctx.template.key;
|
|
1488
1512
|
if (k === "dashboard") {
|
|
1489
1513
|
return [
|
|
1490
|
-
{ path: "/analytics", label: "Analytics", nav: true, group: "Product" },
|
|
1514
|
+
{ path: "/analytics", label: "Analytics", nav: true, group: "Product", real: true },
|
|
1491
1515
|
{ path: "/customers", label: "Customers", nav: true, group: "Product" },
|
|
1492
1516
|
{ path: "/billing", label: "Billing", nav: true, group: "Product" },
|
|
1493
1517
|
{ path: "/settings", label: "Settings", nav: true, group: "Product", real: true },
|
|
@@ -1529,7 +1553,7 @@ function pagesFor(ctx) {
|
|
|
1529
1553
|
blog: [
|
|
1530
1554
|
{ path: "/blog", label: "Articles", nav: true, group: "Product", real: true },
|
|
1531
1555
|
{ path: "/topics", label: "Topics", nav: true, group: "Product", real: true },
|
|
1532
|
-
{ path: "/newsletter", label: "Newsletter", group: "Resources" }
|
|
1556
|
+
{ path: "/newsletter", label: "Newsletter", group: "Resources", real: true }
|
|
1533
1557
|
],
|
|
1534
1558
|
docs: [
|
|
1535
1559
|
{ path: "/docs", label: "Docs", nav: true, group: "Product", real: true },
|
|
@@ -1878,39 +1902,58 @@ export function DashboardShell({ title, subtitle, children }: { title: string; s
|
|
|
1878
1902
|
};
|
|
1879
1903
|
var dashboardHome = (ctx) => `import { site } from "@/lib/site";
|
|
1880
1904
|
import { DashboardShell } from "@/components/dashboard-shell";
|
|
1881
|
-
import {
|
|
1905
|
+
import { StatCard, AreaChart } from "@/components/ui";
|
|
1882
1906
|
|
|
1883
1907
|
export const metadata = site.meta({ title: "Overview", path: "/" });
|
|
1884
1908
|
|
|
1885
1909
|
export default function Home() {
|
|
1886
1910
|
return (
|
|
1887
1911
|
<DashboardShell title="Overview" subtitle=${JSON.stringify(ctx.template.siteDescription)}>
|
|
1888
|
-
<
|
|
1889
|
-
|
|
1890
|
-
<
|
|
1891
|
-
<
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
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>
|
|
1898
1936
|
</div>
|
|
1899
1937
|
</div>
|
|
1900
1938
|
</DashboardShell>
|
|
1901
1939
|
);
|
|
1902
1940
|
}
|
|
1903
1941
|
`;
|
|
1942
|
+
var ctaBandJsx = (c2) => ` <CTABand title=${JSON.stringify(c2.title)}${c2.subtitle ? ` subtitle=${JSON.stringify(c2.subtitle)}` : ""}${c2.label ? ` ctaLabel=${JSON.stringify(c2.label)}` : ""}${c2.href ? ` ctaHref=${JSON.stringify(c2.href)}` : ""} />`;
|
|
1904
1943
|
var pageFile = (o) => `import type { Metadata } from "next";
|
|
1905
1944
|
import Link from "next/link";
|
|
1906
1945
|
import { site } from "@/lib/site";
|
|
1946
|
+
import { Aurora } from "@/components/aurora";
|
|
1947
|
+
import { Section, Pill, StatCard, FeatureCard, Testimonial, Steps, CTABand, Bento, AreaChart, Newsletter } from "@/components/ui";
|
|
1907
1948
|
|
|
1908
1949
|
export const metadata: Metadata = site.meta({ title: ${JSON.stringify(o.title)}, path: ${JSON.stringify(o.path)}, description: ${JSON.stringify(o.description)} });
|
|
1909
1950
|
|
|
1910
1951
|
export default function Page() {
|
|
1911
1952
|
return (
|
|
1912
|
-
<main className="min-h-screen">
|
|
1953
|
+
<main className="relative min-h-screen overflow-hidden">
|
|
1954
|
+
<Aurora />
|
|
1913
1955
|
${o.body}
|
|
1956
|
+
${o.cta ? ctaBandJsx(o.cta) : ""}
|
|
1914
1957
|
</main>
|
|
1915
1958
|
);
|
|
1916
1959
|
}
|
|
@@ -1919,9 +1962,10 @@ var pricingPage = (ctx) => pageFile({
|
|
|
1919
1962
|
title: "Pricing",
|
|
1920
1963
|
path: "/pricing",
|
|
1921
1964
|
description: `Simple, transparent pricing for ${ctx.template.siteName}. Start free and upgrade anytime.`,
|
|
1965
|
+
cta: { title: "Still deciding?", subtitle: "Talk to us and we'll help you pick the right plan.", label: "Contact sales", href: "/contact" },
|
|
1922
1966
|
body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
1923
|
-
<
|
|
1924
|
-
<h1 className="mt-
|
|
1967
|
+
<Pill>Pricing</Pill>
|
|
1968
|
+
<h1 className="mt-4 text-4xl font-bold sm:text-5xl">Simple, transparent <span className="gradient-text">pricing</span></h1>
|
|
1925
1969
|
<p className="mx-auto mt-4 max-w-xl text-lg text-muted">Start free. Upgrade when you're ready. Cancel anytime.</p>
|
|
1926
1970
|
</section>
|
|
1927
1971
|
<section className="mx-auto max-w-6xl px-6 pb-24">
|
|
@@ -1949,15 +1993,19 @@ var pricingPage = (ctx) => pageFile({
|
|
|
1949
1993
|
))}
|
|
1950
1994
|
</div>
|
|
1951
1995
|
<p className="mt-10 text-center text-sm text-faint">All plans include SSL, unlimited bandwidth and a 14-day money-back guarantee.</p>
|
|
1996
|
+
</section>
|
|
1997
|
+
<section className="px-6 pb-8">
|
|
1998
|
+
<Testimonial quote="We switched in an afternoon and never looked back. Worth every penny." author="Sam Rivera" role="CTO, Globex" />
|
|
1952
1999
|
</section>`
|
|
1953
2000
|
});
|
|
1954
2001
|
var servicesPage = (ctx) => pageFile({
|
|
1955
2002
|
title: "Services",
|
|
1956
2003
|
path: "/services",
|
|
1957
2004
|
description: `What ${ctx.template.siteName} can do for you \u2014 from strategy to launch.`,
|
|
2005
|
+
cta: { title: "Have a project in mind?", subtitle: "Tell us what you're building and we'll take it from there.", label: "Start a conversation", href: "/contact" },
|
|
1958
2006
|
body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
1959
|
-
<
|
|
1960
|
-
<h1 className="mt-
|
|
2007
|
+
<Pill>Services</Pill>
|
|
2008
|
+
<h1 className="mt-4 text-4xl font-bold sm:text-5xl">What we <span className="gradient-text">do</span></h1>
|
|
1961
2009
|
<p className="mx-auto mt-4 max-w-xl text-lg text-muted">End-to-end help \u2014 from the first sketch to a product your customers love.</p>
|
|
1962
2010
|
</section>
|
|
1963
2011
|
<section className="mx-auto max-w-6xl px-6 pb-16">
|
|
@@ -1970,29 +2018,25 @@ var servicesPage = (ctx) => pageFile({
|
|
|
1970
2018
|
{ icon: "\u{1F4C8}", title: "Growth", desc: "Experiments and optimization that move the numbers." },
|
|
1971
2019
|
{ icon: "\u{1F91D}", title: "Support", desc: "Ongoing care so your product keeps getting better." },
|
|
1972
2020
|
].map((s) => (
|
|
1973
|
-
<
|
|
1974
|
-
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl gradient-bg text-2xl">{s.icon}</div>
|
|
1975
|
-
<h3 className="font-semibold">{s.title}</h3>
|
|
1976
|
-
<p className="mt-1 text-sm text-muted">{s.desc}</p>
|
|
1977
|
-
</div>
|
|
2021
|
+
<FeatureCard key={s.title} icon={s.icon} title={s.title} desc={s.desc} />
|
|
1978
2022
|
))}
|
|
1979
2023
|
</div>
|
|
1980
2024
|
</section>
|
|
1981
|
-
<section className="
|
|
1982
|
-
<
|
|
1983
|
-
<Link href="/contact" className="mt-6 inline-block rounded-full gradient-bg px-8 py-3 font-semibold text-black">Start a conversation</Link>
|
|
2025
|
+
<section className="px-6 pb-8">
|
|
2026
|
+
<Testimonial quote="They shipped faster than we thought possible \u2014 and it looked incredible." author="Jordan Ellis" role="Founder, Northwind" />
|
|
1984
2027
|
</section>`
|
|
1985
2028
|
});
|
|
1986
2029
|
var featuresPage = (ctx) => pageFile({
|
|
1987
2030
|
title: "Features",
|
|
1988
2031
|
path: "/features",
|
|
1989
2032
|
description: `Everything ${ctx.template.siteName} gives your team, in one place.`,
|
|
2033
|
+
cta: { title: "Ready to ship faster?", subtitle: "Start free \u2014 no credit card required.", label: "See pricing", href: "/pricing" },
|
|
1990
2034
|
body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
1991
|
-
<
|
|
1992
|
-
<h1 className="mt-
|
|
2035
|
+
<Pill>Features</Pill>
|
|
2036
|
+
<h1 className="mt-4 text-4xl font-bold sm:text-5xl">Built to <span className="gradient-text">ship faster</span></h1>
|
|
1993
2037
|
<p className="mx-auto mt-4 max-w-xl text-lg text-muted">A focused set of features that do the heavy lifting so your team can move.</p>
|
|
1994
2038
|
</section>
|
|
1995
|
-
<section className="mx-auto max-w-6xl px-6 pb-
|
|
2039
|
+
<section className="mx-auto max-w-6xl px-6 pb-16">
|
|
1996
2040
|
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
1997
2041
|
{[
|
|
1998
2042
|
{ icon: "\u26A1", title: "Fast by default", desc: "Edge-rendered and cached \u2014 instant everywhere." },
|
|
@@ -2002,25 +2046,37 @@ var featuresPage = (ctx) => pageFile({
|
|
|
2002
2046
|
{ icon: "\u{1F9E9}", title: "Extensible", desc: "A clean API and webhooks for anything custom." },
|
|
2003
2047
|
{ icon: "\u{1F317}", title: "Delightful UX", desc: "Dark mode, a \u2318K palette and thoughtful details." },
|
|
2004
2048
|
].map((f) => (
|
|
2005
|
-
<
|
|
2006
|
-
<div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl gradient-bg text-2xl">{f.icon}</div>
|
|
2007
|
-
<h3 className="font-semibold">{f.title}</h3>
|
|
2008
|
-
<p className="mt-1 text-sm text-muted">{f.desc}</p>
|
|
2009
|
-
</div>
|
|
2049
|
+
<FeatureCard key={f.title} icon={f.icon} title={f.title} desc={f.desc} />
|
|
2010
2050
|
))}
|
|
2011
2051
|
</div>
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
</
|
|
2052
|
+
</section>
|
|
2053
|
+
<section className="mx-auto max-w-5xl px-6 pb-8">
|
|
2054
|
+
<h2 className="mb-8 text-center text-2xl font-bold">How it works</h2>
|
|
2055
|
+
<Steps items={[
|
|
2056
|
+
{ title: "Connect", desc: "Sign up and link the tools you already use." },
|
|
2057
|
+
{ title: "Configure", desc: "Set it up your way in a few clicks \u2014 no code required." },
|
|
2058
|
+
{ title: "Ship", desc: "Go live and watch the numbers in real time." },
|
|
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
|
+
]} />
|
|
2015
2070
|
</section>`
|
|
2016
2071
|
});
|
|
2017
2072
|
var workPage = (ctx) => pageFile({
|
|
2018
2073
|
title: "Work",
|
|
2019
2074
|
path: "/work",
|
|
2020
2075
|
description: `Selected projects by ${ctx.template.siteName}.`,
|
|
2076
|
+
cta: { title: "Have something in mind?", subtitle: "I'm currently open to new projects.", label: "Get in touch", href: "/contact" },
|
|
2021
2077
|
body: ` <section className="mx-auto max-w-3xl px-6 py-24">
|
|
2022
|
-
<
|
|
2023
|
-
<h1 className="mt-
|
|
2078
|
+
<Pill>Work</Pill>
|
|
2079
|
+
<h1 className="mt-4 text-4xl font-bold sm:text-5xl">Selected <span className="gradient-text">work</span></h1>
|
|
2024
2080
|
<p className="mt-4 text-lg text-muted">A few things I've designed and built recently.</p>
|
|
2025
2081
|
</section>
|
|
2026
2082
|
<section className="mx-auto max-w-6xl px-6 pb-24">
|
|
@@ -2049,9 +2105,10 @@ var menuPage = (ctx) => pageFile({
|
|
|
2049
2105
|
title: "Menu",
|
|
2050
2106
|
path: "/menu",
|
|
2051
2107
|
description: `The menu at ${ctx.template.siteName} \u2014 seasonal plates and natural wine.`,
|
|
2108
|
+
cta: { title: "Hungry?", subtitle: "Book a table \u2014 we can't wait to cook for you.", label: "Reserve a table", href: "/reservations" },
|
|
2052
2109
|
body: ` <section className="mx-auto max-w-3xl px-6 py-24 text-center">
|
|
2053
|
-
<
|
|
2054
|
-
<h1 className="mt-
|
|
2110
|
+
<Pill>Menu</Pill>
|
|
2111
|
+
<h1 className="mt-4 text-4xl font-bold sm:text-5xl gradient-text">Tonight's menu</h1>
|
|
2055
2112
|
<p className="mx-auto mt-4 max-w-xl text-lg text-muted">Seasonal, ingredient-led and always changing. Here's what we're serving now.</p>
|
|
2056
2113
|
</section>
|
|
2057
2114
|
<section className="mx-auto max-w-3xl px-6 pb-24">
|
|
@@ -2072,18 +2129,16 @@ var menuPage = (ctx) => pageFile({
|
|
|
2072
2129
|
</div>
|
|
2073
2130
|
</div>
|
|
2074
2131
|
))}
|
|
2075
|
-
<div className="text-center">
|
|
2076
|
-
<Link href="/reservations" className="inline-block rounded-full gradient-bg px-8 py-3 font-semibold text-black">Book a table</Link>
|
|
2077
|
-
</div>
|
|
2078
2132
|
</section>`
|
|
2079
2133
|
});
|
|
2080
2134
|
var cardGridPage = (o) => pageFile({
|
|
2081
2135
|
title: o.title,
|
|
2082
2136
|
path: o.path,
|
|
2083
2137
|
description: o.lead,
|
|
2138
|
+
cta: o.cta,
|
|
2084
2139
|
body: ` <section className="mx-auto max-w-3xl px-6 py-24">
|
|
2085
|
-
<
|
|
2086
|
-
<h1 className="mt-
|
|
2140
|
+
<Pill>${o.eyebrow}</Pill>
|
|
2141
|
+
<h1 className="mt-4 text-4xl font-bold sm:text-5xl">${o.heading}</h1>
|
|
2087
2142
|
<p className="mt-4 text-lg text-muted">${o.lead}</p>
|
|
2088
2143
|
</section>
|
|
2089
2144
|
<section className="mx-auto max-w-6xl px-6 pb-24">
|
|
@@ -2103,6 +2158,7 @@ var topicsPage = (ctx) => cardGridPage({
|
|
|
2103
2158
|
eyebrow: "Topics",
|
|
2104
2159
|
heading: "Browse by topic",
|
|
2105
2160
|
lead: "Find what you care about \u2014 from deep dives to quick notes.",
|
|
2161
|
+
cta: { title: "Never miss a post", subtitle: "Subscribe and get new pieces in your inbox.", label: "Join the newsletter", href: "/newsletter" },
|
|
2106
2162
|
items: [
|
|
2107
2163
|
{ title: "Engineering", desc: "How we build and the decisions behind it." },
|
|
2108
2164
|
{ title: "Design", desc: "Craft, systems and the details that matter." },
|
|
@@ -2118,6 +2174,7 @@ var guidesPage = (ctx) => cardGridPage({
|
|
|
2118
2174
|
eyebrow: "Guides",
|
|
2119
2175
|
heading: "Guides & tutorials",
|
|
2120
2176
|
lead: "Task-focused walkthroughs to get you productive fast.",
|
|
2177
|
+
cta: { title: "Can't find what you need?", subtitle: "We're happy to help \u2014 reach out any time.", label: "Contact us", href: "/contact" },
|
|
2121
2178
|
items: [
|
|
2122
2179
|
{ title: "Getting started", desc: "Install, configure and run your first build." },
|
|
2123
2180
|
{ title: "Authentication", desc: "Add sign-in, sessions and protected routes." },
|
|
@@ -2127,6 +2184,51 @@ var guidesPage = (ctx) => cardGridPage({
|
|
|
2127
2184
|
{ title: "Troubleshooting", desc: "Fix the most common issues quickly." }
|
|
2128
2185
|
]
|
|
2129
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
|
+
`;
|
|
2130
2232
|
var shopPage = (ctx) => `import type { Metadata } from "next";
|
|
2131
2233
|
import { site } from "@/lib/site";
|
|
2132
2234
|
import { ProductGrid } from "@/components/product-grid";
|
|
@@ -2301,6 +2403,174 @@ export function SettingsPanel() {
|
|
|
2301
2403
|
);
|
|
2302
2404
|
}
|
|
2303
2405
|
`;
|
|
2406
|
+
var uiKitFiles = () => ({
|
|
2407
|
+
"components/ui/section.tsx": `import type { ReactNode } from "react";
|
|
2408
|
+
|
|
2409
|
+
export function Section({ eyebrow, title, lead, children, className = "" }: { eyebrow?: string; title?: string; lead?: string; children?: ReactNode; className?: string }) {
|
|
2410
|
+
return (
|
|
2411
|
+
<section className={"mx-auto max-w-6xl px-6 py-16 " + className}>
|
|
2412
|
+
{eyebrow ? <p className="text-sm font-semibold uppercase tracking-widest text-faint">{eyebrow}</p> : null}
|
|
2413
|
+
{title ? <h2 className="mt-3 text-3xl font-bold sm:text-4xl">{title}</h2> : null}
|
|
2414
|
+
{lead ? <p className="mt-4 max-w-2xl text-lg text-muted">{lead}</p> : null}
|
|
2415
|
+
{children ? <div className="mt-10">{children}</div> : null}
|
|
2416
|
+
</section>
|
|
2417
|
+
);
|
|
2418
|
+
}
|
|
2419
|
+
`,
|
|
2420
|
+
"components/ui/pill.tsx": `import type { ReactNode } from "react";
|
|
2421
|
+
|
|
2422
|
+
export function Pill({ children, className = "" }: { children: ReactNode; className?: string }) {
|
|
2423
|
+
return <span className={"inline-flex items-center gap-1 rounded-full border border-hairline bg-surface px-3 py-1 text-xs font-semibold uppercase tracking-widest text-muted " + className}>{children}</span>;
|
|
2424
|
+
}
|
|
2425
|
+
`,
|
|
2426
|
+
"components/ui/stat-card.tsx": `export function StatCard({ label, value, delta }: { label: string; value: string; delta?: string }) {
|
|
2427
|
+
return (
|
|
2428
|
+
<div className="rounded-2xl border border-hairline bg-surface p-6 transition hover:-translate-y-1">
|
|
2429
|
+
<div className="text-3xl font-bold tabular-nums">{value}</div>
|
|
2430
|
+
<div className="mt-1 text-sm text-muted">{label}</div>
|
|
2431
|
+
{delta ? <div className="mt-2 text-xs font-medium text-emerald-400">{delta}</div> : null}
|
|
2432
|
+
</div>
|
|
2433
|
+
);
|
|
2434
|
+
}
|
|
2435
|
+
`,
|
|
2436
|
+
"components/ui/feature-card.tsx": `import type { ReactNode } from "react";
|
|
2437
|
+
|
|
2438
|
+
export function FeatureCard({ icon, title, desc }: { icon?: ReactNode; title: string; desc: string }) {
|
|
2439
|
+
return (
|
|
2440
|
+
<div className="rounded-2xl border border-hairline bg-surface p-6 transition hover:-translate-y-1 hover:border-[color:var(--accent-to)]">
|
|
2441
|
+
{icon ? <div className="mb-4 inline-flex h-12 w-12 items-center justify-center rounded-xl gradient-bg text-2xl">{icon}</div> : null}
|
|
2442
|
+
<h3 className="font-semibold">{title}</h3>
|
|
2443
|
+
<p className="mt-1 text-sm leading-relaxed text-muted">{desc}</p>
|
|
2444
|
+
</div>
|
|
2445
|
+
);
|
|
2446
|
+
}
|
|
2447
|
+
`,
|
|
2448
|
+
"components/ui/testimonial.tsx": `export function Testimonial({ quote, author, role }: { quote: string; author: string; role?: string }) {
|
|
2449
|
+
return (
|
|
2450
|
+
<figure className="mx-auto max-w-2xl rounded-3xl border border-hairline bg-surface p-8 text-center">
|
|
2451
|
+
<div className="text-4xl leading-none gradient-text">“</div>
|
|
2452
|
+
<blockquote className="mt-2 text-lg font-medium leading-relaxed">{quote}</blockquote>
|
|
2453
|
+
<figcaption className="mt-4 text-sm text-muted">\u2014 {author}{role ? ", " + role : ""}</figcaption>
|
|
2454
|
+
</figure>
|
|
2455
|
+
);
|
|
2456
|
+
}
|
|
2457
|
+
`,
|
|
2458
|
+
"components/ui/steps.tsx": `export function Steps({ items }: { items: { title: string; desc: string }[] }) {
|
|
2459
|
+
return (
|
|
2460
|
+
<ol className="grid gap-6 sm:grid-cols-3">
|
|
2461
|
+
{items.map((s, i) => (
|
|
2462
|
+
<li key={s.title} className="rounded-2xl border border-hairline bg-surface p-6">
|
|
2463
|
+
<div className="mb-3 inline-flex h-8 w-8 items-center justify-center rounded-full gradient-bg text-sm font-bold text-black">{i + 1}</div>
|
|
2464
|
+
<h3 className="font-semibold">{s.title}</h3>
|
|
2465
|
+
<p className="mt-1 text-sm text-muted">{s.desc}</p>
|
|
2466
|
+
</li>
|
|
2467
|
+
))}
|
|
2468
|
+
</ol>
|
|
2469
|
+
);
|
|
2470
|
+
}
|
|
2471
|
+
`,
|
|
2472
|
+
"components/ui/cta-band.tsx": `import Link from "next/link";
|
|
2473
|
+
|
|
2474
|
+
export function CTABand({ title, subtitle, ctaLabel = "Get started", ctaHref = "/contact" }: { title: string; subtitle?: string; ctaLabel?: string; ctaHref?: string }) {
|
|
2475
|
+
return (
|
|
2476
|
+
<section className="mx-auto max-w-6xl px-6 py-20">
|
|
2477
|
+
<div className="relative overflow-hidden rounded-3xl border border-hairline bg-surface p-10 text-center sm:p-16">
|
|
2478
|
+
<div aria-hidden className="pointer-events-none absolute -right-16 -top-16 h-56 w-56 rounded-full gradient-bg opacity-40 blur-3xl animate-floaty" />
|
|
2479
|
+
<div aria-hidden className="pointer-events-none absolute -bottom-16 -left-16 h-56 w-56 rounded-full gradient-bg opacity-30 blur-3xl animate-floaty-slow" />
|
|
2480
|
+
<h2 className="relative text-3xl font-bold sm:text-4xl">{title}</h2>
|
|
2481
|
+
{subtitle ? <p className="relative mx-auto mt-3 max-w-xl text-muted">{subtitle}</p> : null}
|
|
2482
|
+
<Link href={ctaHref} className="relative mt-8 inline-block rounded-full gradient-bg px-8 py-3 font-semibold text-black transition hover:opacity-90">{ctaLabel}</Link>
|
|
2483
|
+
</div>
|
|
2484
|
+
</section>
|
|
2485
|
+
);
|
|
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
|
+
}
|
|
2561
|
+
`,
|
|
2562
|
+
"components/ui/index.ts": `export * from "./section";
|
|
2563
|
+
export * from "./pill";
|
|
2564
|
+
export * from "./stat-card";
|
|
2565
|
+
export * from "./feature-card";
|
|
2566
|
+
export * from "./testimonial";
|
|
2567
|
+
export * from "./steps";
|
|
2568
|
+
export * from "./cta-band";
|
|
2569
|
+
export * from "./bento";
|
|
2570
|
+
export * from "./area-chart";
|
|
2571
|
+
export * from "./newsletter";
|
|
2572
|
+
`
|
|
2573
|
+
});
|
|
2304
2574
|
function realPageFiles(ctx) {
|
|
2305
2575
|
const k = ctx.template.key;
|
|
2306
2576
|
const f = {};
|
|
@@ -2319,12 +2589,16 @@ function realPageFiles(ctx) {
|
|
|
2319
2589
|
f["components/product-grid.tsx"] = productGrid();
|
|
2320
2590
|
f["components/cart-view.tsx"] = cartView();
|
|
2321
2591
|
}
|
|
2322
|
-
if (k === "blog")
|
|
2592
|
+
if (k === "blog") {
|
|
2593
|
+
f["app/topics/page.tsx"] = topicsPage();
|
|
2594
|
+
f["app/newsletter/page.tsx"] = newsletterPage(ctx);
|
|
2595
|
+
}
|
|
2323
2596
|
if (k === "docs") f["app/guides/page.tsx"] = guidesPage();
|
|
2324
2597
|
if (k === "restaurant") f["app/menu/page.tsx"] = menuPage(ctx);
|
|
2325
2598
|
if (k === "dashboard") {
|
|
2326
2599
|
f["app/settings/page.tsx"] = settingsPage(ctx);
|
|
2327
2600
|
f["components/settings-panel.tsx"] = settingsPanel();
|
|
2601
|
+
f["app/analytics/page.tsx"] = analyticsPage(ctx);
|
|
2328
2602
|
}
|
|
2329
2603
|
return f;
|
|
2330
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": {
|