From f14f8388ea05fa528bab0438d98e468eb380bf1f Mon Sep 17 00:00:00 2001 From: Akshay Date: Sat, 26 Jul 2025 01:31:54 +0530 Subject: [PATCH] feat: add profile settings page and membership card functionality - Implemented ProfilePage component for user profile settings with authentication check. - Created MembershipCard component for generating and downloading membership cards as PDFs. - Added ProfileSettings component to manage user profile information, including skills and social links. - Introduced useProfile hook for fetching and updating user profile data. - Developed ProfileService for handling profile-related API interactions. - Added new UI components: Progress and Switch for better user experience. - Updated package.json with new dependencies for PDF generation and image handling. --- app/protected/page.tsx | 31 +- app/protected/profile/page.tsx | 37 +++ components/MembershipCard.tsx | 282 ++++++++++++++++++ components/profile/ProfileSettings.tsx | 391 +++++++++++++++++++++++++ components/ui/progress.tsx | 31 ++ components/ui/switch.tsx | 31 ++ hooks/useProfile.ts | 119 ++++++++ lib/services/profile.ts | 149 ++++++++++ package-lock.json | 248 ++++++++++++++++ package.json | 5 + types/profile.ts | 49 ++++ 11 files changed, 1372 insertions(+), 1 deletion(-) create mode 100644 app/protected/profile/page.tsx create mode 100644 components/MembershipCard.tsx create mode 100644 components/profile/ProfileSettings.tsx create mode 100644 components/ui/progress.tsx create mode 100644 components/ui/switch.tsx create mode 100644 hooks/useProfile.ts create mode 100644 lib/services/profile.ts create mode 100644 types/profile.ts 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 + + + + + + +
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

+
+

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 */} +
+ +
+
+ + ); +}; + +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 + + +
+
+ + handleInputChange('first_name', e.target.value)} + placeholder="Enter your first name" + /> +
+
+ + handleInputChange('last_name', e.target.value)} + placeholder="Enter your last name" + /> +
+
+ +
+ + handleInputChange('display_name', e.target.value)} + placeholder="How others will see your name" + /> +
+ +
+ +