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 {
|
try {
|
||||||
toast({ title: "Enviando foto..." });
|
toast({ title: "Enviando foto..." });
|
||||||
// 1. Get presigned URL
|
toast({ title: "Enviando foto..." });
|
||||||
const { uploadUrl, publicUrl } = await storageApi.getUploadUrl(file.name, file.type);
|
// 1. Upload via Proxy (avoids CORS)
|
||||||
// 2. Upload
|
const { publicUrl } = await storageApi.uploadFile(file, "avatars");
|
||||||
await storageApi.uploadFile(uploadUrl, file);
|
|
||||||
// 3. Update state
|
// 2. Update state
|
||||||
setProfilePic(publicUrl);
|
setProfilePic(publicUrl);
|
||||||
toast({ title: "Foto enviada!", description: "Não esqueça de salvar o perfil." });
|
toast({ title: "Foto enviada!", description: "Não esqueça de salvar o perfil." });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,6 @@
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
<<<<<<< HEAD
|
|
||||||
import { ptBR } from "date-fns/locale";
|
import { ptBR } from "date-fns/locale";
|
||||||
import {
|
import {
|
||||||
Building2,
|
Building2,
|
||||||
|
|
@ -11,7 +10,12 @@ import {
|
||||||
Search,
|
Search,
|
||||||
ExternalLink,
|
ExternalLink,
|
||||||
Loader2,
|
Loader2,
|
||||||
AlertCircle
|
AlertCircle,
|
||||||
|
Calendar,
|
||||||
|
CheckCircle2,
|
||||||
|
XCircle,
|
||||||
|
Clock,
|
||||||
|
FileText
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -27,15 +31,30 @@ import { Input } from "@/components/ui/input";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Navbar } from "@/components/navbar";
|
import { Navbar } from "@/components/navbar";
|
||||||
import { Footer } from "@/components/footer";
|
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;
|
jobTitle: string;
|
||||||
companyName: 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() {
|
export default function MyApplicationsPage() {
|
||||||
const [applications, setApplications] = useState<ApplicationWithJob[]>([]);
|
const [applications, setApplications] = useState<Application[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [error, setError] = useState("");
|
const [error, setError] = useState("");
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
|
|
@ -48,9 +67,9 @@ export default function MyApplicationsPage() {
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const data = await applicationsApi.listMyApplications();
|
const data = await applicationsApi.listMyApplications();
|
||||||
// The backend now returns ApplicationWithDetails which has jobTitle and companyName
|
// Backend endpoint consistency check: listMyApplications usually returns ApplicationWithDetails
|
||||||
// We cast it to our extended type
|
// Casting to ensure type safety if generic
|
||||||
setApplications(data as unknown as ApplicationWithJob[]);
|
setApplications(data as unknown as Application[]);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error("Failed to fetch applications", err);
|
console.error("Failed to fetch applications", err);
|
||||||
setError("Não foi possível carregar suas candidaturas. Tente novamente.");
|
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 =>
|
const filteredApplications = applications.filter(app =>
|
||||||
app.jobTitle.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
app.jobTitle.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
app.companyName.toLowerCase().includes(searchTerm.toLowerCase())
|
app.companyName.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
|
@ -137,7 +135,15 @@ export default function MyApplicationsPage() {
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid gap-4 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2">
|
<div className="grid gap-4 md:grid-cols-1 lg:grid-cols-2 xl:grid-cols-2">
|
||||||
{filteredApplications.map((app) => (
|
{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">
|
<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">
|
<CardHeader className="flex flex-row items-start justify-between space-y-0 pb-2">
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
|
|
@ -151,8 +157,11 @@ export default function MyApplicationsPage() {
|
||||||
{app.companyName}
|
{app.companyName}
|
||||||
</CardDescription>
|
</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Badge className={getStatusColor(app.status)} variant="secondary">
|
<Badge className={status.color} variant="secondary">
|
||||||
{getStatusLabel(app.status)}
|
<div className="flex items-center gap-1">
|
||||||
|
<StatusIcon className="h-3 w-3" />
|
||||||
|
{status.label}
|
||||||
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
|
|
@ -162,6 +171,11 @@ export default function MyApplicationsPage() {
|
||||||
Aplicado em {format(new Date(app.createdAt), "dd 'de' MMMM, yyyy", { locale: ptBR })}
|
Aplicado em {format(new Date(app.createdAt), "dd 'de' MMMM, yyyy", { locale: ptBR })}
|
||||||
</div>
|
</div>
|
||||||
</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>
|
</CardContent>
|
||||||
<CardFooter className="bg-muted/50 p-4 flex justify-between items-center">
|
<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">
|
<Link href={`/vagas/${app.jobId}`} className="text-sm font-medium text-primary hover:underline flex items-center">
|
||||||
|
|
@ -171,192 +185,21 @@ export default function MyApplicationsPage() {
|
||||||
<Link
|
<Link
|
||||||
href={app.resumeUrl}
|
href={app.resumeUrl}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4"
|
className="text-sm text-muted-foreground hover:text-foreground underline underline-offset-4 flex items-center"
|
||||||
>
|
>
|
||||||
Ver Currículo Enviado
|
<FileText className="h-3 w-3 mr-1" />
|
||||||
|
Ver Currículo
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</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>
|
</div>
|
||||||
)}
|
)}
|
||||||
>>>>>>> dev
|
</main>
|
||||||
|
|
||||||
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,8 @@ import { useEffect, useState, useCallback } from "react"
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
>>>>>>> dev
|
|
||||||
} from "@/components/ui/card"
|
} from "@/components/ui/card"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Badge } from "@/components/ui/badge"
|
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 { Button } from "@/components/ui/button";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle, CardFooter } from "@/components/ui/card";
|
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 { Label } from "@/components/ui/label";
|
||||||
import { Navbar } from "@/components/navbar";
|
import { Navbar } from "@/components/navbar";
|
||||||
import { Footer } from "@/components/footer";
|
import { Footer } from "@/components/footer";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import { ArrowLeft, Mail } from "lucide-react";
|
|
||||||
|
|
||||||
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8521";
|
const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8521";
|
||||||
|
|
||||||
|
|
@ -53,30 +53,6 @@ export default function ForgotPasswordPage() {
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
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="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">
|
<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 */}
|
{/* Back to Login - Mobile friendly top placement */}
|
||||||
|
|
@ -108,7 +84,8 @@ export default function ForgotPasswordPage() {
|
||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<form onSubmit={handleSubmit} className="space-y-4">
|
{!submitted && (
|
||||||
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="email" className="text-sm sm:text-base">
|
<Label htmlFor="email" className="text-sm sm:text-base">
|
||||||
{t("auth.forgot.fields.email")}
|
{t("auth.forgot.fields.email")}
|
||||||
|
|
@ -116,58 +93,24 @@ export default function ForgotPasswordPage() {
|
||||||
<Input
|
<Input
|
||||||
id="email"
|
id="email"
|
||||||
type="email"
|
type="email"
|
||||||
value={email}
|
|
||||||
onChange={(event) => setEmail(event.target.value)}
|
|
||||||
placeholder={t("auth.forgot.fields.emailPlaceholder")}
|
placeholder={t("auth.forgot.fields.emailPlaceholder")}
|
||||||
required
|
|
||||||
className="h-10 sm:h-11"
|
className="h-10 sm:h-11"
|
||||||
/>
|
|
||||||
>>>>>>> dev
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<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" })}
|
{...register("email", { required: t("validations.required") || "Email is required" })}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
{errors.email && <p className="text-sm text-destructive">{errors.email.message}</p>}
|
{errors.email && <p className="text-sm text-destructive">{errors.email.message}</p>}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<<<<<<< HEAD
|
|
||||||
{error && <p className="text-sm text-destructive text-center">{error}</p>}
|
{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}
|
{loading ? <Loader2 className="w-4 h-4 mr-2 animate-spin" /> : null}
|
||||||
{t("auth.forgot.submit")}
|
{t("auth.forgot.submit")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
=======
|
|
||||||
<Button type="submit" className="w-full h-10 sm:h-11">
|
|
||||||
{t("auth.forgot.submit")}
|
|
||||||
</Button>
|
|
||||||
</form>
|
|
||||||
>>>>>>> dev
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
{!submitted && (
|
{!submitted && (
|
||||||
<CardFooter className="justify-center">
|
<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>
|
|
||||||
</CardFooter>
|
|
||||||
)}
|
|
||||||
</Card>
|
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
|
|
||||||
{/* Back to Login - Desktop */}
|
{/* Back to Login - Desktop */}
|
||||||
<div className="hidden sm:block text-center">
|
<div className="hidden sm:block text-center">
|
||||||
<Link
|
<Link
|
||||||
|
|
@ -178,10 +121,13 @@ export default function ForgotPasswordPage() {
|
||||||
{t("auth.forgot.backLogin")}
|
{t("auth.forgot.backLogin")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
>>>>>>> dev
|
</CardFooter>
|
||||||
|
)}
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<Footer />
|
<Footer />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,7 @@ import {
|
||||||
MessageSquare,
|
MessageSquare,
|
||||||
Save,
|
Save,
|
||||||
ArrowLeft,
|
ArrowLeft,
|
||||||
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
|
|
@ -42,16 +43,11 @@ import { Navbar } from "@/components/navbar";
|
||||||
import { Footer } from "@/components/footer";
|
import { Footer } from "@/components/footer";
|
||||||
import { useNotify } from "@/contexts/notification-context";
|
import { useNotify } from "@/contexts/notification-context";
|
||||||
import { jobsApi, applicationsApi, storageApi, type ApiJob } from "@/lib/api";
|
import { jobsApi, applicationsApi, storageApi, type ApiJob } from "@/lib/api";
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
import { formatPhone } from "@/lib/utils";
|
import { formatPhone } from "@/lib/utils";
|
||||||
import { useTranslation } from "@/lib/i18n";
|
import { useTranslation } from "@/lib/i18n";
|
||||||
import { getCurrentUser } from "@/lib/auth";
|
import { getCurrentUser } from "@/lib/auth";
|
||||||
|
|
||||||
|
|
||||||
>>>>>>> dev
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const runtime = 'edge';
|
export const runtime = 'edge';
|
||||||
|
|
||||||
|
|
@ -234,27 +230,9 @@ export default function JobApplicationPage({
|
||||||
const handleSubmit = async () => {
|
const handleSubmit = async () => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true);
|
||||||
try {
|
try {
|
||||||
let resumeUrl = "";
|
// 1. Resume is already uploaded via handleResumeUpload, so we use formData.resumeUrl
|
||||||
|
const resumeUrl = formData.resumeUrl;
|
||||||
// 1. Upload Curriculo if present
|
// Note: If you want to enforce upload here, you'd need the File object, but we uploaded it earlier.
|
||||||
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.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await applicationsApi.create({
|
await applicationsApi.create({
|
||||||
jobId: Number(id), // ID might need number conversion depending on API
|
jobId: Number(id), // ID might need number conversion depending on API
|
||||||
|
|
@ -262,7 +240,6 @@ export default function JobApplicationPage({
|
||||||
email: formData.email,
|
email: formData.email,
|
||||||
phone: formData.phone,
|
phone: formData.phone,
|
||||||
linkedin: formData.linkedin,
|
linkedin: formData.linkedin,
|
||||||
<<<<<<< HEAD
|
|
||||||
coverLetter: formData.coverLetter || formData.whyUs, // Fallback
|
coverLetter: formData.coverLetter || formData.whyUs, // Fallback
|
||||||
resumeUrl: resumeUrl,
|
resumeUrl: resumeUrl,
|
||||||
portfolioUrl: formData.portfolioUrl,
|
portfolioUrl: formData.portfolioUrl,
|
||||||
|
|
@ -270,18 +247,6 @@ export default function JobApplicationPage({
|
||||||
hasExperience: formData.hasExperience,
|
hasExperience: formData.hasExperience,
|
||||||
whyUs: formData.whyUs,
|
whyUs: formData.whyUs,
|
||||||
availability: formData.availability,
|
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(
|
notify.success(
|
||||||
|
|
@ -311,7 +276,21 @@ export default function JobApplicationPage({
|
||||||
|
|
||||||
const progress = (currentStep / steps.length) * 100;
|
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) {
|
if (isSubmitted) {
|
||||||
const user = getCurrentUser();
|
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 className="flex flex-col md:flex-row md:items-center md:justify-between gap-4">
|
||||||
<div>
|
<div>
|
||||||
<h1 className="text-2xl md:text-3xl font-bold text-foreground">
|
<h1 className="text-2xl md:text-3xl font-bold text-foreground">
|
||||||
<<<<<<< HEAD
|
|
||||||
Application: {job?.title}
|
|
||||||
=======
|
|
||||||
{t("application.title", { jobTitle: job.title })}
|
{t("application.title", { jobTitle: job.title })}
|
||||||
>>>>>>> dev
|
|
||||||
</h1>
|
</h1>
|
||||||
<p className="text-muted-foreground mt-1">
|
<p className="text-muted-foreground mt-1">
|
||||||
{job?.companyName || 'Company'} • {job?.location || 'Remote'}
|
{job?.companyName || 'Company'} • {job?.location || 'Remote'}
|
||||||
|
|
@ -604,34 +579,12 @@ export default function JobApplicationPage({
|
||||||
disabled={isUploading}
|
disabled={isUploading}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col items-center gap-2">
|
<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">
|
<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" />}
|
{isUploading ? <Loader2 className="h-6 w-6 animate-spin" /> : <Upload className="h-6 w-6" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-1">
|
<div className="space-y-1">
|
||||||
<p className="text-sm font-medium">
|
<p className="text-sm font-medium">
|
||||||
{formData.resumeName || t("application.form.upload.click")}
|
{formData.resumeName || t("application.form.upload.click")}
|
||||||
>>>>>>> dev
|
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground">
|
<p className="text-xs text-muted-foreground">
|
||||||
{formData.resumeName ? t("application.form.upload.change") : t("application.form.upload.formats")}
|
{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-6">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="whyUs">
|
<Label htmlFor="whyUs">
|
||||||
<<<<<<< HEAD
|
{t("application.form.whyUs", { company: job?.companyName || 'this company' })}
|
||||||
Why do you want to work at {job?.companyName || 'this company'}? *
|
|
||||||
=======
|
|
||||||
{t("application.form.whyUs", { company: job.companyName || 'this company' })}
|
|
||||||
>>>>>>> dev
|
|
||||||
</Label>
|
</Label>
|
||||||
<Textarea
|
<Textarea
|
||||||
id="whyUs"
|
id="whyUs"
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ function JobsContent() {
|
||||||
})
|
})
|
||||||
|
|
||||||
// Transform the raw API response to frontend format
|
// Transform the raw API response to frontend format
|
||||||
const mappedJobs = (response.data || []).map(transformApiJobToFrontend)
|
const mappedJobs = (response.data || []).map(job => transformApiJobToFrontend(job));
|
||||||
|
|
||||||
if (isMounted) {
|
if (isMounted) {
|
||||||
setJobs(mappedJobs)
|
setJobs(mappedJobs)
|
||||||
|
|
|
||||||
|
|
@ -44,15 +44,15 @@ export function CandidateDashboardContent() {
|
||||||
try {
|
try {
|
||||||
// Fetch recommended jobs (latest ones for now)
|
// Fetch recommended jobs (latest ones for now)
|
||||||
const jobsRes = await jobsApi.list({ limit: 3, sortBy: "created_at" });
|
const jobsRes = await jobsApi.list({ limit: 3, sortBy: "created_at" });
|
||||||
const appsRes = await applicationsApi.listMine();
|
const appsRes = await applicationsApi.listMyApplications();
|
||||||
|
|
||||||
if (jobsRes && jobsRes.data) {
|
if (jobsRes && jobsRes.data) {
|
||||||
const mappedJobs = jobsRes.data.map(transformApiJobToFrontend);
|
const mappedJobs = jobsRes.data.map(job => transformApiJobToFrontend(job));
|
||||||
setJobs(mappedJobs);
|
setJobs(mappedJobs);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (appsRes) {
|
if (appsRes) {
|
||||||
setApplications(appsRes);
|
setApplications(appsRes as unknown as ApplicationWithDetails[]);
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
||||||
|
|
@ -16,21 +16,16 @@ function logCrudAction(action: string, entity: string, details?: any) {
|
||||||
* Generic API Request Wrapper
|
* Generic API Request Wrapper
|
||||||
*/
|
*/
|
||||||
async function apiRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
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)
|
// 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 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",
|
"Content-Type": "application/json",
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
...(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>,
|
...options.headers as Record<string, string>,
|
||||||
>>>>>>> dev
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.body) {
|
if (options.body) {
|
||||||
|
|
@ -439,18 +434,10 @@ export const applicationsApi = {
|
||||||
if (params.companyId) query.append("companyId", params.companyId);
|
if (params.companyId) query.append("companyId", params.companyId);
|
||||||
return apiRequest<any[]>(`/api/v1/applications?${query.toString()}`);
|
return apiRequest<any[]>(`/api/v1/applications?${query.toString()}`);
|
||||||
},
|
},
|
||||||
<<<<<<< HEAD
|
|
||||||
|
|
||||||
listMyApplications: () => {
|
listMyApplications: () => {
|
||||||
// Backend should support /applications/me or similar. Using /applications/me for now.
|
// Backend should support /applications/me or similar. Using /applications/me for now.
|
||||||
return apiRequest<ApiApplication[]>("/api/v1/applications/me");
|
return apiRequest<ApiApplication[]>("/api/v1/applications/me");
|
||||||
},
|
},
|
||||||
|
|
||||||
=======
|
|
||||||
listMine: () => {
|
|
||||||
return apiRequest<any[]>("/api/v1/applications/me");
|
|
||||||
},
|
|
||||||
>>>>>>> dev
|
|
||||||
delete: (id: string) => {
|
delete: (id: string) => {
|
||||||
return apiRequest<void>(`/api/v1/applications/${id}`, {
|
return apiRequest<void>(`/api/v1/applications/${id}`, {
|
||||||
method: "DELETE"
|
method: "DELETE"
|
||||||
|
|
@ -469,18 +456,41 @@ export const storageApi = {
|
||||||
}
|
}
|
||||||
),
|
),
|
||||||
|
|
||||||
uploadFile: async (uploadUrl: string, file: File) => {
|
uploadFile: async (file: File, folder = "uploads") => {
|
||||||
const res = await fetch(uploadUrl, {
|
// Use backend proxy to avoid CORS/403
|
||||||
method: "PUT",
|
// Note: initConfig usage removed as it was commented out in apiRequest, but we might need it if proxy depends on it?
|
||||||
body: file,
|
// 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: {
|
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 ---
|
// --- Helper Functions ---
|
||||||
|
|
@ -670,20 +680,13 @@ export const profileApi = {
|
||||||
// Backoffice URL - now uses runtime config
|
// Backoffice URL - now uses runtime config
|
||||||
|
|
||||||
async function backofficeRequest<T>(endpoint: string, options: RequestInit = {}): Promise<T> {
|
async function backofficeRequest<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("token") : null;
|
const token = typeof window !== 'undefined' ? (localStorage.getItem("auth_token") || localStorage.getItem("token")) : null;
|
||||||
const headers = {
|
|
||||||
|
const headers: Record<string, string> = {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
...(token ? { Authorization: `Bearer ${token}` } : {}),
|
...(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>,
|
...options.headers as Record<string, string>,
|
||||||
>>>>>>> dev
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (options.body) {
|
if (options.body) {
|
||||||
|
|
@ -818,9 +821,9 @@ export interface Conversation {
|
||||||
lastMessageAt: string;
|
lastMessageAt: string;
|
||||||
participantName: string;
|
participantName: string;
|
||||||
participantAvatar?: string;
|
participantAvatar?: string;
|
||||||
|
unreadCount?: number;
|
||||||
}
|
}
|
||||||
<<<<<<< HEAD
|
|
||||||
=======
|
|
||||||
|
|
||||||
export const chatApi = {
|
export const chatApi = {
|
||||||
listConversations: () => apiRequest<Conversation[]>("/api/v1/conversations"),
|
listConversations: () => apiRequest<Conversation[]>("/api/v1/conversations"),
|
||||||
|
|
@ -863,37 +866,8 @@ export const credentialsApi = {
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const storageApi = {
|
// Duplicate storageApi removed
|
||||||
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);
|
|
||||||
|
|
||||||
// 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 ---
|
// --- Email Templates & Settings ---
|
||||||
export interface EmailTemplate {
|
export interface EmailTemplate {
|
||||||
|
|
@ -995,4 +969,4 @@ export const locationsApi = {
|
||||||
return res || [];
|
return res || [];
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
>>>>>>> dev
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue