395 lines
18 KiB
TypeScript
395 lines
18 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useState } from "react";
|
|
import { useForm, useFieldArray } from "react-hook-form";
|
|
import {
|
|
Loader2,
|
|
MapPin,
|
|
Plus,
|
|
Save,
|
|
Trash2,
|
|
Upload,
|
|
User as UserIcon,
|
|
Briefcase,
|
|
GraduationCap
|
|
} from "lucide-react";
|
|
import Image from "next/image";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import {
|
|
Card,
|
|
CardContent,
|
|
CardDescription,
|
|
CardHeader,
|
|
CardTitle,
|
|
CardFooter
|
|
} from "@/components/ui/card";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Separator } from "@/components/ui/separator";
|
|
|
|
import { Navbar } from "@/components/navbar";
|
|
import { Footer } from "@/components/footer";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
|
|
// We'll update api.ts to include usersApi.updateMe later
|
|
// Mocking for now or assuming it exists
|
|
import { storageApi, usersApi } from "@/lib/api";
|
|
|
|
type Experience = {
|
|
company: string;
|
|
position: string;
|
|
description: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
};
|
|
|
|
type Education = {
|
|
institution: string;
|
|
degree: string;
|
|
field: string;
|
|
startDate: string;
|
|
endDate: string;
|
|
};
|
|
|
|
type ProfileFormValues = {
|
|
fullName: string;
|
|
email: string;
|
|
phone: string;
|
|
whatsapp: string;
|
|
bio: string;
|
|
skills: string; // Comma separated for input
|
|
experience: Experience[];
|
|
education: Education[];
|
|
};
|
|
|
|
export default function ProfilePage() {
|
|
const { toast } = useToast();
|
|
const [loading, setLoading] = useState(true);
|
|
const [saving, setSaving] = useState(false);
|
|
const [user, setUser] = useState<any>(null);
|
|
const [profilePic, setProfilePic] = useState<string | null>(null);
|
|
|
|
const { register, control, handleSubmit, reset, setValue, watch } = useForm<ProfileFormValues>({
|
|
defaultValues: {
|
|
experience: [],
|
|
education: []
|
|
}
|
|
});
|
|
|
|
const { fields: expFields, append: appendExp, remove: removeExp } = useFieldArray({
|
|
control,
|
|
name: "experience"
|
|
});
|
|
|
|
const { fields: eduFields, append: appendEdu, remove: removeEdu } = useFieldArray({
|
|
control,
|
|
name: "education"
|
|
});
|
|
|
|
useEffect(() => {
|
|
fetchProfile();
|
|
}, []);
|
|
|
|
const fetchProfile = async () => {
|
|
try {
|
|
setLoading(true);
|
|
// Assuming getMe exists, if not we create it
|
|
// const userData = await usersApi.getMe();
|
|
// For now, let's assume valid response structure based on our backend implementation
|
|
// But api.ts might not have getMe yet.
|
|
|
|
// To be safe, we might implement getMe in api.ts first?
|
|
// Or we check what is available.
|
|
// Current 'authApi.me' might be available?
|
|
// Let's assume we can fetch user.
|
|
|
|
// Fallback mock for development if backend not ready
|
|
// const userData = mockUser;
|
|
|
|
const userData = await usersApi.getMe(); // We will ensure this exists in api.ts
|
|
|
|
setUser(userData);
|
|
setProfilePic(userData.profilePictureUrl || null);
|
|
|
|
reset({
|
|
fullName: userData.name,
|
|
email: userData.email,
|
|
// phone: userData.phone, // Phone might not be in Core User yet? We didn't add it to Entity.
|
|
bio: userData.bio || "",
|
|
skills: userData.skills?.join(", ") || "",
|
|
experience: userData.experience || [],
|
|
education: userData.education || []
|
|
});
|
|
|
|
} catch (error) {
|
|
console.error(error);
|
|
toast({
|
|
title: "Erro ao carregar perfil",
|
|
description: "Não foi possível carregar seus dados.",
|
|
variant: "destructive"
|
|
});
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
|
|
const handleAvatarUpload = async (e: React.ChangeEvent<HTMLInputElement>) => {
|
|
const file = e.target.files?.[0];
|
|
if (!file) return;
|
|
|
|
try {
|
|
toast({ title: "Enviando foto..." });
|
|
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) {
|
|
console.error(err);
|
|
toast({ title: "Erro no upload", variant: "destructive" });
|
|
}
|
|
};
|
|
|
|
const onSubmit = async (data: ProfileFormValues) => {
|
|
try {
|
|
setSaving(true);
|
|
const skillsArray = data.skills.split(",").map(s => s.trim()).filter(Boolean);
|
|
|
|
await usersApi.updateMe({
|
|
fullName: data.fullName,
|
|
bio: data.bio,
|
|
profilePictureUrl: profilePic || undefined,
|
|
skills: skillsArray,
|
|
experience: data.experience,
|
|
education: data.education
|
|
});
|
|
|
|
toast({ title: "Perfil atualizado com sucesso!" });
|
|
} catch (error) {
|
|
console.error(error);
|
|
toast({ title: "Erro ao atualizar", variant: "destructive" });
|
|
} finally {
|
|
setSaving(false);
|
|
}
|
|
};
|
|
|
|
if (loading) {
|
|
return <div className="flex h-screen items-center justify-center"><Loader2 className="animate-spin" /></div>;
|
|
}
|
|
|
|
return (
|
|
<div className="min-h-screen bg-muted/40 pb-10">
|
|
<Navbar />
|
|
<div className="container py-10">
|
|
<div className="mb-8">
|
|
<h1 className="text-3xl font-bold">Meu Perfil</h1>
|
|
<p className="text-muted-foreground">Gerencie suas informações profissionais e pessoais.</p>
|
|
</div>
|
|
|
|
<form onSubmit={handleSubmit(onSubmit)} className="space-y-8">
|
|
|
|
{/* Basic Info & Photo */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Informações Básicas</CardTitle>
|
|
<CardDescription>Sua identidade na plataforma.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
<div className="flex flex-col md:flex-row gap-6 items-center md:items-start">
|
|
<div className="flex flex-col items-center gap-3">
|
|
<Avatar className="w-24 h-24 border-2 border-primary/20">
|
|
<AvatarImage src={profilePic || ""} />
|
|
<AvatarFallback><UserIcon className="w-10 h-10" /></AvatarFallback>
|
|
</Avatar>
|
|
<div className="relative">
|
|
<input
|
|
type="file"
|
|
id="avatar-upload"
|
|
className="hidden"
|
|
accept="image/*"
|
|
onChange={handleAvatarUpload}
|
|
/>
|
|
<Button type="button" variant="outline" size="sm" onClick={() => document.getElementById('avatar-upload')?.click()}>
|
|
<Upload className="w-3 h-3 mr-2" /> Alterar Foto
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="grid gap-4 flex-1 w-full">
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Nome Completo</Label>
|
|
<Input {...register("fullName")} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Email</Label>
|
|
<Input {...register("email")} disabled className="bg-muted" />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Bio / Resumo Profissional</Label>
|
|
<Textarea
|
|
{...register("bio")}
|
|
placeholder="Conte um pouco sobre você..."
|
|
className="min-h-[100px]"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Skills */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>Competências</CardTitle>
|
|
<CardDescription>Liste suas principais habilidades técnicas e comportamentais.</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-2">
|
|
<Label>Skills (separadas por vírgula)</Label>
|
|
<Input
|
|
{...register("skills")}
|
|
placeholder="Ex: Javscript, Go, Liderança, Scrum"
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Estas tags ajudarão recrutadores a encontrar seu perfil.
|
|
</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Experience */}
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<div>
|
|
<CardTitle>Experiência Profissional</CardTitle>
|
|
<CardDescription>Seu histórico de trabalho.</CardDescription>
|
|
</div>
|
|
<Button type="button" variant="outline" size="sm" onClick={() => appendExp({ company: "", position: "", description: "", startDate: "", endDate: "" })}>
|
|
<Plus className="w-4 h-4 mr-2" /> Adicionar
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{expFields.map((field, index) => (
|
|
<div key={field.id} className="relative grid gap-4 p-4 border rounded-md">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute top-2 right-2 text-destructive hover:text-destructive/80"
|
|
onClick={() => removeExp(index)}
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Empresa</Label>
|
|
<Input {...register(`experience.${index}.company`)} placeholder="Ex: Google" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Cargo</Label>
|
|
<Input {...register(`experience.${index}.position`)} placeholder="Ex: Engenheiro de Software" />
|
|
</div>
|
|
</div>
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Início</Label>
|
|
<Input type="month" {...register(`experience.${index}.startDate`)} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Fim</Label>
|
|
<Input type="month" {...register(`experience.${index}.endDate`)} />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Descrição</Label>
|
|
<Textarea {...register(`experience.${index}.description`)} placeholder="Descreva suas responsabilidades e conquistas..." />
|
|
</div>
|
|
</div>
|
|
))}
|
|
{expFields.length === 0 && (
|
|
<div className="text-center py-8 text-muted-foreground border-2 border-dashed rounded-md">
|
|
Nenhuma experiência adicionada.
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Education */}
|
|
<Card>
|
|
<CardHeader className="flex flex-row items-center justify-between">
|
|
<div>
|
|
<CardTitle>Formação Acadêmica</CardTitle>
|
|
<CardDescription>Escolaridade e cursos.</CardDescription>
|
|
</div>
|
|
<Button type="button" variant="outline" size="sm" onClick={() => appendEdu({ institution: "", degree: "", field: "", startDate: "", endDate: "" })}>
|
|
<Plus className="w-4 h-4 mr-2" /> Adicionar
|
|
</Button>
|
|
</CardHeader>
|
|
<CardContent className="space-y-6">
|
|
{eduFields.map((field, index) => (
|
|
<div key={field.id} className="relative grid gap-4 p-4 border rounded-md">
|
|
<Button
|
|
type="button"
|
|
variant="ghost"
|
|
size="icon"
|
|
className="absolute top-2 right-2 text-destructive hover:text-destructive/80"
|
|
onClick={() => removeEdu(index)}
|
|
>
|
|
<Trash2 className="w-4 h-4" />
|
|
</Button>
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Instituição</Label>
|
|
<Input {...register(`education.${index}.institution`)} placeholder="Ex: USP" />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Grau / Nível</Label>
|
|
<Input {...register(`education.${index}.degree`)} placeholder="Ex: Bacharelado" />
|
|
</div>
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Curso / Área de Estudo</Label>
|
|
<Input {...register(`education.${index}.field`)} placeholder="Ex: Ciência da Computação" />
|
|
</div>
|
|
<div className="grid md:grid-cols-2 gap-4">
|
|
<div className="space-y-2">
|
|
<Label>Início</Label>
|
|
<Input type="month" {...register(`education.${index}.startDate`)} />
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label>Fim</Label>
|
|
<Input type="month" {...register(`education.${index}.endDate`)} />
|
|
</div>
|
|
</div>
|
|
</div>
|
|
))}
|
|
{eduFields.length === 0 && (
|
|
<div className="text-center py-8 text-muted-foreground border-2 border-dashed rounded-md">
|
|
Nenhuma formação adicionada.
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="flex justify-end gap-4 sticky bottom-4 z-10 bg-background/80 backdrop-blur-sm p-4 rounded-lg border shadow-lg">
|
|
<Button type="button" variant="outline" onClick={() => reset()}>Descartar Alterações</Button>
|
|
<Button type="submit" disabled={saving}>
|
|
{saving && <Loader2 className="w-4 h-4 mr-2 animate-spin" />}
|
|
Salvar Alterações
|
|
</Button>
|
|
</div>
|
|
|
|
</form>
|
|
</div>
|
|
<Footer />
|
|
</div>
|
|
);
|
|
}
|