create-lacspace-app 1.16.0 → 1.17.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/README.md +15 -9
- package/dist/index.js +886 -2
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -30,7 +30,7 @@ Same machine, both CLIs run **binary-direct, scaffold-only**, median of 7 runs:
|
|
|
30
30
|
| **Files produced** | 18 | **70** |
|
|
31
31
|
| **Pages** | 1 (blank) | **11** (finished, 5–6 sections each) |
|
|
32
32
|
| **Prebuilt components** | 0 | **37** (incl. a 26-component UI kit) |
|
|
33
|
-
| **Templates** | 1 | **
|
|
33
|
+
| **Templates** | 1 | **9** |
|
|
34
34
|
| SEO metadata + JSON-LD | ✗ | ✓ |
|
|
35
35
|
| Dynamic OG images (`/og`) | ✗ | ✓ |
|
|
36
36
|
| `sitemap.xml` + `robots.txt` | ✗ | ✓ |
|
|
@@ -48,7 +48,7 @@ Same machine, both CLIs run **binary-direct, scaffold-only**, median of 7 runs:
|
|
|
48
48
|
|
|
49
49
|
Every generated app arrives **done**, not started:
|
|
50
50
|
|
|
51
|
-
- 🧩 **
|
|
51
|
+
- 🧩 **9 templates**, each a complete site — `personal`, `business`, `ecommerce`, `saas`, `blog`, `docs`, `dashboard`, `restaurant`, `marketplace`.
|
|
52
52
|
- 📄 **Every page is filled** — home, about, pricing, services, features, FAQ, careers, work/case-studies, collections, integrations, changelog, gallery, reservations, and more. Each is **5–6 real sections** (hero → feature split with illustration → stat band → feature grid → showcase → CTA), personalised with your project name. No "coming soon" stubs.
|
|
53
53
|
- 🎨 **A 26-component UI kit** (`components/ui/`): `Section`, `Hero`, `FeatureCard`, `FeatureSplit`, `Bento`, `PricingTable`, `Testimonial`, `StatBand`, `Timeline`, `Steps`, `Tabs`, `Accordion`, `FAQ`, `Gallery`, `TeamGrid`, `LogoCloud`, `CTABand`, `Newsletter`, `AreaChart`, `Badge`, `Callout`, `Rating`, `Progress`, `Breadcrumbs`, `Avatar` … all theme-aware, dependency-free, server-first.
|
|
54
54
|
- 🔎 **SEO, done for you** — one [`@lacspace/seo`](https://www.npmjs.com/package/@lacspace/seo) `defineSite()` config drives `<title>`, canonical, Open Graph, Twitter, and JSON-LD across every route.
|
|
@@ -91,6 +91,7 @@ import { PricingSection } from "@/components/sections/pricing";
|
|
|
91
91
|
| **docs** | Docs site — guides, API reference, changelog, search |
|
|
92
92
|
| **dashboard** | App shell — sidebar, stat cards, charts, settings, analytics |
|
|
93
93
|
| **restaurant** | Menu, reservations, gallery, private events |
|
|
94
|
+
| **marketplace** | Real commerce — cart, product pages, checkout, tax + shipping, orders/invoices, eSewa & Khalti payments |
|
|
94
95
|
|
|
95
96
|
Every template is Next.js 15 App Router + React 19 + Tailwind v4 — dark, modern, responsive, with a themeable gradient accent.
|
|
96
97
|
|
|
@@ -98,7 +99,7 @@ Every template is Next.js 15 App Router + React 19 + Tailwind v4 — dark, moder
|
|
|
98
99
|
|
|
99
100
|
| Flag | Meaning |
|
|
100
101
|
| --- | --- |
|
|
101
|
-
| `-t, --template <key>` | `personal` · `business` · `ecommerce` · `saas` · `blog` · `docs` · `dashboard` · `restaurant` |
|
|
102
|
+
| `-t, --template <key>` | `personal` · `business` · `ecommerce` · `saas` · `blog` · `docs` · `dashboard` · `restaurant` · `marketplace` |
|
|
102
103
|
| `--theme <name\|hex>` | accent gradient — a preset, a `"#hex"`, or `"from,to"` (see below) |
|
|
103
104
|
| `--pm <npm\|pnpm\|yarn\|bun>` | package manager (default `npm`) |
|
|
104
105
|
| `--no-install` | skip installing dependencies |
|
|
@@ -119,16 +120,21 @@ npx create-lacspace-app my-app --theme "#0bb9d9,#7c3aed" # an exact fr
|
|
|
119
120
|
|
|
120
121
|
## Licensing
|
|
121
122
|
|
|
122
|
-
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** —
|
|
123
|
+
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — permissive freedoms. Use it in personal and commercial projects at no cost; the apps you generate are entirely yours.
|
|
124
|
+
|
|
125
|
+
<!-- LACSPACE-DEV-PLATFORM -->
|
|
123
126
|
|
|
124
127
|
---
|
|
125
128
|
|
|
126
|
-
|
|
129
|
+
## The Lacspace Developer Platform
|
|
127
130
|
|
|
128
|
-
|
|
131
|
+
`create-lacspace-app` is part of **63 zero-dependency, isomorphic TypeScript packages** — one standard library for the modern web. Explore the ecosystem:
|
|
129
132
|
|
|
130
|
-
|
|
133
|
+
- 🗂️ **All 63 packages** — https://developer.lacspace.com/packages
|
|
134
|
+
- 🧭 **Developer handbook** — guides & runnable recipes — https://developer.lacspace.com/handbook
|
|
135
|
+
- 🧪 **Live playground** — run any package in your browser — https://developer.lacspace.com/playground
|
|
136
|
+
- 🖥️ **Finished app templates** — https://templates.lacspace.com
|
|
137
|
+
- 🚀 **Scaffold a full app** — `npm create lacspace-app@latest`
|
|
131
138
|
|
|
132
|
-
|
|
139
|
+
Free under the **[Lacspace Free Licence](https://lacspace.com/licenses/lacspace-free-1.0)** — a permissive, free-to-use licence.
|
|
133
140
|
|
|
134
|
-
<div align="center"><sub>Built with care by <a href="https://lacspace.com">Lacspace</a> · Lacspace Free Licence</sub></div>
|
package/dist/index.js
CHANGED
|
@@ -24,7 +24,8 @@ var TEMPLATES = [
|
|
|
24
24
|
{ key: "blog", label: "Blog / magazine", description: "A clean editorial blog home with a featured post and a grid.", accent: ["#f97316", "#ef4444"], siteName: "The Journal", siteDescription: "Essays, notes and stories on building things that matter." },
|
|
25
25
|
{ key: "docs", label: "Documentation", description: "A docs landing with quick-start and feature cards.", accent: ["#0ea5e9", "#6366f1"], siteName: "Acme Docs", siteDescription: "Everything you need to build with Acme \u2014 guides, API and examples." },
|
|
26
26
|
{ key: "dashboard", label: "Admin dashboard", description: "An app dashboard shell with stat cards and a table.", accent: ["#10b981", "#14b8a6"], siteName: "Acme Admin", siteDescription: "Your control center \u2014 metrics, activity and management in one place." },
|
|
27
|
-
{ key: "restaurant", label: "Restaurant / cafe", description: "A warm restaurant home with menu highlights and reservations.", accent: ["#e11d48", "#f59e0b"], siteName: "Olive & Ember", siteDescription: "Seasonal plates, natural wine and a warm room. Book a table." }
|
|
27
|
+
{ key: "restaurant", label: "Restaurant / cafe", description: "A warm restaurant home with menu highlights and reservations.", accent: ["#e11d48", "#f59e0b"], siteName: "Olive & Ember", siteDescription: "Seasonal plates, natural wine and a warm room. Book a table." },
|
|
28
|
+
{ key: "marketplace", label: "Marketplace / commerce", description: "A real storefront wired to the Lacspace commerce packages \u2014 cart, checkout, tax, shipping, orders, invoices and Nepal payments.", accent: ["#0d9488", "#6366f1"], siteName: "Bazaar", siteDescription: "A modern storefront \u2014 cart to checkout, wired end to end." }
|
|
28
29
|
];
|
|
29
30
|
var pkgJson = (ctx) => JSON.stringify({
|
|
30
31
|
name: ctx.name,
|
|
@@ -51,7 +52,22 @@ var pkgJson = (ctx) => JSON.stringify({
|
|
|
51
52
|
"@lacspace/query": "^1.0.1",
|
|
52
53
|
// The blog & docs templates render Markdown with @lacspace/markdown
|
|
53
54
|
// (>=1.0.1 includes the URL-scheme XSS hardening — keep the floor there).
|
|
54
|
-
...ctx.template.key === "blog" || ctx.template.key === "docs" ? { "@lacspace/markdown": "^1.0.1" } : {}
|
|
55
|
+
...ctx.template.key === "blog" || ctx.template.key === "docs" ? { "@lacspace/markdown": "^1.0.1" } : {},
|
|
56
|
+
// The marketplace template composes the Lacspace commerce packages end to
|
|
57
|
+
// end: a headless cart, order + invoice engines, tax & shipping calculators,
|
|
58
|
+
// coupons, integer-safe money, ids, and the eSewa + Khalti payment gateways.
|
|
59
|
+
...ctx.template.key === "marketplace" ? {
|
|
60
|
+
"@lacspace/cart": "^1.0.0",
|
|
61
|
+
"@lacspace/order": "^1.0.0",
|
|
62
|
+
"@lacspace/tax": "^1.0.0",
|
|
63
|
+
"@lacspace/shipping": "^1.0.0",
|
|
64
|
+
"@lacspace/coupon": "^1.0.0",
|
|
65
|
+
"@lacspace/invoice": "^1.0.0",
|
|
66
|
+
"@lacspace/money": "^1.0.2",
|
|
67
|
+
"@lacspace/id": "^1.0.2",
|
|
68
|
+
"@lacspace/esewa": "^1.0.0",
|
|
69
|
+
"@lacspace/khalti": "^1.0.0"
|
|
70
|
+
} : {}
|
|
55
71
|
},
|
|
56
72
|
devDependencies: {
|
|
57
73
|
typescript: "^5.7.0",
|
|
@@ -426,6 +442,7 @@ Built with [Lacspace](https://lacspace.com/packages).
|
|
|
426
442
|
function homePage(ctx) {
|
|
427
443
|
const n = ctx.template.siteName;
|
|
428
444
|
if (ctx.template.key === "dashboard") return dashboardHome(ctx);
|
|
445
|
+
if (ctx.template.key === "marketplace") return marketplaceHome(ctx);
|
|
429
446
|
const shell = (inner) => `import { site } from "@/lib/site";
|
|
430
447
|
import { LiveStats } from "@/components/live-stats";
|
|
431
448
|
import { Aurora } from "@/components/aurora";
|
|
@@ -1775,6 +1792,9 @@ function buildFiles(ctx) {
|
|
|
1775
1792
|
}
|
|
1776
1793
|
Object.assign(files, uiKitFiles());
|
|
1777
1794
|
Object.assign(files, realPageFiles(ctx));
|
|
1795
|
+
if (ctx.template.key === "marketplace") {
|
|
1796
|
+
files[".env.example"] = marketEnvExample();
|
|
1797
|
+
}
|
|
1778
1798
|
if (isBlog) {
|
|
1779
1799
|
files["lib/posts.ts"] = postsLib();
|
|
1780
1800
|
files["app/blog/page.tsx"] = blogListPage(ctx);
|
|
@@ -1853,6 +1873,11 @@ function pagesFor(ctx) {
|
|
|
1853
1873
|
{ path: "/reservations", label: "Reservations", nav: true, group: "Product", real: true },
|
|
1854
1874
|
{ path: "/gallery", label: "Gallery", nav: true, group: "Product", real: true },
|
|
1855
1875
|
{ path: "/events", label: "Private events", group: "Resources", real: true }
|
|
1876
|
+
],
|
|
1877
|
+
marketplace: [
|
|
1878
|
+
{ path: "/shop", label: "Shop", nav: true, group: "Product", real: true },
|
|
1879
|
+
{ path: "/cart", label: "Cart", group: "Product", real: true },
|
|
1880
|
+
{ path: "/checkout", label: "Checkout", group: "Product", real: true }
|
|
1856
1881
|
]
|
|
1857
1882
|
};
|
|
1858
1883
|
return [...specific[k] ?? [], ...common];
|
|
@@ -1863,6 +1888,8 @@ function stubPages(ctx) {
|
|
|
1863
1888
|
var linkLiteral = (pages) => pages.map((l) => `{ href: ${JSON.stringify(l.path)}, label: ${JSON.stringify(l.label)} }`).join(", ");
|
|
1864
1889
|
var siteHeader = (ctx) => {
|
|
1865
1890
|
const isEcom = ctx.template.key === "ecommerce";
|
|
1891
|
+
const isMarket = ctx.template.key === "marketplace";
|
|
1892
|
+
if (isMarket) return marketHeader(ctx);
|
|
1866
1893
|
const cartImport = isEcom ? `
|
|
1867
1894
|
import { useCart } from "@/lib/store";
|
|
1868
1895
|
import { useIsMounted } from "@lacspace/hooks";` : "";
|
|
@@ -2626,6 +2653,834 @@ export function CartView() {
|
|
|
2626
2653
|
);
|
|
2627
2654
|
}
|
|
2628
2655
|
`;
|
|
2656
|
+
var productsLib = (ctx) => `// The catalogue for ${ctx.template.siteName}. Swap this for a database or CMS \u2014
|
|
2657
|
+
// every price is an integer in **paisa** (minor units), so there is never a
|
|
2658
|
+
// floating-point rounding bug. NPR 450.00 is written as 45000.
|
|
2659
|
+
|
|
2660
|
+
export interface Product {
|
|
2661
|
+
slug: string;
|
|
2662
|
+
name: string;
|
|
2663
|
+
/** Price in integer minor units (paisa). */
|
|
2664
|
+
price: number;
|
|
2665
|
+
/** A placeholder visual \u2014 swap for a real image URL when you have one. */
|
|
2666
|
+
emoji: string;
|
|
2667
|
+
/** Optional image URL (falls back to the emoji tile). */
|
|
2668
|
+
image?: string;
|
|
2669
|
+
blurb: string;
|
|
2670
|
+
/** Shipping weight in grams \u2014 drives the weight-based shipping rate. */
|
|
2671
|
+
weight: number;
|
|
2672
|
+
}
|
|
2673
|
+
|
|
2674
|
+
export const PRODUCTS: Product[] = [
|
|
2675
|
+
{ slug: "himalayan-gold-tea", name: "Himalayan Gold Tea", price: 45000, emoji: "\\u{1F375}", blurb: "Hand-picked orthodox black tea from the high hills \u2014 bright, malty and endlessly re-steepable.", weight: 250 },
|
|
2676
|
+
{ slug: "lokta-notebook", name: "Lokta Paper Notebook", price: 68000, emoji: "\\u{1F4D3}", blurb: "A5 notebook bound in handmade lokta paper. 160 pages that take fountain-pen ink beautifully.", weight: 320 },
|
|
2677
|
+
{ slug: "pashmina-scarf", name: "Pashmina Scarf", price: 320000, emoji: "\\u{1F9E3}", blurb: "Feather-light, ethically sourced pashmina, hand-loomed in a natural undyed grey.", weight: 180 },
|
|
2678
|
+
{ slug: "singing-bowl", name: "Hand-hammered Singing Bowl", price: 540000, emoji: "\\u{1F514}", blurb: "A seven-metal bowl with a long, resonant hum. Comes with a wooden striker and cushion.", weight: 900 },
|
|
2679
|
+
{ slug: "ceramic-mug", name: "Glazed Ceramic Mug", price: 52000, emoji: "\\u2615", blurb: "A chunky, wheel-thrown mug in a speckled reactive glaze. Holds a generous 350ml.", weight: 420 },
|
|
2680
|
+
{ slug: "wool-socks", name: "Merino Wool Socks", price: 38000, emoji: "\\u{1F9E6}", blurb: "Cushioned merino socks that stay warm even when damp \u2014 the ones you will keep reaching for.", weight: 120 },
|
|
2681
|
+
];
|
|
2682
|
+
|
|
2683
|
+
export function getProduct(slug: string): Product | undefined {
|
|
2684
|
+
return PRODUCTS.find((p) => p.slug === slug);
|
|
2685
|
+
}
|
|
2686
|
+
`;
|
|
2687
|
+
var commerceLib = (ctx) => `import { tax, RATES } from "@lacspace/tax";
|
|
2688
|
+
import { cheapestQuote, type ShippingMethod } from "@lacspace/shipping";
|
|
2689
|
+
import { createOrder, orderNumber, type Order } from "@lacspace/order";
|
|
2690
|
+
import { createInvoice, type Invoice } from "@lacspace/invoice";
|
|
2691
|
+
import { Money } from "@lacspace/money";
|
|
2692
|
+
|
|
2693
|
+
/** Everything internal is integer paisa; this is the only currency the store uses. */
|
|
2694
|
+
export const CURRENCY = "NPR";
|
|
2695
|
+
/** Nepal VAT \u2014 13% \u2014 straight from @lacspace/tax's RATES table. */
|
|
2696
|
+
export const VAT_RATE = RATES.NP_VAT;
|
|
2697
|
+
|
|
2698
|
+
/** Format integer paisa for display, e.g. 45000 -> "NPR 450.00". */
|
|
2699
|
+
export function formatMoney(minor: number): string {
|
|
2700
|
+
return Money.fromMinor(Math.round(minor), CURRENCY).format();
|
|
2701
|
+
}
|
|
2702
|
+
|
|
2703
|
+
/** A cart line the quote engine understands. All amounts are integer paisa. */
|
|
2704
|
+
export interface QuoteLine {
|
|
2705
|
+
id: string;
|
|
2706
|
+
name: string;
|
|
2707
|
+
unitPrice: number;
|
|
2708
|
+
qty: number;
|
|
2709
|
+
/** Per-unit weight in grams (drives shipping). */
|
|
2710
|
+
weight?: number;
|
|
2711
|
+
}
|
|
2712
|
+
|
|
2713
|
+
/**
|
|
2714
|
+
* The shipping methods offered at checkout. @lacspace/shipping rates each one
|
|
2715
|
+
* against the shipment (weight / subtotal / item count); "standard" ships free
|
|
2716
|
+
* once the order is large enough.
|
|
2717
|
+
*/
|
|
2718
|
+
export const SHIPPING_METHODS: ShippingMethod[] = [
|
|
2719
|
+
{
|
|
2720
|
+
id: "standard",
|
|
2721
|
+
label: "Standard (3-5 days)",
|
|
2722
|
+
strategy: "weight",
|
|
2723
|
+
bands: [
|
|
2724
|
+
{ min: 0, max: 500, cost: 8000 },
|
|
2725
|
+
{ min: 501, max: 2000, cost: 12000 },
|
|
2726
|
+
{ min: 2001, cost: 20000 },
|
|
2727
|
+
],
|
|
2728
|
+
freeOver: 500000,
|
|
2729
|
+
etaDays: [3, 5],
|
|
2730
|
+
},
|
|
2731
|
+
{ id: "express", label: "Express (1-2 days)", strategy: "flat", flat: 25000, etaDays: [1, 2] },
|
|
2732
|
+
];
|
|
2733
|
+
|
|
2734
|
+
/** The computed money breakdown for a cart. Every field is integer paisa. */
|
|
2735
|
+
export interface Quote {
|
|
2736
|
+
subtotal: number;
|
|
2737
|
+
tax: number;
|
|
2738
|
+
shipping: number;
|
|
2739
|
+
total: number;
|
|
2740
|
+
itemCount: number;
|
|
2741
|
+
currency: string;
|
|
2742
|
+
shippingMethod: string;
|
|
2743
|
+
/** Paisa still needed to unlock free standard shipping (0 once unlocked). */
|
|
2744
|
+
freeShippingRemaining: number;
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
const asQty = (n: number): number => Math.max(0, Math.trunc(n));
|
|
2748
|
+
|
|
2749
|
+
/** Compose subtotal + 13% VAT (@lacspace/tax) + cheapest shipping (@lacspace/shipping). */
|
|
2750
|
+
export function quote(lines: QuoteLine[]): Quote {
|
|
2751
|
+
const subtotal = lines.reduce((sum, l) => sum + l.unitPrice * asQty(l.qty), 0);
|
|
2752
|
+
const weight = lines.reduce((sum, l) => sum + (l.weight ?? 0) * asQty(l.qty), 0);
|
|
2753
|
+
const itemCount = lines.reduce((sum, l) => sum + asQty(l.qty), 0);
|
|
2754
|
+
|
|
2755
|
+
const vat = tax(subtotal, { rate: VAT_RATE });
|
|
2756
|
+
const ship = cheapestQuote(SHIPPING_METHODS, { weight, subtotal, itemCount });
|
|
2757
|
+
const shipping = ship?.cost ?? 0;
|
|
2758
|
+
const total = subtotal + vat.tax + shipping;
|
|
2759
|
+
const freeOver = 500000;
|
|
2760
|
+
|
|
2761
|
+
return {
|
|
2762
|
+
subtotal,
|
|
2763
|
+
tax: vat.tax,
|
|
2764
|
+
shipping,
|
|
2765
|
+
total,
|
|
2766
|
+
itemCount,
|
|
2767
|
+
currency: CURRENCY,
|
|
2768
|
+
shippingMethod: ship?.label ?? "-",
|
|
2769
|
+
freeShippingRemaining: Math.max(0, freeOver - subtotal),
|
|
2770
|
+
};
|
|
2771
|
+
}
|
|
2772
|
+
|
|
2773
|
+
export interface BuildOrderInput {
|
|
2774
|
+
lines: QuoteLine[];
|
|
2775
|
+
customer?: { name?: string; email?: string };
|
|
2776
|
+
paymentMethod: string;
|
|
2777
|
+
/** Sequence number for the human-facing order number. */
|
|
2778
|
+
seq?: number;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
export interface BuiltOrder {
|
|
2782
|
+
order: Order;
|
|
2783
|
+
invoice: Invoice;
|
|
2784
|
+
quote: Quote;
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
/** Build an immutable Order (+ Invoice) from cart lines. Used by the checkout API. */
|
|
2788
|
+
export function buildOrder(input: BuildOrderInput): BuiltOrder {
|
|
2789
|
+
const q = quote(input.lines);
|
|
2790
|
+
const seq = input.seq ?? Math.floor(Math.random() * 9000) + 1000;
|
|
2791
|
+
const number = orderNumber(seq, { prefix: "BZR" });
|
|
2792
|
+
|
|
2793
|
+
const order = createOrder({
|
|
2794
|
+
number,
|
|
2795
|
+
currency: CURRENCY,
|
|
2796
|
+
lines: input.lines.map((l) => ({ sku: l.id, name: l.name, unitPrice: l.unitPrice, qty: asQty(l.qty) })),
|
|
2797
|
+
tax: q.tax,
|
|
2798
|
+
shipping: q.shipping,
|
|
2799
|
+
customer: input.customer,
|
|
2800
|
+
meta: { paymentMethod: input.paymentMethod, shippingMethod: q.shippingMethod },
|
|
2801
|
+
});
|
|
2802
|
+
|
|
2803
|
+
const invoice = createInvoice({
|
|
2804
|
+
number: number.replace("BZR", "INV"),
|
|
2805
|
+
currency: CURRENCY,
|
|
2806
|
+
seller: { name: ${JSON.stringify(ctx.template.siteName)}, email: "orders@example.com" },
|
|
2807
|
+
buyer: { name: input.customer?.name ?? "Guest", email: input.customer?.email },
|
|
2808
|
+
lines: input.lines.map((l) => ({ description: l.name, qty: asQty(l.qty), unitPrice: l.unitPrice, taxRate: VAT_RATE })),
|
|
2809
|
+
status: "issued",
|
|
2810
|
+
issuedAt: Date.now(),
|
|
2811
|
+
notes: "Paid via " + input.paymentMethod + ". Shipping: " + q.shippingMethod + ".",
|
|
2812
|
+
});
|
|
2813
|
+
|
|
2814
|
+
return { order, invoice, quote: q };
|
|
2815
|
+
}
|
|
2816
|
+
`;
|
|
2817
|
+
var cartStore = () => `"use client";
|
|
2818
|
+
|
|
2819
|
+
import { create, persist } from "@lacspace/store";
|
|
2820
|
+
import { createCart, addItem, setQty, removeItem, type Cart } from "@lacspace/cart";
|
|
2821
|
+
|
|
2822
|
+
const EMPTY: Cart = createCart({ currency: "NPR" });
|
|
2823
|
+
|
|
2824
|
+
export interface AddInput {
|
|
2825
|
+
id: string;
|
|
2826
|
+
name: string;
|
|
2827
|
+
unitPrice: number;
|
|
2828
|
+
qty?: number;
|
|
2829
|
+
meta?: Record<string, unknown>;
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
interface CartStore {
|
|
2833
|
+
cart: Cart;
|
|
2834
|
+
add: (item: AddInput) => void;
|
|
2835
|
+
setQty: (id: string, qty: number) => void;
|
|
2836
|
+
remove: (id: string) => void;
|
|
2837
|
+
clear: () => void;
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
// @lacspace/cart is pure & immutable \u2014 every op returns a brand-new Cart, which
|
|
2841
|
+
// is exactly what @lacspace/store wants. persist() guards SSR (no localStorage
|
|
2842
|
+
// on the server), so this is safe to import anywhere.
|
|
2843
|
+
export const useCart = create<CartStore>(
|
|
2844
|
+
persist(
|
|
2845
|
+
(set, get) => ({
|
|
2846
|
+
cart: EMPTY,
|
|
2847
|
+
add: (item) =>
|
|
2848
|
+
set({
|
|
2849
|
+
cart: addItem(get().cart, {
|
|
2850
|
+
id: item.id,
|
|
2851
|
+
name: item.name,
|
|
2852
|
+
unitPrice: item.unitPrice,
|
|
2853
|
+
qty: item.qty ?? 1,
|
|
2854
|
+
meta: item.meta,
|
|
2855
|
+
}),
|
|
2856
|
+
}),
|
|
2857
|
+
setQty: (id, qty) => set({ cart: setQty(get().cart, id, qty) }),
|
|
2858
|
+
remove: (id) => set({ cart: removeItem(get().cart, id) }),
|
|
2859
|
+
clear: () => set({ cart: createCart({ currency: "NPR" }) }),
|
|
2860
|
+
}),
|
|
2861
|
+
{ name: "market-cart" },
|
|
2862
|
+
),
|
|
2863
|
+
);
|
|
2864
|
+
`;
|
|
2865
|
+
var cartButton = () => `"use client";
|
|
2866
|
+
|
|
2867
|
+
import Link from "next/link";
|
|
2868
|
+
import { useCart } from "@/components/cart-store";
|
|
2869
|
+
import { useIsMounted } from "@lacspace/hooks";
|
|
2870
|
+
|
|
2871
|
+
export function CartButton() {
|
|
2872
|
+
const count = useCart((s) => s.cart.items.reduce((n, i) => n + i.qty, 0));
|
|
2873
|
+
const mounted = useIsMounted();
|
|
2874
|
+
return (
|
|
2875
|
+
<Link href="/cart" aria-label="Cart" className="relative inline-flex h-9 w-9 items-center justify-center rounded-full border border-hairline text-muted transition hover:text-fg">
|
|
2876
|
+
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden><circle cx="9" cy="21" r="1" /><circle cx="20" cy="21" r="1" /><path d="M1 1h4l2.68 13.39a2 2 0 0 0 2 1.61h9.72a2 2 0 0 0 2-1.61L23 6H6" /></svg>
|
|
2877
|
+
{mounted() && count > 0 ? (
|
|
2878
|
+
<span className="absolute -right-1 -top-1 inline-flex h-4 min-w-4 items-center justify-center rounded-full gradient-bg px-1 text-[10px] font-bold text-black">{count}</span>
|
|
2879
|
+
) : null}
|
|
2880
|
+
</Link>
|
|
2881
|
+
);
|
|
2882
|
+
}
|
|
2883
|
+
`;
|
|
2884
|
+
var addToCart = () => `"use client";
|
|
2885
|
+
|
|
2886
|
+
import { useState } from "react";
|
|
2887
|
+
import { useCart } from "@/components/cart-store";
|
|
2888
|
+
|
|
2889
|
+
export interface AddToCartProduct {
|
|
2890
|
+
id: string;
|
|
2891
|
+
name: string;
|
|
2892
|
+
unitPrice: number;
|
|
2893
|
+
weight?: number;
|
|
2894
|
+
emoji?: string;
|
|
2895
|
+
}
|
|
2896
|
+
|
|
2897
|
+
export function AddToCart({ product, className = "" }: { product: AddToCartProduct; className?: string }) {
|
|
2898
|
+
const add = useCart((s) => s.add);
|
|
2899
|
+
const [added, setAdded] = useState(false);
|
|
2900
|
+
return (
|
|
2901
|
+
<button
|
|
2902
|
+
type="button"
|
|
2903
|
+
onClick={() => {
|
|
2904
|
+
add({
|
|
2905
|
+
id: product.id,
|
|
2906
|
+
name: product.name,
|
|
2907
|
+
unitPrice: product.unitPrice,
|
|
2908
|
+
qty: 1,
|
|
2909
|
+
meta: { weight: product.weight ?? 0, emoji: product.emoji ?? "" },
|
|
2910
|
+
});
|
|
2911
|
+
setAdded(true);
|
|
2912
|
+
window.setTimeout(() => setAdded(false), 1200);
|
|
2913
|
+
}}
|
|
2914
|
+
className={"rounded-full gradient-bg px-4 py-2 text-sm font-semibold text-black transition hover:opacity-90 " + className}
|
|
2915
|
+
>
|
|
2916
|
+
{added ? "Added \\u2713" : "Add to cart"}
|
|
2917
|
+
</button>
|
|
2918
|
+
);
|
|
2919
|
+
}
|
|
2920
|
+
`;
|
|
2921
|
+
var marketHeader = (ctx) => `"use client";
|
|
2922
|
+
|
|
2923
|
+
import Link from "next/link";
|
|
2924
|
+
import { useUI } from "@/lib/store";
|
|
2925
|
+
import { ThemeToggle } from "./theme-toggle";
|
|
2926
|
+
import { CartButton } from "./cart-button";
|
|
2927
|
+
|
|
2928
|
+
const LINKS = [${linkLiteral(pagesFor(ctx).filter((p) => p.nav))}];
|
|
2929
|
+
|
|
2930
|
+
export function SiteHeader() {
|
|
2931
|
+
const open = useUI((s) => s.navOpen);
|
|
2932
|
+
const toggle = useUI((s) => s.toggleNav);
|
|
2933
|
+
const setOpen = useUI((s) => s.setNavOpen);
|
|
2934
|
+
return (
|
|
2935
|
+
<header className="sticky top-0 z-40 border-b border-hairline bg-app/90 backdrop-blur">
|
|
2936
|
+
<nav className="mx-auto flex max-w-6xl items-center justify-between px-6 py-4">
|
|
2937
|
+
<Link href="/" className="text-lg font-black gradient-text">${ctx.template.siteName}</Link>
|
|
2938
|
+
<div className="hidden items-center gap-6 md:flex">
|
|
2939
|
+
{LINKS.map((l) => (
|
|
2940
|
+
<Link key={l.href} href={l.href} className="text-sm text-muted transition hover:text-fg">{l.label}</Link>
|
|
2941
|
+
))}
|
|
2942
|
+
<ThemeToggle />
|
|
2943
|
+
<CartButton />
|
|
2944
|
+
</div>
|
|
2945
|
+
<div className="flex items-center gap-2 md:hidden">
|
|
2946
|
+
<ThemeToggle />
|
|
2947
|
+
<CartButton />
|
|
2948
|
+
<button type="button" aria-label="Menu" onClick={toggle} className="inline-flex h-9 w-9 items-center justify-center rounded-full border border-hairline text-muted transition hover:text-fg">
|
|
2949
|
+
<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden><path d="M3 6h18M3 12h18M3 18h18" /></svg>
|
|
2950
|
+
</button>
|
|
2951
|
+
</div>
|
|
2952
|
+
</nav>
|
|
2953
|
+
{open ? (
|
|
2954
|
+
<div className="border-t border-hairline md:hidden">
|
|
2955
|
+
<div className="mx-auto flex max-w-6xl flex-col gap-1 px-6 py-3">
|
|
2956
|
+
{LINKS.map((l) => (
|
|
2957
|
+
<Link key={l.href} href={l.href} onClick={() => setOpen(false)} className="rounded-lg px-3 py-2 text-muted transition hover:bg-surface hover:text-fg">{l.label}</Link>
|
|
2958
|
+
))}
|
|
2959
|
+
</div>
|
|
2960
|
+
</div>
|
|
2961
|
+
) : null}
|
|
2962
|
+
</header>
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2965
|
+
`;
|
|
2966
|
+
var marketplaceHome = (ctx) => {
|
|
2967
|
+
const n = ctx.template.siteName;
|
|
2968
|
+
return `import { site } from "@/lib/site";
|
|
2969
|
+
import Link from "next/link";
|
|
2970
|
+
import { Aurora } from "@/components/aurora";
|
|
2971
|
+
import { HeroArt } from "@/components/hero-art";
|
|
2972
|
+
import { LiveStats } from "@/components/live-stats";
|
|
2973
|
+
import { AddToCart } from "@/components/add-to-cart";
|
|
2974
|
+
import { PRODUCTS } from "@/lib/products";
|
|
2975
|
+
import { formatMoney } from "@/lib/commerce";
|
|
2976
|
+
|
|
2977
|
+
export const metadata = site.meta({ title: ${JSON.stringify(n)}, path: "/" });
|
|
2978
|
+
|
|
2979
|
+
export default function Home() {
|
|
2980
|
+
const featured = PRODUCTS.slice(0, 3);
|
|
2981
|
+
return (
|
|
2982
|
+
<main className="relative min-h-screen overflow-hidden">
|
|
2983
|
+
<Aurora />
|
|
2984
|
+
<section className="mx-auto grid max-w-6xl items-center gap-12 px-6 py-24 md:grid-cols-2">
|
|
2985
|
+
<div>
|
|
2986
|
+
<p className="mb-4 text-sm font-semibold uppercase tracking-widest text-faint">Storefront</p>
|
|
2987
|
+
<h1 className="text-5xl font-black leading-tight sm:text-6xl">Shop <span className="gradient-text">${n}</span></h1>
|
|
2988
|
+
<p className="mt-6 max-w-md text-lg text-muted">${ctx.template.siteDescription}</p>
|
|
2989
|
+
<div className="mt-10 flex gap-4">
|
|
2990
|
+
<Link href="/shop" className="gradient-bg rounded-full px-8 py-3 font-semibold text-black">Shop the collection</Link>
|
|
2991
|
+
<Link href="/cart" className="rounded-full border border-hairline px-8 py-3 font-semibold hover:bg-surface">View cart</Link>
|
|
2992
|
+
</div>
|
|
2993
|
+
<p className="mt-6 text-sm text-muted">Free standard shipping over {formatMoney(500000)}.</p>
|
|
2994
|
+
</div>
|
|
2995
|
+
<HeroArt />
|
|
2996
|
+
</section>
|
|
2997
|
+
<section className="mx-auto max-w-6xl px-6 py-16">
|
|
2998
|
+
<div className="mb-10 flex items-end justify-between">
|
|
2999
|
+
<h2 className="text-3xl font-bold">Featured</h2>
|
|
3000
|
+
<Link href="/shop" className="text-sm text-muted underline underline-offset-4 hover:text-fg">See all</Link>
|
|
3001
|
+
</div>
|
|
3002
|
+
<div className="grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
3003
|
+
{featured.map((p) => (
|
|
3004
|
+
<div key={p.slug} className="group flex flex-col rounded-2xl border border-hairline bg-surface p-5 transition hover:-translate-y-1">
|
|
3005
|
+
<Link href={"/product/" + p.slug} className="mb-4 flex aspect-square items-center justify-center rounded-xl gradient-bg text-6xl">{p.emoji}</Link>
|
|
3006
|
+
<Link href={"/product/" + p.slug} className="font-semibold group-hover:opacity-90">{p.name}</Link>
|
|
3007
|
+
<div className="mt-1 text-muted">{formatMoney(p.price)}</div>
|
|
3008
|
+
<div className="mt-4">
|
|
3009
|
+
<AddToCart product={{ id: p.slug, name: p.name, unitPrice: p.price, weight: p.weight, emoji: p.emoji }} className="w-full" />
|
|
3010
|
+
</div>
|
|
3011
|
+
</div>
|
|
3012
|
+
))}
|
|
3013
|
+
</div>
|
|
3014
|
+
</section>
|
|
3015
|
+
${builtWithSection(ctx)}
|
|
3016
|
+
</main>
|
|
3017
|
+
);
|
|
3018
|
+
}
|
|
3019
|
+
`;
|
|
3020
|
+
};
|
|
3021
|
+
var marketShopPage = (ctx) => `import type { Metadata } from "next";
|
|
3022
|
+
import Link from "next/link";
|
|
3023
|
+
import { site } from "@/lib/site";
|
|
3024
|
+
import { PRODUCTS } from "@/lib/products";
|
|
3025
|
+
import { formatMoney } from "@/lib/commerce";
|
|
3026
|
+
import { AddToCart } from "@/components/add-to-cart";
|
|
3027
|
+
|
|
3028
|
+
export const metadata: Metadata = site.meta({ title: "Shop", path: "/shop", description: ${JSON.stringify(`Everything in the ${ctx.template.siteName} store.`)} });
|
|
3029
|
+
|
|
3030
|
+
export default function Page() {
|
|
3031
|
+
return (
|
|
3032
|
+
<main className="mx-auto min-h-screen max-w-6xl px-6 py-24">
|
|
3033
|
+
<p className="text-sm font-semibold uppercase tracking-widest text-faint">Shop</p>
|
|
3034
|
+
<h1 className="mt-3 text-4xl font-bold">Everything in the <span className="gradient-text">store</span></h1>
|
|
3035
|
+
<p className="mt-4 max-w-xl text-muted">Prices are exact to the paisa with @lacspace/money, and your cart is saved locally with @lacspace/store.</p>
|
|
3036
|
+
<div className="mt-12 grid gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
|
3037
|
+
{PRODUCTS.map((p) => (
|
|
3038
|
+
<div key={p.slug} className="group flex flex-col rounded-2xl border border-hairline bg-surface p-5 transition hover:-translate-y-1">
|
|
3039
|
+
<Link href={"/product/" + p.slug} className="mb-4 flex aspect-square items-center justify-center rounded-xl gradient-bg text-6xl">{p.emoji}</Link>
|
|
3040
|
+
<Link href={"/product/" + p.slug} className="font-semibold group-hover:opacity-90">{p.name}</Link>
|
|
3041
|
+
<p className="mt-1 line-clamp-2 text-sm text-muted">{p.blurb}</p>
|
|
3042
|
+
<div className="mt-3 text-muted">{formatMoney(p.price)}</div>
|
|
3043
|
+
<div className="mt-4">
|
|
3044
|
+
<AddToCart product={{ id: p.slug, name: p.name, unitPrice: p.price, weight: p.weight, emoji: p.emoji }} className="w-full" />
|
|
3045
|
+
</div>
|
|
3046
|
+
</div>
|
|
3047
|
+
))}
|
|
3048
|
+
</div>
|
|
3049
|
+
</main>
|
|
3050
|
+
);
|
|
3051
|
+
}
|
|
3052
|
+
`;
|
|
3053
|
+
var marketProductPage = (ctx) => `import type { Metadata } from "next";
|
|
3054
|
+
import Link from "next/link";
|
|
3055
|
+
import { notFound } from "next/navigation";
|
|
3056
|
+
import { site } from "@/lib/site";
|
|
3057
|
+
import { PRODUCTS, getProduct } from "@/lib/products";
|
|
3058
|
+
import { formatMoney } from "@/lib/commerce";
|
|
3059
|
+
import { AddToCart } from "@/components/add-to-cart";
|
|
3060
|
+
|
|
3061
|
+
export function generateStaticParams() {
|
|
3062
|
+
return PRODUCTS.map((p) => ({ slug: p.slug }));
|
|
3063
|
+
}
|
|
3064
|
+
|
|
3065
|
+
export async function generateMetadata({ params }: { params: Promise<{ slug: string }> }): Promise<Metadata> {
|
|
3066
|
+
const { slug } = await params;
|
|
3067
|
+
const product = getProduct(slug);
|
|
3068
|
+
if (!product) return site.meta({ title: "Not found", path: "/product/" + slug });
|
|
3069
|
+
return site.meta({ title: product.name, path: "/product/" + slug, description: product.blurb });
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
export default async function Page({ params }: { params: Promise<{ slug: string }> }) {
|
|
3073
|
+
const { slug } = await params;
|
|
3074
|
+
const product = getProduct(slug);
|
|
3075
|
+
if (!product) notFound();
|
|
3076
|
+
return (
|
|
3077
|
+
<main className="mx-auto min-h-screen max-w-5xl px-6 py-24">
|
|
3078
|
+
<Link href="/shop" className="text-sm text-muted underline underline-offset-4 hover:text-fg">\u2190 Back to shop</Link>
|
|
3079
|
+
<div className="mt-8 grid gap-12 md:grid-cols-2">
|
|
3080
|
+
<div className="flex aspect-square items-center justify-center rounded-3xl gradient-bg text-[10rem]">{product.emoji}</div>
|
|
3081
|
+
<div className="flex flex-col justify-center">
|
|
3082
|
+
<h1 className="text-4xl font-bold">{product.name}</h1>
|
|
3083
|
+
<div className="mt-3 text-2xl font-semibold gradient-text">{formatMoney(product.price)}</div>
|
|
3084
|
+
<p className="mt-6 text-lg text-muted">{product.blurb}</p>
|
|
3085
|
+
<div className="mt-8 flex items-center gap-4">
|
|
3086
|
+
<AddToCart product={{ id: product.slug, name: product.name, unitPrice: product.price, weight: product.weight, emoji: product.emoji }} />
|
|
3087
|
+
<Link href="/cart" className="rounded-full border border-hairline px-6 py-2 text-sm font-semibold hover:bg-surface">Go to cart</Link>
|
|
3088
|
+
</div>
|
|
3089
|
+
<p className="mt-6 text-sm text-muted">Ships in 3-5 days \xB7 free over {formatMoney(500000)} \xB7 {product.weight}g</p>
|
|
3090
|
+
</div>
|
|
3091
|
+
</div>
|
|
3092
|
+
</main>
|
|
3093
|
+
);
|
|
3094
|
+
}
|
|
3095
|
+
`;
|
|
3096
|
+
var marketCartPage = (ctx) => `"use client";
|
|
3097
|
+
|
|
3098
|
+
import Link from "next/link";
|
|
3099
|
+
import { useCart } from "@/components/cart-store";
|
|
3100
|
+
import { quote, formatMoney, type QuoteLine } from "@/lib/commerce";
|
|
3101
|
+
import { useIsMounted } from "@lacspace/hooks";
|
|
3102
|
+
|
|
3103
|
+
export default function Page() {
|
|
3104
|
+
const cart = useCart((s) => s.cart);
|
|
3105
|
+
const setQty = useCart((s) => s.setQty);
|
|
3106
|
+
const remove = useCart((s) => s.remove);
|
|
3107
|
+
const clear = useCart((s) => s.clear);
|
|
3108
|
+
const mounted = useIsMounted();
|
|
3109
|
+
|
|
3110
|
+
if (!mounted()) {
|
|
3111
|
+
return <main className="mx-auto min-h-screen max-w-3xl px-6 py-24 text-center text-muted">Loading your cart\u2026</main>;
|
|
3112
|
+
}
|
|
3113
|
+
|
|
3114
|
+
if (cart.items.length === 0) {
|
|
3115
|
+
return (
|
|
3116
|
+
<main className="mx-auto min-h-screen max-w-3xl px-6 py-24 text-center">
|
|
3117
|
+
<h1 className="text-4xl font-bold">Your <span className="gradient-text">cart</span></h1>
|
|
3118
|
+
<p className="mt-4 text-lg text-muted">It's empty for now.</p>
|
|
3119
|
+
<Link href="/shop" className="mt-8 inline-block rounded-full gradient-bg px-6 py-3 font-semibold text-black">Browse the shop</Link>
|
|
3120
|
+
</main>
|
|
3121
|
+
);
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
const lines: QuoteLine[] = cart.items.map((i) => ({
|
|
3125
|
+
id: i.id,
|
|
3126
|
+
name: i.name ?? i.id,
|
|
3127
|
+
unitPrice: i.unitPrice,
|
|
3128
|
+
qty: i.qty,
|
|
3129
|
+
weight: Number((i.meta && (i.meta as Record<string, unknown>).weight) ?? 0),
|
|
3130
|
+
}));
|
|
3131
|
+
const q = quote(lines);
|
|
3132
|
+
|
|
3133
|
+
const rows: { label: string; value: string; strong?: boolean }[] = [
|
|
3134
|
+
{ label: "Subtotal", value: formatMoney(q.subtotal) },
|
|
3135
|
+
{ label: "VAT (13%)", value: formatMoney(q.tax) },
|
|
3136
|
+
{ label: "Shipping (" + q.shippingMethod + ")", value: q.shipping === 0 ? "Free" : formatMoney(q.shipping) },
|
|
3137
|
+
{ label: "Total", value: formatMoney(q.total), strong: true },
|
|
3138
|
+
];
|
|
3139
|
+
|
|
3140
|
+
return (
|
|
3141
|
+
<main className="mx-auto min-h-screen max-w-5xl px-6 py-24">
|
|
3142
|
+
<h1 className="text-4xl font-bold">Your <span className="gradient-text">cart</span></h1>
|
|
3143
|
+
<div className="mt-12 grid gap-12 lg:grid-cols-[1fr_20rem]">
|
|
3144
|
+
<ul className="divide-y divide-hairline">
|
|
3145
|
+
{cart.items.map((i) => {
|
|
3146
|
+
const emoji = String((i.meta && (i.meta as Record<string, unknown>).emoji) ?? "\\u{1F6D2}");
|
|
3147
|
+
return (
|
|
3148
|
+
<li key={i.id} className="flex items-center gap-4 py-5">
|
|
3149
|
+
<div className="flex h-16 w-16 shrink-0 items-center justify-center rounded-xl gradient-bg text-3xl">{emoji}</div>
|
|
3150
|
+
<div className="min-w-0 flex-1">
|
|
3151
|
+
<div className="truncate font-semibold">{i.name}</div>
|
|
3152
|
+
<div className="text-sm text-muted">{formatMoney(i.unitPrice)} each</div>
|
|
3153
|
+
</div>
|
|
3154
|
+
<div className="flex items-center gap-2">
|
|
3155
|
+
<button type="button" aria-label="Decrease" onClick={() => setQty(i.id, i.qty - 1)} className="h-8 w-8 rounded-full border border-hairline text-muted transition hover:text-fg">\u2212</button>
|
|
3156
|
+
<span className="w-8 text-center tabular-nums">{i.qty}</span>
|
|
3157
|
+
<button type="button" aria-label="Increase" onClick={() => setQty(i.id, i.qty + 1)} className="h-8 w-8 rounded-full border border-hairline text-muted transition hover:text-fg">+</button>
|
|
3158
|
+
</div>
|
|
3159
|
+
<div className="w-24 text-right font-semibold tabular-nums">{formatMoney(i.unitPrice * i.qty)}</div>
|
|
3160
|
+
<button type="button" aria-label="Remove" onClick={() => remove(i.id)} className="text-faint transition hover:text-fg">\u2715</button>
|
|
3161
|
+
</li>
|
|
3162
|
+
);
|
|
3163
|
+
})}
|
|
3164
|
+
</ul>
|
|
3165
|
+
<aside className="h-fit rounded-2xl border border-hairline bg-surface p-6">
|
|
3166
|
+
<h2 className="font-semibold">Order summary</h2>
|
|
3167
|
+
<dl className="mt-5 space-y-3 text-sm">
|
|
3168
|
+
{rows.map((r) => (
|
|
3169
|
+
<div key={r.label} className={"flex items-center justify-between " + (r.strong ? "border-t border-hairline pt-3 text-base font-bold" : "text-muted")}>
|
|
3170
|
+
<dt>{r.label}</dt>
|
|
3171
|
+
<dd className="tabular-nums">{r.value}</dd>
|
|
3172
|
+
</div>
|
|
3173
|
+
))}
|
|
3174
|
+
</dl>
|
|
3175
|
+
{q.freeShippingRemaining > 0 ? (
|
|
3176
|
+
<p className="mt-4 text-xs text-muted">Add {formatMoney(q.freeShippingRemaining)} more for free shipping.</p>
|
|
3177
|
+
) : null}
|
|
3178
|
+
<Link href="/checkout" className="mt-6 block w-full rounded-full gradient-bg px-6 py-3 text-center font-semibold text-black">Checkout</Link>
|
|
3179
|
+
<button type="button" onClick={clear} className="mt-3 block w-full text-center text-sm text-muted transition hover:text-fg">Clear cart</button>
|
|
3180
|
+
</aside>
|
|
3181
|
+
</div>
|
|
3182
|
+
</main>
|
|
3183
|
+
);
|
|
3184
|
+
}
|
|
3185
|
+
`;
|
|
3186
|
+
var marketCheckoutPage = (ctx) => `"use client";
|
|
3187
|
+
|
|
3188
|
+
import { useState } from "react";
|
|
3189
|
+
import Link from "next/link";
|
|
3190
|
+
import { useRouter } from "next/navigation";
|
|
3191
|
+
import { useCart } from "@/components/cart-store";
|
|
3192
|
+
import { quote, formatMoney, type QuoteLine } from "@/lib/commerce";
|
|
3193
|
+
import { useIsMounted } from "@lacspace/hooks";
|
|
3194
|
+
|
|
3195
|
+
type Method = "esewa" | "khalti" | "cod";
|
|
3196
|
+
|
|
3197
|
+
export default function Page() {
|
|
3198
|
+
const router = useRouter();
|
|
3199
|
+
const cart = useCart((s) => s.cart);
|
|
3200
|
+
const clear = useCart((s) => s.clear);
|
|
3201
|
+
const mounted = useIsMounted();
|
|
3202
|
+
const [name, setName] = useState("");
|
|
3203
|
+
const [email, setEmail] = useState("");
|
|
3204
|
+
const [address, setAddress] = useState("");
|
|
3205
|
+
const [method, setMethod] = useState<Method>("esewa");
|
|
3206
|
+
const [busy, setBusy] = useState(false);
|
|
3207
|
+
const [error, setError] = useState("");
|
|
3208
|
+
|
|
3209
|
+
if (!mounted()) {
|
|
3210
|
+
return <main className="mx-auto min-h-screen max-w-3xl px-6 py-24 text-center text-muted">Loading\u2026</main>;
|
|
3211
|
+
}
|
|
3212
|
+
|
|
3213
|
+
if (cart.items.length === 0) {
|
|
3214
|
+
return (
|
|
3215
|
+
<main className="mx-auto min-h-screen max-w-3xl px-6 py-24 text-center">
|
|
3216
|
+
<h1 className="text-4xl font-bold">Checkout</h1>
|
|
3217
|
+
<p className="mt-4 text-lg text-muted">Your cart is empty.</p>
|
|
3218
|
+
<Link href="/shop" className="mt-8 inline-block rounded-full gradient-bg px-6 py-3 font-semibold text-black">Browse the shop</Link>
|
|
3219
|
+
</main>
|
|
3220
|
+
);
|
|
3221
|
+
}
|
|
3222
|
+
|
|
3223
|
+
const lines: QuoteLine[] = cart.items.map((i) => ({
|
|
3224
|
+
id: i.id,
|
|
3225
|
+
name: i.name ?? i.id,
|
|
3226
|
+
unitPrice: i.unitPrice,
|
|
3227
|
+
qty: i.qty,
|
|
3228
|
+
weight: Number((i.meta && (i.meta as Record<string, unknown>).weight) ?? 0),
|
|
3229
|
+
}));
|
|
3230
|
+
const q = quote(lines);
|
|
3231
|
+
|
|
3232
|
+
async function submitEsewaForm(amount: number, orderNumber: string) {
|
|
3233
|
+
const res = await fetch("/api/pay/esewa", {
|
|
3234
|
+
method: "POST",
|
|
3235
|
+
headers: { "content-type": "application/json" },
|
|
3236
|
+
body: JSON.stringify({ amount, orderNumber }),
|
|
3237
|
+
});
|
|
3238
|
+
const data = (await res.json()) as { action: string; fields: Record<string, string> };
|
|
3239
|
+
const form = document.createElement("form");
|
|
3240
|
+
form.method = "POST";
|
|
3241
|
+
form.action = data.action;
|
|
3242
|
+
Object.entries(data.fields).forEach(([k, v]) => {
|
|
3243
|
+
const input = document.createElement("input");
|
|
3244
|
+
input.type = "hidden";
|
|
3245
|
+
input.name = k;
|
|
3246
|
+
input.value = v;
|
|
3247
|
+
form.appendChild(input);
|
|
3248
|
+
});
|
|
3249
|
+
document.body.appendChild(form);
|
|
3250
|
+
form.submit();
|
|
3251
|
+
}
|
|
3252
|
+
|
|
3253
|
+
async function onSubmit(e: React.FormEvent) {
|
|
3254
|
+
e.preventDefault();
|
|
3255
|
+
setBusy(true);
|
|
3256
|
+
setError("");
|
|
3257
|
+
try {
|
|
3258
|
+
const res = await fetch("/api/checkout", {
|
|
3259
|
+
method: "POST",
|
|
3260
|
+
headers: { "content-type": "application/json" },
|
|
3261
|
+
body: JSON.stringify({ lines, customer: { name, email, address }, paymentMethod: method }),
|
|
3262
|
+
});
|
|
3263
|
+
if (!res.ok) throw new Error("Could not place the order.");
|
|
3264
|
+
const order = (await res.json()) as { orderNumber: string; total: number };
|
|
3265
|
+
|
|
3266
|
+
if (method === "cod") {
|
|
3267
|
+
clear();
|
|
3268
|
+
router.push("/checkout/success?order=" + encodeURIComponent(order.orderNumber) + "&method=cod");
|
|
3269
|
+
return;
|
|
3270
|
+
}
|
|
3271
|
+
if (method === "khalti") {
|
|
3272
|
+
const pay = await fetch("/api/pay/khalti", {
|
|
3273
|
+
method: "POST",
|
|
3274
|
+
headers: { "content-type": "application/json" },
|
|
3275
|
+
body: JSON.stringify({ amount: order.total, orderNumber: order.orderNumber, orderName: ${JSON.stringify(`${ctx.template.siteName} order`)}, customer: { name, email } }),
|
|
3276
|
+
});
|
|
3277
|
+
const data = (await pay.json()) as { payment_url: string; mock?: boolean };
|
|
3278
|
+
clear();
|
|
3279
|
+
window.location.href = data.payment_url;
|
|
3280
|
+
return;
|
|
3281
|
+
}
|
|
3282
|
+
// eSewa: build + auto-submit the signed form (navigates to the gateway).
|
|
3283
|
+
await submitEsewaForm(order.total, order.orderNumber);
|
|
3284
|
+
} catch (err) {
|
|
3285
|
+
setError(err instanceof Error ? err.message : "Something went wrong.");
|
|
3286
|
+
setBusy(false);
|
|
3287
|
+
}
|
|
3288
|
+
}
|
|
3289
|
+
|
|
3290
|
+
const field = "w-full rounded-xl border border-hairline bg-surface px-4 py-3 outline-none focus:border-hairline";
|
|
3291
|
+
const methods: { id: Method; label: string; hint: string }[] = [
|
|
3292
|
+
{ id: "esewa", label: "eSewa", hint: "Test gateway \\u2014 signed with the documented sandbox credentials." },
|
|
3293
|
+
{ id: "khalti", label: "Khalti", hint: "Runs in mock mode until you set KHALTI_SECRET." },
|
|
3294
|
+
{ id: "cod", label: "Cash on delivery", hint: "Pay when your order arrives." },
|
|
3295
|
+
];
|
|
3296
|
+
|
|
3297
|
+
return (
|
|
3298
|
+
<main className="mx-auto min-h-screen max-w-5xl px-6 py-24">
|
|
3299
|
+
<h1 className="text-4xl font-bold">Checkout</h1>
|
|
3300
|
+
<form onSubmit={onSubmit} className="mt-12 grid gap-12 lg:grid-cols-[1fr_20rem]">
|
|
3301
|
+
<div className="space-y-8">
|
|
3302
|
+
<section className="space-y-4">
|
|
3303
|
+
<h2 className="font-semibold">Your details</h2>
|
|
3304
|
+
<input required value={name} onChange={(e) => setName(e.target.value)} placeholder="Full name" className={field} />
|
|
3305
|
+
<input required type="email" value={email} onChange={(e) => setEmail(e.target.value)} placeholder="Email" className={field} />
|
|
3306
|
+
<textarea required value={address} onChange={(e) => setAddress(e.target.value)} placeholder="Delivery address" rows={3} className={field} />
|
|
3307
|
+
</section>
|
|
3308
|
+
<section className="space-y-3">
|
|
3309
|
+
<h2 className="font-semibold">Payment</h2>
|
|
3310
|
+
{methods.map((m) => (
|
|
3311
|
+
<label key={m.id} className={"flex cursor-pointer items-start gap-3 rounded-xl border p-4 transition " + (method === m.id ? "border-transparent bg-surface ring-2 ring-[var(--accent-to)]" : "border-hairline hover:bg-surface")}>
|
|
3312
|
+
<input type="radio" name="method" checked={method === m.id} onChange={() => setMethod(m.id)} className="mt-1" />
|
|
3313
|
+
<span>
|
|
3314
|
+
<span className="block font-medium">{m.label}</span>
|
|
3315
|
+
<span className="block text-sm text-muted">{m.hint}</span>
|
|
3316
|
+
</span>
|
|
3317
|
+
</label>
|
|
3318
|
+
))}
|
|
3319
|
+
</section>
|
|
3320
|
+
{error ? <p className="text-sm text-red-400">{error}</p> : null}
|
|
3321
|
+
</div>
|
|
3322
|
+
<aside className="h-fit rounded-2xl border border-hairline bg-surface p-6">
|
|
3323
|
+
<h2 className="font-semibold">Order summary</h2>
|
|
3324
|
+
<ul className="mt-4 space-y-2 text-sm text-muted">
|
|
3325
|
+
{cart.items.map((i) => (
|
|
3326
|
+
<li key={i.id} className="flex justify-between gap-3">
|
|
3327
|
+
<span className="truncate">{i.name} \xD7 {i.qty}</span>
|
|
3328
|
+
<span className="tabular-nums">{formatMoney(i.unitPrice * i.qty)}</span>
|
|
3329
|
+
</li>
|
|
3330
|
+
))}
|
|
3331
|
+
</ul>
|
|
3332
|
+
<dl className="mt-4 space-y-2 border-t border-hairline pt-4 text-sm">
|
|
3333
|
+
<div className="flex justify-between text-muted"><dt>Subtotal</dt><dd className="tabular-nums">{formatMoney(q.subtotal)}</dd></div>
|
|
3334
|
+
<div className="flex justify-between text-muted"><dt>VAT (13%)</dt><dd className="tabular-nums">{formatMoney(q.tax)}</dd></div>
|
|
3335
|
+
<div className="flex justify-between text-muted"><dt>Shipping</dt><dd className="tabular-nums">{q.shipping === 0 ? "Free" : formatMoney(q.shipping)}</dd></div>
|
|
3336
|
+
<div className="flex justify-between border-t border-hairline pt-2 text-base font-bold"><dt>Total</dt><dd className="tabular-nums">{formatMoney(q.total)}</dd></div>
|
|
3337
|
+
</dl>
|
|
3338
|
+
<button type="submit" disabled={busy} className="mt-6 block w-full rounded-full gradient-bg px-6 py-3 text-center font-semibold text-black transition hover:opacity-90 disabled:opacity-60">
|
|
3339
|
+
{busy ? "Placing order\\u2026" : "Pay " + formatMoney(q.total)}
|
|
3340
|
+
</button>
|
|
3341
|
+
</aside>
|
|
3342
|
+
</form>
|
|
3343
|
+
</main>
|
|
3344
|
+
);
|
|
3345
|
+
}
|
|
3346
|
+
`;
|
|
3347
|
+
var marketSuccessPage = (ctx) => `import type { Metadata } from "next";
|
|
3348
|
+
import Link from "next/link";
|
|
3349
|
+
import { site } from "@/lib/site";
|
|
3350
|
+
|
|
3351
|
+
export const metadata: Metadata = site.meta({ title: "Order confirmed", path: "/checkout/success" });
|
|
3352
|
+
|
|
3353
|
+
export default async function Page({ searchParams }: { searchParams: Promise<{ order?: string; method?: string; mock?: string }> }) {
|
|
3354
|
+
const sp = await searchParams;
|
|
3355
|
+
const order = sp.order ?? "";
|
|
3356
|
+
const method = sp.method ?? "";
|
|
3357
|
+
const mock = sp.mock === "1";
|
|
3358
|
+
return (
|
|
3359
|
+
<main className="mx-auto flex min-h-screen max-w-2xl flex-col items-center justify-center px-6 py-24 text-center">
|
|
3360
|
+
<div className="flex h-20 w-20 items-center justify-center rounded-full gradient-bg text-4xl text-black">\u2713</div>
|
|
3361
|
+
<h1 className="mt-8 text-4xl font-bold">Thank you!</h1>
|
|
3362
|
+
<p className="mt-4 text-lg text-muted">Your order with ${ctx.template.siteName} is confirmed.</p>
|
|
3363
|
+
{order ? (
|
|
3364
|
+
<p className="mt-6 rounded-xl border border-hairline bg-surface px-5 py-3 font-mono text-sm">Order {order}</p>
|
|
3365
|
+
) : null}
|
|
3366
|
+
{method ? <p className="mt-3 text-sm text-muted">Payment method: {method}</p> : null}
|
|
3367
|
+
{mock ? <p className="mt-3 max-w-md text-sm text-muted">This Khalti payment ran in mock mode. Set KHALTI_SECRET in your environment to hit the real gateway.</p> : null}
|
|
3368
|
+
<Link href="/shop" className="mt-10 inline-block rounded-full gradient-bg px-6 py-3 font-semibold text-black">Continue shopping</Link>
|
|
3369
|
+
</main>
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
`;
|
|
3373
|
+
var checkoutRoute = () => `import { NextResponse } from "next/server";
|
|
3374
|
+
import { buildOrder, type QuoteLine } from "@/lib/commerce";
|
|
3375
|
+
|
|
3376
|
+
export async function POST(req: Request) {
|
|
3377
|
+
const body = (await req.json()) as {
|
|
3378
|
+
lines?: QuoteLine[];
|
|
3379
|
+
customer?: { name?: string; email?: string };
|
|
3380
|
+
paymentMethod?: string;
|
|
3381
|
+
};
|
|
3382
|
+
const lines = body.lines ?? [];
|
|
3383
|
+
if (lines.length === 0) {
|
|
3384
|
+
return NextResponse.json({ error: "Cart is empty" }, { status: 400 });
|
|
3385
|
+
}
|
|
3386
|
+
|
|
3387
|
+
// @lacspace/order + @lacspace/invoice build immutable, serializable records.
|
|
3388
|
+
// Persist \`order\` / \`invoice\` to your database here \u2014 this demo keeps them in memory.
|
|
3389
|
+
const { order, invoice, quote } = buildOrder({
|
|
3390
|
+
lines,
|
|
3391
|
+
customer: body.customer,
|
|
3392
|
+
paymentMethod: body.paymentMethod ?? "cod",
|
|
3393
|
+
});
|
|
3394
|
+
|
|
3395
|
+
return NextResponse.json({
|
|
3396
|
+
orderNumber: order.number,
|
|
3397
|
+
invoiceNumber: invoice.number,
|
|
3398
|
+
total: quote.total,
|
|
3399
|
+
currency: quote.currency,
|
|
3400
|
+
});
|
|
3401
|
+
}
|
|
3402
|
+
`;
|
|
3403
|
+
var esewaRoute = () => `import { NextResponse } from "next/server";
|
|
3404
|
+
import { buildForm, ESEWA_TEST_SECRET, ESEWA_TEST_PRODUCT_CODE } from "@lacspace/esewa";
|
|
3405
|
+
|
|
3406
|
+
// eSewa amounts are in **rupees**, so we convert from the paisa the cart uses.
|
|
3407
|
+
export async function POST(req: Request) {
|
|
3408
|
+
const body = (await req.json()) as { amount?: number; orderNumber?: string };
|
|
3409
|
+
const secret = process.env.ESEWA_SECRET ?? ESEWA_TEST_SECRET;
|
|
3410
|
+
const productCode = process.env.ESEWA_PRODUCT_CODE ?? ESEWA_TEST_PRODUCT_CODE;
|
|
3411
|
+
const env: "test" | "prod" = process.env.ESEWA_SECRET ? "prod" : "test";
|
|
3412
|
+
const rupees = Math.round((body.amount ?? 0) / 100);
|
|
3413
|
+
const origin = new URL(req.url).origin;
|
|
3414
|
+
const orderNumber = body.orderNumber ?? "txn-" + Date.now();
|
|
3415
|
+
|
|
3416
|
+
const form = await buildForm(
|
|
3417
|
+
{
|
|
3418
|
+
amount: rupees,
|
|
3419
|
+
transactionUuid: orderNumber,
|
|
3420
|
+
productCode,
|
|
3421
|
+
successUrl: origin + "/checkout/success?order=" + encodeURIComponent(orderNumber) + "&method=esewa",
|
|
3422
|
+
failureUrl: origin + "/checkout?failed=1",
|
|
3423
|
+
},
|
|
3424
|
+
{ secret, env },
|
|
3425
|
+
);
|
|
3426
|
+
|
|
3427
|
+
return NextResponse.json(form);
|
|
3428
|
+
}
|
|
3429
|
+
`;
|
|
3430
|
+
var khaltiRoute = () => `import { NextResponse } from "next/server";
|
|
3431
|
+
import { initiate } from "@lacspace/khalti";
|
|
3432
|
+
|
|
3433
|
+
export async function POST(req: Request) {
|
|
3434
|
+
const body = (await req.json()) as {
|
|
3435
|
+
amount?: number;
|
|
3436
|
+
orderNumber?: string;
|
|
3437
|
+
orderName?: string;
|
|
3438
|
+
customer?: { name?: string; email?: string };
|
|
3439
|
+
};
|
|
3440
|
+
const secret = process.env.KHALTI_SECRET;
|
|
3441
|
+
const origin = new URL(req.url).origin;
|
|
3442
|
+
const orderNumber = body.orderNumber ?? "order";
|
|
3443
|
+
const returnUrl = origin + "/checkout/success?order=" + encodeURIComponent(orderNumber) + "&method=khalti";
|
|
3444
|
+
|
|
3445
|
+
// Zero-config: without a secret we return a clearly-labelled mock so the flow
|
|
3446
|
+
// still completes end-to-end in development.
|
|
3447
|
+
if (!secret) {
|
|
3448
|
+
return NextResponse.json({
|
|
3449
|
+
mock: true,
|
|
3450
|
+
pidx: "mock-" + Date.now(),
|
|
3451
|
+
payment_url: returnUrl + "&mock=1",
|
|
3452
|
+
});
|
|
3453
|
+
}
|
|
3454
|
+
|
|
3455
|
+
const res = await initiate(
|
|
3456
|
+
{
|
|
3457
|
+
return_url: returnUrl,
|
|
3458
|
+
website_url: origin,
|
|
3459
|
+
amount: body.amount ?? 0, // paisa
|
|
3460
|
+
purchase_order_id: orderNumber,
|
|
3461
|
+
purchase_order_name: body.orderName ?? "Order",
|
|
3462
|
+
customer_info: body.customer,
|
|
3463
|
+
},
|
|
3464
|
+
{ secretKey: secret, env: "test" },
|
|
3465
|
+
);
|
|
3466
|
+
|
|
3467
|
+
return NextResponse.json(res);
|
|
3468
|
+
}
|
|
3469
|
+
`;
|
|
3470
|
+
var marketEnvExample = () => `# Your production URL \u2014 powers canonical URLs, sitemap, robots and OG images.
|
|
3471
|
+
NEXT_PUBLIC_SITE_URL=https://example.com
|
|
3472
|
+
|
|
3473
|
+
# --- Payments (all optional) ---
|
|
3474
|
+
# The store runs out of the box: eSewa uses the documented sandbox credentials,
|
|
3475
|
+
# and Khalti falls back to a labelled mock until you add a secret.
|
|
3476
|
+
|
|
3477
|
+
# eSewa \u2014 set both to go live (otherwise the EPAYTEST sandbox is used).
|
|
3478
|
+
# ESEWA_SECRET=
|
|
3479
|
+
# ESEWA_PRODUCT_CODE=
|
|
3480
|
+
|
|
3481
|
+
# Khalti \u2014 set the secret to hit the real gateway (otherwise a mock is returned).
|
|
3482
|
+
# KHALTI_SECRET=
|
|
3483
|
+
`;
|
|
2629
3484
|
var settingsPage = (ctx) => `import type { Metadata } from "next";
|
|
2630
3485
|
import { site } from "@/lib/site";
|
|
2631
3486
|
import { DashboardShell } from "@/components/dashboard-shell";
|
|
@@ -3141,6 +3996,21 @@ function realPageFiles(ctx) {
|
|
|
3141
3996
|
f["components/product-grid.tsx"] = productGrid();
|
|
3142
3997
|
f["components/cart-view.tsx"] = cartView();
|
|
3143
3998
|
}
|
|
3999
|
+
if (k === "marketplace") {
|
|
4000
|
+
f["lib/products.ts"] = productsLib(ctx);
|
|
4001
|
+
f["lib/commerce.ts"] = commerceLib(ctx);
|
|
4002
|
+
f["components/cart-store.tsx"] = cartStore();
|
|
4003
|
+
f["components/cart-button.tsx"] = cartButton();
|
|
4004
|
+
f["components/add-to-cart.tsx"] = addToCart();
|
|
4005
|
+
f["app/shop/page.tsx"] = marketShopPage(ctx);
|
|
4006
|
+
f["app/product/[slug]/page.tsx"] = marketProductPage();
|
|
4007
|
+
f["app/cart/page.tsx"] = marketCartPage();
|
|
4008
|
+
f["app/checkout/page.tsx"] = marketCheckoutPage(ctx);
|
|
4009
|
+
f["app/checkout/success/page.tsx"] = marketSuccessPage(ctx);
|
|
4010
|
+
f["app/api/checkout/route.ts"] = checkoutRoute();
|
|
4011
|
+
f["app/api/pay/esewa/route.ts"] = esewaRoute();
|
|
4012
|
+
f["app/api/pay/khalti/route.ts"] = khaltiRoute();
|
|
4013
|
+
}
|
|
3144
4014
|
if (k === "blog") {
|
|
3145
4015
|
f["app/topics/page.tsx"] = topicsPage();
|
|
3146
4016
|
f["app/newsletter/page.tsx"] = newsletterPage(ctx);
|
|
@@ -3263,6 +4133,20 @@ function creativeData(ctx) {
|
|
|
3263
4133
|
{ q: "Any dietary options?", a: "Plenty \u2014 vegetarian and vegan plates change weekly. Just ask about allergies." },
|
|
3264
4134
|
{ q: "When are you open?", a: "Wednesday to Sunday, 5pm till late." }
|
|
3265
4135
|
]
|
|
4136
|
+
},
|
|
4137
|
+
marketplace: {
|
|
4138
|
+
emoji: "\u{1F6D2}",
|
|
4139
|
+
chips: ["Cart", "Checkout", "Pay"],
|
|
4140
|
+
showcaseTitle: "Cart to checkout, wired end to end",
|
|
4141
|
+
showcaseLead: `${n} is a real storefront \u2014 a headless cart, tax & shipping, orders and invoices, plus eSewa & Khalti payments.`,
|
|
4142
|
+
bullets: ["Integer-safe money (no rounding bugs)", "VAT + weight-based shipping", "eSewa & Khalti, test-mode out of the box"],
|
|
4143
|
+
marqueeLabel: "Powered by",
|
|
4144
|
+
marquee: ["@lacspace/cart", "@lacspace/order", "@lacspace/tax", "@lacspace/shipping", "@lacspace/invoice", "@lacspace/esewa", "@lacspace/khalti"],
|
|
4145
|
+
faq: [
|
|
4146
|
+
{ q: "How is the cart stored?", a: "In a @lacspace/store store persisted to localStorage \u2014 it survives a refresh, and it's SSR-safe." },
|
|
4147
|
+
{ q: "Which payments are supported?", a: "eSewa and Khalti are wired up, plus cash on delivery. They run in test/mock mode with zero config." },
|
|
4148
|
+
{ q: "How are totals calculated?", a: "Subtotal, 13% VAT (@lacspace/tax) and shipping (@lacspace/shipping) are composed in lib/commerce, all in integer paisa." }
|
|
4149
|
+
]
|
|
3266
4150
|
}
|
|
3267
4151
|
};
|
|
3268
4152
|
return map[ctx.template.key] ?? map.business;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-lacspace-app",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.17.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": {
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
],
|
|
35
35
|
"author": "Lacspace <contact@lacspace.com>",
|
|
36
36
|
"license": "SEE LICENSE IN LICENSE",
|
|
37
|
-
"homepage": "https://lacspace.com
|
|
37
|
+
"homepage": "https://templates.lacspace.com",
|
|
38
38
|
"repository": {
|
|
39
39
|
"type": "git",
|
|
40
40
|
"url": "git+https://github.com/lacspace/npm-packages.git",
|