design nosepass template, add tests, add template previews

This commit is contained in:
Amruth Pillai
2023-11-17 08:31:12 +01:00
parent 0b4cb71320
commit 34247f13b6
92 changed files with 24440 additions and 35518 deletions

View File

@ -55,7 +55,7 @@ STORAGE_SECRET_KEY=minioadmin
REDIS_URL=redis://default:password@localhost:6379
# Sentry (for error reporting, Optional)
# SENTRY_DSN=
# VITE_SENTRY_DSN=
# Crowdin (Optional)
CROWDIN_PROJECT_ID=

1
.gitignore vendored
View File

@ -39,6 +39,7 @@ Thumbs.db
# Generated Files
.nx
.swc
fly.toml
stats.html
libs/prisma

View File

@ -10,7 +10,5 @@
"tools/compose/*"
]
},
"i18n-ally.localesPaths": [
"apps/client/src/locales"
]
"i18n-ally.localesPaths": ["apps/client/src/locales"]
}

View File

@ -1,36 +1,38 @@
# --- Base Image ---
FROM node:bullseye-slim AS base
WORKDIR /app
ENV PNPM_HOME="/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
ARG NX_CLOUD_ACCESS_TOKEN
RUN corepack enable
WORKDIR /app
# --- Build Image ---
FROM base AS build
ENV NX_CLOUD_ACCESS_TOKEN=$NX_CLOUD_ACCESS_TOKEN
COPY .npmrc package.json package-lock.json ./
RUN npm ci && npm cache clean --force
COPY .npmrc package.json pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
COPY . .
RUN npm run build
RUN pnpm build
# --- Release Image ---
FROM base AS release
RUN apt update && apt install -y dumb-init --no-install-recommends
COPY --chown=node:node --from=build /app/.npmrc /app/package.json /app/package-lock.json ./
RUN npm ci --omit=dev && npm cache clean --force
COPY --chown=node:node --from=build /app/.npmrc /app/package.json /app/pnpm-lock.yaml ./
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod --frozen-lockfile
# Copy Build Output
COPY --chown=node:node --from=build /app/dist ./dist
# Copy Prisma Generated Client
COPY --chown=node:node --from=build /app/node_modules/.prisma/client ./node_modules/.prisma/client
# Copy Prisma Schema & Migrations
COPY --chown=node:node --from=build /app/tools/prisma ./tools/prisma
RUN pnpm prisma:generate
EXPOSE 3000
CMD [ "dumb-init", "npm", "run", "start" ]
CMD [ "dumb-init", "pnpm", "start" ]

View File

@ -1,4 +1,4 @@
![Reactive Resume](https://res.cloudinary.com/amruth-pillai/image/upload/v1699180255/reactive-resume/readme/banner_zvieca.png)
![Reactive Resume](https://i.imgur.com/FFc4nyZ.jpg)
![App Version](https://img.shields.io/github/package-json/version/AmruthPillai/Reactive-Resume/v4?label=version)
[![Docker Pulls](https://img.shields.io/docker/pulls/amruthpillai/reactive-resume)](https://hub.docker.com/repository/docker/amruthpillai/reactive-resume)

View File

@ -8,6 +8,7 @@ type PictureProps = {
export const Picture = ({ className }: PictureProps) => {
const picture = useArtboardStore((state) => state.resume.basics.picture);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
if (!isUrl(picture.url) || picture.effects.hidden) return null;
@ -15,11 +16,17 @@ export const Picture = ({ className }: PictureProps) => {
<img
src={picture.url}
alt="Profile"
className={cn("relative z-20 object-cover", className)}
className={cn(
"relative z-20 object-cover",
picture.effects.border && "border-primary",
picture.effects.grayscale && "grayscale",
className,
)}
style={{
maxWidth: `${picture.size}px`,
aspectRatio: `${picture.aspectRatio}`,
borderRadius: `${picture.borderRadius}px`,
borderWidth: `${picture.effects.border ? fontSize / 3 : 0}px`,
}}
/>
);

View File

@ -1,5 +1,5 @@
import { SectionKey } from "@reactive-resume/schema";
import { pageSizeMap, TemplateKey } from "@reactive-resume/utils";
import { pageSizeMap, Template } from "@reactive-resume/utils";
import { AnimatePresence, motion } from "framer-motion";
import { useEffect, useMemo, useRef } from "react";
import { ReactZoomPanPinchRef, TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
@ -12,7 +12,7 @@ export const BuilderLayout = () => {
const transformRef = useRef<ReactZoomPanPinchRef>(null);
const format = useArtboardStore((state) => state.resume.metadata.page.format);
const layout = useArtboardStore((state) => state.resume.metadata.layout);
const template = useArtboardStore((state) => state.resume.metadata.template as TemplateKey);
const template = useArtboardStore((state) => state.resume.metadata.template as Template);
const Template = useMemo(() => getTemplate(template), [template]);

View File

@ -1,5 +1,5 @@
import { SectionKey } from "@reactive-resume/schema";
import { TemplateKey } from "@reactive-resume/utils";
import { Template } from "@reactive-resume/utils";
import { useMemo } from "react";
import { Page } from "../components/page";
@ -8,7 +8,7 @@ import { getTemplate } from "../templates";
export const PreviewLayout = () => {
const layout = useArtboardStore((state) => state.resume.metadata.layout);
const template = useArtboardStore((state) => state.resume.metadata.template as TemplateKey);
const template = useArtboardStore((state) => state.resume.metadata.template as Template);
const Template = useMemo(() => getTemplate(template), [template]);

View File

@ -19,5 +19,5 @@
}
.wysiwyg {
@apply prose max-w-none text-current prose-headings:mt-0 prose-headings:mb-2 prose-p:mt-0 prose-p:mb-2 prose-ul:mt-0 prose-ul:mb-2 prose-li:mt-0 prose-li:mb-2 prose-ol:mt-0 prose-ol:mb-2 prose-img:mt-0 prose-img:mb-2 prose-hr:mt-0 prose-hr:mb-2 prose-p:leading-normal;
@apply prose max-w-none text-current prose-headings:mt-0 prose-headings:mb-2 prose-p:mt-0 prose-p:mb-2 prose-ul:mt-0 prose-ul:mb-2 prose-li:mt-0 prose-li:mb-2 prose-ol:mt-0 prose-ol:mb-2 prose-img:mt-0 prose-img:mb-2 prose-hr:mt-0 prose-hr:mb-2 prose-p:leading-normal prose-li:leading-normal;
}

View File

@ -79,11 +79,11 @@ const Summary = () => {
return (
<section id={section.id}>
<div className="mb-2 hidden font-bold uppercase text-primary group-[.main]:block">
<div className="mb-2 hidden font-bold text-primary group-[.main]:block">
<h4>{section.name}</h4>
</div>
<div className="mb-2 hidden items-center gap-x-2 text-center font-bold uppercase text-primary group-[.sidebar]:flex">
<div className="mb-2 hidden items-center gap-x-2 text-center font-bold text-primary group-[.sidebar]:flex">
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
<h4>{section.name}</h4>
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
@ -162,18 +162,18 @@ const Section = <T,>({
return (
<section id={section.id} className="grid">
<div className="mb-2 hidden font-bold uppercase text-primary group-[.main]:block">
<div className="mb-2 hidden font-bold text-primary group-[.main]:block">
<h4>{section.name}</h4>
</div>
<div className="mx-auto mb-2 hidden items-center gap-x-2 text-center font-bold uppercase text-primary group-[.sidebar]:flex">
<div className="mx-auto mb-2 hidden items-center gap-x-2 text-center font-bold text-primary group-[.sidebar]:flex">
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
<h4>{section.name}</h4>
<div className="h-1.5 w-1.5 rounded-full border border-primary" />
</div>
<div
className="grid gap-3 group-[.sidebar]:mx-auto group-[.sidebar]:text-center"
className="grid gap-x-6 gap-y-3 group-[.sidebar]:mx-auto group-[.sidebar]:text-center"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
@ -193,19 +193,19 @@ const Section = <T,>({
className,
)}
>
<div className="leading-snug">{children?.(item as T)}</div>
<div>{children?.(item as T)}</div>
{summary && !isEmptyString(summary) && (
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
)}
{level && level > 0 && <Rating level={level} />}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords && keywords.length > 0 && (
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
{url && <Link url={url} />}
{url !== undefined && <Link url={url} />}
<div className="absolute left-[-4.5px] top-px hidden h-[8px] w-[8px] rounded-full bg-primary group-[.main]:block" />
</div>
@ -223,7 +223,7 @@ const Profiles = () => {
return (
<Section<Profile> section={section}>
{(item) => (
<div className="leading-snug">
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
@ -258,7 +258,7 @@ const Experience = () => {
<div className="font-bold">{item.company}</div>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -276,7 +276,7 @@ const Education = () => {
<div>{item.area}</div>
<div>{item.score}</div>
<div>{item.studyType}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -292,7 +292,7 @@ const Awards = () => {
<div>
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -308,7 +308,7 @@ const Certifications = () => {
<div>
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -349,7 +349,7 @@ const Publications = () => {
<div>
<div className="font-bold">{item.name}</div>
<div>{item.publisher}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -366,7 +366,7 @@ const Volunteer = () => {
<div className="font-bold">{item.organization}</div>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -377,11 +377,11 @@ const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="fluencyLevel">
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.fluency}</div>
<div>{item.description}</div>
</div>
)}
</Section>
@ -399,7 +399,7 @@ const Projects = () => {
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
@ -438,7 +438,7 @@ const Custom = ({ id }: { id: string }) => {
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
<div>{item.location}</div>
</div>
</div>

View File

@ -158,7 +158,7 @@ const Section = <T,>({
</div>
<div
className="col-span-4 grid gap-3"
className="col-span-4 grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
@ -171,18 +171,18 @@ const Section = <T,>({
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div className="leading-snug">
<div>
{children?.(item as T)}
{url && <Link url={url} />}
{url !== undefined && <Link url={url} />}
</div>
{summary && !isEmptyString(summary) && (
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
)}
{level && level > 0 && <Rating level={level} />}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords && keywords.length > 0 && (
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
@ -200,7 +200,7 @@ const Profiles = () => {
return (
<Section<Profile> section={section}>
{(item) => (
<div className="leading-snug">
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
@ -384,11 +384,11 @@ const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="fluencyLevel">
<Section<Language> section={section} levelKey="level">
{(item) => (
<div className="space-y-0.5">
<div className="font-bold">{item.name}</div>
<div>{item.fluency}</div>
<div>{item.description}</div>
</div>
)}
</Section>

View File

@ -7,6 +7,7 @@ import {
Experience,
Interest,
Language,
Profile,
Project,
Publication,
Reference,
@ -26,8 +27,6 @@ import { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
const profiles = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<div className="grid grid-cols-3">
@ -71,31 +70,6 @@ const Header = () => {
</div>
))}
</div>
{profiles.visible && profiles.items.length > 0 && (
<div className="flex items-center gap-x-3 gap-y-0.5">
{profiles.items
.filter((item) => item.visible)
.map((item) => (
<div key={item.id} className="flex items-center gap-x-2">
<Link
url={item.url}
label={item.username}
className="text-sm"
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
</div>
))}
</div>
)}
</div>
</div>
</div>
@ -109,7 +83,7 @@ const Summary = () => {
return (
<section id={section.id}>
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold uppercase">{section.name}</h4>
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
className="wysiwyg"
@ -184,10 +158,10 @@ const Section = <T,>({
return (
<section id={section.id} className="grid">
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold uppercase">{section.name}</h4>
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
className="grid gap-3"
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
@ -200,18 +174,18 @@ const Section = <T,>({
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div className="leading-snug">
<div>
{children?.(item as T)}
{url && <Link url={url} />}
{url !== undefined && <Link url={url} />}
</div>
{summary && !isEmptyString(summary) && (
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
)}
{level && level > 0 && <Rating level={level} />}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords && keywords.length > 0 && (
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
@ -267,6 +241,38 @@ const Education = () => {
);
};
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
label={item.username}
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
@ -381,11 +387,11 @@ const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="fluencyLevel">
<Section<Language> section={section} levelKey="level">
{(item) => (
<div className="space-y-0.5">
<div className="font-bold">{item.name}</div>
<div>{item.fluency}</div>
<div>{item.description}</div>
</div>
)}
</Section>
@ -457,6 +463,8 @@ const Custom = ({ id }: { id: string }) => {
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles":
return <Profiles />;
case "summary":
return <Summary />;
case "experience":

View File

@ -101,7 +101,7 @@ const Summary = () => {
return (
<section id={section.id}>
<h4 className="mb-2 text-base font-bold uppercase">{section.name}</h4>
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
<div
className="wysiwyg"
@ -173,10 +173,10 @@ const Section = <T,>({
return (
<section id={section.id} className="grid">
<h4 className="mb-2 text-base font-bold uppercase">{section.name}</h4>
<h4 className="mb-2 text-base font-bold">{section.name}</h4>
<div
className="grid gap-3"
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
@ -193,21 +193,21 @@ const Section = <T,>({
className={cn("relative space-y-2 pl-4 group-[.sidebar]:pl-0", className)}
>
<div className="relative -ml-4 group-[.sidebar]:ml-0">
<div className="pl-4 leading-snug group-[.sidebar]:pl-0">
<div className="pl-4 group-[.sidebar]:pl-0">
{children?.(item as T)}
{url && <Link url={url} />}
{url !== undefined && <Link url={url} />}
</div>
<div className="absolute inset-y-0 -left-px border-l-[4px] border-primary group-[.sidebar]:hidden" />
</div>
{summary && !isEmptyString(summary) && (
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
)}
{level && level > 0 && <Rating level={level} />}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords && keywords.length > 0 && (
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
@ -227,7 +227,7 @@ const Profiles = () => {
return (
<Section<Profile> section={section}>
{(item) => (
<div className="leading-snug">
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
@ -411,11 +411,11 @@ const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="fluencyLevel">
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.fluency}</div>
<div>{item.description}</div>
</div>
)}
</Section>

View File

@ -1,15 +1,16 @@
import { TemplateKey } from "@reactive-resume/utils";
import { Template } from "@reactive-resume/utils";
import { Azurill } from "./azurill";
import { Bronzor } from "./bronzor";
import { Chikorita } from "./chikorita";
import { Ditto } from "./ditto";
import { Kakuna } from "./kakuna";
import { Nosepass } from "./nosepass";
import { Onyx } from "./onyx";
import { Pikachu } from "./pikachu";
import { Rhyhorn } from "./rhyhorn";
export const getTemplate = (template: TemplateKey) => {
export const getTemplate = (template: Template) => {
switch (template) {
case "onyx":
return Onyx;
@ -27,6 +28,8 @@ export const getTemplate = (template: TemplateKey) => {
return Bronzor;
case "pikachu":
return Pikachu;
case "nosepass":
return Nosepass;
default:
return Onyx;
}

View File

@ -105,7 +105,7 @@ const Summary = () => {
return (
<section id={section.id}>
<h4 className="mb-2 border-b border-primary text-center font-bold uppercase text-primary">
<h4 className="mb-2 border-b border-primary text-center font-bold text-primary">
{section.name}
</h4>
@ -179,12 +179,12 @@ const Section = <T,>({
return (
<section id={section.id} className="grid">
<h4 className="mb-2 border-b border-primary text-center font-bold uppercase text-primary">
<h4 className="mb-2 border-b border-primary text-center font-bold text-primary">
{section.name}
</h4>
<div
className="grid gap-3"
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
@ -197,19 +197,19 @@ const Section = <T,>({
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div className="leading-snug">{children?.(item as T)}</div>
<div>{children?.(item as T)}</div>
{summary && !isEmptyString(summary) && (
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
)}
{level && level > 0 && <Rating level={level} />}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords && keywords.length > 0 && (
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
{url && <Link url={url} />}
{url !== undefined && <Link url={url} />}
</div>
);
})}
@ -228,7 +228,7 @@ const Experience = () => {
<div className="font-bold">{item.company}</div>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -246,7 +246,7 @@ const Education = () => {
<div>{item.area}</div>
<div>{item.score}</div>
<div>{item.studyType}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -262,7 +262,7 @@ const Awards = () => {
<div>
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -278,7 +278,7 @@ const Certifications = () => {
<div>
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -319,7 +319,7 @@ const Publications = () => {
<div>
<div className="font-bold">{item.name}</div>
<div>{item.publisher}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -336,7 +336,7 @@ const Volunteer = () => {
<div className="font-bold">{item.organization}</div>
<div>{item.position}</div>
<div>{item.location}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
)}
</Section>
@ -347,11 +347,11 @@ const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="fluencyLevel">
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.fluency}</div>
<div>{item.description}</div>
</div>
)}
</Section>
@ -368,7 +368,7 @@ const Projects = () => {
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
</div>
)}
@ -407,7 +407,7 @@ const Custom = ({ id }: { id: string }) => {
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
<div>{item.location}</div>
<div className="font-bold italic">{item.date}</div>
<div className="font-bold">{item.date}</div>
</div>
</div>
)}

View File

@ -0,0 +1,529 @@
import {
Award,
Certification,
CustomSection,
CustomSectionGroup,
Education,
Experience,
Interest,
Language,
Profile,
Project,
Publication,
Reference,
SectionKey,
SectionWithItem,
Skill,
URL,
Volunteer,
} from "@reactive-resume/schema";
import { cn, isEmptyString, isUrl } from "@reactive-resume/utils";
import get from "lodash.get";
import { Fragment } from "react";
import { Picture } from "../components/picture";
import { useArtboardStore } from "../store/artboard";
import { TemplateProps } from "../types/template";
const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
return (
<div className="grid grid-cols-4 gap-x-6">
<div className="mt-1 space-y-2 text-right">
<p className="font-medium text-primary">Personal Information</p>
<Picture className="ml-auto" />
</div>
<div className="col-span-3 space-y-2">
<div>
<div className="text-2xl font-bold">{basics.name}</div>
<div className="text-base">{basics.headline}</div>
</div>
<div className="space-y-1 text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-map-pin text-primary" />
<div>{basics.location}</div>
</div>
)}
{basics.phone && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-phone text-primary" />
<a href={`tel:${basics.phone}`} target="_blank" rel="noreferrer">
{basics.phone}
</a>
</div>
)}
{basics.email && (
<div className="flex items-center gap-x-1.5">
<i className="ph ph-bold ph-at text-primary" />
<a href={`mailto:${basics.email}`} target="_blank" rel="noreferrer">
{basics.email}
</a>
</div>
)}
<Link url={basics.url} />
</div>
<div className="flex flex-wrap gap-x-3 text-sm">
{basics.customFields.map((item) => (
<div key={item.id} className="flex items-center gap-x-1.5">
<i className={cn(`ph ph-bold ph-${item.icon}`, "text-primary")} />
<span className="text-primary">{item.name}</span>
<span>{item.value}</span>
</div>
))}
</div>
</div>
</div>
);
};
const Summary = () => {
const section = useArtboardStore((state) => state.resume.sections.summary);
if (!section.visible || isEmptyString(section.content)) return null;
return (
<section id={section.id} className="grid grid-cols-4 gap-x-6">
<div className="text-right">
<h4 className="font-medium text-primary">{section.name}</h4>
</div>
<div className="col-span-3">
<div className="relative">
<hr className="mt-3 border-primary pb-3" />
<div className="absolute bottom-3 right-0 h-3 w-3 bg-primary" />
</div>
<div
className="wysiwyg"
style={{ columns: section.columns }}
dangerouslySetInnerHTML={{ __html: section.content }}
/>
</div>
</section>
);
};
type LinkProps = {
url: URL;
icon?: React.ReactNode;
label?: string;
className?: string;
};
const Link = ({ url, icon, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
{icon ?? <i className="ph ph-bold ph-link text-primary" />}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
>
{label || url.label || url.href}
</a>
</div>
);
};
type SectionProps<T> = {
section: SectionWithItem<T> | CustomSectionGroup;
children?: (item: T) => React.ReactNode;
urlKey?: keyof T;
dateKey?: keyof T;
levelKey?: keyof T;
summaryKey?: keyof T;
keywordsKey?: keyof T;
};
const Section = <T,>({
section,
children,
urlKey,
dateKey,
summaryKey,
keywordsKey,
}: SectionProps<T>) => {
if (!section.visible || !section.items.length) return null;
return (
<section id={section.id} className={cn("grid", dateKey !== undefined && "gap-y-4")}>
<div className="grid grid-cols-4 gap-x-6">
<div className="text-right">
<h4 className="font-medium text-primary">{section.name}</h4>
</div>
<div className="col-span-3">
<div className="relative">
<hr className="mt-3 border-primary" />
<div className="absolute bottom-0 right-0 h-3 w-3 bg-primary" />
</div>
</div>
</div>
{dateKey !== undefined && (
<div className="grid grid-cols-4 gap-x-6 gap-y-4">
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const date = (dateKey && get(item, dateKey, "")) as string | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as string[] | undefined;
return (
<Fragment key={item.id}>
<div className="text-right font-medium text-primary">{date}</div>
<div className="col-span-3 space-y-1">
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
)}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
</Fragment>
);
})}
</div>
)}
{dateKey === undefined && (
<div className="grid grid-cols-4 gap-x-6">
<div
className="col-span-3 col-start-2 grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
.filter((item) => item.visible)
.map((item) => {
const url = (urlKey && get(item, urlKey)) as URL | undefined;
const summary = (summaryKey && get(item, summaryKey, "")) as string | undefined;
const keywords = (keywordsKey && get(item, keywordsKey, [])) as
| string[]
| undefined;
return (
<div key={item.id}>
{children?.(item as T)}
{url !== undefined && <Link url={url} />}
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
)}
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
);
})}
</div>
</div>
)}
</section>
);
};
const Profiles = () => {
const section = useArtboardStore((state) => state.resume.sections.profiles);
const fontSize = useArtboardStore((state) => state.resume.metadata.typography.font.size);
return (
<Section<Profile> section={section}>
{(item) => (
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
label={item.username}
icon={
<img
className="ph"
width={fontSize}
height={fontSize}
alt={item.network}
src={`https://cdn.simpleicons.org/${item.icon}`}
/>
}
/>
) : (
<p>{item.username}</p>
)}
<p className="text-sm">{item.network}</p>
</div>
)}
</Section>
);
};
const Experience = () => {
const section = useArtboardStore((state) => state.resume.sections.experience);
return (
<Section<Experience> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.company}</div>
<div>{item.position}</div>
<div>{item.location}</div>
</div>
)}
</Section>
);
};
const Education = () => {
const section = useArtboardStore((state) => state.resume.sections.education);
return (
<Section<Education> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.institution}</div>
<div>{item.area}</div>
<div>{item.studyType}</div>
<div>{item.score}</div>
</div>
)}
</Section>
);
};
const Awards = () => {
const section = useArtboardStore((state) => state.resume.sections.awards);
return (
<Section<Award> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.title}</div>
<div>{item.awarder}</div>
</div>
)}
</Section>
);
};
const Certifications = () => {
const section = useArtboardStore((state) => state.resume.sections.certifications);
return (
<Section<Certification> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.issuer}</div>
</div>
)}
</Section>
);
};
const Skills = () => {
const section = useArtboardStore((state) => state.resume.sections.skills);
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} keywordsKey="keywords">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
};
const Publications = () => {
const section = useArtboardStore((state) => state.resume.sections.publications);
return (
<Section<Publication> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.publisher}</div>
</div>
)}
</Section>
);
};
const Volunteer = () => {
const section = useArtboardStore((state) => state.resume.sections.volunteer);
return (
<Section<Volunteer> section={section} urlKey="url" dateKey="date" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.organization}</div>
<div>{item.position}</div>
<div>{item.location}</div>
</div>
)}
</Section>
);
};
const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Projects = () => {
const section = useArtboardStore((state) => state.resume.sections.projects);
return (
<Section<Project>
section={section}
urlKey="url"
dateKey="date"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const References = () => {
const section = useArtboardStore((state) => state.resume.sections.references);
return (
<Section<Reference> section={section} urlKey="url" summaryKey="summary">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
)}
</Section>
);
};
const Custom = ({ id }: { id: string }) => {
const section = useArtboardStore((state) => state.resume.sections.custom[id]);
return (
<Section<CustomSection>
section={section}
urlKey="url"
dateKey="date"
summaryKey="summary"
keywordsKey="keywords"
>
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
<div>{item.location}</div>
</div>
)}
</Section>
);
};
const mapSectionToComponent = (section: SectionKey) => {
switch (section) {
case "profiles":
return <Profiles />;
case "summary":
return <Summary />;
case "experience":
return <Experience />;
case "education":
return <Education />;
case "awards":
return <Awards />;
case "certifications":
return <Certifications />;
case "skills":
return <Skills />;
case "interests":
return <Interests />;
case "publications":
return <Publications />;
case "volunteer":
return <Volunteer />;
case "languages":
return <Languages />;
case "projects":
return <Projects />;
case "references":
return <References />;
default:
if (section.startsWith("custom.")) return <Custom id={section.split(".")[1]} />;
return null;
}
};
export const Nosepass = ({ columns, isFirstPage = false }: TemplateProps) => {
const name = useArtboardStore((state) => state.resume.basics.name);
const [main, sidebar] = columns;
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<img alt="Europass Logo" className="h-[48px]" src="https://i.imgur.com/eRK005p.png" />
<p className="font-medium text-primary">Curriculum Vitae</p>
<p className="font-medium text-primary">{name}</p>
</div>
{isFirstPage && <Header />}
<div className="space-y-4">
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
</div>
</div>
);
};

View File

@ -110,7 +110,7 @@ const Summary = () => {
return (
<section id={section.id}>
<h4 className="font-bold uppercase text-primary">{section.name}</h4>
<h4 className="font-bold text-primary">{section.name}</h4>
<div
className="wysiwyg"
@ -182,10 +182,10 @@ const Section = <T,>({
return (
<section id={section.id} className="grid">
<h4 className="font-bold uppercase text-primary">{section.name}</h4>
<h4 className="font-bold text-primary">{section.name}</h4>
<div
className="grid gap-3"
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
@ -198,18 +198,18 @@ const Section = <T,>({
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div className="leading-snug">
<div>
{children?.(item as T)}
{url && <Link url={url} />}
{url !== undefined && <Link url={url} />}
</div>
{summary && !isEmptyString(summary) && (
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
)}
{level && level > 0 && <Rating level={level} />}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords && keywords.length > 0 && (
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
@ -379,11 +379,11 @@ const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="fluencyLevel">
<Section<Language> section={section} levelKey="level">
{(item) => (
<div className="space-y-0.5">
<div className="font-bold">{item.name}</div>
<div>{item.fluency}</div>
<div>{item.description}</div>
</div>
)}
</Section>

View File

@ -34,8 +34,8 @@ const Header = () => {
className="summary group bg-primary px-6 pb-7 pt-6 text-background"
style={{ borderRadius: `calc(${borderRadius}px - 2px)` }}
>
<div className="col-span-2 space-y-4">
<div className="leading-tight">
<div className="col-span-2 space-y-2.5">
<div>
<h2 className="text-2xl font-bold">{basics.name}</h2>
<p>{basics.headline}</p>
</div>
@ -102,7 +102,7 @@ const Summary = () => {
return (
<section id={section.id}>
<h4 className="mb-2 text-base font-bold uppercase">{section.name}</h4>
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
<div
className="wysiwyg"
@ -182,10 +182,10 @@ const Section = <T,>({
return (
<section id={section.id} className="grid">
<h4 className="mb-2 border-b border-primary text-base font-bold uppercase">{section.name}</h4>
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
<div
className="grid gap-3"
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
@ -198,18 +198,18 @@ const Section = <T,>({
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div className="leading-snug">
<div>
{children?.(item as T)}
{url && <Link url={url} />}
{url !== undefined && <Link url={url} />}
</div>
{summary && !isEmptyString(summary) && (
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
)}
{level && level > 0 && <Rating level={level} />}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords && keywords.length > 0 && (
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
@ -227,7 +227,7 @@ const Profiles = () => {
return (
<Section<Profile> section={section}>
{(item) => (
<div className="leading-snug">
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
@ -358,7 +358,7 @@ const Interests = () => {
const section = useArtboardStore((state) => state.resume.sections.interests);
return (
<Section<Interest> section={section} className="space-y-0" keywordsKey="keywords">
<Section<Interest> section={section} className="space-y-1" keywordsKey="keywords">
{(item) => <div className="font-bold">{item.name}</div>}
</Section>
);
@ -411,11 +411,11 @@ const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="fluencyLevel">
<Section<Language> section={section} levelKey="level">
{(item) => (
<div>
<div className="font-bold">{item.name}</div>
<div>{item.fluency}</div>
<div>{item.description}</div>
</div>
)}
</Section>

View File

@ -82,7 +82,7 @@ const Summary = () => {
return (
<section id={section.id}>
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold uppercase">{section.name}</h4>
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
className="wysiwyg"
@ -154,10 +154,10 @@ const Section = <T,>({
return (
<section id={section.id} className="grid">
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold uppercase">{section.name}</h4>
<h4 className="mb-2 border-b pb-0.5 text-sm font-bold">{section.name}</h4>
<div
className="grid gap-3"
className="grid gap-x-6 gap-y-3"
style={{ gridTemplateColumns: `repeat(${section.columns}, 1fr)` }}
>
{section.items
@ -170,18 +170,18 @@ const Section = <T,>({
return (
<div key={item.id} className={cn("space-y-2", className)}>
<div className="leading-snug">
<div>
{children?.(item as T)}
{url && <Link url={url} />}
{url !== undefined && <Link url={url} />}
</div>
{summary && !isEmptyString(summary) && (
{summary !== undefined && !isEmptyString(summary) && (
<div className="wysiwyg" dangerouslySetInnerHTML={{ __html: summary }} />
)}
{level && level > 0 && <Rating level={level} />}
{level !== undefined && level > 0 && <Rating level={level} />}
{keywords && keywords.length > 0 && (
{keywords !== undefined && keywords.length > 0 && (
<p className="text-sm">{keywords.join(", ")}</p>
)}
</div>
@ -199,7 +199,7 @@ const Profiles = () => {
return (
<Section<Profile> section={section}>
{(item) => (
<div className="leading-snug">
<div>
{isUrl(item.url.href) ? (
<Link
url={item.url}
@ -317,7 +317,7 @@ const Skills = () => {
return (
<Section<Skill> section={section} levelKey="level" keywordsKey="keywords">
{(item) => (
<div className="leading-tight">
<div>
<div className="font-bold">{item.name}</div>
<div>{item.description}</div>
</div>
@ -383,11 +383,11 @@ const Languages = () => {
const section = useArtboardStore((state) => state.resume.sections.languages);
return (
<Section<Language> section={section} levelKey="fluencyLevel">
<Section<Language> section={section} levelKey="level">
{(item) => (
<div className="space-y-0.5">
<div className="font-bold">{item.name}</div>
<div>{item.fluency}</div>
<div>{item.description}</div>
</div>
)}
</Section>

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 99 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

View File

@ -0,0 +1,321 @@
{
"basics": {
"name": "John Doe",
"headline": "Creative and Innovative Web Developer",
"email": "john.doe@gmail.com",
"phone": "(555) 123-4567",
"location": "Pleasantville, CA 94588",
"url": {
"label": "",
"href": "https://johndoe.me/"
},
"customFields": [],
"picture": {
"url": "https://i.imgur.com/HgwyOuJ.jpg",
"size": 120,
"aspectRatio": 1.2,
"borderRadius": 4,
"effects": {
"hidden": false,
"border": false,
"grayscale": false
}
}
},
"sections": {
"summary": {
"name": "Summary",
"columns": 1,
"visible": true,
"id": "summary",
"content": "<p>Innovative Web Developer with 5 years of experience in building impactful and user-friendly websites and applications. Specializes in <strong>front-end technologies</strong> and passionate about modern web standards and cutting-edge development techniques. Proven track record of leading successful projects from concept to deployment.</p>"
},
"awards": {
"name": "Awards",
"columns": 1,
"visible": true,
"id": "awards",
"items": []
},
"certifications": {
"name": "Certifications",
"columns": 1,
"visible": true,
"id": "certifications",
"items": [
{
"id": "spdhh9rrqi1gvj0yqnbqunlo",
"visible": true,
"name": "Full-Stack Web Development",
"issuer": "CodeAcademy",
"date": "2020",
"summary": "",
"url": {
"label": "",
"href": ""
}
},
{
"id": "n838rddyqv47zexn6cxauwqp",
"visible": true,
"name": "AWS Certified Developer",
"issuer": "Amazon Web Services",
"date": "2019",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"education": {
"name": "Education",
"columns": 1,
"visible": true,
"id": "education",
"items": [
{
"id": "yo3p200zo45c6cdqc6a2vtt3",
"visible": true,
"institution": "University of California",
"studyType": "Bachelor's in Computer Science",
"area": "Berkeley, CA",
"score": "",
"date": "August 2012 to May 2016",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"experience": {
"name": "Experience",
"columns": 1,
"visible": true,
"id": "experience",
"items": [
{
"id": "lhw25d7gf32wgdfpsktf6e0x",
"visible": true,
"company": "Creative Solutions Inc.",
"position": "Senior Web Developer",
"location": "San Francisco, CA",
"date": "January 2019 to Present",
"summary": "<ul><li><p>Spearheaded the redesign of the main product website, resulting in a 40% increase in user engagement.</p></li><li><p>Developed and implemented a new responsive framework, improving cross-device compatibility.</p></li><li><p>Mentored a team of four junior developers, fostering a culture of technical excellence.</p></li></ul>",
"url": {
"label": "",
"href": "https://creativesolutions.inc/"
}
},
{
"id": "r6543lil53ntrxmvel53gbtm",
"visible": true,
"company": "TechAdvancers",
"position": "Web Developer",
"location": "San Jose, CA",
"date": "June 2016 to December 2018",
"summary": "<ul><li><p>Collaborated in a team of 10 to develop high-quality web applications using React.js and Node.js.</p></li><li><p>Managed the integration of third-party services such as Stripe for payments and Twilio for SMS services.</p></li><li><p>Optimized application performance, achieving a 30% reduction in load times.</p></li></ul>",
"url": {
"label": "",
"href": "https://techadvancers.com/"
}
}
]
},
"volunteer": {
"name": "Volunteering",
"columns": 1,
"visible": true,
"id": "volunteer",
"items": []
},
"interests": {
"name": "Interests",
"columns": 1,
"visible": true,
"id": "interests",
"items": []
},
"languages": {
"name": "Languages",
"columns": 1,
"visible": true,
"id": "languages",
"items": []
},
"profiles": {
"name": "Profiles",
"columns": 1,
"visible": true,
"id": "profiles",
"items": [
{
"id": "cnbk5f0aeqvhx69ebk7hktwd",
"visible": true,
"network": "LinkedIn",
"username": "johndoe",
"icon": "linkedin",
"url": {
"label": "",
"href": "https://linkedin.com/in/johndoe"
}
},
{
"id": "ukl0uecvzkgm27mlye0wazlb",
"visible": true,
"network": "GitHub",
"username": "johndoe",
"icon": "github",
"url": {
"label": "",
"href": "https://github.com/johndoe"
}
}
]
},
"projects": {
"name": "Projects",
"columns": 1,
"visible": true,
"id": "projects",
"items": [
{
"id": "yw843emozcth8s1ubi1ubvlf",
"visible": true,
"name": "E-Commerce Platform",
"description": "Project Lead",
"date": "",
"summary": "<p>Led the development of a full-stack e-commerce platform, improving sales conversion by 25%.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
},
{
"id": "ncxgdjjky54gh59iz2t1xi1v",
"visible": true,
"name": "Interactive Dashboard",
"description": "Frontend Developer",
"date": "",
"summary": "<p>Created an interactive analytics dashboard for a SaaS application, enhancing data visualization for clients.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
}
]
},
"publications": {
"name": "Publications",
"columns": 1,
"visible": true,
"id": "publications",
"items": []
},
"references": {
"name": "References",
"columns": 1,
"visible": false,
"id": "references",
"items": [
{
"id": "f2sv5z0cce6ztjl87yuk8fak",
"visible": true,
"name": "Available upon request",
"description": "",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"skills": {
"name": "Skills",
"columns": 1,
"visible": true,
"id": "skills",
"items": [
{
"id": "hn0keriukh6c0ojktl9gsgjm",
"visible": true,
"name": "Web Technologies",
"description": "Advanced",
"level": 0,
"keywords": ["HTML5", "JavaScript", "PHP", "Python"]
},
{
"id": "r8c3y47vykausqrgmzwg5pur",
"visible": true,
"name": "Web Frameworks",
"description": "Intermediate",
"level": 0,
"keywords": ["React.js", "Angular", "Vue.js", "Laravel", "Django"]
},
{
"id": "b5l75aseexqv17quvqgh73fe",
"visible": true,
"name": "Tools",
"description": "Intermediate",
"level": 0,
"keywords": ["Webpack", "Git", "Jenkins", "Docker", "JIRA"]
}
]
},
"custom": {}
},
"metadata": {
"template": "azurill",
"layout": [
[
["summary", "experience", "education", "references"],
[
"profiles",
"skills",
"certifications",
"projects",
"interests",
"languages",
"awards",
"volunteer",
"publications"
]
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {
"margin": 14,
"format": "a4",
"options": {
"breakLine": true,
"pageNumbers": true
}
},
"theme": {
"background": "#ffffff",
"text": "#000000",
"primary": "#ca8a04"
},
"typography": {
"font": {
"family": "Merriweather",
"subset": "latin",
"variants": ["regular"],
"size": 13
},
"lineHeight": 1.75,
"hideIcons": false,
"underlineLinks": true
},
"notes": ""
}
}

View File

@ -0,0 +1,346 @@
{
"basics": {
"name": "John Doe",
"headline": "Creative and Innovative Web Developer",
"email": "john.doe@gmail.com",
"phone": "(555) 123-4567",
"location": "Pleasantville, CA 94588",
"url": {
"label": "",
"href": "https://johndoe.me/"
},
"customFields": [],
"picture": {
"url": "https://i.imgur.com/HgwyOuJ.jpg",
"size": 140,
"aspectRatio": 1,
"borderRadius": 4,
"effects": {
"hidden": true,
"border": false,
"grayscale": false
}
}
},
"sections": {
"summary": {
"name": "Summary",
"columns": 1,
"visible": true,
"id": "summary",
"content": "<p>Innovative Web Developer with 5 years of experience in building impactful and user-friendly websites and applications. Specializes in <strong>front-end technologies</strong> and passionate about modern web standards and cutting-edge development techniques. Proven track record of leading successful projects from concept to deployment.</p>"
},
"awards": {
"name": "Awards",
"columns": 1,
"visible": true,
"id": "awards",
"items": []
},
"certifications": {
"name": "Certifications",
"columns": 2,
"visible": true,
"id": "certifications",
"items": [
{
"id": "spdhh9rrqi1gvj0yqnbqunlo",
"visible": true,
"name": "Full-Stack Web Development",
"issuer": "CodeAcademy",
"date": "2020",
"summary": "",
"url": {
"label": "",
"href": ""
}
},
{
"id": "n838rddyqv47zexn6cxauwqp",
"visible": true,
"name": "AWS Certified Developer",
"issuer": "Amazon Web Services",
"date": "2019",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"education": {
"name": "Education",
"columns": 1,
"visible": true,
"id": "education",
"items": [
{
"id": "yo3p200zo45c6cdqc6a2vtt3",
"visible": true,
"institution": "University of California",
"studyType": "Bachelor's in Computer Science",
"area": "Berkeley, CA",
"score": "",
"date": "August 2012 to May 2016",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"experience": {
"name": "Experience",
"columns": 1,
"visible": true,
"id": "experience",
"items": [
{
"id": "lhw25d7gf32wgdfpsktf6e0x",
"visible": true,
"company": "Creative Solutions Inc.",
"position": "Senior Web Developer",
"location": "San Francisco, CA",
"date": "January 2019 to Present",
"summary": "<ul><li><p>Spearheaded the redesign of the main product website, resulting in a 40% increase in user engagement.</p></li><li><p>Developed and implemented a new responsive framework, improving cross-device compatibility.</p></li><li><p>Mentored a team of four junior developers, fostering a culture of technical excellence.</p></li></ul>",
"url": {
"label": "",
"href": "https://creativesolutions.inc/"
}
},
{
"id": "r6543lil53ntrxmvel53gbtm",
"visible": true,
"company": "TechAdvancers",
"position": "Web Developer",
"location": "San Jose, CA",
"date": "June 2016 to December 2018",
"summary": "<ul><li><p>Collaborated in a team of 10 to develop high-quality web applications using React.js and Node.js.</p></li><li><p>Managed the integration of third-party services such as Stripe for payments and Twilio for SMS services.</p></li><li><p>Optimized application performance, achieving a 30% reduction in load times.</p></li></ul>",
"url": {
"label": "",
"href": "https://techadvancers.com/"
}
}
]
},
"volunteer": {
"name": "Volunteering",
"columns": 1,
"visible": true,
"id": "volunteer",
"items": []
},
"interests": {
"name": "Interests",
"columns": 1,
"visible": true,
"id": "interests",
"items": []
},
"languages": {
"name": "Languages",
"columns": 2,
"visible": true,
"id": "languages",
"items": [
{
"id": "ey0c353j3apepzqmyl8zd37c",
"visible": true,
"name": "English",
"description": "Native Speaker",
"level": 0
},
{
"id": "a4dnf32wyj5phgln2rx97qk2",
"visible": true,
"name": "Spanish",
"description": "Intermediate",
"level": 0
}
]
},
"profiles": {
"name": "Profiles",
"columns": 3,
"visible": true,
"id": "profiles",
"items": [
{
"id": "cnbk5f0aeqvhx69ebk7hktwd",
"visible": true,
"network": "LinkedIn",
"username": "johndoe",
"icon": "linkedin",
"url": {
"label": "",
"href": "https://linkedin.com/in/johndoe"
}
},
{
"id": "ukl0uecvzkgm27mlye0wazlb",
"visible": true,
"network": "GitHub",
"username": "johndoe",
"icon": "github",
"url": {
"label": "",
"href": "https://github.com/johndoe"
}
},
{
"id": "k97vdxzmssmgel79km48kxgg",
"visible": true,
"network": "StackOverflow",
"username": "johndoe",
"icon": "stackoverflow",
"url": {
"label": "",
"href": "https://stackoverflow.com/u/johndoe"
}
}
]
},
"projects": {
"name": "Projects",
"columns": 2,
"visible": true,
"id": "projects",
"items": [
{
"id": "yw843emozcth8s1ubi1ubvlf",
"visible": true,
"name": "E-Commerce Platform",
"description": "Project Lead",
"date": "",
"summary": "<p>Led the development of a full-stack e-commerce platform, improving sales conversion by 25%.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
},
{
"id": "ncxgdjjky54gh59iz2t1xi1v",
"visible": true,
"name": "Interactive Dashboard",
"description": "Frontend Developer",
"date": "",
"summary": "<p>Created an interactive analytics dashboard for a SaaS application, enhancing data visualization for clients.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
}
]
},
"publications": {
"name": "Publications",
"columns": 1,
"visible": true,
"id": "publications",
"items": []
},
"references": {
"name": "References",
"columns": 1,
"visible": true,
"id": "references",
"items": [
{
"id": "f2sv5z0cce6ztjl87yuk8fak",
"visible": true,
"name": "Available upon request",
"description": "",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"skills": {
"name": "Skills",
"columns": 3,
"visible": true,
"id": "skills",
"items": [
{
"id": "hn0keriukh6c0ojktl9gsgjm",
"visible": true,
"name": "Web Technologies",
"description": "Advanced",
"level": 0,
"keywords": ["HTML5", "JavaScript", "PHP", "Python"]
},
{
"id": "r8c3y47vykausqrgmzwg5pur",
"visible": true,
"name": "Web Frameworks",
"description": "Intermediate",
"level": 0,
"keywords": ["React.js", "Angular", "Vue.js", "Laravel", "Django"]
},
{
"id": "b5l75aseexqv17quvqgh73fe",
"visible": true,
"name": "Tools",
"description": "Intermediate",
"level": 0,
"keywords": ["Webpack", "Git", "Jenkins", "Docker", "JIRA"]
}
]
},
"custom": {}
},
"metadata": {
"template": "bronzor",
"layout": [
[
["profiles", "summary", "experience", "education", "projects"],
[
"skills",
"certifications",
"languages",
"references",
"interests",
"awards",
"volunteer",
"publications"
]
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {
"margin": 14,
"format": "a4",
"options": {
"breakLine": true,
"pageNumbers": true
}
},
"theme": {
"background": "#ffffff",
"text": "#000000",
"primary": "#059669"
},
"typography": {
"font": {
"family": "IBM Plex Sans",
"subset": "latin",
"variants": ["regular", "500"],
"size": 13
},
"lineHeight": 1.5,
"hideIcons": false,
"underlineLinks": true
},
"notes": ""
}
}

View File

@ -0,0 +1,346 @@
{
"basics": {
"name": "John Doe",
"headline": "Creative and Innovative Web Developer",
"email": "john.doe@gmail.com",
"phone": "(555) 123-4567",
"location": "Pleasantville, CA 94588",
"url": {
"label": "",
"href": "https://johndoe.me/"
},
"customFields": [],
"picture": {
"url": "https://i.imgur.com/HgwyOuJ.jpg",
"size": 140,
"aspectRatio": 1,
"borderRadius": 4,
"effects": {
"hidden": false,
"border": false,
"grayscale": false
}
}
},
"sections": {
"summary": {
"name": "Summary",
"columns": 1,
"visible": true,
"id": "summary",
"content": "<p>Innovative Web Developer with 5 years of experience in building impactful and user-friendly websites and applications. Specializes in <strong>front-end technologies</strong> and passionate about modern web standards and cutting-edge development techniques. Proven track record of leading successful projects from concept to deployment.</p>"
},
"awards": {
"name": "Awards",
"columns": 1,
"visible": true,
"id": "awards",
"items": []
},
"certifications": {
"name": "Certifications",
"columns": 1,
"visible": true,
"id": "certifications",
"items": [
{
"id": "spdhh9rrqi1gvj0yqnbqunlo",
"visible": true,
"name": "Full-Stack Web Development",
"issuer": "CodeAcademy",
"date": "2020",
"summary": "",
"url": {
"label": "",
"href": ""
}
},
{
"id": "n838rddyqv47zexn6cxauwqp",
"visible": true,
"name": "AWS Certified Developer",
"issuer": "Amazon Web Services",
"date": "2019",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"education": {
"name": "Education",
"columns": 1,
"visible": true,
"id": "education",
"items": [
{
"id": "yo3p200zo45c6cdqc6a2vtt3",
"visible": true,
"institution": "University of California",
"studyType": "Bachelor's in Computer Science",
"area": "Berkeley, CA",
"score": "",
"date": "August 2012 to May 2016",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"experience": {
"name": "Experience",
"columns": 1,
"visible": true,
"id": "experience",
"items": [
{
"id": "lhw25d7gf32wgdfpsktf6e0x",
"visible": true,
"company": "Creative Solutions Inc.",
"position": "Senior Web Developer",
"location": "San Francisco, CA",
"date": "January 2019 to Present",
"summary": "<ul><li><p>Spearheaded the redesign of the main product website, resulting in a 40% increase in user engagement.</p></li><li><p>Developed and implemented a new responsive framework, improving cross-device compatibility.</p></li><li><p>Mentored a team of four junior developers, fostering a culture of technical excellence.</p></li></ul>",
"url": {
"label": "",
"href": "https://creativesolutions.inc/"
}
},
{
"id": "r6543lil53ntrxmvel53gbtm",
"visible": true,
"company": "TechAdvancers",
"position": "Web Developer",
"location": "San Jose, CA",
"date": "June 2016 to December 2018",
"summary": "<ul><li><p>Collaborated in a team of 10 to develop high-quality web applications using React.js and Node.js.</p></li><li><p>Managed the integration of third-party services such as Stripe for payments and Twilio for SMS services.</p></li><li><p>Optimized application performance, achieving a 30% reduction in load times.</p></li></ul>",
"url": {
"label": "",
"href": "https://techadvancers.com/"
}
}
]
},
"volunteer": {
"name": "Volunteering",
"columns": 1,
"visible": true,
"id": "volunteer",
"items": []
},
"interests": {
"name": "Interests",
"columns": 1,
"visible": true,
"id": "interests",
"items": []
},
"languages": {
"name": "Languages",
"columns": 1,
"visible": true,
"id": "languages",
"items": [
{
"id": "ey0c353j3apepzqmyl8zd37c",
"visible": true,
"name": "English",
"description": "Native Speaker",
"level": 0
},
{
"id": "a4dnf32wyj5phgln2rx97qk2",
"visible": true,
"name": "Spanish",
"description": "Intermediate",
"level": 0
}
]
},
"profiles": {
"name": "Profiles",
"columns": 3,
"visible": true,
"id": "profiles",
"items": [
{
"id": "cnbk5f0aeqvhx69ebk7hktwd",
"visible": true,
"network": "LinkedIn",
"username": "johndoe",
"icon": "linkedin",
"url": {
"label": "",
"href": "https://linkedin.com/in/johndoe"
}
},
{
"id": "ukl0uecvzkgm27mlye0wazlb",
"visible": true,
"network": "GitHub",
"username": "johndoe",
"icon": "github",
"url": {
"label": "",
"href": "https://github.com/johndoe"
}
},
{
"id": "k97vdxzmssmgel79km48kxgg",
"visible": true,
"network": "StackOverflow",
"username": "johndoe",
"icon": "stackoverflow",
"url": {
"label": "",
"href": "https://stackoverflow.com/u/johndoe"
}
}
]
},
"projects": {
"name": "Projects",
"columns": 2,
"visible": true,
"id": "projects",
"items": [
{
"id": "yw843emozcth8s1ubi1ubvlf",
"visible": true,
"name": "E-Commerce Platform",
"description": "Project Lead",
"date": "",
"summary": "<p>Led the development of a full-stack e-commerce platform, improving sales conversion by 25%.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
},
{
"id": "ncxgdjjky54gh59iz2t1xi1v",
"visible": true,
"name": "Interactive Dashboard",
"description": "Frontend Developer",
"date": "",
"summary": "<p>Created an interactive analytics dashboard for a SaaS application, enhancing data visualization for clients.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
}
]
},
"publications": {
"name": "Publications",
"columns": 1,
"visible": true,
"id": "publications",
"items": []
},
"references": {
"name": "References",
"columns": 1,
"visible": true,
"id": "references",
"items": [
{
"id": "f2sv5z0cce6ztjl87yuk8fak",
"visible": true,
"name": "Available upon request",
"description": "",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"skills": {
"name": "Skills",
"columns": 1,
"visible": true,
"id": "skills",
"items": [
{
"id": "hn0keriukh6c0ojktl9gsgjm",
"visible": true,
"name": "Web Technologies",
"description": "Advanced",
"level": 0,
"keywords": ["HTML5", "JavaScript", "PHP", "Python"]
},
{
"id": "r8c3y47vykausqrgmzwg5pur",
"visible": true,
"name": "Web Frameworks",
"description": "Intermediate",
"level": 0,
"keywords": ["React.js", "Angular", "Vue.js", "Laravel", "Django"]
},
{
"id": "b5l75aseexqv17quvqgh73fe",
"visible": true,
"name": "Tools",
"description": "Intermediate",
"level": 0,
"keywords": ["Webpack", "Git", "Jenkins", "Docker", "JIRA"]
}
]
},
"custom": {}
},
"metadata": {
"template": "chikorita",
"layout": [
[
["profiles", "summary", "experience", "education", "projects"],
[
"skills",
"certifications",
"languages",
"references",
"interests",
"awards",
"volunteer",
"publications"
]
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {
"margin": 14,
"format": "a4",
"options": {
"breakLine": true,
"pageNumbers": true
}
},
"theme": {
"background": "#ffffff",
"text": "#000000",
"primary": "#059669"
},
"typography": {
"font": {
"family": "Merriweather",
"subset": "latin",
"variants": ["regular"],
"size": 13
},
"lineHeight": 1.5,
"hideIcons": false,
"underlineLinks": true
},
"notes": ""
}
}

View File

@ -0,0 +1,347 @@
{
"basics": {
"name": "John Doe",
"headline": "Creative and Innovative Web Developer",
"email": "john.doe@gmail.com",
"phone": "(555) 123-4567",
"location": "Pleasantville, CA 94588",
"url": {
"label": "",
"href": "https://johndoe.me/"
},
"customFields": [],
"picture": {
"url": "https://i.imgur.com/HgwyOuJ.jpg",
"size": 140,
"aspectRatio": 1,
"borderRadius": 4,
"effects": {
"hidden": false,
"border": false,
"grayscale": false
}
}
},
"sections": {
"summary": {
"name": "Summary",
"columns": 1,
"visible": true,
"id": "summary",
"content": "<p>Innovative Web Developer with 5 years of experience in building impactful and user-friendly websites and applications. Specializes in <strong>front-end technologies</strong> and passionate about modern web standards and cutting-edge development techniques. Proven track record of leading successful projects from concept to deployment.</p>"
},
"awards": {
"name": "Awards",
"columns": 1,
"visible": true,
"id": "awards",
"items": []
},
"certifications": {
"name": "Certifications",
"columns": 1,
"visible": true,
"id": "certifications",
"items": [
{
"id": "spdhh9rrqi1gvj0yqnbqunlo",
"visible": true,
"name": "Full-Stack Web Development",
"issuer": "CodeAcademy",
"date": "2020",
"summary": "",
"url": {
"label": "",
"href": ""
}
},
{
"id": "n838rddyqv47zexn6cxauwqp",
"visible": true,
"name": "AWS Certified Developer",
"issuer": "Amazon Web Services",
"date": "2019",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"education": {
"name": "Education",
"columns": 1,
"visible": true,
"id": "education",
"items": [
{
"id": "yo3p200zo45c6cdqc6a2vtt3",
"visible": true,
"institution": "University of California",
"studyType": "Bachelor's in Computer Science",
"area": "Berkeley, CA",
"score": "",
"date": "August 2012 to May 2016",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"experience": {
"name": "Experience",
"columns": 1,
"visible": true,
"id": "experience",
"items": [
{
"id": "lhw25d7gf32wgdfpsktf6e0x",
"visible": true,
"company": "Creative Solutions Inc.",
"position": "Senior Web Developer",
"location": "San Francisco, CA",
"date": "January 2019 to Present",
"summary": "<ul><li><p>Spearheaded the redesign of the main product website, resulting in a 40% increase in user engagement.</p></li><li><p>Developed and implemented a new responsive framework, improving cross-device compatibility.</p></li><li><p>Mentored a team of four junior developers, fostering a culture of technical excellence.</p></li></ul>",
"url": {
"label": "",
"href": "https://creativesolutions.inc/"
}
},
{
"id": "r6543lil53ntrxmvel53gbtm",
"visible": true,
"company": "TechAdvancers",
"position": "Web Developer",
"location": "San Jose, CA",
"date": "June 2016 to December 2018",
"summary": "<ul><li><p>Collaborated in a team of 10 to develop high-quality web applications using React.js and Node.js.</p></li><li><p>Managed the integration of third-party services such as Stripe for payments and Twilio for SMS services.</p></li><li><p>Optimized application performance, achieving a 30% reduction in load times.</p></li></ul>",
"url": {
"label": "",
"href": "https://techadvancers.com/"
}
}
]
},
"volunteer": {
"name": "Volunteering",
"columns": 1,
"visible": true,
"id": "volunteer",
"items": []
},
"interests": {
"name": "Interests",
"columns": 1,
"visible": true,
"id": "interests",
"items": []
},
"languages": {
"name": "Languages",
"columns": 1,
"visible": true,
"id": "languages",
"items": [
{
"id": "ey0c353j3apepzqmyl8zd37c",
"visible": true,
"name": "English",
"description": "Native Speaker",
"level": 0
},
{
"id": "a4dnf32wyj5phgln2rx97qk2",
"visible": true,
"name": "Spanish",
"description": "Intermediate",
"level": 0
}
]
},
"profiles": {
"name": "Profiles",
"columns": 1,
"visible": true,
"id": "profiles",
"items": [
{
"id": "cnbk5f0aeqvhx69ebk7hktwd",
"visible": true,
"network": "LinkedIn",
"username": "johndoe",
"icon": "linkedin",
"url": {
"label": "",
"href": "https://linkedin.com/in/johndoe"
}
},
{
"id": "ukl0uecvzkgm27mlye0wazlb",
"visible": true,
"network": "GitHub",
"username": "johndoe",
"icon": "github",
"url": {
"label": "",
"href": "https://github.com/johndoe"
}
},
{
"id": "k97vdxzmssmgel79km48kxgg",
"visible": true,
"network": "StackOverflow",
"username": "johndoe",
"icon": "stackoverflow",
"url": {
"label": "",
"href": "https://stackoverflow.com/u/johndoe"
}
}
]
},
"projects": {
"name": "Projects",
"columns": 2,
"visible": true,
"id": "projects",
"items": [
{
"id": "yw843emozcth8s1ubi1ubvlf",
"visible": true,
"name": "E-Commerce Platform",
"description": "Project Lead",
"date": "",
"summary": "<p>Led the development of a full-stack e-commerce platform, improving sales conversion by 25%.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
},
{
"id": "ncxgdjjky54gh59iz2t1xi1v",
"visible": true,
"name": "Interactive Dashboard",
"description": "Frontend Developer",
"date": "",
"summary": "<p>Created an interactive analytics dashboard for a SaaS application, enhancing data visualization for clients.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
}
]
},
"publications": {
"name": "Publications",
"columns": 1,
"visible": true,
"id": "publications",
"items": []
},
"references": {
"name": "References",
"columns": 1,
"visible": true,
"id": "references",
"items": [
{
"id": "f2sv5z0cce6ztjl87yuk8fak",
"visible": true,
"name": "Available upon request",
"description": "",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"skills": {
"name": "Skills",
"columns": 1,
"visible": true,
"id": "skills",
"items": [
{
"id": "hn0keriukh6c0ojktl9gsgjm",
"visible": true,
"name": "Web Technologies",
"description": "Advanced",
"level": 0,
"keywords": ["HTML5", "JavaScript", "PHP", "Python"]
},
{
"id": "r8c3y47vykausqrgmzwg5pur",
"visible": true,
"name": "Web Frameworks",
"description": "Intermediate",
"level": 0,
"keywords": ["React.js", "Angular", "Vue.js", "Laravel", "Django"]
},
{
"id": "b5l75aseexqv17quvqgh73fe",
"visible": true,
"name": "Tools",
"description": "Intermediate",
"level": 0,
"keywords": ["Webpack", "Git", "Jenkins", "Docker", "JIRA"]
}
]
},
"custom": {}
},
"metadata": {
"template": "ditto",
"layout": [
[
["summary", "experience", "education", "projects"],
[
"profiles",
"skills",
"certifications",
"languages",
"references",
"interests",
"awards",
"volunteer",
"publications"
]
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {
"margin": 14,
"format": "a4",
"options": {
"breakLine": true,
"pageNumbers": true
}
},
"theme": {
"background": "#ffffff",
"text": "#000000",
"primary": "#0891b2"
},
"typography": {
"font": {
"family": "Merriweather",
"subset": "latin",
"variants": ["regular"],
"size": 13
},
"lineHeight": 1.75,
"hideIcons": false,
"underlineLinks": true
},
"notes": ""
}
}

View File

@ -0,0 +1,319 @@
{
"basics": {
"name": "John Doe",
"headline": "Creative and Innovative Web Developer",
"email": "john.doe@gmail.com",
"phone": "(555) 123-4567",
"location": "Pleasantville, CA 94588",
"url": {
"label": "",
"href": "https://johndoe.me/"
},
"customFields": [],
"picture": {
"url": "https://i.imgur.com/HgwyOuJ.jpg",
"size": 64,
"aspectRatio": 1,
"borderRadius": 0,
"effects": {
"hidden": false,
"border": false,
"grayscale": false
}
}
},
"sections": {
"summary": {
"name": "Summary",
"columns": 1,
"visible": true,
"id": "summary",
"content": "<p>Innovative Web Developer with 5 years of experience in building impactful and user-friendly websites and applications. Specializes in <strong>front-end technologies</strong> and passionate about modern web standards and cutting-edge development techniques. Proven track record of leading successful projects from concept to deployment.</p>"
},
"awards": {
"name": "Awards",
"columns": 1,
"visible": true,
"id": "awards",
"items": []
},
"certifications": {
"name": "Certifications",
"columns": 2,
"visible": true,
"id": "certifications",
"items": [
{
"id": "spdhh9rrqi1gvj0yqnbqunlo",
"visible": true,
"name": "Full-Stack Web Development",
"issuer": "CodeAcademy",
"date": "2020",
"summary": "",
"url": {
"label": "",
"href": ""
}
},
{
"id": "n838rddyqv47zexn6cxauwqp",
"visible": true,
"name": "AWS Certified Developer",
"issuer": "Amazon Web Services",
"date": "2019",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"education": {
"name": "Education",
"columns": 1,
"visible": true,
"id": "education",
"items": [
{
"id": "yo3p200zo45c6cdqc6a2vtt3",
"visible": true,
"institution": "University of California",
"studyType": "Bachelor's in Computer Science",
"area": "",
"score": "",
"date": "August 2012 to May 2016",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"experience": {
"name": "Experience",
"columns": 2,
"visible": true,
"id": "experience",
"items": [
{
"id": "lhw25d7gf32wgdfpsktf6e0x",
"visible": true,
"company": "Creative Solutions Inc.",
"position": "Senior Web Developer",
"location": "San Francisco, CA",
"date": "January 2019 to Present",
"summary": "<ul><li><p>Spearheaded the redesign of the main product website, resulting in a 40% increase in user engagement.</p></li><li><p>Developed and implemented a new responsive framework, improving cross-device compatibility.</p></li><li><p>Mentored a team of four junior developers, fostering a culture of technical excellence.</p></li></ul>",
"url": {
"label": "",
"href": "https://creativesolutions.inc/"
}
},
{
"id": "r6543lil53ntrxmvel53gbtm",
"visible": true,
"company": "TechAdvancers",
"position": "Web Developer",
"location": "San Jose, CA",
"date": "June 2016 to December 2018",
"summary": "<ul><li><p>Collaborated in a team of 10 to develop high-quality web applications using React.js and Node.js.</p></li><li><p>Managed the integration of third-party services such as Stripe for payments and Twilio for SMS services.</p></li><li><p>Optimized application performance, achieving a 30% reduction in load times.</p></li></ul>",
"url": {
"label": "",
"href": "https://techadvancers.com/"
}
}
]
},
"volunteer": {
"name": "Volunteering",
"columns": 1,
"visible": true,
"id": "volunteer",
"items": []
},
"interests": {
"name": "Interests",
"columns": 1,
"visible": true,
"id": "interests",
"items": []
},
"languages": {
"name": "Languages",
"columns": 1,
"visible": true,
"id": "languages",
"items": []
},
"profiles": {
"name": "Profiles",
"columns": 1,
"visible": true,
"id": "profiles",
"items": [
{
"id": "cnbk5f0aeqvhx69ebk7hktwd",
"visible": true,
"network": "LinkedIn",
"username": "johndoe",
"icon": "linkedin",
"url": {
"label": "",
"href": "https://linkedin.com/in/johndoe"
}
},
{
"id": "ukl0uecvzkgm27mlye0wazlb",
"visible": true,
"network": "GitHub",
"username": "johndoe",
"icon": "github",
"url": {
"label": "",
"href": "https://github.com/johndoe"
}
}
]
},
"projects": {
"name": "Projects",
"columns": 2,
"visible": true,
"id": "projects",
"items": [
{
"id": "yw843emozcth8s1ubi1ubvlf",
"visible": true,
"name": "E-Commerce Platform",
"description": "Project Lead",
"date": "",
"summary": "<p>Led the development of a full-stack e-commerce platform, improving sales conversion by 25%.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
},
{
"id": "ncxgdjjky54gh59iz2t1xi1v",
"visible": true,
"name": "Interactive Dashboard",
"description": "Frontend Developer",
"date": "",
"summary": "<p>Created an interactive analytics dashboard for a SaaS application, enhancing data visualization for clients.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
}
]
},
"publications": {
"name": "Publications",
"columns": 1,
"visible": true,
"id": "publications",
"items": []
},
"references": {
"name": "References",
"columns": 1,
"visible": true,
"id": "references",
"items": [
{
"id": "f2sv5z0cce6ztjl87yuk8fak",
"visible": true,
"name": "Available upon request",
"description": "",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"skills": {
"name": "Skills",
"columns": 3,
"visible": true,
"id": "skills",
"items": [
{
"id": "hn0keriukh6c0ojktl9gsgjm",
"visible": true,
"name": "Web Technologies",
"description": "Advanced",
"level": 0,
"keywords": ["HTML5", "JavaScript", "PHP", "Python"]
},
{
"id": "r8c3y47vykausqrgmzwg5pur",
"visible": true,
"name": "Web Frameworks",
"description": "Intermediate",
"level": 0,
"keywords": ["React.js", "Angular", "Vue.js", "Laravel", "Django"]
},
{
"id": "b5l75aseexqv17quvqgh73fe",
"visible": true,
"name": "Tools",
"description": "Intermediate",
"level": 0,
"keywords": ["Webpack", "Git", "Jenkins", "Docker", "JIRA"]
}
]
},
"custom": {}
},
"metadata": {
"template": "kakuna",
"layout": [
[
[
"summary",
"experience",
"education",
"projects",
"certifications",
"skills",
"references"
],
["profiles", "interests", "languages", "awards", "volunteer", "publications"]
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {
"margin": 14,
"format": "a4",
"options": {
"breakLine": true,
"pageNumbers": true
}
},
"theme": {
"background": "#ffffff",
"text": "#000000",
"primary": "#57534e"
},
"typography": {
"font": {
"family": "Lora",
"subset": "latin",
"variants": ["regular"],
"size": 13
},
"lineHeight": 1.5,
"hideIcons": false,
"underlineLinks": true
},
"notes": ""
}
}

View File

@ -0,0 +1,338 @@
{
"basics": {
"name": "John Doe",
"headline": "Creative and Innovative Web Developer",
"email": "john.doe@gmail.com",
"phone": "(555) 123-4567",
"location": "Pleasantville, CA 94588",
"url": {
"label": "",
"href": "https://johndoe.me/"
},
"customFields": [],
"picture": {
"url": "https://i.imgur.com/HgwyOuJ.jpg",
"size": 90,
"aspectRatio": 1,
"borderRadius": 4,
"effects": {
"hidden": false,
"border": false,
"grayscale": false
}
}
},
"sections": {
"summary": {
"name": "Summary",
"columns": 1,
"visible": true,
"id": "summary",
"content": "<p>Innovative Web Developer with 5 years of experience in building impactful and user-friendly websites and applications. Specializes in <strong>front-end technologies</strong> and passionate about modern web standards and cutting-edge development techniques. Proven track record of leading successful projects from concept to deployment.</p>"
},
"awards": {
"name": "Awards",
"columns": 1,
"visible": true,
"id": "awards",
"items": []
},
"certifications": {
"name": "Certifications",
"columns": 1,
"visible": true,
"id": "certifications",
"items": [
{
"id": "spdhh9rrqi1gvj0yqnbqunlo",
"visible": true,
"name": "Full-Stack Web Development",
"issuer": "CodeAcademy",
"date": "2020",
"summary": "",
"url": {
"label": "",
"href": ""
}
},
{
"id": "n838rddyqv47zexn6cxauwqp",
"visible": true,
"name": "AWS Certified Developer",
"issuer": "Amazon Web Services",
"date": "2019",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"education": {
"name": "Education",
"columns": 1,
"visible": true,
"id": "education",
"items": [
{
"id": "yo3p200zo45c6cdqc6a2vtt3",
"visible": true,
"institution": "University of California",
"studyType": "Bachelor's in Computer Science",
"area": "Berkeley, CA",
"score": "",
"date": "Aug 2012 - May 2016",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"experience": {
"name": "Experience",
"columns": 1,
"visible": true,
"id": "experience",
"items": [
{
"id": "lhw25d7gf32wgdfpsktf6e0x",
"visible": true,
"company": "Creative Solutions Inc.",
"position": "Senior Web Developer",
"location": "San Francisco, CA",
"date": "Jan 2019 - Present",
"summary": "<ul><li><p>Spearheaded the redesign of the main product website, resulting in a 40% increase in user engagement.</p></li><li><p>Developed and implemented a new responsive framework, improving cross-device compatibility.</p></li><li><p>Mentored a team of four junior developers, fostering a culture of technical excellence.</p></li></ul>",
"url": {
"label": "",
"href": "https://creativesolutions.inc/"
}
},
{
"id": "r6543lil53ntrxmvel53gbtm",
"visible": true,
"company": "TechAdvancers",
"position": "Web Developer",
"location": "San Jose, CA",
"date": "Jun 2016 - Dec 2018",
"summary": "<ul><li><p>Collaborated in a team of 10 to develop high-quality web applications using React.js and Node.js.</p></li><li><p>Managed the integration of third-party services such as Stripe for payments and Twilio for SMS services.</p></li><li><p>Optimized application performance, achieving a 30% reduction in load times.</p></li></ul>",
"url": {
"label": "",
"href": "https://techadvancers.com/"
}
}
]
},
"volunteer": {
"name": "Volunteering",
"columns": 1,
"visible": true,
"id": "volunteer",
"items": []
},
"interests": {
"name": "Interests",
"columns": 1,
"visible": true,
"id": "interests",
"items": []
},
"languages": {
"name": "Languages",
"columns": 1,
"visible": true,
"id": "languages",
"items": [
{
"id": "ey0c353j3apepzqmyl8zd37c",
"visible": true,
"name": "English",
"description": "Native Speaker",
"level": 0
},
{
"id": "a4dnf32wyj5phgln2rx97qk2",
"visible": true,
"name": "Spanish",
"description": "Intermediate",
"level": 0
}
]
},
"profiles": {
"name": "Profiles",
"columns": 3,
"visible": true,
"id": "profiles",
"items": [
{
"id": "cnbk5f0aeqvhx69ebk7hktwd",
"visible": true,
"network": "LinkedIn",
"username": "johndoe",
"icon": "linkedin",
"url": {
"label": "",
"href": "https://linkedin.com/in/johndoe"
}
},
{
"id": "ukl0uecvzkgm27mlye0wazlb",
"visible": true,
"network": "GitHub",
"username": "johndoe",
"icon": "github",
"url": {
"label": "",
"href": "https://github.com/johndoe"
}
},
{
"id": "k97vdxzmssmgel79km48kxgg",
"visible": true,
"network": "StackOverflow",
"username": "johndoe",
"icon": "stackoverflow",
"url": {
"label": "",
"href": "https://stackoverflow.com/u/johndoe"
}
}
]
},
"projects": {
"name": "Projects",
"columns": 1,
"visible": true,
"id": "projects",
"items": [
{
"id": "yw843emozcth8s1ubi1ubvlf",
"visible": true,
"name": "E-Commerce Platform",
"description": "Project Lead",
"date": "",
"summary": "<p>Led the development of a full-stack e-commerce platform, improving sales conversion by 25%.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
},
{
"id": "ncxgdjjky54gh59iz2t1xi1v",
"visible": true,
"name": "Interactive Dashboard",
"description": "Frontend Developer",
"date": "",
"summary": "<p>Created an interactive analytics dashboard for a SaaS application, enhancing data visualization for clients.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
}
]
},
"publications": {
"name": "Publications",
"columns": 1,
"visible": true,
"id": "publications",
"items": []
},
"references": {
"name": "References",
"columns": 1,
"visible": true,
"id": "references",
"items": [
{
"id": "f2sv5z0cce6ztjl87yuk8fak",
"visible": true,
"name": "Available upon request",
"description": "",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"skills": {
"name": "Skills",
"columns": 1,
"visible": true,
"id": "skills",
"items": [
{
"id": "hn0keriukh6c0ojktl9gsgjm",
"visible": true,
"name": "Web Technologies",
"description": "Advanced",
"level": 0,
"keywords": ["HTML5", "JavaScript", "PHP", "Python"]
},
{
"id": "r8c3y47vykausqrgmzwg5pur",
"visible": true,
"name": "Web Frameworks",
"description": "Intermediate",
"level": 0,
"keywords": ["React.js", "Angular", "Vue.js", "Laravel", "Django"]
},
{
"id": "b5l75aseexqv17quvqgh73fe",
"visible": true,
"name": "Tools",
"description": "Intermediate",
"level": 0,
"keywords": ["Webpack", "Git", "Jenkins", "Docker", "JIRA"]
}
]
},
"custom": {}
},
"metadata": {
"template": "nosepass",
"layout": [
[
["profiles", "summary", "experience", "education"],
["interests", "awards", "volunteer", "publications"]
],
[["projects", "certifications", "skills", "languages", "references"], []]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {
"margin": 24,
"format": "a4",
"options": {
"breakLine": true,
"pageNumbers": true
}
},
"theme": {
"background": "#ffffff",
"text": "#000000",
"primary": "#5d93bc"
},
"typography": {
"font": {
"family": "Work Sans",
"subset": "latin",
"variants": ["regular"],
"size": 13.8
},
"lineHeight": 1.5,
"hideIcons": true,
"underlineLinks": true
},
"notes": ""
}
}

View File

@ -0,0 +1,319 @@
{
"basics": {
"name": "John Doe",
"headline": "Creative and Innovative Web Developer",
"email": "john.doe@gmail.com",
"phone": "(555) 123-4567",
"location": "Pleasantville, CA 94588",
"url": {
"label": "",
"href": "https://johndoe.me/"
},
"customFields": [],
"picture": {
"url": "https://i.imgur.com/HgwyOuJ.jpg",
"size": 100,
"aspectRatio": 1,
"borderRadius": 0,
"effects": {
"hidden": false,
"border": false,
"grayscale": false
}
}
},
"sections": {
"summary": {
"name": "Summary",
"columns": 1,
"visible": true,
"id": "summary",
"content": "<p>Innovative Web Developer with 5 years of experience in building impactful and user-friendly websites and applications. Specializes in <strong>front-end technologies</strong> and passionate about modern web standards and cutting-edge development techniques. Proven track record of leading successful projects from concept to deployment.</p>"
},
"awards": {
"name": "Awards",
"columns": 1,
"visible": true,
"id": "awards",
"items": []
},
"certifications": {
"name": "Certifications",
"columns": 2,
"visible": true,
"id": "certifications",
"items": [
{
"id": "spdhh9rrqi1gvj0yqnbqunlo",
"visible": true,
"name": "Full-Stack Web Development",
"issuer": "CodeAcademy",
"date": "2020",
"summary": "",
"url": {
"label": "",
"href": ""
}
},
{
"id": "n838rddyqv47zexn6cxauwqp",
"visible": true,
"name": "AWS Certified Developer",
"issuer": "Amazon Web Services",
"date": "2019",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"education": {
"name": "Education",
"columns": 1,
"visible": true,
"id": "education",
"items": [
{
"id": "yo3p200zo45c6cdqc6a2vtt3",
"visible": true,
"institution": "University of California",
"studyType": "Bachelor of Science",
"area": "Computer Science",
"score": "Berkeley, CA",
"date": "August 2012 to May 2016",
"summary": "",
"url": {
"label": "",
"href": "https://www.universityofcalifornia.edu/"
}
}
]
},
"experience": {
"name": "Experience",
"columns": 1,
"visible": true,
"id": "experience",
"items": [
{
"id": "lhw25d7gf32wgdfpsktf6e0x",
"visible": true,
"company": "Creative Solutions Inc.",
"position": "Senior Web Developer",
"location": "San Francisco, CA",
"date": "January 2019 to Present",
"summary": "<ul><li><p>Spearheaded the redesign of the main product website, resulting in a 40% increase in user engagement.</p></li><li><p>Developed and implemented a new responsive framework, improving cross-device compatibility.</p></li><li><p>Mentored a team of four junior developers, fostering a culture of technical excellence.</p></li></ul>",
"url": {
"label": "",
"href": "https://creativesolutions.inc/"
}
},
{
"id": "r6543lil53ntrxmvel53gbtm",
"visible": true,
"company": "TechAdvancers",
"position": "Web Developer",
"location": "San Jose, CA",
"date": "June 2016 to December 2018",
"summary": "<ul><li><p>Collaborated in a team of 10 to develop high-quality web applications using React.js and Node.js.</p></li><li><p>Managed the integration of third-party services such as Stripe for payments and Twilio for SMS services.</p></li><li><p>Optimized application performance, achieving a 30% reduction in load times.</p></li></ul>",
"url": {
"label": "",
"href": "https://techadvancers.com/"
}
}
]
},
"volunteer": {
"name": "Volunteering",
"columns": 1,
"visible": true,
"id": "volunteer",
"items": []
},
"interests": {
"name": "Interests",
"columns": 1,
"visible": true,
"id": "interests",
"items": []
},
"languages": {
"name": "Languages",
"columns": 1,
"visible": true,
"id": "languages",
"items": []
},
"profiles": {
"name": "Profiles",
"columns": 1,
"visible": true,
"id": "profiles",
"items": [
{
"id": "cnbk5f0aeqvhx69ebk7hktwd",
"visible": true,
"network": "LinkedIn",
"username": "johndoe",
"icon": "linkedin",
"url": {
"label": "",
"href": "https://linkedin.com/in/johndoe"
}
},
{
"id": "ukl0uecvzkgm27mlye0wazlb",
"visible": true,
"network": "GitHub",
"username": "johndoe",
"icon": "github",
"url": {
"label": "",
"href": "https://github.com/johndoe"
}
}
]
},
"projects": {
"name": "Projects",
"columns": 1,
"visible": true,
"id": "projects",
"items": [
{
"id": "yw843emozcth8s1ubi1ubvlf",
"visible": true,
"name": "E-Commerce Platform",
"description": "Project Lead",
"date": "",
"summary": "<p>Led the development of a full-stack e-commerce platform, improving sales conversion by 25%.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
},
{
"id": "ncxgdjjky54gh59iz2t1xi1v",
"visible": true,
"name": "Interactive Dashboard",
"description": "Frontend Developer",
"date": "",
"summary": "<p>Created an interactive analytics dashboard for a SaaS application, enhancing data visualization for clients.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
}
]
},
"publications": {
"name": "Publications",
"columns": 1,
"visible": true,
"id": "publications",
"items": []
},
"references": {
"name": "References",
"columns": 1,
"visible": true,
"id": "references",
"items": [
{
"id": "f2sv5z0cce6ztjl87yuk8fak",
"visible": true,
"name": "Available upon request",
"description": "",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"skills": {
"name": "Skills",
"columns": 3,
"visible": true,
"id": "skills",
"items": [
{
"id": "hn0keriukh6c0ojktl9gsgjm",
"visible": true,
"name": "Web Technologies",
"description": "Advanced",
"level": 0,
"keywords": ["HTML5", "JavaScript", "PHP", "Python"]
},
{
"id": "r8c3y47vykausqrgmzwg5pur",
"visible": true,
"name": "Web Frameworks",
"description": "Intermediate",
"level": 0,
"keywords": ["React.js", "Angular", "Vue.js", "Laravel", "Django"]
},
{
"id": "b5l75aseexqv17quvqgh73fe",
"visible": true,
"name": "Tools",
"description": "Intermediate",
"level": 0,
"keywords": ["Webpack", "Git", "Jenkins", "Docker", "JIRA"]
}
]
},
"custom": {}
},
"metadata": {
"template": "onyx",
"layout": [
[
[
"summary",
"experience",
"education",
"projects",
"certifications",
"skills",
"references"
],
["profiles", "interests", "languages", "awards", "volunteer", "publications"]
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {
"margin": 18,
"format": "a4",
"options": {
"breakLine": true,
"pageNumbers": true
}
},
"theme": {
"background": "#ffffff",
"text": "#000000",
"primary": "#dc2626"
},
"typography": {
"font": {
"family": "PT Serif",
"subset": "latin",
"variants": ["regular"],
"size": 13
},
"lineHeight": 1.5,
"hideIcons": false,
"underlineLinks": false
},
"notes": ""
}
}

View File

@ -0,0 +1,347 @@
{
"basics": {
"name": "John Doe",
"headline": "Creative and Innovative Web Developer",
"email": "john.doe@gmail.com",
"phone": "(555) 123-4567",
"location": "Pleasantville, CA 94588",
"url": {
"label": "",
"href": "https://johndoe.me/"
},
"customFields": [],
"picture": {
"url": "https://i.imgur.com/HgwyOuJ.jpg",
"size": 140,
"aspectRatio": 1,
"borderRadius": 4,
"effects": {
"hidden": false,
"border": false,
"grayscale": false
}
}
},
"sections": {
"summary": {
"name": "Summary",
"columns": 1,
"visible": true,
"id": "summary",
"content": "<p>Innovative Web Developer with 5 years of experience in building impactful and user-friendly websites and applications. Specializes in <strong>front-end technologies</strong> and passionate about modern web standards and cutting-edge development techniques. Proven track record of leading successful projects from concept to deployment.</p>"
},
"awards": {
"name": "Awards",
"columns": 1,
"visible": true,
"id": "awards",
"items": []
},
"certifications": {
"name": "Certifications",
"columns": 1,
"visible": true,
"id": "certifications",
"items": [
{
"id": "spdhh9rrqi1gvj0yqnbqunlo",
"visible": true,
"name": "Full-Stack Web Development",
"issuer": "CodeAcademy",
"date": "2020",
"summary": "",
"url": {
"label": "",
"href": ""
}
},
{
"id": "n838rddyqv47zexn6cxauwqp",
"visible": true,
"name": "AWS Certified Developer",
"issuer": "Amazon Web Services",
"date": "2019",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"education": {
"name": "Education",
"columns": 1,
"visible": true,
"id": "education",
"items": [
{
"id": "yo3p200zo45c6cdqc6a2vtt3",
"visible": true,
"institution": "University of California",
"studyType": "Bachelor's in Computer Science",
"area": "Berkeley, CA",
"score": "",
"date": "August 2012 to May 2016",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"experience": {
"name": "Experience",
"columns": 1,
"visible": true,
"id": "experience",
"items": [
{
"id": "lhw25d7gf32wgdfpsktf6e0x",
"visible": true,
"company": "Creative Solutions Inc.",
"position": "Senior Web Developer",
"location": "San Francisco, CA",
"date": "January 2019 to Present",
"summary": "<ul><li><p>Spearheaded the redesign of the main product website, resulting in a 40% increase in user engagement.</p></li><li><p>Developed and implemented a new responsive framework, improving cross-device compatibility.</p></li><li><p>Mentored a team of four junior developers, fostering a culture of technical excellence.</p></li></ul>",
"url": {
"label": "",
"href": "https://creativesolutions.inc/"
}
},
{
"id": "r6543lil53ntrxmvel53gbtm",
"visible": true,
"company": "TechAdvancers",
"position": "Web Developer",
"location": "San Jose, CA",
"date": "June 2016 to December 2018",
"summary": "<ul><li><p>Collaborated in a team of 10 to develop high-quality web applications using React.js and Node.js.</p></li><li><p>Managed the integration of third-party services such as Stripe for payments and Twilio for SMS services.</p></li><li><p>Optimized application performance, achieving a 30% reduction in load times.</p></li></ul>",
"url": {
"label": "",
"href": "https://techadvancers.com/"
}
}
]
},
"volunteer": {
"name": "Volunteering",
"columns": 1,
"visible": true,
"id": "volunteer",
"items": []
},
"interests": {
"name": "Interests",
"columns": 1,
"visible": true,
"id": "interests",
"items": []
},
"languages": {
"name": "Languages",
"columns": 1,
"visible": true,
"id": "languages",
"items": [
{
"id": "ey0c353j3apepzqmyl8zd37c",
"visible": true,
"name": "English",
"description": "Native Speaker",
"level": 0
},
{
"id": "a4dnf32wyj5phgln2rx97qk2",
"visible": true,
"name": "Spanish",
"description": "Intermediate",
"level": 0
}
]
},
"profiles": {
"name": "Profiles",
"columns": 1,
"visible": true,
"id": "profiles",
"items": [
{
"id": "cnbk5f0aeqvhx69ebk7hktwd",
"visible": true,
"network": "LinkedIn",
"username": "johndoe",
"icon": "linkedin",
"url": {
"label": "",
"href": "https://linkedin.com/in/johndoe"
}
},
{
"id": "ukl0uecvzkgm27mlye0wazlb",
"visible": true,
"network": "GitHub",
"username": "johndoe",
"icon": "github",
"url": {
"label": "",
"href": "https://github.com/johndoe"
}
},
{
"id": "k97vdxzmssmgel79km48kxgg",
"visible": true,
"network": "StackOverflow",
"username": "johndoe",
"icon": "stackoverflow",
"url": {
"label": "",
"href": "https://stackoverflow.com/u/johndoe"
}
}
]
},
"projects": {
"name": "Projects",
"columns": 1,
"visible": true,
"id": "projects",
"items": [
{
"id": "yw843emozcth8s1ubi1ubvlf",
"visible": true,
"name": "E-Commerce Platform",
"description": "Project Lead",
"date": "",
"summary": "<p>Led the development of a full-stack e-commerce platform, improving sales conversion by 25%.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
},
{
"id": "ncxgdjjky54gh59iz2t1xi1v",
"visible": true,
"name": "Interactive Dashboard",
"description": "Frontend Developer",
"date": "",
"summary": "<p>Created an interactive analytics dashboard for a SaaS application, enhancing data visualization for clients.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
}
]
},
"publications": {
"name": "Publications",
"columns": 1,
"visible": true,
"id": "publications",
"items": []
},
"references": {
"name": "References",
"columns": 1,
"visible": true,
"id": "references",
"items": [
{
"id": "f2sv5z0cce6ztjl87yuk8fak",
"visible": true,
"name": "Available upon request",
"description": "",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"skills": {
"name": "Skills",
"columns": 1,
"visible": true,
"id": "skills",
"items": [
{
"id": "hn0keriukh6c0ojktl9gsgjm",
"visible": true,
"name": "Web Technologies",
"description": "Advanced",
"level": 0,
"keywords": ["HTML5", "JavaScript", "PHP", "Python"]
},
{
"id": "r8c3y47vykausqrgmzwg5pur",
"visible": true,
"name": "Web Frameworks",
"description": "Intermediate",
"level": 0,
"keywords": ["React.js", "Angular", "Vue.js", "Laravel", "Django"]
},
{
"id": "b5l75aseexqv17quvqgh73fe",
"visible": true,
"name": "Tools",
"description": "Intermediate",
"level": 0,
"keywords": ["Webpack", "Git", "Jenkins", "Docker", "JIRA"]
}
]
},
"custom": {}
},
"metadata": {
"template": "pikachu",
"layout": [
[
["summary", "experience", "education", "projects"],
[
"profiles",
"skills",
"certifications",
"languages",
"references",
"interests",
"awards",
"volunteer",
"publications"
]
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {
"margin": 14,
"format": "a4",
"options": {
"breakLine": true,
"pageNumbers": true
}
},
"theme": {
"background": "#ffffff",
"text": "#000000",
"primary": "#ca8a04"
},
"typography": {
"font": {
"family": "Titillium Web",
"subset": "latin",
"variants": ["regular"],
"size": 13.8
},
"lineHeight": 1.5,
"hideIcons": false,
"underlineLinks": false
},
"notes": ""
}
}

View File

@ -0,0 +1,320 @@
{
"basics": {
"name": "John Doe",
"headline": "Creative and Innovative Web Developer",
"email": "john.doe@gmail.com",
"phone": "(555) 123-4567",
"location": "Pleasantville, CA 94588",
"url": {
"label": "",
"href": "https://johndoe.me/"
},
"customFields": [],
"picture": {
"url": "https://i.imgur.com/HgwyOuJ.jpg",
"size": 64,
"aspectRatio": 1,
"borderRadius": 0,
"effects": {
"hidden": false,
"border": false,
"grayscale": false
}
}
},
"sections": {
"summary": {
"name": "Summary",
"columns": 1,
"visible": true,
"id": "summary",
"content": "<p>Innovative Web Developer with 5 years of experience in building impactful and user-friendly websites and applications. Specializes in <strong>front-end technologies</strong> and passionate about modern web standards and cutting-edge development techniques. Proven track record of leading successful projects from concept to deployment.</p>"
},
"awards": {
"name": "Awards",
"columns": 1,
"visible": true,
"id": "awards",
"items": []
},
"certifications": {
"name": "Certifications",
"columns": 2,
"visible": true,
"id": "certifications",
"items": [
{
"id": "spdhh9rrqi1gvj0yqnbqunlo",
"visible": true,
"name": "Full-Stack Web Development",
"issuer": "CodeAcademy",
"date": "2020",
"summary": "",
"url": {
"label": "",
"href": ""
}
},
{
"id": "n838rddyqv47zexn6cxauwqp",
"visible": true,
"name": "AWS Certified Developer",
"issuer": "Amazon Web Services",
"date": "2019",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"education": {
"name": "Education",
"columns": 1,
"visible": true,
"id": "education",
"items": [
{
"id": "yo3p200zo45c6cdqc6a2vtt3",
"visible": true,
"institution": "University of California",
"studyType": "Bachelor's in Computer Science",
"area": "Berkeley, CA",
"score": "",
"date": "August 2012 to May 2016",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"experience": {
"name": "Experience",
"columns": 1,
"visible": true,
"id": "experience",
"items": [
{
"id": "lhw25d7gf32wgdfpsktf6e0x",
"visible": true,
"company": "Creative Solutions Inc.",
"position": "Senior Web Developer",
"location": "San Francisco, CA",
"date": "January 2019 to Present",
"summary": "<ul><li><p>Spearheaded the redesign of the main product website, resulting in a 40% increase in user engagement.</p></li><li><p>Developed and implemented a new responsive framework, improving cross-device compatibility.</p></li><li><p>Mentored a team of four junior developers, fostering a culture of technical excellence.</p></li></ul>",
"url": {
"label": "",
"href": "https://creativesolutions.inc/"
}
},
{
"id": "r6543lil53ntrxmvel53gbtm",
"visible": true,
"company": "TechAdvancers",
"position": "Web Developer",
"location": "San Jose, CA",
"date": "June 2016 to December 2018",
"summary": "<ul><li><p>Collaborated in a team of 10 to develop high-quality web applications using React.js and Node.js.</p></li><li><p>Managed the integration of third-party services such as Stripe for payments and Twilio for SMS services.</p></li><li><p>Optimized application performance, achieving a 30% reduction in load times.</p></li></ul>",
"url": {
"label": "",
"href": "https://techadvancers.com/"
}
}
]
},
"volunteer": {
"name": "Volunteering",
"columns": 1,
"visible": true,
"id": "volunteer",
"items": []
},
"interests": {
"name": "Interests",
"columns": 1,
"visible": true,
"id": "interests",
"items": []
},
"languages": {
"name": "Languages",
"columns": 1,
"visible": true,
"id": "languages",
"items": []
},
"profiles": {
"name": "Profiles",
"columns": 4,
"visible": true,
"id": "profiles",
"items": [
{
"id": "cnbk5f0aeqvhx69ebk7hktwd",
"visible": true,
"network": "LinkedIn",
"username": "johndoe",
"icon": "linkedin",
"url": {
"label": "",
"href": "https://linkedin.com/in/johndoe"
}
},
{
"id": "ukl0uecvzkgm27mlye0wazlb",
"visible": true,
"network": "GitHub",
"username": "johndoe",
"icon": "github",
"url": {
"label": "",
"href": "https://github.com/johndoe"
}
}
]
},
"projects": {
"name": "Projects",
"columns": 2,
"visible": true,
"id": "projects",
"items": [
{
"id": "yw843emozcth8s1ubi1ubvlf",
"visible": true,
"name": "E-Commerce Platform",
"description": "Project Lead",
"date": "",
"summary": "<p>Led the development of a full-stack e-commerce platform, improving sales conversion by 25%.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
},
{
"id": "ncxgdjjky54gh59iz2t1xi1v",
"visible": true,
"name": "Interactive Dashboard",
"description": "Frontend Developer",
"date": "",
"summary": "<p>Created an interactive analytics dashboard for a SaaS application, enhancing data visualization for clients.</p>",
"keywords": [],
"url": {
"label": "",
"href": ""
}
}
]
},
"publications": {
"name": "Publications",
"columns": 1,
"visible": true,
"id": "publications",
"items": []
},
"references": {
"name": "References",
"columns": 1,
"visible": false,
"id": "references",
"items": [
{
"id": "f2sv5z0cce6ztjl87yuk8fak",
"visible": true,
"name": "Available upon request",
"description": "",
"summary": "",
"url": {
"label": "",
"href": ""
}
}
]
},
"skills": {
"name": "Skills",
"columns": 3,
"visible": true,
"id": "skills",
"items": [
{
"id": "hn0keriukh6c0ojktl9gsgjm",
"visible": true,
"name": "Web Technologies",
"description": "Advanced",
"level": 0,
"keywords": ["HTML5", "JavaScript", "PHP", "Python"]
},
{
"id": "r8c3y47vykausqrgmzwg5pur",
"visible": true,
"name": "Web Frameworks",
"description": "Intermediate",
"level": 0,
"keywords": ["React.js", "Angular", "Vue.js", "Laravel", "Django"]
},
{
"id": "b5l75aseexqv17quvqgh73fe",
"visible": true,
"name": "Tools",
"description": "Intermediate",
"level": 0,
"keywords": ["Webpack", "Git", "Jenkins", "Docker", "JIRA"]
}
]
},
"custom": {}
},
"metadata": {
"template": "rhyhorn",
"layout": [
[
[
"profiles",
"summary",
"experience",
"education",
"projects",
"certifications",
"skills",
"references"
],
["interests", "languages", "awards", "volunteer", "publications"]
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {
"margin": 14,
"format": "a4",
"options": {
"breakLine": true,
"pageNumbers": true
}
},
"theme": {
"background": "#ffffff",
"text": "#000000",
"primary": "#ca8a04"
},
"typography": {
"font": {
"family": "Merriweather",
"subset": "latin",
"variants": ["regular"],
"size": 13
},
"lineHeight": 1.75,
"hideIcons": false,
"underlineLinks": true
},
"notes": ""
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

View File

@ -1,20 +1,21 @@
export const colors: string[] = [
"#78716c", // stone-500
"#ef4444", // red-500
"#f97316", // orange-500
"#f59e0b", // amber-500
"#eab308", // yellow-500
"#84cc16", // lime-500
"#22c55e", // green-500
"#10b981", // emerald-500
"#14b8a6", // teal-500
"#06b6d4", // cyan-500
"#0ea5e9", // sky-500
"#3b82f6", // blue-500
"#6366f1", // indigo-500
"#8b5cf6", // violet-500
"#a855f7", // purple-500
"#d946ef", // fuchsia-500
"#ec4899", // pink-500
"#f43f5e", // rose-500
"#475569", // slate-600
"#57534e", // stone-600
"#dc2626", // red-600
"#ea580c", // orange-600
"#d97706", // amber-600
"#ca8a04", // yellow-600
"#65a30d", // lime-600
"#16a34a", // green-600
"#059669", // emerald-600
"#0d9488", // teal-600
"#0891b2", // cyan-600
"#0284c7", // sky-600
"#2563eb", // blue-600
"#4f46e5", // indigo-600
"#7c3aed", // violet-600
"#9333ea", // purple-600
"#c026d3", // fuchsia-600
"#db2777", // pink-600
"#e11d48", // rose-600
];

View File

@ -210,7 +210,7 @@ msgid "Backup Codes may contain only lowercase letters or numbers, and must be e
msgstr "Backup Codes may contain only lowercase letters or numbers, and must be exactly 10 characters."
#: apps/client/src/pages/builder/sidebars/left/index.tsx:57
msgctxt "The Basics section of a Resume consists of User's Picture, Full Name, Location etc."
msgctxt "The basics section of a resume consists of User's Picture, Full Name, Location etc."
msgid "Basics"
msgstr "Basics"
@ -247,7 +247,7 @@ msgstr "Cancel"
msgid "Casual"
msgstr "Casual"
#: apps/client/src/pages/builder/_components/toolbar.tsx:90
#: apps/client/src/pages/builder/_components/toolbar.tsx:89
msgid "Center Artboard"
msgstr "Center Artboard"
@ -310,7 +310,7 @@ msgstr "Continue"
msgid "Copy"
msgstr "Copy"
#: apps/client/src/pages/builder/_components/toolbar.tsx:124
#: apps/client/src/pages/builder/_components/toolbar.tsx:123
msgid "Copy Link to Resume"
msgstr "Copy Link to Resume"
@ -521,7 +521,7 @@ msgstr "Even if you're not in a position to contribute financially, you can stil
msgid "Export"
msgstr "Export"
#: apps/client/src/pages/builder/_components/toolbar.tsx:130
#: apps/client/src/pages/builder/_components/toolbar.tsx:129
msgid "Export as PDF"
msgstr "Export as PDF"
@ -652,6 +652,8 @@ msgstr "Here, you can update your account information such as your profile pictu
msgid "Here, you can update your profile to customize and personalize your experience."
msgstr "Here, you can update your profile to customize and personalize your experience."
#: apps/client/src/pages/builder/sidebars/left/dialogs/languages.tsx:77
#: apps/client/src/pages/builder/sidebars/left/dialogs/skills.tsx:82
#: apps/client/src/pages/builder/sidebars/left/sections/picture/options.tsx:185
msgid "Hidden"
msgstr "Hidden"
@ -787,7 +789,7 @@ msgstr "JSON"
#: apps/client/src/pages/builder/sidebars/left/dialogs/custom-section.tsx:145
#: apps/client/src/pages/builder/sidebars/left/dialogs/interests.tsx:55
#: apps/client/src/pages/builder/sidebars/left/dialogs/projects.tsx:122
#: apps/client/src/pages/builder/sidebars/left/dialogs/skills.tsx:95
#: apps/client/src/pages/builder/sidebars/left/dialogs/skills.tsx:99
msgid "Keywords"
msgstr "Keywords"
@ -952,7 +954,7 @@ msgstr "Notes"
msgid "One-Time Password"
msgstr "One-Time Password"
#: apps/client/src/libs/axios.ts:32
#: apps/client/src/libs/axios.ts:34
#: apps/client/src/pages/dashboard/resumes/_dialogs/import.tsx:190
msgid "Oops, the server returned an error."
msgstr "Oops, the server returned an error."
@ -1028,7 +1030,7 @@ msgstr "Photograph by Patrick Tomasso"
msgid "Pick any font from Google Fonts"
msgstr "Pick any font from Google Fonts"
#: apps/client/src/pages/builder/sidebars/left/sections/picture/section.tsx:52
#: apps/client/src/pages/builder/sidebars/left/sections/picture/section.tsx:69
#: apps/client/src/pages/dashboard/settings/_sections/account.tsx:120
msgid "Picture"
msgstr "Picture"
@ -1118,7 +1120,7 @@ msgstr "Reactive Resume is a passion project of over 3 years of hard work, and w
msgid "Reactive Resume thrives thanks to its vibrant community. This project owes its progress to numerous individuals who've dedicated their time and skills. Below, we celebrate the coders who've enhanced its features on GitHub and the linguists whose translations on Crowdin have made it accessible to a broader audience."
msgstr "Reactive Resume thrives thanks to its vibrant community. This project owes its progress to numerous individuals who've dedicated their time and skills. Below, we celebrate the coders who've enhanced its features on GitHub and the linguists whose translations on Crowdin have made it accessible to a broader audience."
#: apps/client/src/pages/builder/_components/toolbar.tsx:64
#: apps/client/src/pages/builder/_components/toolbar.tsx:63
msgid "Redo"
msgstr "Redo"
@ -1150,7 +1152,7 @@ msgstr "Reset Layout"
msgid "Reset your password"
msgstr "Reset your password"
#: apps/client/src/pages/builder/_components/toolbar.tsx:84
#: apps/client/src/pages/builder/_components/toolbar.tsx:83
msgid "Reset Zoom"
msgstr "Reset Zoom"
@ -1444,11 +1446,11 @@ msgstr "Title"
msgid "Title"
msgstr "Title"
#: apps/client/src/pages/builder/_components/toolbar.tsx:98
#: apps/client/src/pages/builder/_components/toolbar.tsx:97
msgid "Toggle Page Break Line"
msgstr "Toggle Page Break Line"
#: apps/client/src/pages/builder/_components/toolbar.tsx:110
#: apps/client/src/pages/builder/_components/toolbar.tsx:109
msgid "Toggle Page Numbers"
msgstr "Toggle Page Numbers"
@ -1487,7 +1489,7 @@ msgstr "Typography"
msgid "Underline Links"
msgstr "Underline Links"
#: apps/client/src/pages/builder/_components/toolbar.tsx:58
#: apps/client/src/pages/builder/_components/toolbar.tsx:57
msgid "Undo"
msgstr "Undo"
@ -1603,7 +1605,7 @@ msgstr "What's new in the latest version"
#: apps/client/src/pages/builder/sidebars/left/dialogs/custom-section.tsx:150
#: apps/client/src/pages/builder/sidebars/left/dialogs/interests.tsx:60
#: apps/client/src/pages/builder/sidebars/left/dialogs/projects.tsx:127
#: apps/client/src/pages/builder/sidebars/left/dialogs/skills.tsx:100
#: apps/client/src/pages/builder/sidebars/left/dialogs/skills.tsx:104
msgid "You can add multiple keywords by separating them with a comma or pressing enter."
msgstr "You can add multiple keywords by separating them with a comma or pressing enter."
@ -1651,10 +1653,10 @@ msgstr "Your OpenAI API Key has not been set yet. Please go to your account sett
msgid "Your password has been updated successfully."
msgstr "Your password has been updated successfully."
#: apps/client/src/pages/builder/_components/toolbar.tsx:72
#: apps/client/src/pages/builder/_components/toolbar.tsx:71
msgid "Zoom In"
msgstr "Zoom In"
#: apps/client/src/pages/builder/_components/toolbar.tsx:78
#: apps/client/src/pages/builder/_components/toolbar.tsx:77
msgid "Zoom Out"
msgstr "Zoom Out"

View File

@ -5,7 +5,7 @@ import {
CircleNotch,
ClockClockwise,
CubeFocus,
DownloadSimple,
FilePdf,
Hash,
LineSegment,
LinkSimple,
@ -51,7 +51,7 @@ export const BuilderToolbar = () => {
<motion.div
initial={{ opacity: 0.5 }}
whileHover={{ opacity: 1 }}
className="fixed inset-x-0 mx-auto hidden pb-4 pt-6 text-center md:block"
className="fixed inset-x-0 bottom-0 mx-auto pb-4 pt-6 text-center md:block"
>
<div className="inline-flex items-center justify-center rounded-full bg-background px-4 shadow-xl">
<Tooltip content={t`Undo`}>
@ -134,7 +134,7 @@ export const BuilderToolbar = () => {
onClick={onPrint}
disabled={loading}
>
{loading ? <CircleNotch className="animate-spin" /> : <DownloadSimple />}
{loading ? <CircleNotch className="animate-spin" /> : <FilePdf />}
</Button>
</Tooltip>
</div>

View File

@ -10,7 +10,6 @@ import {
Input,
Slider,
} from "@reactive-resume/ui";
import { getCEFRLevel } from "@reactive-resume/utils";
import { useForm } from "react-hook-form";
import { z } from "zod";
@ -44,11 +43,11 @@ export const LanguagesDialog = () => {
/>
<FormField
name="fluency"
name="description"
control={form.control}
render={({ field }) => (
<FormItem>
<FormLabel>{t`Fluency`}</FormLabel>
<FormLabel>{t`Description`}</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
@ -58,22 +57,26 @@ export const LanguagesDialog = () => {
/>
<FormField
name="fluencyLevel"
name="level"
control={form.control}
render={({ field }) => (
<FormItem className="sm:col-span-2">
<FormLabel>{t`Fluency (CEFR)`}</FormLabel>
<FormLabel>{t`Level`}</FormLabel>
<FormControl className="py-2">
<div className="flex items-center gap-x-4">
<Slider
{...field}
min={1}
max={6}
min={0}
max={5}
value={[field.value]}
onValueChange={(value) => field.onChange(value[0])}
/>
<span className="text-base font-bold">{getCEFRLevel(field.value)}</span>
{field.value === 0 ? (
<span className="text-base font-bold">{t`Hidden`}</span>
) : (
<span className="text-base font-bold">{field.value}</span>
)}
</div>
</FormControl>
<FormMessage />

View File

@ -71,14 +71,18 @@ export const SkillsDialog = () => {
<div className="flex items-center gap-x-4">
<Slider
{...field}
min={1}
min={0}
max={5}
value={[field.value]}
orientation="horizontal"
onValueChange={(value) => field.onChange(value[0])}
/>
<span className="text-base font-bold">{field.value}</span>
{field.value === 0 ? (
<span className="text-base font-bold">{t`Hidden`}</span>
) : (
<span className="text-base font-bold">{field.value}</span>
)}
</div>
</FormControl>
<FormMessage />

View File

@ -16,7 +16,6 @@ import {
Volunteer,
} from "@reactive-resume/schema";
import { Button, ScrollArea, Separator } from "@reactive-resume/ui";
import { getCEFRLevel } from "@reactive-resume/utils";
import { Fragment, useRef } from "react";
import { Link } from "react-router-dom";
@ -57,21 +56,21 @@ export const LeftSidebar = () => {
name={t({
message: "Basics",
context:
"The Basics section of a Resume consists of User's Picture, Full Name, Location etc.",
"The basics section of a resume consists of User's Picture, Full Name, Location etc.",
})}
/>
<SectionIcon id="summary" onClick={() => scrollIntoView("#summary")} />
<SectionIcon id="profiles" onClick={() => scrollIntoView("#profiles")} />
<SectionIcon id="experience" onClick={() => scrollIntoView("#experience")} />
<SectionIcon id="education" onClick={() => scrollIntoView("#education")} />
<SectionIcon id="skills" onClick={() => scrollIntoView("#skills")} />
<SectionIcon id="languages" onClick={() => scrollIntoView("#languages")} />
<SectionIcon id="awards" onClick={() => scrollIntoView("#awards")} />
<SectionIcon id="certifications" onClick={() => scrollIntoView("#certifications")} />
<SectionIcon id="interests" onClick={() => scrollIntoView("#interests")} />
<SectionIcon id="languages" onClick={() => scrollIntoView("#languages")} />
<SectionIcon id="volunteer" onClick={() => scrollIntoView("#volunteer")} />
<SectionIcon id="projects" onClick={() => scrollIntoView("#projects")} />
<SectionIcon id="publications" onClick={() => scrollIntoView("#publications")} />
<SectionIcon id="skills" onClick={() => scrollIntoView("#skills")} />
<SectionIcon id="volunteer" onClick={() => scrollIntoView("#volunteer")} />
<SectionIcon id="references" onClick={() => scrollIntoView("#references")} />
<SectionIcon
@ -118,6 +117,21 @@ export const LeftSidebar = () => {
description={(item) => item.area}
/>
<Separator />
<SectionBase<Skill>
id="skills"
title={(item) => item.name}
description={(item) => {
if (item.description) return item.description;
if (item.keywords.length > 0) return `${item.keywords.length} keywords`;
}}
/>
<Separator />
<SectionBase<Language>
id="languages"
title={(item) => item.name}
description={(item) => item.description}
/>
<Separator />
<SectionBase<Award>
id="awards"
title={(item) => item.title}
@ -138,18 +152,6 @@ export const LeftSidebar = () => {
}}
/>
<Separator />
<SectionBase<Language>
id="languages"
title={(item) => item.name}
description={(item) => item.fluency || getCEFRLevel(item.fluencyLevel)}
/>
<Separator />
<SectionBase<Volunteer>
id="volunteer"
title={(item) => item.organization}
description={(item) => item.position}
/>
<Separator />
<SectionBase<Project>
id="projects"
title={(item) => item.name}
@ -162,13 +164,10 @@ export const LeftSidebar = () => {
description={(item) => item.publisher}
/>
<Separator />
<SectionBase<Skill>
id="skills"
title={(item) => item.name}
description={(item) => {
if (item.description) return item.description;
if (item.keywords.length > 0) return `${item.keywords.length} keywords`;
}}
<SectionBase<Volunteer>
id="volunteer"
title={(item) => item.organization}
description={(item) => item.position}
/>
<Separator />
<SectionBase<Reference>

View File

@ -1,8 +1,7 @@
import { t } from "@lingui/macro";
import { Aperture, UploadSimple } from "@phosphor-icons/react";
import { Aperture, Trash, UploadSimple } from "@phosphor-icons/react";
import {
Avatar,
AvatarFallback,
AvatarImage,
buttonVariants,
Input,
@ -11,8 +10,8 @@ import {
PopoverContent,
PopoverTrigger,
} from "@reactive-resume/ui";
import { cn, getInitials } from "@reactive-resume/utils";
import { AnimatePresence, motion } from "framer-motion";
import { cn } from "@reactive-resume/utils";
import { motion } from "framer-motion";
import { useMemo, useRef } from "react";
import { z } from "zod";
@ -23,10 +22,9 @@ import { PictureOptions } from "./options";
export const PictureSection = () => {
const inputRef = useRef<HTMLInputElement>(null);
const { uploadImage, loading } = useUploadImage();
const { uploadImage } = useUploadImage();
const setValue = useResumeStore((state) => state.setValue);
const name = useResumeStore((state) => state.resume.data.basics.name);
const picture = useResumeStore((state) => state.resume.data.basics.picture);
const isValidUrl = useMemo(() => z.string().url().safeParse(picture.url).success, [picture.url]);
@ -41,16 +39,37 @@ export const PictureSection = () => {
}
};
const onAvatarClick = () => {
if (isValidUrl) {
setValue("basics.picture.url", "");
} else {
inputRef.current?.click();
}
};
return (
<div className="flex items-center gap-x-4">
<Avatar className="h-14 w-14">
{isValidUrl && <AvatarImage src={picture.url} />}
<AvatarFallback className="text-lg font-bold">{getInitials(name)}</AvatarFallback>
</Avatar>
<div className="group relative cursor-pointer" onClick={onAvatarClick}>
<Avatar className="h-14 w-14 bg-secondary">
<AvatarImage src={picture.url} />
</Avatar>
{isValidUrl ? (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-full bg-background/30 opacity-0 transition-opacity group-hover:opacity-100">
<Trash size={16} weight="bold" />
</div>
) : (
<div className="pointer-events-none absolute inset-0 flex items-center justify-center rounded-full bg-background/30 opacity-0 transition-opacity group-hover:opacity-100">
<UploadSimple size={16} weight="bold" />
</div>
)}
</div>
<div className="flex w-full flex-col gap-y-1.5">
<Label htmlFor="basics.picture.url">{t`Picture`}</Label>
<div className="flex items-center gap-x-2">
<input hidden type="file" ref={inputRef} onChange={onSelectImage} />
<Input
id="basics.picture.url"
placeholder="https://..."
@ -58,44 +77,23 @@ export const PictureSection = () => {
onChange={(event) => setValue("basics.picture.url", event.target.value)}
/>
<AnimatePresence>
{/* Show options button if picture exists */}
{isValidUrl && (
<Popover>
<PopoverTrigger asChild>
<motion.button
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
className={cn(buttonVariants({ size: "icon", variant: "ghost" }))}
>
<Aperture />
</motion.button>
</PopoverTrigger>
<PopoverContent className="w-[360px]">
<PictureOptions />
</PopoverContent>
</Popover>
)}
{/* Show upload button if picture doesn't exist, else show remove button to delete picture */}
{!isValidUrl && (
<>
<input hidden type="file" ref={inputRef} onChange={onSelectImage} />
{isValidUrl && (
<Popover>
<PopoverTrigger asChild>
<motion.button
disabled={loading}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
onClick={() => inputRef.current?.click()}
className={cn(buttonVariants({ size: "icon", variant: "ghost" }))}
>
<UploadSimple />
<Aperture />
</motion.button>
</>
)}
</AnimatePresence>
</PopoverTrigger>
<PopoverContent className="w-[360px]">
<PictureOptions />
</PopoverContent>
</Popover>
)}
</div>
</div>
</div>

View File

@ -1,6 +1,7 @@
import { t } from "@lingui/macro";
import { Button, HoverCard, HoverCardContent, HoverCardTrigger } from "@reactive-resume/ui";
import { AspectRatio } from "@reactive-resume/ui";
import { cn, templatesList } from "@reactive-resume/utils";
import { motion } from "framer-motion";
import { useResumeStore } from "@/client/stores/resume";
@ -19,31 +20,28 @@ export const TemplateSection = () => {
</div>
</header>
<main className="grid grid-cols-2 gap-4">
{templatesList.map(({ id, name }) => (
<HoverCard key={id} openDelay={0} closeDelay={0}>
<HoverCardTrigger asChild>
<Button
variant="outline"
onClick={() => setValue("metadata.template", id)}
className={cn(
"flex h-12 items-center justify-center overflow-hidden rounded border text-center text-sm capitalize ring-primary transition-colors hover:bg-secondary-accent focus:outline-none focus:ring-1 disabled:opacity-100",
id === currentTemplate && "ring-1",
)}
>
{name}
</Button>
</HoverCardTrigger>
<main className="grid grid-cols-2 gap-5 @lg/right:grid-cols-3 @2xl/right:grid-cols-4">
{templatesList.map((template, index) => (
<AspectRatio ratio={1 / 1.4142}>
<motion.div
initial={{ opacity: 0, x: -50 }}
animate={{ opacity: 1, x: 0, transition: { delay: index * 0.1 } }}
whileTap={{ scale: 0.98, transition: { duration: 0.1 } }}
onClick={() => setValue("metadata.template", template)}
className={cn(
"relative cursor-pointer rounded-sm ring-primary transition-all hover:ring-2",
currentTemplate === template && "ring-2",
)}
>
<img src={`/templates/jpg/${template}.jpg`} alt={template} className="rounded-sm" />
<HoverCardContent className="max-w-xs overflow-hidden border-none bg-white p-0">
<img
alt={name}
loading="lazy"
src="/templates/sample.jpg"
className="aspect-[1/1.4142]"
/>
</HoverCardContent>
</HoverCard>
<div className="absolute inset-x-0 bottom-0 h-32 w-full bg-gradient-to-b from-transparent to-background/80">
<p className="absolute inset-x-0 bottom-2 text-center font-bold capitalize text-primary">
{template}
</p>
</div>
</motion.div>
</AspectRatio>
))}
</main>
</section>

View File

@ -137,7 +137,7 @@ export const TypographySection = () => {
<Slider
min={12}
max={18}
step={1}
step={0.05}
value={[typography.font.size]}
onValueChange={(value) => {
setValue("metadata.typography.font.size", value[0]);
@ -154,7 +154,7 @@ export const TypographySection = () => {
<Slider
min={0}
max={3}
step={0.25}
step={0.05}
value={[typography.lineHeight]}
onValueChange={(value) => {
setValue("metadata.typography.lineHeight", value[0]);

View File

@ -1,6 +1,6 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { t } from "@lingui/macro";
import { Check, DownloadSimple, Warning } from "@phosphor-icons/react";
import { Check, DownloadSimple } from "@phosphor-icons/react";
import {
JsonResume,
JsonResumeParser,
@ -141,7 +141,6 @@ export const ImportDialog = () => {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: t`An error occurred while validating the file.`,
});
}
@ -186,7 +185,6 @@ export const ImportDialog = () => {
} catch (error) {
toast({
variant: "error",
icon: <Warning size={16} weight="bold" />,
title: t`Oops, the server returned an error.`,
description: importError?.message,
});

View File

@ -6,9 +6,9 @@ import { FAQSection } from "./sections/faq";
import { FeaturesSection } from "./sections/features";
import { HeroSection } from "./sections/hero";
import { LogoCloudSection } from "./sections/logo-cloud";
import { SampleResumesSection } from "./sections/sample-resumes";
import { StatisticsSection } from "./sections/statistics";
import { SupportSection } from "./sections/support";
import { TemplatesSection } from "./sections/templates";
import { TestimonialsSection } from "./sections/testimonials";
export const HomePage = () => (
@ -23,7 +23,7 @@ export const HomePage = () => (
<LogoCloudSection />
<StatisticsSection />
<FeaturesSection />
<SampleResumesSection />
<TemplatesSection />
<TestimonialsSection />
<SupportSection />
<FAQSection />

View File

@ -1,35 +1,22 @@
import { t, Trans } from "@lingui/macro";
import { t } from "@lingui/macro";
import { templatesList } from "@reactive-resume/utils";
import { motion } from "framer-motion";
const resumes = [
"/sample-resumes/ditto",
"/sample-resumes/ditto",
"/sample-resumes/ditto",
"/sample-resumes/ditto",
];
export const SampleResumesSection = () => (
export const TemplatesSection = () => (
<section id="sample-resumes" className="relative py-24 sm:py-32">
<div className="container flex flex-col gap-12 lg:min-h-[600px] lg:flex-row lg:items-start">
<div className="space-y-4 lg:mt-16 lg:basis-96">
<h2 className="text-4xl font-bold">{t`Sample Resumes`}</h2>
<h2 className="text-4xl font-bold">{t`Templates`}</h2>
<Trans>
<p className="leading-relaxed">
Have a look at some of the resume created to showcase the templates available on
Reactive Resume.
</p>
<p className="leading-relaxed">
They could also serve as examples to help guide the creation of your next resume.
</p>
</Trans>
<p className="leading-relaxed">
{t`Explore the templates available in Reactive Resume and view the resumes crafted with them. They could also serve as examples to help guide the creation of your next resume.`}
</p>
</div>
<div className="w-full overflow-hidden lg:absolute lg:right-0 lg:max-w-[55%]">
<motion.div
animate={{
x: [0, -400],
x: [0, templatesList.length * 200 * -1],
transition: {
x: {
duration: 30,
@ -40,20 +27,21 @@ export const SampleResumesSection = () => (
}}
className="flex items-center gap-x-6"
>
{resumes.map((resume, index) => (
{templatesList.map((template, index) => (
<motion.a
key={index}
target="_blank"
rel="noreferrer"
href={`${resume}.pdf`}
href={`templates/pdf/${template}.pdf`}
className="max-w-none flex-none"
viewport={{ once: true }}
initial={{ opacity: 0, x: -100 }}
whileInView={{ opacity: 1, x: 0, transition: { delay: (index + 1) * 0.5 } }}
whileInView={{ opacity: 1, x: 0 }}
>
<img
alt={resume}
src={`${resume}.jpg`}
alt={template}
loading="lazy"
src={`/templates/jpg/${template}.jpg`}
className=" aspect-[1/1.4142] h-[400px] rounded object-cover lg:h-[600px]"
/>
</motion.a>

View File

@ -1,7 +1,9 @@
import { t } from "@lingui/macro";
import { StatisticsDto, UrlDto } from "@reactive-resume/dto";
import { useMutation } from "@tanstack/react-query";
import { RESUME_KEY } from "@/client/constants/query-keys";
import { toast } from "@/client/hooks/use-toast";
import { axios } from "@/client/libs/axios";
import { queryClient } from "@/client/libs/query-client";
@ -24,6 +26,15 @@ export const usePrintResume = () => {
return { ...cache, downloads: cache.downloads + 1 } satisfies StatisticsDto;
});
},
onError: (error) => {
const message = error?.message;
toast({
variant: "error",
title: t`Oops, the server returned an error.`,
description: message,
});
},
});
return { printResume: printResumeFn, loading, error };

View File

@ -40,7 +40,7 @@ export const configSchema = z.object({
REDIS_URL: z.string().url().startsWith("redis://").optional(),
// Sentry
SENTRY_DSN: z.string().url().startsWith("https://").optional(),
VITE_SENTRY_DSN: z.string().url().startsWith("https://").optional(),
// Crowdin (Optional)
CROWDIN_PROJECT_ID: z.coerce.number().optional(),

View File

@ -1,6 +1,5 @@
import { Injectable } from "@nestjs/common";
import { HealthIndicator, HealthIndicatorResult } from "@nestjs/terminus";
import { withTimeout } from "@reactive-resume/utils";
import { PrinterService } from "../printer/printer.service";
@ -12,8 +11,7 @@ export class BrowserHealthIndicator extends HealthIndicator {
async isHealthy(): Promise<HealthIndicatorResult> {
try {
const version = await withTimeout(this.printerService.getVersion(), 5000);
// const version = await this.printerService.getVersion();
const version = await this.printerService.getVersion();
return this.getStatus("browser", true, { version });
} catch (error) {

View File

@ -23,7 +23,7 @@ async function bootstrap() {
// Sentry
// Error Reporting and Performance Monitoring
const sentryDsn = configService.get("SENTRY_DSN");
const sentryDsn = configService.get("VITE_SENTRY_DSN");
if (sentryDsn) {
const express = app.getHttpAdapter().getInstance();

View File

@ -4,7 +4,7 @@ import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import fontkit from "@pdf-lib/fontkit";
import { ResumeDto } from "@reactive-resume/dto";
import { getFontUrls, withTimeout } from "@reactive-resume/utils";
import { getFontUrls } from "@reactive-resume/utils";
import { ErrorMessage } from "@reactive-resume/utils";
import retry from "async-retry";
import { PDFDocument } from "pdf-lib";
@ -14,8 +14,6 @@ import { Config } from "../config/schema";
import { StorageService } from "../storage/storage.service";
import { UtilsService } from "../utils/utils.service";
const PRINTER_TIMEOUT = 10000; // 10 seconds
@Injectable()
export class PrinterService {
private readonly logger = new Logger(PrinterService.name);
@ -55,11 +53,11 @@ export class PrinterService {
async () => {
const start = performance.now();
const url = await retry(() => withTimeout(this.generateResume(resume), PRINTER_TIMEOUT), {
retries: 2,
const url = await retry(() => this.generateResume(resume), {
retries: 3,
randomize: true,
onRetry: (_, attempt) => {
this.logger.debug(`Print Resume: retry attempt #${attempt}`);
this.logger.log(`Retrying to print resume #${resume.id}, attempt #${attempt}`);
},
});
@ -76,117 +74,138 @@ export class PrinterService {
async printPreview(resume: ResumeDto) {
return this.utils.getCachedOrSet(
`user:${resume.userId}:storage:previews:${resume.id}`,
async () => withTimeout(this.generatePreview(resume), PRINTER_TIMEOUT),
async () => {
const start = performance.now();
const url = await retry(() => this.generatePreview(resume), {
retries: 3,
randomize: true,
onRetry: (_, attempt) => {
this.logger.log(
`Retrying to generate preview of resume #${resume.id}, attempt #${attempt}`,
);
},
});
const duration = Number(performance.now() - start).toFixed(0);
this.logger.debug(`Chrome took ${duration}ms to generate preview`);
return url;
},
);
}
async generateResume(resume: ResumeDto) {
const browser = await this.getBrowser();
const page = await browser.newPage();
try {
const browser = await this.getBrowser();
const page = await browser.newPage();
let url = this.utils.getUrl();
const publicUrl = this.configService.getOrThrow<string>("PUBLIC_URL");
const storageUrl = this.configService.getOrThrow<string>("STORAGE_URL");
let url = this.utils.getUrl();
const publicUrl = this.configService.getOrThrow<string>("PUBLIC_URL");
const storageUrl = this.configService.getOrThrow<string>("STORAGE_URL");
if ([publicUrl, storageUrl].some((url) => url.includes("localhost"))) {
// Switch client URL from `localhost` to `host.docker.internal` in development
// This is required because the browser is running in a container and the client is running on the host machine.
url = url.replace("localhost", "host.docker.internal");
if ([publicUrl, storageUrl].some((url) => url.includes("localhost"))) {
// Switch client URL from `localhost` to `host.docker.internal` in development
// This is required because the browser is running in a container and the client is running on the host machine.
url = url.replace("localhost", "host.docker.internal");
await page.setRequestInterception(true);
await page.setRequestInterception(true);
// Intercept requests of `localhost` to `host.docker.internal` in development
page.on("request", (request) => {
if (request.url().startsWith(storageUrl)) {
const modifiedUrl = request.url().replace("localhost", `host.docker.internal`);
// Intercept requests of `localhost` to `host.docker.internal` in development
page.on("request", (request) => {
if (request.url().startsWith(storageUrl)) {
const modifiedUrl = request.url().replace("localhost", `host.docker.internal`);
request.continue({ url: modifiedUrl });
} else {
request.continue();
}
});
request.continue({ url: modifiedUrl });
} else {
request.continue();
}
});
}
// Set the data of the resume to be printed in the browser's session storage
const numPages = resume.data.metadata.layout.length;
await page.evaluateOnNewDocument((data) => {
window.localStorage.setItem("resume", JSON.stringify(data));
}, resume.data);
await page.goto(`${url}/artboard/preview`, { waitUntil: "networkidle0" });
const pagesBuffer: Buffer[] = [];
const processPage = async (index: number) => {
const pageElement = await page.$(`[data-page="${index}"]`);
const width = (await (await pageElement?.getProperty("scrollWidth"))?.jsonValue()) ?? 0;
const height = (await (await pageElement?.getProperty("scrollHeight"))?.jsonValue()) ?? 0;
const tempHtml = await page.evaluate((element: HTMLDivElement) => {
const clonedElement = element.cloneNode(true) as HTMLDivElement;
const tempHtml = document.body.innerHTML;
document.body.innerHTML = `${clonedElement.outerHTML}`;
return tempHtml;
}, pageElement);
pagesBuffer.push(await page.pdf({ width, height, printBackground: true }));
await page.evaluate((tempHtml: string) => {
document.body.innerHTML = tempHtml;
}, tempHtml);
};
// Loop through all the pages and print them, by first displaying them, printing the PDF and then hiding them back
for (let index = 1; index <= numPages; index++) {
await processPage(index);
}
// Using 'pdf-lib', merge all the pages from their buffers into a single PDF
const pdf = await PDFDocument.create();
pdf.registerFontkit(fontkit);
// Get information about fonts used in the resume from the metadata
const fontData = resume.data.metadata.typography.font;
const fontUrls = getFontUrls(fontData.family, fontData.variants);
// Load all the fonts from the URLs using HttpService
const responses = await Promise.all(
fontUrls.map((url) =>
this.httpService.axiosRef.get(url, {
responseType: "arraybuffer",
}),
),
);
const fontsBuffer = responses.map((response) => response.data as ArrayBuffer);
// Embed all the fonts in the PDF
await Promise.all(fontsBuffer.map((buffer) => pdf.embedFont(buffer)));
for (let index = 0; index < pagesBuffer.length; index++) {
const page = await PDFDocument.load(pagesBuffer[index]);
const [copiedPage] = await pdf.copyPages(page, [0]);
pdf.addPage(copiedPage);
}
// Save the PDF to storage and return the URL to download the resume
// Store the URL in cache for future requests, under the previously generated hash digest
const buffer = Buffer.from(await pdf.save());
// This step will also save the resume URL in cache
const resumeUrl = await this.storageService.uploadObject(
resume.userId,
"resumes",
buffer,
resume.id,
);
// Close all the pages and disconnect from the browser
await page.close();
browser.disconnect();
return resumeUrl;
} catch (error) {
console.trace(error);
}
// Set the data of the resume to be printed in the browser's session storage
const numPages = resume.data.metadata.layout.length;
await page.evaluateOnNewDocument((data) => {
window.localStorage.setItem("resume", JSON.stringify(data));
}, resume.data);
await page.goto(`${url}/artboard/preview`, { waitUntil: "networkidle0" });
const pagesBuffer: Buffer[] = [];
const processPage = async (index: number) => {
const pageElement = await page.$(`[data-page="${index}"]`);
const width = (await (await pageElement?.getProperty("scrollWidth"))?.jsonValue()) ?? 0;
const height = (await (await pageElement?.getProperty("scrollHeight"))?.jsonValue()) ?? 0;
const tempHtml = await page.evaluate((element: HTMLDivElement) => {
const clonedElement = element.cloneNode(true) as HTMLDivElement;
const tempHtml = document.body.innerHTML;
document.body.innerHTML = `${clonedElement.outerHTML}`;
return tempHtml;
}, pageElement);
pagesBuffer.push(await page.pdf({ width, height, printBackground: true }));
await page.evaluate((tempHtml: string) => {
document.body.innerHTML = tempHtml;
}, tempHtml);
};
// Loop through all the pages and print them, by first displaying them, printing the PDF and then hiding them back
for (let index = 1; index <= numPages; index++) {
await processPage(index);
}
// Using 'pdf-lib', merge all the pages from their buffers into a single PDF
const pdf = await PDFDocument.create();
pdf.registerFontkit(fontkit);
// Get information about fonts used in the resume from the metadata
const fontData = resume.data.metadata.typography.font;
const fontUrls = getFontUrls(fontData.family, fontData.variants);
// Load all the fonts from the URLs using HttpService
const responses = await Promise.all(
fontUrls.map((url) =>
this.httpService.axiosRef.get(url, {
responseType: "arraybuffer",
}),
),
);
const fontsBuffer = responses.map((response) => response.data as ArrayBuffer);
// Embed all the fonts in the PDF
await Promise.all(fontsBuffer.map((buffer) => pdf.embedFont(buffer)));
for (let index = 0; index < pagesBuffer.length; index++) {
const page = await PDFDocument.load(pagesBuffer[index]);
const copiedPage = await pdf.copyPages(page, [0]);
pdf.addPage(copiedPage[0]);
}
// Save the PDF to storage and return the URL to download the resume
// Store the URL in cache for future requests, under the previously generated hash digest
const buffer = Buffer.from(await pdf.save());
// This step will also save the resume URL in cache
const resumeUrl = await this.storageService.uploadObject(
resume.userId,
"resumes",
buffer,
resume.id,
);
// Close all the pages and disconnect from the browser
await page.close();
browser.disconnect();
return resumeUrl;
}
async generatePreview(resume: ResumeDto) {

View File

@ -1,4 +1,4 @@
import { BadRequestException, Injectable, Logger } from "@nestjs/common";
import { BadRequestException, Injectable } from "@nestjs/common";
import { Prisma } from "@prisma/client";
import { CreateResumeDto, ImportResumeDto, ResumeDto, UpdateResumeDto } from "@reactive-resume/dto";
import { defaultResumeData, ResumeData } from "@reactive-resume/schema";
@ -18,7 +18,6 @@ import { UtilsService } from "../utils/utils.service";
@Injectable()
export class ResumeService {
private readonly redis: Redis;
private readonly logger = new Logger(ResumeService.name);
constructor(
private readonly prisma: PrismaService,

View File

@ -190,7 +190,7 @@ export class JsonResumeParser implements Parser<Json, JsonResume> {
...defaultLanguage,
id: createId(),
name: language.language ?? "",
fluency: language.fluency ?? "",
description: language.fluency ?? "",
});
}
}

View File

@ -130,7 +130,7 @@ export class LinkedInParser implements Parser<JSZip, LinkedIn> {
...defaultLanguage,
id: createId(),
name: language.Name,
fluency: language.Proficiency ?? "",
description: language.Proficiency ?? "",
});
}
}

View File

@ -177,8 +177,8 @@ export class ReactiveResumeV3Parser implements Parser<Json, ReactiveResumeV3> {
...defaultLanguage,
id: createId(),
name: language.name,
fluency: language.level,
fluencyLevel: Math.floor(language.levelNum / 2),
description: language.level,
level: Math.floor(language.levelNum / 2),
});
}
}

View File

@ -13,8 +13,8 @@ export const sampleResume: ResumeData = {
},
customFields: [],
picture: {
url: "https://res.cloudinary.com/amruth-pillai/image/upload/v1699362669/reactive-resume/sample-resume/sample-picture_iitowc.jpg",
size: 128,
url: "https://i.imgur.com/HgwyOuJ.jpg",
size: 70,
aspectRatio: 1,
borderRadius: 6,
effects: {
@ -212,35 +212,35 @@ export const sampleResume: ResumeData = {
id: "je6s7k590yp9wtv65bbcyiua",
visible: true,
name: "English",
fluency: "Native Speaker",
fluencyLevel: 5,
description: "Native Speaker",
level: 5,
},
{
id: "qdltq93lqi5g290t70brrpit",
visible: true,
name: "German",
fluency: "Advanced",
fluencyLevel: 5,
description: "Advanced",
level: 5,
},
{
id: "txxfl568h5yg5xojwj5sk3ms",
visible: true,
name: "French",
fluency: "Advanced",
fluencyLevel: 5,
description: "Advanced",
level: 5,
},
{
id: "zla2ezdkhemy68kg8bj86jk0",
visible: true,
name: "Spanish",
fluency: "Learning",
fluencyLevel: 2,
description: "Learning",
level: 2,
},
],
},
profiles: {
name: "Profiles",
columns: 2,
columns: 1,
visible: true,
id: "profiles",
items: [
@ -479,16 +479,16 @@ export const sampleResume: ResumeData = {
theme: {
background: "#ffffff",
text: "#000000",
primary: "#DC1A4E",
primary: "#398dc9",
},
typography: {
font: {
family: "IBM Plex Serif",
family: "IBM Plex Sans Condensed",
subset: "latin",
variants: ["regular", "italic", "500", "600", "600italic"],
variants: ["regular", "500"],
size: 13,
},
lineHeight: 2,
lineHeight: 1.5,
hideIcons: false,
underlineLinks: true,
},

View File

@ -5,8 +5,8 @@ import { defaultItem, itemSchema } from "../shared";
// Schema
export const languageSchema = itemSchema.extend({
name: z.string().min(1),
fluency: z.string(),
fluencyLevel: z.number().min(1).max(6),
description: z.string(),
level: z.number().min(0).max(5).default(1),
});
// Type
@ -16,6 +16,6 @@ export type Language = z.infer<typeof languageSchema>;
export const defaultLanguage: Language = {
...defaultItem,
name: "",
fluency: "",
fluencyLevel: 1,
description: "",
level: 1,
};

View File

@ -6,7 +6,7 @@ import { defaultItem, itemSchema } from "../shared";
export const skillSchema = itemSchema.extend({
name: z.string(),
description: z.string(),
level: z.number().min(1).max(5).default(1),
level: z.number().min(0).max(5).default(1),
keywords: z.array(z.string()).default([]),
});

View File

@ -1,10 +1,9 @@
export * from "./namespaces/array";
export * from "./namespaces/cefr";
export * from "./namespaces/csv";
export * from "./namespaces/date";
export * from "./namespaces/error";
export * from "./namespaces/fonts";
export * from "./namespaces/languages";
export * from "./namespaces/language";
export * from "./namespaces/number";
export * from "./namespaces/object";
export * from "./namespaces/page";

View File

@ -1,33 +1,5 @@
import { LayoutLocator } from "./types";
type CombinationsInput<T> = {
[K in keyof T]: T[K][];
};
export const generateCombinations = <T>(obj: CombinationsInput<T>): Array<T> => {
const keys = Object.keys(obj) as (keyof T)[];
const results: Array<T> = [];
function backtrack(combination: Partial<T>, index: number): void {
if (index === keys.length) {
results.push(combination as T);
return;
}
const key = keys[index];
const values = obj[key];
for (const value of values) {
backtrack({ ...combination, [key]: value }, index + 1);
}
}
backtrack({}, 0);
return results;
};
// Function to find a specific item in a layout
export const findItemInLayout = (item: string, layout: string[][][]): LayoutLocator | null => {
for (let page = 0; page < layout.length; page++) {
@ -60,17 +32,21 @@ export const moveItemInLayout = (
target: LayoutLocator,
layout: string[][][],
): string[][][] => {
// Create a deep copy of the layout to avoid mutating the original array
const newLayout = JSON.parse(JSON.stringify(layout)) as string[][][];
try {
// Create a deep copy of the layout to avoid mutating the original array
const newLayout = JSON.parse(JSON.stringify(layout)) as string[][][];
// Get the item from the current location
const item = newLayout[current.page][current.column][current.section];
// Get the item from the current location
const item = newLayout[current.page][current.column][current.section];
// Remove the item from the current location
newLayout[current.page][current.column].splice(current.section, 1);
// Remove the item from the current location
newLayout[current.page][current.column].splice(current.section, 1);
// Insert the item at the target location
newLayout[target.page][target.column].splice(target.section, 0, item);
// Insert the item at the target location
newLayout[target.page][target.column].splice(target.section, 0, item);
return newLayout;
return newLayout;
} catch (error) {
return layout;
}
};

View File

@ -1,13 +0,0 @@
// CEFR Levels
const cefrMap = {
1: "A1",
2: "A2",
3: "B1",
4: "B2",
5: "C1",
6: "C2",
};
export const getCEFRLevel = (level: number) => {
return cefrMap[level as keyof typeof cefrMap];
};

View File

@ -4,6 +4,7 @@ export const sortByDate = <T>(a: T, b: T, key: keyof T, desc = true) => {
if (!a[key] || !b[key]) return 0;
if (!(a[key] instanceof Date) || !(b[key] instanceof Date)) return 0;
if (dayjs(a[key] as Date).isSame(dayjs(b[key] as Date))) return 0;
if (desc) return dayjs(a[key] as Date).isBefore(dayjs(b[key] as Date)) ? 1 : -1;
else return dayjs(a[key] as Date).isBefore(dayjs(b[key] as Date)) ? -1 : 1;
};

View File

@ -1,3 +1,4 @@
// Languages
export type Language = {
id: string;
name: string;

View File

@ -4,4 +4,9 @@ export const linearTransform = (
inMax: number,
outMin: number,
outMax: number,
) => ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
) => {
if (inMax === inMin) return value === inMax ? outMin : NaN;
return ((value - inMin) * (outMax - outMin)) / (inMax - inMin) + outMin;
};
// Handle this case: returns output minimum if input maximum equals input minimum

View File

@ -1,11 +1 @@
export const delay = (time: number) => new Promise((resolve) => setTimeout(resolve, time));
export const withTimeout = async <T>(promise: Promise<T>, time: number): Promise<T> => {
const timeout = new Promise((_, reject) =>
setTimeout(() => {
return reject(new Error(`Operation timed out after ${time}ms`));
}, time),
);
return Promise.race([promise, timeout]) as T;
};

View File

@ -1,46 +1,13 @@
export type TemplateKey =
| "onyx"
| "kakuna"
| "rhyhorn"
| "azurill"
| "ditto"
| "chikorita"
| "bronzor"
| "pikachu";
export const templatesList = [
"azurill",
"bronzor",
"chikorita",
"ditto",
"kakuna",
"nosepass",
"onyx",
"pikachu",
"rhyhorn",
] as const;
export type Template = { id: TemplateKey; name: string };
export const templatesList: Template[] = [
{
id: "onyx",
name: "Onyx",
},
{
id: "kakuna",
name: "Kakuna",
},
{
id: "rhyhorn",
name: "Rhyhorn",
},
{
id: "azurill",
name: "Azurill",
},
{
id: "ditto",
name: "Ditto",
},
{
id: "chikorita",
name: "Chikorita",
},
{
id: "bronzor",
name: "Bronzor",
},
{
id: "pikachu",
name: "Pikachu",
},
];
export type Template = (typeof templatesList)[number];

View File

@ -0,0 +1,162 @@
import { describe, expect, it } from "vitest";
import { findItemInLayout, moveItemInLayout, removeItemInLayout } from "../array";
describe("findItemInLayout", () => {
it("should find the correct location of an item", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const item = "item3";
const expectedLocation = { page: 1, column: 0, section: 0 };
const location = findItemInLayout(item, layout);
expect(location).toEqual(expectedLocation);
});
it("should return null if the item is not found", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const item = "item5";
const location = findItemInLayout(item, layout);
expect(location).toBeNull();
});
});
describe("removeItemInLayout", () => {
it("should remove the item and return its location", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const item = "item3";
const expectedLocation = { page: 1, column: 0, section: 0 };
const location = removeItemInLayout(item, layout);
expect(location).toEqual(expectedLocation);
expect(layout[1][0]).not.toContain(item);
});
it("should return null and not modify layout if the item is not found", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const item = "item5";
const location = removeItemInLayout(item, layout);
expect(location).toBeNull();
expect(layout).toEqual([
[["item1"], ["item2"]],
[["item3"], ["item4"]],
]);
});
});
describe("moveItemInLayout", () => {
it("should move an item from the current location to the target location", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const current = { page: 0, column: 1, section: 0 };
const target = { page: 1, column: 0, section: 1 };
const expectedLayout = [
[["item1"], []],
[["item3", "item2"], ["item4"]],
];
const newLayout = moveItemInLayout(current, target, layout);
expect(newLayout).toEqual(expectedLayout);
});
it("should not mutate the original layout array", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const layoutCopy = JSON.parse(JSON.stringify(layout));
const current = { page: 0, column: 1, section: 0 };
const target = { page: 1, column: 0, section: 1 };
moveItemInLayout(current, target, layout);
expect(layout).toEqual(layoutCopy);
});
it("should handle the case where the current and target locations are the same", () => {
const layout = [
[["item1"], ["item2"]],
[["item3"], ["item4"]],
];
const current = { page: 1, column: 0, section: 0 };
const target = { page: 1, column: 0, section: 0 };
const newLayout = moveItemInLayout(current, target, layout);
expect(newLayout).toEqual(layout);
});
it("moves an item to the specified target location", () => {
const layout = [
[["A", "B"], ["C"]],
[["D"], ["E", "F"]],
];
const current = { page: 0, column: 0, section: 1 };
const target = { page: 1, column: 1, section: 1 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual([
[["A"], ["C"]],
[["D"], ["E", "B", "F"]],
]);
});
it("handles moving an item within the same column", () => {
const layout = [[["A", "B"]], [["C", "D"]]];
const current = { page: 0, column: 0, section: 0 };
const target = { page: 0, column: 0, section: 1 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual([[["B", "A"]], [["C", "D"]]]);
});
it("handles moving an item to the beginning of a column", () => {
const layout = [[["A"], ["B", "C"]], [["D"]]];
const current = { page: 1, column: 0, section: 0 };
const target = { page: 0, column: 1, section: 0 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual([[["A"], ["D", "B", "C"]], [[]]]);
});
it("handles moving an item to an empty column", () => {
const layout = [[["A"], []], [["B"]]];
const current = { page: 0, column: 0, section: 0 };
const target = { page: 0, column: 1, section: 0 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual([[[], ["A"]], [["B"]]]);
});
it("returns the same layout if the current location is invalid", () => {
const layout = [[["A"], ["B"]]];
const current = { page: 2, column: 0, section: 0 };
const target = { page: 0, column: 1, section: 0 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual(layout);
});
it("returns the same layout if the target location is invalid", () => {
const layout = [[["A"], ["B"]]];
const current = { page: 0, column: 0, section: 0 };
const target = { page: 2, column: 0, section: 0 };
const result = moveItemInLayout(current, target, layout);
expect(result).toEqual(layout);
});
});

View File

@ -0,0 +1,107 @@
import { describe, expect, it } from "vitest";
import { deepSearchAndParseDates, sortByDate } from "../date";
type TestType = { date?: Date };
describe("sortByDate", () => {
it("sorts by date in descending order when desc is true", () => {
const a: TestType = { date: new Date("2023-01-01") };
const b: TestType = { date: new Date("2023-01-02") };
expect(sortByDate(a, b, "date")).toBe(1);
expect(sortByDate(b, a, "date")).toBe(-1);
});
it("sorts by date in ascending order when desc is false", () => {
const a: TestType = { date: new Date("2023-01-01") };
const b: TestType = { date: new Date("2023-01-02") };
expect(sortByDate(a, b, "date", false)).toBe(-1);
expect(sortByDate(b, a, "date", false)).toBe(1);
});
it("returns 0 if one of the dates is missing", () => {
const a: TestType = { date: new Date("2023-01-01") };
const b: TestType = {};
expect(sortByDate(a, b, "date")).toBe(0);
});
it("returns 0 if one of the values is not a date", () => {
const a: TestType = { date: new Date("2023-01-01") };
const b: TestType = { date: "2023-01-02" as unknown as Date };
expect(sortByDate(a, b, "date")).toBe(0);
});
it("handles equal dates", () => {
const a: TestType = { date: new Date("2023-01-01") };
const b: TestType = { date: new Date("2023-01-01") };
expect(sortByDate(a, b, "date")).toBe(0);
expect(sortByDate(a, b, "date", false)).toBe(0);
});
});
describe("deepSearchAndParseDates", () => {
it("parses dates at various nesting levels", () => {
const input = {
level1: {
date: "2021-08-17T00:00:00Z",
nested: {
date: "2022-08-17T00:00:00Z",
},
},
otherKey: "value",
};
const dateKeys = ["date"];
const output = deepSearchAndParseDates(input, dateKeys);
expect(output.level1.date).toBeInstanceOf(Date);
expect(output.level1.nested.date).toBeInstanceOf(Date);
expect(output.otherKey).toBe("value");
});
it("does not parse invalid date strings", () => {
const input = {
date: "not a date",
};
const dateKeys = ["date"];
const output = deepSearchAndParseDates(input, dateKeys);
expect(output.date).toBe("not a date");
});
it("does not modify non-object input", () => {
const input = "2021-08-17T00:00:00Z";
const dateKeys = ["date"];
const output = deepSearchAndParseDates(input, dateKeys);
expect(output).toBe(input);
});
it("returns null for null input", () => {
const input = null;
const dateKeys = ["date"];
const output = deepSearchAndParseDates(input, dateKeys);
expect(output).toBeNull();
});
it("handles arrays with date strings", () => {
const input = ["2021-08-17T00:00:00Z", "2022-08-17"];
const dateKeys = ["0", "1"]; // Assuming the keys are stringified indices
const output = deepSearchAndParseDates(input, dateKeys);
expect(output[0]).toBeInstanceOf(Date);
expect(output[1]).toBeInstanceOf(Date);
});
it("ignores keys that are not in the dateKeys", () => {
const input = {
date: "2021-08-17T00:00:00Z",
notADate: "2021-08-17T00:00:00Z",
};
const dateKeys = ["date"];
const output = deepSearchAndParseDates(input, dateKeys);
expect(output.date).toBeInstanceOf(Date);
expect(output.notADate).toBe("2021-08-17T00:00:00Z");
});
});

View File

@ -0,0 +1,53 @@
import { describe, expect, it } from "vitest";
import { linearTransform } from "../number";
describe("linearTransform", () => {
it("transforms values from one range to another", () => {
const value = 5;
const result = linearTransform(value, 0, 10, 0, 100);
expect(result).toBe(50);
});
it("handles negative ranges", () => {
const value = -5;
const result = linearTransform(value, -10, 0, 0, 100);
expect(result).toBe(50);
});
it("correctly transforms the minimum input value to the minimum output value", () => {
const value = 0;
const result = linearTransform(value, 0, 10, 0, 100);
expect(result).toBe(0);
});
it("correctly transforms the maximum input value to the maximum output value", () => {
const value = 10;
const result = linearTransform(value, 0, 10, 0, 100);
expect(result).toBe(100);
});
it("transforms values outside the input range", () => {
const value = 15;
const result = linearTransform(value, 0, 10, 0, 100);
expect(result).toBe(150);
});
it("handles inverted output ranges", () => {
const value = 5;
const result = linearTransform(value, 0, 10, 100, 0);
expect(result).toBe(50);
});
it("returns NaN if input maximum equals input minimum", () => {
const value = 5;
const result = linearTransform(value, 0, 0, 0, 100);
expect(result).toBe(NaN);
});
it("returns NaN if input range is zero (avoids division by zero)", () => {
const value = 5;
const result = linearTransform(value, 10, 10, 0, 100);
expect(result).toBeNaN();
});
});

View File

@ -0,0 +1,75 @@
import { describe, expect, it } from "vitest";
import { exclude } from "../object";
describe("exclude", () => {
type TestObject = {
id: number;
name: string;
age: number;
email: string;
};
it("excludes specified keys from the object", () => {
const object: TestObject = {
id: 1,
name: "Alice",
age: 30,
email: "alice@example.com",
};
const result = exclude(object, ["age", "email"]);
expect(result).toEqual({ id: 1, name: "Alice" });
expect(result).not.toHaveProperty("age");
expect(result).not.toHaveProperty("email");
});
it("returns the same object if no keys are specified", () => {
const object: TestObject = {
id: 1,
name: "Alice",
age: 30,
email: "alice@example.com",
};
const keysToExclude: Array<keyof TestObject> = [];
const result = exclude(object, keysToExclude);
expect(result).toEqual(object);
});
it("does not modify the original object", () => {
const object: TestObject = {
id: 1,
name: "Alice",
age: 30,
email: "alice@example.com",
};
exclude(object, ["age", "email"]);
expect(object).toHaveProperty("age");
expect(object).toHaveProperty("email");
});
it("handles cases where keys to exclude are not present in the object", () => {
const object: TestObject = {
id: 1,
name: "Alice",
age: 30,
email: "alice@example.com",
};
const keysToExclude = ["nonExistentKey" as keyof TestObject];
const result = exclude(object, keysToExclude);
expect(result).toEqual(object);
});
it("returns the input if it is not an object", () => {
const object: unknown = null;
const keysToExclude = ["id"];
// @ts-expect-error passing invalid input type for tests
const result = exclude(object, keysToExclude);
expect(result).toBeNull();
});
});

View File

@ -0,0 +1,66 @@
import { describe, expect, it } from "vitest";
import {
extractUrl,
generateRandomName,
getInitials,
isEmptyString,
isUrl,
kebabCase,
processUsername,
} from "../string";
describe("getInitials", () => {
it("returns the initials of a name", () => {
expect(getInitials("John Doe")).toBe("JD");
expect(getInitials("Mary Jane Watson")).toBe("MW");
});
});
describe("isUrl", () => {
it("checks if a string is a URL", () => {
expect(isUrl("https://example.com")).toBe(true);
expect(isUrl("not a url")).toBe(false);
});
});
describe("isEmptyString", () => {
it("checks if a string is empty or only contains whitespace", () => {
expect(isEmptyString("")).toBe(true);
expect(isEmptyString(" ")).toBe(true);
expect(isEmptyString("<p></p>")).toBe(true);
expect(isEmptyString("not empty")).toBe(false);
});
});
describe("extractUrl", () => {
it("extracts a URL from a string", () => {
expect(extractUrl("Visit https://example.com today!")).toBe("https://example.com");
expect(extractUrl("No URL here.")).toBeNull();
});
});
describe("kebabCase", () => {
it("converts a string to kebab-case", () => {
expect(kebabCase("fooBar")).toBe("foo-bar");
expect(kebabCase("Foo Bar")).toBe("foo-bar");
expect(kebabCase("foo_bar")).toBe("foo-bar");
expect(kebabCase("")).toBe("");
expect(kebabCase(null)).toBe("");
});
});
describe("generateRandomName", () => {
it("generates a random name", () => {
const name = generateRandomName();
expect(name).toMatch(/^[A-Z][a-z]+ [A-Z][a-z]+ [A-Z][a-z]+$/);
});
});
describe("processUsername", () => {
it("processes a username by removing non-alphanumeric characters", () => {
expect(processUsername("User@Name!")).toBe("username");
expect(processUsername("")).toBe("");
expect(processUsername(null)).toBe("");
});
});

34925
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,7 @@
{
"name": "@reactive-resume/source",
"description": "A free and open-source resume builder that simplifies the process of creating, updating, and sharing your resume.",
"version": "4.0.0-alpha.3",
"version": "4.0.0-alpha.4",
"license": "MIT",
"private": true,
"author": {
@ -16,13 +16,16 @@
"scripts": {
"dev": "nx run-many -t serve",
"test": "nx run-many -t test",
"prebuild": "npx prisma generate",
"prebuild": "pnpm prisma:generate",
"build": "nx run-many -t build",
"prestart": "npx prisma migrate deploy",
"prestart": "pnpm prisma:migrate",
"start": "node dist/apps/server/main",
"lint": "nx run-many -t lint --fix",
"format": "npx prettier -w .",
"messages:extract": "npx lingui extract --clean --overwrite --locale en-US"
"format": "pnpm exec prettier -w .",
"prisma:generate": "pnpm exec prisma generate",
"prisma:migrate": "pnpm exec prisma migrate deploy",
"prisma:migrate:dev": "pnpm exec prisma migrate dev",
"messages:extract": "pnpm exec lingui extract --clean --overwrite --locale en-US"
},
"devDependencies": {
"@babel/core": "^7.23.3",
@ -46,12 +49,12 @@
"@nx/webpack": "17.1.2",
"@nx/workspace": "17.1.2",
"@swc-node/register": "~1.6.8",
"@swc/cli": "~0.1.62",
"@swc/cli": "~0.1.63",
"@swc/core": "~1.3.96",
"@tailwindcss/container-queries": "^0.1.1",
"@tailwindcss/forms": "^0.5.7",
"@tailwindcss/typography": "^0.5.10",
"@tanstack/eslint-plugin-query": "^5.8.3",
"@tanstack/eslint-plugin-query": "^5.8.4",
"@testing-library/react": "14.1.0",
"@types/async-retry": "^1.4.8",
"@types/bcryptjs": "^2.4.6",
@ -63,7 +66,7 @@
"@types/lodash.get": "^4.4.9",
"@types/lodash.set": "^4.3.9",
"@types/multer": "^1.4.10",
"@types/node": "20.9.0",
"@types/node": "20.9.1",
"@types/nodemailer": "^6.4.14",
"@types/papaparse": "^5.3.11",
"@types/passport": "^1.0.15",
@ -77,12 +80,12 @@
"@types/webfontloader": "^1.6.37",
"@typescript-eslint/eslint-plugin": "^6.11.0",
"@typescript-eslint/parser": "^6.11.0",
"@vitejs/plugin-react": "~4.1.1",
"@vitejs/plugin-react-swc": "~3.4.1",
"@vitejs/plugin-react": "~4.2.0",
"@vitejs/plugin-react-swc": "~3.5.0",
"@vitest/coverage-v8": "^0.34.6",
"@vitest/ui": "~0.34.6",
"autoprefixer": "^10.4.16",
"cypress": "^13.5.0",
"cypress": "^13.5.1",
"eslint": "~8.53.0",
"eslint-config-prettier": "^9.0.0",
"eslint-plugin-cypress": "^2.15.1",
@ -103,19 +106,19 @@
"postcss-import": "^15.1.0",
"postcss-nested": "^6.0.1",
"prettier": "^3.1.0",
"prisma": "^5.6.0",
"tailwindcss": "^3.3.5",
"tailwindcss-animate": "^1.0.7",
"ts-jest": "^29.1.1",
"ts-node": "10.9.1",
"typescript": "~5.2.2",
"vite": "~4.5.0",
"vite": "~5.0.0",
"vite-plugin-dts": "~3.6.3",
"vitest": "~0.34.6"
},
"dependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/modifiers": "^7.0.0",
"prisma": "^5.6.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource/ibm-plex-sans": "^5.0.17",
@ -133,7 +136,7 @@
"@nestjs/passport": "^10.0.2",
"@nestjs/platform-express": "^10.2.8",
"@nestjs/serve-static": "^4.0.0",
"@nestjs/swagger": "^7.1.15",
"@nestjs/swagger": "^7.1.16",
"@nestjs/terminus": "^10.1.1",
"@paralleldrive/cuid2": "^2.2.2",
"@pdf-lib/fontkit": "^1.1.1",
@ -166,7 +169,7 @@
"@songkeys/nestjs-redis": "^10.0.0",
"@songkeys/nestjs-redis-health": "^10.0.0",
"@swc/helpers": "~0.5.3",
"@tanstack/react-query": "^5.8.3",
"@tanstack/react-query": "^5.8.4",
"@tiptap/extension-highlight": "^2.1.12",
"@tiptap/extension-image": "^2.1.12",
"@tiptap/extension-link": "^2.1.12",
@ -176,7 +179,7 @@
"@tiptap/starter-kit": "^2.1.12",
"@types/passport-jwt": "^3.0.13",
"async-retry": "^1.3.3",
"axios": "^1.6.1",
"axios": "^1.6.2",
"axios-auth-refresh": "^3.3.6",
"bcryptjs": "^2.4.3",
"class-variance-authority": "^0.7.0",
@ -200,7 +203,7 @@
"nestjs-prisma": "^0.22.0",
"nestjs-zod": "^3.0.0",
"nodemailer": "^6.9.7",
"openai": "^4.17.5",
"openai": "^4.19.0",
"otplib": "^12.0.1",
"papaparse": "^5.4.1",
"passport": "^0.6.0",
@ -218,7 +221,7 @@
"react-hook-form": "^7.48.2",
"react-parallax-tilt": "^1.7.174",
"react-resizable-panels": "^0.0.54",
"react-router-dom": "6.18.0",
"react-router-dom": "6.19.0",
"react-zoom-pan-pinch": "^3.3.0",
"reflect-metadata": "^0.1.13",
"rxjs": "^7.8.1",

19750
pnpm-lock.yaml generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -46,9 +46,11 @@ services:
ports:
- ${CHROME_PORT:-8080}:3000
environment:
TOKEN: ${CHROME_TOKEN:-chrome_token}
KEEP_ALIVE: true
CONNECTION_TIMEOUT: 10000
EXIT_ON_HEALTH_FAILURE: true
PRE_REQUEST_HEALTH_CHECK: true
TOKEN: ${CHROME_TOKEN:-chrome_token}
# Redis (for cache & server session management)
redis:

View File

@ -0,0 +1,134 @@
version: "3"
# In this Docker Compose example, we use Nginx Proxy Manager to manage the reverse proxy and SSL certificates.
# There's very little configuration to be made on the compose file itself, and most of it is done on the Nginx Proxy Manager UI.
services:
# Database (Postgres)
postgres:
image: postgres:alpine
restart: unless-stopped
volumes:
- postgres_data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
POSTGRES_DB: postgres
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres -d postgres"]
interval: 10s
timeout: 5s
retries: 5
# Storage (for image uploads)
minio:
image: minio/minio
restart: unless-stopped
command: server /data
volumes:
- minio_data:/data
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
labels:
- traefik.enable=true
- traefik.http.routers.storage.rule=Host(`storage.example.com`)
- traefik.http.routers.storage.entrypoints=websecure
- traefik.http.routers.storage.tls.certresolver=letsencrypt
- traefik.http.services.storage.loadbalancer.server.port=9000
# Chrome Browser (for printing and previews)
chrome:
image: browserless/chrome:1.61.0-puppeteer-21.4.1
restart: unless-stopped
environment:
TOKEN: chrome_token
EXIT_ON_HEALTH_FAILURE: true
PRE_REQUEST_HEALTH_CHECK: true
# Redis (for cache & server session management)
redis:
image: redis:alpine
restart: unless-stopped
command: redis-server --save 60 1 --loglevel warning --requirepass password
volumes:
- redis_data:/data
app:
image: amruthpillai/reactive-resume:v4.0.0-alpha.0
restart: unless-stopped
depends_on:
- postgres
- minio
- redis
- chrome
environment:
# -- Environment Variables --
PORT: 3000
NODE_ENV: production
# -- URLs --
PUBLIC_URL: https://example.com
STORAGE_URL: https://storage.example.com
# -- Printer (Chrome) --
CHROME_TOKEN: chrome_token
CHROME_URL: wss://printer.example.com
# -- Database (Postgres) --
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/postgres
# -- Auth --
ACCESS_TOKEN_SECRET: access_token_secret
REFRESH_TOKEN_SECRET: refresh_token_secret
# -- Emails --
# SMTP_URL: smtp://user:pass@smtp:587 # Optional
# -- Storage (Minio) --
STORAGE_ENDPOINT: minio
STORAGE_PORT: 9000
STORAGE_REGION: us-east-1 # Optional
STORAGE_BUCKET: default
STORAGE_ACCESS_KEY: minioadmin
STORAGE_SECRET_KEY: minioadmin
# -- Cache (Redis) --
REDIS_URL: redis://default:password@redis:6379
# -- Sentry --
# VITE_SENTRY_DSN: https://id.sentry.io # Optional
# -- Crowdin (Optional) --
# CROWDIN_PROJECT_ID:
# CROWDIN_ACCESS_TOKEN:
# -- GitHub --
GITHUB_CLIENT_ID: github_client_id
GITHUB_CLIENT_SECRET: github_client_secret
GITHUB_CALLBACK_URL: https://example.com/api/auth/github/callback
# -- Google --
GOOGLE_CLIENT_ID: google_client_id
GOOGLE_CLIENT_SECRET: google_client_secret
GOOGLE_CALLBACK_URL: https://example.com/api/auth/google/callback
nginx:
image: jc21/nginx-proxy-manager
restart: always
ports:
- 80:80
- 443:443
- 81:81 # Port 81 is used for Proxy Manager's Web UI
volumes:
- nginx_data:/data
- letsencrypt_data:/etc/letsencrypt
environment:
DISABLE_IPV6: true
volumes:
minio_data:
redis_data:
nginx_data:
postgres_data:
letsencrypt_data:

View File

@ -94,7 +94,7 @@ services:
REDIS_URL: redis://default:password@redis:6379
# -- Sentry --
# SENTRY_DSN: https://id.sentry.io # Optional
# VITE_SENTRY_DSN: https://id.sentry.io # Optional
# -- Crowdin (Optional) --
# CROWDIN_PROJECT_ID:

View File

@ -105,7 +105,7 @@ services:
REDIS_URL: redis://default:password@redis:6379
# -- Sentry --
# SENTRY_DSN: https://id.sentry.io # Optional
# VITE_SENTRY_DSN: https://id.sentry.io # Optional
# -- Crowdin (Optional) --
# CROWDIN_PROJECT_ID:

View File

@ -99,7 +99,7 @@ services:
REDIS_URL: redis://default:password@redis:6379
# -- Sentry --
# SENTRY_DSN: https://id.sentry.io # Optional
# VITE_SENTRY_DSN: https://id.sentry.io # Optional
# -- Crowdin (Optional) --
# CROWDIN_PROJECT_ID: