mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2026-10-01 01:04:29 +10:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
91c8624d80 | ||
|
|
f6dc3bf8df | ||
|
|
a9b8d1d06f | ||
|
|
76b61d0135 | ||
|
|
d7409832ab | ||
|
|
1749f95086 | ||
|
|
2e11e64881 | ||
|
|
db86f3275b | ||
|
|
62c09ea142 | ||
|
|
589bd2c162 | ||
|
|
4c7ed8ea0d | ||
|
|
25021d1b20 | ||
|
|
53fdfdf8db | ||
|
|
8a45f2de4d | ||
|
|
4ccc7bae40 | ||
|
|
2585c47de8 | ||
|
|
b49798950a | ||
|
|
03f15f91b3 | ||
|
|
8e13d9c3ac | ||
|
|
80932eb80c |
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
|
||||
@@ -82,4 +82,14 @@ git commit -m "fix(homepage): fix typo on homepage in the faq section"
|
||||
|
||||
It helps to be as descriptive as possible in commit messages so that users can be aware of the changes made by you.
|
||||
|
||||
Then, you can run the basic checks, as to make sure the CI has the most chance of being successful:
|
||||
|
||||
```
|
||||
pnpm run lint
|
||||
|
||||
pnpm run test
|
||||
|
||||
pnpm run build
|
||||
```
|
||||
|
||||
Finally, create a pull request to merge the changes on your forked repository to the original repository hosted on AmruthPillai/Reactive-Resume. I can take a look at the changes you've made when I have the time and have it merged onto the app.
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
@@ -187,7 +187,7 @@ const Section = <T,>({
|
||||
if (!section.visible || section.items.filter((item) => item.visible).length === 0) return null;
|
||||
|
||||
return (
|
||||
<section id={section.id} className="grid">
|
||||
<section id={section.id} className="my-custom grid px-custom">
|
||||
<h4 className="mb-2 border-b border-primary text-base font-bold">{section.name}</h4>
|
||||
|
||||
<div
|
||||
@@ -531,7 +531,7 @@ const mapSectionToComponent = (section: SectionKey) => {
|
||||
case "education": {
|
||||
return <Education />;
|
||||
}
|
||||
|
||||
|
||||
case "summary": {
|
||||
return <Summary />;
|
||||
}
|
||||
@@ -586,10 +586,7 @@ export const Gengar = ({ columns, isFirstPage = false }: TemplateProps) => {
|
||||
>
|
||||
{isFirstPage && <Header />}
|
||||
|
||||
<div
|
||||
className="p-custom flex-1 space-y-4"
|
||||
style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}
|
||||
>
|
||||
<div className="flex-1" style={{ backgroundColor: hexToRgb(primaryColor, 0.2) }}>
|
||||
{sidebar.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
@@ -597,11 +594,9 @@ 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">
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
{main.map((section) => (
|
||||
<Fragment key={section}>{mapSectionToComponent(section)}</Fragment>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" />
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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" />}
|
||||
|
||||
|
||||
@@ -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) => (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { Button, Input, Label, Switch, Tooltip } from "@reactive-resume/ui";
|
||||
import { AnimatePresence, motion } from "framer-motion";
|
||||
|
||||
import { useToast } from "@/client/hooks/use-toast";
|
||||
import { axios } from "@/client/libs/axios"
|
||||
import { useUser } from "@/client/services/user";
|
||||
import { useResumeStore } from "@/client/stores/resume";
|
||||
|
||||
@@ -20,6 +21,7 @@ export const SharingSection = () => {
|
||||
|
||||
// Constants
|
||||
const url = `${window.location.origin}/${username}/${slug}`;
|
||||
const rawJsonUrl = `${window.location.origin}${axios.defaults.baseURL}/resume/public/${username}/${slug}/stealthily`;
|
||||
|
||||
const onCopy = async () => {
|
||||
await navigator.clipboard.writeText(url);
|
||||
@@ -31,6 +33,16 @@ export const SharingSection = () => {
|
||||
});
|
||||
};
|
||||
|
||||
const onRawJsonCopy = async () => {
|
||||
await navigator.clipboard.writeText(rawJsonUrl);
|
||||
|
||||
toast({
|
||||
variant: "success",
|
||||
title: t`A link has been copied to your clipboard.`,
|
||||
description: t`Anyone with this link can view and download the json data of the resume. This link will not count towards the resume view count.`,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="sharing" className="grid gap-y-6">
|
||||
<header className="flex items-center justify-between">
|
||||
@@ -65,21 +77,50 @@ export const SharingSection = () => {
|
||||
{isPublic && (
|
||||
<motion.div
|
||||
layout
|
||||
className="space-y-1.5"
|
||||
className="space-y-3"
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
>
|
||||
<Label htmlFor="resume-url">{t`URL`}</Label>
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="resume-url">{t`URL`}</Label>
|
||||
|
||||
<div className="flex gap-x-1.5">
|
||||
<Input readOnly id="resume-url" value={url} className="flex-1" />
|
||||
<div className="flex gap-x-1.5">
|
||||
<Input
|
||||
readOnly
|
||||
id="resume-url"
|
||||
value={url}
|
||||
className="flex-1 caret-transparent"
|
||||
/>
|
||||
|
||||
<Tooltip content={t`Copy to Clipboard`}>
|
||||
<Button size="icon" variant="ghost" onClick={onCopy}>
|
||||
<CopySimpleIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<Tooltip content={t`Copy to Clipboard`}>
|
||||
<Button size="icon" variant="ghost" onClick={onCopy}>
|
||||
<CopySimpleIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-1.5">
|
||||
<Label htmlFor="resume-raw-json-url" className="space-y-1">
|
||||
<p>{t`Raw JSON URL`}</p>
|
||||
<p className="text-xs opacity-60">{t`For advanced users! Does not count towards resume statistics.`}</p>
|
||||
</Label>
|
||||
|
||||
<div className="flex gap-x-1.5">
|
||||
<Input
|
||||
readOnly
|
||||
id="resume-raw-json-url"
|
||||
value={rawJsonUrl}
|
||||
className="flex-1 caret-transparent"
|
||||
/>
|
||||
|
||||
<Tooltip content={t`Copy to Clipboard`}>
|
||||
<Button size="icon" variant="ghost" onClick={onRawJsonCopy}>
|
||||
<CopySimpleIcon />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
)}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
ResumeDto,
|
||||
UpdateResumeDto,
|
||||
} from "@reactive-resume/dto";
|
||||
import { ResumeData, resumeDataSchema } from "@reactive-resume/schema";
|
||||
import { resumeDataSchema } from "@reactive-resume/schema";
|
||||
import { ErrorMessage } from "@reactive-resume/utils";
|
||||
import set from "lodash.set";
|
||||
import { zodToJsonSchema } from "zod-to-json-schema";
|
||||
@@ -92,6 +92,19 @@ export class ResumeController {
|
||||
return this.resumeService.findOneStatistics(id);
|
||||
}
|
||||
|
||||
@Get("/public/:username/:slug/stealthily")
|
||||
@UseGuards(OptionalGuard)
|
||||
async findOneByUsernameSlugStealthily(
|
||||
@Param("username") username: string,
|
||||
@Param("slug") slug: string,
|
||||
) {
|
||||
const resume = await this.resumeService.findOneByUsernameSlug(username, slug);
|
||||
|
||||
redactPrivateNotes(resume as ResumeDto);
|
||||
|
||||
return resume;
|
||||
}
|
||||
|
||||
@Get("/public/:username/:slug")
|
||||
@UseGuards(OptionalGuard)
|
||||
async findOneByUsernameSlug(
|
||||
@@ -99,10 +112,13 @@ export class ResumeController {
|
||||
@Param("slug") slug: string,
|
||||
@User("id") userId: string,
|
||||
) {
|
||||
const resume = await this.resumeService.findOneByUsernameSlug(username, slug, userId);
|
||||
const resume = await this.resumeService.findOneByUsernameSlug(username, slug);
|
||||
|
||||
// Hide private notes from public resume API responses
|
||||
set(resume.data as ResumeData, "metadata.notes", undefined);
|
||||
redactPrivateNotes(resume as ResumeDto);
|
||||
|
||||
if (!userId) {
|
||||
await this.resumeService.incrementViewCountForOne(resume.id);
|
||||
}
|
||||
|
||||
return resume;
|
||||
}
|
||||
@@ -155,3 +171,7 @@ export class ResumeController {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function redactPrivateNotes(resume: ResumeDto) {
|
||||
set(resume.data, "metadata.notes", undefined);
|
||||
}
|
||||
|
||||
@@ -84,21 +84,19 @@ export class ResumeService {
|
||||
};
|
||||
}
|
||||
|
||||
async findOneByUsernameSlug(username: string, slug: string, userId?: string) {
|
||||
const resume = await this.prisma.resume.findFirstOrThrow({
|
||||
async findOneByUsernameSlug(username: string, slug: string) {
|
||||
return await this.prisma.resume.findFirstOrThrow({
|
||||
where: { user: { username }, slug, visibility: "public" },
|
||||
});
|
||||
}
|
||||
|
||||
async incrementViewCountForOne(resumeId: string) {
|
||||
// Update statistics: increment the number of views by 1
|
||||
if (!userId) {
|
||||
await this.prisma.statistics.upsert({
|
||||
where: { resumeId: resume.id },
|
||||
create: { views: 1, downloads: 0, resumeId: resume.id },
|
||||
update: { views: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
return resume;
|
||||
await this.prisma.statistics.upsert({
|
||||
where: { resumeId },
|
||||
create: { views: 1, downloads: 0, resumeId },
|
||||
update: { views: { increment: 1 } },
|
||||
});
|
||||
}
|
||||
|
||||
async update(userId: string, id: string, updateResumeDto: UpdateResumeDto) {
|
||||
|
||||
@@ -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)
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
});
|
||||
+1
-1
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user