Skip to content

Commit e136ffc

Browse files
authored
Merge pull request #54 from reqcore-inc/ux/improve-filters-sync-&-pipeline
fixed-issue-#51-imroved-pipline
2 parents 01c10b0 + 07cc740 commit e136ffc

3 files changed

Lines changed: 90 additions & 22 deletions

File tree

app/pages/dashboard/applications/index.vue

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ useSeoMeta({
1111
description: 'Manage applications across all jobs',
1212
})
1313
14+
const route = useRoute()
1415
const router = useRouter()
1516
1617
// ── Search ────────────────────────────────────────────────────────────────────
@@ -31,7 +32,26 @@ watch(searchInput, (val) => {
3132
const STATUS_OPTIONS = ['new', 'screening', 'interview', 'offer', 'hired', 'rejected'] as const
3233
type Status = typeof STATUS_OPTIONS[number]
3334
34-
const activeStatus = useState<Status | undefined>('app-filter-status', () => undefined)
35+
const initialAppStatus = STATUS_OPTIONS.includes(route.query.status as any)
36+
? (route.query.status as Status)
37+
: undefined
38+
const activeStatus = useState<Status | undefined>('app-filter-status', () => initialAppStatus)
39+
// Ensure the state matches the URL on navigation (useState caches across client-side navigations)
40+
if (initialAppStatus !== undefined) {
41+
activeStatus.value = initialAppStatus
42+
}
43+
44+
// Sync the URL when the status filter changes
45+
watch(activeStatus, (newStatus) => {
46+
const query = { ...route.query }
47+
if (newStatus) {
48+
query.status = newStatus
49+
}
50+
else {
51+
delete query.status
52+
}
53+
router.replace({ query })
54+
})
3555
3656
const statusFilter = computed(() => activeStatus.value)
3757

app/pages/dashboard/index.vue

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,15 @@ const isEmpty = computed(() =>
264264
<!-- Pipeline breakdown -->
265265
<div class="lg:col-span-2 rounded-xl border border-surface-200 dark:border-surface-800 bg-white dark:bg-surface-900 p-6">
266266
<div class="flex items-center justify-between mb-5">
267-
<h2 class="text-base font-semibold text-surface-900 dark:text-surface-100">Pipeline Overview</h2>
267+
<div class="flex items-center gap-3">
268+
<h2 class="text-base font-semibold text-surface-900 dark:text-surface-100">Pipeline Overview</h2>
269+
<span
270+
v-if="pipelineTotal > 0"
271+
class="inline-flex items-center rounded-full bg-surface-100 dark:bg-surface-800 px-2.5 py-0.5 text-xs font-semibold text-surface-600 dark:text-surface-300"
272+
>
273+
{{ pipelineTotal }} total
274+
</span>
275+
</div>
268276
<NuxtLink
269277
to="/dashboard/applications"
270278
class="text-xs font-medium text-brand-600 dark:text-brand-400 hover:text-brand-700 dark:hover:text-brand-300 transition-colors no-underline"
@@ -273,42 +281,62 @@ const isEmpty = computed(() =>
273281
</NuxtLink>
274282
</div>
275283

276-
<!-- Pipeline bar -->
284+
<!-- Pipeline content -->
277285
<div v-if="pipelineTotal > 0">
278-
<div class="flex h-3 rounded-full overflow-hidden bg-surface-100 dark:bg-surface-800 mb-4">
286+
<!-- Segmented progress bar -->
287+
<div class="flex h-4 rounded-lg overflow-hidden bg-surface-100 dark:bg-surface-800 mb-6 gap-0.5">
279288
<NuxtLink
280289
v-for="segment in pipelineSegments"
281290
:key="segment.status"
282291
:to="`/dashboard/applications?status=${segment.status}`"
283-
:title="`${segment.label}: ${segment.count}`"
284-
class="transition-all hover:opacity-80 no-underline"
292+
:title="`${segment.label}: ${segment.count} (${segment.pct}%)`"
293+
class="transition-all duration-200 hover:opacity-80 hover:scale-y-110 origin-center no-underline first:rounded-l-lg last:rounded-r-lg"
285294
:class="segment.bg"
286-
:style="{ width: `${Math.max(segment.pct, 2)}%` }"
295+
:style="{ width: `${Math.max(segment.pct, 3)}%` }"
287296
/>
288297
</div>
289298

290-
<!-- Legend -->
291-
<div class="flex flex-wrap gap-x-5 gap-y-2">
299+
<!-- Stage cards grid -->
300+
<div class="grid grid-cols-2 sm:grid-cols-3 gap-3">
292301
<NuxtLink
293302
v-for="(config, status) in applicationStatusConfig"
294303
:key="status"
295304
:to="`/dashboard/applications?status=${status}`"
296-
class="inline-flex items-center gap-2 text-xs no-underline group/legend"
305+
class="group/stage relative rounded-lg border border-surface-100 dark:border-surface-800 p-3 hover:border-surface-300 dark:hover:border-surface-600 hover:shadow-sm transition-all no-underline"
297306
>
298-
<span class="size-2.5 rounded-full shrink-0" :class="config.bg" />
299-
<span class="text-surface-500 dark:text-surface-400 group-hover/legend:text-surface-700 dark:group-hover/legend:text-surface-200 transition-colors">
300-
{{ config.label }}
301-
</span>
302-
<span class="font-semibold text-surface-700 dark:text-surface-200">
303-
{{ (pipeline as Record<string, number>)[status] ?? 0 }}
304-
</span>
307+
<!-- Subtle top accent line -->
308+
<div class="absolute inset-x-0 top-0 h-0.5 rounded-t-lg opacity-60 group-hover/stage:opacity-100 transition-opacity" :class="config.bg" />
309+
310+
<div class="flex items-center gap-2 mb-2">
311+
<span class="rounded-md p-1.5 bg-surface-50 dark:bg-surface-800 group-hover/stage:bg-surface-100 dark:group-hover/stage:bg-surface-700 transition-colors">
312+
<component :is="config.icon" class="size-3.5" :class="config.color" />
313+
</span>
314+
<span class="text-xs font-medium text-surface-500 dark:text-surface-400 group-hover/stage:text-surface-700 dark:group-hover/stage:text-surface-200 transition-colors">
315+
{{ config.label }}
316+
</span>
317+
</div>
318+
319+
<div class="flex items-baseline gap-1.5">
320+
<span class="text-lg font-bold text-surface-900 dark:text-surface-100">
321+
{{ (pipeline as Record<string, number>)[status] ?? 0 }}
322+
</span>
323+
<span
324+
v-if="pipelineTotal > 0"
325+
class="text-xs text-surface-400 dark:text-surface-500"
326+
>
327+
{{ Math.round(((pipeline as Record<string, number>)[status] ?? 0) / pipelineTotal * 100) }}%
328+
</span>
329+
</div>
305330
</NuxtLink>
306331
</div>
307332
</div>
308333

309-
<div v-else class="text-center py-6">
310-
<Inbox class="size-8 text-surface-300 dark:text-surface-600 mx-auto mb-2" />
311-
<p class="text-sm text-surface-400 dark:text-surface-500">No applications in the pipeline yet.</p>
334+
<div v-else class="text-center py-8">
335+
<div class="rounded-full bg-surface-50 dark:bg-surface-800 p-3 w-fit mx-auto mb-3">
336+
<Inbox class="size-6 text-surface-300 dark:text-surface-600" />
337+
</div>
338+
<p class="text-sm font-medium text-surface-500 dark:text-surface-400 mb-1">No applications yet</p>
339+
<p class="text-xs text-surface-400 dark:text-surface-500">Applications will appear here as candidates apply.</p>
312340
</div>
313341
</div>
314342

app/pages/dashboard/jobs/index.vue

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,28 @@ useSeoMeta({
1111
description: 'Manage your job postings',
1212
})
1313
14-
const statusFilter = ref<string | undefined>(undefined)
15-
const viewMode = ref<'list' | 'gallery'>('list')
14+
const route = useRoute()
15+
const router = useRouter()
16+
17+
// Sync statusFilter with ?status= query param
18+
const validStatuses = ['draft', 'open', 'closed', 'archived'] as const
19+
const initialStatus = validStatuses.includes(route.query.status as any)
20+
? (route.query.status as string)
21+
: undefined
22+
const statusFilter = ref<string | undefined>(initialStatus)
23+
24+
// Sync viewMode with ?view= query param
25+
const initialView = route.query.view === 'gallery' ? 'gallery' : 'list'
26+
const viewMode = ref<'list' | 'gallery'>(initialView)
27+
28+
// Keep URL in sync when statusFilter or viewMode change
29+
watch([statusFilter, viewMode], ([newStatus, newView]) => {
30+
const query: Record<string, string> = {}
31+
if (newStatus) query.status = newStatus
32+
if (newView !== 'list') query.view = newView
33+
router.replace({ query })
34+
})
35+
1636
const { jobs, total, fetchStatus, error, refresh } = useJobs({ status: statusFilter })
1737
1838
const statusTabs = [

0 commit comments

Comments
 (0)