@recruitnepal/shared-packages 1.7.1 → 1.7.4

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 (42) hide show
  1. package/dist/components/common/NoData.d.ts +3 -4
  2. package/dist/components/common/NoData.d.ts.map +1 -1
  3. package/dist/components/common/NoData.js +4 -2
  4. package/dist/components/cv/ResponsivePreview.d.ts +7 -0
  5. package/dist/components/cv/ResponsivePreview.d.ts.map +1 -0
  6. package/dist/components/cv/ResponsivePreview.js +47 -0
  7. package/dist/components/cv-builder/forms/AboutForm.d.ts +2 -0
  8. package/dist/components/cv-builder/forms/AboutForm.d.ts.map +1 -0
  9. package/dist/components/cv-builder/forms/AboutForm.js +86 -0
  10. package/dist/components/cv-builder/forms/AdditionalDetailsForm.d.ts +2 -0
  11. package/dist/components/cv-builder/forms/AdditionalDetailsForm.d.ts.map +1 -0
  12. package/dist/components/cv-builder/forms/AdditionalDetailsForm.js +227 -0
  13. package/dist/components/cv-builder/forms/EducationForm.d.ts +2 -0
  14. package/dist/components/cv-builder/forms/EducationForm.d.ts.map +1 -0
  15. package/dist/components/cv-builder/forms/EducationForm.js +168 -0
  16. package/dist/components/cv-builder/forms/ExperienceForm.d.ts +2 -0
  17. package/dist/components/cv-builder/forms/ExperienceForm.d.ts.map +1 -0
  18. package/dist/components/cv-builder/forms/ExperienceForm.js +266 -0
  19. package/dist/components/cv-builder/forms/PortfolioForm.d.ts +2 -0
  20. package/dist/components/cv-builder/forms/PortfolioForm.d.ts.map +1 -0
  21. package/dist/components/cv-builder/forms/PortfolioForm.js +159 -0
  22. package/dist/components/cv-builder/forms/ProjectsForm.d.ts +2 -0
  23. package/dist/components/cv-builder/forms/ProjectsForm.d.ts.map +1 -0
  24. package/dist/components/cv-builder/forms/ProjectsForm.js +134 -0
  25. package/dist/components/cv-builder/forms/SkillsForm.d.ts +2 -0
  26. package/dist/components/cv-builder/forms/SkillsForm.d.ts.map +1 -0
  27. package/dist/components/cv-builder/forms/SkillsForm.js +250 -0
  28. package/dist/components/ui/BulletListTextarea.d.ts.map +1 -1
  29. package/dist/components/ui/BulletListTextarea.js +3 -2
  30. package/dist/components/ui/DesignationSelect.d.ts +1 -4
  31. package/dist/components/ui/DesignationSelect.d.ts.map +1 -1
  32. package/dist/components/ui/DesignationSelect.js +6 -7
  33. package/dist/components/ui/IndustrySelect.d.ts +1 -4
  34. package/dist/components/ui/IndustrySelect.d.ts.map +1 -1
  35. package/dist/components/ui/IndustrySelect.js +6 -7
  36. package/dist/components/ui/MultiSelectOptions.d.ts +2 -4
  37. package/dist/components/ui/MultiSelectOptions.d.ts.map +1 -1
  38. package/dist/components/ui/MultiSelectOptions.js +17 -20
  39. package/dist/index.d.ts +0 -33
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +1 -27
  42. package/package.json +44 -51
@@ -0,0 +1,266 @@
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 { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion';
8
+ import { Button } from '@/components/ui/button';
9
+ import { Checkbox } from '@/components/ui/checkbox';
10
+ import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage, } from '@/components/ui/form';
11
+ import { Input } from '@/components/ui/input';
12
+ import { BulletListTextarea } from '@/components/ui/BulletListTextarea';
13
+ import { monthsDropdown, yearsDropdown } from '@/utils/commonDropdownOptions';
14
+ import { Plus, Trash2 } from 'lucide-react';
15
+ import { useCvPreview } from '@/stores/cvPreview.store';
16
+ import { useCvDraftsStore } from '@/stores/cvDrafts.store';
17
+ import IndustrySelect from '@/components/common/IndustrySelect';
18
+ import DesignationSelect from '@/components/common/DesignationSelect';
19
+ import NoData from '@/components/common/no-data';
20
+ const dateSchema = z.object({
21
+ year: z
22
+ .union([
23
+ z.number().int().gte(1000).lte(9999),
24
+ z.string().regex(/^\d{4}$/, 'Year must be a 4-digit number'),
25
+ ])
26
+ .transform((val) => (typeof val === 'string' ? Number(val) : val)),
27
+ month: z
28
+ .union([
29
+ z.number().int().gte(1).lte(12),
30
+ z.string().regex(/^([1-9]|1[0-2])$/, 'Month must be between 1 and 12'),
31
+ ])
32
+ .transform((val) => (typeof val === 'string' ? Number(val) : val)),
33
+ day: z
34
+ .union([
35
+ z.number().int().default(1),
36
+ z.string().transform((val) => Number(val)),
37
+ ])
38
+ .default(1),
39
+ });
40
+ const workExperienceSchema = z
41
+ .object({
42
+ id: z.string().optional(),
43
+ organization_name: z
44
+ .string()
45
+ .min(1, { message: 'Organization Name is required' }),
46
+ industry_id: z
47
+ .string()
48
+ .min(1, { message: 'Organization Industry is required' }),
49
+ designation: z.string().min(1, { message: 'Position is required' }),
50
+ still_working: z.boolean().default(false),
51
+ key_responsibilities: z
52
+ .array(z.string())
53
+ .min(1, { message: 'At least one key responsibility is required' })
54
+ .refine((arr) => arr.some((s) => String(s).trim().length > 0), { message: 'At least one key responsibility is required' }),
55
+ start: dateSchema,
56
+ end: z.union([dateSchema, z.null()]).optional(),
57
+ })
58
+ .refine((data) => {
59
+ if (data.still_working) {
60
+ return data.end === null || data.end === undefined;
61
+ }
62
+ return data.end !== null && data.end !== undefined;
63
+ }, {
64
+ message: 'End date is required when not currently working',
65
+ path: ['end'],
66
+ });
67
+ const workExperienceFormSchema = z.object({
68
+ workExpEntries: z
69
+ .array(workExperienceSchema)
70
+ .min(0, 'Work experience entries are optional'),
71
+ });
72
+ export default function ExperienceForm() {
73
+ const { live, setSections } = useCvPreview();
74
+ const { getCurrentDraft, updateDraft } = useCvDraftsStore();
75
+ // Removed direct hooks and state - now using IndustrySelect and DesignationSelect components
76
+ // Normalize highlights from API/live (highlights array or description string) to string[]
77
+ const normalizeHighlights = useCallback((exp) => {
78
+ if (Array.isArray(exp.highlights) && exp.highlights.length) {
79
+ return exp.highlights.map(String).filter(Boolean);
80
+ }
81
+ const text = exp.description || '';
82
+ if (!String(text).trim())
83
+ return [];
84
+ return String(text)
85
+ .split(/\r?\n|•|·|\u2022|;|-(?=\s)|\s*●\s*/g)
86
+ .map((s) => s.replace(/^[-–—•·●\s]+/, '').trim())
87
+ .filter(Boolean);
88
+ }, []);
89
+ const experienceList = useMemo(() => {
90
+ if (!live?.sections?.experience || !Array.isArray(live.sections.experience)) {
91
+ return [];
92
+ }
93
+ return live.sections.experience.map((exp) => {
94
+ const bullets = normalizeHighlights(exp);
95
+ return {
96
+ id: exp.id || `exp_${Date.now()}`,
97
+ organization_name: exp.company || '',
98
+ industry_id: '', // Industry ID - will be empty for old data
99
+ designation: exp.position || '', // Store designation title directly
100
+ still_working: exp.current || !exp.endDate,
101
+ key_responsibilities: bullets.length > 0 ? bullets : [''],
102
+ start: exp.startDate
103
+ ? {
104
+ year: new Date(exp.startDate).getFullYear(),
105
+ month: new Date(exp.startDate).getMonth() + 1,
106
+ day: 1,
107
+ }
108
+ : { year: new Date().getFullYear(), month: 1, day: 1 },
109
+ end: exp.endDate && !exp.current
110
+ ? {
111
+ year: new Date(exp.endDate).getFullYear(),
112
+ month: new Date(exp.endDate).getMonth() + 1,
113
+ day: 1,
114
+ }
115
+ : null,
116
+ };
117
+ });
118
+ }, [live?.sections?.experience, normalizeHighlights]);
119
+ const form = useForm({
120
+ resolver: zodResolver(workExperienceFormSchema),
121
+ defaultValues: {
122
+ workExpEntries: experienceList.length > 0 ? experienceList : [],
123
+ },
124
+ });
125
+ const { fields, append, remove } = useFieldArray({
126
+ control: form.control,
127
+ name: 'workExpEntries',
128
+ });
129
+ // Track if we've initialized to prevent reset loops
130
+ const initializedRef = useRef(false);
131
+ const prevExperienceListRef = useRef('');
132
+ useEffect(() => {
133
+ const currentListStr = JSON.stringify(experienceList);
134
+ // Only reset if:
135
+ // 1. We haven't initialized yet AND there's data
136
+ // 2. OR the data actually changed from an external source (different structure)
137
+ if (!initializedRef.current && experienceList.length > 0) {
138
+ form.reset({ workExpEntries: experienceList });
139
+ initializedRef.current = true;
140
+ prevExperienceListRef.current = currentListStr;
141
+ }
142
+ else if (initializedRef.current && prevExperienceListRef.current !== currentListStr) {
143
+ // Only reset if the structure changed significantly (e.g., loading a draft)
144
+ const prevList = JSON.parse(prevExperienceListRef.current || '[]');
145
+ const currentIds = experienceList.map(e => e.id).sort().join(',');
146
+ const prevIds = prevList.map((e) => e.id).sort().join(',');
147
+ // Only reset if IDs changed (new draft loaded) or count changed significantly
148
+ if (currentIds !== prevIds || Math.abs(experienceList.length - prevList.length) > 0) {
149
+ form.reset({ workExpEntries: experienceList });
150
+ prevExperienceListRef.current = currentListStr;
151
+ }
152
+ }
153
+ }, [experienceList, form]);
154
+ useEffect(() => {
155
+ const subscription = form.watch((value, { name }) => {
156
+ if (name?.includes('still_working')) {
157
+ const index = name.split('.')[1];
158
+ const isStillWorking = value.workExpEntries?.[index]?.still_working;
159
+ if (isStillWorking) {
160
+ form.setValue(`workExpEntries.${index}.end`, null);
161
+ }
162
+ else {
163
+ form.setValue(`workExpEntries.${index}.end`, {
164
+ year: new Date().getFullYear(),
165
+ month: 1,
166
+ day: 1,
167
+ });
168
+ }
169
+ }
170
+ });
171
+ return () => subscription.unsubscribe();
172
+ }, [form]);
173
+ const getMonthName = (monthNumber) => {
174
+ const monthObj = monthsDropdown.find((m) => m.value === monthNumber);
175
+ return monthObj ? monthObj.label : '';
176
+ };
177
+ const formatWorkDateRange = (startDate, endDate, stillWorking) => {
178
+ if (!startDate || (!startDate.year && !startDate.month))
179
+ return '';
180
+ const startMonth = getMonthName(startDate.month);
181
+ const startYear = startDate.year;
182
+ const startDisplay = startMonth && startYear ? `${startMonth} ${startYear}` : startYear || '';
183
+ if (stillWorking)
184
+ return `${startDisplay} - Present`;
185
+ if (!endDate || (!endDate.year && !endDate.month))
186
+ return startDisplay;
187
+ const endMonth = getMonthName(endDate.month);
188
+ const endYear = endDate.year;
189
+ const endDisplay = endMonth && endYear ? `${endMonth} ${endYear}` : endYear || '';
190
+ return `${startDisplay} - ${endDisplay}`;
191
+ };
192
+ // ---- LIVE PREVIEW: Experience ----
193
+ const debRefExp = useRef(null);
194
+ const toIso = useCallback((d) => d?.year ? `${d.year}-${String(d.month || 1).padStart(2, '0')}-01` : '', []);
195
+ const mapExp = useCallback((rows) => rows.map((r) => {
196
+ const bullets = Array.isArray(r.key_responsibilities)
197
+ ? r.key_responsibilities.map(String).filter(Boolean)
198
+ : [];
199
+ const designationTitle = r.designation || '';
200
+ return {
201
+ id: r.id || `exp_${Date.now()}`,
202
+ company: r.organization_name || '',
203
+ position: designationTitle,
204
+ location: '',
205
+ startDate: toIso(r.start) || '',
206
+ endDate: r.still_working ? undefined : toIso(r.end),
207
+ current: r.still_working || false,
208
+ highlights: bullets,
209
+ };
210
+ }), [toIso]);
211
+ const pushExp = useCallback((payload) => {
212
+ clearTimeout(debRefExp.current);
213
+ debRefExp.current = setTimeout(() => setSections({ experience: payload }), 180);
214
+ }, [setSections]);
215
+ useEffect(() => {
216
+ const sub = form.watch((_v) => {
217
+ const vals = form.getValues().workExpEntries ?? [];
218
+ pushExp(mapExp(vals));
219
+ });
220
+ pushExp(mapExp(form.getValues().workExpEntries ?? []));
221
+ return () => sub.unsubscribe();
222
+ }, [form, pushExp, mapExp]);
223
+ // Update draft when data changes
224
+ useEffect(() => {
225
+ const currentDraft = getCurrentDraft();
226
+ if (currentDraft && live) {
227
+ updateDraft(currentDraft.id, {
228
+ cv_data: live,
229
+ });
230
+ }
231
+ }, [live, getCurrentDraft, updateDraft]);
232
+ return (_jsxs("div", { className: "mb-6", children: [_jsx("h1", { className: "text-2xl mb-2 font-medium", children: "Work Experience" }), _jsx("p", { className: "text-sm mb-6", children: "Add your work experience. List your previous positions and responsibilities." }), _jsx(Form, { ...form, children: _jsxs("form", { className: "flex flex-col gap-3", children: [!fields.length && _jsx(NoData, {}), fields.map((field, index) => {
233
+ const isStillWorking = form.watch(`workExpEntries.${index}.still_working`);
234
+ const dateRange = formatWorkDateRange(field.start, field.end, isStillWorking);
235
+ // Get designation and organization name for display
236
+ const currentDesignation = form.watch(`workExpEntries.${index}.designation`);
237
+ const organizationName = form.watch(`workExpEntries.${index}.organization_name`);
238
+ return (_jsx("div", { className: "space-y-6", children: _jsx(Accordion, { type: "single", collapsible: true, children: _jsxs(AccordionItem, { value: `work-${index}`, className: "border-none", children: [_jsxs("div", { className: "flex items-center justify-between bg-primary/10 py-3 px-4 rounded-xl", 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: organizationName || 'New Work Experience' }), _jsx("p", { className: "text-sm font-light line-clamp-1", children: currentDesignation || 'Position' }), 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) => {
239
+ e.stopPropagation();
240
+ remove(index);
241
+ }, className: "h-8 w-8 p-0 hover:text-destructive", children: _jsx(Trash2, { size: 20, className: "hover:text-destructive" }) }) })] }), _jsx(AccordionContent, { children: _jsxs("div", { className: "p-4 pt-0 space-y-4", children: [_jsxs("div", { className: "grid grid-cols-1 sm:grid-cols-2 gap-4", children: [_jsx(FormField, { control: form.control, name: `workExpEntries.${index}.organization_name`, render: ({ field }) => (_jsxs(FormItem, { className: "w-full", children: [_jsx(FormLabel, { children: "Company/Organization" }), _jsx(FormControl, { children: _jsx(Input, { placeholder: "Enter Company Name", ...field }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: `workExpEntries.${index}.designation`, render: ({ field }) => {
242
+ return (_jsxs(FormItem, { className: "w-full", children: [_jsx(FormLabel, { children: "Position/Designation" }), _jsx(FormControl, { children: _jsx(DesignationSelect, { value: field.value || '', onChange: field.onChange, placeholder: "Select Position / Designation", valueType: "title", className: "h-9" }) }), _jsx(FormMessage, {})] }));
243
+ } })] }), _jsx(FormField, { control: form.control, name: `workExpEntries.${index}.industry_id`, render: ({ field }) => {
244
+ return (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Industry" }), _jsx(FormControl, { children: _jsx(IndustrySelect, { value: field.value || '', onChange: field.onChange, placeholder: "Select Industry", className: "h-9" }) }), _jsx(FormMessage, {})] }));
245
+ } }), _jsxs("div", { className: "md:flex gap-4", children: [_jsxs("div", { className: "flex gap-2 flex-col w-full sm:w-1/2 mb-3 sm:mb-0", children: [_jsx(FormLabel, { children: "Starting From" }), _jsxs("div", { className: "sm:flex gap-2 w-full space-y-2 sm:space-y-0", children: [_jsx(FormField, { control: form.control, name: `workExpEntries.${index}.start.month`, render: ({ field }) => (_jsxs(FormItem, { className: "w-full sm:w-1/2", children: [_jsxs("select", { value: field.value || '', onChange: (e) => field.onChange(Number(e.target.value)), className: "border rounded-md h-9 px-2 text-sm w-full", children: [_jsx("option", { value: "", children: "Select Month" }), monthsDropdown.map((month) => (_jsx("option", { value: month.value, children: month.label }, month.value)))] }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: `workExpEntries.${index}.start.year`, render: ({ field }) => (_jsxs(FormItem, { className: "w-full sm:w-1/2", children: [_jsxs("select", { value: field.value || '', onChange: (e) => field.onChange(Number(e.target.value)), className: "border rounded-md h-9 px-2 text-sm w-full", children: [_jsx("option", { value: "", children: "Select Year" }), yearsDropdown.map((year) => (_jsx("option", { value: year, children: year }, year)))] }), _jsx(FormMessage, {})] })) })] }), _jsx(FormField, { control: form.control, name: `workExpEntries.${index}.still_working`, render: ({ field }) => (_jsxs(FormItem, { className: "flex flex-row items-center gap-2 mt-2", children: [_jsx(FormControl, { children: _jsx(Checkbox, { checked: field.value, onCheckedChange: field.onChange }) }), _jsx(FormLabel, { children: "Still Working" })] })) })] }), _jsxs("div", { className: "flex gap-2 flex-col w-full sm:w-1/2 mb-3 sm:mb-0", children: [_jsx(FormLabel, { className: isStillWorking ? 'text-gray-400' : '', children: "Ending In" }), _jsxs("div", { className: "sm:flex gap-2 w-full space-y-2 sm:space-y-0", children: [_jsx(FormField, { control: form.control, name: `workExpEntries.${index}.end.month`, render: ({ field }) => (_jsxs(FormItem, { className: "w-full sm:w-1/2", children: [_jsxs("select", { value: field.value || '', onChange: (e) => field.onChange(Number(e.target.value)), disabled: isStillWorking, className: "border rounded-md h-9 px-2 text-sm w-full", children: [_jsx("option", { value: "", children: "Select Month" }), monthsDropdown.map((month) => (_jsx("option", { value: month.value, children: month.label }, month.value)))] }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: `workExpEntries.${index}.end.year`, render: ({ field }) => (_jsxs(FormItem, { className: "w-full sm:w-1/2", children: [_jsxs("select", { value: field.value || '', onChange: (e) => field.onChange(Number(e.target.value)), disabled: isStillWorking, className: "border rounded-md h-9 px-2 text-sm w-full", children: [_jsx("option", { value: "", children: "Select Year" }), yearsDropdown.map((year) => (_jsx("option", { value: year, children: year }, year)))] }), _jsx(FormMessage, {})] })) })] })] })] }), _jsx(FormField, { control: form.control, name: `workExpEntries.${index}.key_responsibilities`, render: ({ field }) => (_jsxs(FormItem, { className: "w-full", children: [_jsx(FormLabel, { children: "Key Responsibilities" }), _jsx(FormControl, { children: _jsx(BulletListTextarea, { value: field.value, onChange: field.onChange, placeholder: "\u2022 e.g. Developed and maintained search ranking features", hint: "Press Enter for a new bullet. Each line is one point." }) }), _jsx(FormMessage, {})] })) })] }) })] }) }) }, field.id));
246
+ }), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "text-primary w-fit", onClick: () => {
247
+ append({
248
+ id: undefined,
249
+ organization_name: '',
250
+ industry_id: '',
251
+ designation: '',
252
+ key_responsibilities: [''],
253
+ start: {
254
+ year: new Date().getFullYear(),
255
+ month: 1,
256
+ day: 1,
257
+ },
258
+ end: {
259
+ year: new Date().getFullYear(),
260
+ month: 1,
261
+ day: 1,
262
+ },
263
+ still_working: false,
264
+ });
265
+ }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), " Add Experience"] })] }) })] }));
266
+ }
@@ -0,0 +1,2 @@
1
+ export default function PortfolioForm(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=PortfolioForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PortfolioForm.d.ts","sourceRoot":"","sources":["../../../../src/components/cv-builder/forms/PortfolioForm.tsx"],"names":[],"mappings":"AAuGA,MAAM,CAAC,OAAO,UAAU,aAAa,4CAsNpC"}
@@ -0,0 +1,159 @@
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 } from 'react-hook-form';
5
+ import { zodResolver } from '@hookform/resolvers/zod';
6
+ import { z } from 'zod';
7
+ import { Form, FormControl, FormField, FormItem, FormMessage, } from '@/components/ui/form';
8
+ import { Input } from '@/components/ui/input';
9
+ import { Card } from '@/components/ui/card';
10
+ import { useCvPreview } from '@/stores/cvPreview.store';
11
+ import { useCvDraftsStore } from '@/stores/cvDrafts.store';
12
+ import { Facebook, Github, Instagram, Link2, Linkedin, Youtube, } from 'lucide-react';
13
+ const uiFormSchema = z.object({
14
+ linkedin: z.string().url('Invalid Linkedin URL').or(z.literal('')),
15
+ github: z
16
+ .union([z.string().url('Invalid Github URL'), z.literal('')])
17
+ .optional(),
18
+ portfolio_link: z
19
+ .union([z.string().url('Invalid Portfolio URL'), z.literal('')])
20
+ .optional(),
21
+ facebook: z
22
+ .union([z.string().url('Invalid Facebook URL'), z.literal('')])
23
+ .optional(),
24
+ instagram: z
25
+ .union([z.string().url('Invalid Instagram URL'), z.literal('')])
26
+ .optional(),
27
+ youtube: z
28
+ .union([z.string().url('Invalid Youtube URL'), z.literal('')])
29
+ .optional(),
30
+ });
31
+ const IconInput = ({ icon: Icon, ...props }) => (_jsxs("div", { className: "relative", children: [_jsx("div", { className: "absolute inset-y-0 left-0 flex items-center pl-4 pointer-events-none", children: _jsx(Icon, { className: "h-5 w-5 text-[#4A90E2]" }) }), _jsx(Input, { className: "pl-12 h-14", ...props })] }));
32
+ const buildLinksFromPortfolio = (p) => {
33
+ const normalize = (url) => {
34
+ const u = String(url ?? '').trim();
35
+ if (!u)
36
+ return '';
37
+ return /^https?:\/\//i.test(u) ? u : `https://${u}`;
38
+ };
39
+ const raw = [
40
+ p?.linkedin ? {
41
+ label: 'LinkedIn',
42
+ type: 'linkedin',
43
+ url: normalize(p.linkedin),
44
+ } : null,
45
+ p?.github ? { label: 'GitHub', type: 'github', url: normalize(p.github) } : null,
46
+ p?.portfolio_link ? {
47
+ label: 'Portfolio',
48
+ type: 'portfolio',
49
+ url: normalize(p.portfolio_link),
50
+ } : null,
51
+ p?.facebook ? {
52
+ label: 'Facebook',
53
+ type: 'website',
54
+ url: normalize(p.facebook),
55
+ } : null,
56
+ p?.instagram ? {
57
+ label: 'Instagram',
58
+ type: 'website',
59
+ url: normalize(p.instagram),
60
+ } : null,
61
+ p?.youtube ? {
62
+ label: 'YouTube',
63
+ type: 'website',
64
+ url: normalize(p.youtube),
65
+ } : null,
66
+ ];
67
+ const seen = new Set();
68
+ return raw
69
+ .filter(Boolean)
70
+ .map((l) => ({ ...l, url: l.url.trim() }))
71
+ .filter((l) => l.url.length > 0 && (seen.has(l.url) ? false : (seen.add(l.url), true)));
72
+ };
73
+ export default function PortfolioForm() {
74
+ const { live, setPersonal } = useCvPreview();
75
+ const { getCurrentDraft, updateDraft } = useCvDraftsStore();
76
+ // Transform links from live data to form format
77
+ const defaultValues = useMemo(() => {
78
+ const values = {
79
+ linkedin: '',
80
+ github: '',
81
+ portfolio_link: '',
82
+ facebook: '',
83
+ instagram: '',
84
+ youtube: '',
85
+ };
86
+ if (live?.personalInfo?.links) {
87
+ live.personalInfo.links.forEach((link) => {
88
+ const key = link.type?.toLowerCase() || link.label?.toLowerCase() || '';
89
+ if (key.includes('linkedin'))
90
+ values.linkedin = link.url;
91
+ else if (key.includes('github'))
92
+ values.github = link.url;
93
+ else if (key.includes('portfolio') || key.includes('behance') || key.includes('dribbble'))
94
+ values.portfolio_link = link.url;
95
+ else if (key.includes('facebook'))
96
+ values.facebook = link.url;
97
+ else if (key.includes('instagram'))
98
+ values.instagram = link.url;
99
+ else if (key.includes('youtube'))
100
+ values.youtube = link.url;
101
+ });
102
+ }
103
+ return values;
104
+ }, [live?.personalInfo?.links]);
105
+ const form = useForm({
106
+ resolver: zodResolver(uiFormSchema),
107
+ defaultValues,
108
+ });
109
+ // Track if we've initialized to prevent reset loops
110
+ const initializedRef = useRef(false);
111
+ const prevLinksRef = useRef('');
112
+ useEffect(() => {
113
+ const currentLinksStr = JSON.stringify(defaultValues);
114
+ if (!initializedRef.current) {
115
+ form.reset(defaultValues);
116
+ initializedRef.current = true;
117
+ prevLinksRef.current = currentLinksStr;
118
+ }
119
+ else if (prevLinksRef.current !== currentLinksStr) {
120
+ // Only reset if links actually changed (e.g., loading a draft)
121
+ form.reset(defaultValues);
122
+ prevLinksRef.current = currentLinksStr;
123
+ }
124
+ }, [defaultValues, form]);
125
+ // ---- LIVE PREVIEW ----
126
+ const debRef = useRef(null);
127
+ const pushLinks = useCallback((links) => {
128
+ if (debRef.current)
129
+ window.clearTimeout(debRef.current);
130
+ debRef.current = window.setTimeout(() => {
131
+ setPersonal({ links });
132
+ const currentDraft = getCurrentDraft();
133
+ if (currentDraft && live) {
134
+ updateDraft(currentDraft.id, {
135
+ cv_data: {
136
+ ...live,
137
+ personalInfo: { ...live.personalInfo, links },
138
+ },
139
+ });
140
+ }
141
+ }, 160);
142
+ }, [setPersonal, getCurrentDraft, updateDraft, live]);
143
+ // Update preview while editing
144
+ useEffect(() => {
145
+ const sub = form.watch((_value, { name }) => {
146
+ // Watch all portfolio-related fields
147
+ if (name === 'linkedin' || name === 'github' || name === 'portfolio_link' || name === 'facebook' || name === 'instagram' || name === 'youtube') {
148
+ pushLinks(buildLinksFromPortfolio(form.getValues()));
149
+ }
150
+ });
151
+ pushLinks(buildLinksFromPortfolio(form.getValues()));
152
+ return () => {
153
+ sub?.unsubscribe?.();
154
+ if (debRef.current)
155
+ window.clearTimeout(debRef.current);
156
+ };
157
+ }, [form, pushLinks]);
158
+ return (_jsxs("div", { className: "mb-6", children: [_jsx("h1", { className: "text-2xl mb-2 font-medium", children: "Portfolio & Social Links" }), _jsx("p", { className: "text-sm mb-6", children: "Add links to your portfolio, GitHub, LinkedIn, or other professional profiles." }), _jsx(Form, { ...form, children: _jsxs("form", { className: "space-y-4 mb-6", children: [_jsxs("div", { className: "space-y-2 bg-primary/10 p-4 rounded-xl", children: [_jsx("p", { className: "text-sm font-semibold", children: "Required Professional Profile" }), _jsx(FormField, { control: form.control, name: "linkedin", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormControl, { children: _jsx(IconInput, { icon: Linkedin, placeholder: "LinkedIn Profile Link", ...field }) }), _jsx(FormMessage, {})] })) })] }), _jsxs(Card, { className: "p-4 space-y-2", children: [_jsx("p", { className: "text-sm font-semibold", children: "Professional Profile" }), _jsxs("div", { className: "space-y-4", children: [_jsx(FormField, { control: form.control, name: "github", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormControl, { children: _jsx(IconInput, { icon: Github, placeholder: "GitHub Profile Link", ...field }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "portfolio_link", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormControl, { children: _jsx(IconInput, { icon: Link2, placeholder: "Behance / Dribbble / Portfolio", ...field }) }), _jsx(FormMessage, {})] })) })] })] }), _jsxs(Card, { className: "p-4 space-y-2", children: [_jsx("p", { className: "text-sm font-semibold", children: "Additional Link" }), _jsxs("div", { className: "space-y-4", children: [_jsx(FormField, { control: form.control, name: "facebook", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormControl, { children: _jsx(IconInput, { icon: Facebook, placeholder: "Facebook Profile Link", ...field }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "instagram", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormControl, { children: _jsx(IconInput, { icon: Instagram, placeholder: "Instagram Profile Link", ...field }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: "youtube", render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormControl, { children: _jsx(IconInput, { icon: Youtube, placeholder: "YouTube Channel Link", ...field }) }), _jsx(FormMessage, {})] })) })] })] })] }) })] }));
159
+ }
@@ -0,0 +1,2 @@
1
+ export default function ProjectsForm(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=ProjectsForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ProjectsForm.d.ts","sourceRoot":"","sources":["../../../../src/components/cv-builder/forms/ProjectsForm.tsx"],"names":[],"mappings":"AA6CA,MAAM,CAAC,OAAO,UAAU,YAAY,4CAkQnC"}
@@ -0,0 +1,134 @@
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 { Textarea } from '@/components/ui/textarea';
10
+ import { Button } from '@/components/ui/button';
11
+ import { useCvPreview } from '@/stores/cvPreview.store';
12
+ import { useCvDraftsStore } from '@/stores/cvDrafts.store';
13
+ import { MultiSelectOptions } from '@/components/multi-select';
14
+ import { useSkills } from '@/hooks/api/useSkills';
15
+ import { Plus, Trash2 } from 'lucide-react';
16
+ import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from '@/components/ui/accordion';
17
+ import NoData from '@/components/common/no-data';
18
+ const projectFormSchema = z.object({
19
+ id: z.string().optional(),
20
+ project_name: z.string().min(1, { message: 'Project Name is required' }),
21
+ project_description: z.string().min(1, { message: 'Project Description is required' }),
22
+ project_url: z.string().url().optional().or(z.literal('')),
23
+ skills_required: z.array(z.string()).default([]),
24
+ });
25
+ const projectFormArraySchema = z.object({
26
+ projectEntries: z.array(projectFormSchema).min(0),
27
+ });
28
+ export default function ProjectsForm() {
29
+ const { live, setSections } = useCvPreview();
30
+ const { getCurrentDraft, updateDraft } = useCvDraftsStore();
31
+ const { skillsQuery: infiniteSkillsQuery } = useSkills();
32
+ // Transform projects from live data to form format
33
+ const projectsList = useMemo(() => {
34
+ if (!live?.sections?.projects || !Array.isArray(live.sections.projects)) {
35
+ return [];
36
+ }
37
+ return live.sections.projects.map((proj) => ({
38
+ id: proj.id || `proj_${Date.now()}`,
39
+ project_name: proj.name || '',
40
+ project_description: proj.description || '',
41
+ project_url: proj.url || '',
42
+ skills_required: proj.technologies || [],
43
+ }));
44
+ }, [live?.sections?.projects]);
45
+ const form = useForm({
46
+ resolver: zodResolver(projectFormArraySchema),
47
+ defaultValues: {
48
+ projectEntries: projectsList.length > 0 ? projectsList : [],
49
+ },
50
+ });
51
+ const { fields, append, remove } = useFieldArray({
52
+ control: form.control,
53
+ name: 'projectEntries',
54
+ });
55
+ // Track if we've initialized to prevent reset loops
56
+ const initializedRef = useRef(false);
57
+ const prevProjectsListRef = useRef('');
58
+ useEffect(() => {
59
+ const currentListStr = JSON.stringify(projectsList);
60
+ // Only reset if:
61
+ // 1. We haven't initialized yet AND there's data
62
+ // 2. OR the data actually changed from an external source (different structure)
63
+ if (!initializedRef.current && projectsList.length > 0) {
64
+ form.reset({ projectEntries: projectsList });
65
+ initializedRef.current = true;
66
+ prevProjectsListRef.current = currentListStr;
67
+ }
68
+ else if (initializedRef.current && prevProjectsListRef.current !== currentListStr) {
69
+ // Only reset if the structure changed significantly (e.g., loading a draft)
70
+ const prevList = JSON.parse(prevProjectsListRef.current || '[]');
71
+ const currentIds = projectsList.map(e => e.id).sort().join(',');
72
+ const prevIds = prevList.map((e) => e.id).sort().join(',');
73
+ // Only reset if IDs changed (new draft loaded) or count changed significantly
74
+ if (currentIds !== prevIds || Math.abs(projectsList.length - prevList.length) > 0) {
75
+ form.reset({ projectEntries: projectsList });
76
+ prevProjectsListRef.current = currentListStr;
77
+ }
78
+ }
79
+ }, [projectsList, form]);
80
+ // ---- LIVE PREVIEW: Projects ----
81
+ const debRefProj = useRef(null);
82
+ const mapProjects = (rows) => rows.map((p, i) => ({
83
+ id: String(p.id ?? `tmp-proj-${i}`),
84
+ name: p.project_name || '',
85
+ description: p.project_description || '',
86
+ technologies: Array.isArray(p.skills_required) ? p.skills_required : [],
87
+ url: p.project_url || '',
88
+ startDate: undefined,
89
+ endDate: undefined,
90
+ highlights: [],
91
+ }));
92
+ const pushProjects = useCallback((payload) => {
93
+ if (debRefProj.current)
94
+ window.clearTimeout(debRefProj.current);
95
+ debRefProj.current = window.setTimeout(() => {
96
+ setSections({ projects: payload });
97
+ }, 160);
98
+ }, [setSections]);
99
+ useEffect(() => {
100
+ const sub = form.watch((_v, { name }) => {
101
+ if (!name?.startsWith('projectEntries'))
102
+ return;
103
+ pushProjects(mapProjects(form.getValues().projectEntries ?? []));
104
+ });
105
+ // bootstrap once on mount with defaults
106
+ pushProjects(mapProjects(form.getValues().projectEntries ?? []));
107
+ return () => {
108
+ sub?.unsubscribe?.();
109
+ if (debRefProj.current)
110
+ window.clearTimeout(debRefProj.current);
111
+ };
112
+ }, [form, pushProjects]);
113
+ // Update draft when data changes
114
+ useEffect(() => {
115
+ const currentDraft = getCurrentDraft();
116
+ if (currentDraft && live) {
117
+ updateDraft(currentDraft.id, {
118
+ cv_data: live,
119
+ });
120
+ }
121
+ }, [live, getCurrentDraft, updateDraft]);
122
+ return (_jsxs("div", { className: "mb-6", children: [_jsx("h1", { className: "text-2xl mb-2 font-medium", children: "Projects" }), _jsx("p", { className: "text-sm mb-6", children: "Add your projects and portfolio work. Showcase what you've built." }), _jsx(Form, { ...form, children: _jsxs("form", { className: "space-y-6 pb-24 lg:pb-0", children: [!fields.length && (_jsx(NoData, { description: "Click 'Add More' to add projects" })), fields.map((field, index) => (_jsx(Accordion, { type: "single", collapsible: true, children: _jsxs(AccordionItem, { value: `projects-${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-[64px] 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(`projectEntries.${index}.project_name`) || 'New Project Entry' }), _jsx("p", { className: "text-sm font-light text-muted-foreground break-all line-clamp-1", children: form.watch(`projectEntries.${index}.project_url`) || 'www.website.com' })] }) }), _jsx("div", { className: "pl-3 border-l border-black/60 ml-4", children: _jsx(Button, { variant: "link", type: "button", size: "sm", onClick: (e) => {
123
+ e.stopPropagation();
124
+ remove(index);
125
+ }, 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: `projectEntries.${index}.project_name`, render: ({ field }) => (_jsxs(FormItem, { className: "w-full", children: [_jsx(FormLabel, { children: "Project Name" }), _jsx(FormControl, { children: _jsx(Input, { placeholder: "Enter Project Name", ...field }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: `projectEntries.${index}.project_url`, render: ({ field }) => (_jsxs(FormItem, { className: "w-full", children: [_jsx(FormLabel, { children: "Project URL" }), _jsx(FormControl, { children: _jsx(Input, { type: "url", inputMode: "url", autoCapitalize: "none", autoCorrect: "off", placeholder: "https://example.com/project", className: "break-all", ...field }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: `projectEntries.${index}.project_description`, render: ({ field }) => (_jsxs(FormItem, { className: "w-full", children: [_jsx(FormLabel, { children: "Project Description" }), _jsx(FormControl, { children: _jsx(Textarea, { placeholder: "Start typing\u2026", rows: 4, className: "resize-y", ...field }) }), _jsx(FormMessage, {})] })) }), _jsx(FormField, { control: form.control, name: `projectEntries.${index}.skills_required`, render: ({ field }) => (_jsxs(FormItem, { children: [_jsx(FormLabel, { children: "Skills Required" }), _jsx(FormControl, { children: _jsx(MultiSelectOptions, { value: field.value || [], onChange: field.onChange, options: [], query: infiniteSkillsQuery }) }), _jsx(FormMessage, {})] })) })] }) }) })] }) }, field.id))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", className: "text-primary w-fit", onClick: () => {
126
+ append({
127
+ id: undefined,
128
+ project_description: '',
129
+ project_name: '',
130
+ project_url: '',
131
+ skills_required: [],
132
+ });
133
+ }, children: [_jsx(Plus, { className: "h-4 w-4 mr-1" }), " Add more"] })] }) })] }));
134
+ }
@@ -0,0 +1,2 @@
1
+ export default function SkillsForm(): import("react/jsx-runtime").JSX.Element;
2
+ //# sourceMappingURL=SkillsForm.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SkillsForm.d.ts","sourceRoot":"","sources":["../../../../src/components/cv-builder/forms/SkillsForm.tsx"],"names":[],"mappings":"AA+EA,MAAM,CAAC,OAAO,UAAU,UAAU,4CAygBjC"}