anky2002 commited on
Commit
2d22bb2
·
verified ·
1 Parent(s): 96a15e0

fix: 🎨 Saved jobs + Cover letters + Discover pages neo-brutalism

Browse files
frontend/src/app/(dashboard)/saved/page.tsx CHANGED
@@ -10,144 +10,70 @@ export default function SavedJobsPage() {
10
  const queryClient = useQueryClient();
11
  const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
12
 
13
- const { data: folders } = useQuery({
14
- queryKey: ["bookmark-folders"],
15
- queryFn: async () => {
16
- const res = await apiClient.get("/bookmarks/folders");
17
- return res.data;
18
- },
19
- });
20
-
21
  const { data: savedJobs, isLoading } = useQuery({
22
  queryKey: ["saved-jobs", selectedFolder],
23
- queryFn: async () => {
24
- const params: any = {};
25
- if (selectedFolder) params.folder_id = selectedFolder;
26
- const res = await apiClient.get("/saved-jobs", { params });
27
- return res.data;
28
- },
29
  });
30
 
31
- const unsave = useMutation({
32
- mutationFn: async (jobId: string) => {
33
- await apiClient.delete(`/jobs/${jobId}/save`);
34
- },
35
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ["saved-jobs"] }),
36
- });
37
 
38
- const formatSalary = (min: number | null, max: number | null) => {
39
- if (!min && !max) return null;
40
- const fmt = (n: number) => `$${(n / 1000).toFixed(0)}k`;
41
- if (min && max) return `${fmt(min)} - ${fmt(max)}`;
42
- if (max) return `Up to ${fmt(max)}`;
43
- return `From ${fmt(min!)}`;
44
- };
45
 
46
  return (
47
  <div className="space-y-6">
48
- <div className="flex items-center justify-between">
49
- <div>
50
- <h1 className="text-2xl font-bold">Saved Jobs</h1>
51
- <p className="text-muted-foreground">
52
- Jobs you've bookmarked for later review
53
- </p>
54
- </div>
55
  </div>
56
 
57
  <div className="flex gap-6">
58
- {/* Folder sidebar */}
59
- <div className="w-48 space-y-1">
60
- <button
61
- onClick={() => setSelectedFolder(null)}
62
- className={`flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm ${
63
- !selectedFolder ? "bg-primary/10 font-medium text-primary" : "text-muted-foreground hover:text-foreground"
64
- }`}
65
- >
66
- <Bookmark className="h-4 w-4" /> All Saved
67
- <span className="ml-auto text-xs">{savedJobs?.total || 0}</span>
68
  </button>
69
-
70
- {folders?.map((folder: any) => (
71
- <button
72
- key={folder.id}
73
- onClick={() => setSelectedFolder(folder.id)}
74
- className={`flex w-full items-center gap-2 rounded-md px-3 py-2 text-sm ${
75
- selectedFolder === folder.id ? "bg-primary/10 font-medium text-primary" : "text-muted-foreground hover:text-foreground"
76
- }`}
77
- >
78
- <Folder className="h-4 w-4" style={{ color: folder.color || undefined }} />
79
- <span className="truncate">{folder.name}</span>
80
- <span className="ml-auto text-xs">{folder.job_count}</span>
81
  </button>
82
  ))}
83
-
84
- <button className="flex w-full items-center gap-2 rounded-md px-3 py-2 text-xs text-muted-foreground hover:text-primary">
85
- <FolderPlus className="h-3.5 w-3.5" /> New Folder
86
- </button>
87
  </div>
88
 
89
- {/* Job list */}
90
  <div className="flex-1">
91
  {isLoading ? (
92
- <div className="space-y-3">
93
- {[...Array(4)].map((_, i) => (
94
- <div key={i} className="h-24 animate-pulse rounded-lg border bg-muted" />
95
- ))}
96
- </div>
97
  ) : savedJobs?.jobs?.length > 0 ? (
98
  <div className="space-y-3">
99
  {savedJobs.jobs.map((item: any) => (
100
- <div key={item.saved_id} className="group rounded-lg border bg-card p-4 transition-shadow hover:shadow-md">
101
  <div className="flex items-start justify-between">
102
  <div className="flex-1">
103
- <Link href={`/jobs/${item.job_id}`} className="text-sm font-semibold hover:text-primary">
104
- {item.title}
105
- </Link>
106
- <p className="text-sm text-muted-foreground">{item.company_name}</p>
107
- <div className="mt-2 flex flex-wrap items-center gap-3 text-xs text-muted-foreground">
108
- {item.locations?.[0]?.raw && (
109
- <span className="flex items-center gap-1"><MapPin className="h-3 w-3" />{item.locations[0].raw}</span>
110
- )}
111
- {item.remote_type && (
112
- <span className="rounded-full bg-primary/10 px-2 py-0.5 text-primary">{item.remote_type}</span>
113
- )}
114
- {formatSalary(item.salary_min, item.salary_max) && (
115
- <span className="font-medium text-foreground">{formatSalary(item.salary_min, item.salary_max)}</span>
116
- )}
117
- <span className="flex items-center gap-1">
118
- <Clock className="h-3 w-3" />
119
- Saved {new Date(item.saved_at).toLocaleDateString()}
120
- </span>
121
  </div>
122
- {item.notes && (
123
- <p className="mt-2 text-xs italic text-muted-foreground">"{item.notes}"</p>
124
- )}
125
  </div>
126
- <div className="flex items-center gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
127
- <a href={item.source_url} target="_blank" className="rounded-md p-1.5 text-muted-foreground hover:text-foreground">
128
- <ExternalLink className="h-3.5 w-3.5" />
129
- </a>
130
- <button
131
- onClick={() => unsave.mutate(item.job_id)}
132
- className="rounded-md p-1.5 text-muted-foreground hover:text-destructive"
133
- title="Remove"
134
- >
135
- <Trash2 className="h-3.5 w-3.5" />
136
- </button>
137
  </div>
138
  </div>
139
  </div>
140
  ))}
141
  </div>
142
  ) : (
143
- <div className="rounded-lg border border-dashed p-8 text-center">
144
- <Bookmark className="mx-auto h-10 w-10 text-muted-foreground" />
145
- <p className="mt-3 text-sm text-muted-foreground">
146
- {selectedFolder ? "No jobs in this folder yet" : "No saved jobs yet. Save jobs while browsing to review later."}
147
- </p>
148
- <Link href="/jobs" className="mt-3 inline-block text-sm text-primary hover:underline">
149
- Browse Jobs →
150
- </Link>
151
  </div>
152
  )}
153
  </div>
 
10
  const queryClient = useQueryClient();
11
  const [selectedFolder, setSelectedFolder] = useState<string | null>(null);
12
 
13
+ const { data: folders } = useQuery({ queryKey: ["bookmark-folders"], queryFn: async () => { const r = await apiClient.get("/bookmarks/folders"); return r.data; } });
 
 
 
 
 
 
 
14
  const { data: savedJobs, isLoading } = useQuery({
15
  queryKey: ["saved-jobs", selectedFolder],
16
+ queryFn: async () => { const params: any = {}; if (selectedFolder) params.folder_id = selectedFolder; const r = await apiClient.get("/saved-jobs", { params }); return r.data; },
 
 
 
 
 
17
  });
18
 
19
+ const unsave = useMutation({ mutationFn: async (jobId: string) => { await apiClient.delete(`/jobs/${jobId}/save`); }, onSuccess: () => queryClient.invalidateQueries({ queryKey: ["saved-jobs"] }) });
 
 
 
 
 
20
 
21
+ const fmt = (min: number | null, max: number | null) => { if (!min && !max) return null; const f = (n: number) => `$${(n/1000).toFixed(0)}k`; if (min && max) return `${f(min)}-${f(max)}`; return max ? `Up to ${f(max)}` : `From ${f(min!)}`; };
 
 
 
 
 
 
22
 
23
  return (
24
  <div className="space-y-6">
25
+ <div>
26
+ <h1 className="text-3xl md:text-4xl" style={{ fontFamily: "var(--font-display)" }}>Saved Jobs</h1>
27
+ <p className="text-[var(--muted-foreground)] font-medium mt-1">Jobs bookmarked for later review</p>
 
 
 
 
28
  </div>
29
 
30
  <div className="flex gap-6">
31
+ {/* Folders */}
32
+ <div className="w-44 space-y-1">
33
+ <button onClick={() => setSelectedFolder(null)} className={`flex w-full items-center gap-2 px-3 py-2 text-xs font-bold uppercase tracking-wide ${!selectedFolder ? "bg-[var(--primary)] text-[var(--primary-foreground)] border-2 border-[var(--border)]" : "text-[var(--muted-foreground)] hover:text-[var(--foreground)]"}`} style={!selectedFolder ? { boxShadow: "var(--neo-xs)" } : {}}>
34
+ <Bookmark className="h-3.5 w-3.5" /> All <span className="ml-auto text-[10px]">{savedJobs?.total || 0}</span>
 
 
 
 
 
 
35
  </button>
36
+ {folders?.map((f: any) => (
37
+ <button key={f.id} onClick={() => setSelectedFolder(f.id)} className={`flex w-full items-center gap-2 px-3 py-2 text-xs font-bold uppercase tracking-wide ${selectedFolder === f.id ? "bg-[var(--primary)] text-[var(--primary-foreground)] border-2 border-[var(--border)]" : "text-[var(--muted-foreground)] hover:text-[var(--foreground)]"}`} style={selectedFolder === f.id ? { boxShadow: "var(--neo-xs)" } : {}}>
38
+ <Folder className="h-3.5 w-3.5" style={{ color: f.color || undefined }} /> <span className="truncate">{f.name}</span> <span className="ml-auto text-[10px]">{f.job_count}</span>
 
 
 
 
 
 
 
 
 
39
  </button>
40
  ))}
41
+ <button className="flex w-full items-center gap-2 px-3 py-2 text-[10px] font-bold text-[var(--muted-foreground)] hover:text-[var(--primary)] uppercase"><FolderPlus className="h-3 w-3" /> New Folder</button>
 
 
 
42
  </div>
43
 
44
+ {/* Jobs */}
45
  <div className="flex-1">
46
  {isLoading ? (
47
+ <div className="space-y-3">{[...Array(4)].map((_, i) => <div key={i} className="h-20 border-2 border-[var(--border)] bg-[var(--muted)] animate-pulse" />)}</div>
 
 
 
 
48
  ) : savedJobs?.jobs?.length > 0 ? (
49
  <div className="space-y-3">
50
  {savedJobs.jobs.map((item: any) => (
51
+ <div key={item.saved_id} className="neo-card p-4 group">
52
  <div className="flex items-start justify-between">
53
  <div className="flex-1">
54
+ <Link href={`/jobs/${item.job_id}`} className="text-sm font-bold hover:text-[var(--primary)] text-[var(--foreground)]">{item.title}</Link>
55
+ <p className="text-xs font-bold text-[var(--muted-foreground)]">{item.company_name}</p>
56
+ <div className="mt-2 flex flex-wrap items-center gap-2 text-[10px] text-[var(--muted-foreground)] font-medium">
57
+ {item.locations?.[0]?.raw && <span className="flex items-center gap-1"><MapPin className="h-3 w-3" />{item.locations[0].raw}</span>}
58
+ {item.remote_type && <span className="neo-badge bg-[var(--accent)] text-[var(--accent-foreground)]">{item.remote_type}</span>}
59
+ {fmt(item.salary_min, item.salary_max) && <span className="font-extrabold text-[var(--foreground)]">{fmt(item.salary_min, item.salary_max)}</span>}
60
+ <span className="flex items-center gap-1"><Clock className="h-3 w-3" />Saved {new Date(item.saved_at).toLocaleDateString()}</span>
 
 
 
 
 
 
 
 
 
 
 
61
  </div>
62
+ {item.notes && <p className="mt-2 text-[10px] italic text-[var(--muted-foreground)]">"{item.notes}"</p>}
 
 
63
  </div>
64
+ <div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
65
+ <a href={item.source_url} target="_blank" className="neo-interactive bg-[var(--card)] p-1.5"><ExternalLink className="h-3 w-3 text-[var(--foreground)]" /></a>
66
+ <button onClick={() => unsave.mutate(item.job_id)} className="neo-interactive bg-[var(--card)] p-1.5"><Trash2 className="h-3 w-3 text-[var(--destructive)]" /></button>
 
 
 
 
 
 
 
 
67
  </div>
68
  </div>
69
  </div>
70
  ))}
71
  </div>
72
  ) : (
73
+ <div className="neo-card p-8 text-center border-dashed">
74
+ <Bookmark className="mx-auto h-10 w-10 text-[var(--muted-foreground)]" />
75
+ <p className="mt-3 text-sm text-[var(--muted-foreground)] font-medium">{selectedFolder ? "No jobs in this folder" : "No saved jobs yet"}</p>
76
+ <Link href="/jobs" className="mt-2 inline-block text-sm font-bold text-[var(--primary)]">Browse Jobs →</Link>
 
 
 
 
77
  </div>
78
  )}
79
  </div>