anky2002 commited on
Commit
fed4eb2
·
verified ·
1 Parent(s): 27d4efc

fix: 🎨 Resume list + Companies list pages neo-brutalism + dark mode

Browse files
frontend/src/app/(dashboard)/resume/page.tsx CHANGED
@@ -2,479 +2,124 @@
2
 
3
  import { useState } from "react";
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
- import {
6
- FileText,
7
- Plus,
8
- Download,
9
- Eye,
10
- Sparkles,
11
- Star,
12
- Trash2,
13
- Copy,
14
- MoreHorizontal,
15
- } from "lucide-react";
16
  import { apiClient } from "@/lib/api/client";
17
 
18
- interface ResumeVersion {
19
- id: string;
20
- name: string;
21
- description: string | null;
22
- is_default: boolean;
23
- is_generated: boolean;
24
- template_id: string | null;
25
- ats_score: number | null;
26
- target_role: string | null;
27
- file_name: string | null;
28
- created_at: string;
29
- updated_at: string;
30
- }
31
-
32
  const TEMPLATES = [
33
- {
34
- id: "simple",
35
- name: "Simple ATS",
36
- description: "Single column, maximum ATS compatibility. No graphics, clean hierarchy.",
37
- preview: "📄",
38
- },
39
- {
40
- id: "modern",
41
- name: "Modern",
42
- description: "Two-column layout with sidebar for skills/contact. Clean and professional.",
43
- preview: "📋",
44
- },
45
- {
46
- id: "executive",
47
- name: "Executive",
48
- description: "Minimal design, high information density. Best for 10+ years experience.",
49
- preview: "📑",
50
- },
51
- {
52
- id: "creative",
53
- name: "Creative",
54
- description: "Bold section headers, accent colors. For design/marketing roles.",
55
- preview: "🎨",
56
- },
57
  ];
58
 
59
  export default function ResumePage() {
60
  const queryClient = useQueryClient();
61
- const [showCreateModal, setShowCreateModal] = useState(false);
62
- const [showReviewModal, setShowReviewModal] = useState<string | null>(null);
63
- const [selectedResume, setSelectedResume] = useState<ResumeVersion | null>(null);
64
 
65
  const { data: resumes, isLoading } = useQuery({
66
  queryKey: ["resumes"],
67
- queryFn: async () => {
68
- const response = await apiClient.get<ResumeVersion[]>("/resumes");
69
- return response.data;
70
- },
71
  });
72
 
73
  const deleteResume = useMutation({
74
- mutationFn: async (id: string) => {
75
- await apiClient.delete(`/resumes/${id}`);
76
- },
77
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ["resumes"] }),
78
- });
79
-
80
- const duplicateResume = useMutation({
81
- mutationFn: async (id: string) => {
82
- await apiClient.post(`/resumes/${id}/duplicate`);
83
- },
84
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ["resumes"] }),
85
  });
86
 
87
  return (
88
  <div className="space-y-6">
89
- {/* Header */}
90
  <div className="flex items-center justify-between">
91
  <div>
92
- <h1 className="text-2xl font-bold">Resume Builder</h1>
93
- <p className="text-muted-foreground">
94
- Create ATS-optimized resumes tailored for each job application
95
- </p>
96
- </div>
97
- <div className="flex gap-2">
98
- <button
99
- onClick={() => setShowCreateModal(true)}
100
- className="inline-flex items-center gap-2 rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
101
- >
102
- <Plus className="h-4 w-4" />
103
- New Resume
104
- </button>
105
  </div>
 
106
  </div>
107
 
108
- {/* Templates Section */}
109
  <div>
110
- <h2 className="mb-3 text-lg font-semibold">Templates</h2>
111
  <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
112
- {TEMPLATES.map((template) => (
113
- <div
114
- key={template.id}
115
- className="cursor-pointer rounded-lg border bg-card p-4 transition-all hover:border-primary hover:shadow-md"
116
- onClick={() => setShowCreateModal(true)}
117
- >
118
- <div className="mb-3 flex h-24 items-center justify-center rounded-md bg-muted text-4xl">
119
- {template.preview}
120
- </div>
121
- <h3 className="font-medium">{template.name}</h3>
122
- <p className="mt-1 text-xs text-muted-foreground">
123
- {template.description}
124
- </p>
125
- </div>
126
  ))}
127
  </div>
128
  </div>
129
 
130
  {/* My Resumes */}
131
  <div>
132
- <h2 className="mb-3 text-lg font-semibold">My Resumes</h2>
133
  {isLoading ? (
134
- <div className="space-y-3">
135
- {[...Array(3)].map((_, i) => (
136
- <div key={i} className="h-20 animate-pulse rounded-lg border bg-muted" />
137
- ))}
138
- </div>
139
  ) : resumes && resumes.length > 0 ? (
140
  <div className="space-y-3">
141
- {resumes.map((resume) => (
142
- <ResumeCard
143
- key={resume.id}
144
- resume={resume}
145
- onReview={() => setShowReviewModal(resume.id)}
146
- onDuplicate={() => duplicateResume.mutate(resume.id)}
147
- onDelete={() => deleteResume.mutate(resume.id)}
148
- />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  ))}
150
  </div>
151
  ) : (
152
- <div className="rounded-lg border border-dashed p-8 text-center">
153
- <FileText className="mx-auto h-12 w-12 text-muted-foreground" />
154
- <h3 className="mt-4 text-lg font-medium">No resumes yet</h3>
155
- <p className="mt-1 text-sm text-muted-foreground">
156
- Create your first resume from a template or upload an existing one
157
- </p>
158
- <div className="mt-4 flex justify-center gap-2">
159
- <button
160
- onClick={() => setShowCreateModal(true)}
161
- className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground"
162
- >
163
- Create from Template
164
- </button>
165
- <button className="rounded-md border px-4 py-2 text-sm font-medium">
166
- Upload PDF/DOCX
167
- </button>
168
- </div>
169
  </div>
170
  )}
171
  </div>
172
 
173
- {/* AI Review Modal */}
174
- {showReviewModal && (
175
- <AIReviewModal
176
- resumeId={showReviewModal}
177
- onClose={() => setShowReviewModal(null)}
178
- />
179
- )}
180
-
181
- {/* Create Modal */}
182
- {showCreateModal && (
183
- <CreateResumeModal onClose={() => setShowCreateModal(false)} />
184
- )}
185
- </div>
186
- );
187
- }
188
-
189
- function ResumeCard({
190
- resume,
191
- onReview,
192
- onDuplicate,
193
- onDelete,
194
- }: {
195
- resume: ResumeVersion;
196
- onReview: () => void;
197
- onDuplicate: () => void;
198
- onDelete: () => void;
199
- }) {
200
- const [showMenu, setShowMenu] = useState(false);
201
-
202
- return (
203
- <div className="flex items-center justify-between rounded-lg border bg-card p-4">
204
- <div className="flex items-center gap-4">
205
- <div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/10">
206
- <FileText className="h-5 w-5 text-primary" />
207
- </div>
208
- <div>
209
- <div className="flex items-center gap-2">
210
- <h3 className="font-medium">{resume.name}</h3>
211
- {resume.is_default && (
212
- <span className="rounded-full bg-primary/10 px-2 py-0.5 text-xs text-primary">
213
- Default
214
- </span>
215
- )}
216
- {resume.is_generated && (
217
- <span className="rounded-full bg-purple-100 px-2 py-0.5 text-xs text-purple-700 dark:bg-purple-900 dark:text-purple-300">
218
- AI Generated
219
- </span>
220
- )}
221
- </div>
222
- <div className="mt-0.5 flex items-center gap-3 text-xs text-muted-foreground">
223
- {resume.target_role && <span>Target: {resume.target_role}</span>}
224
- {resume.ats_score !== null && (
225
- <span className="flex items-center gap-1">
226
- <Star className="h-3 w-3" />
227
- ATS Score: {resume.ats_score}/100
228
- </span>
229
- )}
230
- <span>Updated {new Date(resume.updated_at).toLocaleDateString()}</span>
231
  </div>
232
  </div>
233
- </div>
234
- <div className="flex items-center gap-1">
235
- <button
236
- onClick={onReview}
237
- className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
238
- title="AI Review"
239
- >
240
- <Sparkles className="h-4 w-4" />
241
- </button>
242
- <button
243
- className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
244
- title="Preview"
245
- >
246
- <Eye className="h-4 w-4" />
247
- </button>
248
- <button
249
- className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
250
- title="Download PDF"
251
- >
252
- <Download className="h-4 w-4" />
253
- </button>
254
- <div className="relative">
255
- <button
256
- onClick={() => setShowMenu(!showMenu)}
257
- className="rounded-md p-2 text-muted-foreground hover:bg-accent hover:text-foreground"
258
- >
259
- <MoreHorizontal className="h-4 w-4" />
260
- </button>
261
- {showMenu && (
262
- <div className="absolute right-0 top-full z-10 mt-1 w-40 rounded-md border bg-card py-1 shadow-lg">
263
- <button
264
- onClick={() => { onDuplicate(); setShowMenu(false); }}
265
- className="flex w-full items-center gap-2 px-3 py-2 text-sm hover:bg-accent"
266
- >
267
- <Copy className="h-3 w-3" /> Duplicate
268
- </button>
269
- <button
270
- onClick={() => { onDelete(); setShowMenu(false); }}
271
- className="flex w-full items-center gap-2 px-3 py-2 text-sm text-destructive hover:bg-accent"
272
- >
273
- <Trash2 className="h-3 w-3" /> Delete
274
- </button>
275
- </div>
276
- )}
277
- </div>
278
- </div>
279
- </div>
280
- );
281
- }
282
-
283
- function AIReviewModal({ resumeId, onClose }: { resumeId: string; onClose: () => void }) {
284
- const [jobDescription, setJobDescription] = useState("");
285
- const [review, setReview] = useState<any>(null);
286
- const [loading, setLoading] = useState(false);
287
-
288
- const handleReview = async () => {
289
- setLoading(true);
290
- try {
291
- const response = await apiClient.post("/ai/resume-review", {
292
- resume_text: "TODO: fetch resume text",
293
- job_description: jobDescription || undefined,
294
- });
295
- setReview(response.data);
296
- } catch (err) {
297
- console.error(err);
298
- } finally {
299
- setLoading(false);
300
- }
301
- };
302
-
303
- return (
304
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
305
- <div className="max-h-[80vh] w-full max-w-2xl overflow-y-auto rounded-lg bg-card p-6 shadow-lg">
306
- <h2 className="mb-4 text-lg font-semibold flex items-center gap-2">
307
- <Sparkles className="h-5 w-5 text-primary" />
308
- AI Resume Review
309
- </h2>
310
-
311
- {!review ? (
312
- <div className="space-y-4">
313
- <div>
314
- <label className="mb-1 block text-sm font-medium">
315
- Job Description (optional - for tailored feedback)
316
- </label>
317
- <textarea
318
- value={jobDescription}
319
- onChange={(e) => setJobDescription(e.target.value)}
320
- className="w-full rounded-md border bg-background px-3 py-2 text-sm"
321
- rows={6}
322
- placeholder="Paste the job description here for targeted keyword analysis..."
323
- />
324
- </div>
325
- <div className="flex justify-end gap-2">
326
- <button onClick={onClose} className="rounded-md border px-4 py-2 text-sm">
327
- Cancel
328
- </button>
329
- <button
330
- onClick={handleReview}
331
- disabled={loading}
332
- className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground"
333
- >
334
- {loading ? "Analyzing..." : "Review Resume"}
335
- </button>
336
- </div>
337
- </div>
338
- ) : (
339
- <div className="space-y-4">
340
- {/* Score */}
341
- <div className="flex items-center gap-4">
342
- <div className="flex h-16 w-16 items-center justify-center rounded-full border-4 border-primary text-xl font-bold">
343
- {review.score}
344
- </div>
345
- <div>
346
- <p className="font-medium">ATS Compatibility Score</p>
347
- <p className="text-sm text-muted-foreground">{review.overall_feedback}</p>
348
- </div>
349
- </div>
350
-
351
- {/* Strengths */}
352
- {review.strengths?.length > 0 && (
353
- <div>
354
- <h3 className="mb-2 text-sm font-semibold text-green-600">✅ Strengths</h3>
355
- <ul className="space-y-1">
356
- {review.strengths.map((s: string, i: number) => (
357
- <li key={i} className="text-sm text-muted-foreground">• {s}</li>
358
- ))}
359
- </ul>
360
- </div>
361
- )}
362
-
363
- {/* Weaknesses */}
364
- {review.weaknesses?.length > 0 && (
365
- <div>
366
- <h3 className="mb-2 text-sm font-semibold text-red-600">⚠️ Improvements</h3>
367
- <ul className="space-y-1">
368
- {review.weaknesses.map((w: string, i: number) => (
369
- <li key={i} className="text-sm text-muted-foreground">• {w}</li>
370
- ))}
371
- </ul>
372
- </div>
373
- )}
374
-
375
- {/* Missing Keywords */}
376
- {review.missing_keywords?.length > 0 && (
377
- <div>
378
- <h3 className="mb-2 text-sm font-semibold">🔑 Missing Keywords</h3>
379
- <div className="flex flex-wrap gap-2">
380
- {review.missing_keywords.map((kw: string, i: number) => (
381
- <span
382
- key={i}
383
- className="rounded-full bg-yellow-100 px-3 py-1 text-xs dark:bg-yellow-900"
384
- >
385
- {kw}
386
- </span>
387
- ))}
388
- </div>
389
- </div>
390
- )}
391
-
392
- <div className="flex justify-end">
393
- <button onClick={onClose} className="rounded-md border px-4 py-2 text-sm">
394
- Close
395
- </button>
396
- </div>
397
- </div>
398
- )}
399
- </div>
400
  </div>
401
  );
402
  }
403
 
404
- function CreateResumeModal({ onClose }: { onClose: () => void }) {
405
  const queryClient = useQueryClient();
406
- const [name, setName] = useState("");
407
- const [templateId, setTemplateId] = useState("simple");
408
- const [targetRole, setTargetRole] = useState("");
409
-
410
  const create = useMutation({
411
- mutationFn: async () => {
412
- await apiClient.post("/resumes", {
413
- name: name || "Untitled Resume",
414
- template_id: templateId,
415
- target_role: targetRole || undefined,
416
- });
417
- },
418
- onSuccess: () => {
419
- queryClient.invalidateQueries({ queryKey: ["resumes"] });
420
- onClose();
421
- },
422
  });
423
-
424
  return (
425
- <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
426
- <div className="w-full max-w-md rounded-lg bg-card p-6 shadow-lg">
427
- <h2 className="mb-4 text-lg font-semibold">Create New Resume</h2>
428
- <form
429
- onSubmit={(e) => { e.preventDefault(); create.mutate(); }}
430
- className="space-y-4"
431
- >
432
- <div>
433
- <label className="mb-1 block text-sm font-medium">Resume Name</label>
434
- <input
435
- type="text"
436
- value={name}
437
- onChange={(e) => setName(e.target.value)}
438
- className="w-full rounded-md border bg-background px-3 py-2 text-sm"
439
- placeholder="e.g., Software Engineer Resume"
440
- />
441
- </div>
442
- <div>
443
- <label className="mb-1 block text-sm font-medium">Template</label>
444
- <select
445
- value={templateId}
446
- onChange={(e) => setTemplateId(e.target.value)}
447
- className="w-full rounded-md border bg-background px-3 py-2 text-sm"
448
- >
449
- {TEMPLATES.map((t) => (
450
- <option key={t.id} value={t.id}>{t.name}</option>
451
- ))}
452
- </select>
453
- </div>
454
- <div>
455
- <label className="mb-1 block text-sm font-medium">Target Role (optional)</label>
456
- <input
457
- type="text"
458
- value={targetRole}
459
- onChange={(e) => setTargetRole(e.target.value)}
460
- className="w-full rounded-md border bg-background px-3 py-2 text-sm"
461
- placeholder="e.g., Senior Frontend Engineer"
462
- />
463
- </div>
464
- <div className="flex justify-end gap-2">
465
- <button type="button" onClick={onClose} className="rounded-md border px-4 py-2 text-sm">
466
- Cancel
467
- </button>
468
- <button
469
- type="submit"
470
- className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground"
471
- disabled={create.isPending}
472
- >
473
- {create.isPending ? "Creating..." : "Create"}
474
- </button>
475
- </div>
476
- </form>
477
  </div>
478
- </div>
479
  );
480
  }
 
2
 
3
  import { useState } from "react";
4
  import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
5
+ import { FileText, Plus, Download, Eye, Sparkles, Star, Trash2, Copy, MoreHorizontal } from "lucide-react";
 
 
 
 
 
 
 
 
 
 
6
  import { apiClient } from "@/lib/api/client";
7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  const TEMPLATES = [
9
+ { id: "simple", name: "Simple ATS", desc: "Single column, max ATS compatibility", color: "var(--primary)" },
10
+ { id: "modern", name: "Modern", desc: "Two-column with sidebar", color: "var(--tertiary)" },
11
+ { id: "executive", name: "Executive", desc: "Minimal, high density", color: "var(--accent)" },
12
+ { id: "creative", name: "Creative", desc: "Bold headers, accent colors", color: "var(--pop)" },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
  ];
14
 
15
  export default function ResumePage() {
16
  const queryClient = useQueryClient();
17
+ const [showCreate, setShowCreate] = useState(false);
 
 
18
 
19
  const { data: resumes, isLoading } = useQuery({
20
  queryKey: ["resumes"],
21
+ queryFn: async () => { const r = await apiClient.get("/resumes"); return r.data; },
 
 
 
22
  });
23
 
24
  const deleteResume = useMutation({
25
+ mutationFn: async (id: string) => { await apiClient.delete(`/resumes/${id}`); },
 
 
 
 
 
 
 
 
 
26
  onSuccess: () => queryClient.invalidateQueries({ queryKey: ["resumes"] }),
27
  });
28
 
29
  return (
30
  <div className="space-y-6">
 
31
  <div className="flex items-center justify-between">
32
  <div>
33
+ <h1 className="text-3xl md:text-4xl" style={{ fontFamily: "var(--font-display)" }}>Resume Builder</h1>
34
+ <p className="text-[var(--muted-foreground)] font-medium mt-1">ATS-optimized resumes tailored per job</p>
 
 
 
 
 
 
 
 
 
 
 
35
  </div>
36
+ <button onClick={() => setShowCreate(true)} className="neo-btn bg-[var(--primary)] text-[var(--primary-foreground)]"><Plus className="h-4 w-4" /> New</button>
37
  </div>
38
 
39
+ {/* Templates */}
40
  <div>
41
+ <h2 className="text-sm font-extrabold uppercase tracking-wider mb-3 text-[var(--foreground)]" style={{ fontFamily: "var(--font-display)" }}>Templates</h2>
42
  <div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
43
+ {TEMPLATES.map((t) => (
44
+ <button key={t.id} onClick={() => setShowCreate(true)} className="neo-card p-4 text-left" style={{ backgroundColor: t.color }}>
45
+ <div className="h-16 flex items-center justify-center border-2 border-[var(--border)] bg-[var(--card)] mb-3 text-2xl" style={{ boxShadow: "var(--neo-xs)" }}>📄</div>
46
+ <h3 className="font-bold text-xs uppercase text-[var(--foreground)]">{t.name}</h3>
47
+ <p className="text-[10px] text-[var(--foreground)] opacity-80 mt-0.5">{t.desc}</p>
48
+ </button>
 
 
 
 
 
 
 
 
49
  ))}
50
  </div>
51
  </div>
52
 
53
  {/* My Resumes */}
54
  <div>
55
+ <h2 className="text-sm font-extrabold uppercase tracking-wider mb-3 text-[var(--foreground)]" style={{ fontFamily: "var(--font-display)" }}>My Resumes</h2>
56
  {isLoading ? (
57
+ <div className="space-y-3">{[...Array(3)].map((_, i) => <div key={i} className="h-20 border-2 border-[var(--border)] bg-[var(--muted)] animate-pulse" />)}</div>
 
 
 
 
58
  ) : resumes && resumes.length > 0 ? (
59
  <div className="space-y-3">
60
+ {resumes.map((resume: any) => (
61
+ <div key={resume.id} className="neo-card p-4 flex items-center justify-between">
62
+ <div className="flex items-center gap-4">
63
+ <div className="flex h-10 w-10 items-center justify-center border-2 border-[var(--border)] bg-[var(--primary)]" style={{ boxShadow: "var(--neo-xs)" }}><FileText className="h-5 w-5" /></div>
64
+ <div>
65
+ <div className="flex items-center gap-2">
66
+ <h3 className="font-bold text-sm text-[var(--foreground)]">{resume.name}</h3>
67
+ {resume.is_default && <span className="neo-badge bg-[var(--primary)]">Default</span>}
68
+ {resume.is_generated && <span className="neo-badge bg-[var(--pop)] text-white">AI</span>}
69
+ </div>
70
+ <div className="flex items-center gap-3 text-[10px] text-[var(--muted-foreground)] font-medium mt-0.5">
71
+ {resume.target_role && <span>Target: {resume.target_role}</span>}
72
+ {resume.ats_score && <span className="flex items-center gap-1"><Star className="h-3 w-3" />ATS: {resume.ats_score}</span>}
73
+ <span>Updated {new Date(resume.updated_at).toLocaleDateString()}</span>
74
+ </div>
75
+ </div>
76
+ </div>
77
+ <div className="flex items-center gap-1">
78
+ <a href={`/resume/editor?id=${resume.id}`} className="neo-interactive bg-[var(--card)] p-2"><Eye className="h-4 w-4 text-[var(--foreground)]" /></a>
79
+ <button className="neo-interactive bg-[var(--card)] p-2"><Download className="h-4 w-4 text-[var(--foreground)]" /></button>
80
+ <button onClick={() => deleteResume.mutate(resume.id)} className="neo-interactive bg-[var(--card)] p-2"><Trash2 className="h-4 w-4 text-[var(--destructive)]" /></button>
81
+ </div>
82
+ </div>
83
  ))}
84
  </div>
85
  ) : (
86
+ <div className="neo-card p-8 text-center border-dashed">
87
+ <FileText className="mx-auto h-10 w-10 text-[var(--muted-foreground)]" />
88
+ <h3 className="mt-3 font-extrabold uppercase text-[var(--foreground)]" style={{ fontFamily: "var(--font-display)" }}>No Resumes</h3>
89
+ <p className="mt-1 text-sm text-[var(--muted-foreground)]">Create from a template or upload existing</p>
90
+ <button onClick={() => setShowCreate(true)} className="neo-btn bg-[var(--primary)] text-[var(--primary-foreground)] mt-4">Create First</button>
 
 
 
 
 
 
 
 
 
 
 
 
91
  </div>
92
  )}
93
  </div>
94
 
95
+ {showCreate && (
96
+ <div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60">
97
+ <div className="w-full max-w-md border-[3px] border-[var(--border)] bg-[var(--card)] p-6" style={{ boxShadow: "var(--neo-xl)" }}>
98
+ <h2 className="text-xl font-extrabold uppercase mb-4" style={{ fontFamily: "var(--font-display)" }}>New Resume</h2>
99
+ <CreateForm onClose={() => setShowCreate(false)} />
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
  </div>
101
  </div>
102
+ )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
  </div>
104
  );
105
  }
106
 
107
+ function CreateForm({ onClose }: { onClose: () => void }) {
108
  const queryClient = useQueryClient();
109
+ const [name, setName] = useState(""); const [template, setTemplate] = useState("simple"); const [role, setRole] = useState("");
 
 
 
110
  const create = useMutation({
111
+ mutationFn: async () => { await apiClient.post("/resumes", { name: name || "Untitled", template_id: template, target_role: role || undefined }); },
112
+ onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["resumes"] }); onClose(); },
 
 
 
 
 
 
 
 
 
113
  });
 
114
  return (
115
+ <form onSubmit={(e) => { e.preventDefault(); create.mutate(); }} className="space-y-3">
116
+ <div><label className="block text-[10px] font-bold uppercase tracking-wider text-[var(--muted-foreground)] mb-1">Name</label><input value={name} onChange={(e) => setName(e.target.value)} className="w-full" placeholder="SWE Resume" /></div>
117
+ <div><label className="block text-[10px] font-bold uppercase tracking-wider text-[var(--muted-foreground)] mb-1">Template</label><select value={template} onChange={(e) => setTemplate(e.target.value)} className="w-full">{TEMPLATES.map(t => <option key={t.id} value={t.id}>{t.name}</option>)}</select></div>
118
+ <div><label className="block text-[10px] font-bold uppercase tracking-wider text-[var(--muted-foreground)] mb-1">Target Role</label><input value={role} onChange={(e) => setRole(e.target.value)} className="w-full" placeholder="Senior Frontend Engineer" /></div>
119
+ <div className="flex justify-end gap-2 pt-2">
120
+ <button type="button" onClick={onClose} className="neo-btn bg-[var(--muted)] text-[var(--foreground)]">Cancel</button>
121
+ <button type="submit" disabled={create.isPending} className="neo-btn bg-[var(--primary)] text-[var(--primary-foreground)]">{create.isPending ? "Creating..." : "Create →"}</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  </div>
123
+ </form>
124
  );
125
  }