Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions app/profile/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// app/profile/page.tsx
"use client";

import { useState, useEffect } from "react";
import Navbar from "@/components/Navbar";
import ProfileCard from "@/components/ProfileCard";
import EditProfileModal from "@/components/EditProfileModal";
import { supabase } from "@/lib/supabase";
import { ProfileDTO } from "@/lib/dto/profiles";
import { User } from "@supabase/supabase-js";

const EMPTY_PROFILE: ProfileDTO = {
name: "",
grad_year: "",
linkedin: "",
github: "",
experiences: [],
};

export default function ProfilePage() {
const [user, setUser] = useState<User | null>(null);
const [profile, setProfile] = useState<ProfileDTO>(EMPTY_PROFILE);
const [modalOpen, setModalOpen] = useState(false);

useEffect(() => {
supabase.auth.getSession().then(({ data }) => {
setUser(data.session?.user ?? null);
});

const { data: { subscription } } = supabase.auth.onAuthStateChange((_e, session) => {
setUser(session?.user ?? null);
});

return () => subscription.unsubscribe();
}, []);

function handleSave(data: ProfileDTO) {
setProfile(data);
// TODO: supabase upsert
}

const hasProfile = !!profile.name;

return (
<main className="min-h-screen bg-white">
<Navbar user={user} />

<div className="p-4 sm:p-8">
{!user ? (
<div className="flex items-center justify-center min-h-[60vh]">
<p className="font-mono text-xs uppercase opacity-30">
Sign in to create your profile
</p>
</div>
) : (
<div className="space-y-6">
<div className="flex items-center justify-between">
<p className="font-mono text-[10px] uppercase opacity-40">Your Card</p>
{!hasProfile && (
<button
onClick={() => setModalOpen(true)}
className="font-mono text-xs uppercase border-2 border-black px-3 py-1 hover:bg-black hover:text-white transition-colors"
>
Create Profile
</button>
)}
</div>

<div className="w-full sm:w-72">
<ProfileCard
profile={profile}
editable={hasProfile}
onEdit={() => setModalOpen(true)}
/>
</div>
</div>
)}
</div>

{modalOpen && (
<EditProfileModal
initial={profile}
onSave={handleSave}
onClose={() => setModalOpen(false)}
/>
)}
</main>
);
}
55 changes: 41 additions & 14 deletions components/ProfileCard.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,35 @@
// Thank Peter for his pre-made components
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Github, Linkedin, Pencil } from "lucide-react";
import type { ProfileDTO } from "@/lib/dto/profiles";
import type { ProfileDTO, Contact, Experience } from "@/lib/dto/profile";

interface ProfileCardProps {
profile: ProfileDTO | null;
editable?: boolean;
onEdit?: () => void;
}

// Helper methods for clean formatting
function formatTerm(term: [string, number] | null | undefined, fallback: string): string {
if (!term) return fallback;
return `${term[0]} '${String(term[1]).slice(2)}`;
}

function formatTermRange(profile: ProfileDTO): string {
const start = formatTerm(profile.startTerm, "?");
const end = profile.endTerm ? formatTerm(profile.endTerm, "Present") : "Present";
return `${start} — ${end}`;
}

function getContact(contacts: Contact[], type: "LinkedIn" | "GitHub"): string | null {
const match = contacts.find(([t]) => t === type);
return match ? match[1] : null;
}

function formatExperience(exp: Experience): { company: string; role: string } {
return { company: exp[2], role: exp[1] };
}

// Destructure the prop, ignore all types for now unless we want to add an interface later on
// Thank Peter for his pre-made components

Expand All @@ -17,6 +38,9 @@ export default function ProfileCard({ profile, editable = false, onEdit }: Profi
if (!profile) return null;

const isProfileEmpty = !profile.name;
const linkedin = getContact(profile.contact ?? [], "LinkedIn");
const github = getContact(profile.contact ?? [], "GitHub");
const termRange = formatTermRange(profile);

return (
<Card className="rounded-none border-2 border-black shadow-none hover:bg-black hover:text-white transition-colors group">
Expand All @@ -31,15 +55,15 @@ export default function ProfileCard({ profile, editable = false, onEdit }: Profi
</CardTitle>

<div className="flex gap-3 mt-2">
{profile?.linkedin ? (
<a href={profile.linkedin} target="_blank" rel="noreferrer">
{linkedin ? (
<a href={linkedin} target="_blank" rel="noreferrer">
<Linkedin size={16} className="cursor-pointer hover:opacity-50" />
</a>
) : (
<Linkedin size={16} className="opacity-20" />
)}
{profile?.github ? (
<a href={profile.github} target="_blank" rel="noreferrer">
{github ? (
<a href={github} target="_blank" rel="noreferrer">
<Github size={16} className="cursor-pointer hover:opacity-50" />
</a>
) : (
Expand All @@ -49,8 +73,8 @@ export default function ProfileCard({ profile, editable = false, onEdit }: Profi
</div>

<div className="flex flex-col items-end gap-2">
<span className="font-mono text-xs border border-black px-2 group-hover:border-white shrink-0">
{profile?.grad_year ?? "----"}
<span className="font-mono text-xs border border-black px-2 group-hover:border-white shrink-0 whitespace-nowrap">
{isProfileEmpty ? "---" : termRange}
</span>

{editable && (
Expand All @@ -66,14 +90,17 @@ export default function ProfileCard({ profile, editable = false, onEdit }: Profi
</CardHeader>

<CardContent className="space-y-4">
{(profile.experiences ?? []).map((exp, i: number) => (
<div key={i} className="border-l-2 border-black group-hover:border-white pl-3 transition-colors">
<p className="text-sm font-bold uppercase leading-tight">{exp.company}</p>
<p className="text-xs opacity-70 italic">{exp.role}</p>
</div>
))}
{(profile.experience ?? []).map((exp, i) => {
const { company, role } = formatExperience(exp);
return (
<div key={i} className="border-l-2 border-black group-hover:border-white pl-3 transition-colors">
<p className="text-sm font-bold uppercase leading-tight">{company}</p>
<p className="text-xs opacity-70 italic">{role}</p>
</div>
);
})}

{(!profile.experiences || profile.experiences.length === 0) && (
{(!profile.experience || profile.experience.length === 0) && (
<p className="text-[10px] uppercase opacity-30 italic">No history available</p>
)}
</CardContent>
Expand Down
14 changes: 0 additions & 14 deletions lib/dto/profiles.ts

This file was deleted.