mirror of
https://github.com/AmruthPillai/Reactive-Resume.git
synced 2025-11-13 00:03:27 +10:00
perf: 🎨 remove fragment imports, optimize templates
This commit is contained in:
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
github: AmruthPillai
|
||||||
BIN
apps/client/public/templates/rhyhorn.jpg
Normal file
BIN
apps/client/public/templates/rhyhorn.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 344 KiB |
@ -436,9 +436,9 @@ export const sampleResume: ResumeData = {
|
|||||||
id: "j69rrkkdf94jqqfe5v726d1n",
|
id: "j69rrkkdf94jqqfe5v726d1n",
|
||||||
visible: true,
|
visible: true,
|
||||||
name: "QuantRes",
|
name: "QuantRes",
|
||||||
description: "Nassau, Bahamas",
|
description: "Execution Team Manager",
|
||||||
date: "Jan 2018 - Jul 2020",
|
date: "Jan 2018 - Jul 2020",
|
||||||
level: 0,
|
location: "Nassau, Bahamas",
|
||||||
summary:
|
summary:
|
||||||
"<p>Proprietary quantitative trading firm and privately-held quantitative futures fund with $1B in AUM and a focus on futures and OTC FX opportunities. 25+ year track record of trading global derivatives markets.</p><h3>Execution Team Manager</h3><p>Led international OTC FX and futures trading desk, software development, compliance, and market data for fund averaging $5B in monthly FX transactions and generating 500K+ futures contracts per month.</p><p>Steered a 12-member team of 4 traders and 8 software engineers. Oversaw co-located trade execution and managed direct market access in 4 data centers globally for exchanges not co-located. Built partnerships with trading technology providers. Point of contact with banks, brokerages, FXSpotStream, and 9 other liquidity providers. Led market data feed agreements with exchanges. Maintained CME and Eurex exchange memberships. Headed fulfillment of regulatory compliance and due diligence requests.</p><ul><li><p><strong>Decreased HFT FX trading costs</strong> by integrating streaming of 1 million NDF prices into co-located servers, coordinating with engineers of the FX pricing engine.</p></li></ul><ul><li><p><strong>Streamlined market data contracts and reduced costs by improving data quality and availability.</strong> Consolidated technology providers (Bloomberg and Reuters) and enhanced data collection for time periods needed, data types, and breadth of data.</p></li></ul><ul><li><p><strong>Scaled capabilities by recruiting to enlarge the team</strong>, hiring 3 traders, 2 software engineers, and a consultant with 85% retention for 2+ years. Sourced top talent by posting jobs, leveraging network of contacts, and outsourcing with recruiters.</p></li></ul><ul><li><p><strong>Reduced bank, vendor, and futures broker fees, expanded access to services</strong>, and maintained open lines of communication by strengthening existing relationships. Contacted sales representatives to facilitate introductions.</p></li></ul>",
|
"<p>Proprietary quantitative trading firm and privately-held quantitative futures fund with $1B in AUM and a focus on futures and OTC FX opportunities. 25+ year track record of trading global derivatives markets.</p><h3>Execution Team Manager</h3><p>Led international OTC FX and futures trading desk, software development, compliance, and market data for fund averaging $5B in monthly FX transactions and generating 500K+ futures contracts per month.</p><p>Steered a 12-member team of 4 traders and 8 software engineers. Oversaw co-located trade execution and managed direct market access in 4 data centers globally for exchanges not co-located. Built partnerships with trading technology providers. Point of contact with banks, brokerages, FXSpotStream, and 9 other liquidity providers. Led market data feed agreements with exchanges. Maintained CME and Eurex exchange memberships. Headed fulfillment of regulatory compliance and due diligence requests.</p><ul><li><p><strong>Decreased HFT FX trading costs</strong> by integrating streaming of 1 million NDF prices into co-located servers, coordinating with engineers of the FX pricing engine.</p></li></ul><ul><li><p><strong>Streamlined market data contracts and reduced costs by improving data quality and availability.</strong> Consolidated technology providers (Bloomberg and Reuters) and enhanced data collection for time periods needed, data types, and breadth of data.</p></li></ul><ul><li><p><strong>Scaled capabilities by recruiting to enlarge the team</strong>, hiring 3 traders, 2 software engineers, and a consultant with 85% retention for 2+ years. Sourced top talent by posting jobs, leveraging network of contacts, and outsourcing with recruiters.</p></li></ul><ul><li><p><strong>Reduced bank, vendor, and futures broker fees, expanded access to services</strong>, and maintained open lines of communication by strengthening existing relationships. Contacted sales representatives to facilitate introductions.</p></li></ul>",
|
||||||
keywords: [],
|
keywords: [],
|
||||||
@ -477,7 +477,7 @@ export const sampleResume: ResumeData = {
|
|||||||
theme: {
|
theme: {
|
||||||
background: "#ffffff",
|
background: "#ffffff",
|
||||||
text: "#000000",
|
text: "#000000",
|
||||||
primary: "#000000",
|
primary: "#222222",
|
||||||
},
|
},
|
||||||
typography: {
|
typography: {
|
||||||
font: {
|
font: {
|
||||||
|
|||||||
@ -33,7 +33,7 @@ export const BuilderLayout = () => {
|
|||||||
if (isDesktop) {
|
if (isDesktop) {
|
||||||
return (
|
return (
|
||||||
<div className="relative h-full w-full overflow-hidden">
|
<div className="relative h-full w-full overflow-hidden">
|
||||||
<PanelGroup direction="horizontal">
|
<PanelGroup direction="horizontal" autoSaveId="builder-layout">
|
||||||
<Panel
|
<Panel
|
||||||
collapsible
|
collapsible
|
||||||
minSize={20}
|
minSize={20}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import {
|
|||||||
PageGrid,
|
PageGrid,
|
||||||
PageNumber,
|
PageNumber,
|
||||||
PageWrapper,
|
PageWrapper,
|
||||||
Rhyhorn,
|
templatesList,
|
||||||
} from "@reactive-resume/templates";
|
} from "@reactive-resume/templates";
|
||||||
import { pageSizeMap } from "@reactive-resume/utils";
|
import { pageSizeMap } from "@reactive-resume/utils";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
@ -35,6 +35,15 @@ export const BuilderPage = () => {
|
|||||||
};
|
};
|
||||||
}, [resume.metadata.page]);
|
}, [resume.metadata.page]);
|
||||||
|
|
||||||
|
const Template = useMemo(() => {
|
||||||
|
const Component = templatesList.find((template) => template.id === resume.metadata.template)
|
||||||
|
?.Component;
|
||||||
|
|
||||||
|
if (!Component) return null;
|
||||||
|
|
||||||
|
return Component;
|
||||||
|
}, [resume.metadata.template]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Helmet>
|
<Helmet>
|
||||||
@ -64,7 +73,12 @@ export const BuilderPage = () => {
|
|||||||
<PageWrapper>
|
<PageWrapper>
|
||||||
{showPageNumbers && <PageNumber>Page {pageIndex + 1}</PageNumber>}
|
{showPageNumbers && <PageNumber>Page {pageIndex + 1}</PageNumber>}
|
||||||
|
|
||||||
<Rhyhorn isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
|
{Template !== null && (
|
||||||
|
<Template
|
||||||
|
isFirstPage={pageIndex === 0}
|
||||||
|
columns={columns as SectionKey[][]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{showBreakLine && <PageBreakLine $pageHeight={pageHeight} />}
|
{showBreakLine && <PageBreakLine $pageHeight={pageHeight} />}
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
|
|||||||
@ -12,7 +12,6 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
Input,
|
Input,
|
||||||
RichInput,
|
RichInput,
|
||||||
Slider,
|
|
||||||
} from "@reactive-resume/ui";
|
} from "@reactive-resume/ui";
|
||||||
import { AnimatePresence, motion } from "framer-motion";
|
import { AnimatePresence, motion } from "framer-motion";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@ -88,13 +87,13 @@ export const CustomSectionDialog = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="url"
|
name="location"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-1">
|
<FormItem className="col-span-1">
|
||||||
<FormLabel>Website</FormLabel>
|
<FormLabel>Location</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<URLInput {...field} />
|
<Input {...field} />
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
@ -102,24 +101,13 @@ export const CustomSectionDialog = () => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
name="level"
|
name="url"
|
||||||
control={form.control}
|
control={form.control}
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<FormItem className="col-span-2">
|
<FormItem className="col-span-2">
|
||||||
<FormLabel>Level</FormLabel>
|
<FormLabel>Website</FormLabel>
|
||||||
<FormControl className="py-2">
|
<FormControl>
|
||||||
<div className="flex items-center gap-x-4">
|
<URLInput {...field} />
|
||||||
<Slider
|
|
||||||
{...field}
|
|
||||||
min={0}
|
|
||||||
max={5}
|
|
||||||
value={[field.value]}
|
|
||||||
orientation="horizontal"
|
|
||||||
onValueChange={(value) => field.onChange(value[0])}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span className="text-base font-bold">{field.value}</span>
|
|
||||||
</div>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage />
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
|
|||||||
@ -16,7 +16,7 @@ type CustomFieldProps = {
|
|||||||
export const CustomField = ({ field, onChange, onRemove }: CustomFieldProps) => {
|
export const CustomField = ({ field, onChange, onRemove }: CustomFieldProps) => {
|
||||||
const controls = useDragControls();
|
const controls = useDragControls();
|
||||||
|
|
||||||
const handleChange = (key: "name" | "value", value: string) =>
|
const handleChange = (key: "icon" | "name" | "value", value: string) =>
|
||||||
onChange({ ...field, [key]: value });
|
onChange({ ...field, [key]: value });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -28,7 +28,7 @@ export const CustomField = ({ field, onChange, onRemove }: CustomFieldProps) =>
|
|||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={{ opacity: 0, y: -50 }}
|
exit={{ opacity: 0, y: -50 }}
|
||||||
>
|
>
|
||||||
<div className="flex items-end justify-between space-x-4">
|
<div className="flex items-end justify-between space-x-2">
|
||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
@ -38,9 +38,15 @@ export const CustomField = ({ field, onChange, onRemove }: CustomFieldProps) =>
|
|||||||
<DotsSixVertical />
|
<DotsSixVertical />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
placeholder="Icon"
|
||||||
|
value={field.icon}
|
||||||
|
className="!ml-0"
|
||||||
|
onChange={(event) => handleChange("icon", event.target.value)}
|
||||||
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
placeholder="Name"
|
placeholder="Name"
|
||||||
className="!ml-2"
|
|
||||||
value={field.name}
|
value={field.name}
|
||||||
onChange={(event) => handleChange("name", event.target.value)}
|
onChange={(event) => handleChange("name", event.target.value)}
|
||||||
/>
|
/>
|
||||||
@ -54,7 +60,7 @@ export const CustomField = ({ field, onChange, onRemove }: CustomFieldProps) =>
|
|||||||
<Button
|
<Button
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
className="!ml-2 shrink-0"
|
className="!ml-0 shrink-0"
|
||||||
onClick={() => onRemove(field.id)}
|
onClick={() => onRemove(field.id)}
|
||||||
>
|
>
|
||||||
<X />
|
<X />
|
||||||
@ -73,7 +79,10 @@ export const CustomFieldsSection = ({ className }: Props) => {
|
|||||||
const customFields = useResumeStore((state) => state.resume.data.basics.customFields);
|
const customFields = useResumeStore((state) => state.resume.data.basics.customFields);
|
||||||
|
|
||||||
const onAddCustomField = () => {
|
const onAddCustomField = () => {
|
||||||
setValue("basics.customFields", [...customFields, { id: createId(), name: "", value: "" }]);
|
setValue("basics.customFields", [
|
||||||
|
...customFields,
|
||||||
|
{ id: createId(), icon: "", name: "", value: "" },
|
||||||
|
]);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onChangeCustomField = (field: ICustomField) => {
|
const onChangeCustomField = (field: ICustomField) => {
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { Button } from "@reactive-resume/ui";
|
import { templatesList } from "@reactive-resume/templates";
|
||||||
|
import { Button, HoverCard, HoverCardContent, HoverCardTrigger } from "@reactive-resume/ui";
|
||||||
import { cn } from "@reactive-resume/utils";
|
import { cn } from "@reactive-resume/utils";
|
||||||
|
|
||||||
import { useResumeStore } from "@/client/stores/resume";
|
import { useResumeStore } from "@/client/stores/resume";
|
||||||
@ -6,9 +7,6 @@ import { useResumeStore } from "@/client/stores/resume";
|
|||||||
import { getSectionIcon } from "../shared/section-icon";
|
import { getSectionIcon } from "../shared/section-icon";
|
||||||
|
|
||||||
export const TemplateSection = () => {
|
export const TemplateSection = () => {
|
||||||
// TODO: Import templates from @reactive-resume/templates
|
|
||||||
const templateList = ["rhyhorn"];
|
|
||||||
|
|
||||||
const setValue = useResumeStore((state) => state.setValue);
|
const setValue = useResumeStore((state) => state.setValue);
|
||||||
const currentTemplate = useResumeStore((state) => state.resume.data.metadata.template);
|
const currentTemplate = useResumeStore((state) => state.resume.data.metadata.template);
|
||||||
|
|
||||||
@ -21,20 +19,31 @@ export const TemplateSection = () => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main className="grid grid-cols-2 gap-y-4">
|
<main className="grid grid-cols-2 gap-4">
|
||||||
{templateList.map((template) => (
|
{templatesList.map(({ id, name }) => (
|
||||||
<Button
|
<HoverCard key={id} openDelay={0} closeDelay={0}>
|
||||||
key={template}
|
<HoverCardTrigger asChild>
|
||||||
variant="outline"
|
<Button
|
||||||
disabled={template === currentTemplate}
|
variant="outline"
|
||||||
onClick={() => setValue("metadata.template", template)}
|
onClick={() => setValue("metadata.template", id)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"flex h-12 items-center justify-center overflow-hidden rounded border text-center text-sm capitalize ring-primary transition-colors hover:bg-secondary-accent focus:outline-none focus:ring-1 disabled:opacity-100",
|
"flex h-12 items-center justify-center overflow-hidden rounded border text-center text-sm capitalize ring-primary transition-colors hover:bg-secondary-accent focus:outline-none focus:ring-1 disabled:opacity-100",
|
||||||
template === currentTemplate && "ring-1",
|
id === currentTemplate && "ring-1",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{template}
|
{name}
|
||||||
</Button>
|
</Button>
|
||||||
|
</HoverCardTrigger>
|
||||||
|
|
||||||
|
<HoverCardContent className="max-w-xs overflow-hidden border-none bg-white p-0">
|
||||||
|
<img
|
||||||
|
alt={name}
|
||||||
|
loading="lazy"
|
||||||
|
src={`/templates/${id}.jpg`}
|
||||||
|
className="aspect-[1/1.4142]"
|
||||||
|
/>
|
||||||
|
</HoverCardContent>
|
||||||
|
</HoverCard>
|
||||||
))}
|
))}
|
||||||
</main>
|
</main>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -48,7 +48,7 @@ export const ThemeSection = () => {
|
|||||||
style={{ backgroundColor: theme.primary }}
|
style={{ backgroundColor: theme.primary }}
|
||||||
/>
|
/>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent asChild className="rounded-lg border-none bg-transparent p-0">
|
<PopoverContent className="rounded-lg border-none bg-transparent p-0">
|
||||||
<HexColorPicker
|
<HexColorPicker
|
||||||
color={theme.primary}
|
color={theme.primary}
|
||||||
onChange={(color) => {
|
onChange={(color) => {
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { CaretDown, MagicWand, Plus, TestTube } from "@phosphor-icons/react";
|
import { CaretDown, Flask, MagicWand, Plus } from "@phosphor-icons/react";
|
||||||
import { createResumeSchema, ResumeDto } from "@reactive-resume/dto";
|
import { createResumeSchema, ResumeDto } from "@reactive-resume/dto";
|
||||||
import { idSchema } from "@reactive-resume/schema";
|
import { idSchema } from "@reactive-resume/schema";
|
||||||
import {
|
import {
|
||||||
@ -265,7 +265,7 @@ export const ResumeDialog = () => {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent side="right" align="center">
|
<DropdownMenuContent side="right" align="center">
|
||||||
<DropdownMenuItem onClick={onCreateSample}>
|
<DropdownMenuItem onClick={onCreateSample}>
|
||||||
<TestTube className="mr-2" />
|
<Flask className="mr-2" />
|
||||||
Create Sample Resume
|
Create Sample Resume
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@ -1,10 +1,12 @@
|
|||||||
|
import { useTemplate } from "@reactive-resume/hooks";
|
||||||
import { ResumeData, SectionKey } from "@reactive-resume/schema";
|
import { ResumeData, SectionKey } from "@reactive-resume/schema";
|
||||||
import { Artboard, PageWrapper, Rhyhorn } from "@reactive-resume/templates";
|
import { Artboard, PageWrapper } from "@reactive-resume/templates";
|
||||||
import { Navigate } from "react-router-dom";
|
import { Navigate } from "react-router-dom";
|
||||||
import { useSessionStorage } from "usehooks-ts";
|
import { useSessionStorage } from "usehooks-ts";
|
||||||
|
|
||||||
export const PrinterPage = () => {
|
export const PrinterPage = () => {
|
||||||
const [resume] = useSessionStorage<ResumeData | null>("resume", null);
|
const [resume] = useSessionStorage<ResumeData | null>("resume", null);
|
||||||
|
const template = useTemplate(resume?.metadata.template);
|
||||||
|
|
||||||
if (!resume) return <Navigate to="/" replace />;
|
if (!resume) return <Navigate to="/" replace />;
|
||||||
|
|
||||||
@ -12,7 +14,9 @@ export const PrinterPage = () => {
|
|||||||
<Artboard resume={resume} style={{ pointerEvents: "auto" }}>
|
<Artboard resume={resume} style={{ pointerEvents: "auto" }}>
|
||||||
{resume.metadata.layout.map((columns, pageIndex) => (
|
{resume.metadata.layout.map((columns, pageIndex) => (
|
||||||
<PageWrapper key={pageIndex} data-page={pageIndex + 1}>
|
<PageWrapper key={pageIndex} data-page={pageIndex + 1}>
|
||||||
<Rhyhorn isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
|
{template !== null && (
|
||||||
|
<template.Component isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
|
||||||
|
)}
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
))}
|
))}
|
||||||
</Artboard>
|
</Artboard>
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { ResumeDto } from "@reactive-resume/dto";
|
import { ResumeDto } from "@reactive-resume/dto";
|
||||||
|
import { useTemplate } from "@reactive-resume/hooks";
|
||||||
import { SectionKey } from "@reactive-resume/schema";
|
import { SectionKey } from "@reactive-resume/schema";
|
||||||
import { Artboard, PageWrapper, Rhyhorn } from "@reactive-resume/templates";
|
import { Artboard, PageWrapper } from "@reactive-resume/templates";
|
||||||
import { Button } from "@reactive-resume/ui";
|
import { Button } from "@reactive-resume/ui";
|
||||||
import { pageSizeMap } from "@reactive-resume/utils";
|
import { pageSizeMap } from "@reactive-resume/utils";
|
||||||
import { Helmet } from "react-helmet-async";
|
import { Helmet } from "react-helmet-async";
|
||||||
@ -15,6 +16,7 @@ import { findResumeByUsernameSlug } from "@/client/services/resume";
|
|||||||
export const PublicResumePage = () => {
|
export const PublicResumePage = () => {
|
||||||
const { title, data: resume } = useLoaderData() as ResumeDto;
|
const { title, data: resume } = useLoaderData() as ResumeDto;
|
||||||
const format = resume.metadata.page.format;
|
const format = resume.metadata.page.format;
|
||||||
|
const template = useTemplate(resume.metadata.template);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@ -29,7 +31,12 @@ export const PublicResumePage = () => {
|
|||||||
<Artboard resume={resume} style={{ pointerEvents: "auto" }}>
|
<Artboard resume={resume} style={{ pointerEvents: "auto" }}>
|
||||||
{resume.metadata.layout.map((columns, pageIndex) => (
|
{resume.metadata.layout.map((columns, pageIndex) => (
|
||||||
<PageWrapper key={pageIndex} data-page={pageIndex + 1}>
|
<PageWrapper key={pageIndex} data-page={pageIndex + 1}>
|
||||||
<Rhyhorn isFirstPage={pageIndex === 0} columns={columns as SectionKey[][]} />
|
{template !== null && (
|
||||||
|
<template.Component
|
||||||
|
isFirstPage={pageIndex === 0}
|
||||||
|
columns={columns as SectionKey[][]}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</PageWrapper>
|
</PageWrapper>
|
||||||
))}
|
))}
|
||||||
</Artboard>
|
</Artboard>
|
||||||
|
|||||||
12
libs/hooks/src/hooks/use-template.ts
Normal file
12
libs/hooks/src/hooks/use-template.ts
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import { templatesList } from "@reactive-resume/templates";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export const useTemplate = (templateId?: string) => {
|
||||||
|
const template = useMemo(() => {
|
||||||
|
return templatesList.find((template) => template.id === templateId);
|
||||||
|
}, [templateId]);
|
||||||
|
|
||||||
|
if (!template || !template.Component) return null;
|
||||||
|
|
||||||
|
return template;
|
||||||
|
};
|
||||||
@ -1,4 +1,5 @@
|
|||||||
export * from "./hooks/use-breakpoint";
|
export * from "./hooks/use-breakpoint";
|
||||||
export * from "./hooks/use-form-field";
|
export * from "./hooks/use-form-field";
|
||||||
export * from "./hooks/use-password-toggle";
|
export * from "./hooks/use-password-toggle";
|
||||||
|
export * from "./hooks/use-template";
|
||||||
export * from "./hooks/use-theme";
|
export * from "./hooks/use-theme";
|
||||||
|
|||||||
@ -2,6 +2,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
export const customFieldSchema = z.object({
|
export const customFieldSchema = z.object({
|
||||||
id: z.string().cuid2(),
|
id: z.string().cuid2(),
|
||||||
|
icon: z.string(),
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
value: z.string(),
|
value: z.string(),
|
||||||
});
|
});
|
||||||
|
|||||||
@ -7,7 +7,7 @@ export const customSectionSchema = itemSchema.extend({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
description: z.string(),
|
description: z.string(),
|
||||||
date: z.string(),
|
date: z.string(),
|
||||||
level: z.number().min(0).max(5).default(0),
|
location: z.string(),
|
||||||
summary: z.string(),
|
summary: z.string(),
|
||||||
keywords: z.array(z.string()).default([]),
|
keywords: z.array(z.string()).default([]),
|
||||||
url: urlSchema,
|
url: urlSchema,
|
||||||
@ -22,7 +22,7 @@ export const defaultCustomSection: CustomSection = {
|
|||||||
name: "",
|
name: "",
|
||||||
description: "",
|
description: "",
|
||||||
date: "",
|
date: "",
|
||||||
level: 0,
|
location: "",
|
||||||
summary: "",
|
summary: "",
|
||||||
keywords: [],
|
keywords: [],
|
||||||
url: defaultUrl,
|
url: defaultUrl,
|
||||||
|
|||||||
@ -33,34 +33,48 @@ export const FrameWrapper = ({ children, style }: Props) => {
|
|||||||
setHeight(`${height}px`);
|
setHeight(`${height}px`);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const loadFonts = useCallback(
|
||||||
|
(frame: HTMLIFrameElement) => {
|
||||||
|
if (font.family === "CMU Serif") {
|
||||||
|
return webfontloader.load({
|
||||||
|
classes: false,
|
||||||
|
custom: {
|
||||||
|
families: ["CMU Serif"],
|
||||||
|
urls: ["https://cdn.jsdelivr.net/npm/computer-modern/cmu-serif.min.css"],
|
||||||
|
},
|
||||||
|
context: frame.contentWindow!,
|
||||||
|
fontactive: () => handleResize(frame!),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
webfontloader.load({
|
||||||
|
classes: false,
|
||||||
|
google: { families: [fontString] },
|
||||||
|
context: frame.contentWindow!,
|
||||||
|
fontactive: () => handleResize(frame!),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[font, fontString, handleResize],
|
||||||
|
);
|
||||||
|
|
||||||
|
const loadIconFonts = useCallback((frame: HTMLIFrameElement) => {
|
||||||
|
const document = frame.contentDocument!;
|
||||||
|
|
||||||
|
const link = document.createElement("link");
|
||||||
|
link.type = "text/css";
|
||||||
|
link.rel = "stylesheet";
|
||||||
|
link.href = "https://unpkg.com/@phosphor-icons/web@2.0.3/src/regular/style.css";
|
||||||
|
|
||||||
|
document.head.appendChild(link);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const onLoad = useCallback(() => {
|
const onLoad = useCallback(() => {
|
||||||
if (!frameRef.current) return;
|
if (!frameRef.current) return;
|
||||||
|
|
||||||
handleResize(frameRef.current);
|
handleResize(frameRef.current);
|
||||||
|
loadFonts(frameRef.current);
|
||||||
if (font.family === "CMU Serif") {
|
loadIconFonts(frameRef.current);
|
||||||
return webfontloader.load({
|
}, [frameRef, handleResize, loadFonts, loadIconFonts]);
|
||||||
classes: false,
|
|
||||||
custom: {
|
|
||||||
families: ["CMU Serif"],
|
|
||||||
urls: ["https://cdn.jsdelivr.net/npm/computer-modern/cmu-serif.min.css"],
|
|
||||||
},
|
|
||||||
context: frameRef.current.contentWindow!,
|
|
||||||
fontactive: () => {
|
|
||||||
handleResize(frameRef.current!);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
webfontloader.load({
|
|
||||||
classes: false,
|
|
||||||
google: { families: [fontString] },
|
|
||||||
context: frameRef.current.contentWindow!,
|
|
||||||
fontactive: () => {
|
|
||||||
handleResize(frameRef.current!);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}, [frameRef, font, fontString, handleResize]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onLoad();
|
onLoad();
|
||||||
|
|||||||
@ -41,17 +41,14 @@ export const Shared = css<GlobalStyleProps>`
|
|||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
line-height: calc(var(--line-height) + 1.5rem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
line-height: calc(var(--line-height) + 1rem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
h3 {
|
h3 {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
line-height: calc(var(--line-height) + 0rem);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Paragraphs */
|
/* Paragraphs */
|
||||||
@ -67,7 +64,7 @@ export const Shared = css<GlobalStyleProps>`
|
|||||||
|
|
||||||
small {
|
small {
|
||||||
font-size: calc(var(--font-size) - 2px);
|
font-size: calc(var(--font-size) - 2px);
|
||||||
line-height: calc(var(--line-height) - 0.5rem);
|
line-height: calc(var(--line-height) - 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
i,
|
i,
|
||||||
|
|||||||
@ -1 +1,16 @@
|
|||||||
export * from "./rhyhorn";
|
import { TemplateProps } from "../shared";
|
||||||
|
import { Rhyhorn } from "./rhyhorn";
|
||||||
|
|
||||||
|
type Template = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
Component: (props: TemplateProps) => JSX.Element;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const templatesList: Template[] = [
|
||||||
|
{
|
||||||
|
id: "rhyhorn",
|
||||||
|
name: "Rhyhorn",
|
||||||
|
Component: Rhyhorn,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import { References } from "./sections/references";
|
|||||||
import { Skills } from "./sections/skills";
|
import { Skills } from "./sections/skills";
|
||||||
import { Summary } from "./sections/summary";
|
import { Summary } from "./sections/summary";
|
||||||
import { Volunteer } from "./sections/volunteer";
|
import { Volunteer } from "./sections/volunteer";
|
||||||
import { RhyhornStyles } from "./style";
|
import { RhyhornWrapper } from "./style";
|
||||||
|
|
||||||
const sectionMap: Partial<Record<SectionKey, () => React.ReactNode>> = {
|
const sectionMap: Partial<Record<SectionKey, () => React.ReactNode>> = {
|
||||||
summary: Summary,
|
summary: Summary,
|
||||||
@ -44,7 +44,7 @@ const getSection = (id: SectionKey) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const Rhyhorn = ({ isFirstPage, columns }: TemplateProps) => (
|
export const Rhyhorn = ({ isFirstPage, columns }: TemplateProps) => (
|
||||||
<RhyhornStyles>
|
<RhyhornWrapper>
|
||||||
{isFirstPage && <Header />}
|
{isFirstPage && <Header />}
|
||||||
|
|
||||||
{/* Main */}
|
{/* Main */}
|
||||||
@ -52,5 +52,5 @@ export const Rhyhorn = ({ isFirstPage, columns }: TemplateProps) => (
|
|||||||
|
|
||||||
{/* Sidebar */}
|
{/* Sidebar */}
|
||||||
{columns[1].map(getSection)}
|
{columns[1].map(getSection)}
|
||||||
</RhyhornStyles>
|
</RhyhornWrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Award as IAward } from "@reactive-resume/schema";
|
import { Award as IAward } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { isUrl } from "@reactive-resume/utils";
|
import { isUrl } from "@reactive-resume/utils";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -12,24 +11,27 @@ export const Awards = () => {
|
|||||||
<SectionBase<IAward>
|
<SectionBase<IAward>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
<h6>{item.title}</h6>
|
||||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
|
||||||
<h6>{item.title}</h6>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<h6>{item.title}</h6>
|
|
||||||
)}
|
|
||||||
<p>{item.awarder}</p>
|
<p>{item.awarder}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h6>{item.date}</h6>
|
<h6>{item.date}</h6>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||||
|
footer={(item) => (
|
||||||
|
<div>
|
||||||
|
{isUrl(item.url.href) && (
|
||||||
|
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||||
|
<h6>{item.url.label || item.url.href}</h6>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Certification as ICertification } from "@reactive-resume/schema";
|
import { Certification as ICertification } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { isUrl } from "@reactive-resume/utils";
|
import { isUrl } from "@reactive-resume/utils";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -12,24 +11,27 @@ export const Certifications = () => {
|
|||||||
<SectionBase<ICertification>
|
<SectionBase<ICertification>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
<h6>{item.name}</h6>
|
||||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
|
||||||
<h6>{item.name}</h6>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<h6>{item.name}</h6>
|
|
||||||
)}
|
|
||||||
<p>{item.issuer}</p>
|
<p>{item.issuer}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h6>{item.date}</h6>
|
<h6>{item.date}</h6>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||||
|
footer={(item) => (
|
||||||
|
<div>
|
||||||
|
{isUrl(item.url.href) && (
|
||||||
|
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||||
|
<h6>{item.url.label || item.url.href}</h6>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,7 +6,6 @@ import {
|
|||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { isUrl } from "@reactive-resume/utils";
|
import { isUrl } from "@reactive-resume/utils";
|
||||||
import get from "lodash.get";
|
import get from "lodash.get";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -24,30 +23,32 @@ export const CustomSection = ({ id }: Props) => {
|
|||||||
<SectionBase<ICustomSection>
|
<SectionBase<ICustomSection>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item: CustomSectionItem) => (
|
header={(item: CustomSectionItem) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
<h6>{item.name}</h6>
|
||||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
|
||||||
<h6>{item.name}</h6>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<h6>{item.name}</h6>
|
|
||||||
)}
|
|
||||||
<p>{item.description}</p>
|
<p>{item.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h6>{item.date}</h6>
|
<h6>{item.date}</h6>
|
||||||
<div className="rating">
|
<p>{item.location}</p>
|
||||||
{Array.from({ length: item.level }).map((_, i) => (
|
|
||||||
<span key={i} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
main={(item: CustomSectionItem) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
main={(item: CustomSectionItem) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||||
footer={(item: CustomSectionItem) => <small>{item.keywords.join(", ")}</small>}
|
footer={(item: CustomSectionItem) => (
|
||||||
|
<>
|
||||||
|
<small>{item.keywords.join(", ")}</small>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{isUrl(item.url.href) && (
|
||||||
|
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||||
|
<h6>{item.url.label || item.url.href}</h6>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Education as IEducation } from "@reactive-resume/schema";
|
import { Education as IEducation } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { isUrl } from "@reactive-resume/utils";
|
import { isUrl } from "@reactive-resume/utils";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -12,15 +11,9 @@ export const Education = () => {
|
|||||||
<SectionBase<IEducation>
|
<SectionBase<IEducation>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
<h6>{item.institution}</h6>
|
||||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
|
||||||
<h6>{item.institution}</h6>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<h6>{item.institution}</h6>
|
|
||||||
)}
|
|
||||||
<p>{item.area}</p>
|
<p>{item.area}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -31,9 +24,18 @@ export const Education = () => {
|
|||||||
{item.score ? ` | ${item.score}` : ""}
|
{item.score ? ` | ${item.score}` : ""}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||||
|
footer={(item) => (
|
||||||
|
<div>
|
||||||
|
{isUrl(item.url.href) && (
|
||||||
|
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||||
|
<h6>{item.url.label || item.url.href}</h6>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Experience as IExperience } from "@reactive-resume/schema";
|
import { Experience as IExperience } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { isUrl } from "@reactive-resume/utils";
|
import { isUrl } from "@reactive-resume/utils";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -12,15 +11,9 @@ export const Experience = () => {
|
|||||||
<SectionBase<IExperience>
|
<SectionBase<IExperience>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
<h6>{item.company}</h6>
|
||||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
|
||||||
<h6>{item.company}</h6>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<h6>{item.company}</h6>
|
|
||||||
)}
|
|
||||||
<p>{item.position}</p>
|
<p>{item.position}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -28,9 +21,18 @@ export const Experience = () => {
|
|||||||
<h6>{item.date}</h6>
|
<h6>{item.date}</h6>
|
||||||
<p>{item.location}</p>
|
<p>{item.location}</p>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||||
|
footer={(item) => (
|
||||||
|
<div>
|
||||||
|
{isUrl(item.url.href) && (
|
||||||
|
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||||
|
<h6>{item.url.label || item.url.href}</h6>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,11 +1,12 @@
|
|||||||
import { Picture, useStore } from "@reactive-resume/templates";
|
import { Picture, useStore } from "@reactive-resume/templates";
|
||||||
|
import { isUrl } from "@reactive-resume/utils";
|
||||||
|
|
||||||
export const Header = () => {
|
export const Header = () => {
|
||||||
const basics = useStore((state) => state.basics);
|
const basics = useStore((state) => state.basics);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="header">
|
<div className="header">
|
||||||
{basics.picture.url && !basics.picture.effects.hidden && (
|
{isUrl(basics.picture.url) && !basics.picture.effects.hidden && (
|
||||||
<Picture
|
<Picture
|
||||||
alt={basics.name}
|
alt={basics.name}
|
||||||
src={basics.picture.url}
|
src={basics.picture.url}
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Interest as IInterest } from "@reactive-resume/schema";
|
import { Interest as IInterest } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -11,14 +10,14 @@ export const Interests = () => {
|
|||||||
<SectionBase<IInterest>
|
<SectionBase<IInterest>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<h6>{item.name}</h6>
|
<h6>{item.name}</h6>
|
||||||
<p>{item.keywords.join(", ")}</p>
|
<p>{item.keywords.join(", ")}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div />
|
<div />
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Language as ILanguage } from "@reactive-resume/schema";
|
import { Language as ILanguage } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { getCEFRLevel } from "@reactive-resume/utils";
|
import { getCEFRLevel } from "@reactive-resume/utils";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -12,14 +11,14 @@ export const Languages = () => {
|
|||||||
<SectionBase<ILanguage>
|
<SectionBase<ILanguage>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<h6>{item.name}</h6>
|
<h6>{item.name}</h6>
|
||||||
<p>{item.fluency || getCEFRLevel(item.fluencyLevel)}</p>
|
<p>{item.fluency || getCEFRLevel(item.fluencyLevel)}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div />
|
<div />
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Profile as IProfile } from "@reactive-resume/schema";
|
import { Profile as IProfile } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { isUrl } from "@reactive-resume/utils";
|
import { isUrl } from "@reactive-resume/utils";
|
||||||
import { Fragment } from "react";
|
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
@ -18,7 +17,7 @@ export const Profiles = () => {
|
|||||||
<SectionBase<IProfile>
|
<SectionBase<IProfile>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{item.icon && (
|
{item.icon && (
|
||||||
<i>
|
<i>
|
||||||
@ -42,7 +41,7 @@ export const Profiles = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div />
|
<div />
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Project as IProject } from "@reactive-resume/schema";
|
import { Project as IProject } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { isUrl } from "@reactive-resume/utils";
|
import { isUrl } from "@reactive-resume/utils";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -12,25 +11,31 @@ export const Projects = () => {
|
|||||||
<SectionBase<IProject>
|
<SectionBase<IProject>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
<h6>{item.name}</h6>
|
||||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
|
||||||
<h6>{item.name}</h6>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<h6>{item.name}</h6>
|
|
||||||
)}
|
|
||||||
<p>{item.description}</p>
|
<p>{item.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h6>{item.date}</h6>
|
<h6>{item.date}</h6>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||||
footer={(item) => <small>{item.keywords.join(", ")}</small>}
|
footer={(item) => (
|
||||||
|
<>
|
||||||
|
<small>{item.keywords.join(", ")}</small>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{isUrl(item.url.href) && (
|
||||||
|
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||||
|
<h6>{item.url.label || item.url.href}</h6>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Publication as IPublication } from "@reactive-resume/schema";
|
import { Publication as IPublication } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { isUrl } from "@reactive-resume/utils";
|
import { isUrl } from "@reactive-resume/utils";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -12,24 +11,27 @@ export const Publications = () => {
|
|||||||
<SectionBase<IPublication>
|
<SectionBase<IPublication>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
<h6>{item.name}</h6>
|
||||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
|
||||||
<h6>{item.name}</h6>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<h6>{item.name}</h6>
|
|
||||||
)}
|
|
||||||
<p>{item.publisher}</p>
|
<p>{item.publisher}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<h6>{item.date}</h6>
|
<h6>{item.date}</h6>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||||
|
footer={(item) => (
|
||||||
|
<div>
|
||||||
|
{isUrl(item.url.href) && (
|
||||||
|
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||||
|
<h6>{item.url.label || item.url.href}</h6>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Reference as IReference } from "@reactive-resume/schema";
|
import { Reference as IReference } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { isUrl } from "@reactive-resume/utils";
|
import { isUrl } from "@reactive-resume/utils";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -12,22 +11,25 @@ export const References = () => {
|
|||||||
<SectionBase<IReference>
|
<SectionBase<IReference>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
<h6>{item.name}</h6>
|
||||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
|
||||||
<h6>{item.name}</h6>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<h6>{item.name}</h6>
|
|
||||||
)}
|
|
||||||
<p>{item.description}</p>
|
<p>{item.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div />
|
<div />
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||||
|
footer={(item) => (
|
||||||
|
<div>
|
||||||
|
{isUrl(item.url.href) && (
|
||||||
|
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||||
|
<h6>{item.url.label || item.url.href}</h6>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,6 +1,5 @@
|
|||||||
import { Skill as ISkill } from "@reactive-resume/schema";
|
import { Skill as ISkill } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -11,14 +10,14 @@ export const Skills = () => {
|
|||||||
<SectionBase<ISkill>
|
<SectionBase<ISkill>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<h6>{item.name}</h6>
|
<h6>{item.name}</h6>
|
||||||
<p>{item.description}</p>
|
<p>{item.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div />
|
<div />
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
footer={(item) => <small>{item.keywords.join(", ")}</small>}
|
footer={(item) => <small>{item.keywords.join(", ")}</small>}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,7 +1,5 @@
|
|||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
|
|
||||||
import { Heading } from "../shared/heading";
|
|
||||||
|
|
||||||
export const Summary = () => {
|
export const Summary = () => {
|
||||||
const section = useStore((state) => state.sections.summary);
|
const section = useStore((state) => state.sections.summary);
|
||||||
|
|
||||||
@ -9,7 +7,7 @@ export const Summary = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className={`section section__${section.id}`}>
|
<section id={section.id} className={`section section__${section.id}`}>
|
||||||
<Heading>{section.name}</Heading>
|
<h4 className="section__heading">{section.name}</h4>
|
||||||
|
|
||||||
<main className="section__item-content">
|
<main className="section__item-content">
|
||||||
<div
|
<div
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
import { Volunteer as IVolunteer } from "@reactive-resume/schema";
|
import { Volunteer as IVolunteer } from "@reactive-resume/schema";
|
||||||
import { useStore } from "@reactive-resume/templates";
|
import { useStore } from "@reactive-resume/templates";
|
||||||
import { isUrl } from "@reactive-resume/utils";
|
import { isUrl } from "@reactive-resume/utils";
|
||||||
import { Fragment } from "react";
|
|
||||||
|
|
||||||
import { SectionBase } from "../shared/section-base";
|
import { SectionBase } from "../shared/section-base";
|
||||||
|
|
||||||
@ -12,15 +11,9 @@ export const Volunteer = () => {
|
|||||||
<SectionBase<IVolunteer>
|
<SectionBase<IVolunteer>
|
||||||
section={section}
|
section={section}
|
||||||
header={(item) => (
|
header={(item) => (
|
||||||
<Fragment>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{isUrl(item.url.href) ? (
|
<h6>{item.organization}</h6>
|
||||||
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
|
||||||
<h6>{item.organization}</h6>
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
<h6>{item.organization}</h6>
|
|
||||||
)}
|
|
||||||
<p>{item.position}</p>
|
<p>{item.position}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -28,9 +21,18 @@ export const Volunteer = () => {
|
|||||||
<h6>{item.date}</h6>
|
<h6>{item.date}</h6>
|
||||||
<p>{item.location}</p>
|
<p>{item.location}</p>
|
||||||
</div>
|
</div>
|
||||||
</Fragment>
|
</>
|
||||||
)}
|
)}
|
||||||
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
main={(item) => <div dangerouslySetInnerHTML={{ __html: item.summary }} />}
|
||||||
|
footer={(item) => (
|
||||||
|
<div>
|
||||||
|
{isUrl(item.url.href) && (
|
||||||
|
<a href={item.url.href} target="_blank" rel="noopener noreferrer nofollow">
|
||||||
|
<h6>{item.url.label || item.url.href}</h6>
|
||||||
|
</a>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,10 +0,0 @@
|
|||||||
import styled from "styled-components";
|
|
||||||
|
|
||||||
export const Heading = styled.h4`
|
|
||||||
font-size: 0.9rem;
|
|
||||||
line-height: 1.2rem;
|
|
||||||
padding-bottom: 2px;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
text-transform: uppercase;
|
|
||||||
border-bottom: 1px solid var(--color-text);
|
|
||||||
`;
|
|
||||||
@ -1,8 +1,6 @@
|
|||||||
import { Item, SectionItem, SectionWithItem } from "@reactive-resume/schema";
|
import { Item, SectionItem, SectionWithItem } from "@reactive-resume/schema";
|
||||||
import { ItemGrid } from "@reactive-resume/templates";
|
import { ItemGrid } from "@reactive-resume/templates";
|
||||||
|
|
||||||
import { Heading } from "./heading";
|
|
||||||
|
|
||||||
type Props<T extends Item> = {
|
type Props<T extends Item> = {
|
||||||
section: SectionWithItem<T>;
|
section: SectionWithItem<T>;
|
||||||
header?: (item: T) => React.ReactNode;
|
header?: (item: T) => React.ReactNode;
|
||||||
@ -15,7 +13,7 @@ export const SectionBase = <T extends SectionItem>({ section, header, main, foot
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<section id={section.id} className={`section section__${section.id}`}>
|
<section id={section.id} className={`section section__${section.id}`}>
|
||||||
<Heading>{section.name}</Heading>
|
<h4 className="section__heading">{section.name}</h4>
|
||||||
|
|
||||||
<ItemGrid className="section__items" $columns={section.columns}>
|
<ItemGrid className="section__items" $columns={section.columns}>
|
||||||
{section.items
|
{section.items
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
export const RhyhornStyles = styled.div`
|
export const RhyhornWrapper = styled.div`
|
||||||
display: grid;
|
display: grid;
|
||||||
row-gap: 16px;
|
row-gap: 16px;
|
||||||
|
|
||||||
@ -21,13 +21,17 @@ export const RhyhornStyles = styled.div`
|
|||||||
line-height: calc(var(--line-height) + 0.5rem);
|
line-height: calc(var(--line-height) + 0.5rem);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__headline {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
|
||||||
&__meta {
|
&__meta {
|
||||||
font-size: 0.875rem;
|
font-size: 0.875rem;
|
||||||
line-height: var(--line-height);
|
line-height: var(--line-height);
|
||||||
|
|
||||||
span {
|
span {
|
||||||
padding: 0 6px;
|
padding: 0 6px;
|
||||||
border-right: 1px solid var(--color-primary);
|
border-right: 1px solid currentColor;
|
||||||
|
|
||||||
&:first-child {
|
&:first-child {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
@ -41,6 +45,16 @@ export const RhyhornStyles = styled.div`
|
|||||||
}
|
}
|
||||||
|
|
||||||
.section {
|
.section {
|
||||||
|
&__heading {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
line-height: 1.2rem;
|
||||||
|
padding-bottom: 2px;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
color: var(--color-primary);
|
||||||
|
border-bottom: 1px solid var(--color-text);
|
||||||
|
}
|
||||||
|
|
||||||
&__item {
|
&__item {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
@ -10,16 +10,18 @@ export const HoverCardContent = forwardRef<
|
|||||||
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
React.ElementRef<typeof HoverCardPrimitive.Content>,
|
||||||
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
React.ComponentPropsWithoutRef<typeof HoverCardPrimitive.Content>
|
||||||
>(({ className, align = "center", sideOffset = 6, ...props }, ref) => (
|
>(({ className, align = "center", sideOffset = 6, ...props }, ref) => (
|
||||||
<HoverCardPrimitive.Content
|
<HoverCardPrimitive.Portal>
|
||||||
ref={ref}
|
<HoverCardPrimitive.Content
|
||||||
align={align}
|
ref={ref}
|
||||||
sideOffset={sideOffset}
|
align={align}
|
||||||
className={cn(
|
sideOffset={sideOffset}
|
||||||
"z-50 rounded-md border bg-background p-4 text-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
className={cn(
|
||||||
className,
|
"z-50 rounded-sm border bg-background p-4 text-foreground shadow-md outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
|
||||||
)}
|
className,
|
||||||
{...props}
|
)}
|
||||||
/>
|
{...props}
|
||||||
|
/>
|
||||||
|
</HoverCardPrimitive.Portal>
|
||||||
));
|
));
|
||||||
|
|
||||||
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
HoverCardContent.displayName = HoverCardPrimitive.Content.displayName;
|
||||||
|
|||||||
@ -312,16 +312,6 @@ export const fonts: Font[] = [
|
|||||||
"http://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfMZhrib2Bg-4.ttf",
|
"http://fonts.gstatic.com/s/inter/v13/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMw2boKoduKmMEVuLyfMZhrib2Bg-4.ttf",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
family: "Material Icons",
|
|
||||||
category: "monospace",
|
|
||||||
subsets: ["latin"],
|
|
||||||
variants: ["regular"],
|
|
||||||
files: {
|
|
||||||
regular:
|
|
||||||
"http://fonts.gstatic.com/s/materialicons/v140/flUhRq6tzZclQEJ-Vdg-IuiaDsNZIhI8tIHh.ttf",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
family: "Roboto Mono",
|
family: "Roboto Mono",
|
||||||
category: "monospace",
|
category: "monospace",
|
||||||
|
|||||||
@ -68,14 +68,14 @@
|
|||||||
"@types/passport-github2": "^1.2.8",
|
"@types/passport-github2": "^1.2.8",
|
||||||
"@types/passport-google-oauth20": "^2.0.13",
|
"@types/passport-google-oauth20": "^2.0.13",
|
||||||
"@types/passport-local": "^1.0.37",
|
"@types/passport-local": "^1.0.37",
|
||||||
"@types/react": "18.2.35",
|
"@types/react": "18.2.36",
|
||||||
"@types/react-dom": "18.2.14",
|
"@types/react-dom": "18.2.14",
|
||||||
"@types/react-is": "18.2.3",
|
"@types/react-is": "18.2.3",
|
||||||
"@types/retry": "^0.12.4",
|
"@types/retry": "^0.12.4",
|
||||||
"@types/styled-components": "5.1.29",
|
"@types/styled-components": "5.1.29",
|
||||||
"@types/webfontloader": "^1.6.36",
|
"@types/webfontloader": "^1.6.36",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.9.1",
|
"@typescript-eslint/eslint-plugin": "^6.10.0",
|
||||||
"@typescript-eslint/parser": "^6.9.1",
|
"@typescript-eslint/parser": "^6.10.0",
|
||||||
"@vitejs/plugin-react": "~4.1.1",
|
"@vitejs/plugin-react": "~4.1.1",
|
||||||
"@vitejs/plugin-react-swc": "~3.4.1",
|
"@vitejs/plugin-react-swc": "~3.4.1",
|
||||||
"@vitest/coverage-v8": "^0.34.6",
|
"@vitest/coverage-v8": "^0.34.6",
|
||||||
@ -201,7 +201,7 @@
|
|||||||
"nestjs-prisma": "^0.22.0",
|
"nestjs-prisma": "^0.22.0",
|
||||||
"nestjs-zod": "^3.0.0",
|
"nestjs-zod": "^3.0.0",
|
||||||
"nodemailer": "^6.9.7",
|
"nodemailer": "^6.9.7",
|
||||||
"openai": "^4.15.4",
|
"openai": "^4.16.0",
|
||||||
"otplib": "^12.0.1",
|
"otplib": "^12.0.1",
|
||||||
"papaparse": "^5.4.1",
|
"papaparse": "^5.4.1",
|
||||||
"passport": "^0.6.0",
|
"passport": "^0.6.0",
|
||||||
|
|||||||
840
pnpm-lock.yaml
generated
840
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user