jsonresume-theme-two-column-modernist 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +27 -0
- package/src/Resume.jsx +78 -0
- package/src/components/AdditionalSections.jsx +129 -0
- package/src/components/ContentSection.jsx +160 -0
- package/src/components/Header.jsx +106 -0
- package/src/components/Languages.jsx +62 -0
- package/src/components/Skills.jsx +69 -0
- package/src/components/sharedStyles.jsx +111 -0
- package/src/components/styles.jsx +44 -0
- package/src/index.js +52 -0
package/package.json
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "jsonresume-theme-two-column-modernist",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Structured European minimalism with asymmetric two-column layout and neutral grotesk typography",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"jsonresume",
|
|
8
|
+
"theme",
|
|
9
|
+
"two-column",
|
|
10
|
+
"european",
|
|
11
|
+
"minimalist",
|
|
12
|
+
"structured",
|
|
13
|
+
"grotesk",
|
|
14
|
+
"swiss-design"
|
|
15
|
+
],
|
|
16
|
+
"dependencies": {
|
|
17
|
+
"@resume/core": "0.1.0",
|
|
18
|
+
"styled-components": "6.1.19"
|
|
19
|
+
},
|
|
20
|
+
"peerDependencies": {
|
|
21
|
+
"react": ">=18.0.0",
|
|
22
|
+
"react-dom": ">=18.0.0"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
26
|
+
}
|
|
27
|
+
}
|
package/src/Resume.jsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import {
|
|
3
|
+
Container,
|
|
4
|
+
LeftColumn,
|
|
5
|
+
RightColumn,
|
|
6
|
+
Divider,
|
|
7
|
+
} from './components/styles.jsx';
|
|
8
|
+
import { Header } from './components/Header';
|
|
9
|
+
import { Skills } from './components/Skills';
|
|
10
|
+
import { Languages } from './components/Languages';
|
|
11
|
+
import {
|
|
12
|
+
SummarySection,
|
|
13
|
+
WorkSection,
|
|
14
|
+
EducationSection,
|
|
15
|
+
ProjectsSection,
|
|
16
|
+
InterestsSection,
|
|
17
|
+
} from './components/ContentSection';
|
|
18
|
+
import {
|
|
19
|
+
VolunteerSection,
|
|
20
|
+
AwardsSection,
|
|
21
|
+
PublicationsSection,
|
|
22
|
+
ReferencesSection,
|
|
23
|
+
} from './components/AdditionalSections';
|
|
24
|
+
|
|
25
|
+
function Resume({ resume }) {
|
|
26
|
+
const {
|
|
27
|
+
basics,
|
|
28
|
+
work,
|
|
29
|
+
education,
|
|
30
|
+
skills,
|
|
31
|
+
languages,
|
|
32
|
+
projects,
|
|
33
|
+
volunteer,
|
|
34
|
+
awards,
|
|
35
|
+
publications,
|
|
36
|
+
interests,
|
|
37
|
+
references,
|
|
38
|
+
} = resume;
|
|
39
|
+
|
|
40
|
+
return (
|
|
41
|
+
<Container>
|
|
42
|
+
<LeftColumn>
|
|
43
|
+
<Header basics={basics} />
|
|
44
|
+
{(basics?.email ||
|
|
45
|
+
basics?.phone ||
|
|
46
|
+
basics?.url ||
|
|
47
|
+
basics?.location) && <Divider />}
|
|
48
|
+
{basics?.profiles && basics.profiles.length > 0 && <Divider />}
|
|
49
|
+
{skills && skills.length > 0 && (
|
|
50
|
+
<>
|
|
51
|
+
<Divider />
|
|
52
|
+
<Skills skills={skills} />
|
|
53
|
+
</>
|
|
54
|
+
)}
|
|
55
|
+
{languages && languages.length > 0 && (
|
|
56
|
+
<>
|
|
57
|
+
<Divider />
|
|
58
|
+
<Languages languages={languages} />
|
|
59
|
+
</>
|
|
60
|
+
)}
|
|
61
|
+
</LeftColumn>
|
|
62
|
+
|
|
63
|
+
<RightColumn>
|
|
64
|
+
<SummarySection summary={basics?.summary} />
|
|
65
|
+
<WorkSection work={work} />
|
|
66
|
+
<EducationSection education={education} />
|
|
67
|
+
<ProjectsSection projects={projects} />
|
|
68
|
+
<VolunteerSection volunteer={volunteer} />
|
|
69
|
+
<AwardsSection awards={awards} />
|
|
70
|
+
<PublicationsSection publications={publications} />
|
|
71
|
+
<InterestsSection interests={interests} />
|
|
72
|
+
<ReferencesSection references={references} />
|
|
73
|
+
</RightColumn>
|
|
74
|
+
</Container>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export default Resume;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { safeUrl } from '@resume/core';
|
|
3
|
+
import {
|
|
4
|
+
Section,
|
|
5
|
+
Title,
|
|
6
|
+
Entry,
|
|
7
|
+
EntryHeader,
|
|
8
|
+
EntryTitle,
|
|
9
|
+
EntryMeta,
|
|
10
|
+
EntryOrganization,
|
|
11
|
+
EntryDate,
|
|
12
|
+
EntryDescription,
|
|
13
|
+
HighlightsList,
|
|
14
|
+
HighlightItem,
|
|
15
|
+
} from './sharedStyles.jsx';
|
|
16
|
+
|
|
17
|
+
export function VolunteerSection({ volunteer }) {
|
|
18
|
+
if (!volunteer || volunteer.length === 0) return null;
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<Section>
|
|
22
|
+
<Title>Volunteer</Title>
|
|
23
|
+
{volunteer.map((vol, i) => (
|
|
24
|
+
<Entry key={i}>
|
|
25
|
+
<EntryHeader>
|
|
26
|
+
{vol.position && <EntryTitle>{vol.position}</EntryTitle>}
|
|
27
|
+
<EntryMeta>
|
|
28
|
+
{vol.organization && (
|
|
29
|
+
<EntryOrganization>{vol.organization}</EntryOrganization>
|
|
30
|
+
)}
|
|
31
|
+
{(vol.startDate || vol.endDate) && (
|
|
32
|
+
<EntryDate>
|
|
33
|
+
{vol.startDate} – {vol.endDate || 'Present'}
|
|
34
|
+
</EntryDate>
|
|
35
|
+
)}
|
|
36
|
+
</EntryMeta>
|
|
37
|
+
</EntryHeader>
|
|
38
|
+
{vol.summary && <EntryDescription>{vol.summary}</EntryDescription>}
|
|
39
|
+
{vol.highlights && vol.highlights.length > 0 && (
|
|
40
|
+
<HighlightsList>
|
|
41
|
+
{vol.highlights.map((highlight, j) => (
|
|
42
|
+
<HighlightItem key={j}>{highlight}</HighlightItem>
|
|
43
|
+
))}
|
|
44
|
+
</HighlightsList>
|
|
45
|
+
)}
|
|
46
|
+
</Entry>
|
|
47
|
+
))}
|
|
48
|
+
</Section>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function AwardsSection({ awards }) {
|
|
53
|
+
if (!awards || awards.length === 0) return null;
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Section>
|
|
57
|
+
<Title>Awards</Title>
|
|
58
|
+
{awards.map((award, i) => (
|
|
59
|
+
<Entry key={i}>
|
|
60
|
+
<EntryHeader>
|
|
61
|
+
{award.title && <EntryTitle>{award.title}</EntryTitle>}
|
|
62
|
+
<EntryMeta>
|
|
63
|
+
{award.awarder && (
|
|
64
|
+
<EntryOrganization>{award.awarder}</EntryOrganization>
|
|
65
|
+
)}
|
|
66
|
+
{award.date && <EntryDate>{award.date}</EntryDate>}
|
|
67
|
+
</EntryMeta>
|
|
68
|
+
</EntryHeader>
|
|
69
|
+
{award.summary && (
|
|
70
|
+
<EntryDescription>{award.summary}</EntryDescription>
|
|
71
|
+
)}
|
|
72
|
+
</Entry>
|
|
73
|
+
))}
|
|
74
|
+
</Section>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function PublicationsSection({ publications }) {
|
|
79
|
+
if (!publications || publications.length === 0) return null;
|
|
80
|
+
|
|
81
|
+
return (
|
|
82
|
+
<Section>
|
|
83
|
+
<Title>Publications</Title>
|
|
84
|
+
{publications.map((pub, i) => (
|
|
85
|
+
<Entry key={i}>
|
|
86
|
+
<EntryHeader>
|
|
87
|
+
{pub.name && <EntryTitle>{pub.name}</EntryTitle>}
|
|
88
|
+
<EntryMeta>
|
|
89
|
+
{pub.publisher && (
|
|
90
|
+
<EntryOrganization>{pub.publisher}</EntryOrganization>
|
|
91
|
+
)}
|
|
92
|
+
{pub.releaseDate && <EntryDate>{pub.releaseDate}</EntryDate>}
|
|
93
|
+
</EntryMeta>
|
|
94
|
+
</EntryHeader>
|
|
95
|
+
{pub.summary && <EntryDescription>{pub.summary}</EntryDescription>}
|
|
96
|
+
{pub.url && (
|
|
97
|
+
<div>
|
|
98
|
+
<a
|
|
99
|
+
href={safeUrl(pub.url)}
|
|
100
|
+
target="_blank"
|
|
101
|
+
rel="noopener noreferrer"
|
|
102
|
+
>
|
|
103
|
+
View Publication
|
|
104
|
+
</a>
|
|
105
|
+
</div>
|
|
106
|
+
)}
|
|
107
|
+
</Entry>
|
|
108
|
+
))}
|
|
109
|
+
</Section>
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function ReferencesSection({ references }) {
|
|
114
|
+
if (!references || references.length === 0) return null;
|
|
115
|
+
|
|
116
|
+
return (
|
|
117
|
+
<Section>
|
|
118
|
+
<Title>References</Title>
|
|
119
|
+
{references.map((ref, i) => (
|
|
120
|
+
<Entry key={i}>
|
|
121
|
+
{ref.name && <EntryTitle>{ref.name}</EntryTitle>}
|
|
122
|
+
{ref.reference && (
|
|
123
|
+
<EntryDescription>{ref.reference}</EntryDescription>
|
|
124
|
+
)}
|
|
125
|
+
</Entry>
|
|
126
|
+
))}
|
|
127
|
+
</Section>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { safeUrl } from '@resume/core';
|
|
3
|
+
import {
|
|
4
|
+
Section,
|
|
5
|
+
Title,
|
|
6
|
+
Entry,
|
|
7
|
+
EntryHeader,
|
|
8
|
+
EntryTitle,
|
|
9
|
+
EntryMeta,
|
|
10
|
+
EntryOrganization,
|
|
11
|
+
EntryDate,
|
|
12
|
+
EntryDescription,
|
|
13
|
+
HighlightsList,
|
|
14
|
+
HighlightItem,
|
|
15
|
+
Summary,
|
|
16
|
+
SkillList,
|
|
17
|
+
SkillItem,
|
|
18
|
+
} from './sharedStyles.jsx';
|
|
19
|
+
|
|
20
|
+
export function WorkSection({ work }) {
|
|
21
|
+
if (!work || work.length === 0) return null;
|
|
22
|
+
|
|
23
|
+
return (
|
|
24
|
+
<Section>
|
|
25
|
+
<Title>Experience</Title>
|
|
26
|
+
{work.map((job, i) => (
|
|
27
|
+
<Entry key={i}>
|
|
28
|
+
<EntryHeader>
|
|
29
|
+
{job.position && <EntryTitle>{job.position}</EntryTitle>}
|
|
30
|
+
<EntryMeta>
|
|
31
|
+
{job.name && <EntryOrganization>{job.name}</EntryOrganization>}
|
|
32
|
+
{(job.startDate || job.endDate) && (
|
|
33
|
+
<EntryDate>
|
|
34
|
+
{job.startDate} – {job.endDate || 'Present'}
|
|
35
|
+
</EntryDate>
|
|
36
|
+
)}
|
|
37
|
+
</EntryMeta>
|
|
38
|
+
</EntryHeader>
|
|
39
|
+
{job.summary && <EntryDescription>{job.summary}</EntryDescription>}
|
|
40
|
+
{job.highlights && job.highlights.length > 0 && (
|
|
41
|
+
<HighlightsList>
|
|
42
|
+
{job.highlights.map((highlight, j) => (
|
|
43
|
+
<HighlightItem key={j}>{highlight}</HighlightItem>
|
|
44
|
+
))}
|
|
45
|
+
</HighlightsList>
|
|
46
|
+
)}
|
|
47
|
+
</Entry>
|
|
48
|
+
))}
|
|
49
|
+
</Section>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function EducationSection({ education }) {
|
|
54
|
+
if (!education || education.length === 0) return null;
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Section>
|
|
58
|
+
<Title>Education</Title>
|
|
59
|
+
{education.map((edu, i) => (
|
|
60
|
+
<Entry key={i}>
|
|
61
|
+
<EntryHeader>
|
|
62
|
+
{edu.studyType && edu.area && (
|
|
63
|
+
<EntryTitle>
|
|
64
|
+
{edu.studyType} in {edu.area}
|
|
65
|
+
</EntryTitle>
|
|
66
|
+
)}
|
|
67
|
+
<EntryMeta>
|
|
68
|
+
{edu.institution && (
|
|
69
|
+
<EntryOrganization>{edu.institution}</EntryOrganization>
|
|
70
|
+
)}
|
|
71
|
+
{(edu.startDate || edu.endDate) && (
|
|
72
|
+
<EntryDate>
|
|
73
|
+
{edu.startDate} – {edu.endDate || 'Present'}
|
|
74
|
+
</EntryDate>
|
|
75
|
+
)}
|
|
76
|
+
</EntryMeta>
|
|
77
|
+
</EntryHeader>
|
|
78
|
+
{edu.score && <EntryDescription>GPA: {edu.score}</EntryDescription>}
|
|
79
|
+
{edu.courses && edu.courses.length > 0 && (
|
|
80
|
+
<HighlightsList>
|
|
81
|
+
{edu.courses.map((course, j) => (
|
|
82
|
+
<HighlightItem key={j}>{course}</HighlightItem>
|
|
83
|
+
))}
|
|
84
|
+
</HighlightsList>
|
|
85
|
+
)}
|
|
86
|
+
</Entry>
|
|
87
|
+
))}
|
|
88
|
+
</Section>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function ProjectsSection({ projects }) {
|
|
93
|
+
if (!projects || projects.length === 0) return null;
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Section>
|
|
97
|
+
<Title>Projects</Title>
|
|
98
|
+
{projects.map((project, i) => (
|
|
99
|
+
<Entry key={i}>
|
|
100
|
+
<EntryHeader>
|
|
101
|
+
{project.name && <EntryTitle>{project.name}</EntryTitle>}
|
|
102
|
+
{(project.startDate || project.endDate) && (
|
|
103
|
+
<EntryMeta>
|
|
104
|
+
<EntryDate>
|
|
105
|
+
{project.startDate} – {project.endDate || 'Present'}
|
|
106
|
+
</EntryDate>
|
|
107
|
+
</EntryMeta>
|
|
108
|
+
)}
|
|
109
|
+
</EntryHeader>
|
|
110
|
+
{project.description && (
|
|
111
|
+
<EntryDescription>{project.description}</EntryDescription>
|
|
112
|
+
)}
|
|
113
|
+
{project.highlights && project.highlights.length > 0 && (
|
|
114
|
+
<HighlightsList>
|
|
115
|
+
{project.highlights.map((highlight, j) => (
|
|
116
|
+
<HighlightItem key={j}>{highlight}</HighlightItem>
|
|
117
|
+
))}
|
|
118
|
+
</HighlightsList>
|
|
119
|
+
)}
|
|
120
|
+
{project.url && (
|
|
121
|
+
<div>
|
|
122
|
+
<a
|
|
123
|
+
href={safeUrl(project.url)}
|
|
124
|
+
target="_blank"
|
|
125
|
+
rel="noopener noreferrer"
|
|
126
|
+
>
|
|
127
|
+
View Project
|
|
128
|
+
</a>
|
|
129
|
+
</div>
|
|
130
|
+
)}
|
|
131
|
+
</Entry>
|
|
132
|
+
))}
|
|
133
|
+
</Section>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function SummarySection({ summary }) {
|
|
138
|
+
if (!summary) return null;
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<Section>
|
|
142
|
+
<Summary>{summary}</Summary>
|
|
143
|
+
</Section>
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function InterestsSection({ interests }) {
|
|
148
|
+
if (!interests || interests.length === 0) return null;
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
<Section>
|
|
152
|
+
<Title>Interests</Title>
|
|
153
|
+
<SkillList>
|
|
154
|
+
{interests.map((interest, i) => (
|
|
155
|
+
<SkillItem key={i}>{interest.name}</SkillItem>
|
|
156
|
+
))}
|
|
157
|
+
</SkillList>
|
|
158
|
+
</Section>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
import { safeUrl } from '@resume/core';
|
|
4
|
+
|
|
5
|
+
const HeaderContainer = styled.header`
|
|
6
|
+
display: flex;
|
|
7
|
+
flex-direction: column;
|
|
8
|
+
gap: 0.75rem;
|
|
9
|
+
`;
|
|
10
|
+
|
|
11
|
+
const Name = styled.h1`
|
|
12
|
+
font-size: 1.75rem;
|
|
13
|
+
font-weight: 700;
|
|
14
|
+
letter-spacing: -0.02em;
|
|
15
|
+
margin: 0;
|
|
16
|
+
color: #1f1f1f;
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const Title = styled.p`
|
|
20
|
+
font-size: 0.95rem;
|
|
21
|
+
font-weight: 500;
|
|
22
|
+
color: #4b5563;
|
|
23
|
+
margin: 0;
|
|
24
|
+
letter-spacing: 0.01em;
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const Contact = styled.div`
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
gap: 0.5rem;
|
|
31
|
+
font-size: 0.85rem;
|
|
32
|
+
color: #4b5563;
|
|
33
|
+
|
|
34
|
+
a {
|
|
35
|
+
color: #1f1f1f;
|
|
36
|
+
text-decoration: none;
|
|
37
|
+
transition: color 0.2s;
|
|
38
|
+
|
|
39
|
+
&:hover {
|
|
40
|
+
color: #6b7280;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
export function Header({ basics }) {
|
|
46
|
+
if (!basics) return null;
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<>
|
|
50
|
+
<HeaderContainer>
|
|
51
|
+
<Name>{basics.name}</Name>
|
|
52
|
+
{basics.label && <Title>{basics.label}</Title>}
|
|
53
|
+
</HeaderContainer>
|
|
54
|
+
|
|
55
|
+
{(basics.email || basics.phone || basics.url || basics.location) && (
|
|
56
|
+
<Contact>
|
|
57
|
+
{basics.email && (
|
|
58
|
+
<div>
|
|
59
|
+
<a href={`mailto:${basics.email}`}>{basics.email}</a>
|
|
60
|
+
</div>
|
|
61
|
+
)}
|
|
62
|
+
{basics.phone && <div>{basics.phone}</div>}
|
|
63
|
+
{basics.url && (
|
|
64
|
+
<div>
|
|
65
|
+
<a
|
|
66
|
+
href={safeUrl(basics.url)}
|
|
67
|
+
target="_blank"
|
|
68
|
+
rel="noopener noreferrer"
|
|
69
|
+
>
|
|
70
|
+
{basics.url.replace(/^https?:\/\//, '')}
|
|
71
|
+
</a>
|
|
72
|
+
</div>
|
|
73
|
+
)}
|
|
74
|
+
{basics.location && (
|
|
75
|
+
<div>
|
|
76
|
+
{[
|
|
77
|
+
basics.location.city,
|
|
78
|
+
basics.location.region,
|
|
79
|
+
basics.location.countryCode,
|
|
80
|
+
]
|
|
81
|
+
.filter(Boolean)
|
|
82
|
+
.join(', ')}
|
|
83
|
+
</div>
|
|
84
|
+
)}
|
|
85
|
+
</Contact>
|
|
86
|
+
)}
|
|
87
|
+
|
|
88
|
+
{basics.profiles && basics.profiles.length > 0 && (
|
|
89
|
+
<Contact>
|
|
90
|
+
{basics.profiles.map((profile, i) => (
|
|
91
|
+
<div key={i}>
|
|
92
|
+
<a
|
|
93
|
+
href={safeUrl(profile.url)}
|
|
94
|
+
target="_blank"
|
|
95
|
+
rel="noopener noreferrer"
|
|
96
|
+
>
|
|
97
|
+
{profile.network}
|
|
98
|
+
{profile.username && `: @${profile.username}`}
|
|
99
|
+
</a>
|
|
100
|
+
</div>
|
|
101
|
+
))}
|
|
102
|
+
</Contact>
|
|
103
|
+
)}
|
|
104
|
+
</>
|
|
105
|
+
);
|
|
106
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
const SidebarSection = styled.section`
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: 1rem;
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
const SidebarTitle = styled.h2`
|
|
11
|
+
font-size: 0.9rem;
|
|
12
|
+
font-weight: 600;
|
|
13
|
+
letter-spacing: 0.05em;
|
|
14
|
+
text-transform: uppercase;
|
|
15
|
+
margin: 0;
|
|
16
|
+
color: #1f1f1f;
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const LanguageList = styled.ul`
|
|
20
|
+
list-style: none;
|
|
21
|
+
padding: 0;
|
|
22
|
+
margin: 0;
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
gap: 0.5rem;
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
const LanguageItem = styled.li`
|
|
29
|
+
font-size: 0.85rem;
|
|
30
|
+
color: #374151;
|
|
31
|
+
display: flex;
|
|
32
|
+
justify-content: space-between;
|
|
33
|
+
align-items: center;
|
|
34
|
+
`;
|
|
35
|
+
|
|
36
|
+
const LanguageName = styled.span`
|
|
37
|
+
font-weight: 500;
|
|
38
|
+
color: #1f1f1f;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const LanguageFluency = styled.span`
|
|
42
|
+
font-size: 0.8rem;
|
|
43
|
+
color: #6b7280;
|
|
44
|
+
`;
|
|
45
|
+
|
|
46
|
+
export function Languages({ languages }) {
|
|
47
|
+
if (!languages || languages.length === 0) return null;
|
|
48
|
+
|
|
49
|
+
return (
|
|
50
|
+
<SidebarSection>
|
|
51
|
+
<SidebarTitle>Languages</SidebarTitle>
|
|
52
|
+
<LanguageList>
|
|
53
|
+
{languages.map((lang, i) => (
|
|
54
|
+
<LanguageItem key={i}>
|
|
55
|
+
<LanguageName>{lang.language}</LanguageName>
|
|
56
|
+
{lang.fluency && <LanguageFluency>{lang.fluency}</LanguageFluency>}
|
|
57
|
+
</LanguageItem>
|
|
58
|
+
))}
|
|
59
|
+
</LanguageList>
|
|
60
|
+
</SidebarSection>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styled from 'styled-components';
|
|
3
|
+
|
|
4
|
+
const SidebarSection = styled.section`
|
|
5
|
+
display: flex;
|
|
6
|
+
flex-direction: column;
|
|
7
|
+
gap: 1rem;
|
|
8
|
+
`;
|
|
9
|
+
|
|
10
|
+
const SidebarTitle = styled.h2`
|
|
11
|
+
font-size: 0.9rem;
|
|
12
|
+
font-weight: 600;
|
|
13
|
+
letter-spacing: 0.05em;
|
|
14
|
+
text-transform: uppercase;
|
|
15
|
+
margin: 0;
|
|
16
|
+
color: #1f1f1f;
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const SkillCategory = styled.div`
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 0.5rem;
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
const SkillName = styled.h3`
|
|
26
|
+
font-size: 0.85rem;
|
|
27
|
+
font-weight: 600;
|
|
28
|
+
margin: 0;
|
|
29
|
+
color: #1f1f1f;
|
|
30
|
+
`;
|
|
31
|
+
|
|
32
|
+
const SkillList = styled.ul`
|
|
33
|
+
list-style: none;
|
|
34
|
+
padding: 0;
|
|
35
|
+
margin: 0;
|
|
36
|
+
display: flex;
|
|
37
|
+
flex-wrap: wrap;
|
|
38
|
+
gap: 0.5rem;
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
const SkillItem = styled.li`
|
|
42
|
+
font-size: 0.8rem;
|
|
43
|
+
color: #4b5563;
|
|
44
|
+
background: #f3f4f6;
|
|
45
|
+
padding: 0.25rem 0.75rem;
|
|
46
|
+
border-radius: 0.25rem;
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
export function Skills({ skills }) {
|
|
50
|
+
if (!skills || skills.length === 0) return null;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<SidebarSection>
|
|
54
|
+
<SidebarTitle>Skills</SidebarTitle>
|
|
55
|
+
{skills.map((skill, i) => (
|
|
56
|
+
<SkillCategory key={i}>
|
|
57
|
+
{skill.name && <SkillName>{skill.name}</SkillName>}
|
|
58
|
+
{skill.keywords && skill.keywords.length > 0 && (
|
|
59
|
+
<SkillList>
|
|
60
|
+
{skill.keywords.map((keyword, j) => (
|
|
61
|
+
<SkillItem key={j}>{keyword}</SkillItem>
|
|
62
|
+
))}
|
|
63
|
+
</SkillList>
|
|
64
|
+
)}
|
|
65
|
+
</SkillCategory>
|
|
66
|
+
))}
|
|
67
|
+
</SidebarSection>
|
|
68
|
+
);
|
|
69
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const Section = styled.section`
|
|
4
|
+
display: flex;
|
|
5
|
+
flex-direction: column;
|
|
6
|
+
gap: 1.5rem;
|
|
7
|
+
`;
|
|
8
|
+
|
|
9
|
+
export const Title = styled.h2`
|
|
10
|
+
font-size: 1.1rem;
|
|
11
|
+
font-weight: 700;
|
|
12
|
+
letter-spacing: -0.01em;
|
|
13
|
+
margin: 0;
|
|
14
|
+
color: #1f1f1f;
|
|
15
|
+
border-bottom: 1px solid #d1d5db;
|
|
16
|
+
padding-bottom: 0.5rem;
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
export const Entry = styled.article`
|
|
20
|
+
display: flex;
|
|
21
|
+
flex-direction: column;
|
|
22
|
+
gap: 0.75rem;
|
|
23
|
+
`;
|
|
24
|
+
|
|
25
|
+
export const EntryHeader = styled.div`
|
|
26
|
+
display: flex;
|
|
27
|
+
flex-direction: column;
|
|
28
|
+
gap: 0.25rem;
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
export const EntryTitle = styled.h3`
|
|
32
|
+
font-size: 1rem;
|
|
33
|
+
font-weight: 600;
|
|
34
|
+
margin: 0;
|
|
35
|
+
color: #1f1f1f;
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
export const EntryMeta = styled.div`
|
|
39
|
+
font-size: 0.85rem;
|
|
40
|
+
color: #6b7280;
|
|
41
|
+
display: flex;
|
|
42
|
+
flex-wrap: wrap;
|
|
43
|
+
gap: 0.5rem;
|
|
44
|
+
align-items: center;
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
export const EntryOrganization = styled.span`
|
|
48
|
+
font-weight: 500;
|
|
49
|
+
color: #4b5563;
|
|
50
|
+
`;
|
|
51
|
+
|
|
52
|
+
export const EntryDate = styled.span`
|
|
53
|
+
color: #9ca3af;
|
|
54
|
+
font-size: 0.8rem;
|
|
55
|
+
`;
|
|
56
|
+
|
|
57
|
+
export const EntryDescription = styled.p`
|
|
58
|
+
font-size: 0.9rem;
|
|
59
|
+
line-height: 1.7;
|
|
60
|
+
color: #374151;
|
|
61
|
+
margin: 0;
|
|
62
|
+
white-space: pre-wrap;
|
|
63
|
+
`;
|
|
64
|
+
|
|
65
|
+
export const HighlightsList = styled.ul`
|
|
66
|
+
list-style: none;
|
|
67
|
+
padding: 0;
|
|
68
|
+
margin: 0;
|
|
69
|
+
display: flex;
|
|
70
|
+
flex-direction: column;
|
|
71
|
+
gap: 0.5rem;
|
|
72
|
+
`;
|
|
73
|
+
|
|
74
|
+
export const HighlightItem = styled.li`
|
|
75
|
+
font-size: 0.85rem;
|
|
76
|
+
color: #4b5563;
|
|
77
|
+
padding-left: 1rem;
|
|
78
|
+
position: relative;
|
|
79
|
+
|
|
80
|
+
&::before {
|
|
81
|
+
content: '—';
|
|
82
|
+
position: absolute;
|
|
83
|
+
left: 0;
|
|
84
|
+
color: #d1d5db;
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
|
|
88
|
+
export const Summary = styled.p`
|
|
89
|
+
font-size: 0.9rem;
|
|
90
|
+
line-height: 1.7;
|
|
91
|
+
color: #374151;
|
|
92
|
+
margin: 0;
|
|
93
|
+
white-space: pre-wrap;
|
|
94
|
+
`;
|
|
95
|
+
|
|
96
|
+
export const SkillList = styled.ul`
|
|
97
|
+
list-style: none;
|
|
98
|
+
padding: 0;
|
|
99
|
+
margin: 0;
|
|
100
|
+
display: flex;
|
|
101
|
+
flex-wrap: wrap;
|
|
102
|
+
gap: 0.5rem;
|
|
103
|
+
`;
|
|
104
|
+
|
|
105
|
+
export const SkillItem = styled.li`
|
|
106
|
+
font-size: 0.8rem;
|
|
107
|
+
color: #4b5563;
|
|
108
|
+
background: #f3f4f6;
|
|
109
|
+
padding: 0.25rem 0.75rem;
|
|
110
|
+
border-radius: 0.25rem;
|
|
111
|
+
`;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import styled from 'styled-components';
|
|
2
|
+
|
|
3
|
+
export const Container = styled.div`
|
|
4
|
+
display: grid;
|
|
5
|
+
grid-template-columns: 30% 70%;
|
|
6
|
+
gap: 3rem;
|
|
7
|
+
max-width: 1200px;
|
|
8
|
+
margin: 0 auto;
|
|
9
|
+
padding: 3rem 2rem;
|
|
10
|
+
font-family: 'Space Grotesk', 'Archivo', -apple-system, BlinkMacSystemFont,
|
|
11
|
+
'Segoe UI', sans-serif;
|
|
12
|
+
line-height: 1.6;
|
|
13
|
+
color: #1f1f1f;
|
|
14
|
+
background: #ffffff;
|
|
15
|
+
|
|
16
|
+
@media (max-width: 768px) {
|
|
17
|
+
grid-template-columns: 1fr;
|
|
18
|
+
gap: 2rem;
|
|
19
|
+
padding: 2rem 1rem;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@media print {
|
|
23
|
+
padding: 1rem;
|
|
24
|
+
gap: 2rem;
|
|
25
|
+
}
|
|
26
|
+
`;
|
|
27
|
+
|
|
28
|
+
export const LeftColumn = styled.aside`
|
|
29
|
+
display: flex;
|
|
30
|
+
flex-direction: column;
|
|
31
|
+
gap: 2rem;
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
export const RightColumn = styled.main`
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
gap: 2.5rem;
|
|
38
|
+
`;
|
|
39
|
+
|
|
40
|
+
export const Divider = styled.hr`
|
|
41
|
+
border: none;
|
|
42
|
+
border-top: 1px solid #d1d5db;
|
|
43
|
+
margin: 0;
|
|
44
|
+
`;
|
package/src/index.js
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { renderToString } from 'react-dom/server';
|
|
3
|
+
import { ServerStyleSheet } from 'styled-components';
|
|
4
|
+
import Resume from './Resume.jsx';
|
|
5
|
+
|
|
6
|
+
export function render(resume) {
|
|
7
|
+
const sheet = new ServerStyleSheet();
|
|
8
|
+
try {
|
|
9
|
+
const html = renderToString(
|
|
10
|
+
sheet.collectStyles(<Resume resume={resume} />)
|
|
11
|
+
);
|
|
12
|
+
const styles = sheet.getStyleTags();
|
|
13
|
+
|
|
14
|
+
return `<!DOCTYPE html>
|
|
15
|
+
<html lang="en">
|
|
16
|
+
<head>
|
|
17
|
+
<meta charset="utf-8">
|
|
18
|
+
<title>${resume.basics?.name || 'Resume'} - Resume</title>
|
|
19
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
20
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
21
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
22
|
+
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap" rel="stylesheet">
|
|
23
|
+
${styles}
|
|
24
|
+
<style>
|
|
25
|
+
* {
|
|
26
|
+
box-sizing: border-box;
|
|
27
|
+
}
|
|
28
|
+
body {
|
|
29
|
+
margin: 0;
|
|
30
|
+
padding: 0;
|
|
31
|
+
font-family: 'Space Grotesk', 'Archivo', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
32
|
+
background: #ffffff;
|
|
33
|
+
}
|
|
34
|
+
@media print {
|
|
35
|
+
body {
|
|
36
|
+
margin: 0;
|
|
37
|
+
padding: 0;
|
|
38
|
+
}
|
|
39
|
+
@page {
|
|
40
|
+
margin: 0.5cm;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
</style>
|
|
44
|
+
</head>
|
|
45
|
+
<body>
|
|
46
|
+
${html}
|
|
47
|
+
</body>
|
|
48
|
+
</html>`;
|
|
49
|
+
} finally {
|
|
50
|
+
sheet.seal();
|
|
51
|
+
}
|
|
52
|
+
}
|