mirror of
https://github.com/OwethuManagedServices/oms-website-nextjs.git
synced 2025-12-17 17:18:09 +00:00
feature: create post
This commit is contained in:
148
actions/contact.ts
Normal file
148
actions/contact.ts
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
"use server";
|
||||||
|
|
||||||
|
import { z } from "zod";
|
||||||
|
import nodemailer from "nodemailer";
|
||||||
|
import { GoogleGenerativeAI } from "@google/generative-ai";
|
||||||
|
|
||||||
|
// Define the schema for contact form validation
|
||||||
|
const ContactSchema = z.object({
|
||||||
|
name: z.string().min(2, "Name must be at least 2 characters"),
|
||||||
|
email: z.string().email("Invalid email address"),
|
||||||
|
subject: z.string().min(5, "Subject must be at least 5 characters"),
|
||||||
|
message: z.string().min(10, "Message must be at least 10 characters"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// Define the state structure for the form action
|
||||||
|
export type ContactFormState = {
|
||||||
|
errors?: {
|
||||||
|
name?: string[];
|
||||||
|
email?: string[];
|
||||||
|
subject?: string[];
|
||||||
|
message?: string[];
|
||||||
|
_form?: string[]; // General form error
|
||||||
|
};
|
||||||
|
message?: string | null; // Success or general error message
|
||||||
|
success?: boolean; // Flag for successful submission
|
||||||
|
};
|
||||||
|
|
||||||
|
// --- Initialize Gemini ---
|
||||||
|
const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY || "");
|
||||||
|
const geminiModel = genAI.getGenerativeModel({ model: "gemini-2.0-flash" }); // Or other suitable model
|
||||||
|
|
||||||
|
// --- Initialize Nodemailer Transporter ---
|
||||||
|
const transporter = nodemailer.createTransport({
|
||||||
|
host: process.env.EMAIL_SERVER_HOST,
|
||||||
|
port: parseInt(process.env.EMAIL_SERVER_PORT || "587"), // Default to 587
|
||||||
|
secure: parseInt(process.env.EMAIL_SERVER_PORT || "587") === 465, // true for 465, false for other ports
|
||||||
|
auth: {
|
||||||
|
user: process.env.EMAIL_SERVER_USER,
|
||||||
|
pass: process.env.EMAIL_SERVER_PASSWORD,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// --- Helper function for Spam Check ---
|
||||||
|
async function isSpamOrAdvertisement(content: {
|
||||||
|
subject: string;
|
||||||
|
message: string;
|
||||||
|
email: string;
|
||||||
|
}): Promise<boolean> {
|
||||||
|
if (!process.env.GEMINI_API_KEY) {
|
||||||
|
console.warn("GEMINI_API_KEY not set. Skipping spam check.");
|
||||||
|
return false; // Skip check if API key is missing
|
||||||
|
}
|
||||||
|
|
||||||
|
const prompt = `Analyze the following email content and classify it as "spam", "advertisement", or "legitimate inquiry". Consider the subject, message body, and sender email. Provide only the classification word as the response.
|
||||||
|
|
||||||
|
Subject: ${content.subject}
|
||||||
|
Sender Email: ${content.email}
|
||||||
|
Message:
|
||||||
|
${content.message}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await geminiModel.generateContent(prompt);
|
||||||
|
const response = await result.response;
|
||||||
|
const text = response.text().trim().toLowerCase();
|
||||||
|
console.log("Gemini Classification:", text); // Log classification for debugging
|
||||||
|
// Consider "spam" or "advertisement" as unwanted
|
||||||
|
return text === "spam" || text === "advertisement";
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error checking content with Gemini:", error);
|
||||||
|
// Fail open (treat as not spam) if Gemini check fails
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server action to process the contact form
|
||||||
|
export async function submitContactForm(
|
||||||
|
prevState: ContactFormState,
|
||||||
|
formData: FormData
|
||||||
|
): Promise<ContactFormState> {
|
||||||
|
// Validate form data
|
||||||
|
const validatedFields = ContactSchema.safeParse({
|
||||||
|
name: formData.get("name"),
|
||||||
|
email: formData.get("email"),
|
||||||
|
subject: formData.get("subject"),
|
||||||
|
message: formData.get("message"),
|
||||||
|
});
|
||||||
|
|
||||||
|
// If validation fails, return errors
|
||||||
|
if (!validatedFields.success) {
|
||||||
|
console.error(
|
||||||
|
"Contact Form Validation Errors:",
|
||||||
|
validatedFields.error.flatten().fieldErrors
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
errors: validatedFields.error.flatten().fieldErrors,
|
||||||
|
message: "Please correct the errors above.",
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const { name, email, subject, message } = validatedFields.data;
|
||||||
|
|
||||||
|
// --- Spam/Advertisement Check ---
|
||||||
|
try {
|
||||||
|
const isUnwanted = await isSpamOrAdvertisement({ subject, message, email });
|
||||||
|
if (isUnwanted) {
|
||||||
|
console.log(`Message from ${email} flagged as spam/advertisement.`);
|
||||||
|
// Return generic error as requested
|
||||||
|
return { message: "Message could not be sent.", success: false };
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Log error but proceed if check fails unexpectedly
|
||||||
|
console.error("Error during spam check:", error);
|
||||||
|
}
|
||||||
|
// --- End Spam Check ---
|
||||||
|
|
||||||
|
// --- Send Email using Nodemailer ---
|
||||||
|
const mailOptions = {
|
||||||
|
from: process.env.EMAIL_FROM, // Sender address (configured in .env)
|
||||||
|
to: process.env.EMAIL_TO, // List of receivers (configured in .env)
|
||||||
|
replyTo: email, // Set reply-to to the user's email
|
||||||
|
subject: `Website Contact: ${subject}`, // Subject line
|
||||||
|
text: `Name: ${name}\nEmail: ${email}\n\nMessage:\n${message}`, // Plain text body
|
||||||
|
html: `<p><strong>Name:</strong> ${name}</p>
|
||||||
|
<p><strong>Email:</strong> <a href="mailto:${email}">${email}</a></p>
|
||||||
|
<hr>
|
||||||
|
<p><strong>Message:</strong></p>
|
||||||
|
<p>${message.replace(/\n/g, "<br>")}</p>`, // HTML body
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
await transporter.sendMail(mailOptions);
|
||||||
|
console.log("Contact email sent successfully from:", email);
|
||||||
|
return {
|
||||||
|
message: "Thank you for your message! We'll get back to you soon.",
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to send contact email:", error);
|
||||||
|
return {
|
||||||
|
message:
|
||||||
|
"Failed to send message due to a server error. Please try again later.",
|
||||||
|
success: false,
|
||||||
|
errors: { _form: ["Email sending failed."] },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
// --- End Send Email ---
|
||||||
|
}
|
||||||
@ -48,16 +48,16 @@ const PostSchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type CreatePostState = {
|
export type CreatePostState = {
|
||||||
errors?: {
|
message: string | null;
|
||||||
|
errors: {
|
||||||
title?: string[];
|
title?: string[];
|
||||||
slug?: string[];
|
slug?: string[];
|
||||||
content?: string[];
|
content?: string[];
|
||||||
image?: string[]; // Changed from imageUrl
|
image?: string[];
|
||||||
tags?: string[];
|
tags?: string[];
|
||||||
|
excerpt?: string[];
|
||||||
_form?: string[];
|
_form?: string[];
|
||||||
};
|
};
|
||||||
message?: string | null;
|
|
||||||
// Add a field to hold previous input values
|
|
||||||
previousInput?: {
|
previousInput?: {
|
||||||
title?: string;
|
title?: string;
|
||||||
slug?: string;
|
slug?: string;
|
||||||
@ -65,7 +65,6 @@ export type CreatePostState = {
|
|||||||
excerpt?: string;
|
excerpt?: string;
|
||||||
tags?: string;
|
tags?: string;
|
||||||
published?: boolean;
|
published?: boolean;
|
||||||
// We don't repopulate the file input for security reasons
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
151
app/(website)/contact/page.tsx
Normal file
151
app/(website)/contact/page.tsx
Normal file
@ -0,0 +1,151 @@
|
|||||||
|
import React from "react";
|
||||||
|
import type { Metadata } from "next";
|
||||||
|
import ContactForm from "@/components/ContactForm";
|
||||||
|
import { FiMapPin, FiPhone, FiMail, FiClock } from "react-icons/fi"; // Import icons
|
||||||
|
|
||||||
|
// SEO Metadata for the Contact page
|
||||||
|
export const metadata: Metadata = {
|
||||||
|
title: "Contact Us | Owethu Managed Services (OMS)",
|
||||||
|
description:
|
||||||
|
"Get in touch with Owethu Managed Services. Contact us for IT solutions, resource augmentation, project management, and custom software development inquiries.",
|
||||||
|
alternates: {
|
||||||
|
canonical: "/contact",
|
||||||
|
},
|
||||||
|
openGraph: {
|
||||||
|
title: "Contact OMS",
|
||||||
|
description: "Reach out to OMS for expert IT services and solutions.",
|
||||||
|
url: "https://oms.africa/contact", // Replace with your actual domain
|
||||||
|
images: [
|
||||||
|
{
|
||||||
|
url: "/og-image-contact.jpg", // Create a specific OG image for contact
|
||||||
|
width: 1200,
|
||||||
|
height: 630,
|
||||||
|
alt: "Contact Owethu Managed Services",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
twitter: {
|
||||||
|
card: "summary_large_image",
|
||||||
|
title: "Contact OMS",
|
||||||
|
description: "Get in touch with Owethu Managed Services.",
|
||||||
|
images: ["/og-image-contact.jpg"],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Contact Page Component
|
||||||
|
export default function ContactPage() {
|
||||||
|
return (
|
||||||
|
<div className="bg-gradient-to-b from-background to-secondary/50 text-foreground">
|
||||||
|
{/* Hero Section */}
|
||||||
|
<section className="py-20 md:py-28 text-center bg-primary/10 dark:bg-primary/5 border-b border-border">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<h1 className="text-4xl md:text-5xl font-bold mb-4 text-primary">
|
||||||
|
Get In Touch
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg md:text-xl text-muted-foreground max-w-2xl mx-auto">
|
||||||
|
We're here to help! Whether you have a question about our
|
||||||
|
services, need assistance, or want to discuss a project, reach out
|
||||||
|
and let us know.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Main Content Area (Form + Info) */}
|
||||||
|
<section className="py-16 md:py-24">
|
||||||
|
<div className="container mx-auto px-4">
|
||||||
|
<div className="grid grid-cols-1 lg:grid-cols-2 gap-12 md:gap-16 lg:gap-20 items-start">
|
||||||
|
{/* Contact Information Section */}
|
||||||
|
<div className="space-y-8 bg-card p-8 rounded-lg border border-border shadow-sm">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 text-foreground">
|
||||||
|
Contact Information
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<FiMapPin className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-foreground">
|
||||||
|
Our Office
|
||||||
|
</h3>
|
||||||
|
<address className="text-muted-foreground not-italic text-sm leading-relaxed">
|
||||||
|
Unit 10 B Centuria Park
|
||||||
|
<br />
|
||||||
|
265 Von Willich Avenue
|
||||||
|
<br />
|
||||||
|
Die Hoewes, Centurion, 0159
|
||||||
|
<br />
|
||||||
|
South Africa
|
||||||
|
</address>
|
||||||
|
{/* Optional: Link to Google Maps */}
|
||||||
|
<a
|
||||||
|
href="https://www.google.com/maps/place/Owethu+Managed+Services/@-25.863168,28.186075,17z/data=!3m1!4b1!4m6!3m5!1s0x1e9565b7018f5f9b:0x4d8a4ae3a2c0d9a1!8m2!3d-25.8631728!4d28.1886499!16s%2Fg%2F11h1_q_1_f?entry=ttu" // Replace with your actual Google Maps link
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
className="text-sm text-primary hover:underline mt-2 inline-block"
|
||||||
|
>
|
||||||
|
View on Google Maps
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<FiPhone className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-foreground">Phone</h3>
|
||||||
|
<a
|
||||||
|
href="tel:+27120513282"
|
||||||
|
className="text-muted-foreground hover:text-primary transition text-sm"
|
||||||
|
>
|
||||||
|
(012) 051 3282
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<FiMail className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-foreground">Email</h3>
|
||||||
|
<a
|
||||||
|
href="mailto:hello@oms.africa"
|
||||||
|
className="text-muted-foreground hover:text-primary transition text-sm"
|
||||||
|
>
|
||||||
|
hello@oms.africa
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-start space-x-4">
|
||||||
|
<FiClock className="w-6 h-6 text-primary mt-1 flex-shrink-0" />
|
||||||
|
<div>
|
||||||
|
<h3 className="text-lg font-medium text-foreground">
|
||||||
|
Business Hours
|
||||||
|
</h3>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Monday - Friday: 8:00 AM - 5:00 PM (SAST)
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Weekends & Public Holidays: Closed
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Contact Form Section */}
|
||||||
|
<div className="bg-card p-8 rounded-lg border border-border shadow-sm">
|
||||||
|
<h2 className="text-3xl font-semibold mb-6 text-foreground">
|
||||||
|
Send Us a Message
|
||||||
|
</h2>
|
||||||
|
<ContactForm />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Optional: Map Section Placeholder */}
|
||||||
|
{/* <section className="h-96 bg-muted border-t border-border">
|
||||||
|
<div className="container mx-auto h-full flex items-center justify-center">
|
||||||
|
<p className="text-muted-foreground">[Embedded Map Placeholder - e.g., Google Maps iframe]</p>
|
||||||
|
</div>
|
||||||
|
</section> */}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
160
components/ContactForm.tsx
Normal file
160
components/ContactForm.tsx
Normal file
@ -0,0 +1,160 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import React, { useActionState, useEffect, useRef } from "react";
|
||||||
|
import { useFormStatus } from "react-dom";
|
||||||
|
import { submitContactForm, ContactFormState } from "@/actions/contact";
|
||||||
|
import Button from "@/components/ui/Button"; // Use your existing Button component
|
||||||
|
|
||||||
|
// Submit button component with pending state
|
||||||
|
function SubmitButton() {
|
||||||
|
const { pending } = useFormStatus();
|
||||||
|
return (
|
||||||
|
<Button type="submit" variant="primary" size="lg" disabled={pending}>
|
||||||
|
{pending ? "Sending..." : "Send Message"}
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The main contact form component
|
||||||
|
export default function ContactForm() {
|
||||||
|
const initialState: ContactFormState = {
|
||||||
|
message: null,
|
||||||
|
errors: {},
|
||||||
|
success: false,
|
||||||
|
};
|
||||||
|
const [state, dispatch] = useActionState(submitContactForm, initialState);
|
||||||
|
const formRef = useRef<HTMLFormElement>(null); // Ref to reset the form
|
||||||
|
|
||||||
|
// Reset form on successful submission
|
||||||
|
useEffect(() => {
|
||||||
|
if (state.success) {
|
||||||
|
formRef.current?.reset();
|
||||||
|
}
|
||||||
|
}, [state.success]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form ref={formRef} action={dispatch} className="space-y-6">
|
||||||
|
{/* Name Input */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block text-sm font-medium text-foreground mb-1"
|
||||||
|
>
|
||||||
|
Full Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="name"
|
||||||
|
name="name"
|
||||||
|
required
|
||||||
|
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
||||||
|
aria-describedby="name-error"
|
||||||
|
/>
|
||||||
|
<div id="name-error" aria-live="polite" aria-atomic="true">
|
||||||
|
{state.errors?.name &&
|
||||||
|
state.errors.name.map((error: string) => (
|
||||||
|
<p className="mt-1 text-sm text-destructive" key={error}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Email Input */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="email"
|
||||||
|
className="block text-sm font-medium text-foreground mb-1"
|
||||||
|
>
|
||||||
|
Email Address
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="email"
|
||||||
|
id="email"
|
||||||
|
name="email"
|
||||||
|
required
|
||||||
|
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
||||||
|
aria-describedby="email-error"
|
||||||
|
/>
|
||||||
|
<div id="email-error" aria-live="polite" aria-atomic="true">
|
||||||
|
{state.errors?.email &&
|
||||||
|
state.errors.email.map((error: string) => (
|
||||||
|
<p className="mt-1 text-sm text-destructive" key={error}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Subject Input */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="subject"
|
||||||
|
className="block text-sm font-medium text-foreground mb-1"
|
||||||
|
>
|
||||||
|
Subject
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
id="subject"
|
||||||
|
name="subject"
|
||||||
|
required
|
||||||
|
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
||||||
|
aria-describedby="subject-error"
|
||||||
|
/>
|
||||||
|
<div id="subject-error" aria-live="polite" aria-atomic="true">
|
||||||
|
{state.errors?.subject &&
|
||||||
|
state.errors.subject.map((error: string) => (
|
||||||
|
<p className="mt-1 text-sm text-destructive" key={error}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Message Textarea */}
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="message"
|
||||||
|
className="block text-sm font-medium text-foreground mb-1"
|
||||||
|
>
|
||||||
|
Your Message
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
id="message"
|
||||||
|
name="message"
|
||||||
|
rows={5}
|
||||||
|
required
|
||||||
|
className="block w-full px-4 py-2 border border-border rounded-lg shadow-sm focus:ring-primary focus:border-primary sm:text-sm bg-input text-foreground placeholder-muted-foreground"
|
||||||
|
aria-describedby="message-error"
|
||||||
|
></textarea>
|
||||||
|
<div id="message-error" aria-live="polite" aria-atomic="true">
|
||||||
|
{state.errors?.message &&
|
||||||
|
state.errors.message.map((error: string) => (
|
||||||
|
<p className="mt-1 text-sm text-destructive" key={error}>
|
||||||
|
{error}
|
||||||
|
</p>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* General Form Message (Success or Error) */}
|
||||||
|
<div id="form-response" aria-live="polite" aria-atomic="true">
|
||||||
|
{state.message && (
|
||||||
|
<p
|
||||||
|
className={`text-sm ${
|
||||||
|
state.success ? "text-green-600" : "text-destructive"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{state.message}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Submit Button */}
|
||||||
|
<div className="pt-2">
|
||||||
|
<SubmitButton />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -20,10 +20,18 @@ export default function CreatePostForm() {
|
|||||||
// Use useActionState from React
|
// Use useActionState from React
|
||||||
const [state, dispatch] = useActionState(createPost, initialState);
|
const [state, dispatch] = useActionState(createPost, initialState);
|
||||||
|
|
||||||
// Helper to get default value or empty string
|
// Helper to get default value or empty string, ensuring string return type
|
||||||
const getPreviousInput = (
|
const getPreviousInput = (
|
||||||
key: keyof NonNullable<CreatePostState["previousInput"]>
|
key: keyof NonNullable<CreatePostState["previousInput"]>
|
||||||
) => state.previousInput?.[key] ?? "";
|
): string => {
|
||||||
|
const value = state.previousInput?.[key];
|
||||||
|
// Ensure the return value is always a string for text/textarea defaultValue
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
// Return empty string for non-string types (like boolean) or null/undefined
|
||||||
|
return "";
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
// Remove encType="multipart/form-data"
|
// Remove encType="multipart/form-data"
|
||||||
|
|||||||
@ -2,14 +2,20 @@ import type { NextConfig } from "next";
|
|||||||
|
|
||||||
const nextConfig: NextConfig = {
|
const nextConfig: NextConfig = {
|
||||||
images: {
|
images: {
|
||||||
domains: ["avatars.githubusercontent.com", "storage.cvevolve.com"],
|
remotePatterns: [
|
||||||
},
|
{
|
||||||
serverActions: {
|
protocol: "https",
|
||||||
bodySizeLimit: "10mb", // Increase limit (e.g., to 10MB) - adjust as needed
|
hostname: "avatars.githubusercontent.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
protocol: "https",
|
||||||
|
hostname: "storage.cvevolve.com",
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
serverActions: {
|
serverActions: {
|
||||||
bodySizeLimit: "2mb",
|
bodySizeLimit: "5mb",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
31
package-lock.json
generated
31
package-lock.json
generated
@ -9,11 +9,13 @@
|
|||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.9.0",
|
"@auth/prisma-adapter": "^2.9.0",
|
||||||
|
"@google/generative-ai": "^0.24.0",
|
||||||
"@prisma/client": "^6.6.0",
|
"@prisma/client": "^6.6.0",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
"next-auth": "^5.0.0-beta.26",
|
"next-auth": "^5.0.0-beta.26",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"nodemailer": "^6.10.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
@ -26,6 +28,7 @@
|
|||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
@ -1239,6 +1242,15 @@
|
|||||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@google/generative-ai": {
|
||||||
|
"version": "0.24.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.0.tgz",
|
||||||
|
"integrity": "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q==",
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@humanfs/core": {
|
"node_modules/@humanfs/core": {
|
||||||
"version": "0.19.1",
|
"version": "0.19.1",
|
||||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||||
@ -2970,6 +2982,16 @@
|
|||||||
"undici-types": "~6.19.2"
|
"undici-types": "~6.19.2"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/nodemailer": {
|
||||||
|
"version": "6.4.17",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz",
|
||||||
|
"integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@types/node": "*"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@types/react": {
|
"node_modules/@types/react": {
|
||||||
"version": "19.1.2",
|
"version": "19.1.2",
|
||||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
|
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.2.tgz",
|
||||||
@ -8133,6 +8155,15 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true
|
"peer": true
|
||||||
},
|
},
|
||||||
|
"node_modules/nodemailer": {
|
||||||
|
"version": "6.10.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.10.1.tgz",
|
||||||
|
"integrity": "sha512-Z+iLaBGVaSjbIzQ4pX6XV41HrooLsQ10ZWPUehGmuantvzWoDVBnmsdUcOIDM1t+yPor5pDhVlDESgOMEGxhHA==",
|
||||||
|
"license": "MIT-0",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=6.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/normalize-path": {
|
"node_modules/normalize-path": {
|
||||||
"version": "3.0.0",
|
"version": "3.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||||
|
|||||||
@ -10,11 +10,13 @@
|
|||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@auth/prisma-adapter": "^2.9.0",
|
"@auth/prisma-adapter": "^2.9.0",
|
||||||
|
"@google/generative-ai": "^0.24.0",
|
||||||
"@prisma/client": "^6.6.0",
|
"@prisma/client": "^6.6.0",
|
||||||
"minio": "^8.0.5",
|
"minio": "^8.0.5",
|
||||||
"next": "15.3.1",
|
"next": "15.3.1",
|
||||||
"next-auth": "^5.0.0-beta.26",
|
"next-auth": "^5.0.0-beta.26",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
|
"nodemailer": "^6.10.1",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-icons": "^5.5.0",
|
"react-icons": "^5.5.0",
|
||||||
@ -27,6 +29,7 @@
|
|||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
"@tailwindcss/postcss": "^4",
|
"@tailwindcss/postcss": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
|
"@types/nodemailer": "^6.4.17",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
|
|||||||
Reference in New Issue
Block a user