Introduction
In this post, we’ll guide you through creating a single-page resume app that can be exported as a PDF. Built with React and TypeScript, this app is designed to help you effectively showcase your professional experience. You can view the final product here and access the source code here. We’ll also include carefully crafted prompts you can use with AI tools like ChatGPT to generate content for your resume and LinkedIn profile. Feel free to fork the project and effortlessly create a sleek, high-quality resume of your own!
Forking the Starter Kit
To kick off this project, we’ll fork the RadzionKit repository. This starter kit includes all the essential tools and configurations needed to create a NextJS app and deploy it seamlessly to AWS.
Optimizing the Layout and Responsiveness
Our single resume page will be styled within a container that provides padding and a contrasting background to highlight the resume. For smaller screens, we’ll optimize the layout by removing the padding from the container and adjusting the resume’s border radius, aspect ratio, and max-width.
import { centerContent } from "@lib/ui/css/centerContent"
import styled from "styled-components"
import { verticalPadding } from "@lib/ui/css/verticalPadding"
import { getColor } from "@lib/ui/theme/getters"
import { Resume } from "./Resume"
import { toSizeUnit } from "@lib/ui/css/toSizeUnit"
import { resumeConfig } from "@lib/resume-ui/components/config"
import { PageMetaTags } from "@lib/next-ui/metadata/PageMetaTags"
const Container = styled.div`
${centerContent};
${verticalPadding(40)};
background: ${getColor("foregroundExtra")};
@media (max-width: ${toSizeUnit(resumeConfig.maxWidth + 40)}) {
${verticalPadding(0)};
> * {
&:first-child {
border-radius: 0;
max-width: initial;
aspect-ratio: initial;
}
}
}
`
export const ResumePage = () => (
<Container>
<Resume />
<PageMetaTags
title="Radzion Chachura - Front-End Engineer | Web3 Specialist"
description="Explore the professional journey of Radzion Chachura, a seasoned front-end engineer specializing in web3. Learn about my experience with Terraform Labs, IOG, Zerion, and personal projects like Increaser and RadzionKit."
/>
</Container>
)
Configuring SEO with Meta Tags
To enhance SEO, we set meta tags like title
and description
for our resume page. This is achieved using the PageMetaTags
component, which dynamically inserts all the necessary meta tags into the Next.js <Head>
component. These tags help search engines better understand the content of your page, improving its visibility and ranking.
import Head from "next/head"
interface PageMetaTags {
title?: string
description?: string
image?: string
language?: string
}
export const PageMetaTags = ({
title,
description,
image,
language,
}: PageMetaTags) => (
<Head>
{title && (
<>
<title>{title}</title>
<meta name="application-name" content={title} />
<meta name="apple-mobile-web-app-title" content={title} />
<meta property="og:title" content={title} />
<meta name="twitter:title" content={title} />
</>
)}
{description && (
<>
<meta name="description" content={description} />
<meta property="og:description" content={description} />
<meta name="twitter:description" content={description} />
<meta property="og:image:alt" content={description} />
<meta name="twitter:image:alt" content={description} />
</>
)}
{image && (
<>
<meta property="og:image" content={image} />
<meta name="twitter:image:src" content={image} />
</>
)}
{language && <meta httpEquiv="Content-Language" content={language} />}
</Head>
)
Leveraging AI for Content Generation
Instead of crafting the title
and description
manually, we’ll leverage AI to generate them for us. This is where the prompts
directory in our project comes into play.
The context.md
file is the core of the prompts
directory. Here, we include detailed information about our professional experience: the job we’re looking for, previous roles, responsibilities, technologies used, and any relevant projects. Providing thorough and specific details helps the AI generate precise and relevant content for the resume.
When using ChatGPT, start by pasting the content of context.md
into the prompt field. ChatGPT will acknowledge with a simple "Yes," allowing you to follow up with another prompt. For example, to generate meta tags, you could ask: Write title and description meta tags for my resume page.
To streamline the process of updating the resume, it’s helpful to save all such prompts in a separate file. This way, you can quickly copy and paste them into ChatGPT whenever needed, ensuring consistency and saving time during future updates.
Building the Resume Structure
With the responsive page container ready and meta tags configured for SEO, the next step is the Resume
component. This component acts as the main structure of the resume, consisting of a container with four key sections: Header
, Jobs
, Projects
, and Contacts
.
import { HStack, VStack } from "@lib/ui/css/stack"
import { Text } from "@lib/ui/text"
import { Tag } from "@lib/ui/tags/Tag"
import { useTheme } from "styled-components"
import { ComponentProps, useRef } from "react"
import { ResumeContainer } from "@lib/resume-ui/components/ResumeContainer"
import { DownloadResume } from "@lib/resume-ui/components/DownloadResume"
import { differenceInYears } from "date-fns"
import { useRhythmicRerender } from "@lib/ui/hooks/useRhythmicRerender"
import { convertDuration } from "@lib/utils/time/convertDuration"
import { firstJobStartedAt } from "@product/config"
import { Jobs } from "./jobs/Jobs"
import { Projects } from "./projects/Projects"
import { Contacts } from "./Contacts"
export const Resume = (props: ComponentProps<typeof ResumeContainer>) => {
const { colors } = useTheme()
const containerElement = useRef<HTMLDivElement>(null)
const now = useRhythmicRerender(convertDuration(10, "min", "ms"))
return (
<ResumeContainer {...props} ref={containerElement}>
<HStack alignItems="start" justifyContent="space-between">
<VStack gap={8}>
<HStack wrap="wrap" alignItems="center" gap={8}>
<Text size={20} color="contrast" weight="600">
Radzion
</Text>
<Tag $color={colors.getLabelColor(5)}>
{differenceInYears(now, firstJobStartedAt)} years of experience
</Tag>
<Tag $color={colors.getLabelColor(10)}>CS Degree</Tag>
</HStack>
<HStack>
<Text centerVertically={{ gap: 6 }} color="contrast" weight="500">
<span>💪</span>
<span>React, TypeScript, UX/UI, Web3</span>
<Text as="span" color="supporting">
+ AWS, Node.js, DynamoDB
</Text>
</Text>
</HStack>
</VStack>
<DownloadResume render={() => containerElement.current} />
</HStack>
<Jobs />
<Projects />
<Contacts />
</ResumeContainer>
)
}
To make the resume page resemble a single PDF sheet, we set an aspect-ratio
and a fixed max-width
to ensure it fits within standard dimensions. The container has no padding, allowing section dividers to span the full width. Padding is applied directly to child elements for consistent spacing. For printing, we remove the border and border-radius
so the content fills the entire page.
import { borderRadius } from "@lib/ui/css/borderRadius"
import { toSizeUnit } from "@lib/ui/css/toSizeUnit"
import { getColor } from "@lib/ui/theme/getters"
import styled from "styled-components"
import { resumeConfig } from "./config"
import { vStack } from "@lib/ui/css/stack"
import { horizontalPadding } from "@lib/ui/css/horizontalPadding"
import { verticalPadding } from "@lib/ui/css/verticalPadding"
export const ResumeContainer = styled.div`
max-width: ${toSizeUnit(resumeConfig.maxWidth)};
width: 100%;
aspect-ratio: 1 / 1.414;
line-height: 1.5;
${vStack()}
background: ${getColor("background")};
${borderRadius.m}
overflow: hidden;
@media print {
width: 100%;
height: 100%;
border-radius: 0;
}
> * {
${horizontalPadding(resumeConfig.padding)}
${verticalPadding(resumeConfig.padding * 2)}
&:first-child {
padding-top: ${toSizeUnit(resumeConfig.padding)};
}
&:nth-last-child(2) {
padding-bottom: ${toSizeUnit(resumeConfig.padding)};
}
&:last-child {
${verticalPadding(resumeConfig.padding)};
}
}
`
Highlighting the Header and Skills
The header highlights the most important information. It starts with the name and two tags: one showing years of experience and the other indicating a CS degree. The CS degree is included here instead of a dedicated section, as it’s not a standout detail worth emphasizing separately. Below the name, there’s a line listing technologies and skills. Primary skills are styled with a contrasting color, while secondary skills are shown in a supporting color followed by a plus sign.
Enabling PDF Downloads
To enable printing the resume, we keep a reference to the resume component and pass it to the DownloadResume
component, which uses the react-to-print
library to convert the content into a PDF when the download button is clicked. The DownloadResume
button is styled to remain hidden during printing using @media print
, ensuring it doesn’t appear on the printed resume.
import styled from "styled-components"
import ReactToPrint from "react-to-print"
import { DownloadIcon } from "@lib/ui/icons/DonwloadIcon"
import { Button } from "@lib/ui/buttons/Button"
import { HStack } from "@lib/ui/css/stack"
import { Text } from "@lib/ui/text"
type DownloadResumeProps = {
render: () => React.ReactInstance | null
}
const Container = styled(Button)`
@media print {
display: none;
}
svg {
font-size: 16px;
}
`
export const DownloadResume = ({ render }: DownloadResumeProps) => (
<ReactToPrint
trigger={() => (
<Container size="s" kind="primary">
<HStack alignItems="center" gap={8}>
<DownloadIcon />
<Text>Download</Text>
</HStack>
</Container>
)}
content={render}
/>
)
Showcasing Job Experiences
The Jobs
section comes next, listing full-time job experiences. Since my current focus is on web3 and crypto roles, I’ve split this section into two parts: one highlighting my work at web3 companies and the other covering my previous full-stack experience.
import { JobExperience } from "@lib/resume-ui/components/JobExperience"
import { ResumeSection } from "@lib/resume-ui/components/ResumeSection"
import { VStack } from "@lib/ui/css/stack"
import { IogIcon } from "./IogIcon"
import { TflIcon } from "./TflIcon"
import { ZerionIcon } from "./ZerionIcon"
import { firstJobStartedAt } from "@product/config"
import { KreoIcon } from "./KreoIcon"
import { KontistIcon } from "./KontistIcon"
import { ProductLink } from "@lib/resume-ui/components/ProductLink"
import { Tech } from "@lib/resume-ui/components/Tech"
export const Jobs = () => (
<>
<ResumeSection title={<>Web3</>}>
<JobExperience
position="Front-end Engineer"
company="Terraform Labs"
startedAt={new Date(2022, 2)}
finishedAt={new Date(2024, 7)}
companyIcon={<TflIcon />}
responsibilities={[
<>
Developed and maintained features for{" "}
<ProductLink to="https://app.anchorprotocol.com/">
Anchor Protocol
</ProductLink>{" "}
using <Tech>React</Tech> and <Tech>TypeScript</Tech>.
</>,
<>
Built front-ends for two NFT protocols on Terra with{" "}
<Tech>Next.js</Tech>.
</>,
<>
Led the front-end and back-end development of{" "}
<ProductLink to="https://enterprise.money/">
Enterprise Protocol
</ProductLink>
, supporting DAOs across Cosmos chains.
</>,
<>
Implemented a TypeScript monorepo with <Tech>React</Tech> and{" "}
<Tech>Node.js</Tech>, integrating DynamoDB.
</>,
]}
/>
<VStack gap={20}>
<JobExperience
position="Front-end Engineer"
company="IOG"
finishedAt={new Date(2022, 1)}
startedAt={new Date(2021, 8)}
companyIcon={<IogIcon />}
responsibilities={[
<>
Built the NFT gallery and transaction history for{" "}
<ProductLink to="https://www.lace.io/">Lace</ProductLink> using{" "}
<Tech>React</Tech> and <Tech>TypeScript</Tech>.
</>,
<>
Enhanced wallet functionalities and general UX/UI improvements.
</>,
]}
/>
<JobExperience
position="Front-end Engineer"
company="Zerion"
finishedAt={new Date(2021, 7)}
startedAt={new Date(2020, 9)}
companyIcon={<ZerionIcon />}
responsibilities={[
<>
Improved wallet management and token exchange for{" "}
<ProductLink to="https://app.zerion.io/">Zerion</ProductLink>.
</>,
<>
Integrated staking features and internationalization support with{" "}
<Tech>React</Tech>.
</>,
]}
/>
</VStack>
</ResumeSection>
<ResumeSection title={"Full-Stack"}>
<JobExperience
companyIcon={<KontistIcon />}
position="Senior Software Developer"
company="Kontist"
startedAt={new Date(2019, 6)}
finishedAt={new Date(2020, 8)}
responsibilities={[
<>
Improved onboarding and account creation using <Tech>React</Tech>{" "}
and <Tech>React Native</Tech>.
</>,
<>
Developed a web app for the product with <Tech>TypeScript</Tech>.
</>,
<>
Built and maintained back-end features using <Tech>Node.js</Tech>{" "}
and PostgreSQL.
</>,
]}
/>
<JobExperience
position="Software Developer"
company="KREO"
companyIcon={<KreoIcon />}
startedAt={firstJobStartedAt}
finishedAt={new Date(2019, 5)}
responsibilities={[
<>
Led development of a web app for construction software with{" "}
<Tech>React</Tech>.
</>,
<>Implemented a floor plan editor and site management tools.</>,
<>
Managed infrastructure with <Tech>AWS</Tech> and{" "}
<Tech>Terraform</Tech>.
</>,
]}
/>
</ResumeSection>
</>
)
The ResumeSection
component separates sections with a line, positioning the title in the center using position: absolute
and centerContent
. It organizes the content in a two-column layout with the UniformColumnGrid
component.
import { centerContent } from "@lib/ui/css/centerContent"
import { horizontalPadding } from "@lib/ui/css/horizontalPadding"
import { round } from "@lib/ui/css/round"
import { vStack } from "@lib/ui/css/stack"
import { UniformColumnGrid } from "@lib/ui/css/uniformColumnGrid"
import { ComponentWithChildrenProps, UIComponentProps } from "@lib/ui/props"
import { text } from "@lib/ui/text"
import { getColor } from "@lib/ui/theme/getters"
import { ReactNode } from "react"
import styled from "styled-components"
type ResumeSectionProps = ComponentWithChildrenProps &
UIComponentProps & {
title: ReactNode
}
const Container = styled.div`
${vStack({
gap: 20,
})}
position: relative;
`
const TitleContainer = styled.div`
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 1px;
background: ${getColor("foregroundExtra")};
${centerContent};
`
const Title = styled.div`
height: 32px;
${horizontalPadding(20)};
border: 1px solid ${getColor("foregroundExtra")};
background: ${getColor("background")};
${text({
color: "primary",
weight: 600,
})}
${centerContent};
${round};
`
export const ResumeSection = ({
title,
children,
...rest
}: ResumeSectionProps) => (
<Container {...rest}>
<TitleContainer>
<Title>{title}</Title>
</TitleContainer>
<UniformColumnGrid minChildrenWidth={240} maxColumns={2} gap={40}>
{children}
</UniformColumnGrid>
</Container>
)
Defining Job Experience Properties
The JobExperience
component takes in a set of properties that together shape how each professional role is presented. Here’s what each prop represents:
- position: The specific role you held at the company.
- company: The organization where you worked.
- companyIcon: An optional icon or symbol representing the company.
- startedAt: The start date of your employment.
- finishedAt: The end date of your employment, if applicable.
- responsibilities: An array listing the duties, achievements, or contributions you made in the role.
import { HStack, VStack } from "@lib/ui/css/stack"
import { dotSeparator } from "@lib/ui/layout/StackSeparatedBy"
import { Text } from "@lib/ui/text"
import { ReactNode } from "react"
import { intervalToDuration } from "date-fns"
type JobExperienceProps = {
position: string
company: string
companyIcon?: ReactNode
startedAt: Date
finishedAt?: Date
responsibilities: ReactNode[]
}
const { format: formatDate } = new Intl.DateTimeFormat("en-US", {
month: "short",
year: "numeric",
})
function formatExperienceDuration(startedAt: Date, finishedAt?: Date): string {
const endDate = finishedAt ?? new Date()
const { years = 0, months = 0 } = intervalToDuration({
start: startedAt,
end: endDate,
})
const parts = []
if (years) {
parts.push(`${years} ${years === 1 ? "year" : "years"}`)
}
if (months) {
parts.push(`${months} ${months === 1 ? "month" : "months"}`)
}
if (parts.length === 0) {
parts.push("Less than a month")
}
return parts.join(" ")
}
export const JobExperience = ({
position,
company,
responsibilities,
startedAt,
finishedAt,
companyIcon,
}: JobExperienceProps) => {
return (
<VStack gap={8}>
<Text centerVertically={{ gap: 4 }} color="primary" size={16}>
<span>{position}</span>
<Text as="span" color="shy">
at
</Text>
<Text nowrap centerVertically={{ gap: 8 }} as="span">
{company}
{companyIcon}
</Text>
</Text>
<HStack alignItems="center" gap={8}>
<Text weight="500" size={14}>
{formatExperienceDuration(startedAt, finishedAt)}
</Text>
<Text size={14} color="supporting">
({formatDate(startedAt)} -{" "}
{finishedAt ? formatDate(finishedAt) : "Present"})
</Text>
</HStack>
<VStack gap={8}>
{responsibilities.map((responsibility, index) => (
<HStack key={index} gap={4}>
<Text color="shy">{dotSeparator}</Text>
<Text color="supporting">{responsibility}</Text>
</HStack>
))}
</VStack>
</VStack>
)
}
At the top, the component highlights the position and the company’s name, optionally displaying the company’s icon if available. Just beneath this header, the employment duration is shown in years and months, followed by the precise start and end dates. Each responsibility is then presented as a bullet point, separated by a dot, ensuring clear and easy-to-scan content.
Using AI Prompts for Responsibilities
To populate the responsibilities
prop for each job, use a dedicated prompt that directs the AI to incorporate the Tech
component for highlighting technologies and the ProductLink
component for linking to previously mentioned projects.
Fill the `responsibilities` prop for each position in my resume with concise, impactful bullet points—no more than four per role. Reduce the number of points if I had fewer contributions in that position due to shorter tenure.
When listing responsibilities, you may reference projects using the `ProductLink` component, but only if the project’s URL has been mentioned previously. The `ProductLink` component takes a `to` prop for the URL. To highlight a technology, use the `Tech` component. The `responsibilities` prop expects an array of `React.Node`. Do not repeat the same technology within a single job. You do not need to highlight every technology used in that role; use your best judgment.
Featuring Projects and Achievements
After the job experience section, we move on to the Projects
section, which includes three items: my productivity app, Increaser; a full-stack development toolkit, RadzionKit; and my YouTube channel, Radzion Dev.
import { PersonalProject } from "@lib/resume-ui/components/PersonalProject"
import { ResumeSection } from "@lib/resume-ui/components/ResumeSection"
import { GithubRepoResumeItem } from "@lib/resume-ui/github/components/GithubRepoResumeItem"
import { VStack } from "@lib/ui/css/stack"
import { IncreaserIcon } from "./IncreaserIcon"
import { useApiQuery } from "@product/api-ui/hooks/useApiQuery"
import { ProjectPrimaryStat } from "@lib/resume-ui/components/ProjectPrimaryStat"
import { YouTubeColoredIcon } from "@lib/ui/icons/YouTubeColoredIcon"
import { MatchQuery } from "@lib/ui/query/components/MatchQuery"
import { Tech } from "@lib/resume-ui/components/Tech"
export const Projects = () => {
const query = useApiQuery("stats", undefined)
return (
<ResumeSection style={{ flex: 1 }} title={"My Projects"}>
<PersonalProject
icon={<IncreaserIcon />}
name="Increaser"
url="https://increaser.org"
description={
<>
A productivity toolkit built with <Tech>TypeScript</Tech> and{" "}
<Tech>React</Tech>, featuring tools for planning, time tracking, and
habits. Deployed on <Tech>AWS</Tech> using <Tech>Terraform</Tech>.
</>
}
/>
<VStack gap={20}>
<GithubRepoResumeItem
value={{
owner: "radzionc",
name: "radzionkit",
}}
name="RadzionKit"
description={
<>
A full-stack development toolkit with reusable components, hooks,
and utilities for <Tech>React</Tech>,<Tech>Node.js</Tech>, and{" "}
<Tech>DynamoDB</Tech>. Built as a <Tech>TypeScript</Tech>{" "}
monorepo.
</>
}
/>
<PersonalProject
icon={<YouTubeColoredIcon />}
name="Radzion Dev"
url="https://www.youtube.com/c/radzion"
description={
<>
A YouTube channel covering <Tech>React</Tech>,{" "}
<Tech>TypeScript</Tech>, and <Tech>AWS</Tech>, with practical
content for web developers.
</>
}
primaryStat={
<MatchQuery
value={query}
success={({ devChannelSubscribers }) => (
<ProjectPrimaryStat>
{devChannelSubscribers} subscribers
</ProjectPrimaryStat>
)}
/>
}
/>
</VStack>
</ResumeSection>
)
}
Each personal project is represented by the PersonalProject
component, which is responsible for displaying the project’s key details in a structured and engaging format. Here are the props it accepts:
- name: The project’s title, prominently displayed.
- url: A link to the live project or its repository.
- description: A brief summary of the project’s purpose and features.
- primaryStat: A highlight or key metric that provides additional insight into the project’s impact or performance.
- icon: An optional visual element, such as a logo or icon, to help brand or differentiate the project.
import { ExternalLink } from "@lib/ui/navigation/Link/ExternalLink"
import { HStack, VStack } from "@lib/ui/css/stack"
import { text, Text } from "@lib/ui/text"
import styled from "styled-components"
import { getColor } from "@lib/ui/theme/getters"
import { ReactNode } from "react"
import { toSizeUnit } from "@lib/ui/css/toSizeUnit"
import { resumeConfig } from "./config"
interface Props {
name: string
url: string
description: ReactNode
primaryStat?: ReactNode
icon?: ReactNode
}
const Title = styled.p`
border-bottom: 1px dashed;
${text({
centerVertically: true,
color: "contrast",
weight: 600,
})}
height: ${toSizeUnit(resumeConfig.personalProjectTitleHeight)};
`
const Link = styled(ExternalLink)`
&:hover ${Title} {
color: ${getColor("textPrimary")};
}
`
export const PersonalProject = ({
name,
url,
description,
primaryStat,
icon,
}: Props) => {
return (
<VStack alignItems="start" gap={4}>
<HStack alignItems="center" gap={16}>
<Link to={url}>
<HStack alignItems="center" gap={8}>
{icon}
<Title>{name}</Title>
</HStack>
</Link>
{primaryStat}
</HStack>
<Text color="supporting">{description}</Text>
</VStack>
)
}
Here, we also use an AI prompt to generate the description for each project.
Fill the `description` prop for each project in my resume. Keep the descriptions short and effective. To highlight a technology, use the `Tech` component. The `description` prop accepts a `React.Node`.
Adding Dynamic Statistics
To make the resume more engaging, consider adding a dynamic primary statistic—such as the number of GitHub stars a project has. This is achieved through the useGithubRepoQuery
hook, which fetches the star count directly from the public GitHub API without any need for authentication. Incorporating a live metric like this can lend authenticity and real-time relevance to your resume, helping you stand out from the crowd.
import { useQuery } from "@tanstack/react-query"
import { GithubRepo } from "../GithubRepo"
import { queryUrl } from "@lib/utils/query/queryUrl"
interface GitHubRepoResponse {
stargazers_count: number
}
export function useGithubRepoQuery({ owner, name }: GithubRepo) {
return useQuery({
queryKey: ["repo", owner, name],
queryFn: () =>
queryUrl<GitHubRepoResponse>(
`https://api.github.com/repos/${owner}/${name}`
),
})
}
Incorporating YouTube Subscriber Counts
When it comes to YouTube, directly querying subscriber counts from the front-end isn’t as straightforward. To handle this, we’ve set up a simple API endpoint that returns the current number of subscribers for the Radzion Dev channel. We won’t dive into the technical details of creating this endpoint here—if you’re interested, check out my dedicated video and accompanying guide on how to easily implement an API within a TypeScript repository here.
import { assertFetchResponse } from "@lib/utils/fetch/assertFetchResponse"
import { getEnvVar } from "../../getEnvVar"
import { ApiResolver } from "../../resolvers/ApiResolver"
import { addQueryParams } from "@lib/utils/query/addQueryParams"
const youTubeChannelId = "UC15iv69GgcJWa8GoohNQzpw"
export const stats: ApiResolver<"stats"> = async () => {
const url = addQueryParams("https://www.googleapis.com/youtube/v3/channels", {
part: "statistics",
id: youTubeChannelId,
key: getEnvVar("YOUTUBE_API_KEY"),
})
const response = await fetch(url)
await assertFetchResponse(response)
const data = await response.json()
if (!data.items || data.items.length === 0) {
throw new Error(`No statistics found for channelId: ${youTubeChannelId}`)
}
const subscriberCount = parseInt(data.items[0].statistics.subscriberCount, 10)
return {
devChannelSubscribers: subscriberCount,
}
}
Presenting Contact Information
Finally, we display the contacts in the footer using ResumeFooterLink
components. Each link includes an icon, the contact's name, and a URL.
import { ResumeFooterLink } from "@lib/resume-ui/components/ResumeFooterLink"
import { HStack } from "@lib/ui/css/stack"
import { GitHubIcon } from "@lib/ui/icons/GitHubIcon"
import { LinkedinIcon } from "@lib/ui/icons/LinkedinIcon"
import { MailIcon } from "@lib/ui/icons/MailIcon"
import { TelegramIcon } from "@lib/ui/icons/TelegramIcon"
import { XIcon } from "@lib/ui/icons/XIcon"
import {
email,
xHandle,
linkedInHandle,
githubHandle,
telegramHandle,
} from "@product/config"
export const Contacts = () => (
<HStack
fullWidth
justifyContent="space-between"
alignItems="center"
wrap="wrap"
gap={16}
>
<ResumeFooterLink
icon={<MailIcon />}
name={email}
url={`mailto:${email}`}
/>
<ResumeFooterLink
icon={<XIcon />}
name={xHandle}
url={`https://twitter.com/${xHandle}`}
/>
<ResumeFooterLink
icon={<LinkedinIcon />}
name={linkedInHandle}
url={`https://www.linkedin.com/in/${linkedInHandle}`}
/>
<ResumeFooterLink
icon={<GitHubIcon />}
name={githubHandle}
url={`https://github.com/${githubHandle}`}
/>
<ResumeFooterLink
icon={<TelegramIcon />}
name={telegramHandle}
url={`https://t.me/${telegramHandle}`}
/>
</HStack>
)
Deploying to AWS
Our website is just a single static page, so we will deploy it to AWS S3 and serve it through CloudFront. To setup most of the infrastructure we'll use Terraform, we also won't get into the details about deploying the website here, since there is a dedicated post about it here.
#!/bin/zsh -e
# Required environment variables:
# - BUCKET: S3 bucket name
# - DISTRIBUTION_ID: CloudFront distribution ID
yarn build
OUT_DIR=out
aws s3 sync $OUT_DIR s3://$BUCKET/ \
--delete \
--exclude $OUT_DIR/sw.js \
--exclude "*.html" \
--metadata-directive REPLACE \
--cache-control max-age=31536000,public \
--acl public-read
aws s3 cp $OUT_DIR s3://$BUCKET/ \
--exclude "*" \
--include "*.html" \
--include "$OUT_DIR/sw.js" \
--metadata-directive REPLACE \
--cache-control max-age=0,no-cache,no-store,must-revalidate \
--acl public-read \
--recursive
process_html_file() {
file_path="$1"
relative_path="${file_path#$OUT_DIR/}"
file_name="${relative_path%.html}"
aws s3 cp s3://$BUCKET/$file_name.html s3://$BUCKET/$file_name &
}
find $OUT_DIR -type f -name "*.html" | while read -r html_file; do
process_html_file "$html_file"
done
wait # Wait for all background jobs to complete
aws configure set preview.cloudfront true
aws cloudfront create-invalidation --distribution-id $DISTRIBUTION_ID --paths "/*"
Conclusion
In this guide, we’ve explored how to build a single-page, PDF-exportable resume using React, TypeScript, and Next.js. We covered setting meta tags for better SEO, enhancing presentation with dynamic stats, and deploying to AWS. With a solid foundation and helpful prompts, you now have the tools to effortlessly create and maintain a professional, standout resume.
Top comments (0)