mirror of
https://github.com/hanzo-apps/dataroom.git
synced 2026-10-30 14:47:36 +00:00
317 lines
12 KiB
TypeScript
317 lines
12 KiB
TypeScript
import React, { useEffect, useState } from "react";
|
|
|
|
import { motion } from "motion/react";
|
|
import { HexColorInput, HexColorPicker } from "react-colorful";
|
|
import { z } from "zod";
|
|
|
|
import { Button } from "@/components/ui/button";
|
|
import { Checkbox } from "@/components/ui/checkbox";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import {
|
|
Popover,
|
|
PopoverContent,
|
|
PopoverTrigger,
|
|
} from "@/components/ui/popover";
|
|
import { ScrollArea } from "@/components/ui/scroll-area";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import {
|
|
Sheet,
|
|
SheetContent,
|
|
SheetDescription,
|
|
SheetFooter,
|
|
SheetHeader,
|
|
SheetTitle,
|
|
} from "@/components/ui/sheet";
|
|
|
|
import { FADE_IN_ANIMATION_SETTINGS } from "@/lib/constants";
|
|
import { WatermarkConfig, WatermarkConfigSchema } from "@/lib/types";
|
|
|
|
interface WatermarkConfigSheetProps {
|
|
isOpen: boolean;
|
|
onOpenChange: (open: boolean) => void;
|
|
initialConfig: Partial<WatermarkConfig>;
|
|
onSave: (config: WatermarkConfig) => void;
|
|
}
|
|
|
|
export default function WatermarkConfigSheet({
|
|
isOpen,
|
|
onOpenChange,
|
|
initialConfig,
|
|
onSave,
|
|
}: WatermarkConfigSheetProps) {
|
|
const [formValues, setFormValues] =
|
|
useState<Partial<WatermarkConfig>>(initialConfig);
|
|
const [errors, setErrors] = useState<Record<string, string>>({});
|
|
|
|
useEffect(() => {
|
|
setFormValues(initialConfig);
|
|
}, [initialConfig]);
|
|
|
|
const handleInputChange = (
|
|
e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>,
|
|
) => {
|
|
const { name, value } = e.target;
|
|
setFormValues((prevValues) => ({
|
|
...prevValues,
|
|
[name]: value,
|
|
}));
|
|
};
|
|
|
|
const validateAndSave = () => {
|
|
try {
|
|
const validatedData = WatermarkConfigSchema.parse(formValues);
|
|
onSave(validatedData);
|
|
setErrors({});
|
|
onOpenChange(false);
|
|
} catch (error) {
|
|
if (error instanceof z.ZodError) {
|
|
const fieldErrors: Record<string, string> = {};
|
|
error.errors.forEach((err) => {
|
|
if (err.path[0]) {
|
|
fieldErrors[err.path[0] as string] = err.message;
|
|
}
|
|
});
|
|
setErrors(fieldErrors);
|
|
}
|
|
}
|
|
};
|
|
|
|
return (
|
|
<Sheet open={isOpen} onOpenChange={onOpenChange}>
|
|
<SheetContent className="flex h-full flex-col">
|
|
<SheetHeader>
|
|
<SheetTitle>Watermark Configuration</SheetTitle>
|
|
<SheetDescription>
|
|
Configure the watermark settings for your document.
|
|
</SheetDescription>
|
|
</SheetHeader>
|
|
|
|
<ScrollArea className="flex-1">
|
|
<motion.div
|
|
className="relative mt-4 space-y-3"
|
|
{...FADE_IN_ANIMATION_SETTINGS}
|
|
>
|
|
<div className="flex w-full flex-col items-start gap-6 overflow-x-visible pb-4 pt-0">
|
|
<div className="w-full space-y-2">
|
|
<Label htmlFor="watermark-text">Watermark Text</Label>
|
|
<Input
|
|
id="watermark-text"
|
|
type="text"
|
|
name="text"
|
|
placeholder="e.g. Confidential {{email}} {{date}}"
|
|
value={formValues.text || ""}
|
|
onChange={handleInputChange}
|
|
className="focus:ring-inset"
|
|
/>
|
|
<div className="space-x-1 space-y-1">
|
|
{["email", "date", "time", "link", "ipAddress"].map(
|
|
(item) => (
|
|
<Button
|
|
key={item}
|
|
size="sm"
|
|
variant="outline"
|
|
className="h-7 rounded-3xl bg-muted text-sm font-normal text-foreground/80 hover:bg-muted/70"
|
|
onClick={(e) => {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setFormValues((prevValues) => ({
|
|
...prevValues,
|
|
text: `${prevValues.text || ""}{{${item}}}`,
|
|
}));
|
|
}}
|
|
>{`{{${item}}}`}</Button>
|
|
),
|
|
)}
|
|
</div>
|
|
{errors.text && <p className="text-red-500">{errors.text}</p>}
|
|
</div>
|
|
|
|
<div className="w-full space-y-2">
|
|
<div className="relative flex items-center space-x-2">
|
|
<Checkbox
|
|
id="watermark-tiled"
|
|
checked={formValues.isTiled}
|
|
onCheckedChange={(checked) => {
|
|
setFormValues((prevValues) => ({
|
|
...prevValues,
|
|
isTiled: checked === true,
|
|
}));
|
|
}}
|
|
className="mt-0.5 border border-gray-400 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-gray-300 focus-visible:ring-offset-1 disabled:cursor-not-allowed disabled:opacity-50 data-[state=checked]:border-white data-[state=checked]:bg-black data-[state=checked]:text-white"
|
|
/>
|
|
<Label htmlFor="watermark-tiled">Tiled</Label>
|
|
</div>
|
|
{errors.isTiled && (
|
|
<p className="text-red-500">{errors.isTiled}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="w-full space-y-2">
|
|
<Label htmlFor="watermark-position">Position</Label>
|
|
<Select
|
|
name="position"
|
|
value={formValues.position}
|
|
disabled={formValues.isTiled}
|
|
onValueChange={(value) => {
|
|
setFormValues({
|
|
...formValues,
|
|
position: value as WatermarkConfig["position"],
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select position" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="top-left">Top Left</SelectItem>
|
|
<SelectItem value="top-center">Top Center</SelectItem>
|
|
<SelectItem value="top-right">Top Right</SelectItem>
|
|
<SelectItem value="middle-left">Middle Left</SelectItem>
|
|
<SelectItem value="middle-center">Middle Center</SelectItem>
|
|
<SelectItem value="middle-right">Middle Right</SelectItem>
|
|
<SelectItem value="bottom-left">Bottom Left</SelectItem>
|
|
<SelectItem value="bottom-center">Bottom Center</SelectItem>
|
|
<SelectItem value="bottom-right">Bottom Right</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.position && (
|
|
<p className="text-red-500">{errors.position}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="w-full space-y-2">
|
|
<Label htmlFor="watermark-rotation">Rotation</Label>
|
|
<Select
|
|
name="rotation"
|
|
value={formValues.rotation?.toString()}
|
|
onValueChange={(value) => {
|
|
setFormValues({
|
|
...formValues,
|
|
rotation: parseInt(value) as WatermarkConfig["rotation"],
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select rotation" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="0">0°</SelectItem>
|
|
<SelectItem value="30">30°</SelectItem>
|
|
<SelectItem value="45">45°</SelectItem>
|
|
<SelectItem value="90">90°</SelectItem>
|
|
<SelectItem value="180">180°</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.rotation && (
|
|
<p className="text-red-500">{errors.rotation}</p>
|
|
)}
|
|
</div>
|
|
|
|
<div className="w-full space-y-2">
|
|
<Label htmlFor="watermark-color">Text Color</Label>
|
|
<div className="ml-0.5 mr-0.5 flex space-x-1">
|
|
<Popover>
|
|
<PopoverTrigger>
|
|
<div
|
|
className="h-9 w-9 cursor-pointer rounded-md shadow-sm ring-1 ring-muted-foreground hover:ring-1 hover:ring-gray-300 focus:ring-inset"
|
|
style={{ backgroundColor: formValues.color }}
|
|
/>
|
|
</PopoverTrigger>
|
|
<PopoverContent>
|
|
<HexColorPicker
|
|
color={formValues.color || ""}
|
|
onChange={(value) => {
|
|
setFormValues({
|
|
...formValues,
|
|
color: value as WatermarkConfig["color"],
|
|
});
|
|
}}
|
|
/>
|
|
</PopoverContent>
|
|
</Popover>
|
|
<HexColorInput
|
|
className="flex w-full rounded-md border border-input bg-white text-foreground placeholder-muted-foreground focus:border-muted-foreground focus:outline-none focus:ring-inset focus:ring-muted-foreground dark:border-gray-500 dark:bg-gray-800 focus:dark:bg-transparent sm:text-sm"
|
|
color={formValues.color || ""}
|
|
onChange={(value) => {
|
|
setFormValues({
|
|
...formValues,
|
|
color: value as WatermarkConfig["color"],
|
|
});
|
|
}}
|
|
prefixed
|
|
/>
|
|
</div>
|
|
{errors.color && <p className="text-red-500">{errors.color}</p>}
|
|
</div>
|
|
|
|
<div className="flex w-full space-x-4">
|
|
<div className="w-full space-y-2">
|
|
<Label htmlFor="watermark-fontSize">Font Size</Label>
|
|
<Input
|
|
id="watermark-fontSize"
|
|
type="number"
|
|
name="fontSize"
|
|
step="4"
|
|
value={formValues.fontSize}
|
|
onChange={(e) => {
|
|
setFormValues({
|
|
...formValues,
|
|
fontSize: parseInt(
|
|
e.target.value,
|
|
) as WatermarkConfig["fontSize"],
|
|
});
|
|
}}
|
|
className="focus:ring-inset"
|
|
/>
|
|
{errors.fontSize && (
|
|
<p className="text-red-500">{errors.fontSize}</p>
|
|
)}
|
|
</div>
|
|
<div className="w-full space-y-2">
|
|
<Label htmlFor="watermark-opacity">Transparency</Label>
|
|
<Select
|
|
name="opacity"
|
|
value={formValues.opacity?.toString()}
|
|
onValueChange={(value) => {
|
|
setFormValues({
|
|
...formValues,
|
|
opacity: parseFloat(
|
|
value,
|
|
) as WatermarkConfig["opacity"],
|
|
});
|
|
}}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue placeholder="Select transparency" />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="1">No transparency</SelectItem>
|
|
<SelectItem value="0.25">75%</SelectItem>
|
|
<SelectItem value="0.5">50%</SelectItem>
|
|
<SelectItem value="0.75">25%</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
{errors.opacity && (
|
|
<p className="text-red-500">{errors.opacity}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</motion.div>
|
|
</ScrollArea>
|
|
|
|
<SheetFooter className="flex-shrink-0">
|
|
<Button onClick={validateAndSave}>Save Watermark</Button>
|
|
</SheetFooter>
|
|
</SheetContent>
|
|
</Sheet>
|
|
);
|
|
}
|