Compare commits

..

6 Commits

70 changed files with 625 additions and 155 deletions

View File

@ -153,7 +153,7 @@ jobs:
password: ${{ secrets.DOCKER_TOKEN }}
- name: Deploy the latest image on rxresu.me
run: curl -Xk POST ${{ secrets.SERVICE_WEBHOOK }}
run: curl -kX POST ${{ secrets.SERVICE_WEBHOOK }}
- name: Inform about the release on Discord
uses: sarisia/actions-status-discord@v1.14.3

View File

@ -40,12 +40,5 @@
<!-- Phosphor Icons -->
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<!-- Simple Icons -->
<link
type="text/css"
rel="stylesheet"
href="https://unpkg.com/simple-icons-font@v14/font/simple-icons.min.css"
/>
</body>
</html>

View File

@ -1,5 +1,3 @@
import { cn } from "@reactive-resume/utils";
type BrandIconProps = {
slug: string;
};
@ -8,12 +6,12 @@ export const BrandIcon = ({ slug }: BrandIconProps) => {
if (slug === "linkedin") {
return (
<img
alt="LinkedIn"
alt="linkedin"
className="size-4"
src={`${window.location.origin}/support-logos/linkedin.svg`}
/>
);
}
return <i className={cn("si si--color text-[1rem]", `si-${slug}`)} />;
return <img alt={slug} className="size-4" src={`https://cdn.simpleicons.org/${slug}`} />;
};

View File

@ -1,6 +1,6 @@
import { StrictMode } from "react";
import * as ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { RouterProvider } from "react-router";
import { router } from "./router";

View File

@ -1,5 +1,5 @@
import { useEffect, useMemo } from "react";
import { Outlet } from "react-router-dom";
import { Outlet } from "react-router";
import webfontloader from "webfontloader";
import { useArtboardStore } from "../store/artboard";
@ -55,5 +55,11 @@ export const ArtboardPage = () => {
}
}, [metadata]);
return <Outlet />;
return (
<>
{metadata.css.visible && <style lang="css">{`[data-page] { ${metadata.css.value} }`}</style>}
<Outlet />
</>
);
};

View File

@ -1,5 +1,5 @@
import { useEffect } from "react";
import { Outlet } from "react-router-dom";
import { Outlet } from "react-router";
import { useArtboardStore } from "../store/artboard";

View File

@ -1,4 +1,4 @@
import { createBrowserRouter, createRoutesFromChildren, Route } from "react-router-dom";
import { createBrowserRouter, createRoutesFromChildren, Route } from "react-router";
import { ArtboardPage } from "../pages/artboard";
import { BuilderLayout } from "../pages/builder";
@ -6,7 +6,7 @@ import { PreviewLayout } from "../pages/preview";
import { Providers } from "../providers";
export const routes = createRoutesFromChildren(
<Route element={<Providers />}>
<Route element={<Providers />} hydrateFallbackElement={<div>Loading...</div>}>
<Route path="artboard" element={<ArtboardPage />}>
<Route path="builder" element={<BuilderLayout />} />
<Route path="preview" element={<PreviewLayout />} />

View File

@ -43,12 +43,5 @@
<!-- Phosphor Icons -->
<script src="https://unpkg.com/@phosphor-icons/web"></script>
<!-- Simple Icons -->
<link
type="text/css"
rel="stylesheet"
href="https://unpkg.com/simple-icons-font@v14/font/simple-icons.min.css"
/>
</body>
</html>

View File

@ -0,0 +1,155 @@
code[class*="language-"],
pre[class*="language-"] {
color: #f8f8f2;
background: none;
font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
word-wrap: normal;
line-height: 1.5;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border-radius: 0.3em;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #2b2b2b;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: 0.1em;
border-radius: 0.3em;
white-space: normal;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #d4d0ab;
}
.token.punctuation {
color: #fefefe;
}
.token.property,
.token.tag,
.token.constant,
.token.symbol,
.token.deleted {
color: #ffa07a;
}
.token.boolean,
.token.number {
color: #00e0e0;
}
.token.selector,
.token.attr-name,
.token.string,
.token.char,
.token.builtin,
.token.inserted {
color: #abe338;
}
.token.operator,
.token.entity,
.token.url,
.language-css .token.string,
.style .token.string,
.token.variable {
color: #00e0e0;
}
.token.atrule,
.token.attr-value,
.token.function {
color: #ffd700;
}
.token.keyword {
color: #00e0e0;
}
.token.regex,
.token.important {
color: #ffd700;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
@media screen and (forced-colors: active) {
code[class*="language-"],
pre[class*="language-"] {
color: windowText;
background: window;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: window;
}
.token.important {
background: highlight;
color: window;
font-weight: normal;
}
.token.atrule,
.token.attr-value,
.token.function,
.token.keyword,
.token.operator,
.token.selector {
font-weight: bold;
}
.token.attr-value,
.token.comment,
.token.doctype,
.token.function,
.token.keyword,
.token.operator,
.token.property,
.token.string {
color: highlight;
}
.token.attr-value,
.token.url {
font-weight: normal;
}
}

View File

@ -0,0 +1,167 @@
code[class*="language-"],
pre[class*="language-"] {
color: #393a34;
font-family: "Consolas", "Bitstream Vera Sans Mono", "Courier New", Courier, monospace;
direction: ltr;
text-align: left;
white-space: pre;
word-spacing: normal;
word-break: normal;
font-size: 0.9em;
line-height: 1.2em;
-moz-tab-size: 4;
-o-tab-size: 4;
tab-size: 4;
-webkit-hyphens: none;
-moz-hyphens: none;
-ms-hyphens: none;
hyphens: none;
}
pre > code[class*="language-"] {
font-size: 1em;
}
pre[class*="language-"]::-moz-selection,
pre[class*="language-"] ::-moz-selection,
code[class*="language-"]::-moz-selection,
code[class*="language-"] ::-moz-selection {
background: #c1def1;
}
pre[class*="language-"]::selection,
pre[class*="language-"] ::selection,
code[class*="language-"]::selection,
code[class*="language-"] ::selection {
background: #c1def1;
}
/* Code blocks */
pre[class*="language-"] {
padding: 1em;
margin: 0.5em 0;
overflow: auto;
border: 1px solid #dddddd;
background-color: white;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: 0.2em;
padding-top: 1px;
padding-bottom: 1px;
background: #f8f8f8;
border: 1px solid #dddddd;
}
.token.comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #008000;
font-style: italic;
}
.token.namespace {
opacity: 0.7;
}
.token.string {
color: #a31515;
}
.token.punctuation,
.token.operator {
color: #393a34; /* no highlight */
}
.token.url,
.token.symbol,
.token.number,
.token.boolean,
.token.variable,
.token.constant,
.token.inserted {
color: #36acaa;
}
.token.atrule,
.token.keyword,
.token.attr-value,
.language-autohotkey .token.selector,
.language-json .token.boolean,
.language-json .token.number,
code[class*="language-css"] {
color: #0000ff;
}
.token.function {
color: #393a34;
}
.token.deleted,
.language-autohotkey .token.tag {
color: #9a050f;
}
.token.selector,
.language-autohotkey .token.keyword {
color: #00009f;
}
.token.important {
color: #e90;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.class-name,
.language-json .token.property {
color: #2b91af;
}
.token.tag,
.token.selector {
color: #800000;
}
.token.attr-name,
.token.property,
.token.regex,
.token.entity {
color: #ff0000;
}
.token.directive.tag .tag {
background: #ffff00;
color: #393a34;
}
/* overrides color-values for the Line Numbers plugin
* http://prismjs.com/plugins/line-numbers/
*/
.line-numbers.line-numbers .line-numbers-rows {
border-right-color: #a5a5a5;
}
.line-numbers .line-numbers-rows > span:before {
color: #2b91af;
}
/* overrides color-values for the Line Highlight plugin
* http://prismjs.com/plugins/line-highlight/
*/
.line-highlight.line-highlight {
background: rgba(193, 222, 241, 0.2);
background: -webkit-linear-gradient(left, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0));
background: linear-gradient(to right, rgba(193, 222, 241, 0.2) 70%, rgba(221, 222, 241, 0));
}

View File

@ -289,7 +289,7 @@
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -314,7 +314,7 @@
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -314,7 +314,7 @@
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -315,7 +315,7 @@
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -289,7 +289,7 @@
[[], []]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -288,7 +288,7 @@
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -287,7 +287,7 @@
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -289,7 +289,7 @@
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -306,7 +306,7 @@
[["projects", "certifications", "skills", "languages", "references"], []]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -287,7 +287,7 @@
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -315,7 +315,7 @@
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -288,7 +288,7 @@
]
],
"css": {
"value": ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"value": "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
"visible": false
},
"page": {

View File

@ -7,7 +7,7 @@ import {
DropdownMenuTrigger,
KeyboardShortcut,
} from "@reactive-resume/ui";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { useLogout } from "../services/auth";
@ -26,7 +26,7 @@ export const UserOptions = ({ children }: Props) => {
<DropdownMenuContent side="top" align="start" className="w-48">
<DropdownMenuItem
onClick={() => {
navigate("/dashboard/settings");
void navigate("/dashboard/settings");
}}
>
{t`Settings`}

View File

@ -2,7 +2,7 @@ import { t } from "@lingui/macro";
import { deepSearchAndParseDates, ErrorMessage } from "@reactive-resume/utils";
import _axios from "axios";
import createAuthRefreshInterceptor from "axios-auth-refresh";
import { redirect } from "react-router-dom";
import { redirect } from "react-router";
import { refreshToken } from "@/client/services/auth";

View File

@ -1,6 +1,6 @@
import { StrictMode } from "react";
import * as ReactDOM from "react-dom/client";
import { RouterProvider } from "react-router-dom";
import { RouterProvider } from "react-router";
import { router } from "./router";

View File

@ -16,7 +16,7 @@ import {
import { useRef } from "react";
import { Helmet } from "react-helmet-async";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { z } from "zod";
import { useBackupOtp } from "@/client/services/auth";
@ -39,7 +39,7 @@ export const BackupOtpPage = () => {
try {
await backupOtp(data);
navigate("/dashboard");
void navigate("/dashboard");
} catch {
form.reset();
}
@ -92,7 +92,7 @@ export const BackupOtpPage = () => {
variant="link"
className="px-5"
onClick={() => {
navigate(-1);
void navigate(-1);
}}
>
<ArrowLeft size={14} className="mr-2" />

View File

@ -17,7 +17,7 @@ import {
import { useState } from "react";
import { Helmet } from "react-helmet-async";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { z } from "zod";
import { useForgotPassword } from "@/client/services/auth";
@ -93,7 +93,7 @@ export const ForgotPasswordPage = () => {
variant="link"
className="px-5"
onClick={() => {
navigate(-1);
void navigate(-1);
}}
>
<ArrowLeft size={14} className="mr-2" />

View File

@ -1,7 +1,7 @@
import { t } from "@lingui/macro";
import { cn } from "@reactive-resume/utils";
import { useMemo } from "react";
import { Link, matchRoutes, Outlet, useLocation } from "react-router-dom";
import { Link, matchRoutes, Outlet, useLocation } from "react-router";
import { LocaleSwitch } from "@/client/components/locale-switch";
import { Logo } from "@/client/components/logo";

View File

@ -20,7 +20,7 @@ import { cn } from "@reactive-resume/utils";
import { useRef } from "react";
import { Helmet } from "react-helmet-async";
import { useForm } from "react-hook-form";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import { z } from "zod";
import { useLogin } from "@/client/services/auth";

View File

@ -20,7 +20,7 @@ import { cn } from "@reactive-resume/utils";
import { useRef } from "react";
import { Helmet } from "react-helmet-async";
import { useForm } from "react-hook-form";
import { Link, useNavigate } from "react-router-dom";
import { Link, useNavigate } from "react-router";
import { z } from "zod";
import { useRegister } from "@/client/services/auth";
@ -51,7 +51,7 @@ export const RegisterPage = () => {
try {
await register(data);
navigate("/auth/verify-email");
void navigate("/auth/verify-email");
} catch {
form.reset();
}

View File

@ -16,7 +16,7 @@ import {
import { useEffect, useRef } from "react";
import { Helmet } from "react-helmet-async";
import { useForm } from "react-hook-form";
import { useNavigate, useSearchParams } from "react-router-dom";
import { useNavigate, useSearchParams } from "react-router";
import { z } from "zod";
import { useResetPassword } from "@/client/services/auth";
@ -42,7 +42,7 @@ export const ResetPasswordPage = () => {
try {
await resetPassword(data);
navigate("/auth/login");
void navigate("/auth/login");
} catch {
form.reset();
}
@ -50,7 +50,7 @@ export const ResetPasswordPage = () => {
// Redirect the user to the forgot password page if the token is not present.
useEffect(() => {
if (!token) navigate("/auth/forgot-password");
if (!token) void navigate("/auth/forgot-password");
}, [token, navigate]);
return (

View File

@ -3,7 +3,7 @@ import { ArrowRight, Info, SealCheck } from "@phosphor-icons/react";
import { Alert, AlertDescription, AlertTitle, Button } from "@reactive-resume/ui";
import { useEffect } from "react";
import { Helmet } from "react-helmet-async";
import { Link, useNavigate, useSearchParams } from "react-router-dom";
import { Link, useNavigate, useSearchParams } from "react-router";
import { useToast } from "@/client/hooks/use-toast";
import { queryClient } from "@/client/libs/query-client";
@ -28,7 +28,7 @@ export const VerifyEmailPage = () => {
title: t`Your email address has been verified successfully.`,
});
navigate("/dashboard/resumes", { replace: true });
void navigate("/dashboard/resumes", { replace: true });
};
if (!token) return;

View File

@ -16,7 +16,7 @@ import {
import { useRef } from "react";
import { Helmet } from "react-helmet-async";
import { useForm } from "react-hook-form";
import { Link, useNavigate } from "react-router-dom";
import { Link, useNavigate } from "react-router";
import { z } from "zod";
import { useVerifyOtp } from "@/client/services/auth";
@ -39,7 +39,7 @@ export const VerifyOtpPage = () => {
try {
await verifyOtp(data);
navigate("/dashboard");
void navigate("/dashboard");
} catch {
form.reset();
}

View File

@ -2,7 +2,7 @@ import { t } from "@lingui/macro";
import { HouseSimple, Lock, SidebarSimple } from "@phosphor-icons/react";
import { Button, Tooltip } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import { useBuilderStore } from "@/client/stores/builder";
import { useResumeStore } from "@/client/stores/resume";

View File

@ -1,7 +1,7 @@
import { useBreakpoint } from "@reactive-resume/hooks";
import { Panel, PanelGroup, PanelResizeHandle, Sheet, SheetContent } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
import { Outlet } from "react-router-dom";
import { Outlet } from "react-router";
import { useBuilderStore } from "@/client/stores/builder";

View File

@ -2,7 +2,7 @@ import { t } from "@lingui/macro";
import { ResumeDto } from "@reactive-resume/dto";
import { useCallback, useEffect } from "react";
import { Helmet } from "react-helmet-async";
import { LoaderFunction, redirect } from "react-router-dom";
import { LoaderFunction, redirect } from "react-router";
import { queryClient } from "@/client/libs/query-client";
import { findResumeById } from "@/client/services/resume";

View File

@ -17,7 +17,7 @@ import {
} from "@reactive-resume/schema";
import { Button, ScrollArea, Separator } from "@reactive-resume/ui";
import { Fragment, useRef } from "react";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import { Icon } from "@/client/components/icon";
import { UserAvatar } from "@/client/components/user-avatar";

View File

@ -5,6 +5,7 @@ import { useRef } from "react";
import { Copyright } from "@/client/components/copyright";
import { ThemeSwitch } from "@/client/components/theme-switch";
import { CssSection } from "./sections/css";
import { ExportSection } from "./sections/export";
import { InformationSection } from "./sections/information";
import { LayoutSection } from "./sections/layout";
@ -37,6 +38,8 @@ export const RightSidebar = () => {
<Separator />
<ThemeSection />
<Separator />
<CssSection />
<Separator />
<PageSection />
<Separator />
<SharingSection />
@ -85,6 +88,13 @@ export const RightSidebar = () => {
scrollIntoView("#theme");
}}
/>
<SectionIcon
id="css"
name={t`Custom CSS`}
onClick={() => {
scrollIntoView("#css");
}}
/>
<SectionIcon
id="page"
name={t`Page`}

View File

@ -0,0 +1,58 @@
import { t } from "@lingui/macro";
import { useTheme } from "@reactive-resume/hooks";
import { Label, Switch } from "@reactive-resume/ui";
import Prism from "prismjs";
import { Helmet } from "react-helmet-async";
import CodeEditor from "react-simple-code-editor";
import { useResumeStore } from "@/client/stores/resume";
import { getSectionIcon } from "../shared/section-icon";
export const CssSection = () => {
const { isDarkMode } = useTheme();
const setValue = useResumeStore((state) => state.setValue);
const css = useResumeStore((state) => state.resume.data.metadata.css);
return (
<section id="css" className="grid gap-y-6">
<Helmet>
{isDarkMode && <link rel="stylesheet" href="/styles/prism-dark.css" />}
{!isDarkMode && <link rel="stylesheet" href="/styles/prism-light.css" />}
</Helmet>
<header className="flex items-center justify-between">
<div className="flex items-center gap-x-4">
{getSectionIcon("css")}
<h2 className="line-clamp-1 text-2xl font-bold lg:text-3xl">{t`Custom CSS`}</h2>
</div>
</header>
<main className="space-y-4">
<div className="flex items-center gap-x-4">
<Switch
id="metadata.css.visible"
checked={css.visible}
onCheckedChange={(checked) => {
setValue("metadata.css.visible", checked);
}}
/>
<Label htmlFor="metadata.css.visible">{t`Apply Custom CSS`}</Label>
</div>
<div className="rounded border p-4">
<CodeEditor
tabSize={4}
value={css.value}
className="language-css font-mono"
highlight={(code) => Prism.highlight(code, Prism.languages.css, "css")}
onValueChange={(value) => {
setValue("metadata.css.value", value);
}}
/>
</div>
</main>
</section>
);
};

View File

@ -1,4 +1,5 @@
import {
Code,
DiamondsFour,
DownloadSimple,
IconProps,
@ -19,6 +20,7 @@ export type MetadataKey =
| "layout"
| "typography"
| "theme"
| "css"
| "page"
| "locale"
| "sharing"
@ -45,6 +47,9 @@ export const getSectionIcon = (id: MetadataKey, props: IconProps = {}) => {
case "theme": {
return <Palette size={18} {...props} />;
}
case "css": {
return <Code size={18} {...props} />;
}
case "page": {
return <ReadCvLogo size={18} {...props} />;
}

View File

@ -3,7 +3,7 @@ import { FadersHorizontal, ReadCvLogo } from "@phosphor-icons/react";
import { Button, KeyboardShortcut, Separator } from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
import { motion } from "framer-motion";
import { Link, useLocation, useNavigate } from "react-router-dom";
import { Link, useLocation, useNavigate } from "react-router";
import useKeyboardShortcut from "use-keyboard-shortcut";
import { Copyright } from "@/client/components/copyright";
@ -71,12 +71,12 @@ export const Sidebar = ({ setOpen }: SidebarProps) => {
const navigate = useNavigate();
useKeyboardShortcut(["shift", "r"], () => {
navigate("/dashboard/resumes");
void navigate("/dashboard/resumes");
setOpen?.(false);
});
useKeyboardShortcut(["shift", "s"], () => {
navigate("/dashboard/settings");
void navigate("/dashboard/settings");
setOpen?.(false);
});

View File

@ -2,7 +2,7 @@ import { SidebarSimple } from "@phosphor-icons/react";
import { Button, Sheet, SheetClose, SheetContent, SheetTrigger } from "@reactive-resume/ui";
import { motion } from "framer-motion";
import { useState } from "react";
import { Outlet } from "react-router-dom";
import { Outlet } from "react-router";
import { Sidebar } from "./_components/sidebar";

View File

@ -33,7 +33,8 @@ import {
Input,
Tooltip,
} from "@reactive-resume/ui";
import { cn, generateRandomName, kebabCase } from "@reactive-resume/utils";
import { cn, generateRandomName } from "@reactive-resume/utils";
import slugify from "@sindresorhus/slugify";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
@ -42,7 +43,7 @@ import { useCreateResume, useDeleteResume, useUpdateResume } from "@/client/serv
import { useImportResume } from "@/client/services/resume/import";
import { useDialog } from "@/client/stores/dialog";
const formSchema = createResumeSchema.extend({ id: idSchema.optional() });
const formSchema = createResumeSchema.extend({ id: idSchema.optional(), slug: z.string() });
type FormValues = z.infer<typeof formSchema>;
@ -71,7 +72,7 @@ export const ResumeDialog = () => {
}, [isOpen, payload]);
useEffect(() => {
const slug = kebabCase(form.watch("title"));
const slug = slugify(form.watch("title"));
form.setValue("slug", slug);
}, [form.watch("title")]);
@ -122,7 +123,7 @@ export const ResumeDialog = () => {
const onGenerateRandomName = () => {
const name = generateRandomName();
form.setValue("title", name);
form.setValue("slug", kebabCase(name));
form.setValue("slug", slugify(name));
};
const onCreateSample = async () => {
@ -130,7 +131,7 @@ export const ResumeDialog = () => {
await duplicateResume({
title: randomName,
slug: kebabCase(randomName),
slug: slugify(randomName),
data: sampleResume,
});

View File

@ -18,7 +18,7 @@ import {
import { cn } from "@reactive-resume/utils";
import dayjs from "dayjs";
import { AnimatePresence, motion } from "framer-motion";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { useDialog } from "@/client/stores/dialog";
@ -37,7 +37,7 @@ export const ResumeCard = ({ resume }: Props) => {
const lastUpdated = dayjs().to(resume.updatedAt);
const onOpen = () => {
navigate(`/builder/${resume.id}`);
void navigate(`/builder/${resume.id}`);
};
const onUpdate = () => {

View File

@ -22,7 +22,7 @@ import {
DropdownMenuTrigger,
} from "@reactive-resume/ui";
import dayjs from "dayjs";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { useDialog } from "@/client/stores/dialog";
@ -40,7 +40,7 @@ export const ResumeListItem = ({ resume }: Props) => {
const lastUpdated = dayjs().to(resume.updatedAt);
const onOpen = () => {
navigate(`/builder/${resume.id}`);
void navigate(`/builder/${resume.id}`);
};
const onUpdate = () => {

View File

@ -11,7 +11,7 @@ import {
Input,
} from "@reactive-resume/ui";
import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { useCounter } from "usehooks-ts";
import { z } from "zod";
@ -52,7 +52,7 @@ export const DangerZoneSettings = () => {
title: t`Your account and all your data has been deleted successfully. Goodbye!`,
});
navigate("/");
void navigate("/");
}
};

View File

@ -1,6 +1,6 @@
import { t } from "@lingui/macro";
import { Separator } from "@reactive-resume/ui";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import { Copyright } from "@/client/components/copyright";
import { LocaleSwitch } from "@/client/components/locale-switch";

View File

@ -1,5 +1,5 @@
import { motion } from "framer-motion";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import { Logo } from "@/client/components/logo";

View File

@ -1,5 +1,5 @@
import { ScrollArea } from "@reactive-resume/ui";
import { Outlet } from "react-router-dom";
import { Outlet } from "react-router";
import { Footer } from "./components/footer";
import { Header } from "./components/header";

View File

@ -1,7 +1,7 @@
import { t } from "@lingui/macro";
import { Book, SignOut } from "@phosphor-icons/react";
import { Button } from "@reactive-resume/ui";
import { Link } from "react-router-dom";
import { Link } from "react-router";
import { useLogout } from "@/client/services/auth";
import { useAuthStore } from "@/client/stores/auth";

View File

@ -5,7 +5,7 @@ import { Button } from "@reactive-resume/ui";
import { pageSizeMap } from "@reactive-resume/utils";
import { useCallback, useEffect, useRef } from "react";
import { Helmet } from "react-helmet-async";
import { Link, LoaderFunction, redirect, useLoaderData } from "react-router-dom";
import { Link, LoaderFunction, redirect, useLoaderData } from "react-router";
import { Icon } from "@/client/components/icon";
import { ThemeSwitch } from "@/client/components/theme-switch";
@ -22,8 +22,8 @@ export const PublicResumePage = () => {
const { printResume, loading } = usePrintResume();
const { id, title, data: resume } = useLoaderData() as ResumeDto;
const format = resume.metadata.page.format;
const { id, title, data: resume } = useLoaderData();
const format = resume.metadata.page.format as keyof typeof pageSizeMap;
const updateResumeInFrame = useCallback(() => {
if (!frameRef.current?.contentWindow) return;

View File

@ -1,7 +1,7 @@
import { TooltipProvider } from "@reactive-resume/ui";
import { QueryClientProvider } from "@tanstack/react-query";
import { HelmetProvider } from "react-helmet-async";
import { Outlet } from "react-router-dom";
import { Outlet } from "react-router";
import { helmetContext } from "../constants/helmet";
import { queryClient } from "../libs/query-client";

View File

@ -1,4 +1,4 @@
import { Navigate, Outlet, useLocation } from "react-router-dom";
import { Navigate, Outlet, useLocation } from "react-router";
import { useUser } from "@/client/services/user";

View File

@ -1,4 +1,4 @@
import { Navigate, Outlet, useSearchParams } from "react-router-dom";
import { Navigate, Outlet, useSearchParams } from "react-router";
import { useAuthStore } from "@/client/stores/auth";

View File

@ -1,4 +1,4 @@
import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router-dom";
import { createBrowserRouter, createRoutesFromElements, Navigate, Route } from "react-router";
import { BackupOtpPage } from "../pages/auth/backup-otp/page";
import { ForgotPasswordPage } from "../pages/auth/forgot-password/page";
@ -23,7 +23,8 @@ import { GuestGuard } from "./guards/guest";
import { authLoader } from "./loaders/auth";
export const routes = createRoutesFromElements(
<Route element={<Providers />}>
// eslint-disable-next-line lingui/no-unlocalized-strings
<Route element={<Providers />} hydrateFallbackElement={<div>Loading...</div>}>
<Route element={<HomeLayout />}>
<Route path="/" element={<HomePage />} />

View File

@ -1,5 +1,5 @@
import { authResponseSchema, UserDto } from "@reactive-resume/dto";
import { LoaderFunction, redirect } from "react-router-dom";
import { LoaderFunction, redirect } from "react-router";
import { USER_KEY } from "@/client/constants/query-keys";
import { queryClient } from "@/client/libs/query-client";

View File

@ -1,7 +1,7 @@
import { AuthResponseDto, LoginDto } from "@reactive-resume/dto";
import { useMutation } from "@tanstack/react-query";
import { AxiosResponse } from "axios";
import { useNavigate } from "react-router-dom";
import { useNavigate } from "react-router";
import { axios } from "@/client/libs/axios";
import { queryClient } from "@/client/libs/query-client";
@ -28,7 +28,7 @@ export const useLogin = () => {
mutationFn: login,
onSuccess: (data) => {
if (data.status === "2fa_required") {
navigate("/auth/verify-otp");
void navigate("/auth/verify-otp");
return;
}

View File

@ -150,6 +150,17 @@ export class PrinterService {
return temporaryHtml_;
}, pageElement);
// Apply custom CSS if enabled
const css = resume.data.metadata.css;
if (css.visible) {
await page.evaluate((cssValue: string) => {
const styleTag = document.createElement("style");
styleTag.textContent = cssValue;
document.head.append(styleTag);
}, css.value);
}
const uint8array = await page.pdf({ width, height, printBackground: true });
const buffer = Buffer.from(uint8array);
pagesBuffer.push(buffer);
@ -209,6 +220,8 @@ export class PrinterService {
return resumeUrl;
} catch (error) {
this.logger.error(error);
throw new InternalServerErrorException(
ErrorMessage.ResumePrinterError,
(error as Error).message,

View File

@ -8,7 +8,8 @@ import { Prisma } from "@prisma/client";
import { CreateResumeDto, ImportResumeDto, ResumeDto, UpdateResumeDto } from "@reactive-resume/dto";
import { defaultResumeData, ResumeData } from "@reactive-resume/schema";
import type { DeepPartial } from "@reactive-resume/utils";
import { ErrorMessage, generateRandomName, kebabCase } from "@reactive-resume/utils";
import { ErrorMessage, generateRandomName } from "@reactive-resume/utils";
import slugify from "@sindresorhus/slugify";
import deepmerge from "deepmerge";
import { PrismaService } from "nestjs-prisma";
@ -40,7 +41,7 @@ export class ResumeService {
userId,
title: createResumeDto.title,
visibility: createResumeDto.visibility,
slug: createResumeDto.slug ?? kebabCase(createResumeDto.title),
slug: createResumeDto.slug ?? slugify(createResumeDto.title),
},
});
}
@ -54,7 +55,7 @@ export class ResumeService {
visibility: "private",
data: importResumeDto.data,
title: importResumeDto.title ?? randomTitle,
slug: importResumeDto.slug ?? kebabCase(randomTitle),
slug: importResumeDto.slug ?? slugify(randomTitle),
},
});
}

View File

@ -1,6 +1,7 @@
import { Injectable, InternalServerErrorException, Logger, OnModuleInit } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { createId } from "@paralleldrive/cuid2";
import slugify from "@sindresorhus/slugify";
import { MinioClient, MinioService } from "nestjs-minio-client";
import sharp from "sharp";
@ -116,14 +117,19 @@ export class StorageService implements OnModuleInit {
) {
const extension = type === "resumes" ? "pdf" : "jpg";
const storageUrl = this.configService.getOrThrow<string>("STORAGE_URL");
const filepath = `${userId}/${type}/${filename}.${extension}`;
let normalizedFilename = slugify(filename);
if (!normalizedFilename) normalizedFilename = createId();
const filepath = `${userId}/${type}/${normalizedFilename}.${extension}`;
const url = `${storageUrl}/${filepath}`;
const metadata =
extension === "jpg"
? { "Content-Type": "image/jpeg" }
: {
"Content-Type": "application/pdf",
"Content-Disposition": `attachment; filename=${filename}.${extension}`,
"Content-Disposition": `attachment; filename=${normalizedFilename}.${extension}`,
};
try {

View File

@ -11,8 +11,10 @@
"dependencies": {
"@reactive-resume/utils": "*",
"@reactive-resume/schema": "*",
"@sindresorhus/slugify": "^2.2.1",
"nestjs-zod": "^3.0.0",
"@swc/helpers": "~0.5.11",
"zod": "^3.24.1"
"zod": "^3.24.1",
"@paralleldrive/cuid2": "^2.2.2"
}
}

View File

@ -1,10 +1,19 @@
import { kebabCase } from "@reactive-resume/utils";
import { createId } from "@paralleldrive/cuid2";
import slugify from "@sindresorhus/slugify";
import { createZodDto } from "nestjs-zod/dto";
import { z } from "zod";
export const createResumeSchema = z.object({
title: z.string().min(1),
slug: z.string().min(1).transform(kebabCase).optional(),
slug: z
.string()
.min(1)
.transform((value) => {
const slug = slugify(value);
if (!slug) return createId();
return slug;
})
.optional(),
visibility: z.enum(["public", "private"]).default("private"),
});

View File

@ -1,11 +1,20 @@
import { createId } from "@paralleldrive/cuid2";
import { resumeDataSchema } from "@reactive-resume/schema";
import { kebabCase } from "@reactive-resume/utils";
import slugify from "@sindresorhus/slugify";
import { createZodDto } from "nestjs-zod/dto";
import { z } from "zod";
export const importResumeSchema = z.object({
title: z.string().optional(),
slug: z.string().min(1).transform(kebabCase).optional(),
slug: z
.string()
.min(1)
.transform((value) => {
const slug = slugify(value);
if (slug === "") return createId();
return slug;
})
.optional(),
visibility: z.enum(["public", "private"]).default("private").optional(),
data: resumeDataSchema,
});

View File

@ -12,7 +12,7 @@ export const metadataSchema = z.object({
template: z.string().default("rhyhorn"),
layout: z.array(z.array(z.array(z.string()))).default(defaultLayout), // pages -> columns -> sections
css: z.object({
value: z.string().default(".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}"),
value: z.string().default("* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}"),
visible: z.boolean().default(false),
}),
page: z.object({
@ -50,7 +50,7 @@ export const defaultMetadata: Metadata = {
template: "rhyhorn",
layout: defaultLayout,
css: {
value: ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
value: "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
visible: false,
},
page: {

View File

@ -308,7 +308,7 @@ export const sampleResume: ResumeData = {
],
],
css: {
value: ".section {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
value: "* {\n\toutline: 1px solid #000;\n\toutline-offset: 4px;\n}",
visible: false,
},
page: {

View File

@ -7,4 +7,4 @@ export const pageSizeMap = {
width: 216,
height: 279,
},
};
} as const;

View File

@ -30,17 +30,6 @@ export const extractUrl = (string: string) => {
return result ? result[0] : null;
};
export const kebabCase = (string?: string | null) => {
if (!string) return "";
return (
string
.match(/[A-Z]{2,}(?=[A-Z][a-z]+\d*|\b)|[A-Z]?[a-z]+\d*|[A-Z]|\d+/gu)
?.join("-")
.toLowerCase() ?? ""
);
};
export const generateRandomName = () => {
return uniqueNamesGenerator({
dictionaries: [adjectives, adjectives, animals],

View File

@ -6,7 +6,6 @@ import {
getInitials,
isEmptyString,
isUrl,
kebabCase,
processUsername,
} from "../string";
@ -40,16 +39,6 @@ describe("extractUrl", () => {
});
});
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();

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.3.3",
"version": "4.3.5",
"license": "MIT",
"private": true,
"author": {
@ -75,6 +75,7 @@
"@types/passport-github2": "^1.2.9",
"@types/passport-google-oauth20": "^2.0.16",
"@types/passport-local": "^1.0.38",
"@types/prismjs": "^1.26.5",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"@types/react-is": "^18.3.1",
@ -168,6 +169,7 @@
"@radix-ui/react-toggle-group": "^1.1.1",
"@radix-ui/react-tooltip": "^1.1.6",
"@radix-ui/react-visually-hidden": "^1.1.1",
"@sindresorhus/slugify": "^2.2.1",
"@swc/helpers": "^0.5.15",
"@tanstack/react-query": "^5.64.0",
"@tiptap/extension-highlight": "^2.11.2",
@ -214,6 +216,7 @@
"passport-local": "^1.0.0",
"pdf-lib": "^1.17.1",
"prisma": "^5.22.0",
"prismjs": "^1.29.0",
"puppeteer": "^23.11.1",
"qrcode.react": "^4.2.0",
"react": "^18.3.1",
@ -223,7 +226,8 @@
"react-hook-form": "^7.54.2",
"react-parallax-tilt": "^1.7.272",
"react-resizable-panels": "^2.1.7",
"react-router-dom": "^6.28.1",
"react-router": "^7.1.1",
"react-simple-code-editor": "^0.14.1",
"react-zoom-pan-pinch": "^3.6.1",
"reflect-metadata": "^0.2.2",
"rxjs": "^7.8.1",

113
pnpm-lock.yaml generated
View File

@ -155,6 +155,9 @@ importers:
'@radix-ui/react-visually-hidden':
specifier: ^1.1.1
version: 1.1.1(@types/react-dom@18.3.5(@types/react@18.3.18))(@types/react@18.3.18)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@sindresorhus/slugify':
specifier: ^2.2.1
version: 2.2.1
'@swc/helpers':
specifier: ^0.5.15
version: 0.5.15
@ -293,6 +296,9 @@ importers:
prisma:
specifier: ^5.22.0
version: 5.22.0
prismjs:
specifier: ^1.29.0
version: 1.29.0
puppeteer:
specifier: ^23.11.1
version: 23.11.1(typescript@5.7.3)
@ -320,9 +326,12 @@ importers:
react-resizable-panels:
specifier: ^2.1.7
version: 2.1.7(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router-dom:
specifier: ^6.28.1
version: 6.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-router:
specifier: ^7.1.1
version: 7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-simple-code-editor:
specifier: ^0.14.1
version: 0.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-zoom-pan-pinch:
specifier: ^3.6.1
version: 3.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -504,6 +513,9 @@ importers:
'@types/passport-local':
specifier: ^1.0.38
version: 1.0.38
'@types/prismjs':
specifier: ^1.26.5
version: 1.26.5
'@types/react':
specifier: ^18.3.18
version: 18.3.18
@ -3576,10 +3588,6 @@ packages:
'@remirror/core-constants@3.0.0':
resolution: {integrity: sha512-42aWfPrimMfDKDi4YegyS7x+/0tlzaqwPQCULLanv3DMIlu96KTJR0fM5isWX2UViOqlGnX6YFgqWepcX+XMNg==}
'@remix-run/router@1.21.0':
resolution: {integrity: sha512-xfSkCAchbdG5PnbrKqFWwia4Bi61nH+wm8wLEqfHDyp7Y3dZzgqS2itV8i4gAq9pC2HsTpwyBC6Ds8VHZ96JlA==}
engines: {node: '>=14.0.0'}
'@rollup/pluginutils@5.1.4':
resolution: {integrity: sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==}
engines: {node: '>=14.0.0'}
@ -3749,6 +3757,14 @@ packages:
resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==}
engines: {node: '>=10'}
'@sindresorhus/slugify@2.2.1':
resolution: {integrity: sha512-MkngSCRZ8JdSOCHRaYd+D01XhvU3Hjy6MGl06zhOk614hp9EOAp5gIkBeQg7wtmxpitU6eAL4kdiRMcJa2dlrw==}
engines: {node: '>=12'}
'@sindresorhus/transliterate@1.6.0':
resolution: {integrity: sha512-doH1gimEu3A46VX6aVxpHTeHrytJAG6HgdxntYnCFiIFHEM/ZGpG8KiZGBChchjQmG0XFIBL552kBTjVcMZXwQ==}
engines: {node: '>=12'}
'@sinonjs/commons@3.0.1':
resolution: {integrity: sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==}
@ -4224,6 +4240,9 @@ packages:
peerDependencies:
'@types/express': '*'
'@types/cookie@0.6.0':
resolution: {integrity: sha512-4Kh9a6B2bQciAhf7FSuMRRkUWecJgJu9nPnx3yzpsfXX/c50REIqpHY4C82bXP90qrLtXtkDxTZosYO3UpOwlA==}
'@types/cookies@0.9.0':
resolution: {integrity: sha512-40Zk8qR147RABiQ7NQnBzWzDcjKzNrntB5BAmeGCb2p/MIyOE+4BVvc17wumsUqUw00bJYqoXFHYygQnEFh4/Q==}
@ -4395,6 +4414,9 @@ packages:
'@types/pg@8.6.1':
resolution: {integrity: sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==}
'@types/prismjs@1.26.5':
resolution: {integrity: sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==}
'@types/prop-types@15.7.11':
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
@ -5590,6 +5612,10 @@ packages:
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
engines: {node: '>= 0.6'}
cookie@1.0.2:
resolution: {integrity: sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==}
engines: {node: '>=18'}
cookies@0.9.1:
resolution: {integrity: sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==}
engines: {node: '>= 0.8'}
@ -9384,6 +9410,10 @@ packages:
engines: {node: '>=16.13'}
hasBin: true
prismjs@1.29.0:
resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==}
engines: {node: '>=6'}
proc-log@3.0.0:
resolution: {integrity: sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@ -9669,18 +9699,21 @@ packages:
react: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-dom: ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc
react-router-dom@6.28.1:
resolution: {integrity: sha512-YraE27C/RdjcZwl5UCqF/ffXnZDxpJdk9Q6jw38SZHjXs7NNdpViq2l2c7fO7+4uWaEfcwfGCv3RSg4e1By/fQ==}
engines: {node: '>=14.0.0'}
react-router@7.1.1:
resolution: {integrity: sha512-39sXJkftkKWRZ2oJtHhCxmoCrBCULr/HAH4IT5DHlgu/Q0FCPV0S4Lx+abjDTx/74xoZzNYDYbOZWlJjruyuDQ==}
engines: {node: '>=20.0.0'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
react: '>=18'
react-dom: '>=18'
peerDependenciesMeta:
react-dom:
optional: true
react-router@6.28.1:
resolution: {integrity: sha512-2omQTA3rkMljmrvvo6WtewGdVh45SpL9hGiCI9uUrwGGfNFDIvGK4gYJsKlJoNVi6AQZcopSCballL+QGOm7fA==}
engines: {node: '>=14.0.0'}
react-simple-code-editor@0.14.1:
resolution: {integrity: sha512-BR5DtNRy+AswWJECyA17qhUDvrrCZ6zXOCfkQY5zSmb96BVUbpVAv03WpcjcwtCwiLbIANx3gebHOcXYn1EHow==}
peerDependencies:
react: '>=16.8'
react: '>=16.8.0'
react-dom: '>=16.8.0'
react-style-singleton@2.2.1:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
@ -10062,6 +10095,9 @@ packages:
resolution: {integrity: sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==}
engines: {node: '>= 0.8.0'}
set-cookie-parser@2.7.1:
resolution: {integrity: sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==}
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@ -10757,6 +10793,9 @@ packages:
resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==}
engines: {node: '>=0.6.x'}
turbo-stream@2.4.0:
resolution: {integrity: sha512-FHncC10WpBd2eOmGwpmQsWLDoK4cqsA/UT/GqNoaKOQnT8uzhtCbg3EoUDMvqpOSAI0S26mr0rkjzbOO6S3v1g==}
type-check@0.4.0:
resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==}
engines: {node: '>= 0.8.0'}
@ -15212,8 +15251,6 @@ snapshots:
'@remirror/core-constants@3.0.0': {}
'@remix-run/router@1.21.0': {}
'@rollup/pluginutils@5.1.4(rollup@4.30.1)':
dependencies:
'@types/estree': 1.0.6
@ -15388,6 +15425,15 @@ snapshots:
'@sindresorhus/is@4.6.0': {}
'@sindresorhus/slugify@2.2.1':
dependencies:
'@sindresorhus/transliterate': 1.6.0
escape-string-regexp: 5.0.0
'@sindresorhus/transliterate@1.6.0':
dependencies:
escape-string-regexp: 5.0.0
'@sinonjs/commons@3.0.1':
dependencies:
type-detect: 4.0.8
@ -15907,6 +15953,8 @@ snapshots:
dependencies:
'@types/express': 4.17.21
'@types/cookie@0.6.0': {}
'@types/cookies@0.9.0':
dependencies:
'@types/connect': 3.4.38
@ -16135,6 +16183,8 @@ snapshots:
pg-protocol: 1.7.0
pg-types: 2.2.0
'@types/prismjs@1.26.5': {}
'@types/prop-types@15.7.11': {}
'@types/pug@2.0.10':
@ -17577,6 +17627,8 @@ snapshots:
cookie@0.7.2: {}
cookie@1.0.2: {}
cookies@0.9.1:
dependencies:
depd: 2.0.0
@ -22285,6 +22337,8 @@ snapshots:
optionalDependencies:
fsevents: 2.3.3
prismjs@1.29.0: {}
proc-log@3.0.0: {}
process-nextick-args@2.0.1: {}
@ -22674,17 +22728,20 @@ snapshots:
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-router-dom@6.28.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
react-router@7.1.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@types/cookie': 0.6.0
cookie: 1.0.2
react: 18.3.1
set-cookie-parser: 2.7.1
turbo-stream: 2.4.0
optionalDependencies:
react-dom: 18.3.1(react@18.3.1)
react-simple-code-editor@0.14.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@remix-run/router': 1.21.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-router: 6.28.1(react@18.3.1)
react-router@6.28.1(react@18.3.1):
dependencies:
'@remix-run/router': 1.21.0
react: 18.3.1
react-style-singleton@2.2.1(@types/react@18.3.18)(react@18.3.1):
dependencies:
@ -23133,6 +23190,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
set-cookie-parser@2.7.1: {}
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@ -23953,6 +24012,8 @@ snapshots:
tsscmp@1.0.6: {}
turbo-stream@2.4.0: {}
type-check@0.4.0:
dependencies:
prelude-ls: 1.2.1