diff --git a/app/protected/page.tsx b/app/protected/page.tsx
index 04352e6c..10f50a92 100644
--- a/app/protected/page.tsx
+++ b/app/protected/page.tsx
@@ -1,7 +1,10 @@
import { redirect } from "next/navigation";
import { createClient } from "@/lib/supabase/server";
-import { Sparkles, Rocket, Shield, User } from "lucide-react";
+import { Sparkles, Rocket, Shield, User, Settings } from "lucide-react";
import Link from "next/link";
+import { Button } from "@/components/ui/button";
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
+import MembershipCard from "@/components/MembershipCard";
export default async function ProtectedPage() {
const supabase = await createClient();
@@ -61,6 +64,11 @@ export default async function ProtectedPage() {
+ {/* Membership Card Section */}
+
+
+
+
@@ -96,6 +104,27 @@ export default async function ProtectedPage() {
+ {/* Profile Settings Card */}
+
+
+
+
+ Customize Your Profile
+
+
+ Add your information, social links, and customize how others see you
+
+
+
+
+
+
+ Manage Profile
+
+
+
+
+
diff --git a/app/protected/profile/page.tsx b/app/protected/profile/page.tsx
new file mode 100644
index 00000000..76924844
--- /dev/null
+++ b/app/protected/profile/page.tsx
@@ -0,0 +1,37 @@
+import { redirect } from "next/navigation";
+import { createClient } from "@/lib/supabase/server";
+import Link from "next/link";
+import { ArrowLeft } from "lucide-react";
+import { ProfileSettings } from "@/components/profile/ProfileSettings";
+
+export default async function ProfilePage() {
+ const supabase = await createClient();
+
+ const { data, error } = await supabase.auth.getUser();
+ if (error || !data?.user) {
+ redirect("/auth/signin");
+ }
+
+ return (
+
+ {/* Header */}
+
+
+
+
+
+ Back to Dashboard
+
+
Profile Settings
+
+
+
+
+ {/* Profile Settings Component */}
+
+
+ );
+}
diff --git a/components/MembershipCard.tsx b/components/MembershipCard.tsx
new file mode 100644
index 00000000..51940a87
--- /dev/null
+++ b/components/MembershipCard.tsx
@@ -0,0 +1,282 @@
+"use client";
+
+import React, { useRef } from 'react';
+import jsPDF from 'jspdf';
+import html2canvas from 'html2canvas';
+
+interface MembershipCardProps {
+ uid: string;
+}
+
+const MembershipCard: React.FC
= ({ uid }) => {
+ const cardRef = useRef(null);
+ const pdfContentRef = useRef(null);
+ const memberId = `CU-${uid.slice(-4)}`;
+
+ const handleDownload = async () => {
+ if (pdfContentRef.current) {
+ const canvas = await html2canvas(pdfContentRef.current, {
+ scale: 2,
+ useCORS: true,
+ allowTaint: true,
+ backgroundColor: '#ffffff'
+ });
+
+ const imgData = canvas.toDataURL('image/png');
+ const pdf = new jsPDF({
+ orientation: 'portrait',
+ unit: 'mm',
+ format: 'a4'
+ });
+
+ const imgWidth = 210;
+ const imgHeight = (canvas.height * imgWidth) / canvas.width;
+
+ pdf.addImage(imgData, 'PNG', 0, 0, imgWidth, imgHeight);
+ pdf.save(`${memberId}-membership-card.pdf`);
+ }
+ };
+
+ return (
+ <>
+ {/* Hidden PDF Content - Only visible when generating PDF */}
+
+ {/* PDF Header */}
+
+
+
CodeUnia members achieve great things
+
+
+ {/* Thank You Section */}
+
+
THANK YOU
+
FOR YOUR MEMBERSHIP
+
+
+ You are a member of the CodeUnia Community.
+
+
+
+ Below is a digital version of your membership card for easy access to your membership
+ information. You can also access this in your CodeUnia Profile or in the CodeUnia app anytime!
+
+
+
+ {/* Benefits Grid */}
+
+
+
Local CodeUnia Community
+
+ Get involved with colleagues at your local CodeUnia Community,
+ who can help connect you to professionals who can advance your goals.
+
+
+
Technical Publications
+
+ Take advantage of discounts and access to cutting-edge journals, magazines, and
+ digital publications.
+
+
+
Professional Network
+
+ Build a professional network from the wealth of university expertise and connections
+ found within CodeUnia.
+
+
+
+
+
Local CodeUnia Student Branch
+
+ Join and enjoy exciting technical competitions, expert speakers,
+ professional networking, and colleagues for life.
+
+
+
Career Opportunities
+
+ Drive your career goals forward with online learning,
+ job listings, a consultants network, and more!
+
+
+
Local Activities
+
+ Through your local CodeUnia community, events and conferences -
+ there are many ways to become involved.
+
+
+
+
+ {/* Membership Card in PDF */}
+
+
+
+ {/* Left Section - Member Info */}
+
+ {/* Student Member Badge */}
+
+
+ STUDENT MEMBER
+
+
+
+ {/* CodeUnia Title */}
+
+
+ code unia
+
+
+
+ {/* Member ID */}
+
+
Member ID:
+ {memberId}
+
+
+
+ {/* Status and Year */}
+
+
+ Active Member
+
+
+ 2025
+
+
+
+ {/* Validity Info */}
+
+
Valued Codeunia Member for 1 Year
+
Valid through 31 December 2025
+
+
+
+ {/* Right Section - Purple Background with Logo */}
+
+ {/* Logo Circle */}
+
+
+ {/* CodeUnia Text */}
+
+
CodeUnia
+
Empowering Coders
+
+
+ {/* Footer */}
+
+
Powered by Codeunia
+
support@codeunia.com
+
+
+
+
+
+
+ {/* Footer Info */}
+
+
+ Make the Most of Your Membership. Learn about these and all CodeUnia member benefits at{' '}
+ codeunia.com/benefits
+
+
+
+
+ {/* Visible Card for Display */}
+
+ {/* Main Card */}
+
+
+ {/* Left Section - Member Info */}
+
+ {/* Student Member Badge */}
+
+
+ STUDENT MEMBER
+
+
+
+ {/* CodeUnia Title */}
+
+
+ code unia
+
+
+
+ {/* Member ID */}
+
+
Member ID:
+ {memberId}
+
+
+
+ {/* Status and Year */}
+
+
+ Active Member
+
+
+ 2025
+
+
+
+ {/* Validity Info */}
+
+
Valued Codeunia Member for 1 Year
+
Valid through 31 December 2025
+
+
+
+ {/* Right Section - Purple Background with Logo */}
+
+ {/* Logo Circle */}
+
+
+ {/* CodeUnia Text */}
+
+
CodeUnia
+
Empowering Coders
+
+
+ {/* Footer */}
+
+
Powered by Codeunia
+
support@codeunia.com
+
+
+
+
+
+ {/* Download Button */}
+
+
+
+
+
+
+ Download PDF Card
+
+
+
+
+ >
+ );
+};
+
+export default MembershipCard;
diff --git a/components/profile/ProfileSettings.tsx b/components/profile/ProfileSettings.tsx
new file mode 100644
index 00000000..be894c5c
--- /dev/null
+++ b/components/profile/ProfileSettings.tsx
@@ -0,0 +1,391 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { useProfile } from '@/hooks/useProfile'
+import { Button } from '@/components/ui/button'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { Textarea } from '@/components/ui/textarea'
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
+import { Badge } from '@/components/ui/badge'
+import { Switch } from '@/components/ui/switch'
+import { Separator } from '@/components/ui/separator'
+import { Progress } from '@/components/ui/progress'
+import { Alert, AlertDescription } from '@/components/ui/alert'
+import {
+ User,
+ Github,
+ Linkedin,
+ Twitter,
+ Phone,
+ MapPin,
+ Briefcase,
+ Building,
+ Plus,
+ X,
+ AlertCircle,
+ CheckCircle,
+ Loader2
+} from 'lucide-react'
+import { ProfileUpdateData } from '@/types/profile'
+
+export function ProfileSettings() {
+ const { profile, loading, updating, error, updateProfile, clearError } = useProfile()
+
+ const [formData, setFormData] = useState({})
+ const [skills, setSkills] = useState([])
+ const [newSkill, setNewSkill] = useState('')
+ const [successMessage, setSuccessMessage] = useState('')
+
+ // Initialize form data when profile loads
+ useEffect(() => {
+ if (profile) {
+ setFormData({
+ first_name: profile.first_name || '',
+ last_name: profile.last_name || '',
+ display_name: profile.display_name || '',
+ bio: profile.bio || '',
+ phone: profile.phone || '',
+ github_url: profile.github_url || '',
+ linkedin_url: profile.linkedin_url || '',
+ twitter_url: profile.twitter_url || '',
+ current_position: profile.current_position || '',
+ company: profile.company || '',
+ location: profile.location || '',
+ is_public: profile.is_public,
+ email_notifications: profile.email_notifications
+ })
+ setSkills(profile.skills || [])
+ }
+ }, [profile])
+
+ const handleInputChange = (field: keyof ProfileUpdateData, value: string | boolean) => {
+ setFormData(prev => ({ ...prev, [field]: value }))
+ }
+
+ const handleAddSkill = () => {
+ if (newSkill.trim() && !skills.includes(newSkill.trim())) {
+ setSkills(prev => [...prev, newSkill.trim()])
+ setNewSkill('')
+ }
+ }
+
+ const handleRemoveSkill = (skillToRemove: string) => {
+ setSkills(prev => prev.filter(skill => skill !== skillToRemove))
+ }
+
+ const handleSaveProfile = async () => {
+ const updatedData: ProfileUpdateData = {
+ ...formData,
+ skills: skills
+ }
+
+ const success = await updateProfile(updatedData)
+ if (success) {
+ setSuccessMessage('Profile updated successfully!')
+ setTimeout(() => setSuccessMessage(''), 3000)
+ }
+ }
+
+
+ if (loading) {
+ return (
+
+
+
+ )
+ }
+
+ return (
+
+ {/* Profile Completion */}
+
+
+
+
+ Profile Completion
+
+
+ Complete your profile to unlock all features
+
+
+
+
+
+ Profile Completion
+ {profile?.profile_completion_percentage || 0}%
+
+
+
+
+
+
+ {/* Alerts */}
+ {error && (
+
+
+
+ {error}
+
+
+
+
+
+ )}
+
+ {successMessage && (
+
+
+ {successMessage}
+
+ )}
+
+
+ {/* Basic Information */}
+
+
+ Basic Information
+ Your personal information and contact details
+
+
+
+
+
+ Display Name
+ handleInputChange('display_name', e.target.value)}
+ placeholder="How others will see your name"
+ />
+
+
+
+ Bio
+
+
+
+
+
+
+ Phone
+
+
handleInputChange('phone', e.target.value)}
+ placeholder="+1 (555) 123-4567"
+ />
+
+
+
+
+ Location
+
+ handleInputChange('location', e.target.value)}
+ placeholder="City, Country"
+ />
+
+
+
+
+
+ {/* Professional Information */}
+
+
+ Professional Information
+ Your work and career details
+
+
+
+
+
+
Skills
+
+ {skills.map((skill, index) => (
+
+ {skill}
+ handleRemoveSkill(skill)}>
+
+
+
+ ))}
+
+
+
setNewSkill(e.target.value)}
+ placeholder="Add a skill"
+ onKeyPress={(e) => e.key === 'Enter' && handleAddSkill()}
+ />
+
+
+
+
+
+
+
+
+ {/* Social Links */}
+
+
+ Social Links
+ Connect your social media profiles
+
+
+
+
+
+
+
+ Twitter
+
+ handleInputChange('twitter_url', e.target.value)}
+ placeholder="https://twitter.com/username"
+ />
+
+
+
+
+ {/* Privacy Settings */}
+
+
+ Privacy Settings
+ Control who can see your profile and how you receive notifications
+
+
+
+
+
Public Profile
+
+ Allow others to view your profile information
+
+
+
handleInputChange('is_public', checked)}
+ />
+
+
+
+
+
+
+
Email Notifications
+
+ Receive email notifications about your account
+
+
+
handleInputChange('email_notifications', checked)}
+ />
+
+
+
+
+ {/* Save Button */}
+
+
+
+ {updating ? (
+ <>
+
+ Saving...
+ >
+ ) : (
+ 'Save Profile'
+ )}
+
+
+
+
+ )
+}
diff --git a/components/ui/progress.tsx b/components/ui/progress.tsx
new file mode 100644
index 00000000..e7a416c3
--- /dev/null
+++ b/components/ui/progress.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as ProgressPrimitive from "@radix-ui/react-progress"
+
+import { cn } from "@/lib/utils"
+
+function Progress({
+ className,
+ value,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { Progress }
diff --git a/components/ui/switch.tsx b/components/ui/switch.tsx
new file mode 100644
index 00000000..6a2b5241
--- /dev/null
+++ b/components/ui/switch.tsx
@@ -0,0 +1,31 @@
+"use client"
+
+import * as React from "react"
+import * as SwitchPrimitive from "@radix-ui/react-switch"
+
+import { cn } from "@/lib/utils"
+
+function Switch({
+ className,
+ ...props
+}: React.ComponentProps) {
+ return (
+
+
+
+ )
+}
+
+export { Switch }
diff --git a/hooks/useProfile.ts b/hooks/useProfile.ts
new file mode 100644
index 00000000..1ab32352
--- /dev/null
+++ b/hooks/useProfile.ts
@@ -0,0 +1,119 @@
+import { useState, useEffect } from 'react'
+import { useAuth } from '@/lib/hooks/useAuth'
+import { profileService } from '@/lib/services/profile'
+import { Profile, ProfileUpdateData } from '@/types/profile'
+
+export function useProfile() {
+ const { user } = useAuth()
+ const [profile, setProfile] = useState(null)
+ const [loading, setLoading] = useState(true)
+ const [updating, setUpdating] = useState(false)
+ const [error, setError] = useState(null)
+
+ // Fetch profile data
+ const fetchProfile = async () => {
+ if (!user?.id) return
+
+ try {
+ setLoading(true)
+ setError(null)
+ const profileData = await profileService.getProfile(user.id)
+ setProfile(profileData)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to fetch profile')
+ console.error('Error fetching profile:', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ // Update profile data
+ const updateProfile = async (updates: ProfileUpdateData): Promise => {
+ if (!user?.id) return false
+
+ try {
+ setUpdating(true)
+ setError(null)
+ const updatedProfile = await profileService.updateProfile(user.id, updates)
+ setProfile(updatedProfile)
+ return true
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to update profile')
+ console.error('Error updating profile:', err)
+ return false
+ } finally {
+ setUpdating(false)
+ }
+ }
+
+
+ // Refresh profile data
+ const refresh = () => {
+ fetchProfile()
+ }
+
+ // Clear error
+ const clearError = () => {
+ setError(null)
+ }
+
+ // Load profile on mount or user change
+ useEffect(() => {
+ if (user?.id) {
+ fetchProfile()
+ } else {
+ setProfile(null)
+ setLoading(false)
+ }
+ }, [user?.id])
+
+ return {
+ profile,
+ loading,
+ updating,
+ error,
+ updateProfile,
+ refresh,
+ clearError,
+ isComplete: profile ? profile.profile_completion_percentage >= 80 : false
+ }
+}
+
+// Hook for getting public profile (for viewing other users)
+export function usePublicProfile(userId: string | null) {
+ const [profile, setProfile] = useState(null)
+ const [loading, setLoading] = useState(false)
+ const [error, setError] = useState(null)
+
+ const fetchPublicProfile = async () => {
+ if (!userId) return
+
+ try {
+ setLoading(true)
+ setError(null)
+ const profileData = await profileService.getPublicProfile(userId)
+ setProfile(profileData)
+ } catch (err) {
+ setError(err instanceof Error ? err.message : 'Failed to fetch profile')
+ console.error('Error fetching public profile:', err)
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ useEffect(() => {
+ if (userId) {
+ fetchPublicProfile()
+ } else {
+ setProfile(null)
+ setLoading(false)
+ }
+ }, [userId])
+
+ return {
+ profile,
+ loading,
+ error,
+ refresh: fetchPublicProfile
+ }
+}
diff --git a/lib/services/profile.ts b/lib/services/profile.ts
new file mode 100644
index 00000000..0c741de9
--- /dev/null
+++ b/lib/services/profile.ts
@@ -0,0 +1,149 @@
+import { createClient } from '@/lib/supabase/client'
+import { Profile, ProfileUpdateData } from '@/types/profile'
+
+export class ProfileService {
+ private supabase = createClient()
+
+ // Get user profile by ID
+ async getProfile(userId: string): Promise {
+ const { data, error } = await this.supabase
+ .from('profiles')
+ .select('*')
+ .eq('id', userId)
+ .single()
+
+ if (error) {
+ if (error.code === 'PGRST116') {
+ // Profile doesn't exist, create one
+ return await this.createProfile(userId)
+ }
+ console.error('Error fetching profile:', error)
+ throw new Error(`Failed to fetch profile: ${error.message}`)
+ }
+
+ return data
+ }
+
+ // Create a new profile
+ async createProfile(userId: string): Promise {
+ const { data: user } = await this.supabase.auth.getUser()
+
+ const profileData = {
+ id: userId,
+ first_name: user.user?.user_metadata?.first_name || '',
+ last_name: user.user?.user_metadata?.last_name || '',
+ display_name: user.user?.user_metadata?.first_name
+ ? `${user.user.user_metadata.first_name} ${user.user.user_metadata.last_name || ''}`.trim()
+ : user.user?.email?.split('@')[0] || '',
+ is_public: true,
+ email_notifications: true,
+ profile_completion_percentage: 0
+ }
+
+ const { data, error } = await this.supabase
+ .from('profiles')
+ .insert([profileData])
+ .select()
+ .single()
+
+ if (error) {
+ console.error('Error creating profile:', error)
+ throw new Error(`Failed to create profile: ${error.message}`)
+ }
+
+ return data
+ }
+
+ // Update user profile
+ async updateProfile(userId: string, updates: ProfileUpdateData): Promise {
+ // Calculate profile completion percentage
+ const completionPercentage = this.calculateProfileCompletion(updates)
+
+ const updateData = {
+ ...updates,
+ profile_completion_percentage: completionPercentage,
+ updated_at: new Date().toISOString()
+ }
+
+ const { data, error } = await this.supabase
+ .from('profiles')
+ .update(updateData)
+ .eq('id', userId)
+ .select()
+ .single()
+
+ if (error) {
+ console.error('Error updating profile:', error)
+ throw new Error(`Failed to update profile: ${error.message}`)
+ }
+
+ return data
+ }
+
+ // Calculate profile completion percentage
+ private calculateProfileCompletion(profile: Partial): number {
+ const fields = [
+ 'first_name',
+ 'last_name',
+ 'bio',
+ 'phone',
+ 'github_url',
+ 'linkedin_url',
+ 'current_position',
+ 'company',
+ 'location',
+ 'skills'
+ ]
+
+ const filledFields = fields.filter(field => {
+ const value = profile[field as keyof Profile]
+ if (Array.isArray(value)) {
+ return value.length > 0
+ }
+ return value && value.toString().trim() !== ''
+ })
+
+ return Math.round((filledFields.length / fields.length) * 100)
+ }
+
+
+ // Get public profile (for viewing other users)
+ async getPublicProfile(userId: string): Promise {
+ const { data, error } = await this.supabase
+ .from('profiles')
+ .select('*')
+ .eq('id', userId)
+ .eq('is_public', true)
+ .single()
+
+ if (error) {
+ if (error.code === 'PGRST116') {
+ return null // Profile not found or not public
+ }
+ console.error('Error fetching public profile:', error)
+ throw new Error(`Failed to fetch public profile: ${error.message}`)
+ }
+
+ return data
+ }
+
+ // Search public profiles
+ async searchProfiles(query: string, limit: number = 10): Promise {
+ const { data, error } = await this.supabase
+ .from('profiles')
+ .select('*')
+ .eq('is_public', true)
+ .or(`display_name.ilike.%${query}%,bio.ilike.%${query}%,skills.cs.{${query}}`)
+ .limit(limit)
+
+ if (error) {
+ console.error('Error searching profiles:', error)
+ throw new Error(`Failed to search profiles: ${error.message}`)
+ }
+
+ return data || []
+ }
+}
+
+// Export singleton instance
+export const profileService = new ProfileService()
diff --git a/package-lock.json b/package-lock.json
index d73e3fb4..1356c058 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,9 +9,11 @@
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-label": "^2.1.6",
+ "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"@react-three/drei": "^10.2.0",
@@ -27,6 +29,9 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.17.3",
+ "html-to-image": "^1.11.13",
+ "html2canvas": "^1.4.1",
+ "jspdf": "^3.0.1",
"keen-slider": "^6.8.6",
"lucide-react": "^0.511.0",
"motion": "^12.18.1",
@@ -1430,6 +1435,30 @@
}
}
},
+ "node_modules/@radix-ui/react-progress": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-progress/-/react-progress-1.1.7.tgz",
+ "integrity": "sha512-vPdg/tF6YC/ynuBIJlk1mm7Le0VgW6ub6J2UWnTQ7/D23KXcPI1qy+0vBkgKgd38RCMJavBXpB83HPNFMTb0Fg==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-roving-focus": {
"version": "1.1.10",
"resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.10.tgz",
@@ -1541,6 +1570,35 @@
}
}
},
+ "node_modules/@radix-ui/react-switch": {
+ "version": "1.2.5",
+ "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.2.5.tgz",
+ "integrity": "sha512-5ijLkak6ZMylXsaImpZ8u4Rlf5grRmoc0p0QeX9VJtlrM4f5m3nCTX8tWga/zOA8PZYIR/t0p2Mnvd7InrJ6yQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@radix-ui/primitive": "1.1.2",
+ "@radix-ui/react-compose-refs": "1.1.2",
+ "@radix-ui/react-context": "1.1.2",
+ "@radix-ui/react-primitive": "2.1.3",
+ "@radix-ui/react-use-controllable-state": "1.2.2",
+ "@radix-ui/react-use-previous": "1.1.1",
+ "@radix-ui/react-use-size": "1.1.1"
+ },
+ "peerDependencies": {
+ "@types/react": "*",
+ "@types/react-dom": "*",
+ "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc",
+ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "@types/react-dom": {
+ "optional": true
+ }
+ }
+ },
"node_modules/@radix-ui/react-tabs": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.12.tgz",
@@ -2568,6 +2626,13 @@
"resolved": "https://registry.npmjs.org/@types/phoenix/-/phoenix-1.6.6.tgz",
"integrity": "sha512-PIzZZlEppgrpoT2QgbnDU+MMzuR6BbCjllj0bM70lWoejMeNJAxCchxnv7J3XFkI8MpygtRpzXrIlmWUBclP5A=="
},
+ "node_modules/@types/raf": {
+ "version": "3.4.3",
+ "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz",
+ "integrity": "sha512-c4YAvMedbPZ5tEyxzQdMoOhhJ4RD3rngZIdwC2/qDN3d7JpEhB6fiBRKVY1lg5B7Wk+uPBjn5f39j1/2MY1oOw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/react": {
"version": "19.1.6",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.6.tgz",
@@ -2622,6 +2687,13 @@
"meshoptimizer": "~0.18.1"
}
},
+ "node_modules/@types/trusted-types": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
+ "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/@types/unist": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz",
@@ -3449,6 +3521,18 @@
"node": ">= 0.4"
}
},
+ "node_modules/atob": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz",
+ "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==",
+ "license": "(MIT OR Apache-2.0)",
+ "bin": {
+ "atob": "bin/atob.js"
+ },
+ "engines": {
+ "node": ">= 4.5.0"
+ }
+ },
"node_modules/autoprefixer": {
"version": "10.4.21",
"resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz",
@@ -3534,6 +3618,15 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true
},
+ "node_modules/base64-arraybuffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
+ "integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -3627,6 +3720,18 @@
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
+ "node_modules/btoa": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz",
+ "integrity": "sha512-SB4/MIGlsiVkMcHmT+pSmIPoNDoHg+7cMzmt3Uxt628MTz2487DKSqK/fuhFBrkuqrYv5UCEnACpF4dTFNKc/g==",
+ "license": "(MIT OR Apache-2.0)",
+ "bin": {
+ "btoa": "bin/btoa.js"
+ },
+ "engines": {
+ "node": ">= 0.4.0"
+ }
+ },
"node_modules/buffer": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
@@ -3753,6 +3858,26 @@
}
]
},
+ "node_modules/canvg": {
+ "version": "3.0.11",
+ "resolved": "https://registry.npmjs.org/canvg/-/canvg-3.0.11.tgz",
+ "integrity": "sha512-5ON+q7jCTgMp9cjpu4Jo6XbvfYwSB2Ow3kzHKfIyJfaCAOHLbdKPQqGKgfED/R5B+3TFFfe8pegYA+b423SRyA==",
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "@babel/runtime": "^7.12.5",
+ "@types/raf": "^3.4.0",
+ "core-js": "^3.8.3",
+ "raf": "^3.4.1",
+ "regenerator-runtime": "^0.13.7",
+ "rgbcolor": "^1.0.1",
+ "stackblur-canvas": "^2.0.0",
+ "svg-pathdata": "^6.0.3"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/ccount": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz",
@@ -3947,6 +4072,18 @@
"node": ">=18"
}
},
+ "node_modules/core-js": {
+ "version": "3.44.0",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.44.0.tgz",
+ "integrity": "sha512-aFCtd4l6GvAXwVEh3XbbVqJGHDJt0OZRa+5ePGx3LLwi12WfexqQxcsohb2wgsa/92xtl19Hd66G/L+TaAxDMw==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/core-js"
+ }
+ },
"node_modules/cross-env": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
@@ -3977,6 +4114,15 @@
"node": ">= 8"
}
},
+ "node_modules/css-line-break": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/css-line-break/-/css-line-break-2.1.0.tgz",
+ "integrity": "sha512-FHcKFCZcAha3LwfVBhCQbW2nCNbkZXn7KVUJcsT5/P8YmfsVja0FMPJr0B903j/E69HUphKiV9iQArX8SDYA4w==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -4344,6 +4490,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/dompurify": {
+ "version": "3.2.6",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz",
+ "integrity": "sha512-/2GogDQlohXPZe6D6NOgQvXLPSYBqIWMnZ8zzOhn09REE4eyAzb+Hed3jhoM9OkuaJ8P6ZGTTVWQKAi8ieIzfQ==",
+ "license": "(MPL-2.0 OR Apache-2.0)",
+ "optional": true,
+ "optionalDependencies": {
+ "@types/trusted-types": "^2.0.7"
+ }
+ },
"node_modules/draco3d": {
"version": "1.5.7",
"resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
@@ -5681,6 +5837,12 @@
"resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.5.tgz",
"integrity": "sha512-KMn5n7JBK+olC342740hDPHnGWfE8FiHtGMOdJPfUjRdARTWj9OB+8c13fnsf9sk1VtpuU2fKSgUjHvg4rNbzQ=="
},
+ "node_modules/html-to-image": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/html-to-image/-/html-to-image-1.11.13.tgz",
+ "integrity": "sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==",
+ "license": "MIT"
+ },
"node_modules/html-url-attributes": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz",
@@ -5699,6 +5861,19 @@
"url": "https://github.com/sponsors/wooorm"
}
},
+ "node_modules/html2canvas": {
+ "version": "1.4.1",
+ "resolved": "https://registry.npmjs.org/html2canvas/-/html2canvas-1.4.1.tgz",
+ "integrity": "sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==",
+ "license": "MIT",
+ "dependencies": {
+ "css-line-break": "^2.1.0",
+ "text-segmentation": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
"node_modules/ieee754": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
@@ -6368,6 +6543,24 @@
"json5": "lib/cli.js"
}
},
+ "node_modules/jspdf": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/jspdf/-/jspdf-3.0.1.tgz",
+ "integrity": "sha512-qaGIxqxetdoNnFQQXxTKUD9/Z7AloLaw94fFsOiJMxbfYdBbrBuhWmbzI8TVjrw7s3jBY1PFHofBKMV/wZPapg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.7",
+ "atob": "^2.1.2",
+ "btoa": "^1.2.1",
+ "fflate": "^0.8.1"
+ },
+ "optionalDependencies": {
+ "canvg": "^3.0.11",
+ "core-js": "^3.6.0",
+ "dompurify": "^3.2.4",
+ "html2canvas": "^1.0.0-rc.5"
+ }
+ },
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@@ -8128,6 +8321,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "license": "MIT",
+ "optional": true
+ },
"node_modules/regexp.prototype.flags": {
"version": "1.5.4",
"resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz",
@@ -8249,6 +8449,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/rgbcolor": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz",
+ "integrity": "sha512-9aZLIrhRaD97sgVhtJOW6ckOEh6/GnvQtdVNfdZ6s67+3/XwLS9lBcQYzEEhYVeUowN7pRzMLsyGhK2i/xvWbw==",
+ "license": "MIT OR SEE LICENSE IN FEEL-FREE.md",
+ "optional": true,
+ "engines": {
+ "node": ">= 0.8.15"
+ }
+ },
"node_modules/robust-predicates": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.2.tgz",
@@ -8592,6 +8802,16 @@
"integrity": "sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==",
"dev": true
},
+ "node_modules/stackblur-canvas": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.7.0.tgz",
+ "integrity": "sha512-yf7OENo23AGJhBriGx0QivY5JP6Y1HbrrDI6WLt6C5auYZXlQrheoY8hD4ibekFKz1HOfE48Ww8kMWMnJD/zcQ==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=0.1.14"
+ }
+ },
"node_modules/stats-gl": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
@@ -8965,6 +9185,16 @@
"react": ">=17.0"
}
},
+ "node_modules/svg-pathdata": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/svg-pathdata/-/svg-pathdata-6.0.3.tgz",
+ "integrity": "sha512-qsjeeq5YjBZ5eMdFuUa4ZosMLxgr5RZ+F+Y1OrDhuOCEInRMA3x74XdBtggJcj9kOeInz0WE+LgCPDkZFlBYJw==",
+ "license": "MIT",
+ "optional": true,
+ "engines": {
+ "node": ">=12.0.0"
+ }
+ },
"node_modules/tailwind-merge": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-3.3.1.tgz",
@@ -9083,6 +9313,15 @@
}
}
},
+ "node_modules/text-segmentation": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/text-segmentation/-/text-segmentation-1.0.3.tgz",
+ "integrity": "sha512-iOiPUo/BGnZ6+54OsWxZidGCsdU8YbE4PSpdPinp7DeMtUJNJBoJ/ouUSTJjHkh1KntHaltHl/gDs2FC4i5+Nw==",
+ "license": "MIT",
+ "dependencies": {
+ "utrie": "^1.0.2"
+ }
+ },
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz",
@@ -9750,6 +9989,15 @@
"node": ">= 4"
}
},
+ "node_modules/utrie": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz",
+ "integrity": "sha512-1MLa5ouZiOmQzUbjbu9VmjLzn1QLXBhwpUa7kdLUQK+KQ5KA9I1vk5U4YHe/X2Ch7PYnJfWuWT+VbuxbGwljhw==",
+ "license": "MIT",
+ "dependencies": {
+ "base64-arraybuffer": "^1.0.2"
+ }
+ },
"node_modules/vfile": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
diff --git a/package.json b/package.json
index f9a7dfcd..3e49cfa5 100644
--- a/package.json
+++ b/package.json
@@ -11,9 +11,11 @@
"@radix-ui/react-dialog": "^1.1.14",
"@radix-ui/react-dropdown-menu": "^2.1.14",
"@radix-ui/react-label": "^2.1.6",
+ "@radix-ui/react-progress": "^1.1.7",
"@radix-ui/react-select": "^2.2.5",
"@radix-ui/react-separator": "^1.1.7",
"@radix-ui/react-slot": "^1.2.3",
+ "@radix-ui/react-switch": "^1.2.5",
"@radix-ui/react-tabs": "^1.1.12",
"@radix-ui/react-tooltip": "^1.2.7",
"@react-three/drei": "^10.2.0",
@@ -29,6 +31,9 @@
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"framer-motion": "^12.17.3",
+ "html-to-image": "^1.11.13",
+ "html2canvas": "^1.4.1",
+ "jspdf": "^3.0.1",
"keen-slider": "^6.8.6",
"lucide-react": "^0.511.0",
"motion": "^12.18.1",
diff --git a/types/profile.ts b/types/profile.ts
new file mode 100644
index 00000000..a3185fd7
--- /dev/null
+++ b/types/profile.ts
@@ -0,0 +1,49 @@
+export interface Profile {
+ id: string
+ created_at: string
+ updated_at: string
+
+ // Basic profile info
+ first_name?: string
+ last_name?: string
+ display_name?: string
+ bio?: string
+
+ // Contact information
+ phone?: string
+
+ // Social links
+ github_url?: string
+ linkedin_url?: string
+ twitter_url?: string
+
+ // Professional info
+ current_position?: string
+ company?: string
+ location?: string
+ skills?: string[]
+
+ // Settings
+ is_public: boolean
+ email_notifications: boolean
+
+ // Metadata
+ profile_completion_percentage: number
+}
+
+export interface ProfileUpdateData {
+ first_name?: string
+ last_name?: string
+ display_name?: string
+ bio?: string
+ phone?: string
+ github_url?: string
+ linkedin_url?: string
+ twitter_url?: string
+ current_position?: string
+ company?: string
+ location?: string
+ skills?: string[]
+ is_public?: boolean
+ email_notifications?: boolean
+}