212 lines
6.9 KiB
TypeScript
212 lines
6.9 KiB
TypeScript
"use client"
|
|
|
|
import type { Job } from "@/lib/types";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardFooter,
|
|
CardHeader,
|
|
} from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import {
|
|
MapPin,
|
|
Briefcase,
|
|
DollarSign,
|
|
Clock,
|
|
Building2,
|
|
Heart,
|
|
} from "lucide-react";
|
|
import Link from "next/link";
|
|
import { motion } from "framer-motion";
|
|
import { useState } from "react";
|
|
import { useNotify } from "@/contexts/notification-context";
|
|
import { useTranslation } from "@/lib/i18n";
|
|
|
|
interface JobCardProps {
|
|
job: Job;
|
|
}
|
|
|
|
export function JobCard({ job }: JobCardProps) {
|
|
const { t } = useTranslation();
|
|
const [isFavorited, setIsFavorited] = useState(false);
|
|
const notify = useNotify();
|
|
|
|
const formatTimeAgo = (dateString: string) => {
|
|
const date = new Date(dateString);
|
|
const now = new Date();
|
|
const diffInMs = now.getTime() - date.getTime();
|
|
const diffInDays = Math.floor(diffInMs / (1000 * 60 * 60 * 24));
|
|
|
|
if (diffInDays === 0) return t('jobs.posted.today');
|
|
if (diffInDays === 1) return t('jobs.posted.yesterday');
|
|
if (diffInDays < 7) return t('jobs.posted.daysAgo', { count: diffInDays });
|
|
if (diffInDays < 30) return t('jobs.posted.weeksAgo', { count: Math.floor(diffInDays / 7) });
|
|
return t('jobs.posted.monthsAgo', { count: Math.floor(diffInDays / 30) });
|
|
};
|
|
|
|
const getTypeLabel = (type: string) => {
|
|
return t(`jobs.types.${type}`) !== `jobs.types.${type}` ? t(`jobs.types.${type}`) : type;
|
|
};
|
|
|
|
const getTypeBadgeVariant = (type: string) => {
|
|
switch (type) {
|
|
case "full-time":
|
|
return "default";
|
|
case "part-time":
|
|
return "secondary";
|
|
case "contract":
|
|
return "outline";
|
|
case "remote":
|
|
return "default";
|
|
default:
|
|
return "outline";
|
|
}
|
|
};
|
|
|
|
const getCompanyInitials = (company: string) => {
|
|
return company
|
|
.split(" ")
|
|
.map((word) => word[0])
|
|
.join("")
|
|
.toUpperCase()
|
|
.slice(0, 2);
|
|
};
|
|
|
|
const handleFavorite = () => {
|
|
setIsFavorited(!isFavorited);
|
|
if (!isFavorited) {
|
|
notify.info(
|
|
t('jobs.favorites.added.title'),
|
|
t('jobs.favorites.added.desc', { title: job.title }),
|
|
{
|
|
actionUrl: "/dashboard/favorites",
|
|
actionLabel: t('jobs.favorites.action'),
|
|
}
|
|
);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<motion.div
|
|
whileHover={{ y: -2 }}
|
|
transition={{ type: "spring", stiffness: 300, damping: 20 }}
|
|
>
|
|
<Card className="relative hover:shadow-lg transition-all duration-300 border-l-4 border-l-primary/20 hover:border-l-primary h-full flex flex-col">
|
|
<CardHeader className="pb-4">
|
|
<div className="flex items-start justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<Avatar className="h-12 w-12">
|
|
<AvatarImage
|
|
src={`https://avatar.vercel.sh/${job.company}`}
|
|
alt={job.company}
|
|
/>
|
|
<AvatarFallback className="bg-primary/10 text-primary font-semibold">
|
|
{getCompanyInitials(job.company)}
|
|
</AvatarFallback>
|
|
</Avatar>
|
|
<div>
|
|
<h3 className="font-semibold text-lg text-balance leading-tight hover:text-primary transition-colors">
|
|
{job.title}
|
|
</h3>
|
|
<div className="flex items-center gap-2 mt-1">
|
|
<Building2 className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-muted-foreground font-medium">
|
|
{job.company}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={handleFavorite}
|
|
className="shrink-0"
|
|
>
|
|
<Heart
|
|
className={`h-4 w-4 transition-colors ${isFavorited
|
|
? "fill-red-500 text-red-500"
|
|
: "text-muted-foreground hover:text-red-500"
|
|
}`}
|
|
/>
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
|
|
<CardContent className="space-y-4 flex-1">
|
|
{/* Job Meta Information */}
|
|
{/* Job Meta Information */}
|
|
<div className="flex flex-col gap-2 text-sm">
|
|
<div className="flex items-center gap-2 text-muted-foreground">
|
|
<MapPin className="h-4 w-4 shrink-0" />
|
|
<span className="truncate">{job.location}</span>
|
|
</div>
|
|
|
|
<div className="flex items-center justify-between w-full">
|
|
<div className="flex items-center gap-2">
|
|
<Briefcase className="h-4 w-4 shrink-0 text-muted-foreground" />
|
|
<Badge
|
|
variant={getTypeBadgeVariant(job.type)}
|
|
className="text-xs"
|
|
>
|
|
{getTypeLabel(job.type)}
|
|
</Badge>
|
|
</div>
|
|
|
|
{job.salary && (
|
|
<span className="font-medium text-foreground">
|
|
{job.salary}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Job Description Preview */}
|
|
<div className="text-sm text-muted-foreground">
|
|
<p className="line-clamp-2">{job.description}</p>
|
|
</div>
|
|
|
|
{/* Skills/Requirements Preview */}
|
|
{job.requirements && job.requirements.length > 0 && (
|
|
<div className="flex flex-wrap gap-2">
|
|
{job.requirements.slice(0, 3).map((requirement, index) => (
|
|
<Badge key={index} variant="outline" className="text-xs">
|
|
{requirement}
|
|
</Badge>
|
|
))}
|
|
{job.requirements.length > 3 && (
|
|
<Badge
|
|
variant="outline"
|
|
className="text-xs text-muted-foreground"
|
|
>
|
|
{t('jobs.requirements.more', { count: job.requirements.length - 3 })}
|
|
</Badge>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Time Posted */}
|
|
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
|
<Clock className="h-3 w-3" />
|
|
<span>{formatTimeAgo(job.postedAt)}</span>
|
|
</div>
|
|
</CardContent>
|
|
|
|
<CardFooter className="pt-4 border-t">
|
|
<div className="flex gap-2 w-full">
|
|
<Link href={`/jobs/${job.id}`} className="flex-1">
|
|
<Button variant="outline" className="w-full cursor-pointer">
|
|
{t('jobs.card.viewDetails')}
|
|
</Button>
|
|
</Link>
|
|
<Link href={`/jobs/${job.id}/apply`} className="flex-1">
|
|
<Button className="w-full cursor-pointer">{t('jobs.card.apply')}</Button>
|
|
</Link>
|
|
</div>
|
|
</CardFooter>
|
|
</Card>
|
|
</motion.div>
|
|
);
|
|
}
|