@underpeaks/generator-nextjs 1.0.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/generator.d.ts +3 -0
- package/dist/generator.d.ts.map +1 -0
- package/dist/generator.js +113 -0
- package/dist/generator.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -0
- package/dist/integrations/algolia.d.ts +3 -0
- package/dist/integrations/algolia.d.ts.map +1 -0
- package/dist/integrations/algolia.js +56 -0
- package/dist/integrations/algolia.js.map +1 -0
- package/dist/integrations/anthropic.d.ts +3 -0
- package/dist/integrations/anthropic.d.ts.map +1 -0
- package/dist/integrations/anthropic.js +54 -0
- package/dist/integrations/anthropic.js.map +1 -0
- package/dist/integrations/aws-s3.d.ts +3 -0
- package/dist/integrations/aws-s3.d.ts.map +1 -0
- package/dist/integrations/aws-s3.js +59 -0
- package/dist/integrations/aws-s3.js.map +1 -0
- package/dist/integrations/clerk.d.ts +3 -0
- package/dist/integrations/clerk.d.ts.map +1 -0
- package/dist/integrations/clerk.js +34 -0
- package/dist/integrations/clerk.js.map +1 -0
- package/dist/integrations/cloudinary.d.ts +3 -0
- package/dist/integrations/cloudinary.d.ts.map +1 -0
- package/dist/integrations/cloudinary.js +57 -0
- package/dist/integrations/cloudinary.js.map +1 -0
- package/dist/integrations/crisp.d.ts +3 -0
- package/dist/integrations/crisp.d.ts.map +1 -0
- package/dist/integrations/crisp.js +53 -0
- package/dist/integrations/crisp.js.map +1 -0
- package/dist/integrations/firebase.d.ts +3 -0
- package/dist/integrations/firebase.d.ts.map +1 -0
- package/dist/integrations/firebase.js +53 -0
- package/dist/integrations/firebase.js.map +1 -0
- package/dist/integrations/flutterwave.d.ts +3 -0
- package/dist/integrations/flutterwave.d.ts.map +1 -0
- package/dist/integrations/flutterwave.js +53 -0
- package/dist/integrations/flutterwave.js.map +1 -0
- package/dist/integrations/gemini.d.ts +3 -0
- package/dist/integrations/gemini.d.ts.map +1 -0
- package/dist/integrations/gemini.js +40 -0
- package/dist/integrations/gemini.js.map +1 -0
- package/dist/integrations/github.d.ts +3 -0
- package/dist/integrations/github.d.ts.map +1 -0
- package/dist/integrations/github.js +53 -0
- package/dist/integrations/github.js.map +1 -0
- package/dist/integrations/hubspot.d.ts +3 -0
- package/dist/integrations/hubspot.d.ts.map +1 -0
- package/dist/integrations/hubspot.js +54 -0
- package/dist/integrations/hubspot.js.map +1 -0
- package/dist/integrations/index.d.ts +3 -0
- package/dist/integrations/index.d.ts.map +1 -0
- package/dist/integrations/index.js +63 -0
- package/dist/integrations/index.js.map +1 -0
- package/dist/integrations/lemonsqueezy.d.ts +3 -0
- package/dist/integrations/lemonsqueezy.d.ts.map +1 -0
- package/dist/integrations/lemonsqueezy.js +43 -0
- package/dist/integrations/lemonsqueezy.js.map +1 -0
- package/dist/integrations/mailchimp.d.ts +3 -0
- package/dist/integrations/mailchimp.d.ts.map +1 -0
- package/dist/integrations/mailchimp.js +52 -0
- package/dist/integrations/mailchimp.js.map +1 -0
- package/dist/integrations/openai.d.ts +3 -0
- package/dist/integrations/openai.d.ts.map +1 -0
- package/dist/integrations/openai.js +78 -0
- package/dist/integrations/openai.js.map +1 -0
- package/dist/integrations/payfast.d.ts +3 -0
- package/dist/integrations/payfast.d.ts.map +1 -0
- package/dist/integrations/payfast.js +94 -0
- package/dist/integrations/payfast.js.map +1 -0
- package/dist/integrations/pusher.d.ts +3 -0
- package/dist/integrations/pusher.d.ts.map +1 -0
- package/dist/integrations/pusher.js +86 -0
- package/dist/integrations/pusher.js.map +1 -0
- package/dist/integrations/resend.d.ts +3 -0
- package/dist/integrations/resend.d.ts.map +1 -0
- package/dist/integrations/resend.js +45 -0
- package/dist/integrations/resend.js.map +1 -0
- package/dist/integrations/sendgrid.d.ts +3 -0
- package/dist/integrations/sendgrid.d.ts.map +1 -0
- package/dist/integrations/sendgrid.js +42 -0
- package/dist/integrations/sendgrid.js.map +1 -0
- package/dist/integrations/sentry.d.ts +3 -0
- package/dist/integrations/sentry.d.ts.map +1 -0
- package/dist/integrations/sentry.js +49 -0
- package/dist/integrations/sentry.js.map +1 -0
- package/dist/integrations/slack.d.ts +3 -0
- package/dist/integrations/slack.d.ts.map +1 -0
- package/dist/integrations/slack.js +54 -0
- package/dist/integrations/slack.js.map +1 -0
- package/dist/integrations/storage.d.ts +3 -0
- package/dist/integrations/storage.d.ts.map +1 -0
- package/dist/integrations/storage.js +71 -0
- package/dist/integrations/storage.js.map +1 -0
- package/dist/integrations/stripe.d.ts +3 -0
- package/dist/integrations/stripe.d.ts.map +1 -0
- package/dist/integrations/stripe.js +65 -0
- package/dist/integrations/stripe.js.map +1 -0
- package/dist/integrations/twilio.d.ts +3 -0
- package/dist/integrations/twilio.d.ts.map +1 -0
- package/dist/integrations/twilio.js +45 -0
- package/dist/integrations/twilio.js.map +1 -0
- package/dist/integrations/uploadthing.d.ts +3 -0
- package/dist/integrations/uploadthing.d.ts.map +1 -0
- package/dist/integrations/uploadthing.js +41 -0
- package/dist/integrations/uploadthing.js.map +1 -0
- package/dist/templates/api-builder.d.ts +12 -0
- package/dist/templates/api-builder.d.ts.map +1 -0
- package/dist/templates/api-builder.js +76 -0
- package/dist/templates/api-builder.js.map +1 -0
- package/dist/templates/api-route-id.d.ts +3 -0
- package/dist/templates/api-route-id.d.ts.map +1 -0
- package/dist/templates/api-route-id.js +137 -0
- package/dist/templates/api-route-id.js.map +1 -0
- package/dist/templates/api-route.d.ts +3 -0
- package/dist/templates/api-route.d.ts.map +1 -0
- package/dist/templates/api-route.js +85 -0
- package/dist/templates/api-route.js.map +1 -0
- package/dist/templates/app-nav.d.ts +23 -0
- package/dist/templates/app-nav.d.ts.map +1 -0
- package/dist/templates/app-nav.js +132 -0
- package/dist/templates/app-nav.js.map +1 -0
- package/dist/templates/env.d.ts +3 -0
- package/dist/templates/env.d.ts.map +1 -0
- package/dist/templates/env.js +39 -0
- package/dist/templates/env.js.map +1 -0
- package/dist/templates/layout.d.ts +3 -0
- package/dist/templates/layout.d.ts.map +1 -0
- package/dist/templates/layout.js +50 -0
- package/dist/templates/layout.js.map +1 -0
- package/dist/templates/navigation.d.ts +3 -0
- package/dist/templates/navigation.d.ts.map +1 -0
- package/dist/templates/navigation.js +129 -0
- package/dist/templates/navigation.js.map +1 -0
- package/dist/templates/nxf-client.d.ts +3 -0
- package/dist/templates/nxf-client.d.ts.map +1 -0
- package/dist/templates/nxf-client.js +96 -0
- package/dist/templates/nxf-client.js.map +1 -0
- package/dist/templates/page-auth.d.ts +3 -0
- package/dist/templates/page-auth.d.ts.map +1 -0
- package/dist/templates/page-auth.js +344 -0
- package/dist/templates/page-auth.js.map +1 -0
- package/dist/templates/page-detail.d.ts +3 -0
- package/dist/templates/page-detail.d.ts.map +1 -0
- package/dist/templates/page-detail.js +86 -0
- package/dist/templates/page-detail.js.map +1 -0
- package/dist/templates/page-form.d.ts +3 -0
- package/dist/templates/page-form.d.ts.map +1 -0
- package/dist/templates/page-form.js +159 -0
- package/dist/templates/page-form.js.map +1 -0
- package/dist/templates/page-landing.d.ts +3 -0
- package/dist/templates/page-landing.d.ts.map +1 -0
- package/dist/templates/page-landing.js +140 -0
- package/dist/templates/page-landing.js.map +1 -0
- package/dist/templates/page-list.d.ts +3 -0
- package/dist/templates/page-list.d.ts.map +1 -0
- package/dist/templates/page-list.js +130 -0
- package/dist/templates/page-list.js.map +1 -0
- package/dist/templates/page-remaining.d.ts +3 -0
- package/dist/templates/page-remaining.d.ts.map +1 -0
- package/dist/templates/page-remaining.js +870 -0
- package/dist/templates/page-remaining.js.map +1 -0
- package/dist/templates/readme.d.ts +3 -0
- package/dist/templates/readme.d.ts.map +1 -0
- package/dist/templates/readme.js +87 -0
- package/dist/templates/readme.js.map +1 -0
- package/dist/templates/tailwind.d.ts +3 -0
- package/dist/templates/tailwind.d.ts.map +1 -0
- package/dist/templates/tailwind.js +63 -0
- package/dist/templates/tailwind.js.map +1 -0
- package/dist/templates/theme.d.ts +3 -0
- package/dist/templates/theme.d.ts.map +1 -0
- package/dist/templates/theme.js +49 -0
- package/dist/templates/theme.js.map +1 -0
- package/dist/templates/types.d.ts +3 -0
- package/dist/templates/types.d.ts.map +1 -0
- package/dist/templates/types.js +74 -0
- package/dist/templates/types.js.map +1 -0
- package/dist/templates/validators.d.ts +3 -0
- package/dist/templates/validators.d.ts.map +1 -0
- package/dist/templates/validators.js +37 -0
- package/dist/templates/validators.js.map +1 -0
- package/package.json +24 -0
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// File: underpeaks_codegen/packages/generator-nextjs/src/templates/page-remaining.ts
|
|
3
|
+
// Covers: dashboard, profile, blog-post, contact, cart, checkout, wallet,
|
|
4
|
+
// invoice, chat, messages, feed, map, search, settings, gallery,
|
|
5
|
+
// kanban, calendar, otp_request, otp_entry, custom
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.generateRemainingNextjsPage = generateRemainingNextjsPage;
|
|
8
|
+
const generator_core_1 = require("@underpeaks/generator-core");
|
|
9
|
+
function generateRemainingNextjsPage(page, model) {
|
|
10
|
+
const routePath = page.slug.replace(/^\//, '');
|
|
11
|
+
const className = model ? (0, generator_core_1.modelToClassName)(model.name) : 'Item';
|
|
12
|
+
const fileName = model ? (0, generator_core_1.modelToFileName)(model.name) : 'item';
|
|
13
|
+
const pk = model ? (0, generator_core_1.getPrimaryKey)(model) : null;
|
|
14
|
+
const pkName = pk?.name ?? 'id';
|
|
15
|
+
const columns = model ? (0, generator_core_1.getVisibleColumns)(model) : [];
|
|
16
|
+
const lines = [];
|
|
17
|
+
lines.push(`// GENERATED BY UNDERPEAKS CODEGEN v1.0.0`);
|
|
18
|
+
lines.push(`// Page: ${page.name} — ${page.template_type}`);
|
|
19
|
+
lines.push(`// Presentation layer — generated once, you own this file.`);
|
|
20
|
+
lines.push(`// Regenerate: nxf generate nextjs --include-views`);
|
|
21
|
+
lines.push(``);
|
|
22
|
+
switch (page.template_type) {
|
|
23
|
+
case 'dashboard':
|
|
24
|
+
lines.push(`import type { ${className} } from '../../../generated/types/${fileName}.types'`);
|
|
25
|
+
lines.push(``);
|
|
26
|
+
lines.push(`async function getItems(): Promise<${className}[]> {`);
|
|
27
|
+
lines.push(` const res = await fetch(\`\${process.env.NEXT_PUBLIC_APP_URL}/api/${fileName}\`, { cache: 'no-store' })`);
|
|
28
|
+
lines.push(` if (!res.ok) return []`);
|
|
29
|
+
lines.push(` const json = await res.json()`);
|
|
30
|
+
lines.push(` return json.data ?? []`);
|
|
31
|
+
lines.push(`}`);
|
|
32
|
+
lines.push(``);
|
|
33
|
+
lines.push(`export default async function DashboardPage() {`);
|
|
34
|
+
lines.push(` const items = await getItems()`);
|
|
35
|
+
lines.push(` return (`);
|
|
36
|
+
lines.push(` <div className="container mx-auto px-4 py-8">`);
|
|
37
|
+
lines.push(` <h1 className="text-2xl font-bold mb-6">Dashboard</h1>`);
|
|
38
|
+
lines.push(` <div className="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">`);
|
|
39
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl p-6">`);
|
|
40
|
+
lines.push(` <p className="text-sm text-gray-500">Total ${className}s</p>`);
|
|
41
|
+
lines.push(` <p className="text-3xl font-bold mt-1">{items.length}</p>`);
|
|
42
|
+
lines.push(` </div>`);
|
|
43
|
+
lines.push(` </div>`);
|
|
44
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl overflow-hidden">`);
|
|
45
|
+
lines.push(` <div className="px-6 py-4 border-b border-gray-100">`);
|
|
46
|
+
lines.push(` <h2 className="font-semibold">Recent ${className}s</h2>`);
|
|
47
|
+
lines.push(` </div>`);
|
|
48
|
+
lines.push(` <div className="divide-y divide-gray-100">`);
|
|
49
|
+
lines.push(` {items.slice(0, 5).map((item) => (`);
|
|
50
|
+
lines.push(` <div key={item.${pkName}} className="px-6 py-4">`);
|
|
51
|
+
if (columns[0])
|
|
52
|
+
lines.push(` <p className="font-medium">{item.${columns[0].name}?.toString() ?? '—'}</p>`);
|
|
53
|
+
if (columns[1])
|
|
54
|
+
lines.push(` <p className="text-sm text-gray-500">{item.${columns[1].name}?.toString() ?? ''}</p>`);
|
|
55
|
+
lines.push(` </div>`);
|
|
56
|
+
lines.push(` ))}`);
|
|
57
|
+
lines.push(` </div>`);
|
|
58
|
+
lines.push(` </div>`);
|
|
59
|
+
lines.push(` </div>`);
|
|
60
|
+
lines.push(` )`);
|
|
61
|
+
lines.push(`}`);
|
|
62
|
+
break;
|
|
63
|
+
case 'profile':
|
|
64
|
+
lines.push(`import type { ${className} } from '../../../../generated/types/${fileName}.types'`);
|
|
65
|
+
lines.push(`import { notFound } from 'next/navigation'`);
|
|
66
|
+
lines.push(`import Link from 'next/link'`);
|
|
67
|
+
lines.push(``);
|
|
68
|
+
lines.push(`async function getItem(id: string): Promise<${className} | null> {`);
|
|
69
|
+
lines.push(` const res = await fetch(\`\${process.env.NEXT_PUBLIC_APP_URL}/api/${fileName}/\${id}\`, { cache: 'no-store' })`);
|
|
70
|
+
lines.push(` if (res.status === 404) return null`);
|
|
71
|
+
lines.push(` const json = await res.json()`);
|
|
72
|
+
lines.push(` return json.data`);
|
|
73
|
+
lines.push(`}`);
|
|
74
|
+
lines.push(``);
|
|
75
|
+
lines.push(`interface Props { params: Promise<{ ${pkName}: string }> }`);
|
|
76
|
+
lines.push(``);
|
|
77
|
+
lines.push(`export default async function ProfilePage({ params }: Props) {`);
|
|
78
|
+
lines.push(` const { ${pkName} } = await params`);
|
|
79
|
+
lines.push(` const item = await getItem(${pkName})`);
|
|
80
|
+
lines.push(` if (!item) notFound()`);
|
|
81
|
+
lines.push(` return (`);
|
|
82
|
+
lines.push(` <div className="container mx-auto px-4 py-8 max-w-2xl">`);
|
|
83
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl overflow-hidden">`);
|
|
84
|
+
lines.push(` <div className="h-32 bg-gray-100" />`);
|
|
85
|
+
lines.push(` <div className="px-6 pb-6">`);
|
|
86
|
+
lines.push(` <div className="-mt-12 mb-4">`);
|
|
87
|
+
lines.push(` <div className="w-24 h-24 rounded-full bg-gray-200 border-4 border-white flex items-center justify-center text-3xl">👤</div>`);
|
|
88
|
+
lines.push(` </div>`);
|
|
89
|
+
for (const col of columns) {
|
|
90
|
+
const label = col.name.replaceAll('_', ' ');
|
|
91
|
+
lines.push(` <div className="flex items-center justify-between py-3 border-b border-gray-100">`);
|
|
92
|
+
lines.push(` <span className="text-sm text-gray-500 capitalize">${label}</span>`);
|
|
93
|
+
lines.push(` <span className="text-sm font-medium">{item.${col.name}?.toString() ?? '—'}</span>`);
|
|
94
|
+
lines.push(` </div>`);
|
|
95
|
+
}
|
|
96
|
+
lines.push(` <div className="mt-4">`);
|
|
97
|
+
lines.push(` <Link href={\`/${routePath}/\${item.${pkName}}/edit\`}`);
|
|
98
|
+
lines.push(` className="px-4 py-2 border border-gray-200 rounded-lg text-sm font-medium hover:bg-gray-50 transition">`);
|
|
99
|
+
lines.push(` Edit Profile`);
|
|
100
|
+
lines.push(` </Link>`);
|
|
101
|
+
lines.push(` </div>`);
|
|
102
|
+
lines.push(` </div>`);
|
|
103
|
+
lines.push(` </div>`);
|
|
104
|
+
lines.push(` </div>`);
|
|
105
|
+
lines.push(` )`);
|
|
106
|
+
lines.push(`}`);
|
|
107
|
+
break;
|
|
108
|
+
case 'blog-post': {
|
|
109
|
+
const titleField = columns.find((c) => c.name.includes('title'))?.name ?? columns[0]?.name ?? pkName;
|
|
110
|
+
const bodyField = columns.find((c) => c.name.includes('body') || c.name.includes('content'))?.name ?? null;
|
|
111
|
+
const imageField = columns.find((c) => c.name.includes('image') || c.name.includes('cover'))?.name ?? null;
|
|
112
|
+
const authorField = columns.find((c) => c.name.includes('author'))?.name ?? null;
|
|
113
|
+
lines.push(`import type { ${className} } from '../../../../generated/types/${fileName}.types'`);
|
|
114
|
+
lines.push(`import { notFound } from 'next/navigation'`);
|
|
115
|
+
lines.push(`import Image from 'next/image'`);
|
|
116
|
+
lines.push(``);
|
|
117
|
+
lines.push(`async function getItem(id: string): Promise<${className} | null> {`);
|
|
118
|
+
lines.push(` const res = await fetch(\`\${process.env.NEXT_PUBLIC_APP_URL}/api/${fileName}/\${id}\`, { cache: 'no-store' })`);
|
|
119
|
+
lines.push(` if (res.status === 404) return null`);
|
|
120
|
+
lines.push(` const json = await res.json()`);
|
|
121
|
+
lines.push(` return json.data`);
|
|
122
|
+
lines.push(`}`);
|
|
123
|
+
lines.push(``);
|
|
124
|
+
lines.push(`interface Props { params: Promise<{ ${pkName}: string }> }`);
|
|
125
|
+
lines.push(``);
|
|
126
|
+
lines.push(`export default async function BlogPostPage({ params }: Props) {`);
|
|
127
|
+
lines.push(` const { ${pkName} } = await params`);
|
|
128
|
+
lines.push(` const item = await getItem(${pkName})`);
|
|
129
|
+
lines.push(` if (!item) notFound()`);
|
|
130
|
+
lines.push(` return (`);
|
|
131
|
+
lines.push(` <article className="container mx-auto px-4 py-8 max-w-2xl">`);
|
|
132
|
+
if (imageField) {
|
|
133
|
+
lines.push(` {item.${imageField} && (`);
|
|
134
|
+
lines.push(` <div className="relative w-full h-64 rounded-xl overflow-hidden mb-8">`);
|
|
135
|
+
lines.push(` <Image src={item.${imageField} as string} alt="" fill className="object-cover" />`);
|
|
136
|
+
lines.push(` </div>`);
|
|
137
|
+
lines.push(` )}`);
|
|
138
|
+
}
|
|
139
|
+
lines.push(` <h1 className="text-3xl font-bold text-gray-900 mb-4">{item.${titleField}?.toString()}</h1>`);
|
|
140
|
+
if (authorField)
|
|
141
|
+
lines.push(` <p className="text-sm text-gray-500 mb-8">By {item.${authorField}?.toString()}</p>`);
|
|
142
|
+
if (bodyField)
|
|
143
|
+
lines.push(` <div className="prose max-w-none"><p className="text-gray-700 leading-relaxed">{item.${bodyField}?.toString()}</p></div>`);
|
|
144
|
+
lines.push(` </article>`);
|
|
145
|
+
lines.push(` )`);
|
|
146
|
+
lines.push(`}`);
|
|
147
|
+
break;
|
|
148
|
+
}
|
|
149
|
+
case 'contact':
|
|
150
|
+
lines.push(`'use client'`);
|
|
151
|
+
lines.push(`import { useState } from 'react'`);
|
|
152
|
+
lines.push(``);
|
|
153
|
+
lines.push(`export default function ContactPage() {`);
|
|
154
|
+
lines.push(` const [name, setName] = useState('')`);
|
|
155
|
+
lines.push(` const [email, setEmail] = useState('')`);
|
|
156
|
+
lines.push(` const [message, setMessage] = useState('')`);
|
|
157
|
+
lines.push(` const [loading, setLoading] = useState(false)`);
|
|
158
|
+
lines.push(` const [sent, setSent] = useState(false)`);
|
|
159
|
+
lines.push(` const [error, setError] = useState<string | null>(null)`);
|
|
160
|
+
lines.push(``);
|
|
161
|
+
lines.push(` async function handleSubmit(e: React.FormEvent) {`);
|
|
162
|
+
lines.push(` e.preventDefault()`);
|
|
163
|
+
lines.push(` setLoading(true)`);
|
|
164
|
+
lines.push(` setError(null)`);
|
|
165
|
+
lines.push(` try {`);
|
|
166
|
+
lines.push(` const res = await fetch('/api/contact', {`);
|
|
167
|
+
lines.push(` method: 'POST',`);
|
|
168
|
+
lines.push(` headers: { 'Content-Type': 'application/json' },`);
|
|
169
|
+
lines.push(` body: JSON.stringify({ name, email, message }),`);
|
|
170
|
+
lines.push(` })`);
|
|
171
|
+
lines.push(` if (!res.ok) throw new Error('Failed to send message')`);
|
|
172
|
+
lines.push(` setSent(true)`);
|
|
173
|
+
lines.push(` } catch (err: any) {`);
|
|
174
|
+
lines.push(` setError(err.message)`);
|
|
175
|
+
lines.push(` } finally {`);
|
|
176
|
+
lines.push(` setLoading(false)`);
|
|
177
|
+
lines.push(` }`);
|
|
178
|
+
lines.push(` }`);
|
|
179
|
+
lines.push(``);
|
|
180
|
+
lines.push(` if (sent) return (`);
|
|
181
|
+
lines.push(` <div className="container mx-auto px-4 py-16 text-center max-w-lg">`);
|
|
182
|
+
lines.push(` <div className="text-5xl mb-4">✅</div>`);
|
|
183
|
+
lines.push(` <h1 className="text-2xl font-bold mb-2">Message sent!</h1>`);
|
|
184
|
+
lines.push(` <p className="text-gray-500">We'll get back to you as soon as possible.</p>`);
|
|
185
|
+
lines.push(` </div>`);
|
|
186
|
+
lines.push(` )`);
|
|
187
|
+
lines.push(``);
|
|
188
|
+
lines.push(` return (`);
|
|
189
|
+
lines.push(` <div className="container mx-auto px-4 py-8 max-w-xl">`);
|
|
190
|
+
lines.push(` <h1 className="text-2xl font-bold mb-2">Contact Us</h1>`);
|
|
191
|
+
lines.push(` <p className="text-gray-500 mb-8 text-sm">Fill in the form and we'll get back to you.</p>`);
|
|
192
|
+
lines.push(` <form onSubmit={handleSubmit} className="bg-white border border-gray-200 rounded-xl p-6 flex flex-col gap-4">`);
|
|
193
|
+
lines.push(` <div className="flex flex-col gap-1">`);
|
|
194
|
+
lines.push(` <label className="text-sm font-medium text-gray-700">Full Name</label>`);
|
|
195
|
+
lines.push(` <input type="text" value={name} onChange={(e) => setName(e.target.value)} required`);
|
|
196
|
+
lines.push(` className="px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-black" />`);
|
|
197
|
+
lines.push(` </div>`);
|
|
198
|
+
lines.push(` <div className="flex flex-col gap-1">`);
|
|
199
|
+
lines.push(` <label className="text-sm font-medium text-gray-700">Email</label>`);
|
|
200
|
+
lines.push(` <input type="email" value={email} onChange={(e) => setEmail(e.target.value)} required`);
|
|
201
|
+
lines.push(` className="px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-black" />`);
|
|
202
|
+
lines.push(` </div>`);
|
|
203
|
+
lines.push(` <div className="flex flex-col gap-1">`);
|
|
204
|
+
lines.push(` <label className="text-sm font-medium text-gray-700">Message</label>`);
|
|
205
|
+
lines.push(` <textarea value={message} onChange={(e) => setMessage(e.target.value)} required rows={5}`);
|
|
206
|
+
lines.push(` className="px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-black resize-none" />`);
|
|
207
|
+
lines.push(` </div>`);
|
|
208
|
+
lines.push(` {error && <p className="text-sm text-red-500">{error}</p>}`);
|
|
209
|
+
lines.push(` <button type="submit" disabled={loading}`);
|
|
210
|
+
lines.push(` className="py-2.5 bg-black text-white rounded-lg text-sm font-medium hover:bg-gray-800 disabled:opacity-40 transition">`);
|
|
211
|
+
lines.push(` {loading ? 'Sending...' : 'Send Message'}`);
|
|
212
|
+
lines.push(` </button>`);
|
|
213
|
+
lines.push(` </form>`);
|
|
214
|
+
lines.push(` </div>`);
|
|
215
|
+
lines.push(` )`);
|
|
216
|
+
lines.push(`}`);
|
|
217
|
+
break;
|
|
218
|
+
case 'cart':
|
|
219
|
+
lines.push(`'use client'`);
|
|
220
|
+
lines.push(`import Link from 'next/link'`);
|
|
221
|
+
lines.push(``);
|
|
222
|
+
lines.push(`export default function CartPage() {`);
|
|
223
|
+
lines.push(` return (`);
|
|
224
|
+
lines.push(` <div className="container mx-auto px-4 py-8 max-w-2xl">`);
|
|
225
|
+
lines.push(` <h1 className="text-2xl font-bold mb-6">Cart</h1>`);
|
|
226
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl p-8 text-center">`);
|
|
227
|
+
lines.push(` <div className="text-5xl mb-4">🛒</div>`);
|
|
228
|
+
lines.push(` <p className="text-gray-500 mb-6">Your cart is empty</p>`);
|
|
229
|
+
lines.push(` <Link href="/products"`);
|
|
230
|
+
lines.push(` className="px-6 py-2.5 bg-black text-white rounded-lg text-sm font-medium hover:bg-gray-800 transition">`);
|
|
231
|
+
lines.push(` Continue Shopping`);
|
|
232
|
+
lines.push(` </Link>`);
|
|
233
|
+
lines.push(` </div>`);
|
|
234
|
+
lines.push(` </div>`);
|
|
235
|
+
lines.push(` )`);
|
|
236
|
+
lines.push(`}`);
|
|
237
|
+
break;
|
|
238
|
+
case 'checkout':
|
|
239
|
+
lines.push(`'use client'`);
|
|
240
|
+
lines.push(`import { useState } from 'react'`);
|
|
241
|
+
lines.push(`import { useRouter } from 'next/navigation'`);
|
|
242
|
+
lines.push(``);
|
|
243
|
+
lines.push(`export default function CheckoutPage() {`);
|
|
244
|
+
lines.push(` const router = useRouter()`);
|
|
245
|
+
lines.push(` const [step, setStep] = useState(0)`);
|
|
246
|
+
lines.push(` const steps = ['Contact', 'Shipping', 'Payment']`);
|
|
247
|
+
lines.push(` return (`);
|
|
248
|
+
lines.push(` <div className="container mx-auto px-4 py-8 max-w-2xl">`);
|
|
249
|
+
lines.push(` <h1 className="text-2xl font-bold mb-6">Checkout</h1>`);
|
|
250
|
+
lines.push(` <div className="flex gap-2 mb-8">`);
|
|
251
|
+
lines.push(` {steps.map((s, i) => (`);
|
|
252
|
+
lines.push(` <div key={s} className="flex items-center gap-2">`);
|
|
253
|
+
lines.push(` <div className={\`w-8 h-8 rounded-full flex items-center justify-center text-sm font-medium \${i <= step ? 'bg-black text-white' : 'bg-gray-100 text-gray-400'}\`}>{i + 1}</div>`);
|
|
254
|
+
lines.push(` <span className="text-sm text-gray-600">{s}</span>`);
|
|
255
|
+
lines.push(` {i < steps.length - 1 && <div className="w-8 h-px bg-gray-200" />}`);
|
|
256
|
+
lines.push(` </div>`);
|
|
257
|
+
lines.push(` ))}`);
|
|
258
|
+
lines.push(` </div>`);
|
|
259
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl p-6">`);
|
|
260
|
+
lines.push(` <p className="text-gray-500 text-sm">Step {step + 1}: {steps[step]}</p>`);
|
|
261
|
+
lines.push(` <div className="mt-4 flex gap-3">`);
|
|
262
|
+
lines.push(` {step > 0 && (`);
|
|
263
|
+
lines.push(` <button onClick={() => setStep((s) => s - 1)}`);
|
|
264
|
+
lines.push(` className="flex-1 py-2.5 border border-gray-200 rounded-lg text-sm font-medium hover:bg-gray-50 transition">`);
|
|
265
|
+
lines.push(` Back`);
|
|
266
|
+
lines.push(` </button>`);
|
|
267
|
+
lines.push(` )}`);
|
|
268
|
+
lines.push(` <button onClick={() => step < 2 ? setStep((s) => s + 1) : router.push('/orders')}`);
|
|
269
|
+
lines.push(` className="flex-1 py-2.5 bg-black text-white rounded-lg text-sm font-medium hover:bg-gray-800 transition">`);
|
|
270
|
+
lines.push(` {step === 2 ? 'Place Order' : 'Continue'}`);
|
|
271
|
+
lines.push(` </button>`);
|
|
272
|
+
lines.push(` </div>`);
|
|
273
|
+
lines.push(` </div>`);
|
|
274
|
+
lines.push(` </div>`);
|
|
275
|
+
lines.push(` )`);
|
|
276
|
+
lines.push(`}`);
|
|
277
|
+
break;
|
|
278
|
+
case 'wallet':
|
|
279
|
+
lines.push(`export default function WalletPage() {`);
|
|
280
|
+
lines.push(` return (`);
|
|
281
|
+
lines.push(` <div className="container mx-auto px-4 py-8 max-w-lg">`);
|
|
282
|
+
lines.push(` <h1 className="text-2xl font-bold mb-6">Wallet</h1>`);
|
|
283
|
+
lines.push(` <div className="bg-black text-white rounded-2xl p-8 mb-6">`);
|
|
284
|
+
lines.push(` <p className="text-sm text-gray-300">Available Balance</p>`);
|
|
285
|
+
lines.push(` <p className="text-4xl font-bold mt-1">$0.00</p>`);
|
|
286
|
+
lines.push(` <div className="flex gap-4 mt-6">`);
|
|
287
|
+
lines.push(` {['Add', 'Send', 'History'].map((action) => (`);
|
|
288
|
+
lines.push(` <button key={action} className="flex flex-col items-center gap-1">`);
|
|
289
|
+
lines.push(` <div className="w-10 h-10 rounded-full bg-white/20 flex items-center justify-center">`);
|
|
290
|
+
lines.push(` {action === 'Add' ? '+' : action === 'Send' ? '↑' : '≡'}`);
|
|
291
|
+
lines.push(` </div>`);
|
|
292
|
+
lines.push(` <span className="text-xs text-gray-300">{action}</span>`);
|
|
293
|
+
lines.push(` </button>`);
|
|
294
|
+
lines.push(` ))}`);
|
|
295
|
+
lines.push(` </div>`);
|
|
296
|
+
lines.push(` </div>`);
|
|
297
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl p-6">`);
|
|
298
|
+
lines.push(` <h2 className="font-semibold mb-4">Recent Transactions</h2>`);
|
|
299
|
+
lines.push(` <p className="text-sm text-gray-400 text-center py-8">No transactions yet.</p>`);
|
|
300
|
+
lines.push(` </div>`);
|
|
301
|
+
lines.push(` </div>`);
|
|
302
|
+
lines.push(` )`);
|
|
303
|
+
lines.push(`}`);
|
|
304
|
+
break;
|
|
305
|
+
case 'invoice': {
|
|
306
|
+
const totalField = columns.find((c) => c.name.includes('total') || c.name.includes('amount'))?.name ?? null;
|
|
307
|
+
lines.push(`import type { ${className} } from '../../../../generated/types/${fileName}.types'`);
|
|
308
|
+
lines.push(`import { notFound } from 'next/navigation'`);
|
|
309
|
+
lines.push(``);
|
|
310
|
+
lines.push(`async function getItem(id: string): Promise<${className} | null> {`);
|
|
311
|
+
lines.push(` const res = await fetch(\`\${process.env.NEXT_PUBLIC_APP_URL}/api/${fileName}/\${id}\`, { cache: 'no-store' })`);
|
|
312
|
+
lines.push(` if (res.status === 404) return null`);
|
|
313
|
+
lines.push(` const json = await res.json()`);
|
|
314
|
+
lines.push(` return json.data`);
|
|
315
|
+
lines.push(`}`);
|
|
316
|
+
lines.push(``);
|
|
317
|
+
lines.push(`interface Props { params: Promise<{ ${pkName}: string }> }`);
|
|
318
|
+
lines.push(``);
|
|
319
|
+
lines.push(`export default async function InvoicePage({ params }: Props) {`);
|
|
320
|
+
lines.push(` const { ${pkName} } = await params`);
|
|
321
|
+
lines.push(` const item = await getItem(${pkName})`);
|
|
322
|
+
lines.push(` if (!item) notFound()`);
|
|
323
|
+
lines.push(` return (`);
|
|
324
|
+
lines.push(` <div className="container mx-auto px-4 py-8 max-w-2xl">`);
|
|
325
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl p-8">`);
|
|
326
|
+
lines.push(` <div className="flex justify-between items-start mb-8">`);
|
|
327
|
+
lines.push(` <div>`);
|
|
328
|
+
lines.push(` <h1 className="text-2xl font-bold">INVOICE</h1>`);
|
|
329
|
+
lines.push(` <p className="text-sm text-gray-500 mt-1">#{item.${pkName}}</p>`);
|
|
330
|
+
lines.push(` </div>`);
|
|
331
|
+
lines.push(` </div>`);
|
|
332
|
+
lines.push(` <div className="border-t border-gray-100 pt-6 flex flex-col gap-3">`);
|
|
333
|
+
for (const col of columns) {
|
|
334
|
+
const label = col.name.replaceAll('_', ' ');
|
|
335
|
+
lines.push(` <div className="flex justify-between text-sm">`);
|
|
336
|
+
lines.push(` <span className="text-gray-500 capitalize">${label}</span>`);
|
|
337
|
+
lines.push(` <span className="font-medium">{item.${col.name}?.toString() ?? '—'}</span>`);
|
|
338
|
+
lines.push(` </div>`);
|
|
339
|
+
}
|
|
340
|
+
lines.push(` </div>`);
|
|
341
|
+
if (totalField) {
|
|
342
|
+
lines.push(` <div className="border-t border-gray-200 mt-6 pt-4 flex justify-between">`);
|
|
343
|
+
lines.push(` <span className="font-bold text-lg">Total</span>`);
|
|
344
|
+
lines.push(` <span className="font-bold text-lg">{item.${totalField}?.toString() ?? '—'}</span>`);
|
|
345
|
+
lines.push(` </div>`);
|
|
346
|
+
}
|
|
347
|
+
lines.push(` </div>`);
|
|
348
|
+
lines.push(` </div>`);
|
|
349
|
+
lines.push(` )`);
|
|
350
|
+
lines.push(`}`);
|
|
351
|
+
break;
|
|
352
|
+
}
|
|
353
|
+
case 'chat':
|
|
354
|
+
lines.push(`'use client'`);
|
|
355
|
+
lines.push(`import { useState, useRef, useEffect } from 'react'`);
|
|
356
|
+
lines.push(``);
|
|
357
|
+
lines.push(`export default function ChatPage() {`);
|
|
358
|
+
lines.push(` const [messages, setMessages] = useState<{ text: string; isMe: boolean }[]>([])`);
|
|
359
|
+
lines.push(` const [input, setInput] = useState('')`);
|
|
360
|
+
lines.push(` const bottomRef = useRef<HTMLDivElement>(null)`);
|
|
361
|
+
lines.push(``);
|
|
362
|
+
lines.push(` useEffect(() => {`);
|
|
363
|
+
lines.push(` bottomRef.current?.scrollIntoView({ behavior: 'smooth' })`);
|
|
364
|
+
lines.push(` }, [messages])`);
|
|
365
|
+
lines.push(``);
|
|
366
|
+
lines.push(` function send() {`);
|
|
367
|
+
lines.push(` if (!input.trim()) return`);
|
|
368
|
+
lines.push(` setMessages((prev) => [...prev, { text: input.trim(), isMe: true }])`);
|
|
369
|
+
lines.push(` setInput('')`);
|
|
370
|
+
lines.push(` }`);
|
|
371
|
+
lines.push(``);
|
|
372
|
+
lines.push(` return (`);
|
|
373
|
+
lines.push(` <div className="flex flex-col h-[calc(100vh-4rem)]">`);
|
|
374
|
+
lines.push(` <div className="flex-1 overflow-y-auto p-4 flex flex-col gap-3">`);
|
|
375
|
+
lines.push(` {messages.length === 0 && (`);
|
|
376
|
+
lines.push(` <p className="text-center text-gray-400 text-sm mt-8">No messages yet. Say hello!</p>`);
|
|
377
|
+
lines.push(` )}`);
|
|
378
|
+
lines.push(` {messages.map((msg, i) => (`);
|
|
379
|
+
lines.push(` <div key={i} className={\`flex \${msg.isMe ? 'justify-end' : 'justify-start'}\`}>`);
|
|
380
|
+
lines.push(` <div className={\`max-w-xs px-4 py-2 rounded-2xl text-sm \${msg.isMe ? 'bg-black text-white' : 'bg-gray-100 text-gray-900'}\`}>`);
|
|
381
|
+
lines.push(` {msg.text}`);
|
|
382
|
+
lines.push(` </div>`);
|
|
383
|
+
lines.push(` </div>`);
|
|
384
|
+
lines.push(` ))}`);
|
|
385
|
+
lines.push(` <div ref={bottomRef} />`);
|
|
386
|
+
lines.push(` </div>`);
|
|
387
|
+
lines.push(` <div className="border-t border-gray-200 p-4 flex gap-2">`);
|
|
388
|
+
lines.push(` <input value={input} onChange={(e) => setInput(e.target.value)}`);
|
|
389
|
+
lines.push(` onKeyDown={(e) => e.key === 'Enter' && send()}`);
|
|
390
|
+
lines.push(` placeholder="Type a message..."`);
|
|
391
|
+
lines.push(` className="flex-1 px-4 py-2 border border-gray-200 rounded-full text-sm focus:outline-none focus:ring-2 focus:ring-black" />`);
|
|
392
|
+
lines.push(` <button onClick={send}`);
|
|
393
|
+
lines.push(` className="w-10 h-10 rounded-full bg-black text-white flex items-center justify-center hover:bg-gray-800 transition">`);
|
|
394
|
+
lines.push(` ↑`);
|
|
395
|
+
lines.push(` </button>`);
|
|
396
|
+
lines.push(` </div>`);
|
|
397
|
+
lines.push(` </div>`);
|
|
398
|
+
lines.push(` )`);
|
|
399
|
+
lines.push(`}`);
|
|
400
|
+
break;
|
|
401
|
+
case 'messages':
|
|
402
|
+
lines.push(`export default function MessagesPage() {`);
|
|
403
|
+
lines.push(` return (`);
|
|
404
|
+
lines.push(` <div className="container mx-auto px-4 py-8 max-w-2xl">`);
|
|
405
|
+
lines.push(` <h1 className="text-2xl font-bold mb-6">Messages</h1>`);
|
|
406
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl p-8 text-center">`);
|
|
407
|
+
lines.push(` <div className="text-5xl mb-4">💬</div>`);
|
|
408
|
+
lines.push(` <p className="text-gray-500">No messages yet</p>`);
|
|
409
|
+
lines.push(` <p className="text-sm text-gray-400 mt-1">Start a conversation to see messages here.</p>`);
|
|
410
|
+
lines.push(` </div>`);
|
|
411
|
+
lines.push(` </div>`);
|
|
412
|
+
lines.push(` )`);
|
|
413
|
+
lines.push(`}`);
|
|
414
|
+
break;
|
|
415
|
+
case 'feed': {
|
|
416
|
+
const bodyField = columns.find((c) => c.name.includes('body') || c.name.includes('content'))?.name ?? columns[0]?.name ?? pkName;
|
|
417
|
+
const nameField = columns.find((c) => c.name.includes('name') || c.name.includes('author'))?.name ?? null;
|
|
418
|
+
const modelSlug = model ? model.name.replace('nxf_', '').replace(/_/g, '-') : fileName;
|
|
419
|
+
lines.push(`import type { ${className} } from '../../../generated/types/${fileName}.types'`);
|
|
420
|
+
lines.push(`import Link from 'next/link'`);
|
|
421
|
+
lines.push(``);
|
|
422
|
+
lines.push(`async function getItems(): Promise<${className}[]> {`);
|
|
423
|
+
lines.push(` const res = await fetch(\`\${process.env.NEXT_PUBLIC_APP_URL}/api/${modelSlug}\`, { cache: 'no-store' })`);
|
|
424
|
+
lines.push(` if (!res.ok) return []`);
|
|
425
|
+
lines.push(` const json = await res.json()`);
|
|
426
|
+
lines.push(` return json.data ?? []`);
|
|
427
|
+
lines.push(`}`);
|
|
428
|
+
lines.push(``);
|
|
429
|
+
lines.push(`export default async function FeedPage() {`);
|
|
430
|
+
lines.push(` const items = await getItems()`);
|
|
431
|
+
lines.push(` return (`);
|
|
432
|
+
lines.push(` <div className="container mx-auto px-4 py-8 max-w-xl">`);
|
|
433
|
+
lines.push(` <h1 className="text-2xl font-bold mb-6">${page.name}</h1>`);
|
|
434
|
+
lines.push(` <div className="flex flex-col gap-4">`);
|
|
435
|
+
lines.push(` {items.length === 0 && <p className="text-center text-gray-400 py-16">No posts yet.</p>}`);
|
|
436
|
+
lines.push(` {items.map((item) => (`);
|
|
437
|
+
lines.push(` <div key={item.${pkName}} className="bg-white border border-gray-200 rounded-xl p-5">`);
|
|
438
|
+
lines.push(` <div className="flex items-center gap-3 mb-3">`);
|
|
439
|
+
lines.push(` <div className="w-8 h-8 rounded-full bg-gray-200 flex items-center justify-center text-sm">👤</div>`);
|
|
440
|
+
if (nameField) {
|
|
441
|
+
lines.push(` <span className="text-sm font-medium">{item.${nameField}?.toString() ?? 'User'}</span>`);
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
lines.push(` <span className="text-sm font-medium">User</span>`);
|
|
445
|
+
}
|
|
446
|
+
lines.push(` </div>`);
|
|
447
|
+
lines.push(` <p className="text-sm text-gray-700 mb-3">{item.${bodyField}?.toString() ?? ''}</p>`);
|
|
448
|
+
lines.push(` <div className="flex gap-4 text-xs text-gray-400">`);
|
|
449
|
+
lines.push(` <button className="hover:text-gray-600">👍 Like</button>`);
|
|
450
|
+
lines.push(` <button className="hover:text-gray-600">💬 Comment</button>`);
|
|
451
|
+
lines.push(` <button className="hover:text-gray-600">↗ Share</button>`);
|
|
452
|
+
lines.push(` </div>`);
|
|
453
|
+
lines.push(` </div>`);
|
|
454
|
+
lines.push(` ))}`);
|
|
455
|
+
lines.push(` </div>`);
|
|
456
|
+
lines.push(` </div>`);
|
|
457
|
+
lines.push(` )`);
|
|
458
|
+
lines.push(`}`);
|
|
459
|
+
break;
|
|
460
|
+
}
|
|
461
|
+
case 'map':
|
|
462
|
+
lines.push(`export default function MapPage() {`);
|
|
463
|
+
lines.push(` return (`);
|
|
464
|
+
lines.push(` <div className="container mx-auto px-4 py-8">`);
|
|
465
|
+
lines.push(` <h1 className="text-2xl font-bold mb-6">Map</h1>`);
|
|
466
|
+
lines.push(` <div className="bg-gray-100 rounded-xl h-96 flex items-center justify-center">`);
|
|
467
|
+
lines.push(` <div className="text-center text-gray-400">`);
|
|
468
|
+
lines.push(` <div className="text-5xl mb-3">🗺️</div>`);
|
|
469
|
+
lines.push(` <p className="font-medium">Map placeholder</p>`);
|
|
470
|
+
lines.push(` <p className="text-sm mt-1">Add @react-google-maps/api or mapbox-gl to implement.</p>`);
|
|
471
|
+
lines.push(` </div>`);
|
|
472
|
+
lines.push(` </div>`);
|
|
473
|
+
lines.push(` </div>`);
|
|
474
|
+
lines.push(` )`);
|
|
475
|
+
lines.push(`}`);
|
|
476
|
+
break;
|
|
477
|
+
case 'search': {
|
|
478
|
+
const titleField = columns.find((c) => c.name.includes('name') || c.name.includes('title'))?.name ?? columns[0]?.name ?? pkName;
|
|
479
|
+
const modelSlug = model ? model.name.replace('nxf_', '').replace(/_/g, '-') : fileName;
|
|
480
|
+
lines.push(`'use client'`);
|
|
481
|
+
lines.push(`import { useState } from 'react'`);
|
|
482
|
+
lines.push(`import Link from 'next/link'`);
|
|
483
|
+
lines.push(`import type { ${className} } from '../../../generated/types/${fileName}.types'`);
|
|
484
|
+
lines.push(``);
|
|
485
|
+
lines.push(`export default function SearchPage() {`);
|
|
486
|
+
lines.push(` const [query, setQuery] = useState('')`);
|
|
487
|
+
lines.push(` const [results, setResults] = useState<${className}[]>([])`);
|
|
488
|
+
lines.push(` const [loading, setLoading] = useState(false)`);
|
|
489
|
+
lines.push(``);
|
|
490
|
+
lines.push(` async function search(q: string) {`);
|
|
491
|
+
lines.push(` setQuery(q)`);
|
|
492
|
+
lines.push(` if (!q.trim()) { setResults([]); return }`);
|
|
493
|
+
lines.push(` setLoading(true)`);
|
|
494
|
+
lines.push(` try {`);
|
|
495
|
+
lines.push(` const res = await fetch(\`/api/${modelSlug}?search=\${encodeURIComponent(q)}\`)`);
|
|
496
|
+
lines.push(` const json = await res.json()`);
|
|
497
|
+
lines.push(` setResults(json.data ?? [])`);
|
|
498
|
+
lines.push(` } finally {`);
|
|
499
|
+
lines.push(` setLoading(false)`);
|
|
500
|
+
lines.push(` }`);
|
|
501
|
+
lines.push(` }`);
|
|
502
|
+
lines.push(``);
|
|
503
|
+
lines.push(` return (`);
|
|
504
|
+
lines.push(` <div className="container mx-auto px-4 py-8 max-w-2xl">`);
|
|
505
|
+
lines.push(` <div className="flex items-center gap-3 bg-white border border-gray-200 rounded-xl px-4 py-3 mb-6">`);
|
|
506
|
+
lines.push(` <span className="text-gray-400">🔍</span>`);
|
|
507
|
+
lines.push(` <input value={query} onChange={(e) => search(e.target.value)} placeholder="Search..."`);
|
|
508
|
+
lines.push(` className="flex-1 text-sm focus:outline-none" autoFocus />`);
|
|
509
|
+
lines.push(` {query && <button onClick={() => { setQuery(''); setResults([]) }} className="text-gray-400 hover:text-gray-600">✕</button>}`);
|
|
510
|
+
lines.push(` </div>`);
|
|
511
|
+
lines.push(` {loading && <p className="text-center text-gray-400 text-sm">Searching...</p>}`);
|
|
512
|
+
lines.push(` {!loading && query && results.length === 0 && (`);
|
|
513
|
+
lines.push(` <p className="text-center text-gray-400 text-sm">No results for "{query}"</p>`);
|
|
514
|
+
lines.push(` )}`);
|
|
515
|
+
lines.push(` <div className="flex flex-col gap-2">`);
|
|
516
|
+
lines.push(` {results.map((item) => (`);
|
|
517
|
+
lines.push(` <Link key={item.${pkName}} href={\`/${routePath}/\${item.${pkName}}\`}`);
|
|
518
|
+
lines.push(` className="flex items-center justify-between px-4 py-3 bg-white border border-gray-200 rounded-xl hover:border-gray-400 transition">`);
|
|
519
|
+
lines.push(` <span className="text-sm font-medium">{item.${titleField}?.toString() ?? '—'}</span>`);
|
|
520
|
+
lines.push(` <span className="text-gray-400">›</span>`);
|
|
521
|
+
lines.push(` </Link>`);
|
|
522
|
+
lines.push(` ))}`);
|
|
523
|
+
lines.push(` </div>`);
|
|
524
|
+
lines.push(` </div>`);
|
|
525
|
+
lines.push(` )`);
|
|
526
|
+
lines.push(`}`);
|
|
527
|
+
break;
|
|
528
|
+
}
|
|
529
|
+
case 'settings':
|
|
530
|
+
lines.push(`'use client'`);
|
|
531
|
+
lines.push(`import { useState } from 'react'`);
|
|
532
|
+
lines.push(``);
|
|
533
|
+
lines.push(`export default function SettingsPage() {`);
|
|
534
|
+
lines.push(` const [notifications, setNotifications] = useState(true)`);
|
|
535
|
+
lines.push(` const [darkMode, setDarkMode] = useState(false)`);
|
|
536
|
+
lines.push(``);
|
|
537
|
+
lines.push(` return (`);
|
|
538
|
+
lines.push(` <div className="container mx-auto px-4 py-8 max-w-2xl">`);
|
|
539
|
+
lines.push(` <h1 className="text-2xl font-bold mb-6">Settings</h1>`);
|
|
540
|
+
lines.push(` <div className="flex flex-col gap-4">`);
|
|
541
|
+
lines.push(``);
|
|
542
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl overflow-hidden">`);
|
|
543
|
+
lines.push(` <div className="px-6 py-3 bg-gray-50 border-b border-gray-100">`);
|
|
544
|
+
lines.push(` <p className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Account</p>`);
|
|
545
|
+
lines.push(` </div>`);
|
|
546
|
+
lines.push(` <div className="divide-y divide-gray-100">`);
|
|
547
|
+
lines.push(` <div className="flex items-center justify-between px-6 py-4 hover:bg-gray-50 cursor-pointer">`);
|
|
548
|
+
lines.push(` <span className="text-sm">Profile</span>`);
|
|
549
|
+
lines.push(` <span className="text-gray-400">›</span>`);
|
|
550
|
+
lines.push(` </div>`);
|
|
551
|
+
lines.push(` <div className="flex items-center justify-between px-6 py-4 hover:bg-gray-50 cursor-pointer">`);
|
|
552
|
+
lines.push(` <span className="text-sm">Change Password</span>`);
|
|
553
|
+
lines.push(` <span className="text-gray-400">›</span>`);
|
|
554
|
+
lines.push(` </div>`);
|
|
555
|
+
lines.push(` </div>`);
|
|
556
|
+
lines.push(` </div>`);
|
|
557
|
+
lines.push(``);
|
|
558
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl overflow-hidden">`);
|
|
559
|
+
lines.push(` <div className="px-6 py-3 bg-gray-50 border-b border-gray-100">`);
|
|
560
|
+
lines.push(` <p className="text-xs font-semibold text-gray-500 uppercase tracking-wider">Preferences</p>`);
|
|
561
|
+
lines.push(` </div>`);
|
|
562
|
+
lines.push(` <div className="divide-y divide-gray-100">`);
|
|
563
|
+
lines.push(` <div className="flex items-center justify-between px-6 py-4">`);
|
|
564
|
+
lines.push(` <span className="text-sm">Notifications</span>`);
|
|
565
|
+
lines.push(` <button onClick={() => setNotifications((v) => !v)}`);
|
|
566
|
+
lines.push(` className={\`w-10 h-6 rounded-full transition-colors \${notifications ? 'bg-black' : 'bg-gray-200'}\`}>`);
|
|
567
|
+
lines.push(` <span className={\`block w-4 h-4 bg-white rounded-full shadow transition-transform mx-1 \${notifications ? 'translate-x-4' : 'translate-x-0'}\`} />`);
|
|
568
|
+
lines.push(` </button>`);
|
|
569
|
+
lines.push(` </div>`);
|
|
570
|
+
lines.push(` <div className="flex items-center justify-between px-6 py-4">`);
|
|
571
|
+
lines.push(` <span className="text-sm">Dark Mode</span>`);
|
|
572
|
+
lines.push(` <button onClick={() => setDarkMode((v) => !v)}`);
|
|
573
|
+
lines.push(` className={\`w-10 h-6 rounded-full transition-colors \${darkMode ? 'bg-black' : 'bg-gray-200'}\`}>`);
|
|
574
|
+
lines.push(` <span className={\`block w-4 h-4 bg-white rounded-full shadow transition-transform mx-1 \${darkMode ? 'translate-x-4' : 'translate-x-0'}\`} />`);
|
|
575
|
+
lines.push(` </button>`);
|
|
576
|
+
lines.push(` </div>`);
|
|
577
|
+
lines.push(` </div>`);
|
|
578
|
+
lines.push(` </div>`);
|
|
579
|
+
lines.push(``);
|
|
580
|
+
lines.push(` <button className="w-full py-3 border border-red-200 text-red-500 rounded-xl text-sm font-medium hover:bg-red-50 transition">`);
|
|
581
|
+
lines.push(` Sign Out`);
|
|
582
|
+
lines.push(` </button>`);
|
|
583
|
+
lines.push(` </div>`);
|
|
584
|
+
lines.push(` </div>`);
|
|
585
|
+
lines.push(` )`);
|
|
586
|
+
lines.push(`}`);
|
|
587
|
+
break;
|
|
588
|
+
case 'gallery': {
|
|
589
|
+
const imageField = columns.find((c) => c.name.includes('image') || c.name.includes('photo') || c.name.includes('url'))?.name ?? columns[0]?.name ?? pkName;
|
|
590
|
+
const titleField = columns.find((c) => c.name.includes('title') || c.name.includes('name'))?.name ?? columns[0]?.name ?? pkName;
|
|
591
|
+
const modelSlug = model ? model.name.replace('nxf_', '').replace(/_/g, '-') : fileName;
|
|
592
|
+
lines.push(`import type { ${className} } from '../../../generated/types/${fileName}.types'`);
|
|
593
|
+
lines.push(`import Image from 'next/image'`);
|
|
594
|
+
lines.push(`import Link from 'next/link'`);
|
|
595
|
+
lines.push(``);
|
|
596
|
+
lines.push(`async function getItems(): Promise<${className}[]> {`);
|
|
597
|
+
lines.push(` const res = await fetch(\`\${process.env.NEXT_PUBLIC_APP_URL}/api/${modelSlug}\`, { cache: 'no-store' })`);
|
|
598
|
+
lines.push(` if (!res.ok) return []`);
|
|
599
|
+
lines.push(` const json = await res.json()`);
|
|
600
|
+
lines.push(` return json.data ?? []`);
|
|
601
|
+
lines.push(`}`);
|
|
602
|
+
lines.push(``);
|
|
603
|
+
lines.push(`export default async function GalleryPage() {`);
|
|
604
|
+
lines.push(` const items = await getItems()`);
|
|
605
|
+
lines.push(` return (`);
|
|
606
|
+
lines.push(` <div className="container mx-auto px-4 py-8">`);
|
|
607
|
+
lines.push(` <h1 className="text-2xl font-bold mb-6">${page.name}</h1>`);
|
|
608
|
+
lines.push(` {items.length === 0 ? (`);
|
|
609
|
+
lines.push(` <div className="text-center py-16 text-gray-400">No items yet.</div>`);
|
|
610
|
+
lines.push(` ) : (`);
|
|
611
|
+
lines.push(` <div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">`);
|
|
612
|
+
lines.push(` {items.map((item) => (`);
|
|
613
|
+
lines.push(` <Link key={item.${pkName}} href={\`/${routePath}/\${item.${pkName}}\`}`);
|
|
614
|
+
lines.push(` className="group relative aspect-square bg-gray-100 rounded-xl overflow-hidden">`);
|
|
615
|
+
lines.push(` {item.${imageField} ? (`);
|
|
616
|
+
lines.push(` <Image src={item.${imageField} as string} alt={item.${titleField}?.toString() ?? ''} fill className="object-cover group-hover:scale-105 transition-transform duration-300" />`);
|
|
617
|
+
lines.push(` ) : (`);
|
|
618
|
+
lines.push(` <div className="w-full h-full flex items-center justify-center text-gray-300 text-3xl">🖼</div>`);
|
|
619
|
+
lines.push(` )}`);
|
|
620
|
+
lines.push(` <div className="absolute inset-0 bg-black/0 group-hover:bg-black/20 transition-colors duration-300" />`);
|
|
621
|
+
lines.push(` <div className="absolute bottom-0 left-0 right-0 p-3 translate-y-full group-hover:translate-y-0 transition-transform duration-300">`);
|
|
622
|
+
lines.push(` <p className="text-white text-xs font-medium truncate">{item.${titleField}?.toString() ?? ''}</p>`);
|
|
623
|
+
lines.push(` </div>`);
|
|
624
|
+
lines.push(` </Link>`);
|
|
625
|
+
lines.push(` ))}`);
|
|
626
|
+
lines.push(` </div>`);
|
|
627
|
+
lines.push(` )}`);
|
|
628
|
+
lines.push(` </div>`);
|
|
629
|
+
lines.push(` )`);
|
|
630
|
+
lines.push(`}`);
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
case 'kanban': {
|
|
634
|
+
const titleField = columns.find((c) => c.name.includes('title') || c.name.includes('name'))?.name ?? columns[0]?.name ?? pkName;
|
|
635
|
+
const statusField = columns.find((c) => c.name.includes('status') || c.name.includes('stage'))?.name ?? null;
|
|
636
|
+
const modelSlug = model ? model.name.replace('nxf_', '').replace(/_/g, '-') : fileName;
|
|
637
|
+
lines.push(`import type { ${className} } from '../../../generated/types/${fileName}.types'`);
|
|
638
|
+
lines.push(``);
|
|
639
|
+
lines.push(`const COLUMNS = ['To Do', 'In Progress', 'Done']`);
|
|
640
|
+
lines.push(``);
|
|
641
|
+
lines.push(`async function getItems(): Promise<${className}[]> {`);
|
|
642
|
+
lines.push(` const res = await fetch(\`\${process.env.NEXT_PUBLIC_APP_URL}/api/${modelSlug}\`, { cache: 'no-store' })`);
|
|
643
|
+
lines.push(` if (!res.ok) return []`);
|
|
644
|
+
lines.push(` const json = await res.json()`);
|
|
645
|
+
lines.push(` return json.data ?? []`);
|
|
646
|
+
lines.push(`}`);
|
|
647
|
+
lines.push(``);
|
|
648
|
+
lines.push(`export default async function KanbanPage() {`);
|
|
649
|
+
lines.push(` const items = await getItems()`);
|
|
650
|
+
lines.push(` return (`);
|
|
651
|
+
lines.push(` <div className="p-6 overflow-x-auto">`);
|
|
652
|
+
lines.push(` <h1 className="text-2xl font-bold mb-6">${page.name}</h1>`);
|
|
653
|
+
lines.push(` <div className="flex gap-4 min-w-max">`);
|
|
654
|
+
lines.push(` {COLUMNS.map((col) => {`);
|
|
655
|
+
lines.push(` const colItems = items.filter((item) => {`);
|
|
656
|
+
if (statusField) {
|
|
657
|
+
lines.push(` return (item.${statusField}?.toString() ?? 'To Do') === col`);
|
|
658
|
+
}
|
|
659
|
+
else {
|
|
660
|
+
lines.push(` return col === 'To Do'`);
|
|
661
|
+
}
|
|
662
|
+
lines.push(` })`);
|
|
663
|
+
lines.push(` return (`);
|
|
664
|
+
lines.push(` <div key={col} className="w-72 bg-gray-50 rounded-xl p-4 flex flex-col gap-3">`);
|
|
665
|
+
lines.push(` <div className="flex items-center justify-between mb-2">`);
|
|
666
|
+
lines.push(` <span className="text-sm font-semibold text-gray-700">{col}</span>`);
|
|
667
|
+
lines.push(` <span className="text-xs bg-gray-200 text-gray-600 rounded-full px-2 py-0.5">{colItems.length}</span>`);
|
|
668
|
+
lines.push(` </div>`);
|
|
669
|
+
lines.push(` {colItems.map((item) => (`);
|
|
670
|
+
lines.push(` <div key={item.${pkName}} className="bg-white border border-gray-200 rounded-lg p-3 shadow-sm">`);
|
|
671
|
+
lines.push(` <p className="text-sm font-medium text-gray-900">{item.${titleField}?.toString() ?? '—'}</p>`);
|
|
672
|
+
lines.push(` </div>`);
|
|
673
|
+
lines.push(` ))}`);
|
|
674
|
+
lines.push(` </div>`);
|
|
675
|
+
lines.push(` )`);
|
|
676
|
+
lines.push(` })}`);
|
|
677
|
+
lines.push(` </div>`);
|
|
678
|
+
lines.push(` </div>`);
|
|
679
|
+
lines.push(` )`);
|
|
680
|
+
lines.push(`}`);
|
|
681
|
+
break;
|
|
682
|
+
}
|
|
683
|
+
case 'calendar': {
|
|
684
|
+
const titleField = columns.find((c) => c.name.includes('title') || c.name.includes('name'))?.name ?? columns[0]?.name ?? pkName;
|
|
685
|
+
const dateField = columns.find((c) => c.name.includes('date') || c.name.includes('start'))?.name ?? null;
|
|
686
|
+
const modelSlug = model ? model.name.replace('nxf_', '').replace(/_/g, '-') : fileName;
|
|
687
|
+
lines.push(`import type { ${className} } from '../../../generated/types/${fileName}.types'`);
|
|
688
|
+
lines.push(``);
|
|
689
|
+
lines.push(`async function getItems(): Promise<${className}[]> {`);
|
|
690
|
+
lines.push(` const res = await fetch(\`\${process.env.NEXT_PUBLIC_APP_URL}/api/${modelSlug}\`, { cache: 'no-store' })`);
|
|
691
|
+
lines.push(` if (!res.ok) return []`);
|
|
692
|
+
lines.push(` const json = await res.json()`);
|
|
693
|
+
lines.push(` return json.data ?? []`);
|
|
694
|
+
lines.push(`}`);
|
|
695
|
+
lines.push(``);
|
|
696
|
+
lines.push(`export default async function CalendarPage() {`);
|
|
697
|
+
lines.push(` const items = await getItems()`);
|
|
698
|
+
lines.push(` const now = new Date()`);
|
|
699
|
+
lines.push(` return (`);
|
|
700
|
+
lines.push(` <div className="container mx-auto px-4 py-8">`);
|
|
701
|
+
lines.push(` <h1 className="text-2xl font-bold mb-6">${page.name}</h1>`);
|
|
702
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl overflow-hidden">`);
|
|
703
|
+
lines.push(` <div className="flex items-center justify-between px-6 py-4 border-b border-gray-100">`);
|
|
704
|
+
lines.push(` <h2 className="font-semibold">{now.toLocaleString('default', { month: 'long', year: 'numeric' })}</h2>`);
|
|
705
|
+
lines.push(` </div>`);
|
|
706
|
+
lines.push(` <div className="p-4 flex flex-col gap-2">`);
|
|
707
|
+
lines.push(` {items.length === 0 ? (`);
|
|
708
|
+
lines.push(` <p className="text-center text-gray-400 py-8">No events yet.</p>`);
|
|
709
|
+
lines.push(` ) : (`);
|
|
710
|
+
lines.push(` items.map((item) => (`);
|
|
711
|
+
lines.push(` <div key={item.${pkName}} className="flex items-center gap-3 px-4 py-3 bg-gray-50 rounded-lg">`);
|
|
712
|
+
lines.push(` <div className="w-2 h-2 rounded-full bg-black shrink-0" />`);
|
|
713
|
+
lines.push(` <span className="text-sm font-medium">{item.${titleField}?.toString() ?? '—'}</span>`);
|
|
714
|
+
if (dateField) {
|
|
715
|
+
lines.push(` <span className="text-xs text-gray-400 ml-auto">{item.${dateField}?.toString() ?? ''}</span>`);
|
|
716
|
+
}
|
|
717
|
+
lines.push(` </div>`);
|
|
718
|
+
lines.push(` ))`);
|
|
719
|
+
lines.push(` )}`);
|
|
720
|
+
lines.push(` </div>`);
|
|
721
|
+
lines.push(` </div>`);
|
|
722
|
+
lines.push(` </div>`);
|
|
723
|
+
lines.push(` )`);
|
|
724
|
+
lines.push(`}`);
|
|
725
|
+
break;
|
|
726
|
+
}
|
|
727
|
+
case 'otp_request':
|
|
728
|
+
lines.push(`'use client'`);
|
|
729
|
+
lines.push(`import { useState } from 'react'`);
|
|
730
|
+
lines.push(`import { useRouter } from 'next/navigation'`);
|
|
731
|
+
lines.push(``);
|
|
732
|
+
lines.push(`export default function OtpRequestPage() {`);
|
|
733
|
+
lines.push(` const router = useRouter()`);
|
|
734
|
+
lines.push(` const [phone, setPhone] = useState('')`);
|
|
735
|
+
lines.push(` const [loading, setLoading] = useState(false)`);
|
|
736
|
+
lines.push(` const [error, setError] = useState<string | null>(null)`);
|
|
737
|
+
lines.push(``);
|
|
738
|
+
lines.push(` async function handleSubmit(e: React.FormEvent) {`);
|
|
739
|
+
lines.push(` e.preventDefault()`);
|
|
740
|
+
lines.push(` setLoading(true)`);
|
|
741
|
+
lines.push(` setError(null)`);
|
|
742
|
+
lines.push(` try {`);
|
|
743
|
+
lines.push(` const res = await fetch('/api/auth/otp/request', {`);
|
|
744
|
+
lines.push(` method: 'POST',`);
|
|
745
|
+
lines.push(` headers: { 'Content-Type': 'application/json' },`);
|
|
746
|
+
lines.push(` body: JSON.stringify({ phone }),`);
|
|
747
|
+
lines.push(` })`);
|
|
748
|
+
lines.push(` if (!res.ok) throw new Error('Failed to send OTP')`);
|
|
749
|
+
lines.push(` router.push(\`/otp-entry?phone=\${encodeURIComponent(phone)}\`)`);
|
|
750
|
+
lines.push(` } catch (err: any) {`);
|
|
751
|
+
lines.push(` setError(err.message)`);
|
|
752
|
+
lines.push(` } finally {`);
|
|
753
|
+
lines.push(` setLoading(false)`);
|
|
754
|
+
lines.push(` }`);
|
|
755
|
+
lines.push(` }`);
|
|
756
|
+
lines.push(``);
|
|
757
|
+
lines.push(` return (`);
|
|
758
|
+
lines.push(` <div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">`);
|
|
759
|
+
lines.push(` <div className="w-full max-w-sm text-center">`);
|
|
760
|
+
lines.push(` <div className="text-5xl mb-4">📱</div>`);
|
|
761
|
+
lines.push(` <h1 className="text-2xl font-bold mb-2">Verify Phone</h1>`);
|
|
762
|
+
lines.push(` <p className="text-gray-500 mb-8 text-sm">We will send you a one-time verification code.</p>`);
|
|
763
|
+
lines.push(` <form onSubmit={handleSubmit} className="bg-white border border-gray-200 rounded-xl p-6 flex flex-col gap-4 text-left">`);
|
|
764
|
+
lines.push(` <div className="flex flex-col gap-1">`);
|
|
765
|
+
lines.push(` <label className="text-sm font-medium text-gray-700">Phone Number</label>`);
|
|
766
|
+
lines.push(` <input type="tel" value={phone} onChange={(e) => setPhone(e.target.value)} required`);
|
|
767
|
+
lines.push(` className="px-3 py-2 border border-gray-200 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-black" />`);
|
|
768
|
+
lines.push(` </div>`);
|
|
769
|
+
lines.push(` {error && <p className="text-sm text-red-500">{error}</p>}`);
|
|
770
|
+
lines.push(` <button type="submit" disabled={loading}`);
|
|
771
|
+
lines.push(` className="py-2.5 bg-black text-white rounded-lg text-sm font-medium hover:bg-gray-800 disabled:opacity-40 transition">`);
|
|
772
|
+
lines.push(` {loading ? 'Sending...' : 'Send OTP'}`);
|
|
773
|
+
lines.push(` </button>`);
|
|
774
|
+
lines.push(` </form>`);
|
|
775
|
+
lines.push(` </div>`);
|
|
776
|
+
lines.push(` </div>`);
|
|
777
|
+
lines.push(` )`);
|
|
778
|
+
lines.push(`}`);
|
|
779
|
+
break;
|
|
780
|
+
case 'otp_entry':
|
|
781
|
+
lines.push(`'use client'`);
|
|
782
|
+
lines.push(`import { useState, useRef } from 'react'`);
|
|
783
|
+
lines.push(`import { useRouter,`);
|
|
784
|
+
lines.push(` useSearchParams } from 'next/navigation'`);
|
|
785
|
+
lines.push(``);
|
|
786
|
+
lines.push(`export default function OtpEntryPage() {`);
|
|
787
|
+
lines.push(` const router = useRouter()`);
|
|
788
|
+
lines.push(` const searchParams = useSearchParams()`);
|
|
789
|
+
lines.push(` const phone = searchParams.get('phone') ?? ''`);
|
|
790
|
+
lines.push(` const [otp, setOtp] = useState(Array(6).fill(''))`);
|
|
791
|
+
lines.push(` const [loading, setLoading] = useState(false)`);
|
|
792
|
+
lines.push(` const [error, setError] = useState<string | null>(null)`);
|
|
793
|
+
lines.push(` const inputRefs = Array.from({ length: 6 }, () => useRef<HTMLInputElement>(null))`);
|
|
794
|
+
lines.push(``);
|
|
795
|
+
lines.push(` function handleChange(i: number, value: string) {`);
|
|
796
|
+
lines.push(` if (!/^[0-9]?$/.test(value)) return`);
|
|
797
|
+
lines.push(` const next = [...otp]`);
|
|
798
|
+
lines.push(` next[i] = value`);
|
|
799
|
+
lines.push(` setOtp(next)`);
|
|
800
|
+
lines.push(` if (value && i < 5) inputRefs[i + 1].current?.focus()`);
|
|
801
|
+
lines.push(` if (!value && i > 0) inputRefs[i - 1].current?.focus()`);
|
|
802
|
+
lines.push(` if (next.every((d) => d !== '') && i === 5) verify(next.join(''))`);
|
|
803
|
+
lines.push(` }`);
|
|
804
|
+
lines.push(``);
|
|
805
|
+
lines.push(` async function verify(code: string) {`);
|
|
806
|
+
lines.push(` setLoading(true)`);
|
|
807
|
+
lines.push(` setError(null)`);
|
|
808
|
+
lines.push(` try {`);
|
|
809
|
+
lines.push(` const res = await fetch('/api/auth/otp/verify', {`);
|
|
810
|
+
lines.push(` method: 'POST',`);
|
|
811
|
+
lines.push(` headers: { 'Content-Type': 'application/json' },`);
|
|
812
|
+
lines.push(` body: JSON.stringify({ phone, otp: code }),`);
|
|
813
|
+
lines.push(` })`);
|
|
814
|
+
lines.push(` if (!res.ok) throw new Error('Invalid OTP')`);
|
|
815
|
+
lines.push(` router.push('/')`);
|
|
816
|
+
lines.push(` } catch (err: any) {`);
|
|
817
|
+
lines.push(` setError(err.message)`);
|
|
818
|
+
lines.push(` setOtp(Array(6).fill(''))`);
|
|
819
|
+
lines.push(` inputRefs[0].current?.focus()`);
|
|
820
|
+
lines.push(` } finally {`);
|
|
821
|
+
lines.push(` setLoading(false)`);
|
|
822
|
+
lines.push(` }`);
|
|
823
|
+
lines.push(` }`);
|
|
824
|
+
lines.push(``);
|
|
825
|
+
lines.push(` return (`);
|
|
826
|
+
lines.push(` <div className="min-h-screen flex items-center justify-center bg-gray-50 px-4">`);
|
|
827
|
+
lines.push(` <div className="w-full max-w-sm text-center">`);
|
|
828
|
+
lines.push(` <div className="text-5xl mb-4">🔒</div>`);
|
|
829
|
+
lines.push(` <h1 className="text-2xl font-bold mb-2">Verification Code</h1>`);
|
|
830
|
+
lines.push(` <p className="text-gray-500 mb-8 text-sm">Enter the 6-digit code sent to {phone}</p>`);
|
|
831
|
+
lines.push(` <div className="flex justify-center gap-2 mb-6">`);
|
|
832
|
+
lines.push(` {otp.map((digit, i) => (`);
|
|
833
|
+
lines.push(` <input key={i} ref={inputRefs[i]} type="text" inputMode="numeric"`);
|
|
834
|
+
lines.push(` value={digit} onChange={(e) => handleChange(i, e.target.value)} maxLength={1}`);
|
|
835
|
+
lines.push(` className="w-12 h-14 text-center text-xl font-bold border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-black" />`);
|
|
836
|
+
lines.push(` ))}`);
|
|
837
|
+
lines.push(` </div>`);
|
|
838
|
+
lines.push(` {error && <p className="text-sm text-red-500 mb-4">{error}</p>}`);
|
|
839
|
+
lines.push(` <button onClick={() => verify(otp.join(''))} disabled={loading || otp.some((d) => !d)}`);
|
|
840
|
+
lines.push(` className="w-full py-2.5 bg-black text-white rounded-lg text-sm font-medium hover:bg-gray-800 disabled:opacity-40 transition">`);
|
|
841
|
+
lines.push(` {loading ? 'Verifying...' : 'Verify'}`);
|
|
842
|
+
lines.push(` </button>`);
|
|
843
|
+
lines.push(` </div>`);
|
|
844
|
+
lines.push(` </div>`);
|
|
845
|
+
lines.push(` )`);
|
|
846
|
+
lines.push(`}`);
|
|
847
|
+
break;
|
|
848
|
+
case 'custom':
|
|
849
|
+
default:
|
|
850
|
+
lines.push(`export default function CustomPage() {`);
|
|
851
|
+
lines.push(` return (`);
|
|
852
|
+
lines.push(` <div className="container mx-auto px-4 py-8">`);
|
|
853
|
+
lines.push(` <h1 className="text-2xl font-bold mb-4">${page.name}</h1>`);
|
|
854
|
+
lines.push(` <div className="bg-white border border-gray-200 rounded-xl p-8 text-center text-gray-400">`);
|
|
855
|
+
lines.push(` <p className="text-4xl mb-4">⚡</p>`);
|
|
856
|
+
lines.push(` <p className="font-medium">Custom Page</p>`);
|
|
857
|
+
lines.push(` <p className="text-sm mt-1">Build your custom UI here.</p>`);
|
|
858
|
+
lines.push(` </div>`);
|
|
859
|
+
lines.push(` </div>`);
|
|
860
|
+
lines.push(` )`);
|
|
861
|
+
lines.push(`}`);
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
return {
|
|
865
|
+
path: `app/(presentation)/${routePath}/page.tsx`,
|
|
866
|
+
content: lines.join('\n'),
|
|
867
|
+
category: 'customizable',
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
//# sourceMappingURL=page-remaining.js.map
|