jsonresume-theme-berlin-grid 0.1.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 (3) hide show
  1. package/index.js +23 -0
  2. package/package.json +14 -0
  3. package/src/Resume.jsx +544 -0
package/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import { renderToString } from 'react-dom/server';
2
+ import { ServerStyleSheet } from 'styled-components';
3
+ import Resume from './src/Resume.jsx';
4
+
5
+ export function render(resume) {
6
+ const sheet = new ServerStyleSheet();
7
+ const html = renderToString(sheet.collectStyles(<Resume resume={resume} />));
8
+ const styles = sheet.getStyleTags();
9
+ const title = (resume.basics && resume.basics.name) || 'Resume';
10
+
11
+ return `<!DOCTYPE html>
12
+ <html lang="en">
13
+ <head>
14
+ <meta charset="utf-8">
15
+ <title>${title}</title>
16
+ <meta name="viewport" content="width=device-width, initial-scale=1">
17
+ <link rel="preconnect" href="https://fonts.googleapis.com">
18
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600;700&display=swap" rel="stylesheet">
19
+ ${styles}
20
+ </head>
21
+ <body>${html}</body>
22
+ </html>`;
23
+ }
package/package.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "jsonresume-theme-berlin-grid",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "main": "index.js",
6
+ "peerDependencies": {
7
+ "react": "^18.0.0 || ^19.0.0",
8
+ "react-dom": "^18.0.0 || ^19.0.0"
9
+ },
10
+ "dependencies": {
11
+ "styled-components": "^6.1.19",
12
+ "@resume/core": "0.1.0"
13
+ }
14
+ }
package/src/Resume.jsx ADDED
@@ -0,0 +1,544 @@
1
+ import React from 'react';
2
+ import styled from 'styled-components';
3
+ import {
4
+ Section,
5
+ SectionTitle,
6
+ ListItem,
7
+ DateRange,
8
+ Badge,
9
+ BadgeList,
10
+ ContactInfo,
11
+ Link,
12
+ safeUrl,
13
+ } from '@resume/core';
14
+
15
+ const GRID_SIZE = 8;
16
+
17
+ const Layout = styled.div`
18
+ max-width: 880px;
19
+ margin: 0 auto;
20
+ padding: ${GRID_SIZE * 10}px ${GRID_SIZE * 6}px;
21
+ background: #ffffff;
22
+ font-family: 'IBM Plex Sans', -apple-system, BlinkMacSystemFont, sans-serif;
23
+ color: #1a1a1a;
24
+ line-height: ${GRID_SIZE * 3}px;
25
+ font-size: 14px;
26
+
27
+ @media print {
28
+ padding: ${GRID_SIZE * 6}px ${GRID_SIZE * 4}px;
29
+ }
30
+ `;
31
+
32
+ const GridContainer = styled.div`
33
+ display: grid;
34
+ grid-template-columns: repeat(12, 1fr);
35
+ gap: ${GRID_SIZE * 3}px;
36
+ margin-bottom: ${GRID_SIZE * 6}px;
37
+ `;
38
+
39
+ const Header = styled.header`
40
+ grid-column: 1 / -1;
41
+ padding-bottom: ${GRID_SIZE * 4}px;
42
+ border-bottom: 3px solid #1a1a1a;
43
+ margin-bottom: ${GRID_SIZE * 8}px;
44
+ `;
45
+
46
+ const Name = styled.h1`
47
+ font-size: 44px;
48
+ font-weight: 700;
49
+ margin: 0 0 ${GRID_SIZE * 2}px 0;
50
+ letter-spacing: -0.5px;
51
+ line-height: ${GRID_SIZE * 7}px;
52
+ text-transform: uppercase;
53
+ `;
54
+
55
+ const Tagline = styled.div`
56
+ font-size: 16px;
57
+ color: #4a4a4a;
58
+ margin: ${GRID_SIZE}px 0 ${GRID_SIZE * 3}px 0;
59
+ font-weight: 500;
60
+ letter-spacing: 0.3px;
61
+ `;
62
+
63
+ const StyledContactInfo = styled(ContactInfo)`
64
+ display: flex;
65
+ flex-wrap: wrap;
66
+ gap: ${GRID_SIZE * 3}px;
67
+ margin-top: ${GRID_SIZE * 2}px;
68
+
69
+ a {
70
+ color: #1a1a1a;
71
+ text-decoration: none;
72
+ font-size: 13px;
73
+ position: relative;
74
+ padding-bottom: 2px;
75
+
76
+ &::after {
77
+ content: '';
78
+ position: absolute;
79
+ bottom: 0;
80
+ left: 0;
81
+ width: 0;
82
+ height: 1px;
83
+ background: #1a1a1a;
84
+ transition: width 0.2s ease;
85
+ }
86
+
87
+ &:hover::after {
88
+ width: 100%;
89
+ }
90
+ }
91
+ `;
92
+
93
+ const Summary = styled.p`
94
+ grid-column: 1 / -1;
95
+ font-size: 15px;
96
+ line-height: ${GRID_SIZE * 3}px;
97
+ color: #2a2a2a;
98
+ margin: 0 0 ${GRID_SIZE * 8}px 0;
99
+ padding: ${GRID_SIZE * 3}px;
100
+ border-left: 4px solid #e5e5e5;
101
+ background: #fafafa;
102
+ `;
103
+
104
+ const StyledSection = styled(Section)`
105
+ grid-column: 1 / -1;
106
+ margin-bottom: ${GRID_SIZE * 8}px;
107
+ `;
108
+
109
+ const StyledSectionTitle = styled(SectionTitle)`
110
+ font-size: 20px;
111
+ font-weight: 700;
112
+ text-transform: uppercase;
113
+ letter-spacing: 1.5px;
114
+ color: #1a1a1a;
115
+ margin: 0 0 ${GRID_SIZE * 4}px 0;
116
+ padding-bottom: ${GRID_SIZE}px;
117
+ border-bottom: 2px solid #1a1a1a;
118
+ `;
119
+
120
+ const GridItem = styled.div`
121
+ display: grid;
122
+ grid-template-columns: 200px 1fr;
123
+ gap: ${GRID_SIZE * 4}px;
124
+ margin-bottom: ${GRID_SIZE * 5}px;
125
+ padding-bottom: ${GRID_SIZE * 5}px;
126
+ border-bottom: 1px solid #e5e5e5;
127
+
128
+ &:last-child {
129
+ border-bottom: none;
130
+ }
131
+
132
+ @media (max-width: 768px) {
133
+ grid-template-columns: 1fr;
134
+ gap: ${GRID_SIZE * 2}px;
135
+ }
136
+ `;
137
+
138
+ const MetaColumn = styled.div`
139
+ display: flex;
140
+ flex-direction: column;
141
+ gap: ${GRID_SIZE}px;
142
+ padding-right: ${GRID_SIZE * 2}px;
143
+ border-right: 2px solid #e5e5e5;
144
+
145
+ @media (max-width: 768px) {
146
+ border-right: none;
147
+ border-bottom: 2px solid #e5e5e5;
148
+ padding-right: 0;
149
+ padding-bottom: ${GRID_SIZE * 2}px;
150
+ }
151
+ `;
152
+
153
+ const ContentColumn = styled.div`
154
+ display: flex;
155
+ flex-direction: column;
156
+ gap: ${GRID_SIZE * 2}px;
157
+ `;
158
+
159
+ const MetaDate = styled.div`
160
+ font-size: 13px;
161
+ font-weight: 600;
162
+ color: #4a4a4a;
163
+ font-variant-numeric: tabular-nums;
164
+ `;
165
+
166
+ const MetaLocation = styled.div`
167
+ font-size: 12px;
168
+ color: #6a6a6a;
169
+ text-transform: uppercase;
170
+ letter-spacing: 0.5px;
171
+ `;
172
+
173
+ const ItemTitle = styled.h3`
174
+ font-size: 18px;
175
+ font-weight: 700;
176
+ margin: 0;
177
+ color: #1a1a1a;
178
+ letter-spacing: -0.2px;
179
+ `;
180
+
181
+ const ItemSubtitle = styled.div`
182
+ font-size: 15px;
183
+ font-weight: 500;
184
+ color: #4a4a4a;
185
+ margin-top: ${GRID_SIZE / 2}px;
186
+ `;
187
+
188
+ const ItemDescription = styled.p`
189
+ font-size: 14px;
190
+ line-height: ${GRID_SIZE * 3}px;
191
+ color: #2a2a2a;
192
+ margin: ${GRID_SIZE * 2}px 0 0 0;
193
+ `;
194
+
195
+ const HighlightsList = styled.ul`
196
+ margin: ${GRID_SIZE * 2}px 0 0 0;
197
+ padding-left: ${GRID_SIZE * 3}px;
198
+ list-style: none;
199
+
200
+ li {
201
+ position: relative;
202
+ margin-bottom: ${GRID_SIZE}px;
203
+ font-size: 14px;
204
+ line-height: ${GRID_SIZE * 3}px;
205
+
206
+ &::before {
207
+ content: '■';
208
+ position: absolute;
209
+ left: -${GRID_SIZE * 3}px;
210
+ color: #1a1a1a;
211
+ font-size: 10px;
212
+ }
213
+ }
214
+ `;
215
+
216
+ const SkillsGrid = styled.div`
217
+ display: grid;
218
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
219
+ gap: ${GRID_SIZE * 4}px;
220
+ `;
221
+
222
+ const SkillCategory = styled.div`
223
+ padding: ${GRID_SIZE * 2}px;
224
+ border: 2px solid #e5e5e5;
225
+ background: #fafafa;
226
+
227
+ h4 {
228
+ font-size: 14px;
229
+ font-weight: 700;
230
+ text-transform: uppercase;
231
+ letter-spacing: 1px;
232
+ margin: 0 0 ${GRID_SIZE * 2}px 0;
233
+ color: #1a1a1a;
234
+ }
235
+ `;
236
+
237
+ const StyledBadgeList = styled(BadgeList)`
238
+ display: flex;
239
+ flex-wrap: wrap;
240
+ gap: ${GRID_SIZE}px;
241
+ `;
242
+
243
+ const StyledBadge = styled(Badge)`
244
+ font-size: 12px;
245
+ padding: ${GRID_SIZE / 2}px ${GRID_SIZE * 1.5}px;
246
+ background: #ffffff;
247
+ border: 1px solid #d0d0d0;
248
+ color: #2a2a2a;
249
+ font-weight: 500;
250
+ letter-spacing: 0.3px;
251
+ `;
252
+
253
+ const SimpleList = styled.div`
254
+ display: flex;
255
+ flex-direction: column;
256
+ gap: ${GRID_SIZE * 3}px;
257
+ `;
258
+
259
+ const SimpleItem = styled.div`
260
+ padding: ${GRID_SIZE * 2}px;
261
+ border-left: 3px solid #e5e5e5;
262
+ background: #fafafa;
263
+
264
+ h4 {
265
+ font-size: 16px;
266
+ font-weight: 600;
267
+ margin: 0 0 ${GRID_SIZE}px 0;
268
+ }
269
+
270
+ p {
271
+ font-size: 14px;
272
+ margin: 0;
273
+ color: #4a4a4a;
274
+ }
275
+ `;
276
+
277
+ function Resume({ resume }) {
278
+ const {
279
+ basics = {},
280
+ work = [],
281
+ education = [],
282
+ skills = [],
283
+ projects = [],
284
+ volunteer = [],
285
+ awards = [],
286
+ publications = [],
287
+ languages = [],
288
+ interests = [],
289
+ references = [],
290
+ } = resume;
291
+
292
+ return (
293
+ <Layout>
294
+ <Header>
295
+ {basics.name && <Name>{basics.name}</Name>}
296
+ {basics.label && <Tagline>{basics.label}</Tagline>}
297
+ {basics.summary && <Summary>{basics.summary}</Summary>}
298
+ <StyledContactInfo
299
+ email={basics.email}
300
+ phone={basics.phone}
301
+ url={basics.url}
302
+ location={basics.location}
303
+ profiles={basics.profiles}
304
+ />
305
+ </Header>
306
+
307
+ {work.length > 0 && (
308
+ <StyledSection>
309
+ <StyledSectionTitle>Experience</StyledSectionTitle>
310
+ {work.map((job, index) => (
311
+ <GridItem key={index}>
312
+ <MetaColumn>
313
+ <MetaDate>
314
+ <DateRange startDate={job.startDate} endDate={job.endDate} />
315
+ </MetaDate>
316
+ {job.location && <MetaLocation>{job.location}</MetaLocation>}
317
+ </MetaColumn>
318
+ <ContentColumn>
319
+ <ItemTitle>{job.position || job.name}</ItemTitle>
320
+ {job.name && <ItemSubtitle>{job.name}</ItemSubtitle>}
321
+ {job.summary && (
322
+ <ItemDescription>{job.summary}</ItemDescription>
323
+ )}
324
+ {job.highlights && job.highlights.length > 0 && (
325
+ <HighlightsList>
326
+ {job.highlights.map((highlight, i) => (
327
+ <li key={i}>{highlight}</li>
328
+ ))}
329
+ </HighlightsList>
330
+ )}
331
+ </ContentColumn>
332
+ </GridItem>
333
+ ))}
334
+ </StyledSection>
335
+ )}
336
+
337
+ {education.length > 0 && (
338
+ <StyledSection>
339
+ <StyledSectionTitle>Education</StyledSectionTitle>
340
+ {education.map((edu, index) => (
341
+ <GridItem key={index}>
342
+ <MetaColumn>
343
+ <MetaDate>
344
+ <DateRange startDate={edu.startDate} endDate={edu.endDate} />
345
+ </MetaDate>
346
+ {edu.area && <MetaLocation>{edu.area}</MetaLocation>}
347
+ </MetaColumn>
348
+ <ContentColumn>
349
+ <ItemTitle>{edu.institution}</ItemTitle>
350
+ {edu.studyType && edu.area && (
351
+ <ItemSubtitle>
352
+ {edu.studyType} in {edu.area}
353
+ </ItemSubtitle>
354
+ )}
355
+ {edu.score && (
356
+ <ItemDescription>GPA: {edu.score}</ItemDescription>
357
+ )}
358
+ {edu.courses && edu.courses.length > 0 && (
359
+ <HighlightsList>
360
+ {edu.courses.map((course, i) => (
361
+ <li key={i}>{course}</li>
362
+ ))}
363
+ </HighlightsList>
364
+ )}
365
+ </ContentColumn>
366
+ </GridItem>
367
+ ))}
368
+ </StyledSection>
369
+ )}
370
+
371
+ {skills.length > 0 && (
372
+ <StyledSection>
373
+ <StyledSectionTitle>Skills</StyledSectionTitle>
374
+ <SkillsGrid>
375
+ {skills.map((skill, index) => (
376
+ <SkillCategory key={index}>
377
+ <h4>{skill.name}</h4>
378
+ <StyledBadgeList>
379
+ {skill.keywords?.map((keyword, i) => (
380
+ <StyledBadge key={i}>{keyword}</StyledBadge>
381
+ ))}
382
+ </StyledBadgeList>
383
+ </SkillCategory>
384
+ ))}
385
+ </SkillsGrid>
386
+ </StyledSection>
387
+ )}
388
+
389
+ {projects.length > 0 && (
390
+ <StyledSection>
391
+ <StyledSectionTitle>Projects</StyledSectionTitle>
392
+ {projects.map((project, index) => (
393
+ <GridItem key={index}>
394
+ <MetaColumn>
395
+ <MetaDate>
396
+ <DateRange
397
+ startDate={project.startDate}
398
+ endDate={project.endDate}
399
+ />
400
+ </MetaDate>
401
+ {project.type && <MetaLocation>{project.type}</MetaLocation>}
402
+ </MetaColumn>
403
+ <ContentColumn>
404
+ <ItemTitle>
405
+ {project.url ? (
406
+ <Link href={safeUrl(project.url)}>{project.name}</Link>
407
+ ) : (
408
+ project.name
409
+ )}
410
+ </ItemTitle>
411
+ {project.description && (
412
+ <ItemDescription>{project.description}</ItemDescription>
413
+ )}
414
+ {project.highlights && project.highlights.length > 0 && (
415
+ <HighlightsList>
416
+ {project.highlights.map((highlight, i) => (
417
+ <li key={i}>{highlight}</li>
418
+ ))}
419
+ </HighlightsList>
420
+ )}
421
+ </ContentColumn>
422
+ </GridItem>
423
+ ))}
424
+ </StyledSection>
425
+ )}
426
+
427
+ {awards.length > 0 && (
428
+ <StyledSection>
429
+ <StyledSectionTitle>Awards</StyledSectionTitle>
430
+ <SimpleList>
431
+ {awards.map((award, index) => (
432
+ <SimpleItem key={index}>
433
+ <h4>{award.title}</h4>
434
+ <p>
435
+ {award.awarder} {award.date && `• ${award.date}`}
436
+ </p>
437
+ {award.summary && <p>{award.summary}</p>}
438
+ </SimpleItem>
439
+ ))}
440
+ </SimpleList>
441
+ </StyledSection>
442
+ )}
443
+
444
+ {publications.length > 0 && (
445
+ <StyledSection>
446
+ <StyledSectionTitle>Publications</StyledSectionTitle>
447
+ <SimpleList>
448
+ {publications.map((pub, index) => (
449
+ <SimpleItem key={index}>
450
+ <h4>
451
+ {pub.url ? (
452
+ <Link href={safeUrl(pub.url)}>{pub.name}</Link>
453
+ ) : (
454
+ pub.name
455
+ )}
456
+ </h4>
457
+ <p>
458
+ {pub.publisher} {pub.releaseDate && `• ${pub.releaseDate}`}
459
+ </p>
460
+ {pub.summary && <p>{pub.summary}</p>}
461
+ </SimpleItem>
462
+ ))}
463
+ </SimpleList>
464
+ </StyledSection>
465
+ )}
466
+
467
+ {volunteer.length > 0 && (
468
+ <StyledSection>
469
+ <StyledSectionTitle>Volunteer</StyledSectionTitle>
470
+ {volunteer.map((vol, index) => (
471
+ <GridItem key={index}>
472
+ <MetaColumn>
473
+ <MetaDate>
474
+ <DateRange startDate={vol.startDate} endDate={vol.endDate} />
475
+ </MetaDate>
476
+ </MetaColumn>
477
+ <ContentColumn>
478
+ <ItemTitle>{vol.position}</ItemTitle>
479
+ {vol.organization && (
480
+ <ItemSubtitle>{vol.organization}</ItemSubtitle>
481
+ )}
482
+ {vol.summary && (
483
+ <ItemDescription>{vol.summary}</ItemDescription>
484
+ )}
485
+ {vol.highlights && vol.highlights.length > 0 && (
486
+ <HighlightsList>
487
+ {vol.highlights.map((highlight, i) => (
488
+ <li key={i}>{highlight}</li>
489
+ ))}
490
+ </HighlightsList>
491
+ )}
492
+ </ContentColumn>
493
+ </GridItem>
494
+ ))}
495
+ </StyledSection>
496
+ )}
497
+
498
+ {languages.length > 0 && (
499
+ <StyledSection>
500
+ <StyledSectionTitle>Languages</StyledSectionTitle>
501
+ <StyledBadgeList>
502
+ {languages.map((lang, index) => (
503
+ <StyledBadge key={index}>
504
+ {lang.language} {lang.fluency && `— ${lang.fluency}`}
505
+ </StyledBadge>
506
+ ))}
507
+ </StyledBadgeList>
508
+ </StyledSection>
509
+ )}
510
+
511
+ {interests.length > 0 && (
512
+ <StyledSection>
513
+ <StyledSectionTitle>Interests</StyledSectionTitle>
514
+ <SimpleList>
515
+ {interests.map((interest, index) => (
516
+ <SimpleItem key={index}>
517
+ <h4>{interest.name}</h4>
518
+ {interest.keywords && interest.keywords.length > 0 && (
519
+ <p>{interest.keywords.join(', ')}</p>
520
+ )}
521
+ </SimpleItem>
522
+ ))}
523
+ </SimpleList>
524
+ </StyledSection>
525
+ )}
526
+
527
+ {references.length > 0 && (
528
+ <StyledSection>
529
+ <StyledSectionTitle>References</StyledSectionTitle>
530
+ <SimpleList>
531
+ {references.map((ref, index) => (
532
+ <SimpleItem key={index}>
533
+ <h4>{ref.name}</h4>
534
+ {ref.reference && <p>{ref.reference}</p>}
535
+ </SimpleItem>
536
+ ))}
537
+ </SimpleList>
538
+ </StyledSection>
539
+ )}
540
+ </Layout>
541
+ );
542
+ }
543
+
544
+ export default Resume;