kamisaku 0.3.3 → 0.4.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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +17 -0
  3. data/Gemfile.lock +1 -1
  4. data/README.md +28 -64
  5. data/examples/birthday_invitation/dino/invitation.pdf +0 -0
  6. data/examples/resume/chromatic/example.pdf +0 -0
  7. data/examples/resume/gradient/example.pdf +0 -0
  8. data/examples/resume/meridian/example.pdf +0 -0
  9. data/examples/resume/paper/example.pdf +0 -0
  10. data/examples/resume/prism/example.pdf +0 -0
  11. data/examples/resume/sleek/example.pdf +0 -0
  12. data/examples/resume/zenith/example.pdf +0 -0
  13. data/lib/kamisaku/arg_parser.rb +4 -0
  14. data/lib/kamisaku/cli_runner.rb +2 -2
  15. data/lib/kamisaku/content_validators/base_content_validator.rb +14 -0
  16. data/lib/kamisaku/content_validators/birthday_invitation_content_validator.rb +218 -0
  17. data/lib/kamisaku/{content_validator.rb → content_validators/resume_content_validator.rb} +28 -11
  18. data/lib/kamisaku/html_builder.rb +4 -3
  19. data/lib/kamisaku/pdf.rb +17 -12
  20. data/lib/kamisaku/template_helpers.rb +0 -2
  21. data/lib/kamisaku/version.rb +1 -1
  22. data/lib/kamisaku.rb +4 -2
  23. data/lib/schema/birthday_invitation/example.yml +45 -0
  24. data/lib/schema/birthday_invitation/schema.yml +40 -0
  25. data/lib/schema/resume/example.yml +274 -0
  26. data/lib/schema/resume/schema.yml +112 -0
  27. data/lib/templates/birthday_invitation/dino/template.html.erb +486 -0
  28. data/lib/templates/resume/chromatic/template.html.erb +275 -0
  29. data/lib/templates/resume/gradient/template.html.erb +793 -0
  30. data/lib/templates/resume/meridian/template.html.erb +535 -0
  31. data/lib/templates/resume/paper/template.html.erb +525 -0
  32. data/lib/templates/resume/prism/template.html.erb +818 -0
  33. data/lib/templates/{sleek → resume/sleek}/template.html.erb +1 -1
  34. data/lib/templates/resume/zenith/template.html.erb +546 -0
  35. data/scripts/rebuild_examples.rb +45 -0
  36. metadata +25 -9
  37. data/examples/paper/john_doe.pdf +0 -0
  38. data/examples/paper/john_doe.yml +0 -157
  39. data/examples/sleek/john_doe.pdf +0 -0
  40. data/examples/sleek/john_doe.yml +0 -157
  41. data/lib/templates/paper/template.html.erb +0 -420
  42. data/template.yml +0 -64
@@ -0,0 +1,818 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title><%= data[:profile][:name] %> - Resume</title>
7
+ <style>
8
+ /* Import modern font */
9
+ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');
10
+
11
+ /* Base print and preview styles */
12
+ @page {
13
+ margin: 0;
14
+ size: A4;
15
+ }
16
+
17
+ @media screen {
18
+ body {
19
+ background: #f8fafc;
20
+ }
21
+ .paper {
22
+ padding: 0.5in;
23
+ box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
24
+ }
25
+ }
26
+
27
+ @media screen, print {
28
+ .paper {
29
+ background: white;
30
+ margin: 0 auto;
31
+ width: 210mm;
32
+ min-height: 297mm;
33
+ }
34
+ }
35
+
36
+ /* Modern typography and layout */
37
+ * {
38
+ margin: 0;
39
+ padding: 0;
40
+ box-sizing: border-box;
41
+ }
42
+
43
+ body {
44
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
45
+ font-size: 10pt;
46
+ line-height: 1.6;
47
+ color: #374151;
48
+ font-weight: 400;
49
+ }
50
+
51
+ .resume-container {
52
+ display: grid;
53
+ grid-template-columns: 280px 1fr;
54
+ min-height: 100vh;
55
+ position: relative;
56
+ }
57
+
58
+ /* Left sidebar */
59
+ .sidebar {
60
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
61
+ color: white;
62
+ padding: 40px 30px;
63
+ position: relative;
64
+ min-height: 100vh;
65
+ }
66
+
67
+ .sidebar::before {
68
+ content: '';
69
+ position: absolute;
70
+ top: 0;
71
+ left: 0;
72
+ right: 0;
73
+ bottom: 0;
74
+ background: rgba(0, 0, 0, 0.1);
75
+ pointer-events: none;
76
+ }
77
+
78
+ .sidebar > * {
79
+ position: relative;
80
+ z-index: 1;
81
+ }
82
+
83
+ /* Main content area */
84
+ .main-content {
85
+ padding: 40px 40px 40px 50px;
86
+ background: white;
87
+ }
88
+
89
+ /* Print-specific styles */
90
+ @media print {
91
+ * {
92
+ -webkit-print-color-adjust: exact !important;
93
+ color-adjust: exact !important;
94
+ print-color-adjust: exact !important;
95
+ }
96
+
97
+ .resume-container {
98
+ display: block;
99
+ position: relative;
100
+ }
101
+
102
+ .sidebar {
103
+ position: absolute;
104
+ left: 0;
105
+ top: 0;
106
+ width: 280px;
107
+ height: 297mm; /* Full A4 height */
108
+ min-height: 297mm;
109
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%) !important;
110
+ color: white;
111
+ float: none;
112
+ page-break-inside: avoid;
113
+ z-index: 3;
114
+ }
115
+
116
+ .sidebar::before {
117
+ background: rgba(0, 0, 0, 0.1) !important;
118
+ }
119
+
120
+ .main-content {
121
+ margin-left: 280px;
122
+ padding: 40px 40px 40px 50px;
123
+ background: white;
124
+ position: relative;
125
+ z-index: 2;
126
+ min-height: 297mm;
127
+ }
128
+
129
+ .paper {
130
+ width: 210mm;
131
+ min-height: 297mm;
132
+ position: relative;
133
+ overflow: hidden;
134
+ }
135
+
136
+ /* Create repeating background for subsequent pages */
137
+ .main-content::before {
138
+ content: '';
139
+ position: fixed;
140
+ left: 0;
141
+ top: 0;
142
+ width: 280px;
143
+ height: 100vh;
144
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
145
+ z-index: -1;
146
+ }
147
+
148
+ .main-content::after {
149
+ content: '';
150
+ position: fixed;
151
+ left: 0;
152
+ top: 0;
153
+ width: 280px;
154
+ height: 100vh;
155
+ background: rgba(0, 0, 0, 0.1);
156
+ z-index: -1;
157
+ }
158
+ }
159
+
160
+ /* Header section in sidebar */
161
+ .profile-section {
162
+ text-align: center;
163
+ margin-bottom: 40px;
164
+ padding-bottom: 30px;
165
+ border-bottom: 2px solid rgba(255, 255, 255, 0.2);
166
+ }
167
+
168
+ .profile-name {
169
+ font-size: 24pt;
170
+ font-weight: 700;
171
+ margin-bottom: 8px;
172
+ letter-spacing: -0.5px;
173
+ }
174
+
175
+ .profile-title {
176
+ font-size: 12pt;
177
+ font-weight: 400;
178
+ opacity: 0.9;
179
+ margin-bottom: 20px;
180
+ }
181
+
182
+ .contact-info {
183
+ text-align: left;
184
+ }
185
+
186
+ .contact-item {
187
+ display: flex;
188
+ align-items: center;
189
+ margin-bottom: 12px;
190
+ font-size: 9pt;
191
+ opacity: 0.95;
192
+ }
193
+
194
+ .contact-icon {
195
+ width: 16px;
196
+ height: 16px;
197
+ margin-right: 12px;
198
+ opacity: 0.8;
199
+ }
200
+
201
+ /* Sidebar sections */
202
+ .sidebar-section {
203
+ margin-bottom: 35px;
204
+ }
205
+
206
+ .sidebar-title {
207
+ font-size: 12pt;
208
+ font-weight: 600;
209
+ margin-bottom: 15px;
210
+ text-transform: uppercase;
211
+ letter-spacing: 1px;
212
+ opacity: 0.9;
213
+ }
214
+
215
+ /* Skills in sidebar */
216
+ .skill-category {
217
+ margin-bottom: 20px;
218
+ }
219
+
220
+ .skill-name {
221
+ font-size: 10pt;
222
+ font-weight: 500;
223
+ margin-bottom: 8px;
224
+ }
225
+
226
+ .skill-items {
227
+ font-size: 9pt;
228
+ opacity: 0.85;
229
+ line-height: 1.5;
230
+ }
231
+
232
+ .skill-tag {
233
+ display: inline-block;
234
+ background: rgba(255, 255, 255, 0.2);
235
+ padding: 4px 8px;
236
+ border-radius: 12px;
237
+ margin: 2px 4px 2px 0;
238
+ font-size: 8pt;
239
+ }
240
+
241
+ /* Languages and interests */
242
+ .sidebar-list {
243
+ font-size: 9pt;
244
+ opacity: 0.9;
245
+ }
246
+
247
+ .sidebar-list-item {
248
+ margin-bottom: 6px;
249
+ padding-left: 12px;
250
+ position: relative;
251
+ }
252
+
253
+ .sidebar-list-item::before {
254
+ content: '•';
255
+ position: absolute;
256
+ left: 0;
257
+ opacity: 0.7;
258
+ }
259
+
260
+ /* Main content sections */
261
+ .content-section {
262
+ margin-bottom: 40px;
263
+ }
264
+
265
+ .section-title {
266
+ font-size: 16pt;
267
+ font-weight: 600;
268
+ color: #1f2937;
269
+ margin-bottom: 20px;
270
+ position: relative;
271
+ padding-bottom: 8px;
272
+ }
273
+
274
+ .section-title::after {
275
+ content: '';
276
+ position: absolute;
277
+ bottom: 0;
278
+ left: 0;
279
+ width: 40px;
280
+ height: 3px;
281
+ background: linear-gradient(90deg, #667eea, #764ba2);
282
+ border-radius: 2px;
283
+ }
284
+
285
+ /* Professional summary */
286
+ .professional-summary {
287
+ font-size: 11pt;
288
+ line-height: 1.7;
289
+ color: #4b5563;
290
+ text-align: justify;
291
+ }
292
+
293
+ /* Experience items */
294
+ .experience-item {
295
+ margin-bottom: 30px;
296
+ position: relative;
297
+ padding-left: 20px;
298
+ }
299
+
300
+ .experience-item::before {
301
+ content: '';
302
+ position: absolute;
303
+ left: 0;
304
+ top: 8px;
305
+ width: 8px;
306
+ height: 8px;
307
+ background: linear-gradient(45deg, #667eea, #764ba2);
308
+ border-radius: 50%;
309
+ }
310
+
311
+ .job-header {
312
+ margin-bottom: 8px;
313
+ }
314
+
315
+ .job-title {
316
+ font-size: 13pt;
317
+ font-weight: 600;
318
+ color: #1f2937;
319
+ margin-bottom: 4px;
320
+ }
321
+
322
+ .company-info {
323
+ display: flex;
324
+ justify-content: space-between;
325
+ align-items: center;
326
+ margin-bottom: 8px;
327
+ flex-wrap: wrap;
328
+ gap: 10px;
329
+ }
330
+
331
+ .company-name {
332
+ font-weight: 500;
333
+ color: #667eea;
334
+ font-size: 10pt;
335
+ }
336
+
337
+ .job-location {
338
+ font-size: 9pt;
339
+ color: #6b7280;
340
+ }
341
+
342
+ .job-duration {
343
+ font-size: 9pt;
344
+ color: #6b7280;
345
+ font-weight: 500;
346
+ }
347
+
348
+ .job-skills {
349
+ margin-bottom: 12px;
350
+ }
351
+
352
+ .skills-label {
353
+ font-size: 9pt;
354
+ font-weight: 500;
355
+ color: #374151;
356
+ margin-bottom: 4px;
357
+ }
358
+
359
+ .job-skills-list {
360
+ display: flex;
361
+ flex-wrap: wrap;
362
+ gap: 6px;
363
+ }
364
+
365
+ .job-skill-tag {
366
+ background: #f3f4f6;
367
+ color: #374151;
368
+ padding: 3px 8px;
369
+ border-radius: 8px;
370
+ font-size: 8pt;
371
+ font-weight: 400;
372
+ }
373
+
374
+ .achievements-list {
375
+ list-style: none;
376
+ padding: 0;
377
+ }
378
+
379
+ .achievement-item {
380
+ margin-bottom: 8px;
381
+ padding-left: 15px;
382
+ position: relative;
383
+ font-size: 10pt;
384
+ line-height: 1.6;
385
+ color: #4b5563;
386
+ }
387
+
388
+ .achievement-item::before {
389
+ content: '▸';
390
+ position: absolute;
391
+ left: 0;
392
+ color: #667eea;
393
+ font-weight: bold;
394
+ }
395
+
396
+ /* Education */
397
+ .education-item {
398
+ margin-bottom: 25px;
399
+ padding: 20px;
400
+ background: #f8fafc;
401
+ border-radius: 12px;
402
+ border-left: 4px solid #667eea;
403
+ }
404
+
405
+ .education-header {
406
+ margin-bottom: 8px;
407
+ }
408
+
409
+ .degree-title {
410
+ font-size: 12pt;
411
+ font-weight: 600;
412
+ color: #1f2937;
413
+ margin-bottom: 4px;
414
+ }
415
+
416
+ .institution-name {
417
+ font-weight: 500;
418
+ color: #667eea;
419
+ margin-bottom: 4px;
420
+ }
421
+
422
+ .education-meta {
423
+ display: flex;
424
+ justify-content: space-between;
425
+ font-size: 9pt;
426
+ color: #6b7280;
427
+ margin-bottom: 10px;
428
+ }
429
+
430
+ /* Projects */
431
+ .project-item {
432
+ margin-bottom: 25px;
433
+ padding: 20px;
434
+ border: 1px solid #e5e7eb;
435
+ border-radius: 12px;
436
+ transition: all 0.2s ease;
437
+ }
438
+
439
+ .project-item:hover {
440
+ border-color: #667eea;
441
+ box-shadow: 0 4px 12px rgba(102, 126, 234, 0.1);
442
+ }
443
+
444
+ .project-header {
445
+ display: flex;
446
+ justify-content: space-between;
447
+ align-items: flex-start;
448
+ margin-bottom: 8px;
449
+ }
450
+
451
+ .project-name {
452
+ font-size: 12pt;
453
+ font-weight: 600;
454
+ color: #1f2937;
455
+ }
456
+
457
+ .project-link {
458
+ font-size: 8pt;
459
+ color: #667eea;
460
+ text-decoration: none;
461
+ }
462
+
463
+ .project-description {
464
+ font-size: 10pt;
465
+ color: #4b5563;
466
+ margin-bottom: 10px;
467
+ line-height: 1.6;
468
+ }
469
+
470
+ .project-tech {
471
+ display: flex;
472
+ flex-wrap: wrap;
473
+ gap: 6px;
474
+ }
475
+
476
+ .tech-tag {
477
+ background: #f0f4ff;
478
+ color: #667eea;
479
+ padding: 4px 8px;
480
+ border-radius: 8px;
481
+ font-size: 8pt;
482
+ font-weight: 500;
483
+ }
484
+
485
+ /* Certifications and Awards */
486
+ .cert-award-item {
487
+ display: flex;
488
+ justify-content: space-between;
489
+ align-items: flex-start;
490
+ margin-bottom: 15px;
491
+ padding: 15px;
492
+ background: #fefefe;
493
+ border-radius: 8px;
494
+ border-left: 3px solid #10b981;
495
+ }
496
+
497
+ .cert-award-info {
498
+ flex: 1;
499
+ }
500
+
501
+ .cert-award-name {
502
+ font-weight: 600;
503
+ color: #1f2937;
504
+ margin-bottom: 4px;
505
+ }
506
+
507
+ .cert-award-issuer {
508
+ font-size: 9pt;
509
+ color: #6b7280;
510
+ }
511
+
512
+ .cert-award-date {
513
+ font-size: 9pt;
514
+ color: #6b7280;
515
+ font-weight: 500;
516
+ text-align: right;
517
+ margin-left: 15px;
518
+ }
519
+
520
+ /* Responsive adjustments */
521
+ @media screen and (max-width: 768px) {
522
+ .resume-container {
523
+ grid-template-columns: 1fr;
524
+ }
525
+
526
+ .sidebar {
527
+ padding: 30px 20px;
528
+ }
529
+
530
+ .main-content {
531
+ padding: 30px 20px;
532
+ }
533
+ }
534
+
535
+ /* Print optimizations */
536
+ @media print {
537
+ .experience-item, .project-item, .education-item, .cert-award-item {
538
+ break-inside: avoid;
539
+ }
540
+ }
541
+ </style>
542
+ </head>
543
+ <body>
544
+ <!-- Rest of the HTML stays exactly the same -->
545
+ <div class="paper">
546
+ <div class="resume-container">
547
+ <!-- Sidebar -->
548
+ <aside class="sidebar">
549
+ <!-- Profile Section -->
550
+ <div class="profile-section">
551
+ <h1 class="profile-name"><%= data[:profile][:name] %></h1>
552
+ <% if data[:profile][:title] %>
553
+ <div class="profile-title"><%= data[:profile][:title] %></div>
554
+ <% end %>
555
+
556
+ <% if data[:contact] %>
557
+ <div class="contact-info">
558
+ <% if data[:contact][:email] %>
559
+ <div class="contact-item">
560
+ <svg class="contact-icon" fill="currentColor" viewBox="0 0 20 20">
561
+ <path d="M2.003 5.884L10 9.882l7.997-3.998A2 2 0 0016 4H4a2 2 0 00-1.997 1.884z"></path>
562
+ <path d="M18 8.118l-8 4-8-4V14a2 2 0 002 2h12a2 2 0 002-2V8.118z"></path>
563
+ </svg>
564
+ <%= data[:contact][:email] %>
565
+ </div>
566
+ <% end %>
567
+ <% if data[:contact][:mobile] %>
568
+ <div class="contact-item">
569
+ <svg class="contact-icon" fill="currentColor" viewBox="0 0 20 20">
570
+ <path d="M2 3a1 1 0 011-1h2.153a1 1 0 01.986.836l.74 4.435a1 1 0 01-.54 1.06l-1.548.773a11.037 11.037 0 006.105 6.105l.774-1.548a1 1 0 011.059-.54l4.435.74a1 1 0 01.836.986V17a1 1 0 01-1 1h-2C7.82 18 2 12.18 2 5V3z"></path>
571
+ </svg>
572
+ <%= data[:contact][:mobile] %>
573
+ </div>
574
+ <% end %>
575
+ <% if data[:contact][:location] && (data[:contact][:location][:city] || data[:contact][:location][:country]) %>
576
+ <div class="contact-item">
577
+ <svg class="contact-icon" fill="currentColor" viewBox="0 0 20 20">
578
+ <path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z" clip-rule="evenodd"></path>
579
+ </svg>
580
+ <%= [data[:contact][:location][:city], data[:contact][:location][:country]].compact.join(', ') %>
581
+ </div>
582
+ <% end %>
583
+ <% if data[:contact][:linkedin] %>
584
+ <div class="contact-item">
585
+ <svg class="contact-icon" fill="currentColor" viewBox="0 0 20 20">
586
+ <path fill-rule="evenodd" d="M16.338 16.338H13.67V12.16c0-.995-.017-2.277-1.387-2.277-1.39 0-1.601 1.086-1.601 2.207v4.248H8.014v-8.59h2.559v1.174h.037c.356-.675 1.227-1.387 2.526-1.387 2.703 0 3.203 1.778 3.203 4.092v4.711zM5.005 6.575a1.548 1.548 0 11-.003-3.096 1.548 1.548 0 01.003 3.096zm-1.337 9.763H6.34v-8.59H3.667v8.59zM17.668 1H2.328C1.595 1 1 1.581 1 2.298v15.403C1 18.418 1.595 19 2.328 19h15.34c.734 0 1.332-.582 1.332-1.299V2.298C19 1.581 18.402 1 17.668 1z" clip-rule="evenodd"></path>
587
+ </svg>
588
+ <%= data[:contact][:linkedin] %>
589
+ </div>
590
+ <% end %>
591
+ <% if data[:contact][:github] %>
592
+ <div class="contact-item">
593
+ <svg class="contact-icon" fill="currentColor" viewBox="0 0 20 20">
594
+ <path fill-rule="evenodd" d="M10 0C4.477 0 0 4.484 0 10.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0110 4.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.203 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.942.359.31.678.921.678 1.856 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0020 10.017C20 4.484 15.522 0 10 0z" clip-rule="evenodd"></path>
595
+ </svg>
596
+ <%= data[:contact][:github] %>
597
+ </div>
598
+ <% end %>
599
+ </div>
600
+ <% end %>
601
+ </div>
602
+
603
+ <!-- Skills Section -->
604
+ <% if data[:skills] && !data[:skills].empty? %>
605
+ <div class="sidebar-section">
606
+ <h3 class="sidebar-title">Skills</h3>
607
+ <% data[:skills].each do |skill| %>
608
+ <div class="skill-category">
609
+ <div class="skill-name"><%= skill[:name] %></div>
610
+ <% if skill[:items] && !skill[:items].empty? %>
611
+ <div class="skill-items">
612
+ <% skill[:items].each do |item| %>
613
+ <span class="skill-tag"><%= item %></span>
614
+ <% end %>
615
+ </div>
616
+ <% end %>
617
+ </div>
618
+ <% end %>
619
+ </div>
620
+ <% end %>
621
+
622
+ <!-- Languages -->
623
+ <% if data[:languages] && !data[:languages].empty? %>
624
+ <div class="sidebar-section">
625
+ <h3 class="sidebar-title">Languages</h3>
626
+ <div class="sidebar-list">
627
+ <% data[:languages].each do |lang| %>
628
+ <div class="sidebar-list-item"><%= lang[:name] %></div>
629
+ <% end %>
630
+ </div>
631
+ </div>
632
+ <% end %>
633
+
634
+ <!-- Interests -->
635
+ <% if data[:interests] && !data[:interests].empty? %>
636
+ <div class="sidebar-section">
637
+ <h3 class="sidebar-title">Interests</h3>
638
+ <div class="sidebar-list">
639
+ <% data[:interests].each do |interest| %>
640
+ <div class="sidebar-list-item"><%= interest[:name] %></div>
641
+ <% end %>
642
+ </div>
643
+ </div>
644
+ <% end %>
645
+ </aside>
646
+
647
+ <!-- Main Content -->
648
+ <main class="main-content">
649
+ <!-- Professional Summary -->
650
+ <% if data[:profile][:about] %>
651
+ <section class="content-section">
652
+ <h2 class="section-title">About</h2>
653
+ <div class="professional-summary">
654
+ <%= data[:profile][:about] %>
655
+ </div>
656
+ </section>
657
+ <% end %>
658
+
659
+ <!-- Experience -->
660
+ <% if data[:experiences] && !data[:experiences].empty? %>
661
+ <section class="content-section">
662
+ <h2 class="section-title">Experience</h2>
663
+ <% data[:experiences].each do |exp| %>
664
+ <div class="experience-item">
665
+ <div class="job-header">
666
+ <h3 class="job-title"><%= exp[:title] %></h3>
667
+ <div class="company-info">
668
+ <div class="company-name"><%= exp[:organisation] %></div>
669
+ <div class="job-location">
670
+ <% if exp[:location] && (exp[:location][:city] || exp[:location][:country]) %>
671
+ <%= [exp[:location][:city], exp[:location][:country]].compact.join(', ') %>
672
+ <% end %>
673
+ </div>
674
+ <div class="job-duration">
675
+ <%= exp[:from][:month] %>/<%= exp[:from][:year] %> -
676
+ <%= exp[:to] ? "#{exp[:to][:month]}/#{exp[:to][:year]}" : "Present" %>
677
+ </div>
678
+ </div>
679
+ </div>
680
+
681
+ <% if exp[:skills] && !exp[:skills].empty? %>
682
+ <div class="job-skills">
683
+ <div class="skills-label">Key Technologies</div>
684
+ <div class="job-skills-list">
685
+ <% exp[:skills].each do |skill| %>
686
+ <span class="job-skill-tag"><%= skill %></span>
687
+ <% end %>
688
+ </div>
689
+ </div>
690
+ <% end %>
691
+
692
+ <% if exp[:achievements] && !exp[:achievements].empty? %>
693
+ <ul class="achievements-list">
694
+ <% exp[:achievements].each do |achievement| %>
695
+ <li class="achievement-item"><%= achievement %></li>
696
+ <% end %>
697
+ </ul>
698
+ <% end %>
699
+ </div>
700
+ <% end %>
701
+ </section>
702
+ <% end %>
703
+
704
+ <!-- Projects -->
705
+ <% if data[:projects] && !data[:projects].empty? %>
706
+ <section class="content-section">
707
+ <h2 class="section-title">Projects</h2>
708
+ <% data[:projects].each do |project| %>
709
+ <div class="project-item">
710
+ <div class="project-header">
711
+ <h3 class="project-name"><%= project[:name] %></h3>
712
+ <% if project[:link] %>
713
+ <a href="<%= project[:link] %>" class="project-link" target="_blank">View Project</a>
714
+ <% end %>
715
+ </div>
716
+ <% if project[:description] %>
717
+ <div class="project-description"><%= project[:description] %></div>
718
+ <% end %>
719
+ <% if project[:technologies] && !project[:technologies].empty? %>
720
+ <div class="project-tech">
721
+ <% project[:technologies].each do |tech| %>
722
+ <span class="tech-tag"><%= tech %></span>
723
+ <% end %>
724
+ </div>
725
+ <% end %>
726
+ </div>
727
+ <% end %>
728
+ </section>
729
+ <% end %>
730
+
731
+ <!-- Education -->
732
+ <% if data[:education] && !data[:education].empty? %>
733
+ <section class="content-section">
734
+ <h2 class="section-title">Education</h2>
735
+ <% data[:education].each do |edu| %>
736
+ <div class="education-item">
737
+ <div class="education-header">
738
+ <h3 class="degree-title">
739
+ <%= edu[:qualification] %><% if edu[:field] %> in <%= edu[:field] %><% end %>
740
+ </h3>
741
+ <div class="institution-name"><%= edu[:institute] %></div>
742
+ <div class="education-meta">
743
+ <div>
744
+ <% if edu[:location] && (edu[:location][:city] || edu[:location][:country]) %>
745
+ <%= [edu[:location][:city], edu[:location][:country]].compact.join(', ') %>
746
+ <% end %>
747
+ </div>
748
+ <% if edu[:from] %>
749
+ <div>
750
+ <%= edu[:from][:month] %>/<%= edu[:from][:year] %><% if edu[:to] %> - <%= edu[:to][:month] %>/<%= edu[:to][:year] %><% end %>
751
+ </div>
752
+ <% end %>
753
+ </div>
754
+ </div>
755
+
756
+ <% if edu[:achievements] && !edu[:achievements].empty? %>
757
+ <ul class="achievements-list">
758
+ <% edu[:achievements].each do |achievement| %>
759
+ <li class="achievement-item"><%= achievement %></li>
760
+ <% end %>
761
+ </ul>
762
+ <% end %>
763
+ </div>
764
+ <% end %>
765
+ </section>
766
+ <% end %>
767
+
768
+ <!-- Certifications -->
769
+ <% if data[:certifications] && !data[:certifications].empty? %>
770
+ <section class="content-section">
771
+ <h2 class="section-title">Certifications</h2>
772
+ <% data[:certifications].each do |cert| %>
773
+ <div class="cert-award-item">
774
+ <div class="cert-award-info">
775
+ <div class="cert-award-name"><%= cert[:name] %></div>
776
+ <% if cert[:issuer] %>
777
+ <div class="cert-award-issuer"><%= cert[:issuer] %></div>
778
+ <% end %>
779
+ </div>
780
+ <% if cert[:date] %>
781
+ <div class="cert-award-date">
782
+ <%= cert[:date][:month] %>/<%= cert[:date][:year] %>
783
+ </div>
784
+ <% end %>
785
+ </div>
786
+ <% end %>
787
+ </section>
788
+ <% end %>
789
+
790
+ <!-- Awards -->
791
+ <% if data[:awards] && !data[:awards].empty? %>
792
+ <section class="content-section">
793
+ <h2 class="section-title">Awards</h2>
794
+ <% data[:awards].each do |award| %>
795
+ <div class="cert-award-item">
796
+ <div class="cert-award-info">
797
+ <div class="cert-award-name"><%= award[:title] %></div>
798
+ <% if award[:issuer] %>
799
+ <div class="cert-award-issuer"><%= award[:issuer] %></div>
800
+ <% end %>
801
+ <% if award[:description] %>
802
+ <div class="cert-award-issuer"><%= award[:description] %></div>
803
+ <% end %>
804
+ </div>
805
+ <% if award[:date] %>
806
+ <div class="cert-award-date">
807
+ <%= award[:date][:month] %>/<%= award[:date][:year] %>
808
+ </div>
809
+ <% end %>
810
+ </div>
811
+ <% end %>
812
+ </section>
813
+ <% end %>
814
+ </main>
815
+ </div>
816
+ </div>
817
+ </body>
818
+ </html>