Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
9d4d38c
feat: add activity logging and permission management utilities
JoachimLK Mar 1, 2026
cf90ba5
feat: implement settings layout with sidebar navigation and update pa…
JoachimLK Mar 1, 2026
95f8722
feat: add error handling for creation failures in applications, candi…
JoachimLK Mar 1, 2026
8a470bf
feat: centralize status transition rules for applications and jobs
JoachimLK Mar 1, 2026
fb02324
feat: enhance comment permissions to allow deletion for members and u…
JoachimLK Mar 1, 2026
e6851a8
feat: enhance settings and member management with error handling, sea…
JoachimLK Mar 1, 2026
91b480d
feat: implement invitation management with email sending, resending, …
JoachimLK Mar 1, 2026
d08e1bd
feat: add schemas for invite links and join requests
JoachimLK Mar 2, 2026
37cc33d
feat: implement invite link management with API integration for fetch…
JoachimLK Mar 2, 2026
98add59
feat: add JobPipelineMini component for visualizing job application p…
JoachimLK Mar 2, 2026
8d48fd7
refactor: redirect jobs view to main dashboard and update navigation …
JoachimLK Mar 3, 2026
4bd4a70
feat: implement AppTopBar component and enhance layout with responsiv…
JoachimLK Mar 3, 2026
6952bdf
feat: enhance styling and layout of candidate and pipeline components…
JoachimLK Mar 3, 2026
7bb746b
feat: enhance candidate header with status indicators and improved la…
JoachimLK Mar 3, 2026
656aa97
feat: implement scroll-to-section navigation for job detail tabs and …
JoachimLK Mar 3, 2026
a2dc75e
feat: enhance job detail page with sticky status transitions and impr…
JoachimLK Mar 3, 2026
083886a
feat: update job creation and candidate application flows to redirect…
JoachimLK Mar 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
819 changes: 819 additions & 0 deletions TESTING-SECURITY.md

Large diffs are not rendered by default.

29 changes: 29 additions & 0 deletions app/assets/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -325,3 +325,32 @@
-webkit-text-fill-color: transparent;
background-clip: text;
}

/* ── Pipeline view premium touches ──────────────────── */

/* Smooth candidate card highlight on selection */
.pipeline-candidate-card {
position: relative;
transition: all 0.15s ease;
}

.pipeline-candidate-card::after {
content: '';
position: absolute;
inset: 1px;
border-radius: 6px;
opacity: 0;
pointer-events: none;
box-shadow: 0 0 0 1px var(--color-brand-200);
transition: opacity 0.15s ease;
}

.pipeline-candidate-card:hover::after {
opacity: 0.4;
}

/* Pipeline status dot subtle pulse on active tab */
@keyframes pipeline-dot-pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
146 changes: 89 additions & 57 deletions app/components/AppSidebar.vue
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<script setup lang="ts">
import {
LayoutDashboard, Briefcase, Users, Inbox,
ChevronLeft, Eye, Kanban, FileText, LogOut, Table2, Hand,
Sun, Moon, MessageSquarePlus,
Briefcase, Plus, Bell,
ChevronLeft, Kanban, FileText, LogOut, Table2,
Sun, Moon, MessageSquarePlus, Settings,
} from 'lucide-vue-next'

const route = useRoute()
Expand All @@ -24,13 +24,6 @@ async function handleSignOut() {
await navigateTo(localePath('/auth/sign-in'))
}

const navItems = [
{ label: 'Dashboard', to: '/dashboard', icon: LayoutDashboard, exact: true },
{ label: 'Jobs', to: '/dashboard/jobs', icon: Briefcase, exact: false },
{ label: 'Candidates', to: '/dashboard/candidates', icon: Users, exact: false },
{ label: 'Applications', to: '/dashboard/applications', icon: Inbox, exact: false },
]

// ─────────────────────────────────────────────
// Dynamic job context — detect when viewing a specific job
// ─────────────────────────────────────────────
Expand Down Expand Up @@ -58,6 +51,23 @@ const {

const sidebarJobs = computed(() => sidebarJobsData.value?.data ?? [])

// Active jobs sorted by urgency (most new applications first)
const activeJobsSorted = computed(() => {
return [...sidebarJobs.value].sort((a, b) => {
const aNew = a.pipeline?.new ?? 0
const bNew = b.pipeline?.new ?? 0
if (aNew !== bNew) return bNew - aNew
return new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
})
})

// Currently-viewed job title (for context header)
const activeJobTitle = computed(() => {
if (!activeJobId.value) return null
const found = sidebarJobs.value.find((j: any) => j.id === activeJobId.value)
return found?.title ?? 'Job'
})

const { data: feedbackConfig } = useFetch('/api/feedback/config', {
key: 'feedback-config',
headers: useRequestHeaders(['cookie']),
Expand All @@ -69,10 +79,8 @@ const jobTabs = computed(() => {
if (!activeJobId.value) return []
const base = `/dashboard/jobs/${activeJobId.value}`
return [
{ label: 'Overview', to: base, icon: Eye, exact: true },
{ label: 'Pipeline', to: `${base}/pipeline`, icon: Kanban, exact: true },
{ label: 'Swipe', to: `${base}/swipe`, icon: Hand, exact: true },
{ label: 'Candidates', to: `${base}/candidates`, icon: Table2, exact: true },
{ label: 'Pipeline', to: base, icon: Kanban, exact: true },
{ label: 'Table', to: `${base}/candidates`, icon: Table2, exact: true },
{ label: 'Application Form', to: `${base}/application-form`, icon: FileText, exact: true },
]
})
Expand All @@ -89,7 +97,7 @@ function isActiveTab(to: string, exact: boolean) {
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"
>
<!-- Top -->
<div class="flex flex-col gap-5">
<div class="flex flex-col gap-4">
<!-- Logo -->
<NuxtLink :to="$localePath('/')" class="flex items-center gap-2 px-2 no-underline">
<img src="/eagle-mascot-logo.png" alt="Reqcore mascot" class="size-7 shrink-0 object-contain" />
Expand All @@ -105,72 +113,96 @@ function isActiveTab(to: string, exact: boolean) {
<OrgSwitcher />
</div>

<!-- Main navigation -->
<nav class="flex flex-col gap-0.5">
<!-- New Job button -->
<NuxtLink
:to="$localePath('/dashboard/jobs/new')"
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"
>
<Plus class="size-4" />
New Job
</NuxtLink>

<!-- Job context sub-nav (when viewing a specific job) -->
<div v-if="activeJobId" class="border-t border-surface-200 dark:border-surface-800 pt-3">
<NuxtLink
v-for="item in navItems"
:key="item.to"
:to="$localePath(item.to)"
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"
:class="isActiveTab(item.to, item.exact)
? 'bg-surface-100 dark:bg-surface-800 text-surface-900 dark:text-surface-100 font-medium'
: ''"
:to="$localePath('/dashboard')"
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"
>
<component :is="item.icon" class="size-4 shrink-0" />
{{ item.label }}
<ChevronLeft class="size-3.5" />
All Jobs
</NuxtLink>
</nav>

<!-- Job context sub-nav (when viewing a specific job) -->
<div v-if="showJobsList" class="border-t border-surface-200 dark:border-surface-800 pt-4">
<!-- Active job title -->
<div class="px-3 pb-2">
<div class="flex items-center gap-2">
<Briefcase class="size-3.5 text-brand-500 shrink-0" />
<span class="text-sm font-semibold text-surface-900 dark:text-surface-100 truncate">
{{ activeJobTitle }}
</span>
</div>
</div>

<nav class="flex flex-col gap-0.5">
<NuxtLink
v-for="tab in jobTabs"
:key="tab.to"
:to="$localePath(tab.to)"
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"
:class="isActiveTab(tab.to, tab.exact)
? 'bg-surface-100 dark:bg-surface-800 text-surface-900 dark:text-surface-100 font-medium'
: ''"
>
<component :is="tab.icon" class="size-4 shrink-0" />
{{ tab.label }}
</NuxtLink>
</nav>
</div>

<!-- Jobs list (when not viewing a specific job) -->
<div v-if="showJobsList" class="border-t border-surface-200 dark:border-surface-800 pt-3">
<div class="px-3 pb-2 text-xs font-medium uppercase tracking-wide text-surface-500 dark:text-surface-400">
Jobs
My Jobs
</div>

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

<nav v-else class="flex max-h-56 flex-col gap-0.5 overflow-y-auto">
<nav v-else class="flex flex-col gap-0.5 overflow-y-auto max-h-[calc(100vh-24rem)]">
<NuxtLink
v-for="job in sidebarJobs"
v-for="job in activeJobsSorted"
:key="job.id"
:to="$localePath(`/dashboard/jobs/${job.id}`)"
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"
:title="job.title"
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"
>
{{ job.title }}
<span class="truncate flex-1 mr-2">{{ job.title }}</span>
<span
v-if="(job.pipeline?.new ?? 0) > 0"
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"
:title="`${job.pipeline!.new} new application${job.pipeline!.new === 1 ? '' : 's'}`"
>
{{ job.pipeline!.new }}
</span>
</NuxtLink>

<div v-if="sidebarJobs.length === 0" class="px-3 py-2 text-xs text-surface-400">
No jobs yet
<div v-if="sidebarJobs.length === 0" class="px-3 py-4 text-center">
<p class="text-xs text-surface-400 mb-2">No jobs yet</p>
</div>
</nav>
</div>

<div v-if="activeJobId" class="border-t border-surface-200 dark:border-surface-800 pt-4">
<!-- Settings link -->
<div class="border-t border-surface-200 dark:border-surface-800 pt-3">
<NuxtLink
:to="$localePath('/dashboard/jobs')"
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"
:to="$localePath('/dashboard/settings')"
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"
:class="isActiveTab('/dashboard/settings', false)
? 'bg-surface-100 dark:bg-surface-800 text-surface-900 dark:text-surface-100 font-medium'
: ''"
>
<ChevronLeft class="size-3.5" />
All Jobs
<Settings class="size-4 shrink-0" />
Settings
</NuxtLink>

<nav class="flex flex-col gap-0.5">
<NuxtLink
v-for="tab in jobTabs"
:key="tab.to"
:to="$localePath(tab.to)"
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"
:class="isActiveTab(tab.to, tab.exact)
? 'bg-surface-100 dark:bg-surface-800 text-surface-900 dark:text-surface-100 font-medium'
: ''"
>
<component :is="tab.icon" class="size-4 shrink-0" />
{{ tab.label }}
</NuxtLink>
</nav>
</div>
</div>

Expand Down
Loading
Loading