@@ -6,178 +6,94 @@ import { FaGithub, FaLinkedin, FaGlobe, FaDownload, FaCertificate, FaTrophy, FaC
66import { CVTemplate , sampleCVData } from '../components/CVTemplate' ;
77import html2canvas from 'html2canvas' ;
88import jsPDF from 'jspdf' ;
9+ import { getOrCreatePortfolio , updateUserPortfolio , PortfolioData } from '../services/portfolioService' ;
910
1011const Portfolio = ( ) => {
1112 const navigate = useNavigate ( ) ;
1213 const { currentUser, userData } = useAuth ( ) ;
1314 const [ activeTab , setActiveTab ] = useState < 'overview' | 'projects' | 'certificates' | 'skills' | 'cv' > ( 'overview' ) ;
1415 const [ isEditing , setIsEditing ] = useState ( false ) ;
1516 const [ isSaving , setIsSaving ] = useState ( false ) ;
17+ const [ isLoading , setIsLoading ] = useState ( true ) ;
1618 const [ showSuccess , setShowSuccess ] = useState ( false ) ;
1719 const [ newSkill , setNewSkill ] = useState ( { name : '' , level : 50 , category : 'Frontend' } ) ;
1820
21+ // Initialize portfolio data from Firestore
22+ const [ portfolioData , setPortfolioData ] = useState < PortfolioData | null > ( null ) ;
23+
1924 useEffect ( ( ) => {
2025 if ( ! currentUser ) {
2126 navigate ( '/login' ) ;
27+ return ;
2228 }
23- } , [ currentUser , navigate ] ) ;
24-
25- // Mock data - In production, fetch from Firestore
26- const [ portfolioData , setPortfolioData ] = useState ( {
27- name : ( userData as any ) ?. name || 'Mai Tran Thien Tam' ,
28- title : 'Full Stack Developer | Data Science Enthusiast' ,
29- bio : 'Passionate learner on DHV Guiding Light platform. Completed multiple courses and built real-world projects in web development and data science.' ,
30- email : currentUser ?. email || '' ,
31- github : 'github.com/student' ,
32- linkedin : 'linkedin.com/in/student' ,
33- website : 'studentportfolio.com' ,
34- profileImage : '/img/team-1.jpg' ,
35-
36- stats : {
37- coursesCompleted : 5 ,
38- projectsBuilt : 12 ,
39- certificatesEarned : 5 ,
40- totalHours : 340 ,
41- skillsMastered : 24
42- } ,
4329
44- completedCourses : [
45- {
46- id : 1 ,
47- title : 'Web Development Full Stack' ,
48- completedDate : '2024-11-01' ,
49- grade : 95 ,
50- certificate : '/certificates/web-dev-cert.pdf'
51- } ,
52- {
53- id : 2 ,
54- title : 'React & Node.js Bootcamp' ,
55- completedDate : '2024-10-15' ,
56- grade : 92 ,
57- certificate : '/certificates/react-node-cert.pdf'
58- } ,
59- {
60- id : 5 ,
61- title : 'Data Science & Analytics' ,
62- completedDate : '2024-09-20' ,
63- grade : 88 ,
64- certificate : '/certificates/data-science-cert.pdf'
65- } ,
66- ] ,
67-
68- projects : [
69- {
70- id : 1 ,
71- title : 'E-commerce Platform' ,
72- description : 'Full-stack e-commerce application with React, Node.js, and MongoDB. Features include user authentication, product catalog, shopping cart, and payment integration.' ,
73- technologies : [ 'React' , 'Node.js' , 'MongoDB' , 'Stripe' ] ,
74- image : '/img/course-1.jpg' ,
75- githubLink : 'github.com/student/ecommerce' ,
76- liveLink : 'ecommerce-demo.com' ,
77- completedDate : '2024-10-25'
78- } ,
79- {
80- id : 2 ,
81- title : 'Social Media Dashboard' ,
82- description : 'Real-time social media analytics dashboard with data visualization. Built with React, D3.js, and Firebase.' ,
83- technologies : [ 'React' , 'D3.js' , 'Firebase' , 'Tailwind CSS' ] ,
84- image : '/img/course-2.jpg' ,
85- githubLink : 'github.com/student/social-dashboard' ,
86- liveLink : 'social-dashboard-demo.com' ,
87- completedDate : '2024-10-10'
88- } ,
89- {
90- id : 3 ,
91- title : 'Weather Forecast App' ,
92- description : 'Mobile weather application using React Native with geolocation and weather API integration.' ,
93- technologies : [ 'React Native' , 'APIs' , 'Geolocation' ] ,
94- image : '/img/course-3.jpg' ,
95- githubLink : 'github.com/student/weather-app' ,
96- liveLink : null ,
97- completedDate : '2024-09-28'
98- } ,
99- {
100- id : 4 ,
101- title : 'Customer Churn Prediction' ,
102- description : 'Machine learning model to predict customer churn using Python, scikit-learn, and data analysis techniques.' ,
103- technologies : [ 'Python' , 'Scikit-learn' , 'Pandas' , 'Matplotlib' ] ,
104- image : '/img/course-1.jpg' ,
105- githubLink : 'github.com/student/churn-prediction' ,
106- liveLink : null ,
107- completedDate : '2024-09-15'
30+ // Load portfolio data from Firestore
31+ const loadPortfolio = async ( ) => {
32+ try {
33+ setIsLoading ( true ) ;
34+ const data = await getOrCreatePortfolio (
35+ currentUser . uid ,
36+ currentUser . email || '' ,
37+ ( userData as any ) ?. name
38+ ) ;
39+ setPortfolioData ( data ) ;
40+ } catch ( error ) {
41+ console . error ( 'Error loading portfolio:' , error ) ;
42+ } finally {
43+ setIsLoading ( false ) ;
10844 }
109- ] ,
110-
111- skills : [
112- { name : 'JavaScript' , level : 95 , category : 'Frontend' } ,
113- { name : 'React.js' , level : 92 , category : 'Frontend' } ,
114- { name : 'Node.js' , level : 88 , category : 'Backend' } ,
115- { name : 'Python' , level : 85 , category : 'Data Science' } ,
116- { name : 'MongoDB' , level : 82 , category : 'Database' } ,
117- { name : 'HTML/CSS' , level : 98 , category : 'Frontend' } ,
118- { name : 'TypeScript' , level : 87 , category : 'Frontend' } ,
119- { name : 'Express.js' , level : 85 , category : 'Backend' } ,
120- { name : 'Pandas' , level : 80 , category : 'Data Science' } ,
121- { name : 'Git' , level : 90 , category : 'Tools' } ,
122- { name : 'Docker' , level : 75 , category : 'DevOps' } ,
123- { name : 'AWS' , level : 70 , category : 'Cloud' }
124- ] ,
45+ } ;
12546
126- certificates : [
127- {
128- id : 1 ,
129- title : 'Web Development Full Stack Certificate' ,
130- issueDate : '2024-11-01' ,
131- credentialId : 'DHV-WEB-2024-1234' ,
132- image : '/img/course-1.jpg'
133- } ,
134- {
135- id : 2 ,
136- title : 'React & Node.js Bootcamp Certificate' ,
137- issueDate : '2024-10-15' ,
138- credentialId : 'DHV-REACT-2024-5678' ,
139- image : '/img/course-2.jpg'
140- } ,
141- {
142- id : 3 ,
143- title : 'Data Science & Analytics Certificate' ,
144- issueDate : '2024-09-20' ,
145- credentialId : 'DHV-DATA-2024-9101' ,
146- image : '/img/course-3.jpg'
147- }
148- ]
149- } ) ;
47+ loadPortfolio ( ) ;
48+ } , [ currentUser , navigate , userData ] ) ;
15049
15150 const handleSave = async ( ) => {
51+ if ( ! currentUser || ! portfolioData ) return ;
52+
15253 setIsSaving ( true ) ;
153- // Simulate API call
154- await new Promise ( resolve => setTimeout ( resolve , 1500 ) ) ;
155- setIsSaving ( false ) ;
156- setIsEditing ( false ) ;
157- setShowSuccess ( true ) ;
158- setTimeout ( ( ) => setShowSuccess ( false ) , 3000 ) ;
54+ try {
55+ await updateUserPortfolio ( currentUser . uid , portfolioData ) ;
56+ setIsEditing ( false ) ;
57+ setShowSuccess ( true ) ;
58+ setTimeout ( ( ) => setShowSuccess ( false ) , 3000 ) ;
59+ } catch ( error ) {
60+ console . error ( 'Error saving portfolio:' , error ) ;
61+ alert ( 'Failed to save portfolio. Please try again.' ) ;
62+ } finally {
63+ setIsSaving ( false ) ;
64+ }
15965 } ;
16066
16167 const handleAddSkill = ( ) => {
162- if ( newSkill . name . trim ( ) ) {
163- setPortfolioData ( prev => ( {
68+ if ( ! portfolioData || ! newSkill . name . trim ( ) ) return ;
69+
70+ setPortfolioData ( prev => {
71+ if ( ! prev ) return prev ;
72+ return {
16473 ...prev ,
16574 skills : [ ...prev . skills , { ...newSkill } ] ,
16675 stats : { ...prev . stats , skillsMastered : prev . stats . skillsMastered + 1 }
167- } ) ) ;
168- setNewSkill ( { name : '' , level : 50 , category : 'Frontend' } ) ;
169- }
76+ } ;
77+ } ) ;
78+ setNewSkill ( { name : '' , level : 50 , category : 'Frontend' } ) ;
17079 } ;
17180
17281 const handleRemoveSkill = ( index : number ) => {
173- setPortfolioData ( prev => ( {
174- ...prev ,
175- skills : prev . skills . filter ( ( _ , i ) => i !== index ) ,
176- stats : { ...prev . stats , skillsMastered : Math . max ( 0 , prev . stats . skillsMastered - 1 ) }
177- } ) ) ;
82+ if ( ! portfolioData ) return ;
83+
84+ setPortfolioData ( prev => {
85+ if ( ! prev ) return prev ;
86+ return {
87+ ...prev ,
88+ skills : prev . skills . filter ( ( _ , i ) => i !== index ) ,
89+ stats : { ...prev . stats , skillsMastered : Math . max ( 0 , prev . stats . skillsMastered - 1 ) }
90+ } ;
91+ } ) ;
17892 } ;
17993
18094 const handleDownloadCV = async ( ) => {
95+ if ( ! portfolioData ) return ;
96+
18197 const cvElement = document . getElementById ( 'cv-preview' ) ;
18298 if ( ! cvElement ) return ;
18399
@@ -211,6 +127,20 @@ const Portfolio = () => {
211127 < div className = "min-h-screen bg-gradient-to-br from-blue-50 via-white to-purple-50 py-8" >
212128 < div className = "container mx-auto px-4" >
213129
130+ { /* Loading State */ }
131+ { isLoading && (
132+ < div className = "flex items-center justify-center min-h-screen" >
133+ < div className = "text-center" >
134+ < div className = "w-16 h-16 border-4 border-[#001A66] border-t-transparent rounded-full animate-spin mx-auto mb-4" > </ div >
135+ < p className = "text-xl text-gray-600" > Loading your portfolio...</ p >
136+ </ div >
137+ </ div >
138+ ) }
139+
140+ { /* Portfolio Content */ }
141+ { ! isLoading && portfolioData && (
142+ < >
143+
214144 { /* Success Notification */ }
215145 < AnimatePresence >
216146 { showSuccess && (
@@ -280,20 +210,20 @@ const Portfolio = () => {
280210 < input
281211 type = "text"
282212 value = { portfolioData . name }
283- onChange = { ( e ) => setPortfolioData ( prev => ( { ...prev , name : e . target . value } ) ) }
213+ onChange = { ( e ) => setPortfolioData ( prev => prev ? { ...prev , name : e . target . value } : prev ) }
284214 className = "w-full px-4 py-3 text-4xl font-bold bg-white/20 border-2 border-white/50 rounded-xl focus:border-white focus:outline-none text-white placeholder-white/70"
285215 placeholder = "Your Name"
286216 />
287217 < input
288218 type = "text"
289219 value = { portfolioData . title }
290- onChange = { ( e ) => setPortfolioData ( prev => ( { ...prev , title : e . target . value } ) ) }
220+ onChange = { ( e ) => setPortfolioData ( prev => prev ? { ...prev , title : e . target . value } : prev ) }
291221 className = "w-full px-4 py-3 text-xl bg-white/20 border-2 border-white/50 rounded-xl focus:border-white focus:outline-none text-white placeholder-white/70"
292222 placeholder = "Your Title"
293223 />
294224 < textarea
295225 value = { portfolioData . bio }
296- onChange = { ( e ) => setPortfolioData ( prev => ( { ...prev , bio : e . target . value } ) ) }
226+ onChange = { ( e ) => setPortfolioData ( prev => prev ? { ...prev , bio : e . target . value } : prev ) }
297227 className = "w-full px-4 py-3 bg-white/20 border-2 border-white/50 rounded-xl focus:border-white focus:outline-none text-white placeholder-white/70 resize-none"
298228 placeholder = "Your Bio"
299229 rows = { 3 }
@@ -302,21 +232,21 @@ const Portfolio = () => {
302232 < input
303233 type = "text"
304234 value = { portfolioData . github }
305- onChange = { ( e ) => setPortfolioData ( prev => ( { ...prev , github : e . target . value } ) ) }
235+ onChange = { ( e ) => setPortfolioData ( prev => prev ? { ...prev , github : e . target . value } : prev ) }
306236 className = "px-4 py-2 bg-white/20 border-2 border-white/50 rounded-xl focus:border-white focus:outline-none text-white placeholder-white/70"
307237 placeholder = "GitHub URL"
308238 />
309239 < input
310240 type = "text"
311241 value = { portfolioData . linkedin }
312- onChange = { ( e ) => setPortfolioData ( prev => ( { ...prev , linkedin : e . target . value } ) ) }
242+ onChange = { ( e ) => setPortfolioData ( prev => prev ? { ...prev , linkedin : e . target . value } : prev ) }
313243 className = "px-4 py-2 bg-white/20 border-2 border-white/50 rounded-xl focus:border-white focus:outline-none text-white placeholder-white/70"
314244 placeholder = "LinkedIn URL"
315245 />
316246 < input
317247 type = "text"
318248 value = { portfolioData . website }
319- onChange = { ( e ) => setPortfolioData ( prev => ( { ...prev , website : e . target . value } ) ) }
249+ onChange = { ( e ) => setPortfolioData ( prev => prev ? { ...prev , website : e . target . value } : prev ) }
320250 className = "px-4 py-2 bg-white/20 border-2 border-white/50 rounded-xl focus:border-white focus:outline-none text-white placeholder-white/70"
321251 placeholder = "Website URL"
322252 />
@@ -640,6 +570,8 @@ const Portfolio = () => {
640570 </ div >
641571 ) }
642572 </ motion . div >
573+ </ >
574+ ) }
643575 </ div >
644576 </ div >
645577 ) ;
0 commit comments