Multiple forms and Live preview from form inputs on same page.
Unanswered
Yellow croaker posted this in #help-forum
Yellow croakerOP
Hello so I have been making A ResumeBuilder as my project. I have question about having a live preview and forms on same page. There will be option to log in as user, and modify resumes that have been made earlier. But i'm having problems with my Forms and live preview.
How i can manage the server changes and client changes to show the actual live Preview of the CV
How i can manage the server changes and client changes to show the actual live Preview of the CV
'use client';
import React, { useState } from 'react';
import PersonalDetailForm from './forms/PersonalDetailForm';
import WorkExperienceInfo from './forms/WorkExperienceInfo';
import EducationInfo from './forms/EducationInfo';
import CvTemplate from 'app/resumeTemplates/page';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import useSWR from 'swr';
type ResumeMakerProps = {
resume: {
id: string | undefined;
personalInfo:
| {
id: string;
name: string;
address: string | null;
phone: string | null;
email: string | null;
resumeId: string;
}
| null
| undefined;
};
};
export default function ResumeMaker({ resume }: ResumeMakerProps) {
const [openedForm, setOpenedForm] = useState<
null | 'personal' | 'work' | 'education'
>(null);
return (
<div className="grid grid-cols-[1fr,1fr] gap-4 pt-8">
<div>
<h1 className="text-2xl font-bold">Aloita CV tekeminen</h1>
<p className="mb-6">Täytä tietosi</p>
<div className="mb-2">
<Card className="mb-4">
<CardHeader>
<CardTitle>Personal Info</CardTitle>
</CardHeader>
<CardContent>
<div className="mb-2">
<PersonalDetailForm resume={resume} />
</div>
</CardContent>
</Card>
</div>
<div className="mb-2">
<WorkExperienceInfo />
</div>
</div>
<div>
</div>
<div>
<CvTemplate resume={resume} />
</div>
</div>
);
}9 Replies
Yellow croakerOP
This is where resumeMaker is called:
import React from 'react';
import { db } from '@/lib/db';
import { Resume, User } from '@prisma/client';
import { getCurrentUser } from '@/lib/session';
import ResumeMaker from '@/components/ResumeMaker';
async function getResumeForUser(resumeId: Resume['id'], userId: User['id']) {
return await db.resume.findFirst({
where: { id: resumeId, userId },
include: {
personalInfo: true,
},
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const Resume = async (params: any) => {
const user = await getCurrentUser();
if (!user) {
return new Response('Unauthorized', { status: 401 });
}
const account = await db.user.findUnique({
where: { email: user.email ?? '' },
});
if (!account) {
return new Response('Unauthorized', { status: 401 });
}
const resume = await getResumeForUser(params.params.resumeId, account.id);
return (
<div>
<ResumeMaker
resume={{
id: resume?.id,
personalInfo: resume?.personalInfo,
}}
/>
</div>
);
};
export default Resume;And this is the form of personalDetail. I will be replicating somewhat same behavior to other forms, like workexperience, education etc.
import React, { useRef } from 'react';
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form';
import { useUserStore } from '@/lib/zustand'; // Replace 'path-to-your-store' with the actual path to your Zustand store file
import { Input } from '@/components/ui/input';
import { useForm, SubmitHandler } from 'react-hook-form';
import * as z from 'zod';
import { personalSchema } from './ResumePersonalSection';
import { zodResolver } from '@hookform/resolvers/zod';
import { User, Resume } from '@prisma/client';
import { db } from '@/lib/db';
import { getCurrentUser } from '@/lib/session';
import { notFound } from 'next/navigation';
import { Button } from '../ui/button';
import { toast } from '../ui/use-toast';
import { revalidatePath, revalidateTag } from 'next/cache';
import { useRouter } from 'next/navigation';
import updateAction from './updateServerAction';
import useSWR, { mutate } from 'swr';
import { patchResume } from '@/lib/patchResume';
const fields = [
{
name: 'name',
label: 'Name',
description: 'This is your public display name.',
},
{ name: 'email', label: 'Email', description: 'Put your Email here' },
{ name: 'phone', label: 'Phone', description: 'This is your Phone number' },
{ name: 'address', label: 'Address', description: 'Osoite tähän' },
];
interface PersonalDetailFormProps {
resume: any;
}const PersonalDetailForm: React.FC<PersonalDetailFormProps> = ({ resume }) => {
const router = useRouter();
const { updateSection } = useUserStore();
const { data: resumeData, mutate } = useSWR('/api/resume');
const onSubmit = async (values: z.infer<typeof personalSchema>) => {
mutate({ ...values }, false);
try {
const dataToSend = {
...values,
resumeId: resume.id,
};
const updatedData = await patchResume(resume.id, dataToSend);
mutate(updatedData);
if (!updatedData.ok) {
return toast({
title: 'something went wrong',
description: 'Please try again later',
variant: 'destructive',
});
}
if (updatedData.ok) {
updateSection('personalInfo', values);
mutate('/api/resume', updatedData);
return toast({
title: 'Success',
description: 'Your post has been updated',
variant: 'default',
});
}
router.refresh();
} catch (error) {
console.error(error);
return toast({
title: 'Error',
description: 'An error occurred while submitting the form',
variant: 'destructive',
});
}
};
const { personalInfo } = resume;
const form = useForm<z.infer<typeof personalSchema>>({
resolver: zodResolver(personalSchema),
defaultValues: {
name: personalInfo?.name || '',
email: personalInfo?.email || '',
phone: personalInfo?.phone || '',
address: personalInfo?.address || '',
},
}); return (
<div>
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-8 mb-4">
{fields.map(({ name, label, description }) => (
<FormField
key={name}
control={form.control}
name={name as 'name' | 'email' | 'phone' | 'address'}
render={({ field }) => (
<FormItem>
<FormLabel>{label}</FormLabel>
<FormControl>
<Input placeholder={label.toLowerCase()} {...field} />
</FormControl>
<FormDescription>{description}</FormDescription>
<FormMessage />
</FormItem>
)}
/>
))}
<Button type="submit">Submit</Button>
</form>
</Form>
</div>
);
};
export default PersonalDetailForm;So my question is what is the best way to have a live preview of the form data that have been written to the form.
Should i use useState? but how then i manage that with the database?
What i have been thinking is to have a debounce that sends a PATCH after 2-3 sec writing is finished to the server, and then the live preview would update with the latest text.
Should i use useState? but how then i manage that with the database?
What i have been thinking is to have a debounce that sends a PATCH after 2-3 sec writing is finished to the server, and then the live preview would update with the latest text.
The CvTemplate looks like this:
There are now few implementation added, like swr and zustand store. But to be real i'm not sure what kind approach i should use with this kind problem.
The CV is then showed on the live preview with html and css styling
/* eslint-disable prefer-const */
'use client';
import React from 'react';
import CvTemplateType1 from './CvTemplateType1';
import DefaultCvTemplate from './DefaultCvTemplate';
import { useUserStore } from '../lib/zustand';
import BasicCvTemplate from './BasicCvTemplateType';
import SideGreyCvTemplate from './SideGreyCvTemplate';
import JoyfulMinimalism from './JoyfulMinimalism';
import SideFocus from './SideFocus';
import GradienAura from './GradienAura';
const CvTemplate = (resume: any) => {
let resumes = resume.resume;
const chosenTemplate = useUserStore((state) => state.chosenTemplate);
if (chosenTemplate === 'Type1') {
return <CvTemplateType1 />;
}
if (chosenTemplate === 'BasicCvTemplate') {
return <BasicCvTemplate resume={resume} />;
}
if (chosenTemplate === 'SideGreyCvTemplate') {
return <SideGreyCvTemplate />;
return <DefaultCvTemplate />;
};
export default CvTemplate;There are now few implementation added, like swr and zustand store. But to be real i'm not sure what kind approach i should use with this kind problem.
The CV is then showed on the live preview with html and css styling
import React from 'react';
import { useUserStore } from '../lib/zustand';
const CvTemplate = ({ resume }) => {
const res = resume.resume.personalInfo;
const personalInfo = useUserStore((state) => state.user.personalInfo);
const workExperiences = useUserStore((state) => state.user.workExperiences);
return (
<div className="flex flex-col items-center justify-center h-screen bg-gray-100">
<div className="w-[210mm] h-[297mm] bg-white p-8 shadow-lg">
{/* Name */}
<h1 className="text-2xl font-semibold mb-4"> {personalInfo?.name}</h1>
<p>{personalInfo?.occupation}</p>
<hr className="my-4" />
{/* Contact Info */}
<h2 className="text-xl mb-2">Contact Information</h2>
<p>{personalInfo?.phone}</p>
<hr className="my-4" />
{/* Summary */}
<h2 className="text-xl mb-2">Summary</h2>
<p>A short summary about yourself</p>
<hr className="my-4" />
{/* Skills */}
<h2 className="text-xl mb-2">Skills</h2>
<ul>
<li>- Skill 1</li>
<li>- Skill 2</li>
<li>- Skill 3</li>
</ul>
<hr className="my-4" />
<h2 className="text-xl mb-2">Work Experience</h2>
<h3 className="text-lg mb-1">Job Title at Company</h3>
<p>Job Description</p>
{workExperiences?.map((exp, index) => (
<div key={index}>
<h3>{exp.company}</h3>
<p>Role: {exp.role}</p>
<p>Start Date: {exp.startDate}</p>
<p>End Date: {exp.endDate}</p>
</div>
))}
<hr className="my-4" />
{/* Education */}
<h2 className="text-xl mb-2">Education</h2>
<h3 className="text-lg mb-1">Your Degree</h3>
<p>Details about your education.</p>
</div>
</div>
);
};
export default CvTemplate;If needed i can provide more information or pictures.
Yellow croakerOP
Should i use React state to show The live preview, or is there way to use something like swr to mutate The changes values?