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.
Files changed (47) hide show
  1. package/CHANGELOG.md +81 -344
  2. package/README.md +93 -157
  3. package/lib/cli/index.js +29 -3
  4. package/lib/cli/interactive.js +26 -1
  5. package/lib/managers/ProjectManager.js +0 -4
  6. package/lib/templates/base/components.js +243 -0
  7. package/lib/templates/base/index.js +207 -0
  8. package/lib/templates/base/infrastructure.js +314 -0
  9. package/lib/templates/base/linting.js +359 -0
  10. package/lib/templates/base/pwa.js +103 -0
  11. package/lib/templates/base/services.js +362 -0
  12. package/lib/templates/blog/app.js +250 -0
  13. package/lib/templates/blog/components.js +360 -0
  14. package/lib/templates/blog/i18n.js +77 -0
  15. package/lib/templates/blog/index.js +126 -0
  16. package/lib/templates/blog/pages.js +554 -0
  17. package/lib/templates/blog/services.js +390 -0
  18. package/lib/templates/dashboard/app.js +320 -0
  19. package/lib/templates/dashboard/charts.js +305 -0
  20. package/lib/templates/dashboard/components.js +410 -0
  21. package/lib/templates/dashboard/i18n.js +340 -0
  22. package/lib/templates/dashboard/index.js +141 -0
  23. package/lib/templates/dashboard/layout.js +310 -0
  24. package/lib/templates/dashboard/pages.js +681 -0
  25. package/lib/templates/ecommerce/app.js +315 -0
  26. package/lib/templates/ecommerce/components.js +496 -0
  27. package/lib/templates/ecommerce/i18n.js +389 -0
  28. package/lib/templates/ecommerce/index.js +152 -0
  29. package/lib/templates/ecommerce/layout.js +270 -0
  30. package/lib/templates/ecommerce/pages.js +969 -0
  31. package/lib/templates/ecommerce/services.js +300 -0
  32. package/lib/templates/index.js +12 -0
  33. package/lib/templates/landing/index.js +1117 -0
  34. package/lib/templates/portfolio/index.js +1160 -0
  35. package/lib/templates/saas/index.js +1371 -0
  36. package/lib/templates/starter/app.js +364 -0
  37. package/lib/templates/starter/i18n.js +856 -0
  38. package/lib/templates/starter/index.js +53 -4055
  39. package/lib/templates/starter/layout.js +852 -0
  40. package/lib/templates/starter/pages.js +1241 -0
  41. package/package.json +1 -1
  42. package/lib/templates/starter/features.js +0 -867
  43. package/lib/utils/ai-config.js +0 -641
  44. /package/lib/templates/{starter → base}/advanced-features.js +0 -0
  45. /package/lib/templates/{starter → base}/seo-assets.js +0 -0
  46. /package/lib/templates/{starter → base}/seo-features.js +0 -0
  47. /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;