Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
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
14 changes: 0 additions & 14 deletions .github/instructions/snyk_rules.instructions.md

This file was deleted.

2 changes: 2 additions & 0 deletions app/composables/useJob.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ export function useJob(id: MaybeRefOrGetter<string>) {
location: string
type: 'full_time' | 'part_time' | 'contract' | 'internship'
status: 'draft' | 'open' | 'closed' | 'archived'
requireResume: boolean
requireCoverLetter: boolean
}>) {
try {
const updated = await $fetch(`/api/jobs/${jobId.value}`, {
Expand Down
2 changes: 2 additions & 0 deletions app/composables/useJobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ export function useJobs(options?: {
description?: string
location?: string
type?: 'full_time' | 'part_time' | 'contract' | 'internship'
requireResume?: boolean
requireCoverLetter?: boolean
}) {
try {
const created = await $fetch('/api/jobs', {
Expand Down
92 changes: 90 additions & 2 deletions app/pages/dashboard/jobs/[id]/application-form.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { FileText, Link2, ClipboardCopy } from 'lucide-vue-next'
import { FileText, Link2, ClipboardCopy, Check } from 'lucide-vue-next'

definePageMeta({
layout: 'dashboard',
Expand All @@ -9,7 +9,7 @@ definePageMeta({
const route = useRoute()
const jobId = route.params.id as string

const { job, status: fetchStatus, error } = useJob(jobId)
const { job, status: fetchStatus, error, updateJob } = useJob(jobId)

useSeoMeta({
title: computed(() =>
Expand Down Expand Up @@ -39,6 +39,34 @@ async function copyApplicationLink() {
alert(applicationUrl.value)
}
}

// ─────────────────────────────────────────────
// Application requirements (resume / cover letter)
// ─────────────────────────────────────────────

const requireResume = ref(false)
const requireCoverLetter = ref(false)
const isSavingRequirements = ref(false)
const requirementsSaved = ref(false)

// Sync with fetched job data
watch(job, (j) => {
if (j) {
requireResume.value = j.requireResume ?? false
requireCoverLetter.value = j.requireCoverLetter ?? false
}
}, { immediate: true })

async function saveRequirements() {
isSavingRequirements.value = true
try {
await updateJob({ requireResume: requireResume.value, requireCoverLetter: requireCoverLetter.value })
requirementsSaved.value = true
setTimeout(() => { requirementsSaved.value = false }, 2000)
} finally {
isSavingRequirements.value = false
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
}
</script>

<template>
Expand Down Expand Up @@ -96,6 +124,66 @@ async function copyApplicationLink() {
The application link will be available when this job is published (status: <strong>open</strong>).
</div>

<!-- Application Requirements -->
<div class="rounded-lg border border-surface-200 dark:border-surface-800 bg-white dark:bg-surface-900 p-5 mb-6">
<h2 class="text-sm font-semibold text-surface-700 dark:text-surface-300 mb-1">Application requirements</h2>
<p class="text-xs text-surface-400 dark:text-surface-500 mb-4">
Choose what candidates must provide when applying.
</p>
<div class="grid grid-cols-1 sm:grid-cols-2 gap-3 mb-4">
<button
type="button"
class="relative flex items-center gap-3 p-4 rounded-xl border text-left transition-colors"
:class="requireResume
? 'border-brand-300 dark:border-brand-700 bg-brand-50/70 dark:bg-brand-950/30'
: 'border-surface-200 dark:border-surface-800 hover:bg-surface-50 dark:hover:bg-surface-800/50'"
:aria-pressed="requireResume"
@click="requireResume = !requireResume"
>
<span
v-if="requireResume"
class="absolute top-3 right-3 inline-flex items-center justify-center size-5 rounded-full bg-brand-600 text-white"
aria-hidden="true"
>
<Check class="size-3" />
</span>
<div>
<span class="block text-sm font-medium text-surface-900 dark:text-surface-100">Require resume/CV</span>
<span class="text-xs text-surface-500">Candidates must upload a file.</span>
</div>
</button>
<button
type="button"
class="relative flex items-center gap-3 p-4 rounded-xl border text-left transition-colors"
:class="requireCoverLetter
? 'border-brand-300 dark:border-brand-700 bg-brand-50/70 dark:bg-brand-950/30'
: 'border-surface-200 dark:border-surface-800 hover:bg-surface-50 dark:hover:bg-surface-800/50'"
:aria-pressed="requireCoverLetter"
@click="requireCoverLetter = !requireCoverLetter"
>
<span
v-if="requireCoverLetter"
class="absolute top-3 right-3 inline-flex items-center justify-center size-5 rounded-full bg-brand-600 text-white"
aria-hidden="true"
>
<Check class="size-3" />
</span>
<div>
<span class="block text-sm font-medium text-surface-900 dark:text-surface-100">Ask for cover letter</span>
<span class="text-xs text-surface-500">Candidates can write a cover letter.</span>
</div>
</button>
</div>
<button
type="button"
:disabled="isSavingRequirements"
class="inline-flex items-center gap-1.5 rounded-lg bg-brand-600 px-4 py-2 text-sm font-medium text-white hover:bg-brand-700 disabled:opacity-50 transition-colors"
@click="saveRequirements"
>
{{ requirementsSaved ? 'Saved!' : isSavingRequirements ? 'Saving…' : 'Save requirements' }}
</button>
</div>

<!-- Application Form Questions -->
<div class="rounded-lg border border-surface-200 dark:border-surface-800 bg-white dark:bg-surface-900 p-5">
<div class="flex items-center gap-2 mb-3">
Expand Down
105 changes: 99 additions & 6 deletions app/pages/dashboard/jobs/[id]/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import {
ArrowLeft, ArrowRight, Briefcase, Clock, Hash, UserRound, Mail, MessageSquare,
FileText, Paperclip, Download, Eye, Phone, Search, ExternalLink,
UserPlus, Pencil, Trash2, MoreHorizontal, Globe, ChevronDown,
UserPlus, Pencil, Trash2, MoreHorizontal, Globe, ChevronDown, X,
} from 'lucide-vue-next'
import { z } from 'zod'
import { usePreviewReadOnly } from '~/composables/usePreviewReadOnly'
Expand Down Expand Up @@ -353,6 +353,11 @@ function goToNextCard() {
}

function handleKeyNavigation(event: KeyboardEvent) {
if (event.key === 'Escape' && showDocPreview.value) {
closeDocPreview()
return
}

if ((event.target as HTMLElement)?.tagName === 'INPUT' || (event.target as HTMLElement)?.tagName === 'TEXTAREA' || (event.target as HTMLElement)?.tagName === 'SELECT') return

if (event.key === 'ArrowUp') {
Expand Down Expand Up @@ -557,6 +562,41 @@ onBeforeUnmount(() => {
const isLoading = computed(() => {
return jobFetchStatus.value === 'pending' || appFetchStatus.value === 'pending'
})

// ─────────────────────────────────────────────
// Document preview
// ─────────────────────────────────────────────

const { getPreviewUrl } = useDocuments()

const showDocPreview = ref(false)
const docPreviewUrl = ref<string | null>(null)
const docPreviewFilename = ref('')
const docPreviewMimeType = ref('')
const docPreviewDocId = ref<string | null>(null)

const isDocPreviewPdf = computed(() => docPreviewMimeType.value === 'application/pdf')

function handleDocPreview(doc: SwipeDocument) {
if (doc.mimeType !== 'application/pdf') {
// Non-PDFs: fall back to download
window.open(`/api/documents/${doc.id}/download`, '_blank')
return
}
docPreviewDocId.value = doc.id
docPreviewFilename.value = doc.originalFilename
docPreviewMimeType.value = doc.mimeType
docPreviewUrl.value = getPreviewUrl(doc.id)
showDocPreview.value = true
}

function closeDocPreview() {
showDocPreview.value = false
docPreviewUrl.value = null
docPreviewFilename.value = ''
docPreviewMimeType.value = ''
docPreviewDocId.value = null
}
</script>

<template>
Expand Down Expand Up @@ -1068,15 +1108,13 @@ const isLoading = computed(() => {
</div>
</div>
<div class="flex items-center gap-2">
<a
:href="`/api/documents/${doc.id}/preview`"
target="_blank"
rel="noopener noreferrer"
<button
class="inline-flex items-center gap-1.5 rounded-lg border border-surface-200 px-3 py-1.5 text-xs font-medium text-surface-600 hover:bg-surface-50 hover:border-surface-300 dark:border-surface-700 dark:text-surface-300 dark:hover:bg-surface-800 dark:hover:border-surface-600 transition-all duration-150"
@click="handleDocPreview(doc)"
>
<Eye class="size-3.5" />
Preview
</a>
</button>
<a
:href="`/api/documents/${doc.id}/download`"
class="inline-flex items-center gap-1.5 rounded-lg border border-surface-200 px-3 py-1.5 text-xs font-medium text-surface-600 hover:bg-surface-50 hover:border-surface-300 dark:border-surface-700 dark:text-surface-300 dark:hover:bg-surface-800 dark:hover:border-surface-600 transition-all duration-150"
Expand Down Expand Up @@ -1250,5 +1288,60 @@ const isLoading = computed(() => {
@close="showApplyModal = false"
@created="handleCandidateApplied"
/>

<!-- Document Preview Modal -->
<Teleport to="body">
<div v-if="showDocPreview" class="fixed inset-0 z-50 flex items-center justify-center px-4 py-6">
<div class="absolute inset-0 bg-black/60 backdrop-blur-sm" @click="closeDocPreview" />
<div class="relative flex flex-col bg-white dark:bg-surface-900 rounded-2xl shadow-2xl shadow-surface-900/10 dark:shadow-black/30 ring-1 ring-surface-200/80 dark:ring-surface-700/60 w-full max-w-4xl" style="height: calc(100vh - 3rem);">
<!-- Header -->
<div class="flex items-center justify-between px-5 py-3 border-b border-surface-200/80 dark:border-surface-800/60 shrink-0">
<div class="flex items-center gap-2.5 min-w-0">
<FileText class="size-4 text-surface-400 shrink-0" />
<span class="text-sm font-medium text-surface-800 dark:text-surface-100 truncate">{{ docPreviewFilename }}</span>
</div>
<div class="flex items-center gap-2 shrink-0 ml-4">
<a
v-if="docPreviewDocId"
:href="`/api/documents/${docPreviewDocId}/download`"
class="inline-flex items-center gap-1.5 rounded-lg border border-surface-200 px-2.5 py-1.5 text-xs font-medium text-surface-600 hover:bg-surface-50 hover:border-surface-300 dark:border-surface-700 dark:text-surface-300 dark:hover:bg-surface-800 transition-all duration-150"
>
<Download class="size-3.5" />
Download
</a>
<button
class="rounded-lg p-1.5 text-surface-500 hover:text-surface-700 hover:bg-surface-100 dark:hover:text-surface-300 dark:hover:bg-surface-800 transition-colors"
title="Close"
@click="closeDocPreview"
>
<X class="size-4" />
</button>
</div>
</div>
<!-- PDF viewer -->
<iframe
v-if="docPreviewUrl && isDocPreviewPdf"
:src="docPreviewUrl"
class="flex-1 w-full rounded-b-2xl min-h-0"
title="Document preview"
/>
<!-- Non-PDF fallback -->
<div v-else class="flex-1 flex items-center justify-center p-8 text-center">
<div>
<FileText class="size-12 text-surface-300 dark:text-surface-600 mx-auto mb-3" />
<p class="text-sm font-medium text-surface-600 dark:text-surface-300">Preview not available for this file type</p>
<a
v-if="docPreviewDocId"
:href="`/api/documents/${docPreviewDocId}/download`"
class="mt-3 inline-flex items-center gap-1.5 text-sm text-brand-600 hover:text-brand-700 dark:text-brand-400 font-medium"
>
<Download class="size-3.5" />
Download instead
</a>
</div>
</div>
</div>
</div>
</Teleport>
</div>
</template>
Loading
Loading