@burdenoff/microfe-crewfoundry 2026.827.1 → 2026.924.1
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.
- package/dist/crewfoundry/CrewFoundryRoot.d.ts.map +1 -1
- package/dist/crewfoundry/CrewFoundryRoot.js +190 -185
- package/dist/crewfoundry/CrewFoundryRoot.js.map +1 -1
- package/dist/crewfoundry/components/forms/ConfirmDialog.d.ts.map +1 -1
- package/dist/crewfoundry/components/forms/ConfirmDialog.js +25 -24
- package/dist/crewfoundry/components/forms/ConfirmDialog.js.map +1 -1
- package/dist/crewfoundry/components/forms/FormModal.d.ts.map +1 -1
- package/dist/crewfoundry/components/forms/FormModal.js +103 -101
- package/dist/crewfoundry/components/forms/FormModal.js.map +1 -1
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.js +207 -210
- package/dist/crewfoundry/pages/DashboardDashboardPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js +0 -4
- package/dist/crewfoundry/pages/HiringCreateJobPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js +0 -4
- package/dist/crewfoundry/pages/HiringJobDetailsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/HiringJobsPagePage.js +75 -78
- package/dist/crewfoundry/pages/HiringJobsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js +101 -104
- package/dist/crewfoundry/pages/MarketplaceMarketplacePagePage.js.map +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js +95 -98
- package/dist/crewfoundry/pages/ProjectsProjectsPagePage.js.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.d.ts.map +1 -1
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.js +101 -104
- package/dist/crewfoundry/pages/TeamsTeamsPagePage.js.map +1 -1
- package/dist/shared/hooks/useTr.d.ts +17 -0
- package/dist/shared/hooks/useTr.d.ts.map +1 -0
- package/dist/shared/hooks/useTr.js +14 -0
- package/dist/shared/hooks/useTr.js.map +1 -0
- package/package.json +1 -1
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { useCrewFoundryStore as e } from "../store/crewfoundryStore.js";
|
|
2
2
|
import { formatEnumLabel as t } from "../utils/formatEnumLabel.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { useTr as n } from "../../shared/hooks/useTr.js";
|
|
4
|
+
import { AvatarStack as r } from "../components/viz/AvatarStack.js";
|
|
5
|
+
import { SegmentedTabs as i } from "../components/viz/SegmentedTabs.js";
|
|
5
6
|
import "../components/viz/index.js";
|
|
6
|
-
import { useMemo as
|
|
7
|
-
import { Briefcase as
|
|
8
|
-
import { useNavigate as
|
|
9
|
-
import { jsx as
|
|
10
|
-
import { Badge as
|
|
11
|
-
import { usePagination as
|
|
12
|
-
import { useI18n as D } from "@burdenoff/fe-libs/shared/providers/shell/I18nProvider";
|
|
7
|
+
import { useMemo as a, useState as o } from "react";
|
|
8
|
+
import { Briefcase as s, DollarSign as c, MapPin as l, Plus as u, Search as d, Users as f } from "lucide-react";
|
|
9
|
+
import { useNavigate as p } from "react-router";
|
|
10
|
+
import { jsx as m, jsxs as h } from "react/jsx-runtime";
|
|
11
|
+
import { Badge as g, Button as _, CardContent as v, CardHeader as y, CardTitle as b, GlassCard as x, GradientText as S, IllustratedEmptyState as C, Input as w, PagePurpose as T, Pagination as E } from "@burdenoff/fe-libs/ui";
|
|
12
|
+
import { usePagination as D } from "@burdenoff/fe-libs/shared/hooks";
|
|
13
13
|
//#region src/crewfoundry/pages/HiringJobsPagePage.tsx
|
|
14
14
|
var O = [
|
|
15
15
|
"active",
|
|
@@ -33,22 +33,19 @@ function k(e) {
|
|
|
33
33
|
return r === 1 ? "1 month ago" : `${r} months ago`;
|
|
34
34
|
}
|
|
35
35
|
function A({ label: e, children: t }) {
|
|
36
|
-
return /* @__PURE__ */
|
|
36
|
+
return /* @__PURE__ */ h("div", {
|
|
37
37
|
className: "flex items-center justify-between gap-3 text-sm",
|
|
38
|
-
children: [/* @__PURE__ */
|
|
38
|
+
children: [/* @__PURE__ */ m("span", {
|
|
39
39
|
className: "text-text-secondary",
|
|
40
40
|
children: e
|
|
41
|
-
}), /* @__PURE__ */
|
|
41
|
+
}), /* @__PURE__ */ m("span", {
|
|
42
42
|
className: "font-medium text-text-primary text-right",
|
|
43
43
|
children: t
|
|
44
44
|
})]
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
function j() {
|
|
48
|
-
let
|
|
49
|
-
let n = j(e);
|
|
50
|
-
return n === e ? t : n;
|
|
51
|
-
}, W = i(() => {
|
|
48
|
+
let j = n(), M = p(), { jobPostings: N, candidates: P } = e(), [F, I] = o(""), [L, R] = o("all"), { page: z, pageSize: B, offset: V, setPage: H } = D({ initialPageSize: 12 }), U = a(() => {
|
|
52
49
|
let e = F.toLowerCase();
|
|
53
50
|
return N.filter((t) => {
|
|
54
51
|
let n = `${t.title} ${t.department} ${t.location}`.toLowerCase().includes(e), r = L === "all" || t.status === L;
|
|
@@ -58,7 +55,7 @@ function j() {
|
|
|
58
55
|
N,
|
|
59
56
|
F,
|
|
60
57
|
L
|
|
61
|
-
]),
|
|
58
|
+
]), W = U.slice(V, V + B), G = (e) => M(`../hiring/job-details-page?id=${encodeURIComponent(e)}`), K = N.filter((e) => e.status === "active").length, q = N.reduce((e, t) => e + t.applicants, 0), J = a(() => {
|
|
62
59
|
let e = [{
|
|
63
60
|
id: "all",
|
|
64
61
|
label: "All",
|
|
@@ -73,7 +70,7 @@ function j() {
|
|
|
73
70
|
});
|
|
74
71
|
}
|
|
75
72
|
return e;
|
|
76
|
-
}, [N]),
|
|
73
|
+
}, [N]), Y = a(() => {
|
|
77
74
|
let e = /* @__PURE__ */ new Map();
|
|
78
75
|
for (let t of N) {
|
|
79
76
|
let n = t.title.trim().toLowerCase(), r = P.filter((e) => e.position.trim().toLowerCase() === n).map((e) => ({ name: `${e.firstName} ${e.lastName}`.trim() }));
|
|
@@ -81,55 +78,55 @@ function j() {
|
|
|
81
78
|
}
|
|
82
79
|
return e;
|
|
83
80
|
}, [N, P]);
|
|
84
|
-
return /* @__PURE__ */
|
|
81
|
+
return /* @__PURE__ */ h("div", {
|
|
85
82
|
className: "space-y-6 p-6",
|
|
86
83
|
children: [
|
|
87
|
-
/* @__PURE__ */
|
|
84
|
+
/* @__PURE__ */ h("div", {
|
|
88
85
|
className: "flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between",
|
|
89
|
-
children: [/* @__PURE__ */
|
|
90
|
-
/* @__PURE__ */
|
|
86
|
+
children: [/* @__PURE__ */ h("div", { children: [
|
|
87
|
+
/* @__PURE__ */ m("h1", {
|
|
91
88
|
className: "text-2xl font-semibold",
|
|
92
|
-
children: /* @__PURE__ */
|
|
89
|
+
children: /* @__PURE__ */ m(S, { children: j("crewfoundry.hiringJobs.title", "Job Postings") })
|
|
93
90
|
}),
|
|
94
|
-
/* @__PURE__ */
|
|
91
|
+
/* @__PURE__ */ h("p", {
|
|
95
92
|
className: "text-sm text-text-secondary",
|
|
96
93
|
children: [
|
|
97
|
-
|
|
94
|
+
K,
|
|
98
95
|
" ",
|
|
99
|
-
|
|
100
|
-
" ·",
|
|
96
|
+
j("crewfoundry.hiringJobs.summary.activePositions", "active positions"),
|
|
101
97
|
" ",
|
|
102
|
-
|
|
98
|
+
"· ",
|
|
99
|
+
q,
|
|
103
100
|
" ",
|
|
104
|
-
|
|
101
|
+
j("crewfoundry.hiringJobs.summary.totalApplicants", "total applicants")
|
|
105
102
|
]
|
|
106
103
|
}),
|
|
107
|
-
/* @__PURE__ */
|
|
104
|
+
/* @__PURE__ */ m(T, {
|
|
108
105
|
className: "mt-1",
|
|
109
106
|
children: "All your open and past job postings in one place — use it to monitor applicant volume and manage what roles you're actively hiring for."
|
|
110
107
|
})
|
|
111
|
-
] }), /* @__PURE__ */
|
|
108
|
+
] }), /* @__PURE__ */ h(_, {
|
|
112
109
|
size: "sm",
|
|
113
110
|
onClick: () => M("../hiring/create-job-page"),
|
|
114
|
-
children: [/* @__PURE__ */
|
|
111
|
+
children: [/* @__PURE__ */ m(u, { className: "h-4 w-4 mr-1" }), j("crewfoundry.hiringJobs.actions.postNewJob", "Post New Job")]
|
|
115
112
|
})]
|
|
116
113
|
}),
|
|
117
|
-
/* @__PURE__ */
|
|
114
|
+
/* @__PURE__ */ h("div", {
|
|
118
115
|
className: "flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between",
|
|
119
|
-
children: [/* @__PURE__ */
|
|
116
|
+
children: [/* @__PURE__ */ m("div", {
|
|
120
117
|
className: "-mx-1 overflow-x-auto px-1",
|
|
121
|
-
children: /* @__PURE__ */
|
|
122
|
-
tabs:
|
|
118
|
+
children: /* @__PURE__ */ m(i, {
|
|
119
|
+
tabs: J,
|
|
123
120
|
value: L,
|
|
124
121
|
onChange: (e) => {
|
|
125
122
|
R(e), H(1);
|
|
126
123
|
},
|
|
127
124
|
"aria-label": "Filter job postings by status"
|
|
128
125
|
})
|
|
129
|
-
}), /* @__PURE__ */
|
|
126
|
+
}), /* @__PURE__ */ h("div", {
|
|
130
127
|
className: "relative w-full max-w-sm",
|
|
131
|
-
children: [/* @__PURE__ */
|
|
132
|
-
placeholder:
|
|
128
|
+
children: [/* @__PURE__ */ m(d, { className: "absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary" }), /* @__PURE__ */ m(w, {
|
|
129
|
+
placeholder: j("crewfoundry.hiringJobs.searchPlaceholder", "Search positions..."),
|
|
133
130
|
className: "pl-9",
|
|
134
131
|
value: F,
|
|
135
132
|
onChange: (e) => {
|
|
@@ -138,68 +135,68 @@ function j() {
|
|
|
138
135
|
})]
|
|
139
136
|
})]
|
|
140
137
|
}),
|
|
141
|
-
|
|
138
|
+
U.length === 0 && /* @__PURE__ */ m(C, {
|
|
142
139
|
illustration: "empty-search",
|
|
143
140
|
title: "No job postings found",
|
|
144
141
|
description: "Try adjusting your search or status filter, or create a new job posting."
|
|
145
142
|
}),
|
|
146
|
-
/* @__PURE__ */
|
|
143
|
+
/* @__PURE__ */ m("div", {
|
|
147
144
|
className: "grid gap-4 md:grid-cols-2 xl:grid-cols-3",
|
|
148
|
-
children:
|
|
149
|
-
let
|
|
150
|
-
return /* @__PURE__ */
|
|
145
|
+
children: W.map((e) => {
|
|
146
|
+
let n = Y.get(e.id) ?? [];
|
|
147
|
+
return /* @__PURE__ */ h(x, {
|
|
151
148
|
treatment: "glass",
|
|
152
149
|
role: "button",
|
|
153
150
|
tabIndex: 0,
|
|
154
151
|
"aria-label": `View ${e.title}`,
|
|
155
152
|
className: "flex cursor-pointer flex-col transition-shadow hover:shadow-elevation-2 focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]",
|
|
156
|
-
onClick: () =>
|
|
153
|
+
onClick: () => G(e.id),
|
|
157
154
|
onKeyDown: (t) => {
|
|
158
|
-
(t.key === "Enter" || t.key === " ") && (t.preventDefault(),
|
|
155
|
+
(t.key === "Enter" || t.key === " ") && (t.preventDefault(), G(e.id));
|
|
159
156
|
},
|
|
160
|
-
children: [/* @__PURE__ */
|
|
157
|
+
children: [/* @__PURE__ */ h(y, { children: [/* @__PURE__ */ h("div", {
|
|
161
158
|
className: "flex items-start justify-between gap-2",
|
|
162
|
-
children: [/* @__PURE__ */
|
|
159
|
+
children: [/* @__PURE__ */ m(b, {
|
|
163
160
|
className: "text-lg leading-snug",
|
|
164
161
|
children: e.title
|
|
165
|
-
}), /* @__PURE__ */
|
|
162
|
+
}), /* @__PURE__ */ m(g, {
|
|
166
163
|
variant: e.status === "active" ? "default" : e.status === "draft" ? "secondary" : "outline",
|
|
167
164
|
children: t(e.status)
|
|
168
165
|
})]
|
|
169
|
-
}), /* @__PURE__ */
|
|
166
|
+
}), /* @__PURE__ */ h("div", {
|
|
170
167
|
className: "mt-2 flex flex-wrap gap-1.5",
|
|
171
|
-
children: [/* @__PURE__ */
|
|
168
|
+
children: [/* @__PURE__ */ m(g, {
|
|
172
169
|
variant: "outline",
|
|
173
170
|
className: "text-[10px]",
|
|
174
171
|
children: t(e.type)
|
|
175
|
-
}), /* @__PURE__ */
|
|
172
|
+
}), /* @__PURE__ */ m(g, {
|
|
176
173
|
variant: "outline",
|
|
177
174
|
className: "text-[10px]",
|
|
178
175
|
children: t(e.experienceLevel)
|
|
179
176
|
})]
|
|
180
|
-
})] }), /* @__PURE__ */
|
|
177
|
+
})] }), /* @__PURE__ */ h(v, {
|
|
181
178
|
className: "flex flex-1 flex-col gap-2.5",
|
|
182
179
|
children: [
|
|
183
|
-
/* @__PURE__ */
|
|
184
|
-
label:
|
|
185
|
-
children: /* @__PURE__ */
|
|
180
|
+
/* @__PURE__ */ m(A, {
|
|
181
|
+
label: j("crewfoundry.hiringJobs.card.department", "Department"),
|
|
182
|
+
children: /* @__PURE__ */ h("span", {
|
|
186
183
|
className: "inline-flex items-center gap-1",
|
|
187
|
-
children: [/* @__PURE__ */
|
|
184
|
+
children: [/* @__PURE__ */ m(s, { className: "h-3 w-3 text-text-secondary" }), e.department || "—"]
|
|
188
185
|
})
|
|
189
186
|
}),
|
|
190
|
-
/* @__PURE__ */
|
|
191
|
-
label:
|
|
192
|
-
children: /* @__PURE__ */
|
|
187
|
+
/* @__PURE__ */ m(A, {
|
|
188
|
+
label: j("crewfoundry.hiringJobs.card.location", "Location"),
|
|
189
|
+
children: /* @__PURE__ */ h("span", {
|
|
193
190
|
className: "inline-flex items-center gap-1",
|
|
194
|
-
children: [/* @__PURE__ */
|
|
191
|
+
children: [/* @__PURE__ */ m(l, { className: "h-3 w-3 text-text-secondary" }), e.location || "—"]
|
|
195
192
|
})
|
|
196
193
|
}),
|
|
197
|
-
/* @__PURE__ */
|
|
198
|
-
label:
|
|
199
|
-
children: /* @__PURE__ */
|
|
194
|
+
/* @__PURE__ */ m(A, {
|
|
195
|
+
label: j("crewfoundry.hiringJobs.card.salary", "Salary"),
|
|
196
|
+
children: /* @__PURE__ */ h("span", {
|
|
200
197
|
className: "inline-flex items-center gap-0.5 tabular-nums",
|
|
201
198
|
children: [
|
|
202
|
-
/* @__PURE__ */
|
|
199
|
+
/* @__PURE__ */ m(c, { className: "h-3 w-3 text-text-secondary" }),
|
|
203
200
|
(e.salaryMin / 1e3).toFixed(0),
|
|
204
201
|
"k – $",
|
|
205
202
|
(e.salaryMax / 1e3).toFixed(0),
|
|
@@ -207,25 +204,25 @@ function j() {
|
|
|
207
204
|
]
|
|
208
205
|
})
|
|
209
206
|
}),
|
|
210
|
-
/* @__PURE__ */
|
|
211
|
-
label:
|
|
212
|
-
children: /* @__PURE__ */
|
|
207
|
+
/* @__PURE__ */ m(A, {
|
|
208
|
+
label: j("crewfoundry.hiringJobs.card.applicantsLabel", "Applicants"),
|
|
209
|
+
children: /* @__PURE__ */ h("span", {
|
|
213
210
|
className: "inline-flex items-center gap-2",
|
|
214
|
-
children: [
|
|
215
|
-
people:
|
|
211
|
+
children: [n.length > 0 && /* @__PURE__ */ m(r, {
|
|
212
|
+
people: n,
|
|
216
213
|
max: 4,
|
|
217
214
|
size: 22
|
|
218
|
-
}), /* @__PURE__ */
|
|
215
|
+
}), /* @__PURE__ */ h("span", {
|
|
219
216
|
className: "inline-flex items-center gap-1 tabular-nums",
|
|
220
|
-
children: [/* @__PURE__ */
|
|
217
|
+
children: [/* @__PURE__ */ m(f, { className: "h-3 w-3 text-text-secondary" }), e.applicants]
|
|
221
218
|
})]
|
|
222
219
|
})
|
|
223
220
|
}),
|
|
224
|
-
/* @__PURE__ */
|
|
221
|
+
/* @__PURE__ */ m("div", {
|
|
225
222
|
className: "mt-auto pt-1",
|
|
226
|
-
children: /* @__PURE__ */
|
|
227
|
-
label:
|
|
228
|
-
children: /* @__PURE__ */
|
|
223
|
+
children: /* @__PURE__ */ m(A, {
|
|
224
|
+
label: j("crewfoundry.hiringJobs.card.posted", "Posted"),
|
|
225
|
+
children: /* @__PURE__ */ m("span", {
|
|
229
226
|
className: "text-text-secondary font-normal",
|
|
230
227
|
children: k(e.postedDate)
|
|
231
228
|
})
|
|
@@ -236,10 +233,10 @@ function j() {
|
|
|
236
233
|
}, e.id);
|
|
237
234
|
})
|
|
238
235
|
}),
|
|
239
|
-
|
|
236
|
+
U.length > B && /* @__PURE__ */ m(E, {
|
|
240
237
|
page: z,
|
|
241
238
|
pageSize: B,
|
|
242
|
-
totalItems:
|
|
239
|
+
totalItems: U.length,
|
|
243
240
|
onPageChange: H
|
|
244
241
|
})
|
|
245
242
|
]
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HiringJobsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringJobsPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from 'react';\nimport { useI18n } from '@burdenoff/fe-libs/shared/providers/shell/I18nProvider';\nimport {\n Badge,\n Button,\n CardContent,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { Briefcase, DollarSign, MapPin, Plus, Search, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useCrewFoundryStore, type JobPosting } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, SegmentedTabs, type SegmentedTab } from '../components/viz';\n\n/** Ordered status filter set (only rendered when a status has postings). */\nconst STATUS_ORDER: JobPosting['status'][] = ['active', 'draft', 'paused', 'closed', 'filled'];\n\n/** Relative \"Posted N days ago\" label from an ISO date-only string. */\nfunction relativePostedLabel(dateStr: string): string {\n const posted = new Date(dateStr);\n if (Number.isNaN(posted.getTime())) {\n return dateStr;\n }\n const days = Math.floor((Date.now() - posted.getTime()) / 86_400_000);\n if (days <= 0) {\n return 'Today';\n }\n if (days === 1) {\n return '1 day ago';\n }\n if (days < 7) {\n return `${days} days ago`;\n }\n if (days < 30) {\n const weeks = Math.floor(days / 7);\n return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`;\n }\n const months = Math.floor(days / 30);\n return months === 1 ? '1 month ago' : `${months} months ago`;\n}\n\n/** A label→value row matching the mock's clean scannable card layout. */\nfunction InfoRow({ label, children }: { label: string; children: ReactNode }) {\n return (\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span className=\"text-text-secondary\">{label}</span>\n <span className=\"font-medium text-text-primary text-right\">{children}</span>\n </div>\n );\n}\n\nexport function HiringJobsPagePage() {\n const { t: translate } = useI18n();\n const navigate = useNavigate();\n const { jobPostings, candidates } = useCrewFoundryStore();\n const [search, setSearch] = useState('');\n const [statusFilter, setStatusFilter] = useState<string>('all');\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 12 });\n const t = (key: string, fallback: string) => {\n const value = translate(key);\n return value === key ? fallback : value;\n };\n\n const filtered = useMemo(() => {\n const query = search.toLowerCase();\n return jobPostings.filter((j) => {\n const matchesSearch = `${j.title} ${j.department} ${j.location}`\n .toLowerCase()\n .includes(query);\n const matchesStatus = statusFilter === 'all' || j.status === statusFilter;\n return matchesSearch && matchesStatus;\n });\n }, [jobPostings, search, statusFilter]);\n\n const paged = filtered.slice(offset, offset + pageSize);\n const openJob = (id: string) =>\n navigate(`../hiring/job-details-page?id=${encodeURIComponent(id)}`);\n const activeJobs = jobPostings.filter((j) => j.status === 'active').length;\n const totalApplicants = jobPostings.reduce((s, j) => s + j.applicants, 0);\n\n // Live per-status counts drive the filter pills.\n const statusTabs = useMemo<SegmentedTab[]>(() => {\n const tabs: SegmentedTab[] = [{ id: 'all', label: 'All', count: jobPostings.length }];\n for (const status of STATUS_ORDER) {\n const count = jobPostings.filter((j) => j.status === status).length;\n if (count > 0 || status === 'active') {\n tabs.push({ id: status, label: formatEnumLabel(status), count });\n }\n }\n return tabs;\n }, [jobPostings]);\n\n // Real applicant faces per job, matched by the candidate's applied-for position.\n const facesByJob = useMemo(() => {\n const map = new Map<string, { name: string }[]>();\n for (const job of jobPostings) {\n const title = job.title.trim().toLowerCase();\n const people = candidates\n .filter((c) => c.position.trim().toLowerCase() === title)\n .map((c) => ({ name: `${c.firstName} ${c.lastName}`.trim() }));\n map.set(job.id, people);\n }\n return map;\n }, [jobPostings, candidates]);\n\n const setFilter = (id: string) => {\n setStatusFilter(id);\n setPage(1);\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{t('crewfoundry.hiringJobs.title', 'Job Postings')}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {activeJobs} {t('crewfoundry.hiringJobs.summary.activePositions', 'active positions')} ·{' '}\n {totalApplicants}{' '}\n {t('crewfoundry.hiringJobs.summary.totalApplicants', 'total applicants')}\n </p>\n <PagePurpose className=\"mt-1\">\n All your open and past job postings in one place — use it to monitor applicant volume\n and manage what roles you're actively hiring for.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => navigate('../hiring/create-job-page')}>\n <Plus className=\"h-4 w-4 mr-1\" />\n {t('crewfoundry.hiringJobs.actions.postNewJob', 'Post New Job')}\n </Button>\n </div>\n\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"-mx-1 overflow-x-auto px-1\">\n <SegmentedTabs\n tabs={statusTabs}\n value={statusFilter}\n onChange={setFilter}\n aria-label=\"Filter job postings by status\"\n />\n </div>\n <div className=\"relative w-full max-w-sm\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\" />\n <Input\n placeholder={t('crewfoundry.hiringJobs.searchPlaceholder', 'Search positions...')}\n className=\"pl-9\"\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(1);\n }}\n />\n </div>\n </div>\n\n {filtered.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No job postings found\"\n description=\"Try adjusting your search or status filter, or create a new job posting.\"\n />\n )}\n\n <div className=\"grid gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {paged.map((job) => {\n const faces = facesByJob.get(job.id) ?? [];\n return (\n <GlassCard\n key={job.id}\n treatment=\"glass\"\n role=\"button\"\n tabIndex={0}\n aria-label={`View ${job.title}`}\n className=\"flex cursor-pointer flex-col transition-shadow hover:shadow-elevation-2 focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n onClick={() => openJob(job.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n openJob(job.id);\n }\n }}\n >\n <CardHeader>\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-lg leading-snug\">{job.title}</CardTitle>\n <Badge\n variant={\n job.status === 'active'\n ? 'default'\n : job.status === 'draft'\n ? 'secondary'\n : 'outline'\n }\n >\n {formatEnumLabel(job.status)}\n </Badge>\n </div>\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {formatEnumLabel(job.type)}\n </Badge>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {formatEnumLabel(job.experienceLevel)}\n </Badge>\n </div>\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-2.5\">\n <InfoRow label={t('crewfoundry.hiringJobs.card.department', 'Department')}>\n <span className=\"inline-flex items-center gap-1\">\n <Briefcase className=\"h-3 w-3 text-text-secondary\" />\n {job.department || '—'}\n </span>\n </InfoRow>\n <InfoRow label={t('crewfoundry.hiringJobs.card.location', 'Location')}>\n <span className=\"inline-flex items-center gap-1\">\n <MapPin className=\"h-3 w-3 text-text-secondary\" />\n {job.location || '—'}\n </span>\n </InfoRow>\n <InfoRow label={t('crewfoundry.hiringJobs.card.salary', 'Salary')}>\n <span className=\"inline-flex items-center gap-0.5 tabular-nums\">\n <DollarSign className=\"h-3 w-3 text-text-secondary\" />\n {(job.salaryMin / 1000).toFixed(0)}k – ${(job.salaryMax / 1000).toFixed(0)}k\n </span>\n </InfoRow>\n <InfoRow label={t('crewfoundry.hiringJobs.card.applicantsLabel', 'Applicants')}>\n <span className=\"inline-flex items-center gap-2\">\n {faces.length > 0 && <AvatarStack people={faces} max={4} size={22} />}\n <span className=\"inline-flex items-center gap-1 tabular-nums\">\n <Users className=\"h-3 w-3 text-text-secondary\" />\n {job.applicants}\n </span>\n </span>\n </InfoRow>\n <div className=\"mt-auto pt-1\">\n <InfoRow label={t('crewfoundry.hiringJobs.card.posted', 'Posted')}>\n <span className=\"text-text-secondary font-normal\">\n {relativePostedLabel(job.postedDate)}\n </span>\n </InfoRow>\n </div>\n </CardContent>\n </GlassCard>\n );\n })}\n </div>\n\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"HiringJobsPagePage.js","names":[],"sources":["../../../src/crewfoundry/pages/HiringJobsPagePage.tsx"],"sourcesContent":["import { useMemo, useState, type ReactNode } from 'react';\nimport { useTr } from '../../shared/hooks/useTr';\nimport {\n Badge,\n Button,\n CardContent,\n CardHeader,\n CardTitle,\n GlassCard,\n GradientText,\n IllustratedEmptyState,\n Input,\n PagePurpose,\n Pagination,\n} from '@burdenoff/fe-libs/ui';\nimport { usePagination } from '@burdenoff/fe-libs/shared/hooks';\nimport { Briefcase, DollarSign, MapPin, Plus, Search, Users } from 'lucide-react';\nimport { useNavigate } from 'react-router';\nimport { useCrewFoundryStore, type JobPosting } from '../store/crewfoundryStore';\nimport { formatEnumLabel } from '../utils/formatEnumLabel';\nimport { AvatarStack, SegmentedTabs, type SegmentedTab } from '../components/viz';\n\n/** Ordered status filter set (only rendered when a status has postings). */\nconst STATUS_ORDER: JobPosting['status'][] = ['active', 'draft', 'paused', 'closed', 'filled'];\n\n/** Relative \"Posted N days ago\" label from an ISO date-only string. */\nfunction relativePostedLabel(dateStr: string): string {\n const posted = new Date(dateStr);\n if (Number.isNaN(posted.getTime())) {\n return dateStr;\n }\n const days = Math.floor((Date.now() - posted.getTime()) / 86_400_000);\n if (days <= 0) {\n return 'Today';\n }\n if (days === 1) {\n return '1 day ago';\n }\n if (days < 7) {\n return `${days} days ago`;\n }\n if (days < 30) {\n const weeks = Math.floor(days / 7);\n return weeks === 1 ? '1 week ago' : `${weeks} weeks ago`;\n }\n const months = Math.floor(days / 30);\n return months === 1 ? '1 month ago' : `${months} months ago`;\n}\n\n/** A label→value row matching the mock's clean scannable card layout. */\nfunction InfoRow({ label, children }: { label: string; children: ReactNode }) {\n return (\n <div className=\"flex items-center justify-between gap-3 text-sm\">\n <span className=\"text-text-secondary\">{label}</span>\n <span className=\"font-medium text-text-primary text-right\">{children}</span>\n </div>\n );\n}\n\nexport function HiringJobsPagePage() {\n const tr = useTr();\n const navigate = useNavigate();\n const { jobPostings, candidates } = useCrewFoundryStore();\n const [search, setSearch] = useState('');\n const [statusFilter, setStatusFilter] = useState<string>('all');\n const { page, pageSize, offset, setPage } = usePagination({ initialPageSize: 12 });\n const filtered = useMemo(() => {\n const query = search.toLowerCase();\n return jobPostings.filter((j) => {\n const matchesSearch = `${j.title} ${j.department} ${j.location}`\n .toLowerCase()\n .includes(query);\n const matchesStatus = statusFilter === 'all' || j.status === statusFilter;\n return matchesSearch && matchesStatus;\n });\n }, [jobPostings, search, statusFilter]);\n\n const paged = filtered.slice(offset, offset + pageSize);\n const openJob = (id: string) =>\n navigate(`../hiring/job-details-page?id=${encodeURIComponent(id)}`);\n const activeJobs = jobPostings.filter((j) => j.status === 'active').length;\n const totalApplicants = jobPostings.reduce((s, j) => s + j.applicants, 0);\n\n // Live per-status counts drive the filter pills.\n const statusTabs = useMemo<SegmentedTab[]>(() => {\n const tabs: SegmentedTab[] = [{ id: 'all', label: 'All', count: jobPostings.length }];\n for (const status of STATUS_ORDER) {\n const count = jobPostings.filter((j) => j.status === status).length;\n if (count > 0 || status === 'active') {\n tabs.push({ id: status, label: formatEnumLabel(status), count });\n }\n }\n return tabs;\n }, [jobPostings]);\n\n // Real applicant faces per job, matched by the candidate's applied-for position.\n const facesByJob = useMemo(() => {\n const map = new Map<string, { name: string }[]>();\n for (const job of jobPostings) {\n const title = job.title.trim().toLowerCase();\n const people = candidates\n .filter((c) => c.position.trim().toLowerCase() === title)\n .map((c) => ({ name: `${c.firstName} ${c.lastName}`.trim() }));\n map.set(job.id, people);\n }\n return map;\n }, [jobPostings, candidates]);\n\n const setFilter = (id: string) => {\n setStatusFilter(id);\n setPage(1);\n };\n\n return (\n <div className=\"space-y-6 p-6\">\n <div className=\"flex flex-col gap-4 sm:flex-row sm:items-start sm:justify-between\">\n <div>\n <h1 className=\"text-2xl font-semibold\">\n <GradientText>{tr('crewfoundry.hiringJobs.title', 'Job Postings')}</GradientText>\n </h1>\n <p className=\"text-sm text-text-secondary\">\n {activeJobs} {tr('crewfoundry.hiringJobs.summary.activePositions', 'active positions')}{' '}\n · {totalApplicants}{' '}\n {tr('crewfoundry.hiringJobs.summary.totalApplicants', 'total applicants')}\n </p>\n <PagePurpose className=\"mt-1\">\n All your open and past job postings in one place — use it to monitor applicant volume\n and manage what roles you're actively hiring for.\n </PagePurpose>\n </div>\n <Button size=\"sm\" onClick={() => navigate('../hiring/create-job-page')}>\n <Plus className=\"h-4 w-4 mr-1\" />\n {tr('crewfoundry.hiringJobs.actions.postNewJob', 'Post New Job')}\n </Button>\n </div>\n\n <div className=\"flex flex-col gap-3 lg:flex-row lg:items-center lg:justify-between\">\n <div className=\"-mx-1 overflow-x-auto px-1\">\n <SegmentedTabs\n tabs={statusTabs}\n value={statusFilter}\n onChange={setFilter}\n aria-label=\"Filter job postings by status\"\n />\n </div>\n <div className=\"relative w-full max-w-sm\">\n <Search className=\"absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-text-secondary\" />\n <Input\n placeholder={tr('crewfoundry.hiringJobs.searchPlaceholder', 'Search positions...')}\n className=\"pl-9\"\n value={search}\n onChange={(e) => {\n setSearch(e.target.value);\n setPage(1);\n }}\n />\n </div>\n </div>\n\n {filtered.length === 0 && (\n <IllustratedEmptyState\n illustration=\"empty-search\"\n title=\"No job postings found\"\n description=\"Try adjusting your search or status filter, or create a new job posting.\"\n />\n )}\n\n <div className=\"grid gap-4 md:grid-cols-2 xl:grid-cols-3\">\n {paged.map((job) => {\n const faces = facesByJob.get(job.id) ?? [];\n return (\n <GlassCard\n key={job.id}\n treatment=\"glass\"\n role=\"button\"\n tabIndex={0}\n aria-label={`View ${job.title}`}\n className=\"flex cursor-pointer flex-col transition-shadow hover:shadow-elevation-2 focus-visible:outline-none focus-visible:shadow-[var(--shadow-focus)]\"\n onClick={() => openJob(job.id)}\n onKeyDown={(e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n openJob(job.id);\n }\n }}\n >\n <CardHeader>\n <div className=\"flex items-start justify-between gap-2\">\n <CardTitle className=\"text-lg leading-snug\">{job.title}</CardTitle>\n <Badge\n variant={\n job.status === 'active'\n ? 'default'\n : job.status === 'draft'\n ? 'secondary'\n : 'outline'\n }\n >\n {formatEnumLabel(job.status)}\n </Badge>\n </div>\n <div className=\"mt-2 flex flex-wrap gap-1.5\">\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {formatEnumLabel(job.type)}\n </Badge>\n <Badge variant=\"outline\" className=\"text-[10px]\">\n {formatEnumLabel(job.experienceLevel)}\n </Badge>\n </div>\n </CardHeader>\n <CardContent className=\"flex flex-1 flex-col gap-2.5\">\n <InfoRow label={tr('crewfoundry.hiringJobs.card.department', 'Department')}>\n <span className=\"inline-flex items-center gap-1\">\n <Briefcase className=\"h-3 w-3 text-text-secondary\" />\n {job.department || '—'}\n </span>\n </InfoRow>\n <InfoRow label={tr('crewfoundry.hiringJobs.card.location', 'Location')}>\n <span className=\"inline-flex items-center gap-1\">\n <MapPin className=\"h-3 w-3 text-text-secondary\" />\n {job.location || '—'}\n </span>\n </InfoRow>\n <InfoRow label={tr('crewfoundry.hiringJobs.card.salary', 'Salary')}>\n <span className=\"inline-flex items-center gap-0.5 tabular-nums\">\n <DollarSign className=\"h-3 w-3 text-text-secondary\" />\n {(job.salaryMin / 1000).toFixed(0)}k – ${(job.salaryMax / 1000).toFixed(0)}k\n </span>\n </InfoRow>\n <InfoRow label={tr('crewfoundry.hiringJobs.card.applicantsLabel', 'Applicants')}>\n <span className=\"inline-flex items-center gap-2\">\n {faces.length > 0 && <AvatarStack people={faces} max={4} size={22} />}\n <span className=\"inline-flex items-center gap-1 tabular-nums\">\n <Users className=\"h-3 w-3 text-text-secondary\" />\n {job.applicants}\n </span>\n </span>\n </InfoRow>\n <div className=\"mt-auto pt-1\">\n <InfoRow label={tr('crewfoundry.hiringJobs.card.posted', 'Posted')}>\n <span className=\"text-text-secondary font-normal\">\n {relativePostedLabel(job.postedDate)}\n </span>\n </InfoRow>\n </div>\n </CardContent>\n </GlassCard>\n );\n })}\n </div>\n\n {filtered.length > pageSize && (\n <Pagination\n page={page}\n pageSize={pageSize}\n totalItems={filtered.length}\n onPageChange={setPage}\n />\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"MarketplaceMarketplacePagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/MarketplaceMarketplacePagePage.tsx"],"names":[],"mappings":"AAgDA,wBAAgB,8BAA8B,
|
|
1
|
+
{"version":3,"file":"MarketplaceMarketplacePagePage.d.ts","sourceRoot":"","sources":["../../../src/crewfoundry/pages/MarketplaceMarketplacePagePage.tsx"],"names":[],"mappings":"AAgDA,wBAAgB,8BAA8B,gCAsO7C"}
|