fix: correçao de conflitos
This commit is contained in:
parent
c97aeacf3b
commit
c5ec964f78
8 changed files with 183 additions and 474 deletions
|
|
@ -143,11 +143,11 @@ export default function ProfilePage() {
|
|||
|
||||
try {
|
||||
toast({ title: "Enviando foto..." });
|
||||
// 1. Get presigned URL
|
||||
const { uploadUrl, publicUrl } = await storageApi.getUploadUrl(file.name, file.type);
|
||||
// 2. Upload
|
||||
await storageApi.uploadFile(uploadUrl, file);
|
||||
// 3. Update state
|
||||
toast({ title: "Enviando foto..." });
|
||||
// 1. Upload via Proxy (avoids CORS)
|
||||
const { publicUrl } = await storageApi.uploadFile(file, "avatars");
|
||||
|
||||
// 2. Update state
|
||||
setProfilePic(publicUrl);
|
||||
toast({ title: "Foto enviada!", description: "Não esqueça de salvar o perfil." });
|
||||
} catch (err) {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,6 @@
|
|||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { format } from "date-fns";
|
||||
<<<<<<< HEAD
|
||||
import { ptBR } from "date-fns/locale";
|
||||
import {
|
||||
Building2,
|
||||
|
|
@ -11,7 +10,12 @@ import {
|
|||
Search,
|
||||
ExternalLink,
|
||||
Loader2,
|
||||
AlertCircle
|
||||
AlertCircle,
|
||||
Calendar,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
Clock,
|
||||
FileText
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -27,15 +31,30 @@ import { Input } from "@/components/ui/input";
|
|||
import { Badge } from "@/components/ui/badge";
|
||||
import { Navbar } from "@/components/navbar";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { applicationsApi, ApiApplication } from "@/lib/api";
|
||||
import { applicationsApi } from "@/lib/api";
|
||||
|
||||
type ApplicationWithJob = ApiApplication & {
|
||||
type Application = {
|
||||
id: string;
|
||||
jobId: string;
|
||||
jobTitle: string;
|
||||
companyName: string;
|
||||
companyId: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
resumeUrl?: string;
|
||||
message?: string;
|
||||
};
|
||||
|
||||
const statusConfig: Record<string, { label: string; color: string; icon: any }> = {
|
||||
pending: { label: "Em Análise", color: "bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80", icon: Clock },
|
||||
reviewed: { label: "Visualizado", color: "bg-blue-100 text-blue-800 hover:bg-blue-100/80", icon: CheckCircle2 },
|
||||
shortlisted: { label: "Selecionado", color: "bg-purple-100 text-purple-800 hover:bg-purple-100/80", icon: CheckCircle2 },
|
||||
hired: { label: "Contratado", color: "bg-green-100 text-green-800 hover:bg-green-100/80", icon: CheckCircle2 },
|
||||
rejected: { label: "Não Selecionado", color: "bg-red-100 text-red-800 hover:bg-red-100/80", icon: XCircle },
|
||||
};
|
||||
|
||||
export default function MyApplicationsPage() {
|
||||
const [applications, setApplications] = useState<ApplicationWithJob[]>([]);
|
||||
const [applications, setApplications] = useState<Application[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState("");
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
|
|
@ -48,9 +67,9 @@ export default function MyApplicationsPage() {
|
|||
try {
|
||||
setLoading(true);
|
||||
const data = await applicationsApi.listMyApplications();
|
||||
// The backend now returns ApplicationWithDetails which has jobTitle and companyName
|
||||
// We cast it to our extended type
|
||||
setApplications(data as unknown as ApplicationWithJob[]);
|
||||
// Backend endpoint consistency check: listMyApplications usually returns ApplicationWithDetails
|
||||
// Casting to ensure type safety if generic
|
||||
setApplications(data as unknown as Application[]);
|
||||
} catch (err) {
|
||||
console.error("Failed to fetch applications", err);
|
||||
setError("Não foi possível carregar suas candidaturas. Tente novamente.");
|
||||
|
|
@ -59,27 +78,6 @@ export default function MyApplicationsPage() {
|
|||
}
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
switch (status) {
|
||||
case "pending": return "bg-yellow-100 text-yellow-800 hover:bg-yellow-100/80";
|
||||
case "reviewed": return "bg-blue-100 text-blue-800 hover:bg-blue-100/80";
|
||||
case "hired": return "bg-green-100 text-green-800 hover:bg-green-100/80";
|
||||
case "rejected": return "bg-red-100 text-red-800 hover:bg-red-100/80";
|
||||
default: return "bg-gray-100 text-gray-800 hover:bg-gray-100/80";
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusLabel = (status: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
pending: "Em Análise",
|
||||
reviewed: "Visualizado",
|
||||
shortlisted: "Selecionado",
|
||||
hired: "Contratado",
|
||||
rejected: "Não Selecionado"
|
||||
};
|
||||
return labels[status] || status;
|
||||
};
|
||||
|
||||
const filteredApplications = applications.filter(app =>
|
||||
app.jobTitle.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||
app.companyName.toLowerCase().includes(searchTerm.toLowerCase())
|
||||
|
|
@ -137,226 +135,71 @@ export default function MyApplicationsPage() {
|
|||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2">
|
||||
{filteredApplications.map((app) => (
|
||||
<Card key={app.id} className="overflow-hidden hover:shadow-md transition-shadow">
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-2">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-xl">
|
||||
<Link href={`/vagas/${app.jobId}`} className="hover:text-primary transition-colors">
|
||||
{app.jobTitle}
|
||||
</Link>
|
||||
</CardTitle>
|
||||
<CardDescription className="flex items-center gap-2">
|
||||
<Building2 className="h-3.5 w-3.5" />
|
||||
{app.companyName}
|
||||
</CardDescription>
|
||||
</div>
|
||||
<Badge className={getStatusColor(app.status)} variant="secondary">
|
||||
{getStatusLabel(app.status)}
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm mt-4">
|
||||
<div className="flex items-center text-muted-foreground">
|
||||
<CalendarDays className="mr-2 h-4 w-4" />
|
||||
Aplicado em {format(new Date(app.createdAt), "dd 'de' MMMM, yyyy", { locale: ptBR })}
|
||||
{filteredApplications.map((app) => {
|
||||
const status = statusConfig[app.status] || {
|
||||
label: app.status,
|
||||
color: "bg-gray-100 text-gray-800 hover:bg-gray-100/80",
|
||||
icon: AlertCircle
|
||||
};
|
||||
const StatusIcon = status.icon;
|
||||
|
||||
return (
|
||||
<Card key={app.id} className="overflow-hidden hover:shadow-md transition-shadow">
|
||||
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-2">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-xl">
|
||||
<Link href={`/vagas/${app.jobId}`} className="hover:text-primary transition-colors">
|
||||
{app.jobTitle}
|
||||
</Link>
|
||||
</CardTitle>
|
||||
<CardDescription className="flex items-center gap-2">
|
||||
<Building2 className="h-3.5 w-3.5" />
|
||||
{app.companyName}
|
||||
</CardDescription>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="bg-muted/50 p-4 flex justify-between items-center">
|
||||
<Link href={`/vagas/${app.jobId}`} className="text-sm font-medium text-primary hover:underline flex items-center">
|
||||
Ver Vaga <ExternalLink className="ml-1 h-3 w-3" />
|
||||
</Link>
|
||||
{app.resumeUrl && (
|
||||
<Link
|
||||
href={app.resumeUrl}
|
||||
target="_blank"
|
||||
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4"
|
||||
>
|
||||
Ver Currículo Enviado
|
||||
<Badge className={status.color} variant="secondary">
|
||||
<div className="flex items-center gap-1">
|
||||
<StatusIcon className="h-3 w-3" />
|
||||
{status.label}
|
||||
</div>
|
||||
</Badge>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="grid grid-cols-2 gap-4 text-sm mt-4">
|
||||
<div className="flex items-center text-muted-foreground">
|
||||
<CalendarDays className="mr-2 h-4 w-4" />
|
||||
Aplicado em {format(new Date(app.createdAt), "dd 'de' MMMM, yyyy", { locale: ptBR })}
|
||||
</div>
|
||||
</div>
|
||||
{app.message && (
|
||||
<div className="mt-4 bg-muted/50 p-3 rounded-md text-sm italic">
|
||||
"{app.message.length > 100 ? app.message.substring(0, 100) + "..." : app.message}"
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
<CardFooter className="bg-muted/50 p-4 flex justify-between items-center">
|
||||
<Link href={`/vagas/${app.jobId}`} className="text-sm font-medium text-primary hover:underline flex items-center">
|
||||
Ver Vaga <ExternalLink className="ml-1 h-3 w-3" />
|
||||
</Link>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
))}
|
||||
{app.resumeUrl && (
|
||||
<Link
|
||||
href={app.resumeUrl}
|
||||
target="_blank"
|
||||
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 flex items-center"
|
||||
>
|
||||
<FileText className="h-3 w-3 mr-1" />
|
||||
Ver Currículo
|
||||
</Link>
|
||||
)}
|
||||
</CardFooter>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
|
||||
<Footer />
|
||||
=======
|
||||
import {
|
||||
Building2,
|
||||
MapPin,
|
||||
Calendar,
|
||||
Clock,
|
||||
CheckCircle2,
|
||||
XCircle,
|
||||
AlertCircle,
|
||||
FileText,
|
||||
ExternalLink
|
||||
} from "lucide-react";
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { applicationsApi } from "@/lib/api";
|
||||
import { useNotify } from "@/contexts/notification-context";
|
||||
|
||||
interface Application {
|
||||
id: string;
|
||||
jobId: string;
|
||||
jobTitle: string;
|
||||
companyName: string;
|
||||
companyId: string;
|
||||
status: string;
|
||||
createdAt: string;
|
||||
resumeUrl?: string;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
const statusConfig: Record<string, { label: string; color: string; icon: any }> = {
|
||||
pending: { label: "Pending", color: "bg-yellow-100 text-yellow-800 border-yellow-200", icon: Clock },
|
||||
reviewed: { label: "Viewed", color: "bg-blue-100 text-blue-800 border-blue-200", icon: CheckCircle2 },
|
||||
shortlisted: { label: "Shortlisted", color: "bg-purple-100 text-purple-800 border-purple-200", icon: CheckCircle2 },
|
||||
hired: { label: "Hired", color: "bg-green-100 text-green-800 border-green-200", icon: CheckCircle2 },
|
||||
rejected: { label: "Rejected", color: "bg-red-100 text-red-800 border-red-200", icon: XCircle },
|
||||
};
|
||||
|
||||
export default function MyApplicationsPage() {
|
||||
const [applications, setApplications] = useState<Application[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const notify = useNotify();
|
||||
|
||||
useEffect(() => {
|
||||
async function fetchApplications() {
|
||||
try {
|
||||
const data = await applicationsApi.listMine();
|
||||
setApplications(data || []);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch applications:", error);
|
||||
notify.error("Error", "Failed to load your applications.");
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
fetchApplications();
|
||||
}, [notify]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<h1 className="text-3xl font-bold">My Applications</h1>
|
||||
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
|
||||
{[1, 2, 3].map((i) => (
|
||||
<Card key={i}>
|
||||
<CardHeader>
|
||||
<Skeleton className="h-6 w-3/4" />
|
||||
<Skeleton className="h-4 w-1/2" />
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Skeleton className="h-20 w-full" />
|
||||
</CardContent>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">My Applications</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
Track the status of your job applications.
|
||||
</p>
|
||||
</div>
|
||||
<Button asChild>
|
||||
<Link href="/jobs">Find more jobs</Link>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{applications.length === 0 ? (
|
||||
<Card className="border-dashed">
|
||||
<CardContent className="flex flex-col items-center justify-center py-12 text-center text-muted-foreground">
|
||||
<h3 className="text-lg font-semibold mb-2">No applications yet</h3>
|
||||
<p className="mb-6">You haven't applied to any jobs yet.</p>
|
||||
<Button asChild variant="secondary">
|
||||
<Link href="/jobs">Browse Jobs</Link>
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
) : (
|
||||
<div className="grid gap-4 md:grid-cols-1 lg:grid-cols-2">
|
||||
{applications.map((app) => {
|
||||
const status = statusConfig[app.status] || {
|
||||
label: app.status,
|
||||
color: "bg-gray-100 text-gray-800 border-gray-200",
|
||||
icon: AlertCircle
|
||||
};
|
||||
const StatusIcon = status.icon;
|
||||
|
||||
return (
|
||||
<Card key={app.id} className="hover:border-primary/50 transition-colors">
|
||||
<CardHeader>
|
||||
<div className="flex justify-between items-start gap-4">
|
||||
<div className="space-y-1">
|
||||
<CardTitle className="text-xl">
|
||||
<Link href={`/jobs/${app.jobId}`} className="hover:underline hover:text-primary transition-colors">
|
||||
{app.jobTitle}
|
||||
</Link>
|
||||
</CardTitle>
|
||||
<div className="flex items-center text-muted-foreground text-sm gap-2">
|
||||
<Building2 className="h-4 w-4" />
|
||||
<span>{app.companyName}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant="outline" className={`${status.color} flex items-center gap-1 shrink-0`}>
|
||||
<StatusIcon className="h-3 w-3" />
|
||||
{status.label}
|
||||
</Badge>
|
||||
</div>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex flex-wrap gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Applied on {format(new Date(app.createdAt), "MMM d, yyyy")}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{app.message && (
|
||||
<div className="bg-muted/50 p-3 rounded-md text-sm italic">
|
||||
"{app.message.length > 100 ? app.message.substring(0, 100) + "..." : app.message}"
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 pt-2">
|
||||
{app.resumeUrl && (
|
||||
<Button variant="outline" size="sm" asChild>
|
||||
<a href={app.resumeUrl} target="_blank" rel="noopener noreferrer">
|
||||
<FileText className="h-4 w-4 mr-2" />
|
||||
View Resume
|
||||
</a>
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="ghost" size="sm" asChild className="ml-auto">
|
||||
<Link href={`/jobs/${app.jobId}`}>
|
||||
View Job <ExternalLink className="h-3 w-3 ml-1" />
|
||||
</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
>>>>>>> dev
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,11 +4,8 @@ import { useEffect, useState, useCallback } from "react"
|
|||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
>>>>>>> dev
|
||||
} from "@/components/ui/card"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
|
|
|||
|
|
@ -8,11 +8,11 @@ import { Mail, ArrowLeft, Loader2, CheckCircle } from "lucide-react";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
|
||||
import { Alert, AlertDescription } from "@/components/ui/alert";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Navbar } from "@/components/navbar";
|
||||
import { Footer } from "@/components/footer";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { ArrowLeft, Mail } from "lucide-react";
|
||||
|
||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8521";
|
||||
|
||||
|
|
@ -53,30 +53,6 @@ export default function ForgotPasswordPage() {
|
|||
};
|
||||
|
||||
return (
|
||||
<<<<<<< HEAD
|
||||
<div className="min-h-screen flex flex-col bg-gradient-to-b from-muted/30 to-background">
|
||||
<Navbar />
|
||||
<div className="flex-1 flex items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md">
|
||||
<CardHeader className="text-center">
|
||||
<CardTitle className="text-2xl">{t("auth.forgot.title")}</CardTitle>
|
||||
<CardDescription>
|
||||
{t("auth.forgot.subtitle")}
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{submitted ? (
|
||||
<div className="flex flex-col items-center gap-4 py-6">
|
||||
<CheckCircle className="w-16 h-16 text-green-500" />
|
||||
<p className="text-center text-muted-foreground">
|
||||
{t("auth.forgot.success")}
|
||||
</p>
|
||||
<Link href="/login">
|
||||
<Button variant="outline">
|
||||
<ArrowLeft className="w-4 h-4 mr-2" /> {t("auth.forgot.backLogin")}
|
||||
</Button>
|
||||
</Link>
|
||||
=======
|
||||
<div className="min-h-screen flex items-center justify-center bg-background px-4 sm:px-6 py-8 sm:py-12">
|
||||
<div className="w-full max-w-sm sm:max-w-md space-y-4 sm:space-y-6">
|
||||
{/* Back to Login - Mobile friendly top placement */}
|
||||
|
|
@ -108,80 +84,50 @@ export default function ForgotPasswordPage() {
|
|||
</Alert>
|
||||
)}
|
||||
|
||||
<form onSubmit={handleSubmit} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email" className="text-sm sm:text-base">
|
||||
{t("auth.forgot.fields.email")}
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={email}
|
||||
onChange={(event) => setEmail(event.target.value)}
|
||||
placeholder={t("auth.forgot.fields.emailPlaceholder")}
|
||||
required
|
||||
className="h-10 sm:h-11"
|
||||
/>
|
||||
>>>>>>> dev
|
||||
</div>
|
||||
) : (
|
||||
{!submitted && (
|
||||
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="email">{t("auth.forgot.fields.email")}</Label>
|
||||
<div className="relative">
|
||||
<Mail className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder={t("auth.forgot.fields.emailPlaceholder")}
|
||||
className="pl-10"
|
||||
{...register("email", { required: t("validations.required") || "Email is required" })}
|
||||
/>
|
||||
</div>
|
||||
<Label htmlFor="email" className="text-sm sm:text-base">
|
||||
{t("auth.forgot.fields.email")}
|
||||
</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
placeholder={t("auth.forgot.fields.emailPlaceholder")}
|
||||
className="h-10 sm:h-11"
|
||||
{...register("email", { required: t("validations.required") || "Email is required" })}
|
||||
/>
|
||||
{errors.email && <p className="text-sm text-destructive">{errors.email.message}</p>}
|
||||
</div>
|
||||
|
||||
<<<<<<< HEAD
|
||||
{error && <p className="text-sm text-destructive text-center">{error}</p>}
|
||||
|
||||
<Button type="submit" className="w-full" disabled={loading}>
|
||||
<Button type="submit" className="w-full h-10 sm:h-11" disabled={loading}>
|
||||
{loading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
|
||||
{t("auth.forgot.submit")}
|
||||
</Button>
|
||||
</form>
|
||||
)}
|
||||
=======
|
||||
<Button type="submit" className="w-full h-10 sm:h-11">
|
||||
{t("auth.forgot.submit")}
|
||||
</Button>
|
||||
</form>
|
||||
>>>>>>> dev
|
||||
</CardContent>
|
||||
{!submitted && (
|
||||
<CardFooter className="justify-center">
|
||||
<Link href="/login" className="text-sm text-muted-foreground hover:underline">
|
||||
<ArrowLeft className="w-3 h-3 inline mr-1" /> {t("auth.forgot.backLogin")}
|
||||
</Link>
|
||||
{/* Back to Login - Desktop */}
|
||||
<div className="hidden sm:block text-center">
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t("auth.forgot.backLogin")}
|
||||
</Link>
|
||||
</div>
|
||||
</CardFooter>
|
||||
)}
|
||||
</Card>
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
{/* Back to Login - Desktop */}
|
||||
<div className="hidden sm:block text-center">
|
||||
<Link
|
||||
href="/login"
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors inline-flex items-center gap-2"
|
||||
>
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t("auth.forgot.backLogin")}
|
||||
</Link>
|
||||
</div>
|
||||
>>>>>>> dev
|
||||
</div>
|
||||
<Footer />
|
||||
</div>
|
||||
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import {
|
|||
MessageSquare,
|
||||
Save,
|
||||
ArrowLeft,
|
||||
Loader2,
|
||||
} from "lucide-react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
|
@ -42,16 +43,11 @@ import { Navbar } from "@/components/navbar";
|
|||
import { Footer } from "@/components/footer";
|
||||
import { useNotify } from "@/contexts/notification-context";
|
||||
import { jobsApi, applicationsApi, storageApi, type ApiJob } from "@/lib/api";
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
import { formatPhone } from "@/lib/utils";
|
||||
import { useTranslation } from "@/lib/i18n";
|
||||
import { getCurrentUser } from "@/lib/auth";
|
||||
|
||||
|
||||
>>>>>>> dev
|
||||
|
||||
|
||||
|
||||
export const runtime = 'edge';
|
||||
|
||||
|
|
@ -234,27 +230,9 @@ export default function JobApplicationPage({
|
|||
const handleSubmit = async () => {
|
||||
setIsSubmitting(true);
|
||||
try {
|
||||
let resumeUrl = "";
|
||||
|
||||
// 1. Upload Curriculo if present
|
||||
if (formData.resume) {
|
||||
try {
|
||||
// Check if storageApi is available (it was in HEAD but maybe not hml type definitions?)
|
||||
// Assuming it exists based on HEAD content.
|
||||
if (storageApi && storageApi.getUploadUrl) {
|
||||
const { uploadUrl, publicUrl } = await storageApi.getUploadUrl(
|
||||
formData.resume.name,
|
||||
formData.resume.type
|
||||
);
|
||||
await storageApi.uploadFile(uploadUrl, formData.resume);
|
||||
resumeUrl = publicUrl;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error("Upload error:", err);
|
||||
notify.error("Upload failed", "Could not upload resume, proceeding without it.");
|
||||
// Proceed or return? proceed for now but warn.
|
||||
}
|
||||
}
|
||||
// 1. Resume is already uploaded via handleResumeUpload, so we use formData.resumeUrl
|
||||
const resumeUrl = formData.resumeUrl;
|
||||
// Note: If you want to enforce upload here, you'd need the File object, but we uploaded it earlier.
|
||||
|
||||
await applicationsApi.create({
|
||||
jobId: Number(id), // ID might need number conversion depending on API
|
||||
|
|
@ -262,7 +240,6 @@ export default function JobApplicationPage({
|
|||
email: formData.email,
|
||||
phone: formData.phone,
|
||||
linkedin: formData.linkedin,
|
||||
<<<<<<< HEAD
|
||||
coverLetter: formData.coverLetter || formData.whyUs, // Fallback
|
||||
resumeUrl: resumeUrl,
|
||||
portfolioUrl: formData.portfolioUrl,
|
||||
|
|
@ -270,18 +247,6 @@ export default function JobApplicationPage({
|
|||
hasExperience: formData.hasExperience,
|
||||
whyUs: formData.whyUs,
|
||||
availability: formData.availability,
|
||||
=======
|
||||
resumeUrl: formData.resumeUrl,
|
||||
coverLetter: formData.coverLetter || undefined,
|
||||
portfolioUrl: formData.portfolioUrl || undefined,
|
||||
message: formData.whyUs, // Mapping Why Us to Message/Notes
|
||||
documents: {}, // TODO: Extra docs
|
||||
// salaryExpectation: formData.salaryExpectation, // These fields might need to go into Notes or structured JSON if backend doesn't support them specifically?
|
||||
// hasExperience: formData.hasExperience,
|
||||
// Backend seems to map "documents" as JSONMap. We can put extra info there?
|
||||
// Or put in "message" concatenated.
|
||||
// Let's assume the backend 'message' field is good for "whyUs"
|
||||
>>>>>>> dev
|
||||
});
|
||||
|
||||
notify.success(
|
||||
|
|
@ -311,7 +276,21 @@ export default function JobApplicationPage({
|
|||
|
||||
const progress = (currentStep / steps.length) * 100;
|
||||
|
||||
if (!job && !loading) return null; // Or some error state
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<Loader2 className="h-8 w-8 animate-spin" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!job) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<p>Job not found</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (isSubmitted) {
|
||||
const user = getCurrentUser();
|
||||
|
|
@ -404,11 +383,7 @@ export default function JobApplicationPage({
|
|||
<div className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||
<div>
|
||||
<h1 className="text-2xl md:text-3xl font-bold text-foreground">
|
||||
<<<<<<< HEAD
|
||||
Application: {job?.title}
|
||||
=======
|
||||
{t("application.title", { jobTitle: job.title })}
|
||||
>>>>>>> dev
|
||||
</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
{job?.companyName || 'Company'} • {job?.location || 'Remote'}
|
||||
|
|
@ -604,34 +579,12 @@ export default function JobApplicationPage({
|
|||
disabled={isUploading}
|
||||
/>
|
||||
<div className="flex flex-col items-center gap-2">
|
||||
<<<<<<< HEAD
|
||||
{/* TODO: Implement real file input handler */}
|
||||
<div className="relative">
|
||||
<div className="p-3 bg-primary/10 rounded-full text-primary">
|
||||
<Upload className="h-6 w-6" />
|
||||
</div>
|
||||
<Input
|
||||
type="file"
|
||||
className="absolute inset-0 opacity-0 cursor-pointer w-full h-full"
|
||||
accept=".pdf,.doc,.docx"
|
||||
onChange={(e) => {
|
||||
if (e.target.files && e.target.files[0]) {
|
||||
handleInputChange("resume", e.target.files[0])
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{formData.resume ? formData.resume.name : "Click to upload or drag the file here"}
|
||||
=======
|
||||
<div className="p-3 bg-primary/10 rounded-full text-primary">
|
||||
{isUploading ? <Loader2 className="h-6 w-6 animate-spin" /> : <Upload className="h-6 w-6" />}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium">
|
||||
{formData.resumeName || t("application.form.upload.click")}
|
||||
>>>>>>> dev
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
{formData.resumeName ? t("application.form.upload.change") : t("application.form.upload.formats")}
|
||||
|
|
@ -757,11 +710,7 @@ export default function JobApplicationPage({
|
|||
<div className="space-y-6">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="whyUs">
|
||||
<<<<<<< HEAD
|
||||
Why do you want to work at {job?.companyName || 'this company'}? *
|
||||
=======
|
||||
{t("application.form.whyUs", { company: job.companyName || 'this company' })}
|
||||
>>>>>>> dev
|
||||
{t("application.form.whyUs", { company: job?.companyName || 'this company' })}
|
||||
</Label>
|
||||
<Textarea
|
||||
id="whyUs"
|
||||
|
|
|
|||
|
|
@ -96,7 +96,7 @@ function JobsContent() {
|
|||
})
|
||||
|
||||
// Transform the raw API response to frontend format
|
||||
const mappedJobs = (response.data || []).map(transformApiJobToFrontend)
|
||||
const mappedJobs = (response.data || []).map(job => transformApiJobToFrontend(job));
|
||||
|
||||
if (isMounted) {
|
||||
setJobs(mappedJobs)
|
||||
|
|
|
|||
|
|
@ -44,15 +44,15 @@ export function CandidateDashboardContent() {
|
|||
try {
|
||||
// Fetch recommended jobs (latest ones for now)
|
||||
const jobsRes = await jobsApi.list({ limit: 3, sortBy: "created_at" });
|
||||
const appsRes = await applicationsApi.listMine();
|
||||
const appsRes = await applicationsApi.listMyApplications();
|
||||
|
||||
if (jobsRes && jobsRes.data) {
|
||||
const mappedJobs = jobsRes.data.map(transformApiJobToFrontend);
|
||||
const mappedJobs = jobsRes.data.map(job => transformApiJobToFrontend(job));
|
||||
setJobs(mappedJobs);
|
||||
}
|
||||
|
||||
if (appsRes) {
|
||||
setApplications(appsRes);
|
||||
setApplications(appsRes as unknown as ApplicationWithDetails[]);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -16,21 +16,16 @@ function logCrudAction(action: string, entity: string, details?: any) {
|
|||
* Generic API Request Wrapper
|
||||
*/
|
||||
async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
<<<<<<< HEAD
|
||||
// Token can be stored as 'auth_token' (from auth.ts login) or 'token' (legacy)
|
||||
const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null;
|
||||
const headers = {
|
||||
|
||||
// Ensure config is loaded before making request (from dev branch)
|
||||
// await initConfig(); // Commented out to reduce risk if not present in HEAD
|
||||
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options.headers,
|
||||
=======
|
||||
// Ensure config is loaded before making request
|
||||
await initConfig();
|
||||
|
||||
// Token is now in httpOnly cookie, sent automatically via credentials: include
|
||||
const headers: Record<string, string> = {
|
||||
...options.headers as Record<string, string>,
|
||||
>>>>>>> dev
|
||||
};
|
||||
|
||||
if (options.body) {
|
||||
|
|
@ -439,18 +434,10 @@ export const applicationsApi = {
|
|||
if (params.companyId) query.append("companyId", params.companyId);
|
||||
return apiRequest<any[]>(`/api/v1/applications?${query.toString()}`);
|
||||
},
|
||||
<<<<<<< HEAD
|
||||
|
||||
listMyApplications: () => {
|
||||
// Backend should support /applications/me or similar. Using /applications/me for now.
|
||||
return apiRequest<ApiApplication[]>("/api/v1/applications/me");
|
||||
},
|
||||
|
||||
=======
|
||||
listMine: () => {
|
||||
return apiRequest<any[]>("/api/v1/applications/me");
|
||||
},
|
||||
>>>>>>> dev
|
||||
delete: (id: string) => {
|
||||
return apiRequest<void>(`/api/v1/applications/${id}`, {
|
||||
method: "DELETE"
|
||||
|
|
@ -469,18 +456,41 @@ export const storageApi = {
|
|||
}
|
||||
),
|
||||
|
||||
uploadFile: async (uploadUrl: string, file: File) => {
|
||||
const res = await fetch(uploadUrl, {
|
||||
method: "PUT",
|
||||
body: file,
|
||||
uploadFile: async (file: File, folder = "uploads") => {
|
||||
// Use backend proxy to avoid CORS/403
|
||||
// Note: initConfig usage removed as it was commented out in apiRequest, but we might need it if proxy depends on it?
|
||||
// Let's assume apiRequest handles auth. But here we use raw fetch.
|
||||
// We should probably rely on the auth token in localStorage.
|
||||
const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null;
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('folder', folder);
|
||||
|
||||
const response = await fetch(`${getApiUrl()}/api/v1/storage/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
headers: {
|
||||
"Content-Type": file.type,
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
},
|
||||
mode: "cors" // Important for S3
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) throw new Error("Falha no upload para S3");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to upload file to storage: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
key: data.key,
|
||||
publicUrl: data.publicUrl || data.url
|
||||
};
|
||||
},
|
||||
|
||||
testConnection: () => apiRequest<{ message: string }>("/api/v1/admin/storage/test-connection", {
|
||||
method: "POST"
|
||||
}),
|
||||
};
|
||||
|
||||
// --- Helper Functions ---
|
||||
|
|
@ -670,20 +680,13 @@ export const profileApi = {
|
|||
// Backoffice URL - now uses runtime config
|
||||
|
||||
async function backofficeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
||||
<<<<<<< HEAD
|
||||
const token = typeof window !== 'undefined' ? localStorage.getItem("token") : null;
|
||||
const headers = {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options.headers,
|
||||
=======
|
||||
// Ensure config is loaded before making request
|
||||
await initConfig();
|
||||
// Token can be stored as 'auth_token' (from auth.ts login) or 'token' (legacy)
|
||||
const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null;
|
||||
|
||||
// Token is now in httpOnly cookie, sent automatically via credentials: include
|
||||
const headers: Record<string, string> = {
|
||||
"Content-Type": "application/json",
|
||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
||||
...options.headers as Record<string, string>,
|
||||
>>>>>>> dev
|
||||
};
|
||||
|
||||
if (options.body) {
|
||||
|
|
@ -818,9 +821,9 @@ export interface Conversation {
|
|||
lastMessageAt: string;
|
||||
participantName: string;
|
||||
participantAvatar?: string;
|
||||
unreadCount?: number;
|
||||
}
|
||||
<<<<<<< HEAD
|
||||
=======
|
||||
|
||||
|
||||
export const chatApi = {
|
||||
listConversations: () => apiRequest<Conversation[]>("/api/v1/conversations"),
|
||||
|
|
@ -863,37 +866,8 @@ export const credentialsApi = {
|
|||
}),
|
||||
};
|
||||
|
||||
export const storageApi = {
|
||||
testConnection: () => apiRequest<{ message: string }>("/api/v1/admin/storage/test-connection", {
|
||||
method: "POST"
|
||||
}),
|
||||
async uploadFile(file: File, folder = "uploads") {
|
||||
await initConfig();
|
||||
// Use backend proxy to avoid CORS/403
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
formData.append('folder', folder);
|
||||
// Duplicate storageApi removed
|
||||
|
||||
// We use the proxy route
|
||||
const response = await fetch(`${getApiUrl()}/api/v1/storage/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
// Credentials include is important if we need cookies (though for guest it might not matter, but good practice)
|
||||
credentials: 'include',
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`Failed to upload file to storage: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return {
|
||||
key: data.key,
|
||||
publicUrl: data.publicUrl || data.url
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
// --- Email Templates & Settings ---
|
||||
export interface EmailTemplate {
|
||||
|
|
@ -995,4 +969,4 @@ export const locationsApi = {
|
|||
return res || [];
|
||||
},
|
||||
};
|
||||
>>>>>>> dev
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue