Compare commits

...

9 Commits
v4.5.4 ... main

24 changed files with 155 additions and 92 deletions

View File

@ -5,7 +5,6 @@
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"extends": ["plugin:prettier/recommended"],
"plugins": ["simple-import-sort", "unused-imports"],
"rules": {
// eslint
@ -42,14 +41,6 @@
}
]
}
],
// prettier
"prettier/prettier": [
"warn",
{
"endOfLine": "auto"
}
]
}
},

View File

@ -4,13 +4,6 @@
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"extends": ["plugin:tailwindcss/recommended"],
"settings": {
"tailwindcss": {
"callees": ["cn", "clsx", "cva"],
"config": "tailwind.config.js"
}
},
"rules": {
// eslint
"@typescript-eslint/no-require-imports": "off",
@ -28,10 +21,7 @@
],
// react-hooks
"react-hooks/exhaustive-deps": "off",
// tailwindcss
"tailwindcss/no-custom-classname": "off"
"react-hooks/exhaustive-deps": "off"
}
},
{

View File

@ -1,3 +1,4 @@
import { isLocalFont } from "@reactive-resume/utils";
import { useEffect, useMemo } from "react";
import { Helmet } from "react-helmet-async";
import { Outlet } from "react-router";
@ -18,6 +19,18 @@ export const ArtboardPage = () => {
}, [metadata.typography.font]);
useEffect(() => {
const family = metadata.typography.font.family;
if (isLocalFont(family)) {
let frame = 0;
frame = requestAnimationFrame(() => {
const width = window.document.body.offsetWidth;
const height = window.document.body.offsetHeight;
const message = { type: "PAGE_LOADED", payload: { width, height } };
window.postMessage(message, "*");
});
return () => { cancelAnimationFrame(frame); };
}
webfontloader.load({
google: { families: [fontString] },
active: () => {

View File

@ -131,13 +131,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
<div className="flex items-center gap-x-1.5 break-all">
{!iconOnRight && (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)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -553,7 +553,7 @@ export const Azurill = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-3">
<div className="space-y-3 p-custom">
{isFirstPage && <Header />}
<div className="grid grid-cols-3 gap-x-4">

View File

@ -122,13 +122,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
<div className="flex items-center gap-x-1.5 break-all">
{!iconOnRight && (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)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -568,7 +568,7 @@ export const Bronzor = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
<div className="space-y-4 p-custom">
{isFirstPage && <Header />}
<div className="space-y-4">

View File

@ -125,14 +125,14 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
<div className="flex items-center gap-x-1.5 break-all">
{!iconOnRight &&
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-white" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -571,7 +571,7 @@ export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
<div className="grid min-h-[inherit] grid-cols-3">
<div
className={cn(
"main p-custom group space-y-4",
"main group space-y-4 p-custom",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>
@ -584,7 +584,7 @@ export const Chikorita = ({ columns, isFirstPage = false }: TemplateProps) => {
<div
className={cn(
"sidebar p-custom group h-full space-y-4 bg-primary text-background",
"sidebar group h-full space-y-4 bg-primary p-custom text-background",
sidebar.length === 0 && "hidden",
)}
>

View File

@ -28,7 +28,7 @@ const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
return (
<div className="p-custom relative grid grid-cols-3 space-x-4 pb-0">
<div className="relative grid grid-cols-3 space-x-4 p-custom pb-0">
<Picture className="mx-auto" />
<div className="relative z-10 col-span-2 text-background">
@ -142,13 +142,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
<div className="flex items-center gap-x-1.5 break-all">
{!iconOnRight && (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)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -604,7 +604,7 @@ export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
)}
<div className="grid grid-cols-3">
<div className="sidebar p-custom group space-y-4">
<div className="sidebar group space-y-4 p-custom">
{sidebar.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}
@ -612,7 +612,7 @@ export const Ditto = ({ columns, isFirstPage = false }: TemplateProps) => {
<div
className={cn(
"main p-custom group space-y-4",
"main group space-y-4 p-custom",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>

View File

@ -28,7 +28,7 @@ const Header = () => {
const basics = useArtboardStore((state) => state.resume.basics);
return (
<div className="p-custom space-y-4 bg-primary text-background">
<div className="space-y-4 bg-primary p-custom text-background">
<Picture className="border-background" />
<div>
@ -86,7 +86,7 @@ const Summary = () => {
if (!section.visible || isEmptyString(section.content)) return null;
return (
<div className="p-custom space-y-4" style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}>
<div className="space-y-4 p-custom" style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}>
<section id={section.id}>
<div
dangerouslySetInnerHTML={{ __html: sanitize(section.content) }}
@ -123,7 +123,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
<div className="flex items-center gap-x-1.5 break-all">
{!iconOnRight &&
(icon ?? (
<i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-background" />
@ -132,7 +132,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -531,7 +531,7 @@ const mapSectionToComponent = (section: SectionKey) => {
case "education": {
return <Education />;
}
case "summary": {
return <Summary />;
}
@ -587,7 +587,7 @@ export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
{isFirstPage && <Header />}
<div
className="p-custom flex-1 space-y-4"
className="flex-1 space-y-4 p-custom"
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
>
{sidebar.map((section) => (
@ -597,7 +597,7 @@ export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
</div>
<div className={cn("main group", sidebar.length > 0 ? "col-span-2" : "col-span-3")}>
<div className="p-custom space-y-4">
<div className="space-y-4 p-custom">
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
))}

View File

@ -135,14 +135,14 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
<div className="flex items-center gap-x-1.5 break-all">
{!iconOnRight &&
(icon ?? <i className="ph ph-bold ph-link text-primary group-[.sidebar]:text-primary" />)}
<a
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -587,7 +587,7 @@ export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
return (
<div className="grid min-h-[inherit] grid-cols-3">
<div
className={cn("sidebar p-custom group space-y-4", sidebar.length === 0 && "hidden")}
className={cn("sidebar group space-y-4 p-custom", sidebar.length === 0 && "hidden")}
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
>
{isFirstPage && <Header />}
@ -599,7 +599,7 @@ export const Glalie = ({ columns, isFirstPage = false }: TemplateProps) => {
<div
className={cn(
"main p-custom group space-y-4",
"main group space-y-4 p-custom",
sidebar.length > 0 ? "col-span-2" : "col-span-3",
)}
>

View File

@ -141,13 +141,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
<div className="flex items-center gap-x-1.5 break-all">
{!iconOnRight && (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)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -524,7 +524,7 @@ export const Kakuna = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
<div className="space-y-4 p-custom">
{isFirstPage && <Header />}
<div className="space-y-4">

View File

@ -32,7 +32,7 @@ const Header = () => {
return (
<div>
<div
className="p-custom flex items-center space-x-8"
className="flex items-center space-x-8 p-custom"
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
>
<div className="space-y-3">
@ -51,7 +51,7 @@ const Header = () => {
<Picture />
</div>
<div className="p-custom space-y-3" style={{ backgroundColor: hexToRgb(primaryColor, 0.4) }}>
<div className="space-y-3 p-custom" style={{ backgroundColor: hexToRgb(primaryColor, 0.4) }}>
<div className="flex flex-wrap items-center gap-x-3 gap-y-0.5 text-sm">
{basics.location && (
<div className="flex items-center gap-x-1.5">
@ -136,13 +136,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
<div className="flex items-center gap-x-1.5 break-all">
{!iconOnRight && (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)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -519,7 +519,7 @@ export const Leafish = ({ columns, isFirstPage = false }: TemplateProps) => {
<div>
{isFirstPage && <Header />}
<div className="p-custom grid grid-cols-2 items-start space-x-6">
<div className="grid grid-cols-2 items-start space-x-6 p-custom">
<div className={cn("grid gap-y-4", sidebar.length === 0 && "col-span-2")}>
{main.map((section) => (
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>

View File

@ -126,13 +126,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
<div className="flex items-center gap-x-1.5 break-all">
{!iconOnRight && (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)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -575,7 +575,7 @@ export const Nosepass = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-6">
<div className="space-y-6 p-custom">
<div className="flex items-center justify-between">
<img alt="Europass Logo" className="h-[42px]" src="/assets/europass.png" />

View File

@ -142,13 +142,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
<div className="flex items-center gap-x-1.5 break-all">
{!iconOnRight && (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)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -564,7 +564,7 @@ export const Onyx = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
<div className="space-y-4 p-custom">
{isFirstPage && <Header />}
{main.map((section) => (

View File

@ -126,15 +126,11 @@ const Rating = ({ level }: RatingProps) => (
<i
key={index}
className={cn(
"ph ph-diamond text-primary",
"ph ph-bold ph-diamond text-primary",
level > index && "ph-fill",
level <= index && "ph-bold",
)}
/>
// <div
// key={index}
// className={cn("h-2 w-4 border border-primary", level > index && "bg-primary")}
// />
))}
</div>
);
@ -151,7 +147,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5">
<div className="flex items-center gap-x-1.5 break-all">
{!iconOnRight &&
(icon ?? (
<i className="ph ph-bold ph-link text-primary group-[.summary]:text-background" />
@ -160,7 +156,7 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
href={url.href}
target="_blank"
rel="noreferrer noopener nofollow"
className={cn("inline-block", className)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -601,7 +597,7 @@ export const Pikachu = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom grid grid-cols-3 space-x-6">
<div className="grid grid-cols-3 space-x-6 p-custom">
<div className="sidebar group space-y-4">
{isFirstPage && <Picture className="w-full !max-w-none" />}

View File

@ -123,13 +123,13 @@ const Link = ({ url, icon, iconOnRight, label, className }: LinkProps) => {
if (!isUrl(url.href)) return null;
return (
<div className="flex items-center gap-x-1.5 border-r pr-2 last:border-r-0 last:pr-0">
<div className="flex items-center gap-x-1.5 break-all border-r pr-2 last:border-r-0 last:pr-0">
{!iconOnRight && (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)}
className={cn("line-clamp-1 max-w-fit", className)}
>
{label ?? (url.label || url.href)}
</a>
@ -567,7 +567,7 @@ export const Rhyhorn = ({ columns, isFirstPage = false }: TemplateProps) => {
const [main, sidebar] = columns;
return (
<div className="p-custom space-y-4">
<div className="space-y-4 p-custom">
{isFirstPage && <Header />}
{main.map((section) => (

View File

@ -4,20 +4,10 @@
"overrides": [
{
"files": ["*.ts", "*.tsx"],
"extends": [
"plugin:tailwindcss/recommended",
"plugin:@tanstack/eslint-plugin-query/recommended"
],
"extends": ["plugin:@tanstack/eslint-plugin-query/recommended"],
"parserOptions": {
"projectService": "./apps/client/tsconfig.json"
},
"settings": {
"tailwindcss": {
"callees": ["cn", "clsx", "cva"],
"config": "tailwind.config.js",
"whitelist": ["ph", "ph\\-.*", "si", "si\\-.*"]
}
},
"plugins": ["lingui"],
"rules": {
// eslint

View File

@ -23,7 +23,6 @@ export const AuthLayout = () => {
const hideDivider = !providers.includes("email") || providers.length === 1;
return (
// eslint-disable-next-line tailwindcss/enforces-shorthand -- size-screen not implemented yet
<div className="flex h-screen w-screen">
<div className="relative flex w-full flex-col justify-center gap-y-8 px-12 sm:mx-auto sm:basis-[420px] sm:px-0 lg:basis-[480px] lg:px-12">
<div className="flex items-center justify-between">

View File

@ -53,7 +53,11 @@ export const CustomField = ({ field, onChange, onRemove }: CustomFieldProps) =>
<Tooltip content={t`Icon`}>
<PopoverTrigger asChild>
<Button size="icon" variant="ghost" className="shrink-0">
{field.icon ? <i className={cn(`ph ph-${field.icon}`)} /> : <EnvelopeIcon />}
{field.icon ? (
<i className={cn(`ph ph-bold ph-${field.icon}`)} />
) : (
<EnvelopeIcon />
)}
</Button>
</PopoverTrigger>
</Tooltip>

View File

@ -21,6 +21,13 @@ export default defineConfig({
host: true,
port: 5173,
fs: { allow: [searchForWorkspaceRoot(process.cwd())] },
proxy: {
"/artboard": {
target: "http://localhost:6173",
changeOrigin: true,
},
},
},
optimizeDeps: {

40
azure-pipelines.yml Normal file
View File

@ -0,0 +1,40 @@
# Docker
# Build and push an image to Azure Container Registry
# https://docs.microsoft.com/azure/devops/pipelines/languages/docker
trigger:
- main
resources:
- repo: self
variables:
# Container registry service connection established during pipeline creation
dockerRegistryServiceConnection: 'c6da80c0-7d65-4b93-9203-3b91c4f6b5dc'
imageRepository: 'reactiveresume'
containerRegistry: 'reactiveresume.azurecr.io'
dockerfilePath: '$(Build.SourcesDirectory)/Dockerfile'
tag: '$(Build.BuildId)'
# Agent VM image name
vmImageName: 'ubuntu-latest'
stages:
- stage: Build
displayName: Build and push stage
jobs:
- job: Build
displayName: Build
pool:
vmImage: $(vmImageName)
steps:
- task: Docker@2
displayName: Build and push an image to container registry
inputs:
command: buildAndPush
repository: $(imageRepository)
dockerfile: $(dockerfilePath)
containerRegistry: $(dockerRegistryServiceConnection)
arguments: '--platform linux/amd64,linux/arm64'
tags: |
$(tag)

View File

@ -4,13 +4,6 @@
"overrides": [
{
"files": ["*.ts", "*.tsx", "*.js", "*.jsx"],
"extends": ["plugin:tailwindcss/recommended"],
"settings": {
"tailwindcss": {
"callees": ["cn", "clsx", "cva"],
"config": "tailwind.config.js"
}
},
"rules": {
// eslint
"@typescript-eslint/no-require-imports": "off",

View File

@ -6,6 +6,21 @@ export type Font = {
files: Record<string, string>;
};
/**
* Known system fonts we consider available locally without fetching from Google Fonts.
* Extend this list when adding more system-safe families to the app.
*/
export const localFonts = ["Arial", "Cambria", "Garamond", "Times New Roman"];
/**
* Checks whether a font family is a local/system font.
*
* Input: font family name (case-insensitive)
* Output: true if present in localFonts, otherwise false
*/
export const isLocalFont = (family: string): boolean =>
localFonts.some((f) => f.toLowerCase() === family.toLowerCase());
export const fonts: Font[] = [
{
family: "Roboto",

View File

@ -0,0 +1,25 @@
import { describe, expect, it } from "vitest";
import { isLocalFont, localFonts } from "../fonts";
describe("isLocalFont", () => {
it("returns true for known local fonts (case-insensitive)", () => {
expect(isLocalFont("Arial")).toBe(true);
expect(isLocalFont("arial")).toBe(true);
expect(isLocalFont("Times New Roman")).toBe(true);
expect(isLocalFont("times new roman")).toBe(true);
});
it("returns false for non-local fonts", () => {
expect(isLocalFont("Roboto")).toBe(false);
expect(isLocalFont("Open Sans")).toBe(false);
});
});
describe("localFonts", () => {
it("includes the expected base set", () => {
for (const f of ["Arial", "Cambria", "Garamond", "Times New Roman"]) {
expect(localFonts).toContain(f);
}
});
});

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.5.4",
"version": "4.5.5",
"license": "MIT",
"private": true,
"packageManager": "pnpm@10.20.0",