create-ng-tailwind 3.0.1 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -344
- package/README.md +93 -157
- package/lib/cli/index.js +29 -3
- package/lib/cli/interactive.js +26 -1
- package/lib/managers/ProjectManager.js +0 -4
- package/lib/templates/base/components.js +243 -0
- package/lib/templates/base/index.js +207 -0
- package/lib/templates/base/infrastructure.js +314 -0
- package/lib/templates/base/linting.js +359 -0
- package/lib/templates/base/pwa.js +103 -0
- package/lib/templates/base/services.js +362 -0
- package/lib/templates/blog/app.js +250 -0
- package/lib/templates/blog/components.js +360 -0
- package/lib/templates/blog/i18n.js +77 -0
- package/lib/templates/blog/index.js +126 -0
- package/lib/templates/blog/pages.js +554 -0
- package/lib/templates/blog/services.js +390 -0
- package/lib/templates/dashboard/app.js +320 -0
- package/lib/templates/dashboard/charts.js +305 -0
- package/lib/templates/dashboard/components.js +410 -0
- package/lib/templates/dashboard/i18n.js +340 -0
- package/lib/templates/dashboard/index.js +141 -0
- package/lib/templates/dashboard/layout.js +310 -0
- package/lib/templates/dashboard/pages.js +681 -0
- package/lib/templates/ecommerce/app.js +315 -0
- package/lib/templates/ecommerce/components.js +496 -0
- package/lib/templates/ecommerce/i18n.js +389 -0
- package/lib/templates/ecommerce/index.js +152 -0
- package/lib/templates/ecommerce/layout.js +270 -0
- package/lib/templates/ecommerce/pages.js +969 -0
- package/lib/templates/ecommerce/services.js +300 -0
- package/lib/templates/index.js +12 -0
- package/lib/templates/landing/index.js +1117 -0
- package/lib/templates/portfolio/index.js +1160 -0
- package/lib/templates/saas/index.js +1371 -0
- package/lib/templates/starter/app.js +364 -0
- package/lib/templates/starter/i18n.js +856 -0
- package/lib/templates/starter/index.js +53 -4055
- package/lib/templates/starter/layout.js +852 -0
- package/lib/templates/starter/pages.js +1241 -0
- package/package.json +1 -1
- package/lib/templates/starter/features.js +0 -867
- package/lib/utils/ai-config.js +0 -641
- /package/lib/templates/{starter → base}/advanced-features.js +0 -0
- /package/lib/templates/{starter → base}/seo-assets.js +0 -0
- /package/lib/templates/{starter → base}/seo-features.js +0 -0
- /package/lib/templates/{starter → base}/ui-features.js +0 -0
|
@@ -0,0 +1,1160 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const starter = require('../starter');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Portfolio Template
|
|
7
|
+
* Extends starter template with:
|
|
8
|
+
* - Projects showcase with filtering
|
|
9
|
+
* - Skills section with proficiency levels
|
|
10
|
+
* - Experience timeline
|
|
11
|
+
* - Resume/CV download
|
|
12
|
+
* - Testimonials
|
|
13
|
+
* - Contact form
|
|
14
|
+
*/
|
|
15
|
+
const portfolio = {
|
|
16
|
+
info: {
|
|
17
|
+
name: 'Portfolio',
|
|
18
|
+
description: 'Personal portfolio template with projects showcase, skills, and resume',
|
|
19
|
+
features: [
|
|
20
|
+
...starter.info.features,
|
|
21
|
+
'Projects showcase with filtering',
|
|
22
|
+
'Skills section with proficiency bars',
|
|
23
|
+
'Experience timeline',
|
|
24
|
+
'Education section',
|
|
25
|
+
'Testimonials carousel',
|
|
26
|
+
'Downloadable resume',
|
|
27
|
+
'Social links',
|
|
28
|
+
'Animated hero section',
|
|
29
|
+
],
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
async apply(config, spinner) {
|
|
33
|
+
const chalk = require('chalk');
|
|
34
|
+
|
|
35
|
+
const completeStep = (message) => {
|
|
36
|
+
if (spinner) {
|
|
37
|
+
spinner.stop();
|
|
38
|
+
console.log(chalk.green(' ✔') + chalk.white(' ' + message));
|
|
39
|
+
spinner.start();
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Step 1: Apply starter template
|
|
44
|
+
if (spinner) spinner.update('Setting up starter foundation...');
|
|
45
|
+
await starter.apply(config, null);
|
|
46
|
+
|
|
47
|
+
// Step 2: Create portfolio-specific directories
|
|
48
|
+
if (spinner) spinner.update('Setting up portfolio structure...');
|
|
49
|
+
await this.createDirectories(config);
|
|
50
|
+
|
|
51
|
+
// Step 3: Create services
|
|
52
|
+
await this.createServices(config);
|
|
53
|
+
|
|
54
|
+
// Step 4: Create components
|
|
55
|
+
await this.createComponents(config);
|
|
56
|
+
|
|
57
|
+
// Step 5: Create pages
|
|
58
|
+
await this.createPages(config);
|
|
59
|
+
|
|
60
|
+
// Step 6: Update routing
|
|
61
|
+
await this.createRouting(config);
|
|
62
|
+
|
|
63
|
+
// Step 7: Add i18n translations
|
|
64
|
+
await this.updateI18n(config);
|
|
65
|
+
|
|
66
|
+
// Step 8: Format code
|
|
67
|
+
const base = require('../base');
|
|
68
|
+
await base.formatCode(config);
|
|
69
|
+
|
|
70
|
+
if (spinner) spinner.stop();
|
|
71
|
+
|
|
72
|
+
console.log('');
|
|
73
|
+
completeStep('Portfolio template created');
|
|
74
|
+
completeStep('Projects showcase');
|
|
75
|
+
completeStep('Skills & experience');
|
|
76
|
+
completeStep('Testimonials');
|
|
77
|
+
completeStep('Contact form');
|
|
78
|
+
console.log('');
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
async createDirectories(config) {
|
|
82
|
+
const directories = [
|
|
83
|
+
'src/app/features/portfolio',
|
|
84
|
+
'src/app/features/portfolio/projects',
|
|
85
|
+
'src/app/features/portfolio/project-detail',
|
|
86
|
+
'src/app/features/resume',
|
|
87
|
+
'src/app/shared/components/project-card',
|
|
88
|
+
'src/app/shared/components/skill-bar',
|
|
89
|
+
'src/app/shared/components/timeline',
|
|
90
|
+
'src/app/shared/components/testimonial-carousel',
|
|
91
|
+
'src/app/shared/components/social-links',
|
|
92
|
+
];
|
|
93
|
+
|
|
94
|
+
for (const dir of directories) {
|
|
95
|
+
await fs.ensureDir(path.join(config.fullPath, dir));
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
|
|
99
|
+
async createServices(config) {
|
|
100
|
+
const portfolioService = `import { Injectable, signal } from '@angular/core';
|
|
101
|
+
|
|
102
|
+
export interface Project {
|
|
103
|
+
id: number;
|
|
104
|
+
slug: string;
|
|
105
|
+
title: string;
|
|
106
|
+
description: string;
|
|
107
|
+
longDescription: string;
|
|
108
|
+
image: string;
|
|
109
|
+
images: string[];
|
|
110
|
+
category: string;
|
|
111
|
+
tags: string[];
|
|
112
|
+
liveUrl?: string;
|
|
113
|
+
githubUrl?: string;
|
|
114
|
+
featured?: boolean;
|
|
115
|
+
completedAt: Date;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface Skill {
|
|
119
|
+
name: string;
|
|
120
|
+
level: number; // 0-100
|
|
121
|
+
category: string;
|
|
122
|
+
icon?: string;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface Experience {
|
|
126
|
+
id: number;
|
|
127
|
+
title: string;
|
|
128
|
+
company: string;
|
|
129
|
+
location: string;
|
|
130
|
+
startDate: Date;
|
|
131
|
+
endDate?: Date;
|
|
132
|
+
current?: boolean;
|
|
133
|
+
description: string;
|
|
134
|
+
achievements: string[];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface Education {
|
|
138
|
+
id: number;
|
|
139
|
+
degree: string;
|
|
140
|
+
school: string;
|
|
141
|
+
location: string;
|
|
142
|
+
startDate: Date;
|
|
143
|
+
endDate: Date;
|
|
144
|
+
description?: string;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export interface Testimonial {
|
|
148
|
+
id: number;
|
|
149
|
+
content: string;
|
|
150
|
+
author: string;
|
|
151
|
+
role: string;
|
|
152
|
+
company: string;
|
|
153
|
+
avatar: string;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@Injectable({ providedIn: 'root' })
|
|
157
|
+
export class PortfolioService {
|
|
158
|
+
private projectsSignal = signal<Project[]>(this.getMockProjects());
|
|
159
|
+
private skillsSignal = signal<Skill[]>(this.getMockSkills());
|
|
160
|
+
private experienceSignal = signal<Experience[]>(this.getMockExperience());
|
|
161
|
+
private educationSignal = signal<Education[]>(this.getMockEducation());
|
|
162
|
+
private testimonialsSignal = signal<Testimonial[]>(this.getMockTestimonials());
|
|
163
|
+
|
|
164
|
+
projects = this.projectsSignal.asReadonly();
|
|
165
|
+
skills = this.skillsSignal.asReadonly();
|
|
166
|
+
experience = this.experienceSignal.asReadonly();
|
|
167
|
+
education = this.educationSignal.asReadonly();
|
|
168
|
+
testimonials = this.testimonialsSignal.asReadonly();
|
|
169
|
+
|
|
170
|
+
getProjectBySlug(slug: string): Project | undefined {
|
|
171
|
+
return this.projectsSignal().find(p => p.slug === slug);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
getProjectsByCategory(category: string): Project[] {
|
|
175
|
+
return this.projectsSignal().filter(p => p.category === category);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
getFeaturedProjects(): Project[] {
|
|
179
|
+
return this.projectsSignal().filter(p => p.featured);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
getCategories(): string[] {
|
|
183
|
+
return [...new Set(this.projectsSignal().map(p => p.category))];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
getSkillsByCategory(): Map<string, Skill[]> {
|
|
187
|
+
const map = new Map<string, Skill[]>();
|
|
188
|
+
this.skillsSignal().forEach(skill => {
|
|
189
|
+
const existing = map.get(skill.category) || [];
|
|
190
|
+
map.set(skill.category, [...existing, skill]);
|
|
191
|
+
});
|
|
192
|
+
return map;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
private getMockProjects(): Project[] {
|
|
196
|
+
return [
|
|
197
|
+
{
|
|
198
|
+
id: 1,
|
|
199
|
+
slug: 'ecommerce-platform',
|
|
200
|
+
title: 'E-Commerce Platform',
|
|
201
|
+
description: 'A full-featured online shopping platform with cart, checkout, and payment integration.',
|
|
202
|
+
longDescription: 'Built a comprehensive e-commerce solution featuring product catalog, shopping cart, secure checkout, payment processing with Stripe, order management, and admin dashboard. The platform handles thousands of daily transactions.',
|
|
203
|
+
image: 'https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=800',
|
|
204
|
+
images: [
|
|
205
|
+
'https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?w=800',
|
|
206
|
+
'https://images.unsplash.com/photo-1563013544-824ae1b704d3?w=800',
|
|
207
|
+
],
|
|
208
|
+
category: 'Web App',
|
|
209
|
+
tags: ['Angular', 'Node.js', 'MongoDB', 'Stripe'],
|
|
210
|
+
liveUrl: 'https://example.com',
|
|
211
|
+
githubUrl: 'https://github.com',
|
|
212
|
+
featured: true,
|
|
213
|
+
completedAt: new Date('2024-12-01'),
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
id: 2,
|
|
217
|
+
slug: 'task-management-app',
|
|
218
|
+
title: 'Task Management App',
|
|
219
|
+
description: 'Collaborative project management tool with real-time updates and team features.',
|
|
220
|
+
longDescription: 'Developed a Trello-like task management application with drag-and-drop functionality, real-time collaboration using WebSockets, team workspaces, and automated workflows.',
|
|
221
|
+
image: 'https://images.unsplash.com/photo-1611224923853-80b023f02d71?w=800',
|
|
222
|
+
images: ['https://images.unsplash.com/photo-1611224923853-80b023f02d71?w=800'],
|
|
223
|
+
category: 'Web App',
|
|
224
|
+
tags: ['Angular', 'Firebase', 'RxJS'],
|
|
225
|
+
liveUrl: 'https://example.com',
|
|
226
|
+
featured: true,
|
|
227
|
+
completedAt: new Date('2024-10-15'),
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
id: 3,
|
|
231
|
+
slug: 'fitness-tracker',
|
|
232
|
+
title: 'Fitness Tracker Mobile App',
|
|
233
|
+
description: 'Cross-platform mobile app for tracking workouts, nutrition, and health goals.',
|
|
234
|
+
longDescription: 'Created a comprehensive fitness tracking app with workout logging, nutrition tracking, progress charts, goal setting, and integration with wearable devices.',
|
|
235
|
+
image: 'https://images.unsplash.com/photo-1476480862126-209bfaa8edc8?w=800',
|
|
236
|
+
images: ['https://images.unsplash.com/photo-1476480862126-209bfaa8edc8?w=800'],
|
|
237
|
+
category: 'Mobile App',
|
|
238
|
+
tags: ['Ionic', 'Angular', 'Capacitor'],
|
|
239
|
+
liveUrl: 'https://example.com',
|
|
240
|
+
featured: true,
|
|
241
|
+
completedAt: new Date('2024-08-20'),
|
|
242
|
+
},
|
|
243
|
+
{
|
|
244
|
+
id: 4,
|
|
245
|
+
slug: 'analytics-dashboard',
|
|
246
|
+
title: 'Analytics Dashboard',
|
|
247
|
+
description: 'Real-time data visualization dashboard for business intelligence.',
|
|
248
|
+
longDescription: 'Built an interactive analytics dashboard with customizable widgets, real-time data streaming, export functionality, and role-based access control.',
|
|
249
|
+
image: 'https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800',
|
|
250
|
+
images: ['https://images.unsplash.com/photo-1551288049-bebda4e38f71?w=800'],
|
|
251
|
+
category: 'Dashboard',
|
|
252
|
+
tags: ['Angular', 'D3.js', 'WebSocket'],
|
|
253
|
+
completedAt: new Date('2024-06-10'),
|
|
254
|
+
},
|
|
255
|
+
{
|
|
256
|
+
id: 5,
|
|
257
|
+
slug: 'portfolio-website',
|
|
258
|
+
title: 'Creative Agency Website',
|
|
259
|
+
description: 'Modern portfolio website for a creative design agency.',
|
|
260
|
+
longDescription: 'Designed and developed a stunning portfolio website featuring smooth animations, case studies, team profiles, and an integrated blog.',
|
|
261
|
+
image: 'https://images.unsplash.com/photo-1467232004584-a241de8bcf5d?w=800',
|
|
262
|
+
images: ['https://images.unsplash.com/photo-1467232004584-a241de8bcf5d?w=800'],
|
|
263
|
+
category: 'Website',
|
|
264
|
+
tags: ['Angular', 'GSAP', 'Tailwind'],
|
|
265
|
+
liveUrl: 'https://example.com',
|
|
266
|
+
completedAt: new Date('2024-04-05'),
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
id: 6,
|
|
270
|
+
slug: 'chat-application',
|
|
271
|
+
title: 'Real-Time Chat App',
|
|
272
|
+
description: 'Instant messaging application with group chats and file sharing.',
|
|
273
|
+
longDescription: 'Developed a real-time chat application with direct messaging, group chats, file sharing, read receipts, typing indicators, and push notifications.',
|
|
274
|
+
image: 'https://images.unsplash.com/photo-1611746872915-64382b5c76da?w=800',
|
|
275
|
+
images: ['https://images.unsplash.com/photo-1611746872915-64382b5c76da?w=800'],
|
|
276
|
+
category: 'Web App',
|
|
277
|
+
tags: ['Angular', 'Socket.io', 'Node.js'],
|
|
278
|
+
githubUrl: 'https://github.com',
|
|
279
|
+
completedAt: new Date('2024-02-28'),
|
|
280
|
+
},
|
|
281
|
+
];
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private getMockSkills(): Skill[] {
|
|
285
|
+
return [
|
|
286
|
+
// Frontend
|
|
287
|
+
{ name: 'Angular', level: 95, category: 'Frontend' },
|
|
288
|
+
{ name: 'TypeScript', level: 90, category: 'Frontend' },
|
|
289
|
+
{ name: 'React', level: 75, category: 'Frontend' },
|
|
290
|
+
{ name: 'Tailwind CSS', level: 90, category: 'Frontend' },
|
|
291
|
+
{ name: 'HTML/CSS', level: 95, category: 'Frontend' },
|
|
292
|
+
// Backend
|
|
293
|
+
{ name: 'Node.js', level: 85, category: 'Backend' },
|
|
294
|
+
{ name: 'Python', level: 70, category: 'Backend' },
|
|
295
|
+
{ name: 'PostgreSQL', level: 80, category: 'Backend' },
|
|
296
|
+
{ name: 'MongoDB', level: 75, category: 'Backend' },
|
|
297
|
+
// Tools
|
|
298
|
+
{ name: 'Git', level: 90, category: 'Tools' },
|
|
299
|
+
{ name: 'Docker', level: 70, category: 'Tools' },
|
|
300
|
+
{ name: 'AWS', level: 65, category: 'Tools' },
|
|
301
|
+
{ name: 'Figma', level: 75, category: 'Tools' },
|
|
302
|
+
];
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
private getMockExperience(): Experience[] {
|
|
306
|
+
return [
|
|
307
|
+
{
|
|
308
|
+
id: 1,
|
|
309
|
+
title: 'Senior Frontend Developer',
|
|
310
|
+
company: 'Tech Innovators Inc',
|
|
311
|
+
location: 'San Francisco, CA',
|
|
312
|
+
startDate: new Date('2022-03-01'),
|
|
313
|
+
current: true,
|
|
314
|
+
description: 'Leading frontend development for enterprise applications, mentoring junior developers, and establishing best practices.',
|
|
315
|
+
achievements: [
|
|
316
|
+
'Led migration from AngularJS to Angular 17, improving performance by 40%',
|
|
317
|
+
'Implemented micro-frontend architecture serving 500K+ users',
|
|
318
|
+
'Mentored team of 5 junior developers',
|
|
319
|
+
'Reduced bundle size by 60% through code splitting and lazy loading',
|
|
320
|
+
],
|
|
321
|
+
},
|
|
322
|
+
{
|
|
323
|
+
id: 2,
|
|
324
|
+
title: 'Frontend Developer',
|
|
325
|
+
company: 'Digital Solutions Co',
|
|
326
|
+
location: 'Austin, TX',
|
|
327
|
+
startDate: new Date('2019-06-01'),
|
|
328
|
+
endDate: new Date('2022-02-28'),
|
|
329
|
+
description: 'Developed responsive web applications and collaborated with design teams to implement pixel-perfect UIs.',
|
|
330
|
+
achievements: [
|
|
331
|
+
'Built 15+ client projects using Angular and React',
|
|
332
|
+
'Introduced automated testing, achieving 85% code coverage',
|
|
333
|
+
'Optimized Core Web Vitals, improving SEO rankings by 25%',
|
|
334
|
+
],
|
|
335
|
+
},
|
|
336
|
+
{
|
|
337
|
+
id: 3,
|
|
338
|
+
title: 'Junior Web Developer',
|
|
339
|
+
company: 'StartUp Labs',
|
|
340
|
+
location: 'Remote',
|
|
341
|
+
startDate: new Date('2017-08-01'),
|
|
342
|
+
endDate: new Date('2019-05-31'),
|
|
343
|
+
description: 'Started career building websites and learning modern web technologies.',
|
|
344
|
+
achievements: [
|
|
345
|
+
'Developed company website from scratch',
|
|
346
|
+
'Learned Angular and built first production app',
|
|
347
|
+
'Contributed to open-source projects',
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
];
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
private getMockEducation(): Education[] {
|
|
354
|
+
return [
|
|
355
|
+
{
|
|
356
|
+
id: 1,
|
|
357
|
+
degree: 'Master of Science in Computer Science',
|
|
358
|
+
school: 'Stanford University',
|
|
359
|
+
location: 'Stanford, CA',
|
|
360
|
+
startDate: new Date('2015-09-01'),
|
|
361
|
+
endDate: new Date('2017-06-01'),
|
|
362
|
+
description: 'Specialized in Human-Computer Interaction and Web Technologies',
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: 2,
|
|
366
|
+
degree: 'Bachelor of Science in Software Engineering',
|
|
367
|
+
school: 'MIT',
|
|
368
|
+
location: 'Cambridge, MA',
|
|
369
|
+
startDate: new Date('2011-09-01'),
|
|
370
|
+
endDate: new Date('2015-06-01'),
|
|
371
|
+
description: 'Graduated with honors, Dean\\'s List all semesters',
|
|
372
|
+
},
|
|
373
|
+
];
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
private getMockTestimonials(): Testimonial[] {
|
|
377
|
+
return [
|
|
378
|
+
{
|
|
379
|
+
id: 1,
|
|
380
|
+
content: 'An exceptional developer who consistently delivers high-quality work. Their attention to detail and problem-solving skills are outstanding.',
|
|
381
|
+
author: 'Sarah Mitchell',
|
|
382
|
+
role: 'Product Manager',
|
|
383
|
+
company: 'Tech Innovators Inc',
|
|
384
|
+
avatar: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?w=150',
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
id: 2,
|
|
388
|
+
content: 'Working with them was a pleasure. They understood our requirements perfectly and delivered beyond expectations.',
|
|
389
|
+
author: 'James Wilson',
|
|
390
|
+
role: 'CEO',
|
|
391
|
+
company: 'StartUp Labs',
|
|
392
|
+
avatar: 'https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=150',
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
id: 3,
|
|
396
|
+
content: 'Their expertise in Angular is remarkable. They transformed our legacy application into a modern, performant solution.',
|
|
397
|
+
author: 'Emily Chen',
|
|
398
|
+
role: 'Tech Lead',
|
|
399
|
+
company: 'Digital Solutions Co',
|
|
400
|
+
avatar: 'https://images.unsplash.com/photo-1438761681033-6461ffad8d80?w=150',
|
|
401
|
+
},
|
|
402
|
+
];
|
|
403
|
+
}
|
|
404
|
+
}`;
|
|
405
|
+
|
|
406
|
+
await fs.writeFile(
|
|
407
|
+
path.join(config.fullPath, 'src/app/core/services/portfolio.service.ts'),
|
|
408
|
+
portfolioService
|
|
409
|
+
);
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
async createComponents(config) {
|
|
413
|
+
// Project Card
|
|
414
|
+
const projectCard = `import { Component, Input } from '@angular/core';
|
|
415
|
+
import { RouterModule } from '@angular/router';
|
|
416
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
417
|
+
import { heroArrowTopRightOnSquare, heroCodeBracket } from '@ng-icons/heroicons/outline';
|
|
418
|
+
import { Project } from '@core/services/portfolio.service';
|
|
419
|
+
|
|
420
|
+
@Component({
|
|
421
|
+
selector: 'app-project-card',
|
|
422
|
+
standalone: true,
|
|
423
|
+
imports: [RouterModule, NgIconComponent],
|
|
424
|
+
viewProviders: [provideIcons({ heroArrowTopRightOnSquare, heroCodeBracket })],
|
|
425
|
+
template: \`
|
|
426
|
+
<article class="group overflow-hidden rounded-2xl border border-gray-200 bg-white shadow-sm transition-all hover:shadow-xl">
|
|
427
|
+
<a [routerLink]="['/portfolio', project.slug]" class="block">
|
|
428
|
+
<div class="aspect-video overflow-hidden">
|
|
429
|
+
<img
|
|
430
|
+
[src]="project.image"
|
|
431
|
+
[alt]="project.title"
|
|
432
|
+
class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-110"
|
|
433
|
+
/>
|
|
434
|
+
</div>
|
|
435
|
+
</a>
|
|
436
|
+
|
|
437
|
+
<div class="p-6">
|
|
438
|
+
<div class="mb-3 flex items-center gap-2">
|
|
439
|
+
<span class="rounded-full bg-primary-100 px-3 py-1 text-xs font-medium text-primary-700">
|
|
440
|
+
{{ project.category }}
|
|
441
|
+
</span>
|
|
442
|
+
@if (project.featured) {
|
|
443
|
+
<span class="rounded-full bg-amber-100 px-3 py-1 text-xs font-medium text-amber-700">
|
|
444
|
+
Featured
|
|
445
|
+
</span>
|
|
446
|
+
}
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<a [routerLink]="['/portfolio', project.slug]">
|
|
450
|
+
<h3 class="mb-2 text-xl font-bold text-gray-900 group-hover:text-primary-600 transition-colors">
|
|
451
|
+
{{ project.title }}
|
|
452
|
+
</h3>
|
|
453
|
+
</a>
|
|
454
|
+
|
|
455
|
+
<p class="mb-4 text-gray-600 line-clamp-2">{{ project.description }}</p>
|
|
456
|
+
|
|
457
|
+
<div class="mb-4 flex flex-wrap gap-2">
|
|
458
|
+
@for (tag of project.tags.slice(0, 3); track tag) {
|
|
459
|
+
<span class="rounded-lg bg-gray-100 px-2 py-1 text-xs text-gray-600">{{ tag }}</span>
|
|
460
|
+
}
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<div class="flex items-center gap-3">
|
|
464
|
+
@if (project.liveUrl) {
|
|
465
|
+
<a
|
|
466
|
+
[href]="project.liveUrl"
|
|
467
|
+
target="_blank"
|
|
468
|
+
rel="noopener noreferrer"
|
|
469
|
+
class="flex items-center gap-1 text-sm font-medium text-primary-600 hover:text-primary-700">
|
|
470
|
+
<ng-icon name="heroArrowTopRightOnSquare" size="16"></ng-icon>
|
|
471
|
+
Live Demo
|
|
472
|
+
</a>
|
|
473
|
+
}
|
|
474
|
+
@if (project.githubUrl) {
|
|
475
|
+
<a
|
|
476
|
+
[href]="project.githubUrl"
|
|
477
|
+
target="_blank"
|
|
478
|
+
rel="noopener noreferrer"
|
|
479
|
+
class="flex items-center gap-1 text-sm font-medium text-gray-600 hover:text-gray-900">
|
|
480
|
+
<ng-icon name="heroCodeBracket" size="16"></ng-icon>
|
|
481
|
+
Code
|
|
482
|
+
</a>
|
|
483
|
+
}
|
|
484
|
+
</div>
|
|
485
|
+
</div>
|
|
486
|
+
</article>
|
|
487
|
+
\`,
|
|
488
|
+
})
|
|
489
|
+
export class ProjectCardComponent {
|
|
490
|
+
@Input({ required: true }) project!: Project;
|
|
491
|
+
}`;
|
|
492
|
+
|
|
493
|
+
await fs.writeFile(
|
|
494
|
+
path.join(
|
|
495
|
+
config.fullPath,
|
|
496
|
+
'src/app/shared/components/project-card/project-card.component.ts'
|
|
497
|
+
),
|
|
498
|
+
projectCard
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
// Skill Bar
|
|
502
|
+
const skillBar = `import { Component, Input } from '@angular/core';
|
|
503
|
+
import { Skill } from '@core/services/portfolio.service';
|
|
504
|
+
|
|
505
|
+
@Component({
|
|
506
|
+
selector: 'app-skill-bar',
|
|
507
|
+
standalone: true,
|
|
508
|
+
template: \`
|
|
509
|
+
<div class="mb-4">
|
|
510
|
+
<div class="flex justify-between mb-1">
|
|
511
|
+
<span class="font-medium text-gray-900">{{ skill.name }}</span>
|
|
512
|
+
<span class="text-sm text-gray-500">{{ skill.level }}%</span>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="h-2 w-full overflow-hidden rounded-full bg-gray-200">
|
|
515
|
+
<div
|
|
516
|
+
class="h-full rounded-full bg-gradient-to-r from-primary-500 to-purple-500 transition-all duration-1000"
|
|
517
|
+
[style.width.%]="animated ? skill.level : 0">
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
</div>
|
|
521
|
+
\`,
|
|
522
|
+
})
|
|
523
|
+
export class SkillBarComponent {
|
|
524
|
+
@Input({ required: true }) skill!: Skill;
|
|
525
|
+
@Input() animated = true;
|
|
526
|
+
}`;
|
|
527
|
+
|
|
528
|
+
await fs.writeFile(
|
|
529
|
+
path.join(config.fullPath, 'src/app/shared/components/skill-bar/skill-bar.component.ts'),
|
|
530
|
+
skillBar
|
|
531
|
+
);
|
|
532
|
+
|
|
533
|
+
// Timeline
|
|
534
|
+
const timeline = `import { Component, Input } from '@angular/core';
|
|
535
|
+
import { DatePipe } from '@angular/common';
|
|
536
|
+
import { Experience, Education } from '@core/services/portfolio.service';
|
|
537
|
+
|
|
538
|
+
@Component({
|
|
539
|
+
selector: 'app-timeline',
|
|
540
|
+
standalone: true,
|
|
541
|
+
imports: [DatePipe],
|
|
542
|
+
template: \`
|
|
543
|
+
<div class="relative">
|
|
544
|
+
<!-- Timeline line -->
|
|
545
|
+
<div class="absolute left-4 top-0 h-full w-0.5 bg-gray-200 md:left-1/2 md:-translate-x-1/2"></div>
|
|
546
|
+
|
|
547
|
+
@for (item of items; track item.id; let i = $index) {
|
|
548
|
+
<div class="relative mb-8 last:mb-0">
|
|
549
|
+
<!-- Timeline dot -->
|
|
550
|
+
<div
|
|
551
|
+
class="absolute left-4 h-4 w-4 -translate-x-1/2 rounded-full border-4 border-white md:left-1/2"
|
|
552
|
+
[class.bg-primary-500]="isExperience(item) && item.current"
|
|
553
|
+
[class.bg-gray-400]="!isExperience(item) || !item.current">
|
|
554
|
+
</div>
|
|
555
|
+
|
|
556
|
+
<!-- Content -->
|
|
557
|
+
<div
|
|
558
|
+
class="ml-12 md:ml-0 md:w-5/12"
|
|
559
|
+
[class.md:ml-auto]="i % 2 === 0"
|
|
560
|
+
[class.md:mr-auto]="i % 2 !== 0"
|
|
561
|
+
[class.md:pr-8]="i % 2 !== 0"
|
|
562
|
+
[class.md:pl-8]="i % 2 === 0"
|
|
563
|
+
[class.md:text-right]="i % 2 !== 0">
|
|
564
|
+
|
|
565
|
+
<div class="rounded-xl border border-gray-200 bg-white p-6 shadow-sm">
|
|
566
|
+
<div class="mb-2 text-sm text-gray-500">
|
|
567
|
+
{{ item.startDate | date:'MMM yyyy' }} -
|
|
568
|
+
@if (isExperience(item) && item.current) {
|
|
569
|
+
<span class="text-primary-600 font-medium">Present</span>
|
|
570
|
+
} @else {
|
|
571
|
+
{{ getEndDate(item) | date:'MMM yyyy' }}
|
|
572
|
+
}
|
|
573
|
+
</div>
|
|
574
|
+
|
|
575
|
+
<h3 class="text-lg font-bold text-gray-900">
|
|
576
|
+
{{ isExperience(item) ? item.title : item.degree }}
|
|
577
|
+
</h3>
|
|
578
|
+
|
|
579
|
+
<p class="text-primary-600 font-medium">
|
|
580
|
+
{{ isExperience(item) ? item.company : item.school }}
|
|
581
|
+
</p>
|
|
582
|
+
|
|
583
|
+
<p class="text-sm text-gray-500">{{ item.location }}</p>
|
|
584
|
+
|
|
585
|
+
@if (item.description) {
|
|
586
|
+
<p class="mt-3 text-gray-600">{{ item.description }}</p>
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
@if (isExperience(item) && item.achievements && item.achievements.length > 0) {
|
|
590
|
+
<ul class="mt-3 space-y-1" [class.text-left]="i % 2 !== 0">
|
|
591
|
+
@for (achievement of item.achievements; track achievement) {
|
|
592
|
+
<li class="text-sm text-gray-600">• {{ achievement }}</li>
|
|
593
|
+
}
|
|
594
|
+
</ul>
|
|
595
|
+
}
|
|
596
|
+
</div>
|
|
597
|
+
</div>
|
|
598
|
+
</div>
|
|
599
|
+
}
|
|
600
|
+
</div>
|
|
601
|
+
\`,
|
|
602
|
+
})
|
|
603
|
+
export class TimelineComponent {
|
|
604
|
+
@Input({ required: true }) items!: (Experience | Education)[];
|
|
605
|
+
|
|
606
|
+
isExperience(item: Experience | Education): item is Experience {
|
|
607
|
+
return 'company' in item;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
getEndDate(item: Experience | Education): Date | undefined {
|
|
611
|
+
if (this.isExperience(item)) {
|
|
612
|
+
return item.endDate;
|
|
613
|
+
}
|
|
614
|
+
return item.endDate;
|
|
615
|
+
}
|
|
616
|
+
}`;
|
|
617
|
+
|
|
618
|
+
await fs.writeFile(
|
|
619
|
+
path.join(config.fullPath, 'src/app/shared/components/timeline/timeline.component.ts'),
|
|
620
|
+
timeline
|
|
621
|
+
);
|
|
622
|
+
|
|
623
|
+
// Social Links
|
|
624
|
+
const socialLinks = `import { Component, Input } from '@angular/core';
|
|
625
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
626
|
+
import { heroEnvelope, heroCodeBracket, heroBriefcase, heroPaintBrush } from '@ng-icons/heroicons/outline';
|
|
627
|
+
|
|
628
|
+
export interface SocialLink {
|
|
629
|
+
platform: 'github' | 'linkedin' | 'twitter' | 'dribbble' | 'email';
|
|
630
|
+
url: string;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
@Component({
|
|
634
|
+
selector: 'app-social-links',
|
|
635
|
+
standalone: true,
|
|
636
|
+
imports: [NgIconComponent],
|
|
637
|
+
viewProviders: [provideIcons({ heroEnvelope, heroCodeBracket, heroBriefcase, heroPaintBrush })],
|
|
638
|
+
template: \`
|
|
639
|
+
<div class="flex items-center gap-4">
|
|
640
|
+
@for (link of links; track link.platform) {
|
|
641
|
+
<a
|
|
642
|
+
[href]="link.platform === 'email' ? 'mailto:' + link.url : link.url"
|
|
643
|
+
target="_blank"
|
|
644
|
+
rel="noopener noreferrer"
|
|
645
|
+
class="flex h-10 w-10 items-center justify-center rounded-full transition-all"
|
|
646
|
+
[class]="variant === 'light' ? 'bg-white/10 text-white hover:bg-white/20' : 'bg-gray-100 text-gray-600 hover:bg-gray-200 hover:text-gray-900'"
|
|
647
|
+
[attr.aria-label]="link.platform">
|
|
648
|
+
@if (link.platform === 'twitter') {
|
|
649
|
+
<span class="text-sm font-bold">𝕏</span>
|
|
650
|
+
} @else {
|
|
651
|
+
<ng-icon [name]="getIcon(link.platform)" size="20"></ng-icon>
|
|
652
|
+
}
|
|
653
|
+
</a>
|
|
654
|
+
}
|
|
655
|
+
</div>
|
|
656
|
+
\`,
|
|
657
|
+
})
|
|
658
|
+
export class SocialLinksComponent {
|
|
659
|
+
@Input({ required: true }) links!: SocialLink[];
|
|
660
|
+
@Input() variant: 'light' | 'dark' = 'dark';
|
|
661
|
+
|
|
662
|
+
getIcon(platform: string): string {
|
|
663
|
+
const icons: Record<string, string> = {
|
|
664
|
+
github: 'heroCodeBracket',
|
|
665
|
+
linkedin: 'heroBriefcase',
|
|
666
|
+
dribbble: 'heroPaintBrush',
|
|
667
|
+
email: 'heroEnvelope',
|
|
668
|
+
};
|
|
669
|
+
return icons[platform] || 'heroEnvelope';
|
|
670
|
+
}
|
|
671
|
+
}`;
|
|
672
|
+
|
|
673
|
+
await fs.writeFile(
|
|
674
|
+
path.join(
|
|
675
|
+
config.fullPath,
|
|
676
|
+
'src/app/shared/components/social-links/social-links.component.ts'
|
|
677
|
+
),
|
|
678
|
+
socialLinks
|
|
679
|
+
);
|
|
680
|
+
},
|
|
681
|
+
|
|
682
|
+
async createPages(config) {
|
|
683
|
+
// Portfolio Home (Projects List)
|
|
684
|
+
const portfolioHome = `import { Component, inject, signal, computed } from '@angular/core';
|
|
685
|
+
import { RouterModule } from '@angular/router';
|
|
686
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
687
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
688
|
+
import { heroArrowDown, heroDocumentArrowDown } from '@ng-icons/heroicons/outline';
|
|
689
|
+
import { ProjectCardComponent } from '@shared/components/project-card/project-card.component';
|
|
690
|
+
import { SkillBarComponent } from '@shared/components/skill-bar/skill-bar.component';
|
|
691
|
+
import { TimelineComponent } from '@shared/components/timeline/timeline.component';
|
|
692
|
+
import { SocialLinksComponent, SocialLink } from '@shared/components/social-links/social-links.component';
|
|
693
|
+
import { PortfolioService } from '@core/services/portfolio.service';
|
|
694
|
+
|
|
695
|
+
@Component({
|
|
696
|
+
selector: 'app-portfolio-home',
|
|
697
|
+
standalone: true,
|
|
698
|
+
imports: [RouterModule, TranslateModule, NgIconComponent, ProjectCardComponent, SkillBarComponent, TimelineComponent, SocialLinksComponent],
|
|
699
|
+
viewProviders: [provideIcons({ heroArrowDown, heroDocumentArrowDown })],
|
|
700
|
+
template: \`
|
|
701
|
+
<div>
|
|
702
|
+
<!-- Hero Section -->
|
|
703
|
+
<section class="relative min-h-screen flex items-center bg-linear-to-br from-slate-900 via-purple-900 to-slate-900 overflow-hidden">
|
|
704
|
+
<!-- Animated Background -->
|
|
705
|
+
<div class="absolute inset-0">
|
|
706
|
+
<div class="animate-blob absolute -left-4 top-20 h-72 w-72 rounded-full bg-purple-500 opacity-20 mix-blend-multiply blur-xl"></div>
|
|
707
|
+
<div class="animate-blob animation-delay-2000 absolute right-20 top-40 h-72 w-72 rounded-full bg-yellow-500 opacity-20 mix-blend-multiply blur-xl"></div>
|
|
708
|
+
<div class="animate-blob animation-delay-4000 absolute bottom-20 left-1/3 h-72 w-72 rounded-full bg-pink-500 opacity-20 mix-blend-multiply blur-xl"></div>
|
|
709
|
+
</div>
|
|
710
|
+
|
|
711
|
+
<div class="relative mx-auto max-w-7xl px-4 py-20 sm:px-6 lg:px-8">
|
|
712
|
+
<div class="grid items-center gap-12 lg:grid-cols-2">
|
|
713
|
+
<div class="text-center lg:text-left">
|
|
714
|
+
<p class="text-primary-400 font-medium mb-4">{{ 'portfolio.greeting' | translate }}</p>
|
|
715
|
+
<h1 class="text-4xl font-bold text-white sm:text-5xl lg:text-6xl">
|
|
716
|
+
{{ 'portfolio.name' | translate }}
|
|
717
|
+
</h1>
|
|
718
|
+
<p class="mt-2 text-2xl text-purple-300">{{ 'portfolio.title' | translate }}</p>
|
|
719
|
+
<p class="mt-6 text-lg text-gray-300 max-w-xl">
|
|
720
|
+
{{ 'portfolio.bio' | translate }}
|
|
721
|
+
</p>
|
|
722
|
+
|
|
723
|
+
<div class="mt-8 flex flex-wrap gap-4 justify-center lg:justify-start">
|
|
724
|
+
<a href="#projects" class="inline-flex items-center gap-2 rounded-full bg-white px-6 py-3 font-semibold text-gray-900 hover:bg-gray-100 transition-all">
|
|
725
|
+
{{ 'portfolio.viewWork' | translate }}
|
|
726
|
+
<ng-icon name="heroArrowDown" size="20"></ng-icon>
|
|
727
|
+
</a>
|
|
728
|
+
<a href="/assets/resume.pdf" download class="inline-flex items-center gap-2 rounded-full border-2 border-white/30 px-6 py-3 font-semibold text-white hover:bg-white/10 transition-all">
|
|
729
|
+
<ng-icon name="heroDocumentArrowDown" size="20"></ng-icon>
|
|
730
|
+
{{ 'portfolio.downloadResume' | translate }}
|
|
731
|
+
</a>
|
|
732
|
+
</div>
|
|
733
|
+
|
|
734
|
+
<div class="mt-8">
|
|
735
|
+
<app-social-links [links]="socialLinks" variant="light"></app-social-links>
|
|
736
|
+
</div>
|
|
737
|
+
</div>
|
|
738
|
+
|
|
739
|
+
<div class="hidden lg:block">
|
|
740
|
+
<div class="relative">
|
|
741
|
+
<div class="absolute inset-0 rounded-full bg-gradient-to-r from-primary-500 to-purple-500 blur-3xl opacity-30"></div>
|
|
742
|
+
<img
|
|
743
|
+
src="https://images.unsplash.com/photo-1507003211169-0a1dd7228f2d?w=500"
|
|
744
|
+
alt="Profile"
|
|
745
|
+
class="relative mx-auto h-96 w-96 rounded-full object-cover border-4 border-white/20 shadow-2xl"
|
|
746
|
+
/>
|
|
747
|
+
</div>
|
|
748
|
+
</div>
|
|
749
|
+
</div>
|
|
750
|
+
</div>
|
|
751
|
+
|
|
752
|
+
<!-- Scroll indicator -->
|
|
753
|
+
<div class="absolute bottom-8 left-1/2 -translate-x-1/2 animate-bounce">
|
|
754
|
+
<ng-icon name="heroArrowDown" size="24" class="text-white/50"></ng-icon>
|
|
755
|
+
</div>
|
|
756
|
+
</section>
|
|
757
|
+
|
|
758
|
+
<!-- Projects Section -->
|
|
759
|
+
<section id="projects" class="py-20">
|
|
760
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
761
|
+
<div class="text-center mb-12">
|
|
762
|
+
<h2 class="text-3xl font-bold text-gray-900">{{ 'portfolio.featuredProjects' | translate }}</h2>
|
|
763
|
+
<p class="mt-2 text-gray-600">{{ 'portfolio.projectsSubtitle' | translate }}</p>
|
|
764
|
+
</div>
|
|
765
|
+
|
|
766
|
+
<!-- Category Filter -->
|
|
767
|
+
<div class="mb-8 flex flex-wrap justify-center gap-2">
|
|
768
|
+
<button
|
|
769
|
+
(click)="selectedCategory.set('')"
|
|
770
|
+
class="rounded-full px-4 py-2 text-sm font-medium transition-colors"
|
|
771
|
+
[class.bg-primary-500]="!selectedCategory()"
|
|
772
|
+
[class.text-white]="!selectedCategory()"
|
|
773
|
+
[class.bg-gray-100]="selectedCategory()"
|
|
774
|
+
[class.text-gray-700]="selectedCategory()">
|
|
775
|
+
All
|
|
776
|
+
</button>
|
|
777
|
+
@for (cat of portfolioService.getCategories(); track cat) {
|
|
778
|
+
<button
|
|
779
|
+
(click)="selectedCategory.set(cat)"
|
|
780
|
+
class="rounded-full px-4 py-2 text-sm font-medium transition-colors"
|
|
781
|
+
[class.bg-primary-500]="selectedCategory() === cat"
|
|
782
|
+
[class.text-white]="selectedCategory() === cat"
|
|
783
|
+
[class.bg-gray-100]="selectedCategory() !== cat"
|
|
784
|
+
[class.text-gray-700]="selectedCategory() !== cat">
|
|
785
|
+
{{ cat }}
|
|
786
|
+
</button>
|
|
787
|
+
}
|
|
788
|
+
</div>
|
|
789
|
+
|
|
790
|
+
<div class="grid gap-8 md:grid-cols-2 lg:grid-cols-3">
|
|
791
|
+
@for (project of filteredProjects(); track project.id) {
|
|
792
|
+
<app-project-card [project]="project"></app-project-card>
|
|
793
|
+
}
|
|
794
|
+
</div>
|
|
795
|
+
</div>
|
|
796
|
+
</section>
|
|
797
|
+
|
|
798
|
+
<!-- Skills Section -->
|
|
799
|
+
<section class="py-20 bg-gray-50">
|
|
800
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
801
|
+
<div class="text-center mb-12">
|
|
802
|
+
<h2 class="text-3xl font-bold text-gray-900">{{ 'portfolio.skills' | translate }}</h2>
|
|
803
|
+
<p class="mt-2 text-gray-600">{{ 'portfolio.skillsSubtitle' | translate }}</p>
|
|
804
|
+
</div>
|
|
805
|
+
|
|
806
|
+
<div class="grid gap-8 md:grid-cols-3">
|
|
807
|
+
@for (category of skillCategories; track category) {
|
|
808
|
+
<div class="rounded-xl border border-gray-200 bg-white p-6">
|
|
809
|
+
<h3 class="mb-6 text-lg font-bold text-gray-900">{{ category }}</h3>
|
|
810
|
+
@for (skill of getSkillsByCategory(category); track skill.name) {
|
|
811
|
+
<app-skill-bar [skill]="skill"></app-skill-bar>
|
|
812
|
+
}
|
|
813
|
+
</div>
|
|
814
|
+
}
|
|
815
|
+
</div>
|
|
816
|
+
</div>
|
|
817
|
+
</section>
|
|
818
|
+
|
|
819
|
+
<!-- Experience Section -->
|
|
820
|
+
<section class="py-20">
|
|
821
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
822
|
+
<div class="text-center mb-12">
|
|
823
|
+
<h2 class="text-3xl font-bold text-gray-900">{{ 'portfolio.experience' | translate }}</h2>
|
|
824
|
+
<p class="mt-2 text-gray-600">{{ 'portfolio.experienceSubtitle' | translate }}</p>
|
|
825
|
+
</div>
|
|
826
|
+
|
|
827
|
+
<app-timeline [items]="portfolioService.experience()"></app-timeline>
|
|
828
|
+
</div>
|
|
829
|
+
</section>
|
|
830
|
+
|
|
831
|
+
<!-- Education Section -->
|
|
832
|
+
<section class="py-20 bg-gray-50">
|
|
833
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
834
|
+
<div class="text-center mb-12">
|
|
835
|
+
<h2 class="text-3xl font-bold text-gray-900">{{ 'portfolio.education' | translate }}</h2>
|
|
836
|
+
</div>
|
|
837
|
+
|
|
838
|
+
<app-timeline [items]="portfolioService.education()"></app-timeline>
|
|
839
|
+
</div>
|
|
840
|
+
</section>
|
|
841
|
+
|
|
842
|
+
<!-- Testimonials Section -->
|
|
843
|
+
<section class="py-20">
|
|
844
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
845
|
+
<div class="text-center mb-12">
|
|
846
|
+
<h2 class="text-3xl font-bold text-gray-900">{{ 'portfolio.testimonials' | translate }}</h2>
|
|
847
|
+
<p class="mt-2 text-gray-600">{{ 'portfolio.testimonialsSubtitle' | translate }}</p>
|
|
848
|
+
</div>
|
|
849
|
+
|
|
850
|
+
<div class="grid gap-8 md:grid-cols-3">
|
|
851
|
+
@for (testimonial of portfolioService.testimonials(); track testimonial.id) {
|
|
852
|
+
<div class="rounded-2xl border border-gray-200 bg-white p-8">
|
|
853
|
+
<p class="text-gray-700 italic mb-6">"{{ testimonial.content }}"</p>
|
|
854
|
+
<div class="flex items-center gap-4">
|
|
855
|
+
<img [src]="testimonial.avatar" [alt]="testimonial.author" class="h-12 w-12 rounded-full object-cover" />
|
|
856
|
+
<div>
|
|
857
|
+
<p class="font-semibold text-gray-900">{{ testimonial.author }}</p>
|
|
858
|
+
<p class="text-sm text-gray-500">{{ testimonial.role }}, {{ testimonial.company }}</p>
|
|
859
|
+
</div>
|
|
860
|
+
</div>
|
|
861
|
+
</div>
|
|
862
|
+
}
|
|
863
|
+
</div>
|
|
864
|
+
</div>
|
|
865
|
+
</section>
|
|
866
|
+
|
|
867
|
+
<!-- CTA Section -->
|
|
868
|
+
<section class="py-20 bg-gradient-to-r from-primary-600 to-purple-600">
|
|
869
|
+
<div class="mx-auto max-w-4xl px-4 text-center sm:px-6 lg:px-8">
|
|
870
|
+
<h2 class="text-3xl font-bold text-white">{{ 'portfolio.ctaTitle' | translate }}</h2>
|
|
871
|
+
<p class="mt-4 text-xl text-primary-100">{{ 'portfolio.ctaSubtitle' | translate }}</p>
|
|
872
|
+
<a routerLink="/contact" class="mt-8 inline-flex items-center gap-2 rounded-full bg-white px-8 py-4 font-semibold text-gray-900 hover:bg-gray-100 transition-all">
|
|
873
|
+
{{ 'portfolio.getInTouch' | translate }}
|
|
874
|
+
</a>
|
|
875
|
+
</div>
|
|
876
|
+
</section>
|
|
877
|
+
</div>
|
|
878
|
+
\`,
|
|
879
|
+
})
|
|
880
|
+
export class PortfolioHomeComponent {
|
|
881
|
+
portfolioService = inject(PortfolioService);
|
|
882
|
+
|
|
883
|
+
selectedCategory = signal('');
|
|
884
|
+
skillCategories = ['Frontend', 'Backend', 'Tools'];
|
|
885
|
+
|
|
886
|
+
socialLinks: SocialLink[] = [
|
|
887
|
+
{ platform: 'github', url: 'https://github.com' },
|
|
888
|
+
{ platform: 'linkedin', url: 'https://linkedin.com' },
|
|
889
|
+
{ platform: 'twitter', url: 'https://twitter.com' },
|
|
890
|
+
{ platform: 'email', url: 'hello@example.com' },
|
|
891
|
+
];
|
|
892
|
+
|
|
893
|
+
filteredProjects = computed(() => {
|
|
894
|
+
const cat = this.selectedCategory();
|
|
895
|
+
if (!cat) return this.portfolioService.projects();
|
|
896
|
+
return this.portfolioService.getProjectsByCategory(cat);
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
getSkillsByCategory(category: string) {
|
|
900
|
+
return this.portfolioService.skills().filter(s => s.category === category);
|
|
901
|
+
}
|
|
902
|
+
}`;
|
|
903
|
+
|
|
904
|
+
await fs.writeFile(
|
|
905
|
+
path.join(config.fullPath, 'src/app/features/portfolio/projects/projects.component.ts'),
|
|
906
|
+
portfolioHome
|
|
907
|
+
);
|
|
908
|
+
|
|
909
|
+
// Project Detail
|
|
910
|
+
const projectDetail = `import { Component, inject } from '@angular/core';
|
|
911
|
+
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
|
912
|
+
import { DatePipe } from '@angular/common';
|
|
913
|
+
import { TranslateModule } from '@ngx-translate/core';
|
|
914
|
+
import { NgIconComponent, provideIcons } from '@ng-icons/core';
|
|
915
|
+
import { heroArrowLeft, heroArrowTopRightOnSquare, heroCodeBracket } from '@ng-icons/heroicons/outline';
|
|
916
|
+
import { ProjectCardComponent } from '@shared/components/project-card/project-card.component';
|
|
917
|
+
import { PortfolioService, Project } from '@core/services/portfolio.service';
|
|
918
|
+
|
|
919
|
+
@Component({
|
|
920
|
+
selector: 'app-project-detail',
|
|
921
|
+
standalone: true,
|
|
922
|
+
imports: [RouterModule, DatePipe, TranslateModule, NgIconComponent, ProjectCardComponent],
|
|
923
|
+
viewProviders: [provideIcons({ heroArrowLeft, heroArrowTopRightOnSquare, heroCodeBracket })],
|
|
924
|
+
template: \`
|
|
925
|
+
@if (project) {
|
|
926
|
+
<div>
|
|
927
|
+
<!-- Hero -->
|
|
928
|
+
<section class="relative bg-gray-900">
|
|
929
|
+
<div class="absolute inset-0">
|
|
930
|
+
<img [src]="project.image" [alt]="project.title" class="h-full w-full object-cover opacity-30" />
|
|
931
|
+
</div>
|
|
932
|
+
<div class="relative mx-auto max-w-7xl px-4 py-24 sm:px-6 lg:px-8">
|
|
933
|
+
<a routerLink="/portfolio" class="mb-8 inline-flex items-center gap-2 text-white/80 hover:text-white">
|
|
934
|
+
<ng-icon name="heroArrowLeft" size="20"></ng-icon>
|
|
935
|
+
Back to Portfolio
|
|
936
|
+
</a>
|
|
937
|
+
|
|
938
|
+
<div class="flex items-center gap-3 mb-4">
|
|
939
|
+
<span class="rounded-full bg-primary-500 px-4 py-1 text-sm font-medium text-white">
|
|
940
|
+
{{ project.category }}
|
|
941
|
+
</span>
|
|
942
|
+
<span class="text-white/60">{{ project.completedAt | date:'MMMM yyyy' }}</span>
|
|
943
|
+
</div>
|
|
944
|
+
|
|
945
|
+
<h1 class="text-4xl font-bold text-white sm:text-5xl">{{ project.title }}</h1>
|
|
946
|
+
|
|
947
|
+
<div class="mt-6 flex flex-wrap gap-2">
|
|
948
|
+
@for (tag of project.tags; track tag) {
|
|
949
|
+
<span class="rounded-lg bg-white/10 px-3 py-1 text-sm text-white">{{ tag }}</span>
|
|
950
|
+
}
|
|
951
|
+
</div>
|
|
952
|
+
|
|
953
|
+
<div class="mt-8 flex gap-4">
|
|
954
|
+
@if (project.liveUrl) {
|
|
955
|
+
<a
|
|
956
|
+
[href]="project.liveUrl"
|
|
957
|
+
target="_blank"
|
|
958
|
+
rel="noopener noreferrer"
|
|
959
|
+
class="inline-flex items-center gap-2 rounded-lg bg-white px-6 py-3 font-semibold text-gray-900 hover:bg-gray-100">
|
|
960
|
+
<ng-icon name="heroArrowTopRightOnSquare" size="20"></ng-icon>
|
|
961
|
+
View Live
|
|
962
|
+
</a>
|
|
963
|
+
}
|
|
964
|
+
@if (project.githubUrl) {
|
|
965
|
+
<a
|
|
966
|
+
[href]="project.githubUrl"
|
|
967
|
+
target="_blank"
|
|
968
|
+
rel="noopener noreferrer"
|
|
969
|
+
class="inline-flex items-center gap-2 rounded-lg border-2 border-white/30 px-6 py-3 font-semibold text-white hover:bg-white/10">
|
|
970
|
+
<ng-icon name="heroCodeBracket" size="20"></ng-icon>
|
|
971
|
+
View Code
|
|
972
|
+
</a>
|
|
973
|
+
}
|
|
974
|
+
</div>
|
|
975
|
+
</div>
|
|
976
|
+
</section>
|
|
977
|
+
|
|
978
|
+
<!-- Content -->
|
|
979
|
+
<section class="py-16">
|
|
980
|
+
<div class="mx-auto max-w-4xl px-4 sm:px-6 lg:px-8">
|
|
981
|
+
<h2 class="text-2xl font-bold text-gray-900 mb-6">About This Project</h2>
|
|
982
|
+
<p class="text-lg text-gray-700 leading-relaxed">{{ project.longDescription }}</p>
|
|
983
|
+
|
|
984
|
+
<!-- Gallery -->
|
|
985
|
+
@if (project.images && project.images.length > 1) {
|
|
986
|
+
<div class="mt-12">
|
|
987
|
+
<h3 class="text-xl font-bold text-gray-900 mb-6">Project Gallery</h3>
|
|
988
|
+
<div class="grid gap-4 md:grid-cols-2">
|
|
989
|
+
@for (img of project.images; track img) {
|
|
990
|
+
<div class="overflow-hidden rounded-xl">
|
|
991
|
+
<img [src]="img" [alt]="project.title" class="w-full h-auto" />
|
|
992
|
+
</div>
|
|
993
|
+
}
|
|
994
|
+
</div>
|
|
995
|
+
</div>
|
|
996
|
+
}
|
|
997
|
+
</div>
|
|
998
|
+
</section>
|
|
999
|
+
|
|
1000
|
+
<!-- Related Projects -->
|
|
1001
|
+
@if (relatedProjects.length > 0) {
|
|
1002
|
+
<section class="py-16 bg-gray-50">
|
|
1003
|
+
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
|
1004
|
+
<h2 class="text-2xl font-bold text-gray-900 mb-8">Related Projects</h2>
|
|
1005
|
+
<div class="grid gap-8 md:grid-cols-3">
|
|
1006
|
+
@for (related of relatedProjects; track related.id) {
|
|
1007
|
+
<app-project-card [project]="related"></app-project-card>
|
|
1008
|
+
}
|
|
1009
|
+
</div>
|
|
1010
|
+
</div>
|
|
1011
|
+
</section>
|
|
1012
|
+
}
|
|
1013
|
+
</div>
|
|
1014
|
+
}
|
|
1015
|
+
\`,
|
|
1016
|
+
})
|
|
1017
|
+
export class ProjectDetailComponent {
|
|
1018
|
+
private route = inject(ActivatedRoute);
|
|
1019
|
+
private router = inject(Router);
|
|
1020
|
+
private portfolioService = inject(PortfolioService);
|
|
1021
|
+
|
|
1022
|
+
project?: Project;
|
|
1023
|
+
relatedProjects: Project[] = [];
|
|
1024
|
+
|
|
1025
|
+
constructor() {
|
|
1026
|
+
this.route.params.subscribe(params => {
|
|
1027
|
+
const slug = params['slug'];
|
|
1028
|
+
this.project = this.portfolioService.getProjectBySlug(slug);
|
|
1029
|
+
|
|
1030
|
+
if (this.project) {
|
|
1031
|
+
this.relatedProjects = this.portfolioService
|
|
1032
|
+
.getProjectsByCategory(this.project.category)
|
|
1033
|
+
.filter(p => p.id !== this.project!.id)
|
|
1034
|
+
.slice(0, 3);
|
|
1035
|
+
} else {
|
|
1036
|
+
this.router.navigate(['/portfolio']);
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
}`;
|
|
1041
|
+
|
|
1042
|
+
await fs.writeFile(
|
|
1043
|
+
path.join(
|
|
1044
|
+
config.fullPath,
|
|
1045
|
+
'src/app/features/portfolio/project-detail/project-detail.component.ts'
|
|
1046
|
+
),
|
|
1047
|
+
projectDetail
|
|
1048
|
+
);
|
|
1049
|
+
},
|
|
1050
|
+
|
|
1051
|
+
async createRouting(config) {
|
|
1052
|
+
const routes = `import { Routes } from '@angular/router';
|
|
1053
|
+
|
|
1054
|
+
export const routes: Routes = [
|
|
1055
|
+
// Portfolio as home
|
|
1056
|
+
{
|
|
1057
|
+
path: '',
|
|
1058
|
+
loadComponent: () => import('./features/portfolio/projects/projects.component').then(c => c.PortfolioHomeComponent)
|
|
1059
|
+
},
|
|
1060
|
+
{
|
|
1061
|
+
path: 'portfolio',
|
|
1062
|
+
loadComponent: () => import('./features/portfolio/projects/projects.component').then(c => c.PortfolioHomeComponent)
|
|
1063
|
+
},
|
|
1064
|
+
{
|
|
1065
|
+
path: 'portfolio/:slug',
|
|
1066
|
+
loadComponent: () => import('./features/portfolio/project-detail/project-detail.component').then(c => c.ProjectDetailComponent)
|
|
1067
|
+
},
|
|
1068
|
+
{
|
|
1069
|
+
path: 'about',
|
|
1070
|
+
loadComponent: () => import('./features/about/about.component').then(c => c.AboutComponent)
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
path: 'contact',
|
|
1074
|
+
loadComponent: () => import('./features/contact/contact.component').then(c => c.ContactComponent)
|
|
1075
|
+
},
|
|
1076
|
+
|
|
1077
|
+
// Auth routes
|
|
1078
|
+
{
|
|
1079
|
+
path: 'auth',
|
|
1080
|
+
loadComponent: () => import('./layout/auth/auth-layout.component').then(c => c.AuthLayoutComponent),
|
|
1081
|
+
children: [
|
|
1082
|
+
{ path: '', redirectTo: 'login', pathMatch: 'full' },
|
|
1083
|
+
{ path: 'login', loadComponent: () => import('./features/auth/login/login.component').then(c => c.LoginComponent) },
|
|
1084
|
+
{ path: 'register', loadComponent: () => import('./features/auth/register/register.component').then(c => c.RegisterComponent) },
|
|
1085
|
+
{ path: 'forgot-password', loadComponent: () => import('./features/auth/forgot-password/forgot-password.component').then(c => c.ForgotPasswordComponent) }
|
|
1086
|
+
]
|
|
1087
|
+
},
|
|
1088
|
+
|
|
1089
|
+
{ path: '**', redirectTo: '' }
|
|
1090
|
+
];`;
|
|
1091
|
+
|
|
1092
|
+
await fs.writeFile(path.join(config.fullPath, 'src/app/app.routes.ts'), routes);
|
|
1093
|
+
},
|
|
1094
|
+
|
|
1095
|
+
async updateI18n(config) {
|
|
1096
|
+
const enPath = path.join(config.fullPath, 'public/assets/i18n/en.json');
|
|
1097
|
+
const arPath = path.join(config.fullPath, 'public/assets/i18n/ar.json');
|
|
1098
|
+
|
|
1099
|
+
let en = {};
|
|
1100
|
+
let ar = {};
|
|
1101
|
+
|
|
1102
|
+
try {
|
|
1103
|
+
en = JSON.parse(await fs.readFile(enPath, 'utf-8'));
|
|
1104
|
+
ar = JSON.parse(await fs.readFile(arPath, 'utf-8'));
|
|
1105
|
+
} catch {
|
|
1106
|
+
// Files don't exist yet
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
const portfolioEn = {
|
|
1110
|
+
portfolio: {
|
|
1111
|
+
greeting: "Hi, I'm",
|
|
1112
|
+
name: 'John Doe',
|
|
1113
|
+
title: 'Senior Frontend Developer',
|
|
1114
|
+
bio: 'I create beautiful, performant web applications with modern technologies. Passionate about clean code and great user experiences.',
|
|
1115
|
+
viewWork: 'View My Work',
|
|
1116
|
+
downloadResume: 'Download Resume',
|
|
1117
|
+
featuredProjects: 'Featured Projects',
|
|
1118
|
+
projectsSubtitle: 'A selection of my recent work',
|
|
1119
|
+
skills: 'Skills & Expertise',
|
|
1120
|
+
skillsSubtitle: 'Technologies I work with',
|
|
1121
|
+
experience: 'Work Experience',
|
|
1122
|
+
experienceSubtitle: 'My professional journey',
|
|
1123
|
+
education: 'Education',
|
|
1124
|
+
testimonials: 'What People Say',
|
|
1125
|
+
testimonialsSubtitle: 'Feedback from colleagues and clients',
|
|
1126
|
+
ctaTitle: "Let's Work Together",
|
|
1127
|
+
ctaSubtitle: "Have a project in mind? I'd love to hear about it.",
|
|
1128
|
+
getInTouch: 'Get In Touch',
|
|
1129
|
+
},
|
|
1130
|
+
};
|
|
1131
|
+
|
|
1132
|
+
const portfolioAr = {
|
|
1133
|
+
portfolio: {
|
|
1134
|
+
greeting: 'مرحباً، أنا',
|
|
1135
|
+
name: 'جون دو',
|
|
1136
|
+
title: 'مطور واجهات أمامية أول',
|
|
1137
|
+
bio: 'أقوم بإنشاء تطبيقات ويب جميلة وعالية الأداء باستخدام التقنيات الحديثة. شغوف بالكود النظيف وتجارب المستخدم الرائعة.',
|
|
1138
|
+
viewWork: 'شاهد أعمالي',
|
|
1139
|
+
downloadResume: 'تحميل السيرة الذاتية',
|
|
1140
|
+
featuredProjects: 'المشاريع المميزة',
|
|
1141
|
+
projectsSubtitle: 'مجموعة من أعمالي الأخيرة',
|
|
1142
|
+
skills: 'المهارات والخبرات',
|
|
1143
|
+
skillsSubtitle: 'التقنيات التي أعمل بها',
|
|
1144
|
+
experience: 'الخبرة العملية',
|
|
1145
|
+
experienceSubtitle: 'مسيرتي المهنية',
|
|
1146
|
+
education: 'التعليم',
|
|
1147
|
+
testimonials: 'ماذا يقول الناس',
|
|
1148
|
+
testimonialsSubtitle: 'آراء الزملاء والعملاء',
|
|
1149
|
+
ctaTitle: 'لنعمل معاً',
|
|
1150
|
+
ctaSubtitle: 'هل لديك مشروع في ذهنك؟ أود أن أسمع عنه.',
|
|
1151
|
+
getInTouch: 'تواصل معي',
|
|
1152
|
+
},
|
|
1153
|
+
};
|
|
1154
|
+
|
|
1155
|
+
await fs.writeFile(enPath, JSON.stringify({ ...en, ...portfolioEn }, null, 2));
|
|
1156
|
+
await fs.writeFile(arPath, JSON.stringify({ ...ar, ...portfolioAr }, null, 2));
|
|
1157
|
+
},
|
|
1158
|
+
};
|
|
1159
|
+
|
|
1160
|
+
module.exports = portfolio;
|