Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(input validation): fields are accepted infinity digits, form added #2235

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
import { t } from "@lingui/macro";
import type { AspectRatio } from "@reactive-resume/ui";
import { Checkbox, Input, Label, ToggleGroup, ToggleGroupItem, Tooltip } from "@reactive-resume/ui";
import {
Button,
Checkbox,
Input,
Label,
ToggleGroup,
ToggleGroupItem,
Tooltip,
} from "@reactive-resume/ui";
import type { FormEvent } from "react";
import { useMemo } from "react";

import { useResumeStore } from "@/client/stores/resume";
Expand All @@ -18,60 +26,84 @@ const ratioToStringMap = {
"1.33": "horizontal",
} as const;

type AspectRatio = keyof typeof stringToRatioMap;

// Border Radius Helpers
const stringToBorderRadiusMap = {
square: 0,
rounded: 6,
circle: 9999,
circle: 9998,
};

const borderRadiusToStringMap = {
"0": "square",
"6": "rounded",
"9999": "circle",
"9998": "circle",
};

type AspectRatioType = keyof typeof stringToRatioMap;
type BorderRadius = keyof typeof stringToBorderRadiusMap;
type PictureOptionsProps = {
handleOpen: () => void;
};

export const PictureOptions = () => {
export const PictureOptions = ({ handleOpen }: PictureOptionsProps) => {
const setValue = useResumeStore((state) => state.setValue);
const setValues = useResumeStore((state) => state.setValues);
const picture = useResumeStore((state) => state.resume.data.basics.picture);

const aspectRatio = useMemo(() => {
const ratio = picture.aspectRatio.toString() as keyof typeof ratioToStringMap;
return ratioToStringMap[ratio];
}, [picture.aspectRatio]);

const onAspectRatioChange = (value: string) => {
if (!value) return;
setValue("basics.picture.aspectRatio", stringToRatioMap[value as AspectRatio]);
};

const borderRadius = useMemo(() => {
const radius = picture.borderRadius.toString() as keyof typeof borderRadiusToStringMap;
return borderRadiusToStringMap[radius];
}, [picture.borderRadius]);

const onBorderRadiusChange = (value: string) => {
const onBorderRadiusChange = (value: BorderRadius | "") => {
if (!value) return;
setValue("basics.picture.borderRadius", stringToBorderRadiusMap[value as BorderRadius]);
setValue("basics.picture.borderRadius", stringToBorderRadiusMap[value]);
};

const onAspectRatioChange = (value: AspectRatioType | "") => {
if (!value) return;
setValue("basics.picture.aspectRatio", stringToRatioMap[value]);
};

const handleFormSubmit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const formData = new FormData(event.target as HTMLFormElement);
const payload = [
{
path: "basics.picture.borderRadius",
value: Number(formData.get("borderRadius")),
},
{
path: "basics.picture.aspectRatio",
value: Number(formData.get("aspectRatio")),
},
{
path: "basics.picture.size",
value: Number(formData.get("size")),
},
];
setValues(payload);
handleOpen();
};

return (
<div className="flex flex-col gap-y-5">
<form className="flex flex-col gap-y-5" onSubmit={handleFormSubmit}>
<div className="grid grid-cols-3 items-center gap-x-6">
<Label htmlFor="picture.size">{t`Size (in px)`}</Label>
<Input
min={0}
max={128}
type="number"
id="picture.size"
placeholder="128"
value={picture.size}
defaultValue={picture.size}
className="col-span-2"
onChange={(event) => {
setValue("basics.picture.size", event.target.valueAsNumber);
}}
name={"size"}
/>
</div>

Expand All @@ -85,37 +117,34 @@ export const PictureOptions = () => {
onValueChange={onAspectRatioChange}
>
<Tooltip content={t`Square`}>
<ToggleGroupItem value="square">
<ToggleGroupItem value="square" type="button">
<div className="size-3 border border-foreground" />
</ToggleGroupItem>
</Tooltip>

<Tooltip content={t`Horizontal`}>
<ToggleGroupItem value="horizontal">
<ToggleGroupItem value="horizontal" type="button">
<div className="h-2 w-3 border border-foreground" />
</ToggleGroupItem>
</Tooltip>

<Tooltip content={t`Portrait`}>
<ToggleGroupItem value="portrait">
<ToggleGroupItem value="portrait" type="button">
<div className="h-3 w-2 border border-foreground" />
</ToggleGroupItem>
</Tooltip>
</ToggleGroup>

<Input
key={picture.aspectRatio}
min={0.1}
max={2}
step={0.05}
type="number"
className="w-[60px]"
id="picture.aspectRatio"
value={picture.aspectRatio}
onChange={(event) => {
if (!event.target.valueAsNumber) return;
if (Number.isNaN(event.target.valueAsNumber)) return;
setValue("basics.picture.aspectRatio", event.target.valueAsNumber);
}}
defaultValue={picture.aspectRatio}
name={"aspectRatio"}
/>
</div>
</div>
Expand All @@ -130,35 +159,34 @@ export const PictureOptions = () => {
onValueChange={onBorderRadiusChange}
>
<Tooltip content={t`Square`}>
<ToggleGroupItem value="square">
<ToggleGroupItem value="square" type="button">
<div className="size-3 border border-foreground" />
</ToggleGroupItem>
</Tooltip>

<Tooltip content={t`Rounded`}>
<ToggleGroupItem value="rounded">
<ToggleGroupItem value="rounded" type="button">
<div className="size-3 rounded-sm border border-foreground" />
</ToggleGroupItem>
</Tooltip>

<Tooltip content={t`Circle`}>
<ToggleGroupItem value="circle">
<ToggleGroupItem value="circle" type="button">
<div className="size-3 rounded-full border border-foreground" />
</ToggleGroupItem>
</Tooltip>
</ToggleGroup>

<Input
key={picture.borderRadius}
min={0}
step={2}
max={9999}
max={9998}
type="number"
className="w-[60px]"
id="picture.borderRadius"
value={picture.borderRadius}
onChange={(event) => {
setValue("basics.picture.borderRadius", event.target.valueAsNumber);
}}
defaultValue={picture.borderRadius}
name={"borderRadius"}
/>
</div>
</div>
Expand Down Expand Up @@ -204,6 +232,7 @@ export const PictureOptions = () => {
</div>
</div>
</div>
</div>
<Button>{t`Save`}</Button>
</form>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
} from "@reactive-resume/ui";
import { cn } from "@reactive-resume/utils";
import { motion } from "framer-motion";
import { useMemo, useRef } from "react";
import { useMemo, useRef, useState } from "react";
import { z } from "zod";

import { useUploadImage } from "@/client/services/storage";
Expand All @@ -21,6 +21,7 @@ import { useResumeStore } from "@/client/stores/resume";
import { PictureOptions } from "./options";

export const PictureSection = () => {
const [isOpen, setIsOpen] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const { uploadImage } = useUploadImage();

Expand All @@ -47,6 +48,10 @@ export const PictureSection = () => {
}
};

const handleOpen = () => {
setIsOpen(!isOpen);
};

return (
<div className="flex items-center gap-x-4">
<div className="group relative cursor-pointer" onClick={onAvatarClick}>
Expand Down Expand Up @@ -80,7 +85,7 @@ export const PictureSection = () => {
/>

{isValidUrl && (
<Popover>
<Popover open={isOpen} onOpenChange={handleOpen}>
<PopoverTrigger asChild>
<motion.button
initial={{ opacity: 0 }}
Expand All @@ -92,7 +97,7 @@ export const PictureSection = () => {
</motion.button>
</PopoverTrigger>
<PopoverContent className="w-[360px]">
<PictureOptions />
<PictureOptions handleOpen={handleOpen} />
</PopoverContent>
</Popover>
)}
Expand Down
10 changes: 10 additions & 0 deletions apps/client/src/stores/resume.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type ResumeStore = {

// Actions
setValue: (path: string, value: unknown) => void;
setValues: (payload: { path: string, value: unknown }[]) => void;

// Custom Section Actions
addSection: () => void;
Expand All @@ -40,6 +41,15 @@ export const useResumeStore = create<ResumeStore>()(
void debouncedUpdateResume(JSON.parse(JSON.stringify(state.resume)));
});
},
setValues: (payload: { path: string; value: unknown }[]) => {
set((state) => {
for (const { path, value } of payload) {
state.resume.data = _set(state.resume.data, path, value);
}

void debouncedUpdateResume(JSON.parse(JSON.stringify(state.resume)));
});
},
addSection: () => {
const section: CustomSectionGroup = {
...defaultSection,
Expand Down