Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
55 changes: 55 additions & 0 deletions apps/web/app/components/job/JobDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react'
import { Job } from '../../types/job'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

export const JobDetails = ({ job }: { job: Job }) => {
const aboutRole = job.aboutRole ?? [job.description ?? 'No description provided.']
const responsibilities = job.responsibilities ?? []
const qualifications = job.qualifications ?? []

return (
<Card className="p-2 space-y-6 rounded-2xl">
{/* About the Role */}
<CardHeader className="pb-2">
<CardTitle className="text-lg font-bold">About the Role</CardTitle>
</CardHeader>
<CardContent className="space-y-2 text-sm text-muted-foreground leading-relaxed">
{aboutRole.map((paragraph: string, idx: number) => (
<p key={idx}>{paragraph}</p>
))}
</CardContent>

{/* Key Responsibilities */}
{responsibilities.length > 0 && (
<>
<CardHeader className="pb-2 pt-0">
<CardTitle className="text-lg font-bold">Key Responsibilities</CardTitle>
</CardHeader>
<CardContent>
<ul className="list-disc list-inside space-y-2 text-sm text-muted-foreground">
{responsibilities.map((item: string, idx: number) => (
<li key={idx}>{item}</li>
))}
</ul>
</CardContent>
</>
)}

{/* Qualifications */}
{qualifications.length > 0 && (
<>
<CardHeader className="pb-2 pt-0">
<CardTitle className="text-lg font-bold">Qualifications</CardTitle>
</CardHeader>
<CardContent>
<ul className="list-disc list-inside space-y-2 text-sm text-muted-foreground">
{qualifications.map((item: string, idx: number) => (
<li key={idx}>{item}</li>
))}
</ul>
</CardContent>
</>
)}
</Card>
)
}
129 changes: 129 additions & 0 deletions apps/web/app/components/job/JobHeader.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
// components/job/JobHeader.tsx
'use client'

import React, { useState } from 'react'
import Link from 'next/link'
import { ArrowLeft, Share2, MapPin, DollarSign, Clock, Bookmark, Flag } from 'lucide-react'
import { Job } from '../../types/job'
import { Button } from '@/components/ui/button'
import { Badge } from '@/components/ui/badge'
import { apiFetch } from '@/lib/api-client'

interface JobHeaderProps {
job: Job
}

export const JobHeader = ({ job }: JobHeaderProps) => {
const [isSaved, setIsSaved] = useState(false)
const [isApplying, setIsApplying] = useState(false)

const handleSave = async () => {
try {
setIsSaved((prev) => !prev)
await apiFetch(`/api/jobs/${job.id}/save`, { method: 'POST' })
} catch (err) {
// Gracefully log error if auth session is offline/expired locally
console.warn('Could not persist save action to backend:', err)
}
}

const handleApply = async () => {
try {
setIsApplying(true)
await apiFetch(`/api/jobs/${job.id}/apply`, { method: 'POST' })
alert('Application submitted successfully!')
} catch (err) {
console.warn('Could not submit application to backend:', err)
alert('Action logged (Auth session expired or backend offline).')
} finally {
setIsApplying(false)
}
}

const handleShare = () => {
if (navigator.share) {
navigator.share({ title: job.title, url: window.location.href })
} else {
navigator.clipboard.writeText(window.location.href)
alert('Link copied to clipboard!')
}
}

return (
<div className="space-y-4 mb-6">
<div className="flex items-center justify-between">
<Link
href="/jobs"
className="inline-flex items-center text-sm font-medium text-muted-foreground hover:text-foreground transition-colors"
>
<ArrowLeft className="w-4 h-4 mr-2" />
Back to Feed
</Link>
<div className="flex items-center gap-2">
<Button
variant={isSaved ? 'secondary' : 'outline'}
size="sm"
onClick={handleSave}
className={
isSaved
? 'text-emerald-600 border-emerald-500 bg-emerald-50 hover:bg-emerald-100'
: ''
}
>
<Bookmark className={`w-4 h-4 mr-2 ${isSaved ? 'fill-emerald-600' : ''}`} />
{isSaved ? 'Saved' : 'Save'}
</Button>
<Button variant="outline" size="icon" onClick={handleShare} title="Share">
<Share2 className="w-4 h-4" />
</Button>
<Button
variant="outline"
size="icon"
onClick={() => alert('Job posting reported.')}
className="hover:text-red-600 hover:bg-red-50"
title="Report Job"
>
<Flag className="w-4 h-4" />
</Button>
</div>
</div>

<div className="bg-card text-card-foreground p-6 border rounded-2xl flex flex-col md:flex-row md:items-center justify-between gap-6 shadow-sm">
<div className="space-y-2">
<Badge
variant="secondary"
className="bg-emerald-100 text-emerald-800 hover:bg-emerald-100 font-semibold"
>
{job.category}
</Badge>
<h1 className="text-2xl font-bold tracking-tight">{job.title}</h1>

<div className="flex flex-wrap items-center gap-4 text-sm text-muted-foreground pt-1">
<span className="flex items-center gap-1">
<MapPin className="w-4 h-4" />
{job.location}
</span>
<span className="flex items-center gap-1">
<DollarSign className="w-4 h-4" />
{job.salaryRange}
</span>
<span className="flex items-center gap-1">
<Clock className="w-4 h-4" />
{job.type}
</span>
</div>
</div>

<div className="sticky bottom-4 md:static z-10 bg-background md:bg-transparent p-3 md:p-0 border md:border-none rounded-xl shadow-lg md:shadow-none">
<Button
onClick={handleApply}
disabled={isApplying}
className="w-full md:w-auto px-6 py-3 bg-emerald-600 hover:bg-emerald-700 text-white font-semibold rounded-xl"
>
{isApplying ? 'Applying...' : 'Apply Now'}
</Button>
</div>
</div>
</div>
)
}
57 changes: 57 additions & 0 deletions apps/web/app/components/job/JobSidebar.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react'
import { Building2, Users, Calendar, Code2 } from 'lucide-react'
import { Job } from '../../types/job'
import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'

export const JobSidebar = ({ job }: { job: Job }) => {
return (
<Card className="rounded-2xl shadow-sm">
<CardHeader className="pb-3 border-b border-border">
<CardTitle className="text-lg font-bold">Company Overview</CardTitle>
</CardHeader>

<CardContent className="pt-4 space-y-4 text-sm">
<div className="flex items-center gap-3">
<Building2 className="w-5 h-5 text-muted-foreground" />
<div>
<p className="text-xs text-muted-foreground">Industry</p>
<p className="font-medium">{job.companyOverview?.industry ?? 'Technology'}</p>
</div>
</div>

<div className="flex items-center gap-3">
<Users className="w-5 h-5 text-muted-foreground" />
<div>
<p className="text-xs text-muted-foreground">Company Size</p>
<p className="font-medium">{job.companyOverview?.size ?? '50-200 employees'}</p>
</div>
</div>

<div className="flex items-center gap-3">
<Calendar className="w-5 h-5 text-muted-foreground" />
<div>
<p className="text-xs text-muted-foreground">Founded</p>
<p className="font-medium">{job.companyOverview?.founded ?? '2020'}</p>
</div>
</div>

<div className="flex items-start gap-3">
<Code2 className="w-5 h-5 text-muted-foreground mt-0.5" />
<div>
<p className="text-xs text-muted-foreground">Tech Stack</p>
<div className="flex flex-wrap gap-1 mt-1.5">
{(job.companyOverview?.techStack ?? ['TypeScript', 'Next.js', 'Hono']).map(
(tech: string) => (
<Badge key={tech} variant="secondary" className="text-xs font-normal">
{tech}
</Badge>
),
)}
</div>
</div>
</div>
</CardContent>
</Card>
)
}
130 changes: 130 additions & 0 deletions apps/web/app/components/layout/AppLayout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
'use client'

import React from 'react'
import Link from 'next/link'
import {
Briefcase,
LayoutGrid,
FileText,
User,
HelpCircle,
CheckCircle2,
ArrowRight,
} from 'lucide-react'

interface AppLayoutProps {
children: React.ReactNode
}

export const AppLayout = ({ children }: AppLayoutProps) => {
return (
<div className="min-h-screen bg-[#f8fafc] text-slate-800 flex flex-col font-sans">
{/* Top Header */}
<header className="h-16 bg-white border-b border-slate-200 px-6 flex items-center justify-between sticky top-0 z-50">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-emerald-500 flex items-center justify-center text-white shadow-sm">
<Briefcase size={18} />
</div>
<span className="font-bold text-lg text-slate-900 tracking-tight">CareerLink</span>
</div>

<button className="flex items-center gap-1.5 text-xs text-slate-500 hover:text-slate-800 transition-colors border border-slate-200 rounded-md px-2.5 py-1.5 shadow-sm">
<HelpCircle size={14} />
<span>Need Help?</span>
</button>
</header>

{/* Main Layout Body */}
<div className="flex flex-1">
{/* Left Sidebar */}
<aside className="w-64 bg-white border-r border-slate-200 p-4 flex flex-col justify-between hidden md:flex">
<div className="space-y-6">
<div>
<p className="text-[11px] font-semibold text-slate-400 uppercase tracking-wider px-3 mb-2">
Student Portal
</p>
<nav className="space-y-1">
<Link
href="#"
className="flex items-center gap-3 px-3 py-2 text-sm font-medium text-emerald-700 bg-emerald-50/70 rounded-lg border border-emerald-100"
>
<LayoutGrid size={18} className="text-emerald-600" />
<span>Job Feed</span>
</Link>

<Link
href="#"
className="flex items-center gap-3 px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 hover:bg-slate-50 rounded-lg transition-colors"
>
<FileText size={18} className="text-slate-400" />
<span>My Applications</span>
</Link>

<Link
href="#"
className="flex items-center gap-3 px-3 py-2 text-sm font-medium text-slate-600 hover:text-slate-900 hover:bg-slate-50 rounded-lg transition-colors"
>
<User size={18} className="text-slate-400" />
<span>Profile</span>
</Link>
</nav>
</div>

{/* Verification Card */}
<div className="p-3.5 bg-emerald-50/50 border border-emerald-100 rounded-xl space-y-2">
<div className="flex items-center gap-1.5 text-emerald-700 text-xs font-semibold">
<CheckCircle2 size={14} className="text-emerald-500" />
<span>VERIFICATION</span>
</div>
<p className="text-xs text-slate-600 leading-relaxed">
Complete your profile to unlock &ldquo;Top Match&rdquo; status for roles.
</p>
<Link
href="#"
className="inline-flex items-center gap-1 text-xs font-semibold text-emerald-600 hover:text-emerald-700 pt-1"
>
<span>Finish Setup</span>
<ArrowRight size={12} />
</Link>
</div>
</div>

{/* User Profile Badge */}
<div className="pt-4 border-t border-slate-100 flex items-center gap-3">
<div className="w-8 h-8 rounded-full bg-emerald-100 text-emerald-700 flex items-center justify-center font-bold text-xs">
AM
</div>
<div className="text-xs">
<p className="font-semibold text-slate-800">Alex Mercer</p>
<p className="text-slate-400">Stanford &apos;25</p>
</div>
</div>
</aside>

{/* Content Viewport */}
<main className="flex-1 flex flex-col justify-between">
<div className="p-6 md:p-8 max-w-6xl w-full mx-auto">{children}</div>

{/* Footer */}
<footer className="border-t border-slate-200 bg-white py-4 px-8 flex flex-col md:flex-row items-center justify-between text-[11px] text-slate-400 gap-2">
<div className="flex items-center gap-1">
<Briefcase size={12} />
<span>&copy; 2026 CAREERLINK PLATFORM. ALL RIGHTS RESERVED.</span>
</div>
<div className="flex items-center gap-6 font-medium">
<Link href="#" className="hover:text-slate-600">
HELP CENTER
</Link>
<Link href="#" className="hover:text-slate-600">
PRIVACY POLICY
</Link>
<Link href="#" className="hover:text-slate-600">
TERMS OF SERVICE
</Link>
</div>
</footer>
</main>
</div>
</div>
)
}
Loading