create-githat-app 1.3.0 → 1.4.1
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 +48 -18
- package/dist/cli.js +1161 -114
- package/package.json +34 -9
- package/templates/agent/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/agent/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/agent/app/admin/agent/page.tsx.hbs +127 -0
- package/templates/agent/app/globals.css.hbs +87 -0
- package/templates/agent/app/layout.tsx.hbs +41 -0
- package/templates/agent/app/page.tsx.hbs +100 -0
- package/templates/agent/next.config.ts.hbs +9 -0
- package/templates/agent/postcss.config.mjs.hbs +14 -0
- package/templates/agent/proxy.ts.hbs +10 -0
- package/templates/agent/tsconfig.json.hbs +21 -0
- package/templates/base/.env.example.hbs +2 -2
- package/templates/base/.env.local.example.hbs +20 -0
- package/templates/base/.env.local.hbs +13 -2
- package/templates/base/.github/CODEOWNERS.hbs +1 -0
- package/templates/base/.github/SECURITY.md +10 -0
- package/templates/base/.github/dependabot.yml +19 -0
- package/templates/base/.github/workflows/ci.yml.hbs +77 -0
- package/templates/base/.github/workflows/githat-policy.yml +51 -0
- package/templates/base/.gitignore.hbs +17 -2
- package/templates/base/README.md.hbs +31 -52
- package/templates/classroom/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/classroom/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/classroom/app/globals.css.hbs +87 -0
- package/templates/classroom/app/layout.tsx.hbs +41 -0
- package/templates/classroom/app/page.tsx.hbs +103 -0
- package/templates/classroom/app/projects/[id]/feedback/page.tsx.hbs +159 -0
- package/templates/classroom/app/projects/[id]/present/page.tsx.hbs +113 -0
- package/templates/classroom/next.config.ts.hbs +9 -0
- package/templates/classroom/postcss.config.mjs.hbs +14 -0
- package/templates/classroom/proxy.ts.hbs +10 -0
- package/templates/classroom/tsconfig.json.hbs +21 -0
- package/templates/content/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/content/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/content/app/globals.css.hbs +87 -0
- package/templates/content/app/layout.tsx.hbs +41 -0
- package/templates/content/app/newsletter/page.tsx.hbs +90 -0
- package/templates/content/app/page.tsx.hbs +105 -0
- package/templates/content/app/posts/[slug]/page.tsx.hbs +119 -0
- package/templates/content/next.config.ts.hbs +9 -0
- package/templates/content/postcss.config.mjs.hbs +14 -0
- package/templates/content/proxy.ts.hbs +10 -0
- package/templates/content/tsconfig.json.hbs +21 -0
- package/templates/dashboard/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/dashboard/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/dashboard/app/admin/data/[entity]/page.tsx.hbs +68 -0
- package/templates/dashboard/app/admin/page.tsx.hbs +59 -0
- package/templates/dashboard/app/globals.css.hbs +87 -0
- package/templates/dashboard/app/layout.tsx.hbs +41 -0
- package/templates/dashboard/app/page.tsx.hbs +57 -0
- package/templates/dashboard/next.config.ts.hbs +9 -0
- package/templates/dashboard/postcss.config.mjs.hbs +14 -0
- package/templates/dashboard/proxy.ts.hbs +10 -0
- package/templates/dashboard/src/lib/db.ts.hbs +39 -0
- package/templates/dashboard/tsconfig.json.hbs +21 -0
- package/templates/fullstack/apps-api-express/.env.example.hbs +6 -0
- package/templates/fullstack/apps-api-express/.env.local.hbs +6 -0
- package/templates/fullstack/apps-api-express/package.json.hbs +24 -0
- package/templates/fullstack/apps-api-express/src/index.ts.hbs +41 -0
- package/templates/fullstack/apps-api-express/src/routes/health.ts.hbs +11 -0
- package/templates/fullstack/apps-api-express/src/routes/users.ts.hbs +43 -0
- package/templates/fullstack/apps-api-express/tsconfig.json.hbs +16 -0
- package/templates/fullstack/apps-api-fastify/.env.example.hbs +6 -0
- package/templates/fullstack/apps-api-fastify/.env.local.hbs +6 -0
- package/templates/fullstack/apps-api-fastify/package.json.hbs +22 -0
- package/templates/fullstack/apps-api-fastify/src/index.ts.hbs +28 -0
- package/templates/fullstack/apps-api-fastify/src/routes/health.ts.hbs +11 -0
- package/templates/fullstack/apps-api-fastify/src/routes/users.ts.hbs +43 -0
- package/templates/fullstack/apps-api-fastify/tsconfig.json.hbs +16 -0
- package/templates/fullstack/apps-api-hono/.env.example.hbs +6 -0
- package/templates/fullstack/apps-api-hono/.env.local.hbs +6 -0
- package/templates/fullstack/apps-api-hono/package.json.hbs +22 -0
- package/templates/fullstack/apps-api-hono/src/index.ts.hbs +35 -0
- package/templates/fullstack/apps-api-hono/src/routes/health.ts.hbs +11 -0
- package/templates/fullstack/apps-api-hono/src/routes/users.ts.hbs +43 -0
- package/templates/fullstack/apps-api-hono/tsconfig.json.hbs +16 -0
- package/templates/fullstack/apps-web-nextjs/.env.example.hbs +5 -0
- package/templates/fullstack/apps-web-nextjs/.env.local.hbs +5 -0
- package/templates/fullstack/apps-web-nextjs/app/(auth)/forgot-password/page.tsx.hbs +11 -0
- package/templates/fullstack/apps-web-nextjs/app/(auth)/reset-password/page.tsx.hbs +39 -0
- package/templates/fullstack/apps-web-nextjs/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/fullstack/apps-web-nextjs/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/fullstack/apps-web-nextjs/app/(auth)/verify-email/page.tsx.hbs +11 -0
- package/templates/fullstack/apps-web-nextjs/app/dashboard/layout.tsx.hbs +15 -0
- package/templates/fullstack/apps-web-nextjs/app/dashboard/page.tsx.hbs +27 -0
- package/templates/fullstack/apps-web-nextjs/app/globals.css.hbs +21 -0
- package/templates/fullstack/apps-web-nextjs/app/layout.tsx.hbs +30 -0
- package/templates/fullstack/apps-web-nextjs/app/page.tsx.hbs +17 -0
- package/templates/fullstack/apps-web-nextjs/next.config.ts.hbs +17 -0
- package/templates/fullstack/apps-web-nextjs/package.json.hbs +34 -0
- package/templates/fullstack/apps-web-nextjs/postcss.config.mjs.hbs +9 -0
- package/templates/fullstack/apps-web-nextjs/tsconfig.json.hbs +21 -0
- package/templates/fullstack/root/.gitignore.hbs +42 -0
- package/templates/fullstack/root/githat.yaml.hbs +17 -0
- package/templates/fullstack/root/package.json.hbs +15 -0
- package/templates/fullstack/root/turbo.json.hbs +20 -0
- package/templates/marketplace/CULTURE.md +74 -0
- package/templates/marketplace/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/marketplace/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/marketplace/app/(shop)/[slug]/p/[productId]/page.tsx.hbs +99 -0
- package/templates/marketplace/app/(shop)/[slug]/page.tsx.hbs +90 -0
- package/templates/marketplace/app/admin/page.tsx.hbs +95 -0
- package/templates/marketplace/app/cart/page.tsx.hbs +157 -0
- package/templates/marketplace/app/globals.css.hbs +87 -0
- package/templates/marketplace/app/layout.tsx.hbs +77 -0
- package/templates/marketplace/app/page.tsx.hbs +178 -0
- package/templates/marketplace/app/sell/page.tsx.hbs +78 -0
- package/templates/marketplace/next.config.ts.hbs +9 -0
- package/templates/marketplace/postcss.config.mjs.hbs +14 -0
- package/templates/marketplace/proxy.ts.hbs +10 -0
- package/templates/marketplace/src/lib/anon-session.ts.hbs +117 -0
- package/templates/marketplace/src/lib/categories.ts.hbs +35 -0
- package/templates/marketplace/tsconfig.json.hbs +21 -0
- package/templates/nextjs/.github/workflows/deploy.yml.hbs +107 -0
- package/templates/nextjs/app/(auth)/reset-password/page.tsx.hbs +106 -0
- package/templates/nextjs/app/globals.css.hbs +4 -3
- package/templates/nextjs/app/layout.tsx.hbs +5 -1
- package/templates/nextjs/app/page.tsx.hbs +3 -6
- package/templates/nextjs/next.config.ts.hbs +7 -3
- package/templates/nextjs/proxy.ts.hbs +1 -1
- package/templates/plain/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/plain/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/plain/app/globals.css.hbs +87 -0
- package/templates/plain/app/layout.tsx.hbs +41 -0
- package/templates/plain/app/page.tsx.hbs +123 -0
- package/templates/plain/next.config.ts.hbs +9 -0
- package/templates/plain/postcss.config.mjs.hbs +14 -0
- package/templates/plain/proxy.ts.hbs +10 -0
- package/templates/plain/tsconfig.json.hbs +21 -0
- package/templates/portfolio/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/portfolio/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/portfolio/app/globals.css.hbs +87 -0
- package/templates/portfolio/app/layout.tsx.hbs +41 -0
- package/templates/portfolio/app/page.tsx.hbs +86 -0
- package/templates/portfolio/next.config.ts.hbs +9 -0
- package/templates/portfolio/postcss.config.mjs.hbs +14 -0
- package/templates/portfolio/proxy.ts.hbs +10 -0
- package/templates/portfolio/tsconfig.json.hbs +21 -0
- package/templates/react-vite/src/App.tsx.hbs +11 -9
- package/templates/react-vite/src/index.css.hbs +4 -3
- package/templates/react-vite/src/pages/Home.tsx.hbs +3 -6
- package/templates/saas/app/(auth)/sign-in/page.tsx.hbs +9 -0
- package/templates/saas/app/(auth)/sign-up/page.tsx.hbs +9 -0
- package/templates/saas/app/admin/billing/page.tsx.hbs +145 -0
- package/templates/saas/app/admin/page.tsx.hbs +106 -0
- package/templates/saas/app/admin/team/page.tsx.hbs +134 -0
- package/templates/saas/app/globals.css.hbs +87 -0
- package/templates/saas/app/layout.tsx.hbs +41 -0
- package/templates/saas/app/page.tsx.hbs +108 -0
- package/templates/saas/app/pricing/page.tsx.hbs +131 -0
- package/templates/saas/next.config.ts.hbs +9 -0
- package/templates/saas/postcss.config.mjs.hbs +14 -0
- package/templates/saas/proxy.ts.hbs +10 -0
- package/templates/saas/tsconfig.json.hbs +21 -0
|
@@ -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,107 @@
|
|
|
1
|
+
name: Deploy to EC2
|
|
2
|
+
|
|
3
|
+
# Auto-deploy every push to main. Also supports manual runs from the
|
|
4
|
+
# Actions tab (workflow_dispatch) so you can re-ship without a dummy commit.
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
workflow_dispatch:
|
|
9
|
+
|
|
10
|
+
# Only one deploy at a time. Queue rather than race if a second push
|
|
11
|
+
# lands mid-deploy.
|
|
12
|
+
concurrency:
|
|
13
|
+
group: deploy-ec2
|
|
14
|
+
cancel-in-progress: false
|
|
15
|
+
|
|
16
|
+
jobs:
|
|
17
|
+
build-and-deploy:
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
# Skip if commit message starts with "docs:" unless it includes [deploy].
|
|
20
|
+
if: $\{{ !startsWith(github.event.head_commit.message, 'docs:') || contains(github.event.head_commit.message, '[deploy]') }}
|
|
21
|
+
timeout-minutes: 12
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
|
|
26
|
+
- name: Setup Node 20
|
|
27
|
+
uses: actions/setup-node@v4
|
|
28
|
+
with:
|
|
29
|
+
node-version: "20"
|
|
30
|
+
cache: "npm"
|
|
31
|
+
|
|
32
|
+
- name: Install deps
|
|
33
|
+
run: npm ci
|
|
34
|
+
|
|
35
|
+
- name: Type-check
|
|
36
|
+
run: npx tsc --noEmit
|
|
37
|
+
|
|
38
|
+
- name: Compute BUILD_ID
|
|
39
|
+
id: build_id
|
|
40
|
+
run: |
|
|
41
|
+
BUILD_ID=$(git rev-parse --short HEAD || echo "${GITHUB_SHA:0:7}")
|
|
42
|
+
echo "BUILD_ID=$BUILD_ID" >> "$GITHUB_OUTPUT"
|
|
43
|
+
echo "Resolved BUILD_ID=$BUILD_ID"
|
|
44
|
+
|
|
45
|
+
- name: Build (Next.js standalone)
|
|
46
|
+
run: npm run build
|
|
47
|
+
env:
|
|
48
|
+
# NEXT_PUBLIC_* vars are baked into the client bundle at build time.
|
|
49
|
+
# The real value comes from the GITHAT_PUBLISHABLE_KEY repo secret.
|
|
50
|
+
NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY: $\{{ secrets.GITHAT_PUBLISHABLE_KEY }}
|
|
51
|
+
BUILD_ID: $\{{ steps.build_id.outputs.BUILD_ID }}
|
|
52
|
+
|
|
53
|
+
- name: Load SSH key
|
|
54
|
+
run: |
|
|
55
|
+
mkdir -p ~/.ssh
|
|
56
|
+
echo "$\{{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
|
|
57
|
+
chmod 600 ~/.ssh/deploy_key
|
|
58
|
+
ssh-keyscan -H "${EC2_HOST#*@}" >> ~/.ssh/known_hosts
|
|
59
|
+
env:
|
|
60
|
+
EC2_HOST: $\{{ secrets.EC2_HOST }}
|
|
61
|
+
|
|
62
|
+
- name: Sync production .env
|
|
63
|
+
env:
|
|
64
|
+
PROD_ENV: $\{{ secrets.PROD_ENV }}
|
|
65
|
+
EC2_HOST: $\{{ secrets.EC2_HOST }}
|
|
66
|
+
run: |
|
|
67
|
+
if [ -z "$PROD_ENV" ]; then
|
|
68
|
+
echo "PROD_ENV secret is empty — skipping env sync."
|
|
69
|
+
exit 0
|
|
70
|
+
fi
|
|
71
|
+
SSH_OPTS="-i ~/.ssh/deploy_key -o StrictHostKeyChecking=yes"
|
|
72
|
+
printf '%s' "$PROD_ENV" > /tmp/prod.env
|
|
73
|
+
rsync -az -e "ssh $SSH_OPTS" /tmp/prod.env "$EC2_HOST:/opt/{{projectName}}/.env"
|
|
74
|
+
shred -u /tmp/prod.env 2>/dev/null || rm -f /tmp/prod.env
|
|
75
|
+
ssh $SSH_OPTS "$EC2_HOST" \
|
|
76
|
+
"sudo chgrp {{projectName}} /opt/{{projectName}}/.env && sudo chmod 640 /opt/{{projectName}}/.env"
|
|
77
|
+
|
|
78
|
+
- name: Deploy to EC2
|
|
79
|
+
env:
|
|
80
|
+
EC2_HOST: $\{{ secrets.EC2_HOST }}
|
|
81
|
+
run: |
|
|
82
|
+
SSH_OPTS="-i ~/.ssh/deploy_key -o StrictHostKeyChecking=yes"
|
|
83
|
+
|
|
84
|
+
rsync -az --delete -e "ssh $SSH_OPTS" \
|
|
85
|
+
--exclude .env \
|
|
86
|
+
.next/standalone/ "$EC2_HOST:/opt/{{projectName}}/"
|
|
87
|
+
|
|
88
|
+
rsync -az -e "ssh $SSH_OPTS" \
|
|
89
|
+
.next/static/ "$EC2_HOST:/opt/{{projectName}}/.next/static/"
|
|
90
|
+
|
|
91
|
+
rsync -az -e "ssh $SSH_OPTS" \
|
|
92
|
+
public/ "$EC2_HOST:/opt/{{projectName}}/public/"
|
|
93
|
+
|
|
94
|
+
ssh $SSH_OPTS "$EC2_HOST" "
|
|
95
|
+
sudo systemctl restart {{projectName}}
|
|
96
|
+
sleep 5
|
|
97
|
+
sudo systemctl is-active --quiet {{projectName}} || { sudo journalctl -u {{projectName}} -n 30 --no-pager; exit 1; }
|
|
98
|
+
echo \"deployed build \$(cat /opt/{{projectName}}/.next/BUILD_ID)\"
|
|
99
|
+
"
|
|
100
|
+
|
|
101
|
+
- name: Smoke-test the deploy
|
|
102
|
+
env:
|
|
103
|
+
EC2_HOST: $\{{ secrets.EC2_HOST }}
|
|
104
|
+
run: |
|
|
105
|
+
SSH_OPTS="-i ~/.ssh/deploy_key -o StrictHostKeyChecking=yes"
|
|
106
|
+
ssh $SSH_OPTS "$EC2_HOST" \
|
|
107
|
+
"curl --fail --silent --show-error --max-time 10 http://127.0.0.1:3000/api/health > /dev/null && echo OK"
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
{{#if includeForgotPassword}}
|
|
2
|
+
'use client';
|
|
3
|
+
|
|
4
|
+
import { Suspense, useState } from 'react';
|
|
5
|
+
import { useSearchParams, useRouter } from 'next/navigation';
|
|
6
|
+
{{#if includeGithatFolder}}
|
|
7
|
+
import { authApi } from '../../../githat/api/auth{{#unless typescript}}.js{{/unless}}';
|
|
8
|
+
{{/if}}
|
|
9
|
+
|
|
10
|
+
function ResetPasswordContent() {
|
|
11
|
+
const searchParams = useSearchParams();
|
|
12
|
+
const router = useRouter();
|
|
13
|
+
const token = searchParams.get('token');
|
|
14
|
+
|
|
15
|
+
const [password, setPassword] = useState('');
|
|
16
|
+
const [confirm, setConfirm] = useState('');
|
|
17
|
+
const [error, setError] = useState('');
|
|
18
|
+
const [loading, setLoading] = useState(false);
|
|
19
|
+
|
|
20
|
+
const handleSubmit = async (e{{#if typescript}}: React.FormEvent{{/if}}) => {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
if (password !== confirm) {
|
|
23
|
+
setError('Passwords do not match');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
setError('');
|
|
27
|
+
setLoading(true);
|
|
28
|
+
try {
|
|
29
|
+
{{#if includeGithatFolder}}
|
|
30
|
+
await authApi.resetPassword(token!, password);
|
|
31
|
+
{{else}}
|
|
32
|
+
const res = await fetch('{{apiUrl}}/auth/reset-password', {
|
|
33
|
+
method: 'POST',
|
|
34
|
+
headers: { 'Content-Type': 'application/json' },
|
|
35
|
+
body: JSON.stringify({ token, password }),
|
|
36
|
+
});
|
|
37
|
+
if (!res.ok) throw new Error('Reset failed');
|
|
38
|
+
{{/if}}
|
|
39
|
+
router.push('/sign-in?reset=success');
|
|
40
|
+
} catch (err) {
|
|
41
|
+
setError('Failed to reset password. The link may have expired.');
|
|
42
|
+
} finally {
|
|
43
|
+
setLoading(false);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
if (!token) {
|
|
48
|
+
return (
|
|
49
|
+
<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}}>
|
|
50
|
+
<div style=\{{ textAlign: 'center' }}>
|
|
51
|
+
<h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '0.5rem' }}>Invalid reset link</h1>
|
|
52
|
+
<p style=\{{ color: '#a1a1aa' }}>
|
|
53
|
+
<a href="/forgot-password" style=\{{ color: '#7c3aed' }}>Request a new one</a>
|
|
54
|
+
</p>
|
|
55
|
+
</div>
|
|
56
|
+
</main>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<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}}>
|
|
62
|
+
<div style=\{{ width: '100%', maxWidth: '24rem', padding: '2rem' }}>
|
|
63
|
+
<h1 style=\{{ fontSize: '1.5rem', fontWeight: 600, color: '#fafafa', marginBottom: '0.5rem' }}>Reset password</h1>
|
|
64
|
+
<p style=\{{ color: '#a1a1aa', marginBottom: '1.5rem' }}>Enter your new password below.</p>
|
|
65
|
+
{error && <p style=\{{ color: '#ef4444', marginBottom: '1rem', fontSize: '0.875rem' }}>{error}</p>}
|
|
66
|
+
<form onSubmit={handleSubmit}>
|
|
67
|
+
<input
|
|
68
|
+
type="password"
|
|
69
|
+
value={password}
|
|
70
|
+
onChange={(e) => setPassword(e.target.value)}
|
|
71
|
+
placeholder="New password"
|
|
72
|
+
required
|
|
73
|
+
minLength={8}
|
|
74
|
+
disabled={loading}
|
|
75
|
+
style=\{{ width: '100%', padding: '0.625rem 0.75rem', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.375rem', color: '#fafafa', marginBottom: '1rem', outline: 'none' }}
|
|
76
|
+
/>
|
|
77
|
+
<input
|
|
78
|
+
type="password"
|
|
79
|
+
value={confirm}
|
|
80
|
+
onChange={(e) => setConfirm(e.target.value)}
|
|
81
|
+
placeholder="Confirm password"
|
|
82
|
+
required
|
|
83
|
+
disabled={loading}
|
|
84
|
+
style=\{{ width: '100%', padding: '0.625rem 0.75rem', background: '#111113', border: '1px solid #1e1e2e', borderRadius: '0.375rem', color: '#fafafa', marginBottom: '1rem', outline: 'none' }}
|
|
85
|
+
/>
|
|
86
|
+
<button
|
|
87
|
+
type="submit"
|
|
88
|
+
disabled={loading}
|
|
89
|
+
style=\{{ width: '100%', padding: '0.625rem', background: '#7c3aed', color: '#fff', border: 'none', borderRadius: '0.375rem', fontWeight: 600, cursor: loading ? 'not-allowed' : 'pointer', opacity: loading ? 0.7 : 1 }}
|
|
90
|
+
>
|
|
91
|
+
{loading ? 'Resetting...' : 'Reset password'}
|
|
92
|
+
</button>
|
|
93
|
+
</form>
|
|
94
|
+
</div>
|
|
95
|
+
</main>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export default function ResetPasswordPage() {
|
|
100
|
+
return (
|
|
101
|
+
<Suspense fallback={<div {{#if useTailwind}}className="flex items-center justify-center min-h-screen bg-[#09090b] text-zinc-400"{{else}}style=\{{ display: 'flex', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', background: '#09090b', color: '#a1a1aa' }}{{/if}}>Loading...</div>}>
|
|
102
|
+
<ResetPasswordContent />
|
|
103
|
+
</Suspense>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
{{/if}}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
@import "@githat/ui/tokens.css";
|
|
1
2
|
{{#if useTailwind}}
|
|
2
3
|
@import "tailwindcss";
|
|
3
4
|
{{/if}}
|
|
@@ -9,9 +10,9 @@
|
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
body {
|
|
12
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
13
|
-
background: #09090b;
|
|
14
|
-
color: #fafafa;
|
|
13
|
+
font-family: var(--font-sans, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
14
|
+
background: var(--bg, #09090b);
|
|
15
|
+
color: var(--fg, #fafafa);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
a {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { GitHatProvider } from '@githat/nextjs';
|
|
2
|
+
import { Wordmark } from '@githat/ui';
|
|
2
3
|
import '@githat/nextjs/styles';
|
|
3
4
|
import './globals.css';
|
|
4
5
|
{{#if includeGithatFolder}}
|
|
@@ -25,7 +26,10 @@ export default function RootLayout({ children }{{#if typescript}}: { children: R
|
|
|
25
26
|
afterSignOutUrl: '/',
|
|
26
27
|
{{/if}}
|
|
27
28
|
}}>
|
|
28
|
-
{
|
|
29
|
+
<header style=\{{ padding: 'var(--space-4, 1rem) var(--space-6, 1.5rem)', borderBottom: '1px solid var(--border, #e5e7eb)' }}>
|
|
30
|
+
<Wordmark name="{{businessName}}" size="md" href="/" />
|
|
31
|
+
</header>
|
|
32
|
+
<main>{children}</main>
|
|
29
33
|
</GitHatProvider>
|
|
30
34
|
</body>
|
|
31
35
|
</html>
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SignInButton, SignUpButton } from '@githat/nextjs';
|
|
2
|
+
import { Wordmark } from '@githat/ui';
|
|
2
3
|
|
|
3
4
|
const hasKey = !!process.env.NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY;
|
|
4
5
|
|
|
@@ -6,9 +7,7 @@ function SetupGuide() {
|
|
|
6
7
|
return (
|
|
7
8
|
<main {{#if useTailwind}}className="flex flex-col items-center justify-center min-h-screen gap-8 bg-[#09090b] text-[#fafafa] px-6"{{else}}style=\{{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', gap: '2rem', background: '#09090b', color: '#fafafa', padding: '0 1.5rem' }}{{/if}}>
|
|
8
9
|
<div {{#if useTailwind}}className="text-center"{{else}}style=\{{ textAlign: 'center' }}{{/if}}>
|
|
9
|
-
<
|
|
10
|
-
{{businessName}}
|
|
11
|
-
</h1>
|
|
10
|
+
<Wordmark name="{{businessName}}" size="xl" />
|
|
12
11
|
<p {{#if useTailwind}}className="text-zinc-400"{{else}}style=\{{ color: '#a1a1aa' }}{{/if}}>
|
|
13
12
|
Get started in 3 steps
|
|
14
13
|
</p>
|
|
@@ -58,9 +57,7 @@ export default function Home() {
|
|
|
58
57
|
|
|
59
58
|
return (
|
|
60
59
|
<main {{#if useTailwind}}className="flex flex-col items-center justify-center min-h-screen gap-6 bg-[#09090b] text-[#fafafa]"{{else}}style=\{{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', minHeight: '100vh', gap: '1.5rem', background: '#09090b', color: '#fafafa' }}{{/if}}>
|
|
61
|
-
<
|
|
62
|
-
Welcome to {{businessName}}
|
|
63
|
-
</h1>
|
|
60
|
+
<Wordmark name="{{businessName}}" size="xl" />
|
|
64
61
|
<p {{#if useTailwind}}className="text-zinc-400 max-w-lg text-center"{{else}}style=\{{ color: '#a1a1aa', maxWidth: '32rem', textAlign: 'center' }}{{/if}}>
|
|
65
62
|
{{description}}
|
|
66
63
|
</p>
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type { NextConfig } from 'next';
|
|
1
|
+
{{#if typescript}}import type { NextConfig } from 'next';
|
|
2
|
+
{{/if}}import { withGitHat } from '@githat/nextjs/server';
|
|
2
3
|
|
|
3
|
-
const nextConfig: NextConfig = {
|
|
4
|
+
{{#if typescript}}const nextConfig: NextConfig = {
|
|
5
|
+
{{else}}const nextConfig = {
|
|
6
|
+
{{/if}} output: 'standalone',
|
|
7
|
+
};
|
|
4
8
|
|
|
5
|
-
export default nextConfig;
|
|
9
|
+
export default withGitHat(nextConfig);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { authProxy } from '@githat/nextjs/proxy';
|
|
2
2
|
|
|
3
3
|
export const proxy = authProxy({
|
|
4
|
-
publicRoutes: ['/', '/sign-in', '/sign-up'{{#if includeForgotPassword}}, '/forgot-password'{{/if}}{{#if includeEmailVerification}}, '/verify-email'{{/if}}],
|
|
4
|
+
publicRoutes: ['/', '/sign-in', '/sign-up'{{#if includeForgotPassword}}, '/forgot-password', '/reset-password'{{/if}}{{#if includeEmailVerification}}, '/verify-email'{{/if}}],
|
|
5
5
|
signInUrl: '/sign-in',
|
|
6
6
|
});
|
|
7
7
|
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { GitHatProvider } from '@githat/nextjs';
|
|
2
|
+
import '@githat/nextjs/styles';
|
|
3
|
+
import './globals.css';
|
|
4
|
+
|
|
5
|
+
export const metadata = {
|
|
6
|
+
title: '{{businessName}}',
|
|
7
|
+
description: '{{description}}',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
export default function RootLayout({ children }{{#if typescript}}: { children: React.ReactNode }{{/if}}) {
|
|
11
|
+
return (
|
|
12
|
+
<html lang="en">
|
|
13
|
+
<body>
|
|
14
|
+
{/*
|
|
15
|
+
Plain template: no @githat/ui dep, no Wordmark. The
|
|
16
|
+
full-kit (`nextjs`) template uses @githat/ui for the
|
|
17
|
+
shared design system; the plain scaffold is the smallest
|
|
18
|
+
working app, so we avoid extra deps.
|
|
19
|
+
*/}
|
|
20
|
+
<GitHatProvider config=\{{
|
|
21
|
+
publishableKey: process.env.NEXT_PUBLIC_GITHAT_PUBLISHABLE_KEY || '',
|
|
22
|
+
signInUrl: '/sign-in',
|
|
23
|
+
signUpUrl: '/sign-up',
|
|
24
|
+
afterSignInUrl: '/',
|
|
25
|
+
afterSignOutUrl: '/',
|
|
26
|
+
}}>
|
|
27
|
+
<header style=\{{
|
|
28
|
+
padding: '1rem 1.5rem',
|
|
29
|
+
borderBottom: '1px solid var(--border, #e5e7eb)',
|
|
30
|
+
fontFamily: 'system-ui, -apple-system, sans-serif',
|
|
31
|
+
}}>
|
|
32
|
+
<a href="/" style=\{{ textDecoration: 'none', color: 'inherit', fontWeight: 600 }}>
|
|
33
|
+
{{businessName}}
|
|
34
|
+
</a>
|
|
35
|
+
</header>
|
|
36
|
+
<main>{children}</main>
|
|
37
|
+
</GitHatProvider>
|
|
38
|
+
</body>
|
|
39
|
+
</html>
|
|
40
|
+
);
|
|
41
|
+
}
|