Skip to content

Commit 149f4c8

Browse files
committed
fix: resolve all TypeScript build errors for Vercel deployment
1 parent 42164b2 commit 149f4c8

File tree

9 files changed

+238
-153
lines changed

9 files changed

+238
-153
lines changed

public/img/anh1.png

-1.54 MB
Binary file not shown.

public/img/banner-3.jpg

-2.43 MB
Binary file not shown.

public/img/ready.jpg

-5.72 MB
Binary file not shown.

src/components/ui/Button.tsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
11
import React from 'react';
22
import { motion } from 'framer-motion';
33

4-
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
4+
interface ButtonProps {
55
variant?: 'primary' | 'secondary' | 'outline' | 'ghost' | 'danger';
66
size?: 'sm' | 'md' | 'lg';
77
isLoading?: boolean;
88
leftIcon?: React.ReactNode;
99
rightIcon?: React.ReactNode;
1010
fullWidth?: boolean;
11+
children?: React.ReactNode;
12+
className?: string;
13+
disabled?: boolean;
14+
type?: 'button' | 'submit' | 'reset';
15+
onClick?: (e: React.MouseEvent<HTMLButtonElement>) => void;
1116
}
1217

1318
const variantStyles: Record<string, string> = {
@@ -24,11 +29,12 @@ const sizeStyles: Record<string, string> = {
2429
lg: 'px-8 py-3.5 text-lg rounded-xl',
2530
};
2631

27-
const Button: React.FC<ButtonProps> = ({ variant = 'primary', size = 'md', isLoading, leftIcon, rightIcon, fullWidth, children, className = '', disabled, ...props }) => {
32+
const Button: React.FC<ButtonProps> = ({ variant = 'primary', size = 'md', isLoading, leftIcon, rightIcon, fullWidth, children, className = '', disabled, type = 'button', onClick }) => {
2833
return (
2934
<motion.button whileHover={{ scale: 1.02 }} whileTap={{ scale: 0.98 }}
35+
type={type} onClick={onClick}
3036
className={`inline-flex items-center justify-center gap-2 font-semibold transition-all duration-300 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed ${variantStyles[variant]} ${sizeStyles[size]} ${fullWidth ? 'w-full' : ''} ${className}`}
31-
disabled={disabled || isLoading} {...props}>
37+
disabled={disabled || isLoading}>
3238
{isLoading ? (<svg className="animate-spin h-5 w-5" viewBox="0 0 24 24"><circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" fill="none" /><path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" /></svg>) : leftIcon}
3339
{children}
3440
{!isLoading && rightIcon}

src/pages/Portfolio.tsx

Lines changed: 78 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -6,178 +6,94 @@ import { FaGithub, FaLinkedin, FaGlobe, FaDownload, FaCertificate, FaTrophy, FaC
66
import { CVTemplate, sampleCVData } from '../components/CVTemplate';
77
import html2canvas from 'html2canvas';
88
import jsPDF from 'jspdf';
9+
import { getOrCreatePortfolio, updateUserPortfolio, PortfolioData } from '../services/portfolioService';
910

1011
const 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
);

src/services/blogService.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { db } from '../config/firebase';
2-
import { collection, doc, getDoc, getDocs, query, where, orderBy, limit, addDoc, updateDoc, increment } from 'firebase/firestore';
2+
import { collection, doc, getDocs, query, where, orderBy, limit, addDoc, updateDoc, increment } from 'firebase/firestore';
33
import type { BlogPost, BlogComment } from '../types/blog';
44

55
const BLOG_COLLECTION = 'blog_posts';

0 commit comments

Comments
 (0)