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