create-landing-app 0.2.5 → 0.2.6

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/prompts.js CHANGED
@@ -10,6 +10,9 @@ export async function runPrompts() {
10
10
  const blog = await confirm({ message: "Include Blog section?" });
11
11
  if (isCancel(blog))
12
12
  return null;
13
+ const contact = await confirm({ message: "Include Contact section? (form + API route)" });
14
+ if (isCancel(contact))
15
+ return null;
13
16
  const docker = await confirm({ message: "Include Docker setup? (for VPS deploy)" });
14
17
  if (isCancel(docker))
15
18
  return null;
@@ -20,6 +23,7 @@ export async function runPrompts() {
20
23
  stateManagement: "zustand",
21
24
  dataFetching: "tanstack-query",
22
25
  blog: Boolean(blog),
26
+ contact: Boolean(contact),
23
27
  docker: Boolean(docker),
24
28
  };
25
29
  }
package/dist/scaffold.js CHANGED
@@ -30,6 +30,8 @@ export async function scaffold(config, targetDir) {
30
30
  optionals.push("sections/about");
31
31
  if (config.blog)
32
32
  optionals.push("sections/blog");
33
+ if (config.contact)
34
+ optionals.push("sections/contact");
33
35
  if (config.docker)
34
36
  optionals.push("docker");
35
37
  // 3. Apply each optional feature (files + inject markers)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-landing-app",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "description": "Create a production-ready Next.js landing page with one command",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,24 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { contactFormSchema } from "@/components/sections/contact-form-schema";
3
+
4
+ export async function POST(req: NextRequest) {
5
+ const body = await req.json();
6
+ const parsed = contactFormSchema.safeParse(body);
7
+
8
+ if (!parsed.success) {
9
+ return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
10
+ }
11
+
12
+ // TODO: connect your email service (Resend recommended)
13
+ // import { Resend } from "resend";
14
+ // const resend = new Resend(process.env.RESEND_API_KEY);
15
+ // await resend.emails.send({
16
+ // from: process.env.RESEND_FROM_EMAIL!,
17
+ // to: process.env.RESEND_TO_EMAIL!,
18
+ // subject: `New contact from ${parsed.data.name}`,
19
+ // text: `Phone: ${parsed.data.phone}\nEmail: ${parsed.data.email}\nMessage: ${parsed.data.message ?? ""}`,
20
+ // });
21
+
22
+ console.log("Contact submission:", parsed.data);
23
+ return NextResponse.json({ ok: true });
24
+ }
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+
3
+ export const contactFormSchema = z.object({
4
+ name: z.string().trim().min(1, "Name is required").max(100),
5
+ phone: z.string().trim().regex(/^\+?[\d\s\-().]{7,20}$/, "Invalid phone number"),
6
+ email: z.string().trim().email("Invalid email address"),
7
+ message: z.string().trim().optional(),
8
+ });
9
+
10
+ export type ContactFormValues = z.infer<typeof contactFormSchema>;
11
+
12
+ export const DEFAULT_VALUES: ContactFormValues = {
13
+ name: "", phone: "", email: "", message: "",
14
+ };
@@ -0,0 +1,74 @@
1
+ "use client";
2
+ import { useForm } from "react-hook-form";
3
+ import { zodResolver } from "@hookform/resolvers/zod";
4
+ import { Button } from "@/components/ui/button";
5
+ import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage } from "@/components/ui/form";
6
+ import { contactFormSchema, type ContactFormValues, DEFAULT_VALUES } from "./contact-form-schema";
7
+ import { useContactSubmit } from "./use-contact-submit";
8
+
9
+ export function ContactForm() {
10
+ const form = useForm<ContactFormValues>({
11
+ resolver: zodResolver(contactFormSchema),
12
+ defaultValues: DEFAULT_VALUES,
13
+ });
14
+ const { isSubmitting, onSubmit } = useContactSubmit();
15
+
16
+ async function handleSubmit(data: ContactFormValues) {
17
+ const ok = await onSubmit(data);
18
+ if (ok) form.reset();
19
+ }
20
+
21
+ return (
22
+ <Form {...form}>
23
+ <form onSubmit={form.handleSubmit(handleSubmit)} className="flex flex-col gap-4">
24
+ <FormField control={form.control} name="name" render={({ field }) => (
25
+ <FormItem>
26
+ <FormLabel>Name *</FormLabel>
27
+ <FormControl>
28
+ <input {...field} placeholder="Your name"
29
+ className="h-12 w-full rounded-lg border border-border bg-background px-4 focus:outline-none focus:ring-2 focus:ring-ring" />
30
+ </FormControl>
31
+ <FormMessage />
32
+ </FormItem>
33
+ )} />
34
+
35
+ <FormField control={form.control} name="phone" render={({ field }) => (
36
+ <FormItem>
37
+ <FormLabel>Phone *</FormLabel>
38
+ <FormControl>
39
+ <input {...field} type="tel" placeholder="+84 xxx xxx xxx"
40
+ className="h-12 w-full rounded-lg border border-border bg-background px-4 focus:outline-none focus:ring-2 focus:ring-ring" />
41
+ </FormControl>
42
+ <FormMessage />
43
+ </FormItem>
44
+ )} />
45
+
46
+ <FormField control={form.control} name="email" render={({ field }) => (
47
+ <FormItem>
48
+ <FormLabel>Email *</FormLabel>
49
+ <FormControl>
50
+ <input {...field} type="email" placeholder="you@example.com"
51
+ className="h-12 w-full rounded-lg border border-border bg-background px-4 focus:outline-none focus:ring-2 focus:ring-ring" />
52
+ </FormControl>
53
+ <FormMessage />
54
+ </FormItem>
55
+ )} />
56
+
57
+ <FormField control={form.control} name="message" render={({ field }) => (
58
+ <FormItem>
59
+ <FormLabel>Message</FormLabel>
60
+ <FormControl>
61
+ <textarea {...field} rows={4} placeholder="How can we help?"
62
+ className="w-full resize-none rounded-lg border border-border bg-background px-4 py-3 focus:outline-none focus:ring-2 focus:ring-ring" />
63
+ </FormControl>
64
+ <FormMessage />
65
+ </FormItem>
66
+ )} />
67
+
68
+ <Button type="submit" className="w-full" disabled={isSubmitting}>
69
+ {isSubmitting ? "Sending..." : "Send Message"}
70
+ </Button>
71
+ </form>
72
+ </Form>
73
+ );
74
+ }
@@ -1,34 +1,7 @@
1
- "use client";
2
- import { useState } from "react";
3
1
  import { motion } from "motion/react";
4
- import { Button } from "@/components/ui/button";
5
- import { toast } from "sonner";
6
-
7
- interface FormData {
8
- name: string;
9
- email: string;
10
- message: string;
11
- }
2
+ import { ContactForm } from "./contact-form";
12
3
 
13
4
  export default function ContactSection() {
14
- const [form, setForm] = useState<FormData>({ name: "", email: "", message: "" });
15
- const [submitting, setSubmitting] = useState(false);
16
-
17
- const handleSubmit = async (e: React.FormEvent) => {
18
- e.preventDefault();
19
- setSubmitting(true);
20
- try {
21
- // Replace with your actual form submission logic
22
- await new Promise((r) => setTimeout(r, 1000));
23
- toast.success("Message sent! We'll get back to you soon.");
24
- setForm({ name: "", email: "", message: "" });
25
- } catch {
26
- toast.error("Failed to send. Please try again.");
27
- } finally {
28
- setSubmitting(false);
29
- }
30
- };
31
-
32
5
  return (
33
6
  <section id="contact" className="bg-secondary/30 py-24">
34
7
  <div className="content-container max-w-2xl">
@@ -43,35 +16,7 @@ export default function ContactSection() {
43
16
  <h2 className="text-4xl font-bold">Get in touch</h2>
44
17
  <p className="text-lg text-muted-foreground">We&apos;d love to hear from you.</p>
45
18
  </div>
46
- <form onSubmit={handleSubmit} className="space-y-4">
47
- <input
48
- type="text"
49
- placeholder="Your name"
50
- required
51
- value={form.name}
52
- onChange={(e) => setForm({ ...form, name: e.target.value })}
53
- className="w-full rounded-lg border border-border bg-background px-4 py-3 focus:outline-none focus:ring-2 focus:ring-ring"
54
- />
55
- <input
56
- type="email"
57
- placeholder="Email address"
58
- required
59
- value={form.email}
60
- onChange={(e) => setForm({ ...form, email: e.target.value })}
61
- className="w-full rounded-lg border border-border bg-background px-4 py-3 focus:outline-none focus:ring-2 focus:ring-ring"
62
- />
63
- <textarea
64
- placeholder="Your message"
65
- required
66
- rows={5}
67
- value={form.message}
68
- onChange={(e) => setForm({ ...form, message: e.target.value })}
69
- className="w-full resize-none rounded-lg border border-border bg-background px-4 py-3 focus:outline-none focus:ring-2 focus:ring-ring"
70
- />
71
- <Button type="submit" className="w-full" disabled={submitting}>
72
- {submitting ? "Sending..." : "Send message"}
73
- </Button>
74
- </form>
19
+ <ContactForm />
75
20
  </motion.div>
76
21
  </div>
77
22
  </section>
@@ -0,0 +1,25 @@
1
+ "use client";
2
+ import { useState } from "react";
3
+ import { toast } from "sonner";
4
+ import { sendContact } from "@/lib/apis/contact";
5
+ import type { ContactFormValues } from "./contact-form-schema";
6
+
7
+ export function useContactSubmit() {
8
+ const [isSubmitting, setIsSubmitting] = useState(false);
9
+
10
+ async function onSubmit(data: ContactFormValues) {
11
+ setIsSubmitting(true);
12
+ try {
13
+ await sendContact(data);
14
+ toast.success("Message sent successfully!");
15
+ return true;
16
+ } catch {
17
+ toast.error("Failed to send. Please try again.");
18
+ return false;
19
+ } finally {
20
+ setIsSubmitting(false);
21
+ }
22
+ }
23
+
24
+ return { isSubmitting, onSubmit };
25
+ }
@@ -0,0 +1,127 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import * as LabelPrimitive from "@radix-ui/react-label";
4
+ import { Slot } from "@radix-ui/react-slot";
5
+ import {
6
+ Controller,
7
+ type ControllerProps,
8
+ type FieldPath,
9
+ type FieldValues,
10
+ FormProvider,
11
+ useFormContext,
12
+ } from "react-hook-form";
13
+ import { cn } from "@/lib/utils";
14
+ import { Label } from "@/components/ui/label";
15
+
16
+ const Form = FormProvider;
17
+
18
+ type FormFieldContextValue<
19
+ TFieldValues extends FieldValues = FieldValues,
20
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
21
+ > = { name: TName };
22
+
23
+ const FormFieldContext = React.createContext<FormFieldContextValue>({} as FormFieldContextValue);
24
+
25
+ const FormField = <
26
+ TFieldValues extends FieldValues = FieldValues,
27
+ TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
28
+ >({
29
+ ...props
30
+ }: ControllerProps<TFieldValues, TName>) => (
31
+ <FormFieldContext.Provider value={{ name: props.name }}>
32
+ <Controller {...props} />
33
+ </FormFieldContext.Provider>
34
+ );
35
+
36
+ type FormItemContextValue = { id: string };
37
+ const FormItemContext = React.createContext<FormItemContextValue>({} as FormItemContextValue);
38
+
39
+ const useFormField = () => {
40
+ const fieldContext = React.useContext(FormFieldContext);
41
+ const itemContext = React.useContext(FormItemContext);
42
+ const { getFieldState, formState } = useFormContext();
43
+ const fieldState = getFieldState(fieldContext.name, formState);
44
+ if (!fieldContext) throw new Error("useFormField must be used within <FormField>");
45
+ const { id } = itemContext;
46
+ return {
47
+ id,
48
+ name: fieldContext.name,
49
+ formItemId: `${id}-form-item`,
50
+ formDescriptionId: `${id}-form-item-description`,
51
+ formMessageId: `${id}-form-item-message`,
52
+ ...fieldState,
53
+ };
54
+ };
55
+
56
+ const FormItem = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
57
+ ({ className, ...props }, ref) => {
58
+ const id = React.useId();
59
+ return (
60
+ <FormItemContext.Provider value={{ id }}>
61
+ <div ref={ref} className={cn("space-y-2", className)} {...props} />
62
+ </FormItemContext.Provider>
63
+ );
64
+ }
65
+ );
66
+ FormItem.displayName = "FormItem";
67
+
68
+ const FormLabel = React.forwardRef<
69
+ React.ElementRef<typeof LabelPrimitive.Root>,
70
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root>
71
+ >(({ className, ...props }, ref) => {
72
+ const { error, formItemId } = useFormField();
73
+ return (
74
+ <Label
75
+ ref={ref}
76
+ className={cn(error && "text-destructive", className)}
77
+ htmlFor={formItemId}
78
+ {...props}
79
+ />
80
+ );
81
+ });
82
+ FormLabel.displayName = "FormLabel";
83
+
84
+ const FormControl = React.forwardRef<
85
+ React.ElementRef<typeof Slot>,
86
+ React.ComponentPropsWithoutRef<typeof Slot>
87
+ >(({ ...props }, ref) => {
88
+ const { error, formItemId, formDescriptionId, formMessageId } = useFormField();
89
+ return (
90
+ <Slot
91
+ ref={ref}
92
+ id={formItemId}
93
+ aria-describedby={!error ? formDescriptionId : `${formDescriptionId} ${formMessageId}`}
94
+ aria-invalid={!!error}
95
+ {...props}
96
+ />
97
+ );
98
+ });
99
+ FormControl.displayName = "FormControl";
100
+
101
+ const FormDescription = React.forwardRef<
102
+ HTMLParagraphElement,
103
+ React.HTMLAttributes<HTMLParagraphElement>
104
+ >(({ className, ...props }, ref) => {
105
+ const { formDescriptionId } = useFormField();
106
+ return (
107
+ <p ref={ref} id={formDescriptionId} className={cn("text-sm text-muted-foreground", className)} {...props} />
108
+ );
109
+ });
110
+ FormDescription.displayName = "FormDescription";
111
+
112
+ const FormMessage = React.forwardRef<
113
+ HTMLParagraphElement,
114
+ React.HTMLAttributes<HTMLParagraphElement>
115
+ >(({ className, children, ...props }, ref) => {
116
+ const { error, formMessageId } = useFormField();
117
+ const body = error ? String(error?.message) : children;
118
+ if (!body) return null;
119
+ return (
120
+ <p ref={ref} id={formMessageId} className={cn("text-sm font-medium text-destructive", className)} {...props}>
121
+ {body}
122
+ </p>
123
+ );
124
+ });
125
+ FormMessage.displayName = "FormMessage";
126
+
127
+ export { useFormField, Form, FormItem, FormLabel, FormControl, FormDescription, FormMessage, FormField };
@@ -0,0 +1,20 @@
1
+ "use client";
2
+ import * as React from "react";
3
+ import * as LabelPrimitive from "@radix-ui/react-label";
4
+ import { cva, type VariantProps } from "class-variance-authority";
5
+ import { cn } from "@/lib/utils";
6
+
7
+ const labelVariants = cva(
8
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
9
+ );
10
+
11
+ const Label = React.forwardRef<
12
+ React.ElementRef<typeof LabelPrimitive.Root>,
13
+ React.ComponentPropsWithoutRef<typeof LabelPrimitive.Root> &
14
+ VariantProps<typeof labelVariants>
15
+ >(({ className, ...props }, ref) => (
16
+ <LabelPrimitive.Root ref={ref} className={cn(labelVariants(), className)} {...props} />
17
+ ));
18
+ Label.displayName = LabelPrimitive.Root.displayName;
19
+
20
+ export { Label };
@@ -0,0 +1,11 @@
1
+ import type { ContactFormValues } from "@/components/sections/contact-form-schema";
2
+
3
+ export async function sendContact(data: ContactFormValues) {
4
+ const res = await fetch("/api/contact", {
5
+ method: "POST",
6
+ headers: { "Content-Type": "application/json" },
7
+ body: JSON.stringify(data),
8
+ });
9
+ if (!res.ok) throw new Error("Contact API error");
10
+ return res.json();
11
+ }
@@ -0,0 +1,8 @@
1
+ {
2
+ "dependencies": {
3
+ "@radix-ui/react-label": "^2.0.0",
4
+ "@hookform/resolvers": "^3.0.0",
5
+ "react-hook-form": "^7.0.0",
6
+ "zod": "^3.0.0"
7
+ }
8
+ }