@recruitnepal/shared-packages 1.7.1 → 1.7.2

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.
Files changed (28) hide show
  1. package/dist/components/cv/ResponsivePreview.d.ts +7 -0
  2. package/dist/components/cv/ResponsivePreview.d.ts.map +1 -0
  3. package/dist/components/cv/ResponsivePreview.js +47 -0
  4. package/dist/components/cv-builder/forms/AboutForm.d.ts +2 -0
  5. package/dist/components/cv-builder/forms/AboutForm.d.ts.map +1 -0
  6. package/dist/components/cv-builder/forms/AboutForm.js +86 -0
  7. package/dist/components/cv-builder/forms/AdditionalDetailsForm.d.ts +2 -0
  8. package/dist/components/cv-builder/forms/AdditionalDetailsForm.d.ts.map +1 -0
  9. package/dist/components/cv-builder/forms/AdditionalDetailsForm.js +227 -0
  10. package/dist/components/cv-builder/forms/EducationForm.d.ts +2 -0
  11. package/dist/components/cv-builder/forms/EducationForm.d.ts.map +1 -0
  12. package/dist/components/cv-builder/forms/EducationForm.js +168 -0
  13. package/dist/components/cv-builder/forms/ExperienceForm.d.ts +2 -0
  14. package/dist/components/cv-builder/forms/ExperienceForm.d.ts.map +1 -0
  15. package/dist/components/cv-builder/forms/ExperienceForm.js +266 -0
  16. package/dist/components/cv-builder/forms/PortfolioForm.d.ts +2 -0
  17. package/dist/components/cv-builder/forms/PortfolioForm.d.ts.map +1 -0
  18. package/dist/components/cv-builder/forms/PortfolioForm.js +159 -0
  19. package/dist/components/cv-builder/forms/ProjectsForm.d.ts +2 -0
  20. package/dist/components/cv-builder/forms/ProjectsForm.d.ts.map +1 -0
  21. package/dist/components/cv-builder/forms/ProjectsForm.js +134 -0
  22. package/dist/components/cv-builder/forms/SkillsForm.d.ts +2 -0
  23. package/dist/components/cv-builder/forms/SkillsForm.d.ts.map +1 -0
  24. package/dist/components/cv-builder/forms/SkillsForm.js +250 -0
  25. package/dist/index.d.ts +1 -0
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/index.js +1 -0
  28. package/package.json +50 -51
@@ -0,0 +1,7 @@
1
+ import type { CvTemplateRegistry } from './TemplateRenderer';
2
+ export type ResponsivePreviewProps = {
3
+ /** Template registry provided by the host app */
4
+ registry: CvTemplateRegistry;
5
+ };
6
+ export default function ResponsivePreview({ registry }: ResponsivePreviewProps): import("react/jsx-runtime").JSX.Element;
7
+ //# sourceMappingURL=ResponsivePreview.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ResponsivePreview.d.ts","sourceRoot":"","sources":["../../../src/components/cv/ResponsivePreview.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AAM7D,MAAM,MAAM,sBAAsB,GAAG;IACnC,iDAAiD;IACjD,QAAQ,EAAE,kBAAkB,CAAC;CAC9B,CAAC;AAEF,MAAM,CAAC,OAAO,UAAU,iBAAiB,CAAC,EAAE,QAAQ,EAAE,EAAE,sBAAsB,2CAiE7E"}
@@ -0,0 +1,47 @@
1
+ // components/cv/ResponsivePreview.tsx
2
+ 'use client';
3
+ import { jsx as _jsx } from "react/jsx-runtime";
4
+ import { useEffect, useRef, useState } from 'react';
5
+ import TemplateRenderer from './TemplateRenderer';
6
+ import { useCvPreview } from '../../stores/cvPreview.store';
7
+ // If your paper is a different exact size, adjust these
8
+ const BASE_WIDTH = 794; // A4 @ ~96 dpi
9
+ const BASE_HEIGHT = 1123;
10
+ export default function ResponsivePreview({ registry }) {
11
+ const { slug, live } = useCvPreview();
12
+ const wrapRef = useRef(null);
13
+ const [scale, setScale] = useState(1);
14
+ useEffect(() => {
15
+ if (!wrapRef.current)
16
+ return;
17
+ const el = wrapRef.current;
18
+ const measure = () => {
19
+ const rect = el.getBoundingClientRect();
20
+ // leave a little breathing room (2%)
21
+ const sW = rect.width / BASE_WIDTH;
22
+ const sH = rect.height / BASE_HEIGHT;
23
+ const s = Math.max(0.3, Math.min(1, Math.min(sW, sH) * 0.98));
24
+ setScale(s);
25
+ };
26
+ measure();
27
+ const ro = new ResizeObserver(measure);
28
+ ro.observe(el);
29
+ window.addEventListener('orientationchange', measure);
30
+ return () => {
31
+ ro.disconnect();
32
+ window.removeEventListener('orientationchange', measure);
33
+ };
34
+ }, []);
35
+ if (!live) {
36
+ return (_jsx("div", { className: "text-sm text-muted-foreground p-4", children: "Fill the forms to see the preview." }));
37
+ }
38
+ const scaledW = BASE_WIDTH * scale;
39
+ const scaledH = BASE_HEIGHT * scale;
40
+ return (
41
+ // This box gets as big as the dialog gives it, and can scroll both ways
42
+ _jsx("div", { ref: wrapRef, className: "h-full w-full overflow-auto", children: _jsx("div", { className: "w-full h-full flex items-start justify-center", children: _jsx("div", { className: "relative", style: { width: scaledW, height: scaledH }, children: _jsx("div", { className: "absolute left-0 top-0 origin-top-left", style: {
43
+ width: BASE_WIDTH,
44
+ height: BASE_HEIGHT,
45
+ transform: `scale(${scale})`,
46
+ }, children: _jsx("div", { id: "cv-root-mobile", className: "bg-white border rounded-xl shadow-sm", children: _jsx(TemplateRenderer, { registry: registry, slug: slug, data: live }) }) }) }) }) }));
47
+ }
@@ -0,0 +1,2 @@
1
+ export default function AboutForm(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=AboutForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AboutForm.d.ts","sourceRoot":"","sources":["../../../../src/components/cv-builder/forms/AboutForm.tsx"],"names":[],"mappings":"AAkBA,MAAM,CAAC,OAAO,UAAU,SAAS,4CA2NhC"}
@@ -0,0 +1,86 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useCallback } from 'react';
4
+ import { useForm } from 'react-hook-form';
5
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form';
6
+ import { Input } from '@/components/ui/input';
7
+ import { Textarea } from '@/components/ui/textarea';
8
+ import { useCvPreview } from '@/stores/cvPreview.store';
9
+ import { useCvDraftsStore } from '@/stores/cvDrafts.store';
10
+ export default function AboutForm() {
11
+ const { live, setPersonal } = useCvPreview();
12
+ const { getCurrentDraft, updateDraft } = useCvDraftsStore();
13
+ // Initialize form with current draft data or empty
14
+ const personalInfo = live?.personalInfo || {
15
+ firstName: '',
16
+ lastName: '',
17
+ email: '',
18
+ phone: '',
19
+ location: { city: '', state: '', country: 'Nepal' },
20
+ summary: '',
21
+ links: [],
22
+ };
23
+ const form = useForm({
24
+ defaultValues: {
25
+ firstName: personalInfo?.firstName || '',
26
+ lastName: personalInfo?.lastName || '',
27
+ email: personalInfo?.email || '',
28
+ phone: personalInfo?.phone || '',
29
+ city: personalInfo?.location?.city || '',
30
+ state: personalInfo?.location?.state || '',
31
+ country: personalInfo?.location?.country || 'Nepal',
32
+ summary: personalInfo?.summary || '',
33
+ },
34
+ });
35
+ // Update form when draft changes
36
+ useEffect(() => {
37
+ if (live?.personalInfo) {
38
+ form.reset({
39
+ firstName: live.personalInfo.firstName || '',
40
+ lastName: live.personalInfo.lastName || '',
41
+ email: live.personalInfo.email || '',
42
+ phone: live.personalInfo.phone || '',
43
+ city: live.personalInfo.location?.city || '',
44
+ state: live.personalInfo.location?.state || '',
45
+ country: live.personalInfo.location?.country || 'Nepal',
46
+ summary: live.personalInfo.summary || '',
47
+ });
48
+ }
49
+ }, [live?.personalInfo, form]);
50
+ const debounceRef = useRef(null);
51
+ const setPersonalDebounced = useCallback((payload) => {
52
+ clearTimeout(debounceRef.current);
53
+ debounceRef.current = setTimeout(() => {
54
+ setPersonal(payload);
55
+ // Also update the draft in store
56
+ const currentDraft = getCurrentDraft();
57
+ if (currentDraft && live) {
58
+ updateDraft(currentDraft.id, {
59
+ cv_data: {
60
+ ...live,
61
+ personalInfo: { ...live.personalInfo, ...payload },
62
+ },
63
+ });
64
+ }
65
+ }, 300);
66
+ }, [setPersonal, getCurrentDraft, updateDraft, live]);
67
+ // Watch form changes and update preview
68
+ useEffect(() => {
69
+ const sub = form.watch((v) => {
70
+ setPersonalDebounced({
71
+ firstName: v.firstName ?? '',
72
+ lastName: v.lastName ?? '',
73
+ email: v.email ?? '',
74
+ phone: v.phone ?? '',
75
+ location: {
76
+ city: v.city ?? '',
77
+ state: v.state ?? '',
78
+ country: v.country ?? 'Nepal',
79
+ },
80
+ summary: v.summary ?? '',
81
+ });
82
+ });
83
+ return () => sub.unsubscribe();
84
+ }, [form, setPersonalDebounced]);
85
+ return (_jsxs("div", { className: "mb-6", children: [_jsx("h1", { className: "text-2xl mb-2 font-medium", children: "About Yourself" }), _jsx("p", { className: "text-sm mb-6", children: "Fill in your personal information to build your CV. This data is saved to your CV draft." }), _jsx(Form, { ...form, children: _jsxs("form", { className: "space-y-6", onSubmit: (e) => e.preventDefault(), children: [_jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [_jsx(FormField, { control: form.control, name: "firstName", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "First Name *" }), _jsx(FormControl, { children: _jsx(Input, { placeholder: "John", ...field }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "lastName", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Last Name *" }), _jsx(FormControl, { children: _jsx(Input, { placeholder: "Doe", ...field }) }), _jsx(FormMessage, {})] })) })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-2 gap-4", children: [_jsx(FormField, { control: form.control, name: "email", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Email *" }), _jsx(FormControl, { children: _jsx(Input, { type: "email", placeholder: "john@example.com", ...field }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "phone", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Phone" }), _jsx(FormControl, { children: _jsx(Input, { placeholder: "98XXXXXXXX", ...field }) }), _jsx(FormMessage, {})] })) })] }), _jsxs("div", { className: "grid grid-cols-1 md:grid-cols-3 gap-4", children: [_jsx(FormField, { control: form.control, name: "city", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "City" }), _jsx(FormControl, { children: _jsx(Input, { placeholder: "Kathmandu", ...field }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "state", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "State/Province" }), _jsx(FormControl, { children: _jsx(Input, { placeholder: "Bagmati", ...field }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "country", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Country" }), _jsx(FormControl, { children: _jsx(Input, { placeholder: "Nepal", ...field }) }), _jsx(FormMessage, {})] })) })] }), _jsx(FormField, { control: form.control, name: "summary", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Professional Summary" }), _jsx(FormControl, { children: _jsx(Textarea, { placeholder: "Write a brief summary about yourself...", className: "min-h-[120px]", ...field }) }), _jsx(FormMessage, {})] })) })] }) })] }));
86
+ }
@@ -0,0 +1,2 @@
1
+ export default function AdditionalDetailsForm(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=AdditionalDetailsForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AdditionalDetailsForm.d.ts","sourceRoot":"","sources":["../../../../src/components/cv-builder/forms/AdditionalDetailsForm.tsx"],"names":[],"mappings":"AAoEA,MAAM,CAAC,OAAO,UAAU,qBAAqB,4CA4f5C"}
@@ -0,0 +1,227 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useCallback, useMemo, useState } from 'react';
4
+ import { useForm, useFieldArray } from 'react-hook-form';
5
+ import { zodResolver } from '@hookform/resolvers/zod';
6
+ import { z } from 'zod';
7
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form';
8
+ import { Input } from '@/components/ui/input';
9
+ import { Button } from '@/components/ui/button';
10
+ import { useCvPreview } from '@/stores/cvPreview.store';
11
+ import { useCvDraftsStore } from '@/stores/cvDrafts.store';
12
+ import { Plus, Trash2 } from 'lucide-react';
13
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion';
14
+ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select';
15
+ import { RatingStars } from '@/components/RatingStars';
16
+ import NoData from '@/components/common/no-data';
17
+ const languageSchema = z.object({
18
+ id: z.string().optional(),
19
+ language: z.string().min(1, { message: 'Language is required' }),
20
+ reading: z.number().int().min(0).max(5),
21
+ writing: z.number().int().min(0).max(5),
22
+ speaking: z.number().int().min(0).max(5),
23
+ });
24
+ const languageFormSchema = z.object({
25
+ languageEntries: z.array(languageSchema).min(0),
26
+ });
27
+ const referenceSchema = z.object({
28
+ id: z.string().optional(),
29
+ reference_person: z
30
+ .string()
31
+ .min(1, { message: 'Reference Person is required' }),
32
+ position: z.string().min(1, { message: 'Position is required' }),
33
+ email: z.string().email().min(1, { message: 'Email is required' }),
34
+ phone_number: z
35
+ .string()
36
+ .min(1, { message: 'Phone number is required' })
37
+ .max(10, { message: 'Invalid Phone number' }),
38
+ });
39
+ const referenceFormSchema = z.object({
40
+ referenceEntries: z.array(referenceSchema).min(0),
41
+ });
42
+ export default function AdditionalDetailsForm() {
43
+ const { live, setSections } = useCvPreview();
44
+ const { getCurrentDraft, updateDraft } = useCvDraftsStore();
45
+ // Control accordion state to prevent auto-closing
46
+ const [openLanguageAccordion, setOpenLanguageAccordion] = useState(undefined);
47
+ const [openReferenceAccordion, setOpenReferenceAccordion] = useState(undefined);
48
+ // Transform languages from live data to form format
49
+ const languagesList = useMemo(() => {
50
+ if (!live?.sections?.languages || !Array.isArray(live.sections.languages)) {
51
+ return [];
52
+ }
53
+ return live.sections.languages.map((lang) => ({
54
+ id: lang.id || `lang_${Date.now()}`,
55
+ language: lang.language || '',
56
+ reading: typeof lang.reading === 'number' ? lang.reading : (lang.reading === 'Excellent' ? 5 : lang.reading === 'good' ? 3 : 1),
57
+ writing: typeof lang.writing === 'number' ? lang.writing : (lang.writing === 'Excellent' ? 5 : lang.writing === 'good' ? 3 : 1),
58
+ speaking: typeof lang.speaking === 'number' ? lang.speaking : (lang.speaking === 'Excellent' ? 5 : lang.speaking === 'good' ? 3 : 1),
59
+ }));
60
+ }, [live?.sections?.languages]);
61
+ // Transform references from live data to form format
62
+ const referencesList = useMemo(() => {
63
+ if (!live?.sections?.references || !Array.isArray(live.sections.references)) {
64
+ return [];
65
+ }
66
+ return live.sections.references.map((ref) => ({
67
+ id: ref.id || `ref_${Date.now()}`,
68
+ reference_person: ref.name || '',
69
+ position: ref.position || '',
70
+ email: ref.email || '',
71
+ phone_number: ref.phone || '',
72
+ }));
73
+ }, [live?.sections?.references]);
74
+ const languagesForm = useForm({
75
+ resolver: zodResolver(languageFormSchema),
76
+ defaultValues: {
77
+ languageEntries: languagesList.length > 0 ? languagesList : [],
78
+ },
79
+ });
80
+ const referencesForm = useForm({
81
+ resolver: zodResolver(referenceFormSchema),
82
+ defaultValues: {
83
+ referenceEntries: referencesList.length > 0 ? referencesList : [],
84
+ },
85
+ });
86
+ const { fields: langFields, append: appendLang, remove: removeLang } = useFieldArray({
87
+ control: languagesForm.control,
88
+ name: 'languageEntries',
89
+ });
90
+ const { fields: refFields, append: appendRef, remove: removeRef } = useFieldArray({
91
+ control: referencesForm.control,
92
+ name: 'referenceEntries',
93
+ });
94
+ // Track if we've initialized to prevent reset loops
95
+ const langInitializedRef = useRef(false);
96
+ const refInitializedRef = useRef(false);
97
+ const prevLanguagesRef = useRef('');
98
+ const prevReferencesRef = useRef('');
99
+ useEffect(() => {
100
+ const currentListStr = JSON.stringify(languagesList);
101
+ if (!langInitializedRef.current && languagesList.length > 0) {
102
+ languagesForm.reset({ languageEntries: languagesList });
103
+ langInitializedRef.current = true;
104
+ prevLanguagesRef.current = currentListStr;
105
+ }
106
+ else if (langInitializedRef.current && prevLanguagesRef.current !== currentListStr) {
107
+ const prevList = JSON.parse(prevLanguagesRef.current || '[]');
108
+ const currentIds = languagesList.map(e => e.id).sort().join(',');
109
+ const prevIds = prevList.map((e) => e.id).sort().join(',');
110
+ if (currentIds !== prevIds || Math.abs(languagesList.length - prevList.length) > 0) {
111
+ languagesForm.reset({ languageEntries: languagesList });
112
+ prevLanguagesRef.current = currentListStr;
113
+ }
114
+ }
115
+ }, [languagesList, languagesForm]);
116
+ useEffect(() => {
117
+ const currentListStr = JSON.stringify(referencesList);
118
+ if (!refInitializedRef.current && referencesList.length > 0) {
119
+ referencesForm.reset({ referenceEntries: referencesList });
120
+ refInitializedRef.current = true;
121
+ prevReferencesRef.current = currentListStr;
122
+ }
123
+ else if (refInitializedRef.current && prevReferencesRef.current !== currentListStr) {
124
+ const prevList = JSON.parse(prevReferencesRef.current || '[]');
125
+ const currentIds = referencesList.map(e => e.id).sort().join(',');
126
+ const prevIds = prevList.map((e) => e.id).sort().join(',');
127
+ if (currentIds !== prevIds || Math.abs(referencesList.length - prevList.length) > 0) {
128
+ referencesForm.reset({ referenceEntries: referencesList });
129
+ prevReferencesRef.current = currentListStr;
130
+ }
131
+ }
132
+ }, [referencesList, referencesForm]);
133
+ // ---- LIVE PREVIEW: Languages ----
134
+ const debRefLang = useRef(null);
135
+ const pushLanguages = useCallback(() => {
136
+ if (debRefLang.current)
137
+ window.clearTimeout(debRefLang.current);
138
+ debRefLang.current = window.setTimeout(() => {
139
+ const entries = languagesForm.getValues().languageEntries || [];
140
+ const languages = entries
141
+ .map((e, i) => ({
142
+ id: String(e.id ?? i),
143
+ language: e.language ?? '',
144
+ proficiency: undefined,
145
+ reading: e.reading || undefined,
146
+ writing: e.writing || undefined,
147
+ speaking: e.speaking || undefined,
148
+ }))
149
+ .filter((l) => l.language);
150
+ setSections({ languages });
151
+ }, 150);
152
+ }, [languagesForm, setSections]);
153
+ // ---- LIVE PREVIEW: References ----
154
+ const debRefRef = useRef(null);
155
+ const pushReferences = useCallback(() => {
156
+ if (debRefRef.current)
157
+ window.clearTimeout(debRefRef.current);
158
+ debRefRef.current = window.setTimeout(() => {
159
+ const entries = referencesForm.getValues().referenceEntries || [];
160
+ const references = entries
161
+ .map((e, i) => ({
162
+ id: String(e.id ?? i),
163
+ name: e.reference_person ?? '',
164
+ position: e.position || undefined,
165
+ email: e.email || undefined,
166
+ phone: e.phone_number || undefined,
167
+ company: undefined,
168
+ }))
169
+ .filter((r) => r.name);
170
+ setSections({ references });
171
+ }, 150);
172
+ }, [referencesForm, setSections]);
173
+ // push initial + on any change
174
+ useEffect(() => {
175
+ pushLanguages();
176
+ }, [langFields.length, pushLanguages]);
177
+ useEffect(() => {
178
+ const sub = languagesForm.watch(() => pushLanguages());
179
+ return () => sub.unsubscribe();
180
+ }, [languagesForm, pushLanguages]);
181
+ useEffect(() => {
182
+ pushReferences();
183
+ }, [refFields.length, pushReferences]);
184
+ useEffect(() => {
185
+ const sub = referencesForm.watch(() => pushReferences());
186
+ return () => sub.unsubscribe();
187
+ }, [referencesForm, pushReferences]);
188
+ // Update draft when data changes
189
+ useEffect(() => {
190
+ const currentDraft = getCurrentDraft();
191
+ if (currentDraft && live) {
192
+ updateDraft(currentDraft.id, {
193
+ cv_data: live,
194
+ });
195
+ }
196
+ }, [live, getCurrentDraft, updateDraft]);
197
+ return (_jsxs("div", { className: "mb-6 space-y-8", children: [_jsx("h1", { className: "text-2xl mb-2 font-medium", children: "Additional Details" }), _jsx("p", { className: "text-sm mb-6", children: "Add languages and references to complete your CV." }), _jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold mb-4", children: "Languages" }), _jsx(Form, { ...languagesForm, children: _jsxs("form", { className: "space-y-6", children: [langFields.length === 0 && _jsx(NoData, {}), langFields.map((field, index) => (_jsx(Accordion, { type: "single", collapsible: true, value: openLanguageAccordion, onValueChange: setOpenLanguageAccordion, children: _jsxs(AccordionItem, { value: `languages-${index}`, className: "border-none", children: [_jsxs("div", { className: "flex items-center justify-between bg-primary/10 py-3 px-4 rounded-xl accordion-grow", children: [_jsx(AccordionTrigger, { className: "h-[30px] flex-grow hover:no-underline p-0", children: _jsx("div", { className: "flex items-center justify-between w-full", children: _jsx("div", { className: "flex flex-col items-start", children: _jsx("p", { className: "text-base font-semibold", children: (field.language || 'New Language Entry')
198
+ .charAt(0)
199
+ .toUpperCase() +
200
+ (field.language || 'New Language Entry').slice(1) }) }) }) }), _jsx("div", { className: "pl-3 border-l border-black/60 ml-4", children: _jsx(Button, { variant: "link", type: "button", size: "sm", onClick: (e) => {
201
+ e.stopPropagation();
202
+ removeLang(index);
203
+ }, className: "h-8 w-8 p-0 hover:text-destructive", children: _jsx(Trash2, { size: 20, className: "text-text-primary hover:text-destructive" }) }) })] }), _jsx(AccordionContent, { children: _jsx("div", { className: "p-4 pt-0", children: _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(FormField, { control: languagesForm.control, name: `languageEntries.${index}.language`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Language" }), _jsxs(Select, { onValueChange: (value) => {
204
+ field.onChange(value);
205
+ // Keep accordion open after selection
206
+ setOpenLanguageAccordion(`languages-${index}`);
207
+ }, defaultValue: field.value, value: field.value, children: [_jsx(FormControl, { children: _jsx(SelectTrigger, { children: _jsx(SelectValue, { placeholder: "Select Language" }) }) }), _jsxs(SelectContent, { children: [_jsx(SelectItem, { value: "English", children: "English" }), _jsx(SelectItem, { value: "Nepali", children: "Nepali" }), _jsx(SelectItem, { value: "French", children: "French" }), _jsx(SelectItem, { value: "Spanish", children: "Spanish" }), _jsx(SelectItem, { value: "Hindi", children: "Hindi" }), _jsx(SelectItem, { value: "Mandarin", children: "Mandarin" }), _jsx(SelectItem, { value: "Korean", children: "Korean" }), _jsx(SelectItem, { value: "German", children: "German" })] })] }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: languagesForm.control, name: `languageEntries.${index}.reading`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Reading" }), _jsx(FormControl, { children: _jsx("div", { className: "text-yellow-500", children: _jsx(RatingStars, { "aria-label": "Reading", value: Number(field.value) || 0, onChange: (n) => field.onChange(n) }) }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: languagesForm.control, name: `languageEntries.${index}.writing`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Writing" }), _jsx(FormControl, { children: _jsx("div", { className: "text-yellow-500", children: _jsx(RatingStars, { "aria-label": "Writing", value: Number(field.value) || 0, onChange: (n) => field.onChange(n) }) }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: languagesForm.control, name: `languageEntries.${index}.speaking`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Speaking" }), _jsx(FormControl, { children: _jsx("div", { className: "text-yellow-500", children: _jsx(RatingStars, { "aria-label": "Speaking", value: Number(field.value) || 0, onChange: (n) => field.onChange(n) }) }) }), _jsx(FormMessage, {})] })) })] }) }) })] }) }, field.id))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "text-primary w-fit", onClick: () => {
208
+ appendLang({
209
+ id: undefined,
210
+ language: '',
211
+ reading: 0,
212
+ speaking: 0,
213
+ writing: 0,
214
+ });
215
+ }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), " Add more"] })] }) })] }), _jsxs("div", { children: [_jsx("h2", { className: "text-lg font-semibold mb-4", children: "References" }), _jsx(Form, { ...referencesForm, children: _jsxs("form", { className: "space-y-6", children: [refFields.length === 0 && _jsx(NoData, {}), refFields.map((field, index) => (_jsx(Accordion, { type: "single", collapsible: true, value: openReferenceAccordion, onValueChange: setOpenReferenceAccordion, children: _jsxs(AccordionItem, { value: `references-${index}`, className: "border-none", children: [_jsxs("div", { className: "flex items-center justify-between bg-primary/10 py-3 px-4 rounded-xl accordion-grow", children: [_jsx(AccordionTrigger, { className: "h-[50px] flex-grow hover:no-underline p-0", children: _jsx("div", { className: "flex items-center justify-between w-full", children: _jsxs("div", { className: "flex flex-col items-start", children: [_jsx("p", { className: "text-base font-semibold", children: field.reference_person || 'New Reference Entry' }), _jsx("p", { className: "text-sm text-text-secondary", children: field.position || 'Position' })] }) }) }), _jsx("div", { className: "pl-3 border-l border-black/60 ml-4", children: _jsx(Button, { variant: "link", type: "button", size: "sm", onClick: (e) => {
216
+ e.stopPropagation();
217
+ removeRef(index);
218
+ }, className: "h-8 w-8 p-0 hover:text-destructive", children: _jsx(Trash2, { size: 20, className: "text-text-primary hover:text-destructive" }) }) })] }), _jsx(AccordionContent, { children: _jsxs("div", { className: "p-4 pt-0 space-y-4", children: [_jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsx(FormField, { control: referencesForm.control, name: `referenceEntries.${index}.reference_person`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Reference Person" }), _jsx(FormControl, { children: _jsx(Input, { ...field, placeholder: "Enter reference person name" }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: referencesForm.control, name: `referenceEntries.${index}.position`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Position" }), _jsx(FormControl, { children: _jsx(Input, { ...field, placeholder: "Enter position" }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: referencesForm.control, name: `referenceEntries.${index}.email`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Email" }), _jsx(FormControl, { children: _jsx(Input, { ...field, type: "email", placeholder: "Enter email address" }) }), _jsx(FormMessage, {})] })) })] }), _jsx("div", { children: _jsx(FormField, { control: referencesForm.control, name: `referenceEntries.${index}.phone_number`, render: ({ field }) => (_jsxs(FormItem, { className: "w-full", children: [_jsx(FormLabel, { children: "Phone Number" }), _jsx(FormControl, { children: _jsxs("div", { className: "flex", children: [_jsx(Input, { value: "+977", readOnly: true, className: "w-[100px] shrink-0 rounded-tr-none rounded-br-none" }), _jsx(Input, { ...field, placeholder: "Enter phone number", className: "w-full border-l-0 rounded-tl-none rounded-bl-none" })] }) }), _jsx(FormMessage, {})] })) }) })] }) })] }) }, field.id))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "text-primary w-fit", onClick: () => {
219
+ appendRef({
220
+ id: undefined,
221
+ reference_person: '',
222
+ position: '',
223
+ email: '',
224
+ phone_number: '',
225
+ });
226
+ }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), " Add more"] })] }) })] })] }));
227
+ }
@@ -0,0 +1,2 @@
1
+ export default function EducationForm(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=EducationForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"EducationForm.d.ts","sourceRoot":"","sources":["../../../../src/components/cv-builder/forms/EducationForm.tsx"],"names":[],"mappings":"AA8CA,MAAM,CAAC,OAAO,UAAU,aAAa,4CAwVpC"}
@@ -0,0 +1,168 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { useEffect, useRef, useCallback, useMemo } from 'react';
4
+ import { useForm, useFieldArray } from 'react-hook-form';
5
+ import { zodResolver } from '@hookform/resolvers/zod';
6
+ import { z } from 'zod';
7
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form';
8
+ import { Input } from '@/components/ui/input';
9
+ import { Button } from '@/components/ui/button';
10
+ import { Checkbox } from '@/components/ui/checkbox';
11
+ import { useCvPreview } from '@/stores/cvPreview.store';
12
+ import { useCvDraftsStore } from '@/stores/cvDrafts.store';
13
+ import { Plus, Trash2 } from 'lucide-react';
14
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion';
15
+ import NoData from '@/components/common/no-data';
16
+ const educationFormSchema = z.object({
17
+ id: z.string().optional(),
18
+ institution_name: z.string().min(1, { message: 'Institution Name is required' }),
19
+ course_name: z.string().min(1, { message: 'Course Name is required' }),
20
+ institution_address: z.string().optional(),
21
+ start: z.string().min(1, { message: 'Start date is required' }),
22
+ end: z.string().optional(),
23
+ still_studying: z.boolean().default(false),
24
+ gpa: z.string().optional(),
25
+ });
26
+ const educationFormArraySchema = z.object({
27
+ educationEntries: z.array(educationFormSchema).min(0),
28
+ });
29
+ export default function EducationForm() {
30
+ const { live, setSections } = useCvPreview();
31
+ const { getCurrentDraft, updateDraft } = useCvDraftsStore();
32
+ // Transform education from live data to form format
33
+ const educationList = useMemo(() => {
34
+ if (!live?.sections?.education || !Array.isArray(live.sections.education)) {
35
+ return [];
36
+ }
37
+ return live.sections.education.map((edu) => {
38
+ const startDate = edu.startDate
39
+ ? new Date(edu.startDate).toISOString().split('T')[0]
40
+ : '';
41
+ const endDate = edu.endDate && !edu.current
42
+ ? new Date(edu.endDate).toISOString().split('T')[0]
43
+ : '';
44
+ return {
45
+ id: edu.id || `edu_${Date.now()}`,
46
+ institution_name: edu.institution || '',
47
+ course_name: edu.field || edu.degree || '',
48
+ institution_address: edu.location || '',
49
+ start: startDate,
50
+ end: endDate,
51
+ still_studying: edu.current || false,
52
+ gpa: edu.gpa || '',
53
+ };
54
+ });
55
+ }, [live?.sections?.education]);
56
+ const form = useForm({
57
+ resolver: zodResolver(educationFormArraySchema),
58
+ defaultValues: {
59
+ educationEntries: educationList.length > 0 ? educationList : [],
60
+ },
61
+ });
62
+ const { fields, append, remove } = useFieldArray({
63
+ control: form.control,
64
+ name: 'educationEntries',
65
+ });
66
+ // Track if we've initialized to prevent reset loops
67
+ const initializedRef = useRef(false);
68
+ const prevEducationListRef = useRef('');
69
+ useEffect(() => {
70
+ const currentListStr = JSON.stringify(educationList);
71
+ // Only reset if:
72
+ // 1. We haven't initialized yet AND there's data
73
+ // 2. OR the data actually changed from an external source (different structure)
74
+ if (!initializedRef.current && educationList.length > 0) {
75
+ form.reset({ educationEntries: educationList });
76
+ initializedRef.current = true;
77
+ prevEducationListRef.current = currentListStr;
78
+ }
79
+ else if (initializedRef.current && prevEducationListRef.current !== currentListStr) {
80
+ // Only reset if the structure changed significantly (e.g., loading a draft)
81
+ const prevList = JSON.parse(prevEducationListRef.current || '[]');
82
+ const currentIds = educationList.map(e => e.id).sort().join(',');
83
+ const prevIds = prevList.map((e) => e.id).sort().join(',');
84
+ // Only reset if IDs changed (new draft loaded) or count changed significantly
85
+ if (currentIds !== prevIds || Math.abs(educationList.length - prevList.length) > 0) {
86
+ form.reset({ educationEntries: educationList });
87
+ prevEducationListRef.current = currentListStr;
88
+ }
89
+ }
90
+ }, [educationList, form]);
91
+ // Helper function to format year from date
92
+ const getYearFromDate = (dateString) => {
93
+ if (!dateString)
94
+ return '';
95
+ return new Date(dateString).getFullYear().toString();
96
+ };
97
+ // Helper function to format the date range
98
+ const formatDateRange = (startDate, endDate) => {
99
+ const startYear = getYearFromDate(startDate);
100
+ const endYear = getYearFromDate(endDate);
101
+ if (!startYear && !endYear)
102
+ return '';
103
+ if (!endYear)
104
+ return `${startYear}-Present`;
105
+ if (!startYear)
106
+ return endYear;
107
+ return `${startYear}-${endYear}`;
108
+ };
109
+ // ---- LIVE PREVIEW: Education ----
110
+ const debRefEdu = useRef(null);
111
+ const mapEdu = (rows) => rows.map((r, i) => ({
112
+ id: String(r.id ?? `tmp-edu-${i}`),
113
+ institution: r.institution_name || '',
114
+ degree: r.course_name || '',
115
+ field: r.course_name || '',
116
+ location: r.institution_address || '',
117
+ startDate: r.start || '',
118
+ endDate: r.still_studying ? undefined : r.end || undefined,
119
+ current: r.still_studying ?? !r.end,
120
+ gpa: r.gpa || '',
121
+ honors: [],
122
+ }));
123
+ const pushEdu = useCallback((payload) => {
124
+ clearTimeout(debRefEdu.current);
125
+ debRefEdu.current = setTimeout(() => setSections({ education: payload }), 160);
126
+ }, [setSections]);
127
+ useEffect(() => {
128
+ const sub = form.watch((_v, { name }) => {
129
+ if (!name?.startsWith('educationEntries'))
130
+ return;
131
+ pushEdu(mapEdu(form.getValues().educationEntries ?? []));
132
+ });
133
+ // bootstrap once for defaults
134
+ pushEdu(mapEdu(form.getValues().educationEntries ?? []));
135
+ return () => {
136
+ sub?.unsubscribe?.();
137
+ if (debRefEdu.current)
138
+ clearTimeout(debRefEdu.current);
139
+ };
140
+ }, [form, pushEdu]);
141
+ // Update draft when data changes
142
+ useEffect(() => {
143
+ const currentDraft = getCurrentDraft();
144
+ if (currentDraft && live) {
145
+ updateDraft(currentDraft.id, {
146
+ cv_data: live,
147
+ });
148
+ }
149
+ }, [live, getCurrentDraft, updateDraft]);
150
+ return (_jsxs("div", { className: "mb-6", children: [_jsx("h1", { className: "text-2xl mb-2 font-medium", children: "Education" }), _jsx("p", { className: "text-sm mb-6", children: "Add your educational background. This information will appear on your CV." }), _jsx(Form, { ...form, children: _jsxs("form", { className: "space-y-6 pb-24 lg:pb-0", children: [fields.length === 0 && _jsx(NoData, {}), fields.map((field, index) => {
151
+ const dateRange = formatDateRange(field.start, field.end || '');
152
+ return (_jsx(Accordion, { type: "single", collapsible: true, children: _jsxs(AccordionItem, { value: `education-${index}`, className: "border-none", children: [_jsxs("div", { className: "flex items-center justify-between bg-primary/10 py-3 px-4 rounded-xl accordion-grow", children: [_jsx(AccordionTrigger, { className: "min-h-[70px] flex-grow hover:no-underline p-0 text-left items-start", children: _jsxs("div", { className: "flex flex-col items-start w-full", children: [_jsx("p", { className: "text-base font-semibold line-clamp-1", children: form.watch(`educationEntries.${index}.course_name`) || 'New Education Entry' }), _jsx("p", { className: "text-sm font-light line-clamp-1", children: form.watch(`educationEntries.${index}.institution_name`) }), dateRange && (_jsx("p", { className: "text-sm font-medium text-gray-600 line-clamp-1", children: dateRange }))] }) }), _jsx("div", { className: "pl-3 border-l border-black/60 ml-4", children: _jsx(Button, { variant: "link", type: "button", size: "sm", onClick: (e) => {
153
+ e.stopPropagation();
154
+ remove(index);
155
+ }, className: "h-8 w-8 p-0 hover:text-destructive", children: _jsx(Trash2, { size: 20, className: "text-text-primary hover:text-destructive" }) }) })] }), _jsx(AccordionContent, { children: _jsx("div", { className: "p-4 pt-0", children: _jsxs("div", { className: "space-y-4", children: [_jsx(FormField, { control: form.control, name: `educationEntries.${index}.institution_name`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "University" }), _jsx(FormControl, { children: _jsx(Input, { ...field, placeholder: "University Name" }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: `educationEntries.${index}.course_name`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Course" }), _jsx(FormControl, { children: _jsx(Input, { ...field, placeholder: "Course Name" }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: `educationEntries.${index}.institution_address`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Institution Address" }), _jsx(FormControl, { children: _jsx(Input, { ...field, placeholder: "Institution Address" }) }), _jsx(FormMessage, {})] })) }), _jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4 mb-4", children: [_jsxs("div", { children: [_jsx(FormLabel, { className: "block text-sm font-medium mb-1", children: "Start Date" }), _jsx(FormField, { control: form.control, name: `educationEntries.${index}.start`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormControl, { children: _jsx(Input, { ...field, type: "date" }) }), _jsx(FormMessage, {})] })) })] }), _jsxs("div", { children: [_jsx(FormLabel, { className: "block text-sm font-medium mb-1", children: "End Date" }), _jsx(FormField, { control: form.control, name: `educationEntries.${index}.end`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormControl, { children: _jsx(Input, { ...field, type: "date", disabled: form.watch(`educationEntries.${index}.still_studying`) }) }), _jsx(FormMessage, {})] })) })] })] }), _jsx(FormField, { control: form.control, name: `educationEntries.${index}.still_studying`, render: ({ field }) => (_jsxs(FormItem, { className: "flex flex-row items-start space-x-3 space-y-0", children: [_jsx(FormControl, { children: _jsx(Checkbox, { checked: field.value, onCheckedChange: field.onChange }) }), _jsx("div", { className: "space-y-1 leading-none", children: _jsx(FormLabel, { children: "Still Studying" }) })] })) }), _jsx(FormField, { control: form.control, name: `educationEntries.${index}.gpa`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "GPA" }), _jsx(FormControl, { children: _jsx(Input, { ...field, placeholder: "3.8" }) }), _jsx(FormMessage, {})] })) })] }) }) })] }) }, field.id));
156
+ }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "text-blue-600", onClick: () => {
157
+ append({
158
+ id: undefined,
159
+ institution_name: '',
160
+ course_name: '',
161
+ institution_address: '',
162
+ start: '',
163
+ end: '',
164
+ still_studying: false,
165
+ gpa: '',
166
+ });
167
+ }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), " Add Education"] })] }) })] }));
168
+ }
@@ -0,0 +1,2 @@
1
+ export default function ExperienceForm(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=ExperienceForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ExperienceForm.d.ts","sourceRoot":"","sources":["../../../../src/components/cv-builder/forms/ExperienceForm.tsx"],"names":[],"mappings":"AAgGA,MAAM,CAAC,OAAO,UAAU,cAAc,4CAggBrC"}