gohorsejobs/frontend/src/components/job-card.tsx
2025-12-22 15:30:06 -03:00

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>
);
}