Skip to content

Commit 68dd20d

Browse files
authored
Merge pull request #65 from reqcore-inc/feat/team-collaboration
Feat/team-collaboration
2 parents d283801 + 083886a commit 68dd20d

96 files changed

Lines changed: 12084 additions & 2371 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

TESTING-SECURITY.md

Lines changed: 819 additions & 0 deletions
Large diffs are not rendered by default.

app/assets/css/main.css

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,3 +325,32 @@
325325
-webkit-text-fill-color: transparent;
326326
background-clip: text;
327327
}
328+
329+
/* ── Pipeline view premium touches ──────────────────── */
330+
331+
/* Smooth candidate card highlight on selection */
332+
.pipeline-candidate-card {
333+
position: relative;
334+
transition: all 0.15s ease;
335+
}
336+
337+
.pipeline-candidate-card::after {
338+
content: '';
339+
position: absolute;
340+
inset: 1px;
341+
border-radius: 6px;
342+
opacity: 0;
343+
pointer-events: none;
344+
box-shadow: 0 0 0 1px var(--color-brand-200);
345+
transition: opacity 0.15s ease;
346+
}
347+
348+
.pipeline-candidate-card:hover::after {
349+
opacity: 0.4;
350+
}
351+
352+
/* Pipeline status dot subtle pulse on active tab */
353+
@keyframes pipeline-dot-pulse {
354+
0%, 100% { opacity: 1; }
355+
50% { opacity: 0.5; }
356+
}

app/components/AppSidebar.vue

Lines changed: 89 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
<script setup lang="ts">
22
import {
3-
LayoutDashboard, Briefcase, Users, Inbox,
4-
ChevronLeft, Eye, Kanban, FileText, LogOut, Table2, Hand,
5-
Sun, Moon, MessageSquarePlus,
3+
Briefcase, Plus, Bell,
4+
ChevronLeft, Kanban, FileText, LogOut, Table2,
5+
Sun, Moon, MessageSquarePlus, Settings,
66
} from 'lucide-vue-next'
77
88
const route = useRoute()
@@ -24,13 +24,6 @@ async function handleSignOut() {
2424
await navigateTo(localePath('/auth/sign-in'))
2525
}
2626
27-
const navItems = [
28-
{ label: 'Dashboard', to: '/dashboard', icon: LayoutDashboard, exact: true },
29-
{ label: 'Jobs', to: '/dashboard/jobs', icon: Briefcase, exact: false },
30-
{ label: 'Candidates', to: '/dashboard/candidates', icon: Users, exact: false },
31-
{ label: 'Applications', to: '/dashboard/applications', icon: Inbox, exact: false },
32-
]
33-
3427
// ─────────────────────────────────────────────
3528
// Dynamic job context — detect when viewing a specific job
3629
// ─────────────────────────────────────────────
@@ -58,6 +51,23 @@ const {
5851
5952
const sidebarJobs = computed(() => sidebarJobsData.value?.data ?? [])
6053
54+
// Active jobs sorted by urgency (most new applications first)
55+
const activeJobsSorted = computed(() => {
56+
return [...sidebarJobs.value].sort((a, b) => {
57+
const aNew = a.pipeline?.new ?? 0
58+
const bNew = b.pipeline?.new ?? 0
59+
if (aNew !== bNew) return bNew - aNew
60+
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
61+
})
62+
})
63+
64+
// Currently-viewed job title (for context header)
65+
const activeJobTitle = computed(() => {
66+
if (!activeJobId.value) return null
67+
const found = sidebarJobs.value.find((j: any) => j.id === activeJobId.value)
68+
return found?.title ?? 'Job'
69+
})
70+
6171
const { data: feedbackConfig } = useFetch('/api/feedback/config', {
6272
key: 'feedback-config',
6373
headers: useRequestHeaders(['cookie']),
@@ -69,10 +79,8 @@ const jobTabs = computed(() => {
6979
if (!activeJobId.value) return []
7080
const base = `/dashboard/jobs/${activeJobId.value}`
7181
return [
72-
{ label: 'Overview', to: base, icon: Eye, exact: true },
73-
{ label: 'Pipeline', to: `${base}/pipeline`, icon: Kanban, exact: true },
74-
{ label: 'Swipe', to: `${base}/swipe`, icon: Hand, exact: true },
75-
{ label: 'Candidates', to: `${base}/candidates`, icon: Table2, exact: true },
82+
{ label: 'Pipeline', to: base, icon: Kanban, exact: true },
83+
{ label: 'Table', to: `${base}/candidates`, icon: Table2, exact: true },
7684
{ label: 'Application Form', to: `${base}/application-form`, icon: FileText, exact: true },
7785
]
7886
})
@@ -89,7 +97,7 @@ function isActiveTab(to: string, exact: boolean) {
8997
class="sticky top-0 self-start flex h-screen max-h-screen flex-col justify-between w-60 min-w-60 bg-white dark:bg-surface-900 border-r border-surface-200 dark:border-surface-800 py-5 px-3 overflow-y-auto"
9098
>
9199
<!-- Top -->
92-
<div class="flex flex-col gap-5">
100+
<div class="flex flex-col gap-4">
93101
<!-- Logo -->
94102
<NuxtLink :to="$localePath('/')" class="flex items-center gap-2 px-2 no-underline">
95103
<img src="/eagle-mascot-logo.png" alt="Reqcore mascot" class="size-7 shrink-0 object-contain" />
@@ -105,72 +113,96 @@ function isActiveTab(to: string, exact: boolean) {
105113
<OrgSwitcher />
106114
</div>
107115

108-
<!-- Main navigation -->
109-
<nav class="flex flex-col gap-0.5">
116+
<!-- New Job button -->
117+
<NuxtLink
118+
:to="$localePath('/dashboard/jobs/new')"
119+
class="flex items-center justify-center gap-2 rounded-lg bg-brand-600 px-3 py-2 text-sm font-medium text-white hover:bg-brand-700 transition-colors no-underline"
120+
>
121+
<Plus class="size-4" />
122+
New Job
123+
</NuxtLink>
124+
125+
<!-- Job context sub-nav (when viewing a specific job) -->
126+
<div v-if="activeJobId" class="border-t border-surface-200 dark:border-surface-800 pt-3">
110127
<NuxtLink
111-
v-for="item in navItems"
112-
:key="item.to"
113-
:to="$localePath(item.to)"
114-
class="flex items-center gap-2.5 px-3 py-2 rounded-md text-sm text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-100 transition-colors no-underline"
115-
:class="isActiveTab(item.to, item.exact)
116-
? 'bg-surface-100 dark:bg-surface-800 text-surface-900 dark:text-surface-100 font-medium'
117-
: ''"
128+
:to="$localePath('/dashboard')"
129+
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-surface-500 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-200 transition-colors no-underline mb-2"
118130
>
119-
<component :is="item.icon" class="size-4 shrink-0" />
120-
{{ item.label }}
131+
<ChevronLeft class="size-3.5" />
132+
All Jobs
121133
</NuxtLink>
122-
</nav>
123134

124-
<!-- Job context sub-nav (when viewing a specific job) -->
125-
<div v-if="showJobsList" class="border-t border-surface-200 dark:border-surface-800 pt-4">
135+
<!-- Active job title -->
136+
<div class="px-3 pb-2">
137+
<div class="flex items-center gap-2">
138+
<Briefcase class="size-3.5 text-brand-500 shrink-0" />
139+
<span class="text-sm font-semibold text-surface-900 dark:text-surface-100 truncate">
140+
{{ activeJobTitle }}
141+
</span>
142+
</div>
143+
</div>
144+
145+
<nav class="flex flex-col gap-0.5">
146+
<NuxtLink
147+
v-for="tab in jobTabs"
148+
:key="tab.to"
149+
:to="$localePath(tab.to)"
150+
class="flex items-center gap-2.5 px-3 py-2 rounded-md text-sm text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-100 transition-colors no-underline"
151+
:class="isActiveTab(tab.to, tab.exact)
152+
? 'bg-surface-100 dark:bg-surface-800 text-surface-900 dark:text-surface-100 font-medium'
153+
: ''"
154+
>
155+
<component :is="tab.icon" class="size-4 shrink-0" />
156+
{{ tab.label }}
157+
</NuxtLink>
158+
</nav>
159+
</div>
160+
161+
<!-- Jobs list (when not viewing a specific job) -->
162+
<div v-if="showJobsList" class="border-t border-surface-200 dark:border-surface-800 pt-3">
126163
<div class="px-3 pb-2 text-xs font-medium uppercase tracking-wide text-surface-500 dark:text-surface-400">
127-
Jobs
164+
My Jobs
128165
</div>
129166

130167
<div v-if="sidebarJobsStatus === 'pending'" class="px-3 py-2 text-xs text-surface-400">
131168
Loading jobs…
132169
</div>
133170

134-
<nav v-else class="flex max-h-56 flex-col gap-0.5 overflow-y-auto">
171+
<nav v-else class="flex flex-col gap-0.5 overflow-y-auto max-h-[calc(100vh-24rem)]">
135172
<NuxtLink
136-
v-for="job in sidebarJobs"
173+
v-for="job in activeJobsSorted"
137174
:key="job.id"
138175
:to="$localePath(`/dashboard/jobs/${job.id}`)"
139-
class="px-3 py-2 rounded-md text-sm text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-100 transition-colors no-underline truncate"
140-
:title="job.title"
176+
class="flex items-center justify-between px-3 py-2 rounded-md text-sm text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-100 transition-colors no-underline group"
141177
>
142-
{{ job.title }}
178+
<span class="truncate flex-1 mr-2">{{ job.title }}</span>
179+
<span
180+
v-if="(job.pipeline?.new ?? 0) > 0"
181+
class="inline-flex items-center justify-center min-w-5 h-5 rounded-full bg-warning-100 dark:bg-warning-950 text-warning-700 dark:text-warning-400 text-[11px] font-semibold px-1.5 shrink-0"
182+
:title="`${job.pipeline!.new} new application${job.pipeline!.new === 1 ? '' : 's'}`"
183+
>
184+
{{ job.pipeline!.new }}
185+
</span>
143186
</NuxtLink>
144187

145-
<div v-if="sidebarJobs.length === 0" class="px-3 py-2 text-xs text-surface-400">
146-
No jobs yet
188+
<div v-if="sidebarJobs.length === 0" class="px-3 py-4 text-center">
189+
<p class="text-xs text-surface-400 mb-2">No jobs yet</p>
147190
</div>
148191
</nav>
149192
</div>
150193

151-
<div v-if="activeJobId" class="border-t border-surface-200 dark:border-surface-800 pt-4">
194+
<!-- Settings link -->
195+
<div class="border-t border-surface-200 dark:border-surface-800 pt-3">
152196
<NuxtLink
153-
:to="$localePath('/dashboard/jobs')"
154-
class="flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium text-surface-500 dark:text-surface-400 hover:text-surface-700 dark:hover:text-surface-200 transition-colors no-underline mb-2"
197+
:to="$localePath('/dashboard/settings')"
198+
class="flex items-center gap-2.5 px-3 py-2 rounded-md text-sm text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-100 transition-colors no-underline"
199+
:class="isActiveTab('/dashboard/settings', false)
200+
? 'bg-surface-100 dark:bg-surface-800 text-surface-900 dark:text-surface-100 font-medium'
201+
: ''"
155202
>
156-
<ChevronLeft class="size-3.5" />
157-
All Jobs
203+
<Settings class="size-4 shrink-0" />
204+
Settings
158205
</NuxtLink>
159-
160-
<nav class="flex flex-col gap-0.5">
161-
<NuxtLink
162-
v-for="tab in jobTabs"
163-
:key="tab.to"
164-
:to="$localePath(tab.to)"
165-
class="flex items-center gap-2.5 px-3 py-2 rounded-md text-sm text-surface-600 dark:text-surface-400 hover:bg-surface-100 dark:hover:bg-surface-800 hover:text-surface-900 dark:hover:text-surface-100 transition-colors no-underline"
166-
:class="isActiveTab(tab.to, tab.exact)
167-
? 'bg-surface-100 dark:bg-surface-800 text-surface-900 dark:text-surface-100 font-medium'
168-
: ''"
169-
>
170-
<component :is="tab.icon" class="size-4 shrink-0" />
171-
{{ tab.label }}
172-
</NuxtLink>
173-
</nav>
174206
</div>
175207
</div>
176208

0 commit comments

Comments
 (0)