create-githat-app 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/cli.js +85 -14
  2. package/package.json +2 -2
  3. package/templates/agent/TODO.md +9 -0
  4. package/templates/agent/app/(auth)/sign-in/page.tsx.hbs +9 -0
  5. package/templates/agent/app/(auth)/sign-up/page.tsx.hbs +9 -0
  6. package/templates/agent/app/globals.css.hbs +87 -0
  7. package/templates/agent/app/layout.tsx.hbs +41 -0
  8. package/templates/agent/app/page.tsx.hbs +123 -0
  9. package/templates/agent/next.config.ts.hbs +7 -0
  10. package/templates/agent/postcss.config.mjs.hbs +14 -0
  11. package/templates/agent/proxy.ts.hbs +10 -0
  12. package/templates/agent/tsconfig.json.hbs +21 -0
  13. package/templates/base/.env.example.hbs +2 -2
  14. package/templates/base/.env.local.example.hbs +2 -2
  15. package/templates/base/.env.local.hbs +2 -2
  16. package/templates/content/TODO.md +9 -0
  17. package/templates/content/app/(auth)/sign-in/page.tsx.hbs +9 -0
  18. package/templates/content/app/(auth)/sign-up/page.tsx.hbs +9 -0
  19. package/templates/content/app/globals.css.hbs +87 -0
  20. package/templates/content/app/layout.tsx.hbs +41 -0
  21. package/templates/content/app/page.tsx.hbs +123 -0
  22. package/templates/content/next.config.ts.hbs +7 -0
  23. package/templates/content/postcss.config.mjs.hbs +14 -0
  24. package/templates/content/proxy.ts.hbs +10 -0
  25. package/templates/content/tsconfig.json.hbs +21 -0
  26. package/templates/dashboard/TODO.md +9 -0
  27. package/templates/dashboard/app/(auth)/sign-in/page.tsx.hbs +9 -0
  28. package/templates/dashboard/app/(auth)/sign-up/page.tsx.hbs +9 -0
  29. package/templates/dashboard/app/globals.css.hbs +87 -0
  30. package/templates/dashboard/app/layout.tsx.hbs +41 -0
  31. package/templates/dashboard/app/page.tsx.hbs +123 -0
  32. package/templates/dashboard/next.config.ts.hbs +7 -0
  33. package/templates/dashboard/postcss.config.mjs.hbs +14 -0
  34. package/templates/dashboard/proxy.ts.hbs +10 -0
  35. package/templates/dashboard/tsconfig.json.hbs +21 -0
  36. package/templates/marketplace/CULTURE.md +74 -0
  37. package/templates/marketplace/app/(auth)/sign-in/page.tsx.hbs +9 -0
  38. package/templates/marketplace/app/(auth)/sign-up/page.tsx.hbs +9 -0
  39. package/templates/marketplace/app/(shop)/[slug]/p/[productId]/page.tsx.hbs +99 -0
  40. package/templates/marketplace/app/(shop)/[slug]/page.tsx.hbs +90 -0
  41. package/templates/marketplace/app/admin/page.tsx.hbs +95 -0
  42. package/templates/marketplace/app/cart/page.tsx.hbs +157 -0
  43. package/templates/marketplace/app/globals.css.hbs +87 -0
  44. package/templates/marketplace/app/layout.tsx.hbs +77 -0
  45. package/templates/marketplace/app/page.tsx.hbs +178 -0
  46. package/templates/marketplace/app/sell/page.tsx.hbs +78 -0
  47. package/templates/marketplace/next.config.ts.hbs +7 -0
  48. package/templates/marketplace/postcss.config.mjs.hbs +14 -0
  49. package/templates/marketplace/proxy.ts.hbs +10 -0
  50. package/templates/marketplace/src/lib/anon-session.ts.hbs +117 -0
  51. package/templates/marketplace/src/lib/categories.ts.hbs +35 -0
  52. package/templates/marketplace/tsconfig.json.hbs +21 -0
  53. package/templates/plain/app/(auth)/sign-in/page.tsx.hbs +9 -0
  54. package/templates/plain/app/(auth)/sign-up/page.tsx.hbs +9 -0
  55. package/templates/plain/app/globals.css.hbs +87 -0
  56. package/templates/plain/app/layout.tsx.hbs +41 -0
  57. package/templates/plain/app/page.tsx.hbs +123 -0
  58. package/templates/plain/next.config.ts.hbs +7 -0
  59. package/templates/plain/postcss.config.mjs.hbs +14 -0
  60. package/templates/plain/proxy.ts.hbs +10 -0
  61. package/templates/plain/tsconfig.json.hbs +21 -0
  62. package/templates/saas/TODO.md +9 -0
  63. package/templates/saas/app/(auth)/sign-in/page.tsx.hbs +9 -0
  64. package/templates/saas/app/(auth)/sign-up/page.tsx.hbs +9 -0
  65. package/templates/saas/app/globals.css.hbs +87 -0
  66. package/templates/saas/app/layout.tsx.hbs +41 -0
  67. package/templates/saas/app/page.tsx.hbs +123 -0
  68. package/templates/saas/next.config.ts.hbs +7 -0
  69. package/templates/saas/postcss.config.mjs.hbs +14 -0
  70. package/templates/saas/proxy.ts.hbs +10 -0
  71. package/templates/saas/tsconfig.json.hbs +21 -0
@@ -0,0 +1,77 @@
1
+ import { GitHatProvider } from '@githat/nextjs';
2
+ import '@githat/nextjs/styles';
3
+ import Link from 'next/link';
4
+ import './globals.css';
5
+
6
+ export const metadata = {
7
+ title: '{{businessName}} — el colmado de tu barrio',
8
+ description: '{{description}}',
9
+ };
10
+
11
+ /**
12
+ * Root layout for the marketplace template.
13
+ *
14
+ * Header has three slots, in order of importance:
15
+ * 1. Wordmark (always visible)
16
+ * 2. Tu funda — cart icon, anonymous-aware
17
+ * 3. "Save my stuff" / Sign in — low contrast, never a gate
18
+ *
19
+ * The "Vende en {{businessName}}" link sits in the footer, not the
20
+ * header. Sellers are a small minority of visitors; shoppers come first.
21
+ */
22
+ export default function RootLayout({ children }{{#if typescript}}: { children: React.ReactNode }{{/if}}) {
23
+ return (
24
+ <html lang="es">
25
+ <body>
26
+ <GitHatProvider config=\{{
27
+ publishableKey: process.env.NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY || '',
28
+ signInUrl: '/sign-in',
29
+ signUpUrl: '/sign-up',
30
+ afterSignInUrl: '/',
31
+ afterSignOutUrl: '/',
32
+ }}>
33
+ <header style=\{{
34
+ display: 'flex',
35
+ alignItems: 'center',
36
+ justifyContent: 'space-between',
37
+ padding: 'var(--space-4, 1rem) var(--space-6, 1.5rem)',
38
+ borderBottom: '1px solid var(--border, #e5e7eb)',
39
+ background: 'var(--surface, #fafafa)',
40
+ }}>
41
+ <Link href="/" style=\{{
42
+ fontFamily: 'var(--font-wordmark, Georgia, serif)',
43
+ fontSize: '1.5rem',
44
+ color: 'var(--fg, inherit)',
45
+ textDecoration: 'none',
46
+ }}>
47
+ {{businessName}}
48
+ </Link>
49
+ <nav style=\{{ display: 'flex', alignItems: 'center', gap: 'var(--space-4, 1rem)', fontSize: '0.875rem' }}>
50
+ <Link href="/cart" style=\{{ color: 'var(--fg, inherit)', textDecoration: 'none' }}>
51
+ Tu funda
52
+ </Link>
53
+ <Link href="/sign-in" style=\{{ color: 'var(--fg-subtle, #71717a)', textDecoration: 'none' }}>
54
+ Save my stuff
55
+ </Link>
56
+ </nav>
57
+ </header>
58
+ <main>{children}</main>
59
+ <footer style=\{{
60
+ padding: 'var(--space-6, 1.5rem)',
61
+ borderTop: '1px solid var(--border, #e5e7eb)',
62
+ fontSize: '0.75rem',
63
+ color: 'var(--fg-subtle, #71717a)',
64
+ textAlign: 'center',
65
+ }}>
66
+ <Link href="/sell" style=\{{ color: 'var(--fg-muted, #525252)', textDecoration: 'none' }}>
67
+ ¿Tienes un colmado? Vende aquí →
68
+ </Link>
69
+ <span style=\{{ display: 'block', marginTop: 'var(--space-2, 0.5rem)' }}>
70
+ {{businessName}} — built on GitHat + Sebastn.
71
+ </span>
72
+ </footer>
73
+ </GitHatProvider>
74
+ </body>
75
+ </html>
76
+ );
77
+ }
@@ -0,0 +1,178 @@
1
+ 'use client';
2
+
3
+ import { useState } from 'react';
4
+ import Link from 'next/link';
5
+ import { useAuth } from '@githat/nextjs';
6
+ import { CATEGORIES } from '../src/lib/categories';
7
+
8
+ /**
9
+ * Marketplace homepage — colmado-flavored.
10
+ *
11
+ * Anonymous-first: visitors browse and search without signing in.
12
+ * The header has a low-contrast "Save my stuff" link that opens
13
+ * GitHat sign-in, but it's never a gate. See CULTURE.md for the
14
+ * cultural framing — this is a colmado, not a checkout terminal.
15
+ *
16
+ * Real product data should come from your backend (Postgres,
17
+ * DynamoDB, Sebastn-stored inventory) — this file just renders
18
+ * the shell + seed categories.
19
+ */
20
+ export default function Home() {
21
+ const { isSignedIn } = useAuth();
22
+ const [query, setQuery] = useState('');
23
+
24
+ return (
25
+ <div style=\{{ background: 'var(--bg)', color: 'var(--fg)', minHeight: 'calc(100vh - 64px)' }}>
26
+ {/* Hero — bilingual, warm, anti-Amazon */}
27
+ <section style=\{{
28
+ padding: 'var(--space-8) var(--space-4)',
29
+ background: 'linear-gradient(180deg, var(--surface-sub), var(--bg))',
30
+ textAlign: 'center',
31
+ }}>
32
+ <h1 style=\{{
33
+ fontFamily: 'var(--font-wordmark, Georgia, serif)',
34
+ fontSize: 'clamp(2rem, 5vw, 3rem)',
35
+ lineHeight: 1.1,
36
+ marginBottom: 'var(--space-3)',
37
+ }}>
38
+ Cerquita de ti, todo lo que necesitas.
39
+ </h1>
40
+ <p style=\{{ color: 'var(--fg-muted)', marginBottom: 'var(--space-6)', fontSize: '1.125rem' }}>
41
+ {{businessName}} — el colmado de tu barrio, ahora en tu teléfono.
42
+ <br />
43
+ <span style=\{{ fontSize: '0.875rem', opacity: 0.7 }}>
44
+ Pídelo y te lo llevamos. <em>Order it, we'll bring it to you.</em>
45
+ </span>
46
+ </p>
47
+
48
+ <form
49
+ onSubmit={(e) => { e.preventDefault(); window.location.href = `/buscar?q=${encodeURIComponent(query)}`; }}
50
+ style=\{{ maxWidth: '32rem', margin: '0 auto' }}
51
+ >
52
+ <label htmlFor="q" style=\{{ display: 'block', textAlign: 'left', marginBottom: 'var(--space-2)', fontSize: '0.875rem', color: 'var(--fg-muted)' }}>
53
+ Busca lo que te haga falta
54
+ </label>
55
+ <div style=\{{ display: 'flex', gap: 'var(--space-2)' }}>
56
+ <input
57
+ id="q"
58
+ type="search"
59
+ value={query}
60
+ onChange={(e) => setQuery(e.target.value)}
61
+ placeholder="Plátano, Presidente fría, recargas Claro…"
62
+ style=\{{
63
+ flex: 1,
64
+ padding: 'var(--space-3) var(--space-4)',
65
+ borderRadius: 'var(--radius-md, 0.5rem)',
66
+ border: '1px solid var(--border)',
67
+ background: 'var(--surface)',
68
+ color: 'var(--fg)',
69
+ fontSize: '1rem',
70
+ }}
71
+ />
72
+ <button type="submit" style=\{{
73
+ padding: 'var(--space-3) var(--space-6)',
74
+ borderRadius: 'var(--radius-md, 0.5rem)',
75
+ border: 'none',
76
+ background: 'var(--primary)',
77
+ color: 'var(--bg)',
78
+ fontWeight: 600,
79
+ cursor: 'pointer',
80
+ }}>
81
+ Buscar
82
+ </button>
83
+ </div>
84
+ </form>
85
+
86
+ {isSignedIn && (
87
+ <p style=\{{ marginTop: 'var(--space-6)', fontSize: '0.875rem' }}>
88
+ <Link href="/cart" style=\{{ color: 'var(--primary)' }}>
89
+ Lo de siempre →
90
+ </Link>
91
+ <span style=\{{ color: 'var(--fg-subtle)' }}> (your usual order, one click away)</span>
92
+ </p>
93
+ )}
94
+ </section>
95
+
96
+ <section style=\{{ padding: 'var(--space-8) var(--space-4)' }}>
97
+ <div style=\{{ maxWidth: '64rem', margin: '0 auto' }}>
98
+ <h2 style=\{{ fontSize: '1.5rem', marginBottom: 'var(--space-4)' }}>Categorías</h2>
99
+ <div style=\{{
100
+ display: 'grid',
101
+ gridTemplateColumns: 'repeat(auto-fill, minmax(140px, 1fr))',
102
+ gap: 'var(--space-3)',
103
+ }}>
104
+ {CATEGORIES.map((cat) => (
105
+ <Link
106
+ key={cat.slug}
107
+ href={`/buscar?cat=${cat.slug}`}
108
+ style=\{{
109
+ display: 'flex',
110
+ flexDirection: 'column',
111
+ alignItems: 'center',
112
+ gap: 'var(--space-2)',
113
+ padding: 'var(--space-4)',
114
+ borderRadius: 'var(--radius-md, 0.5rem)',
115
+ border: '1px solid var(--border)',
116
+ background: 'var(--surface)',
117
+ textAlign: 'center',
118
+ textDecoration: 'none',
119
+ color: 'inherit',
120
+ }}
121
+ >
122
+ <span style=\{{ fontSize: '2rem' }} aria-hidden>{cat.emoji}</span>
123
+ <span style=\{{ fontWeight: 600, fontSize: '0.875rem' }}>{cat.es}</span>
124
+ <span style=\{{ fontSize: '0.75rem', color: 'var(--fg-muted)' }}>{cat.en}</span>
125
+ </Link>
126
+ ))}
127
+ </div>
128
+ </div>
129
+ </section>
130
+
131
+ <section style=\{{ padding: 'var(--space-8) var(--space-4)', background: 'var(--surface-sub)' }}>
132
+ <div style=\{{
133
+ maxWidth: '64rem',
134
+ margin: '0 auto',
135
+ display: 'grid',
136
+ gridTemplateColumns: 'repeat(auto-fit, minmax(280px, 1fr))',
137
+ gap: 'var(--space-6)',
138
+ }}>
139
+ <div>
140
+ <h3 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '1.5rem', marginBottom: 'var(--space-2)' }}>
141
+ ¿Tienes un colmado?
142
+ </h3>
143
+ <p style=\{{ color: 'var(--fg-muted)', marginBottom: 'var(--space-3)' }}>
144
+ Pon tu tienda online en cinco minutos. Tu propia página,
145
+ tus precios, tus pedidos. Te quedas con el 96%.
146
+ </p>
147
+ <Link href="/sell" style=\{{
148
+ display: 'inline-block',
149
+ padding: 'var(--space-3) var(--space-5)',
150
+ borderRadius: 'var(--radius-md, 0.5rem)',
151
+ background: 'var(--accent)',
152
+ color: 'var(--bg)',
153
+ fontWeight: 600,
154
+ textDecoration: 'none',
155
+ }}>
156
+ Vende en {{businessName}} →
157
+ </Link>
158
+ </div>
159
+ <div>
160
+ <h3 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '1.5rem', marginBottom: 'var(--space-2)' }}>
161
+ Buying without signing in?
162
+ </h3>
163
+ <p style=\{{ color: 'var(--fg-muted)', marginBottom: 'var(--space-3)' }}>
164
+ Adelante. You don't need an account to browse, add to your
165
+ funda, or check out. Sign up only if you want to save
166
+ addresses, see past orders, or use store credit.
167
+ </p>
168
+ {!isSignedIn && (
169
+ <Link href="/sign-in" style=\{{ fontSize: '0.875rem', color: 'var(--primary)' }}>
170
+ Save my stuff (optional) →
171
+ </Link>
172
+ )}
173
+ </div>
174
+ </div>
175
+ </section>
176
+ </div>
177
+ );
178
+ }
@@ -0,0 +1,78 @@
1
+ import Link from 'next/link';
2
+
3
+ /**
4
+ * Seller pitch page — `/sell`.
5
+ *
6
+ * Bodega owners land here from the homepage CTA "Vende en
7
+ * {{businessName}}." The job of this page is to convince a small
8
+ * shop owner — often someone who's never used a SaaS — that the
9
+ * platform is worth five minutes of their time.
10
+ *
11
+ * Three commitments worth making explicit:
12
+ * 1. They keep 96% (we take 4% as the platform fee)
13
+ * 2. Sebastn handles payouts directly to their bank
14
+ * 3. No lock-in — they can leave any time
15
+ */
16
+ export default function SellPage() {
17
+ return (
18
+ <div style=\{{ background: 'var(--bg)', color: 'var(--fg)', minHeight: 'calc(100vh - 64px)' }}>
19
+ <section style=\{{ padding: 'var(--space-12) var(--space-4)', textAlign: 'center', maxWidth: '48rem', margin: '0 auto' }}>
20
+ <h1 style=\{{
21
+ fontFamily: 'var(--font-wordmark)',
22
+ fontSize: 'clamp(2rem, 5vw, 3rem)',
23
+ lineHeight: 1.1,
24
+ marginBottom: 'var(--space-3)',
25
+ }}>
26
+ Tu colmado, online en 5 minutos.
27
+ </h1>
28
+ <p style=\{{ color: 'var(--fg-muted)', fontSize: '1.125rem', marginBottom: 'var(--space-6)' }}>
29
+ Take orders from your block, take payment to your bank. We handle
30
+ the website, the cart, the receipts. You handle the colmado.
31
+ </p>
32
+ <Link href="/sign-up?role=seller" style=\{{
33
+ display: 'inline-block',
34
+ padding: 'var(--space-4) var(--space-8)',
35
+ borderRadius: 'var(--radius-md, 0.5rem)',
36
+ background: 'var(--primary)',
37
+ color: 'var(--bg)',
38
+ fontWeight: 600,
39
+ fontSize: '1.125rem',
40
+ textDecoration: 'none',
41
+ }}>
42
+ Empieza gratis →
43
+ </Link>
44
+ </section>
45
+
46
+ <section style=\{{ padding: 'var(--space-8) var(--space-4)', background: 'var(--surface-sub)' }}>
47
+ <div style=\{{ maxWidth: '48rem', margin: '0 auto', display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(220px, 1fr))', gap: 'var(--space-6)' }}>
48
+ <Promise emoji="💵" title="Te quedas con el 96%" body="We take a flat 4% on each order. No monthly fee on the free tier. No surprise charges." />
49
+ <Promise emoji="🏦" title="Pagos directo a tu cuenta" body="Sebastn pays you to your bank the next business day. You keep your existing bank account." />
50
+ <Promise emoji="🚪" title="Sin contratos" body="Cancel any time. Export your customers, your products, your orders. They're yours." />
51
+ </div>
52
+ </section>
53
+
54
+ <section style=\{{ padding: 'var(--space-12) var(--space-4)', textAlign: 'center', maxWidth: '40rem', margin: '0 auto' }}>
55
+ <h2 style=\{{ fontFamily: 'var(--font-wordmark)', fontSize: '1.75rem', marginBottom: 'var(--space-3)' }}>
56
+ ¿Cómo funciona?
57
+ </h2>
58
+ <ol style=\{{ textAlign: 'left', color: 'var(--fg-muted)', lineHeight: 1.8, paddingLeft: 'var(--space-6)' }}>
59
+ <li>Te creas tu cuenta de {{businessName}} (es gratis).</li>
60
+ <li>Conectas tu banco (a través de Sebastn).</li>
61
+ <li>Subes tus productos — o los importas de un CSV.</li>
62
+ <li>Tu colmado vive en <code>{{businessName}}/tu-tienda</code>.</li>
63
+ <li>Los pedidos te llegan por email. Tú decides cómo entregar.</li>
64
+ </ol>
65
+ </section>
66
+ </div>
67
+ );
68
+ }
69
+
70
+ function Promise({ emoji, title, body }: { emoji: string; title: string; body: string }) {
71
+ return (
72
+ <div>
73
+ <div style=\{{ fontSize: '2rem', marginBottom: 'var(--space-2)' }} aria-hidden>{emoji}</div>
74
+ <h3 style=\{{ fontWeight: 600, marginBottom: 'var(--space-2)' }}>{title}</h3>
75
+ <p style=\{{ color: 'var(--fg-muted)', fontSize: '0.875rem' }}>{body}</p>
76
+ </div>
77
+ );
78
+ }
@@ -0,0 +1,7 @@
1
+ import type { NextConfig } from 'next';
2
+
3
+ const nextConfig: NextConfig = {
4
+ output: 'standalone',
5
+ };
6
+
7
+ export default nextConfig;
@@ -0,0 +1,14 @@
1
+ /*
2
+ * Plain template — Tailwind v4 PostCSS plugin is required even though
3
+ * the plain scaffold doesn't use Tailwind utility classes. The auth
4
+ * page CSS shipped by `@githat/nextjs/styles` is processed through
5
+ * @tailwindcss/postcss at build time. Drop this config and the
6
+ * auth pages render unstyled.
7
+ */
8
+ const config = {
9
+ plugins: {
10
+ '@tailwindcss/postcss': {},
11
+ },
12
+ };
13
+
14
+ export default config;
@@ -0,0 +1,10 @@
1
+ import { authProxy } from '@githat/nextjs/proxy';
2
+
3
+ export const proxy = authProxy({
4
+ publicRoutes: ['/', '/sign-in', '/sign-up'{{#if includeForgotPassword}}, '/forgot-password', '/reset-password'{{/if}}{{#if includeEmailVerification}}, '/verify-email'{{/if}}],
5
+ signInUrl: '/sign-in',
6
+ });
7
+
8
+ export const config = {
9
+ matcher: ['/((?!_next|api|.*\\..*).*)'],
10
+ };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Anonymous shopper session helpers.
3
+ *
4
+ * The marketplace template is anonymous-first: shoppers can browse,
5
+ * search, fill a cart, and even check out as guests without ever
6
+ * touching GitHat auth. This module manages the cookie that binds
7
+ * that anonymous activity to a server-side cart row.
8
+ *
9
+ * GitHat is only invoked when the shopper opts in to "Save my stuff."
10
+ * At that point, server code can migrate the anon-cart onto the new
11
+ * GitHat user — see migrateAnonCart() below.
12
+ *
13
+ * The cookie is signed (HMAC-SHA-256) with COLMADO_ANON_SECRET so
14
+ * a malicious shopper can't steal another anon's cart by guessing
15
+ * the session id.
16
+ */
17
+
18
+ import { cookies } from 'next/headers';
19
+ import { createHmac, randomBytes, timingSafeEqual } from 'node:crypto';
20
+
21
+ const COOKIE_NAME = 'anon_session';
22
+ const COOKIE_MAX_AGE = 60 * 60 * 24 * 30; // 30 days
23
+
24
+ function secret(): string {
25
+ const s = process.env.COLMADO_ANON_SECRET || process.env.MARKETPLACE_ANON_SECRET;
26
+ if (!s) {
27
+ throw new Error(
28
+ [
29
+ 'Missing COLMADO_ANON_SECRET (or MARKETPLACE_ANON_SECRET) env var.',
30
+ 'Generate one with the openssl one-liner:',
31
+ ' openssl rand -hex 32',
32
+ 'and add it to .env.local before running `npm run dev` again.',
33
+ ].join(' ')
34
+ );
35
+ }
36
+ return s;
37
+ }
38
+
39
+ function sign(id: string): string {
40
+ return createHmac('sha256', secret()).update(id).digest('hex');
41
+ }
42
+
43
+ function verify(id: string, sig: string): boolean {
44
+ try {
45
+ const expected = sign(id);
46
+ const a = Buffer.from(expected, 'hex');
47
+ const b = Buffer.from(sig, 'hex');
48
+ return a.length === b.length && timingSafeEqual(a, b);
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Returns the current anon session id (creating one if needed).
56
+ * Use this in any server component / route handler that needs to
57
+ * read or write the anon cart.
58
+ */
59
+ export async function getOrCreateAnonSession(): Promise<string> {
60
+ const jar = await cookies();
61
+ const raw = jar.get(COOKIE_NAME)?.value;
62
+
63
+ if (raw) {
64
+ const [id, sig] = raw.split('.');
65
+ if (id && sig && verify(id, sig)) {
66
+ return id;
67
+ }
68
+ }
69
+
70
+ // No valid cookie — issue a fresh one
71
+ const id = randomBytes(16).toString('hex');
72
+ const sig = sign(id);
73
+ jar.set(COOKIE_NAME, `${id}.${sig}`, {
74
+ httpOnly: true,
75
+ sameSite: 'lax',
76
+ secure: process.env.NODE_ENV === 'production',
77
+ maxAge: COOKIE_MAX_AGE,
78
+ path: '/',
79
+ });
80
+ return id;
81
+ }
82
+
83
+ /**
84
+ * Read-only — returns the anon session id if a valid one exists,
85
+ * otherwise null. Use this when you don't want to mint a new cookie
86
+ * just to check.
87
+ */
88
+ export async function readAnonSession(): Promise<string | null> {
89
+ const jar = await cookies();
90
+ const raw = jar.get(COOKIE_NAME)?.value;
91
+ if (!raw) return null;
92
+ const [id, sig] = raw.split('.');
93
+ if (id && sig && verify(id, sig)) return id;
94
+ return null;
95
+ }
96
+
97
+ /**
98
+ * Migrate an anonymous cart onto a freshly-created GitHat user.
99
+ * Call this from a `useEffect` (or server action) right after a
100
+ * successful sign-up: hand it the new user.id and we'll rewrite the
101
+ * cart row, then drop the cookie.
102
+ *
103
+ * Stub for now — wire it to your database in your own code.
104
+ */
105
+ export async function migrateAnonCart(userId: string): Promise<void> {
106
+ const anonId = await readAnonSession();
107
+ if (!anonId) return;
108
+
109
+ // TODO: in your project's data layer, transfer cart rows from
110
+ // `cart:anon:${anonId}` to `cart:user:${userId}`. This file is
111
+ // deliberately backend-agnostic — the marketplace template doesn't
112
+ // ship a database, you bring your own.
113
+ void userId;
114
+
115
+ const jar = await cookies();
116
+ jar.delete(COOKIE_NAME);
117
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Category seed for the marketplace template.
3
+ *
4
+ * Default values are shaped for a Caribbean colmado (Dominican-flavored),
5
+ * which is the {{businessName}} project's first audience. Replace as
6
+ * needed for your region — see CULTURE.md for what's region-specific
7
+ * vs universal.
8
+ *
9
+ * Each category is bilingual on purpose: the Spanish term is what
10
+ * the shopper actually says ("recargas," "víveres") and the English
11
+ * gloss helps Anglophone shoppers navigate without hiding the
12
+ * cultural identity.
13
+ */
14
+
15
+ export interface Category {
16
+ slug: string;
17
+ es: string;
18
+ en: string;
19
+ emoji: string;
20
+ }
21
+
22
+ export const CATEGORIES: Category[] = [
23
+ { slug: 'viveres', es: 'Víveres', en: 'Staples', emoji: '🍚' },
24
+ { slug: 'frutas', es: 'Frutas y verduras', en: 'Produce', emoji: '🥑' },
25
+ { slug: 'carnes', es: 'Carnes', en: 'Meat', emoji: '🥩' },
26
+ { slug: 'lacteos', es: 'Lácteos y huevos', en: 'Dairy & eggs', emoji: '🥚' },
27
+ { slug: 'bebidas', es: 'Bebidas', en: 'Drinks', emoji: '🍺' },
28
+ { slug: 'hielo', es: 'Hielo', en: 'Ice', emoji: '🧊' },
29
+ { slug: 'pan', es: 'Pan y galletas', en: 'Bread & crackers', emoji: '🍞' },
30
+ { slug: 'recargas', es: 'Recargas', en: 'Phone top-ups', emoji: '📱' },
31
+ { slug: 'limpieza', es: 'Limpieza', en: 'Cleaning', emoji: '🧼' },
32
+ { slug: 'higiene', es: 'Higiene personal', en: 'Personal care', emoji: '🪥' },
33
+ { slug: 'sazon', es: 'Sazón', en: 'Seasonings', emoji: '🧂' },
34
+ { slug: 'fritura', es: 'Frituras', en: 'Hot snacks', emoji: '🥟' },
35
+ ];
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "preserve",
15
+ "incremental": true,
16
+ "plugins": [{ "name": "next" }],
17
+ "paths": { "@/*": ["./*"] }
18
+ },
19
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
20
+ "exclude": ["node_modules"]
21
+ }
@@ -0,0 +1,9 @@
1
+ import { SignInForm } from '@githat/nextjs';
2
+
3
+ export default function SignInPage() {
4
+ return (
5
+ <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
6
+ <SignInForm signUpUrl="/sign-up" {{#if includeForgotPassword}}forgotPasswordUrl="/forgot-password"{{/if}} />
7
+ </main>
8
+ );
9
+ }
@@ -0,0 +1,9 @@
1
+ import { SignUpForm } from '@githat/nextjs';
2
+
3
+ export default function SignUpPage() {
4
+ return (
5
+ <main {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b]"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b' }}{{/if}}>
6
+ <SignUpForm signInUrl="/sign-in" />
7
+ </main>
8
+ );
9
+ }
@@ -0,0 +1,87 @@
1
+ /*
2
+ * Tailwind v4 — required because @githat/nextjs/styles is processed
3
+ * through @tailwindcss/postcss. Plain doesn't ship utility classes,
4
+ * but the import is needed for the auth pages to render styled.
5
+ */
6
+ @import "tailwindcss";
7
+
8
+ /*
9
+ * Plain template — self-contained globals.
10
+ *
11
+ * Defines the minimum CSS variables a GitHat app uses for layout and
12
+ * the auth-page styling that ships with @githat/nextjs/styles.
13
+ * Override these in your own files when you want a real theme.
14
+ *
15
+ * Light theme by default; flip --bg/--fg for dark.
16
+ */
17
+
18
+ :root {
19
+ /* Surface */
20
+ --bg: #ffffff;
21
+ --surface: #fafafa;
22
+ --surface-sub: #f4f4f5;
23
+
24
+ /* Borders */
25
+ --border: #e5e7eb;
26
+
27
+ /* Foreground */
28
+ --fg: #0a0a0a;
29
+ --fg-muted: #525252;
30
+ --fg-subtle: #737373;
31
+
32
+ /* Brand — change these two to re-skin the whole auth flow */
33
+ --primary: #6366f1;
34
+ --accent: #f59e0b;
35
+
36
+ /* Semantic */
37
+ --success: #16a34a;
38
+ --warn: #d97706;
39
+ --danger: #dc2626;
40
+
41
+ /* Spacing — used by @githat/nextjs/styles */
42
+ --space-1: 0.25rem;
43
+ --space-2: 0.5rem;
44
+ --space-3: 0.75rem;
45
+ --space-4: 1rem;
46
+ --space-6: 1.5rem;
47
+ --space-8: 2rem;
48
+
49
+ /* Radius */
50
+ --radius: 0.5rem;
51
+ --radius-md: 0.5rem;
52
+ --radius-lg: 0.75rem;
53
+
54
+ /* Fonts */
55
+ --font-sans: 'Inter', system-ui, -apple-system, BlinkMacSystemFont, sans-serif;
56
+ --font-wordmark: 'Instrument Serif', Georgia, serif;
57
+ }
58
+
59
+ @media (prefers-color-scheme: dark) {
60
+ :root {
61
+ --bg: #0a0a0a;
62
+ --surface: #18181b;
63
+ --surface-sub: #27272a;
64
+ --border: #3f3f46;
65
+ --fg: #fafafa;
66
+ --fg-muted: #a1a1aa;
67
+ --fg-subtle: #71717a;
68
+ }
69
+ }
70
+
71
+ * {
72
+ box-sizing: border-box;
73
+ margin: 0;
74
+ padding: 0;
75
+ }
76
+
77
+ body {
78
+ font-family: var(--font-sans);
79
+ background: var(--bg);
80
+ color: var(--fg);
81
+ line-height: 1.5;
82
+ }
83
+
84
+ a {
85
+ color: inherit;
86
+ text-decoration: none;
87
+ }