@aex.is/zero 0.1.9 → 0.1.11
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 +4 -4
- package/bin/zero-darwin-amd64 +0 -0
- package/bin/zero-darwin-arm64 +0 -0
- package/bin/zero-linux-amd64 +0 -0
- package/bin/zero-linux-arm64 +0 -0
- package/bin/zero-windows-amd64.exe +0 -0
- package/dist/config/base-env.js +3 -7
- package/dist/config/modules.js +34 -0
- package/dist/engine/scaffold.js +5 -1
- package/dist/engine/templates.js +201 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,14 +28,14 @@ zero
|
|
|
28
28
|
|
|
29
29
|
- Next.js App Router + Tailwind + shadcn-ready config.
|
|
30
30
|
- Expo Router + Tamagui.
|
|
31
|
-
- Minimal layout,
|
|
32
|
-
- Contact form wired to `/api/contact` (Next) and
|
|
31
|
+
- Minimal layout, four routes, metadata, and icon generation.
|
|
32
|
+
- Contact form wired to `/api/contact` (Next) and a device POST to your backend (Expo).
|
|
33
33
|
|
|
34
34
|
## Environment variables
|
|
35
35
|
|
|
36
36
|
The CLI generates `.env.example` with:
|
|
37
|
-
- Next.js: `RESEND_API_KEY`, `
|
|
38
|
-
- Expo: `
|
|
37
|
+
- Next.js: `RESEND_API_KEY`, `CONTACT_FROM_EMAIL`, `CONTACT_TO_EMAIL`.
|
|
38
|
+
- Expo: `EXPO_PUBLIC_CONTACT_ENDPOINT`.
|
|
39
39
|
- Any selected module keys (Neon, Clerk, Payload, Stripe).
|
|
40
40
|
|
|
41
41
|
## Development
|
package/bin/zero-darwin-amd64
CHANGED
|
Binary file
|
package/bin/zero-darwin-arm64
CHANGED
|
Binary file
|
package/bin/zero-linux-amd64
CHANGED
|
Binary file
|
package/bin/zero-linux-arm64
CHANGED
|
Binary file
|
|
Binary file
|
package/dist/config/base-env.js
CHANGED
|
@@ -5,22 +5,18 @@ const nextBaseEnv = [
|
|
|
5
5
|
url: 'https://resend.com'
|
|
6
6
|
},
|
|
7
7
|
{
|
|
8
|
-
key: '
|
|
8
|
+
key: 'CONTACT_FROM_EMAIL',
|
|
9
9
|
description: 'Verified sender email address'
|
|
10
10
|
},
|
|
11
11
|
{
|
|
12
|
-
key: '
|
|
12
|
+
key: 'CONTACT_TO_EMAIL',
|
|
13
13
|
description: 'Destination email address'
|
|
14
14
|
}
|
|
15
15
|
];
|
|
16
16
|
const expoBaseEnv = [
|
|
17
|
-
{
|
|
18
|
-
key: 'EXPO_PUBLIC_CONTACT_EMAIL',
|
|
19
|
-
description: 'Email address used for contact form'
|
|
20
|
-
},
|
|
21
17
|
{
|
|
22
18
|
key: 'EXPO_PUBLIC_CONTACT_ENDPOINT',
|
|
23
|
-
description: '
|
|
19
|
+
description: 'Contact API endpoint (e.g. https://yourdomain.com/api/contact)'
|
|
24
20
|
}
|
|
25
21
|
];
|
|
26
22
|
export function getBaseEnvHelp(framework) {
|
package/dist/config/modules.js
CHANGED
|
@@ -3,6 +3,10 @@ export const modules = [
|
|
|
3
3
|
id: 'neon',
|
|
4
4
|
label: 'Database (Neon)',
|
|
5
5
|
description: 'Serverless Postgres with Neon.',
|
|
6
|
+
connect: {
|
|
7
|
+
label: 'Connect to Neon',
|
|
8
|
+
url: 'https://console.neon.tech/'
|
|
9
|
+
},
|
|
6
10
|
envVars: [
|
|
7
11
|
{
|
|
8
12
|
key: 'DATABASE_URL',
|
|
@@ -19,6 +23,10 @@ export const modules = [
|
|
|
19
23
|
id: 'clerk',
|
|
20
24
|
label: 'Auth (Clerk)',
|
|
21
25
|
description: 'Authentication with Clerk.',
|
|
26
|
+
connect: {
|
|
27
|
+
label: 'Connect to Clerk',
|
|
28
|
+
url: 'https://dashboard.clerk.com'
|
|
29
|
+
},
|
|
22
30
|
envVars: [
|
|
23
31
|
{
|
|
24
32
|
key: 'CLERK_PUBLISHABLE_KEY',
|
|
@@ -40,6 +48,10 @@ export const modules = [
|
|
|
40
48
|
id: 'payload',
|
|
41
49
|
label: 'CMS (Payload)',
|
|
42
50
|
description: 'Headless CMS using Payload.',
|
|
51
|
+
connect: {
|
|
52
|
+
label: 'Generate Payload Secret',
|
|
53
|
+
url: 'https://payloadcms.com/docs'
|
|
54
|
+
},
|
|
43
55
|
envVars: [
|
|
44
56
|
{
|
|
45
57
|
key: 'PAYLOAD_SECRET',
|
|
@@ -61,6 +73,10 @@ export const modules = [
|
|
|
61
73
|
id: 'stripe',
|
|
62
74
|
label: 'Payments (Stripe)',
|
|
63
75
|
description: 'Payments via Stripe SDK.',
|
|
76
|
+
connect: {
|
|
77
|
+
label: 'Connect to Stripe',
|
|
78
|
+
url: 'https://dashboard.stripe.com/apikeys'
|
|
79
|
+
},
|
|
64
80
|
envVars: [
|
|
65
81
|
{
|
|
66
82
|
key: 'STRIPE_SECRET_KEY',
|
|
@@ -118,3 +134,21 @@ export function getModuleEnvHelp(moduleIds) {
|
|
|
118
134
|
}
|
|
119
135
|
return Array.from(map.values());
|
|
120
136
|
}
|
|
137
|
+
export function getModuleConnections(moduleIds) {
|
|
138
|
+
const connections = [];
|
|
139
|
+
for (const id of moduleIds) {
|
|
140
|
+
const module = getModuleDefinition(id);
|
|
141
|
+
if (module.connect) {
|
|
142
|
+
connections.push(module.connect);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const fallback = module.envVars.find((item) => item.url);
|
|
146
|
+
if (fallback?.url) {
|
|
147
|
+
connections.push({
|
|
148
|
+
label: `Connect to ${module.label}`,
|
|
149
|
+
url: fallback.url
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return connections;
|
|
154
|
+
}
|
package/dist/engine/scaffold.js
CHANGED
|
@@ -3,7 +3,7 @@ import { promises as fs } from 'fs';
|
|
|
3
3
|
import { execa } from 'execa';
|
|
4
4
|
import { getFrameworkDefinition } from '../config/frameworks.js';
|
|
5
5
|
import { getBaseEnvHelp } from '../config/base-env.js';
|
|
6
|
-
import { getModuleEnvHelp } from '../config/modules.js';
|
|
6
|
+
import { getModuleConnections, getModuleEnvHelp } from '../config/modules.js';
|
|
7
7
|
import { installBaseDependencies, installModulePackages } from './installers.js';
|
|
8
8
|
import { writeEnvExample } from './env.js';
|
|
9
9
|
import { assertAssetSources, generateExpoAssets, generateNextAssets, resolveAssetSources } from './assets.js';
|
|
@@ -130,10 +130,12 @@ async function applyNextTemplates(config, targetDir) {
|
|
|
130
130
|
const usesSrcDir = await pathExists(srcAppDir);
|
|
131
131
|
const basePath = usesSrcDir ? 'src' : '';
|
|
132
132
|
const envHelp = mergeEnvHelp(getBaseEnvHelp(config.framework), getModuleEnvHelp(config.modules));
|
|
133
|
+
const connections = getModuleConnections(config.modules);
|
|
133
134
|
const files = buildNextTemplateFiles({
|
|
134
135
|
appName: config.appName,
|
|
135
136
|
domain: config.domain,
|
|
136
137
|
envVars: envHelp,
|
|
138
|
+
connections,
|
|
137
139
|
basePath
|
|
138
140
|
});
|
|
139
141
|
await writeTemplateFiles(targetDir, files);
|
|
@@ -146,10 +148,12 @@ async function applyNextTemplates(config, targetDir) {
|
|
|
146
148
|
}
|
|
147
149
|
async function applyExpoTemplates(config, targetDir) {
|
|
148
150
|
const envHelp = mergeEnvHelp(getBaseEnvHelp(config.framework), getModuleEnvHelp(config.modules));
|
|
151
|
+
const connections = getModuleConnections(config.modules);
|
|
149
152
|
const files = buildExpoTemplateFiles({
|
|
150
153
|
appName: config.appName,
|
|
151
154
|
domain: config.domain,
|
|
152
155
|
envVars: envHelp,
|
|
156
|
+
connections,
|
|
153
157
|
basePath: ''
|
|
154
158
|
});
|
|
155
159
|
await writeTemplateFiles(targetDir, files);
|
package/dist/engine/templates.js
CHANGED
|
@@ -20,6 +20,7 @@ export function componentsJsonTemplate(globalsPath, tailwindConfigPath) {
|
|
|
20
20
|
export function buildNextTemplateFiles(data) {
|
|
21
21
|
const base = data.basePath ? `${data.basePath}/` : '';
|
|
22
22
|
const envList = renderNextEnvList(data.envVars);
|
|
23
|
+
const connectionSection = renderNextConnectionSection(data.connections);
|
|
23
24
|
return [
|
|
24
25
|
{
|
|
25
26
|
path: `${base}app/layout.tsx`,
|
|
@@ -27,7 +28,7 @@ export function buildNextTemplateFiles(data) {
|
|
|
27
28
|
},
|
|
28
29
|
{
|
|
29
30
|
path: `${base}app/page.tsx`,
|
|
30
|
-
content: nextHomeTemplate(data.appName, data.domain, envList)
|
|
31
|
+
content: nextHomeTemplate(data.appName, data.domain, envList, connectionSection)
|
|
31
32
|
},
|
|
32
33
|
{
|
|
33
34
|
path: `${base}app/about/page.tsx`,
|
|
@@ -37,6 +38,10 @@ export function buildNextTemplateFiles(data) {
|
|
|
37
38
|
path: `${base}app/guide/page.tsx`,
|
|
38
39
|
content: nextRouteTemplate('Guide', 'Three routes are ready. Customize and ship.')
|
|
39
40
|
},
|
|
41
|
+
{
|
|
42
|
+
path: `${base}app/contact/page.tsx`,
|
|
43
|
+
content: nextContactPageTemplate()
|
|
44
|
+
},
|
|
40
45
|
{
|
|
41
46
|
path: `${base}app/api/contact/route.ts`,
|
|
42
47
|
content: nextContactRouteTemplate(data.appName)
|
|
@@ -57,6 +62,10 @@ export function buildNextTemplateFiles(data) {
|
|
|
57
62
|
path: `${base}components/contact-form.tsx`,
|
|
58
63
|
content: nextContactFormTemplate()
|
|
59
64
|
},
|
|
65
|
+
{
|
|
66
|
+
path: `${base}components/connection-guide.tsx`,
|
|
67
|
+
content: nextConnectionGuideTemplate(data.connections)
|
|
68
|
+
},
|
|
60
69
|
{
|
|
61
70
|
path: `${base}components/env-list.tsx`,
|
|
62
71
|
content: nextEnvListTemplate(envList)
|
|
@@ -69,6 +78,7 @@ export function buildNextTemplateFiles(data) {
|
|
|
69
78
|
}
|
|
70
79
|
export function buildExpoTemplateFiles(data) {
|
|
71
80
|
const envItems = renderExpoEnvItems(data.envVars);
|
|
81
|
+
const connectionItems = renderConnectionItems(data.connections);
|
|
72
82
|
return [
|
|
73
83
|
{
|
|
74
84
|
path: 'app/_layout.tsx',
|
|
@@ -78,6 +88,10 @@ export function buildExpoTemplateFiles(data) {
|
|
|
78
88
|
path: 'app/index.tsx',
|
|
79
89
|
content: expoHomeTemplate(data.appName, data.domain, envItems)
|
|
80
90
|
},
|
|
91
|
+
{
|
|
92
|
+
path: 'app/contact.tsx',
|
|
93
|
+
content: expoContactTemplate()
|
|
94
|
+
},
|
|
81
95
|
{
|
|
82
96
|
path: 'app/about.tsx',
|
|
83
97
|
content: expoRouteTemplate('About', 'A concise overview of your project.')
|
|
@@ -102,6 +116,10 @@ export function buildExpoTemplateFiles(data) {
|
|
|
102
116
|
path: 'components/env-list.tsx',
|
|
103
117
|
content: expoEnvListTemplate(envItems)
|
|
104
118
|
},
|
|
119
|
+
{
|
|
120
|
+
path: 'components/connection-guide.tsx',
|
|
121
|
+
content: expoConnectionGuideTemplate(connectionItems)
|
|
122
|
+
},
|
|
105
123
|
{
|
|
106
124
|
path: 'components/contact-form.tsx',
|
|
107
125
|
content: expoContactFormTemplate()
|
|
@@ -171,9 +189,10 @@ export default function RootLayout({
|
|
|
171
189
|
}
|
|
172
190
|
`;
|
|
173
191
|
}
|
|
174
|
-
function nextHomeTemplate(appName, domain, envList) {
|
|
192
|
+
function nextHomeTemplate(appName, domain, envList, connectionSection) {
|
|
175
193
|
return `import { EnvList } from '@/components/env-list';
|
|
176
194
|
import { ContactForm } from '@/components/contact-form';
|
|
195
|
+
import { ConnectionGuide } from '@/components/connection-guide';
|
|
177
196
|
|
|
178
197
|
export const metadata = {
|
|
179
198
|
title: 'Home',
|
|
@@ -197,11 +216,13 @@ export default function Home() {
|
|
|
197
216
|
</p>
|
|
198
217
|
<EnvList />
|
|
199
218
|
</div>
|
|
219
|
+
${connectionSection}
|
|
200
220
|
<div className="flex flex-col gap-2">
|
|
201
221
|
<h2 className="text-base font-bold">Routes</h2>
|
|
202
222
|
<p className="text-base">
|
|
203
|
-
Explore <code className="bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/about</code
|
|
204
|
-
<code className="bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/guide</code
|
|
223
|
+
Explore <code className="bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/about</code>,{' '}
|
|
224
|
+
<code className="bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/guide</code>, and{' '}
|
|
225
|
+
<code className="bg-[var(--fg)] px-2 py-1 text-[var(--bg)]">/contact</code>.
|
|
205
226
|
</p>
|
|
206
227
|
</div>
|
|
207
228
|
<ContactForm />
|
|
@@ -232,7 +253,8 @@ function nextHeaderTemplate(appName) {
|
|
|
232
253
|
const links = [
|
|
233
254
|
{ href: '/', label: 'Home' },
|
|
234
255
|
{ href: '/about', label: 'About' },
|
|
235
|
-
{ href: '/guide', label: 'Guide' }
|
|
256
|
+
{ href: '/guide', label: 'Guide' },
|
|
257
|
+
{ href: '/contact', label: 'Contact' }
|
|
236
258
|
];
|
|
237
259
|
|
|
238
260
|
export function SiteHeader({ appName }: { appName: string }) {
|
|
@@ -437,12 +459,12 @@ function isNonEmpty(value: unknown): value is string {
|
|
|
437
459
|
|
|
438
460
|
export async function POST(request: Request) {
|
|
439
461
|
const apiKey = process.env.RESEND_API_KEY?.trim();
|
|
440
|
-
const from = process.env.
|
|
441
|
-
const to = process.env.
|
|
462
|
+
const from = process.env.CONTACT_FROM_EMAIL?.trim();
|
|
463
|
+
const to = process.env.CONTACT_TO_EMAIL?.trim();
|
|
442
464
|
|
|
443
465
|
if (!apiKey || !from || !to) {
|
|
444
466
|
return Response.json(
|
|
445
|
-
{ error: 'Set RESEND_API_KEY,
|
|
467
|
+
{ error: 'Set RESEND_API_KEY, CONTACT_FROM_EMAIL, and CONTACT_TO_EMAIL.' },
|
|
446
468
|
{ status: 500 }
|
|
447
469
|
);
|
|
448
470
|
}
|
|
@@ -472,6 +494,25 @@ export async function POST(request: Request) {
|
|
|
472
494
|
}
|
|
473
495
|
`;
|
|
474
496
|
}
|
|
497
|
+
function nextContactPageTemplate() {
|
|
498
|
+
return `import { ContactForm } from '@/components/contact-form';
|
|
499
|
+
|
|
500
|
+
export const metadata = {
|
|
501
|
+
title: 'Contact',
|
|
502
|
+
description: 'Send a message to your inbox.'
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
export default function ContactPage() {
|
|
506
|
+
return (
|
|
507
|
+
<section className="mx-auto flex w-full max-w-3xl flex-col gap-6 px-6 py-12">
|
|
508
|
+
<h1 className="text-base font-bold">Contact</h1>
|
|
509
|
+
<p className="text-base">Send a message to your inbox.</p>
|
|
510
|
+
<ContactForm />
|
|
511
|
+
</section>
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
`;
|
|
515
|
+
}
|
|
475
516
|
function nextGlobalsCss() {
|
|
476
517
|
return `@import url('https://fonts.googleapis.com/css2?family=Geist+Mono:wght@400;700&display=swap');
|
|
477
518
|
|
|
@@ -547,6 +588,47 @@ function renderNextEnvList(envVars) {
|
|
|
547
588
|
})
|
|
548
589
|
.join('\n');
|
|
549
590
|
}
|
|
591
|
+
function renderNextConnectionSection(_connections) {
|
|
592
|
+
return '<ConnectionGuide />';
|
|
593
|
+
}
|
|
594
|
+
function nextConnectionGuideTemplate(connections) {
|
|
595
|
+
const items = renderConnectionItems(connections);
|
|
596
|
+
return `import Link from 'next/link';
|
|
597
|
+
|
|
598
|
+
const connections = ${items};
|
|
599
|
+
|
|
600
|
+
export function ConnectionGuide() {
|
|
601
|
+
if (connections.length === 0) {
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return (
|
|
606
|
+
<div className="flex flex-col gap-3">
|
|
607
|
+
<h2 className="text-base font-bold">Connection guide</h2>
|
|
608
|
+
{connections.map((item) => (
|
|
609
|
+
<div key={item.label} className="flex flex-col gap-1">
|
|
610
|
+
<Link
|
|
611
|
+
href={item.url}
|
|
612
|
+
target="_blank"
|
|
613
|
+
rel="noreferrer"
|
|
614
|
+
className="text-base font-bold underline underline-offset-4"
|
|
615
|
+
>
|
|
616
|
+
{item.label}
|
|
617
|
+
</Link>
|
|
618
|
+
<p className="text-base">
|
|
619
|
+
or go to{' '}
|
|
620
|
+
<Link href={item.url} target="_blank" rel="noreferrer" className="underline underline-offset-4">
|
|
621
|
+
{item.url}
|
|
622
|
+
</Link>{' '}
|
|
623
|
+
directly.
|
|
624
|
+
</p>
|
|
625
|
+
</div>
|
|
626
|
+
))}
|
|
627
|
+
</div>
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
`;
|
|
631
|
+
}
|
|
550
632
|
function expoLayoutTemplate() {
|
|
551
633
|
return `import { Stack } from 'expo-router';
|
|
552
634
|
import { TamaguiProvider, Theme } from 'tamagui';
|
|
@@ -577,6 +659,7 @@ function expoHomeTemplate(appName, domain, envItems) {
|
|
|
577
659
|
import { Text, YStack } from 'tamagui';
|
|
578
660
|
import { PageShell } from '../components/page-shell';
|
|
579
661
|
import { EnvList } from '../components/env-list';
|
|
662
|
+
import { ConnectionGuide } from '../components/connection-guide';
|
|
580
663
|
import { ContactForm } from '../components/contact-form';
|
|
581
664
|
import { FONT_BOLD, FONT_REGULAR, FONT_SIZE, useThemeColors } from '../components/theme';
|
|
582
665
|
|
|
@@ -602,12 +685,13 @@ export default function Home() {
|
|
|
602
685
|
</Text>
|
|
603
686
|
<EnvList />
|
|
604
687
|
</YStack>
|
|
688
|
+
<ConnectionGuide />
|
|
605
689
|
<YStack gap="$2">
|
|
606
690
|
<Text fontFamily={FONT_BOLD} fontSize={FONT_SIZE} color={fg}>
|
|
607
691
|
Routes
|
|
608
692
|
</Text>
|
|
609
693
|
<Text fontFamily={FONT_REGULAR} fontSize={FONT_SIZE} color={fg}>
|
|
610
|
-
Visit /about and /
|
|
694
|
+
Visit /about, /guide, and /contact.
|
|
611
695
|
</Text>
|
|
612
696
|
</YStack>
|
|
613
697
|
<ContactForm />
|
|
@@ -616,6 +700,27 @@ export default function Home() {
|
|
|
616
700
|
}
|
|
617
701
|
`;
|
|
618
702
|
}
|
|
703
|
+
function expoContactTemplate() {
|
|
704
|
+
return `import { Head } from 'expo-router/head';
|
|
705
|
+
import { PageShell } from '../components/page-shell';
|
|
706
|
+
import { ContactForm } from '../components/contact-form';
|
|
707
|
+
|
|
708
|
+
export default function Contact() {
|
|
709
|
+
return (
|
|
710
|
+
<PageShell title="Contact" subtitle="Send a message to your inbox.">
|
|
711
|
+
<Head>
|
|
712
|
+
<title>Contact</title>
|
|
713
|
+
<meta name="description" content="Send a message to your inbox." />
|
|
714
|
+
<meta property="og:title" content="Contact" />
|
|
715
|
+
<meta property="og:description" content="Send a message to your inbox." />
|
|
716
|
+
<meta property="twitter:card" content="summary_large_image" />
|
|
717
|
+
</Head>
|
|
718
|
+
<ContactForm />
|
|
719
|
+
</PageShell>
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
`;
|
|
723
|
+
}
|
|
619
724
|
function expoRouteTemplate(title, body) {
|
|
620
725
|
return `import { Head } from 'expo-router/head';
|
|
621
726
|
import { PageShell } from '../components/page-shell';
|
|
@@ -674,7 +779,8 @@ import { FONT_BOLD, FONT_REGULAR, FONT_SIZE, useThemeColors } from './theme';
|
|
|
674
779
|
const links = [
|
|
675
780
|
{ href: '/', label: 'Home' },
|
|
676
781
|
{ href: '/about', label: 'About' },
|
|
677
|
-
{ href: '/guide', label: 'Guide' }
|
|
782
|
+
{ href: '/guide', label: 'Guide' },
|
|
783
|
+
{ href: '/contact', label: 'Contact' }
|
|
678
784
|
];
|
|
679
785
|
|
|
680
786
|
export function SiteHeader() {
|
|
@@ -793,13 +899,62 @@ export function EnvList() {
|
|
|
793
899
|
}
|
|
794
900
|
`;
|
|
795
901
|
}
|
|
902
|
+
function expoConnectionGuideTemplate(connectionItems) {
|
|
903
|
+
return `import { Linking } from 'react-native';
|
|
904
|
+
import { Text, YStack } from 'tamagui';
|
|
905
|
+
import { FONT_BOLD, FONT_REGULAR, FONT_SIZE, useThemeColors } from './theme';
|
|
906
|
+
|
|
907
|
+
const connections = ${connectionItems};
|
|
908
|
+
|
|
909
|
+
export function ConnectionGuide() {
|
|
910
|
+
const { fg } = useThemeColors();
|
|
911
|
+
|
|
912
|
+
if (connections.length === 0) {
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return (
|
|
917
|
+
<YStack gap="$3">
|
|
918
|
+
<Text fontFamily={FONT_BOLD} fontSize={FONT_SIZE} color={fg}>
|
|
919
|
+
Connection guide
|
|
920
|
+
</Text>
|
|
921
|
+
{connections.map((item) => (
|
|
922
|
+
<YStack key={item.label} gap="$1">
|
|
923
|
+
<Text
|
|
924
|
+
fontFamily={FONT_BOLD}
|
|
925
|
+
fontSize={FONT_SIZE}
|
|
926
|
+
color={fg}
|
|
927
|
+
textDecorationLine="underline"
|
|
928
|
+
onPress={() => Linking.openURL(item.url)}
|
|
929
|
+
>
|
|
930
|
+
{item.label}
|
|
931
|
+
</Text>
|
|
932
|
+
<Text fontFamily={FONT_REGULAR} fontSize={FONT_SIZE} color={fg}>
|
|
933
|
+
or go to{' '}
|
|
934
|
+
<Text
|
|
935
|
+
fontFamily={FONT_REGULAR}
|
|
936
|
+
fontSize={FONT_SIZE}
|
|
937
|
+
color={fg}
|
|
938
|
+
textDecorationLine="underline"
|
|
939
|
+
onPress={() => Linking.openURL(item.url)}
|
|
940
|
+
>
|
|
941
|
+
{item.url}
|
|
942
|
+
</Text>{' '}
|
|
943
|
+
directly.
|
|
944
|
+
</Text>
|
|
945
|
+
</YStack>
|
|
946
|
+
))}
|
|
947
|
+
</YStack>
|
|
948
|
+
);
|
|
949
|
+
}
|
|
950
|
+
`;
|
|
951
|
+
}
|
|
796
952
|
function expoContactFormTemplate() {
|
|
797
953
|
return `import { useState } from 'react';
|
|
798
|
-
import {
|
|
954
|
+
import { TextInput } from 'react-native';
|
|
799
955
|
import { Button, Text, YStack } from 'tamagui';
|
|
800
956
|
import { FONT_BOLD, FONT_REGULAR, FONT_SIZE, useThemeColors } from './theme';
|
|
801
957
|
|
|
802
|
-
const CONTACT_EMAIL = process.env.EXPO_PUBLIC_CONTACT_EMAIL?.trim() ?? '';
|
|
803
958
|
const CONTACT_ENDPOINT = process.env.EXPO_PUBLIC_CONTACT_ENDPOINT?.trim() ?? '';
|
|
804
959
|
|
|
805
960
|
type Status = 'idle' | 'sending' | 'sent' | 'error';
|
|
@@ -828,46 +983,34 @@ export function ContactForm() {
|
|
|
828
983
|
return;
|
|
829
984
|
}
|
|
830
985
|
|
|
986
|
+
if (!CONTACT_ENDPOINT) {
|
|
987
|
+
setStatus('error');
|
|
988
|
+
setError('Set EXPO_PUBLIC_CONTACT_ENDPOINT.');
|
|
989
|
+
return;
|
|
990
|
+
}
|
|
991
|
+
|
|
831
992
|
setStatus('sending');
|
|
832
993
|
setError('');
|
|
833
994
|
|
|
834
995
|
let sent = false;
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
sent = false;
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
|
|
852
|
-
if (!sent && CONTACT_EMAIL) {
|
|
853
|
-
const subject = encodeURIComponent(\`New message from \${name.trim()}\`);
|
|
854
|
-
const body = encodeURIComponent(\`Name: \${name.trim()}\\nEmail: \${email.trim()}\\n\\n\${message.trim()}\`);
|
|
855
|
-
const url = \`mailto:\${CONTACT_EMAIL}?subject=\${subject}&body=\${body}\`;
|
|
856
|
-
try {
|
|
857
|
-
await Linking.openURL(url);
|
|
858
|
-
sent = true;
|
|
859
|
-
} catch {
|
|
860
|
-
sent = false;
|
|
861
|
-
}
|
|
996
|
+
try {
|
|
997
|
+
const response = await fetch(CONTACT_ENDPOINT, {
|
|
998
|
+
method: 'POST',
|
|
999
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1000
|
+
body: JSON.stringify({
|
|
1001
|
+
name: name.trim(),
|
|
1002
|
+
email: email.trim(),
|
|
1003
|
+
message: message.trim()
|
|
1004
|
+
})
|
|
1005
|
+
});
|
|
1006
|
+
sent = response.ok;
|
|
1007
|
+
} catch {
|
|
1008
|
+
sent = false;
|
|
862
1009
|
}
|
|
863
1010
|
|
|
864
1011
|
if (!sent) {
|
|
865
1012
|
setStatus('error');
|
|
866
|
-
|
|
867
|
-
setError('Unable to send message.');
|
|
868
|
-
} else {
|
|
869
|
-
setError('Set EXPO_PUBLIC_CONTACT_ENDPOINT or EXPO_PUBLIC_CONTACT_EMAIL.');
|
|
870
|
-
}
|
|
1013
|
+
setError('Unable to send message.');
|
|
871
1014
|
return;
|
|
872
1015
|
}
|
|
873
1016
|
|
|
@@ -1047,6 +1190,20 @@ function renderExpoEnvItems(envVars) {
|
|
|
1047
1190
|
${items.join(',\n ')}
|
|
1048
1191
|
]`;
|
|
1049
1192
|
}
|
|
1193
|
+
function renderConnectionItems(connections) {
|
|
1194
|
+
if (connections.length === 0) {
|
|
1195
|
+
return '[]';
|
|
1196
|
+
}
|
|
1197
|
+
const items = connections.map((item) => {
|
|
1198
|
+
return `{
|
|
1199
|
+
label: '${escapeTemplate(item.label)}',
|
|
1200
|
+
url: '${escapeTemplate(item.url)}'
|
|
1201
|
+
}`;
|
|
1202
|
+
});
|
|
1203
|
+
return `[
|
|
1204
|
+
${items.join(',\n ')}
|
|
1205
|
+
]`;
|
|
1206
|
+
}
|
|
1050
1207
|
function escapeTemplate(value) {
|
|
1051
1208
|
return value
|
|
1052
1209
|
.replace(/`/g, '\\`')
|