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

feat: add watermarking #576

Merged
merged 9 commits into from
Aug 31, 2024
Merged
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
6 changes: 5 additions & 1 deletion components/links/link-sheet/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import {
import { useAnalytics } from "@/lib/analytics";
import { usePlan } from "@/lib/swr/use-billing";
import { useDomains } from "@/lib/swr/use-domains";
import { LinkWithViews } from "@/lib/types";
import { LinkWithViews, WatermarkConfig } from "@/lib/types";
import { convertDataUrlToFile, uploadImage } from "@/lib/utils";

import DomainSection from "./domain-section";
Expand Down Expand Up @@ -54,6 +54,8 @@ export const DEFAULT_LINK_PROPS = (linkType: LinkType) => ({
enableAgreement: false,
agreementId: null,
showBanner: linkType === LinkType.DOCUMENT_LINK ? true : false,
enableWatermark: false,
watermarkConfig: null,
});

export type DEFAULT_LINK_TYPE = {
Expand Down Expand Up @@ -81,6 +83,8 @@ export type DEFAULT_LINK_TYPE = {
enableAgreement: boolean; // agreement
agreementId: string | null;
showBanner: boolean;
enableWatermark: boolean;
watermarkConfig: WatermarkConfig | null;
};

export default function LinkSheet({
Expand Down
6 changes: 6 additions & 0 deletions components/links/link-sheet/link-options.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import useLimits from "@/lib/swr/use-limits";
import AgreementSection from "./agreement-section";
import QuestionSection from "./question-section";
import ScreenshotProtectionSection from "./screenshot-protection-section";
import WatermarkSection from "./watermark-section";

export type LinkUpgradeOptions = {
state: boolean;
Expand Down Expand Up @@ -131,6 +132,11 @@ export const LinkOptions = ({
}
handleUpgradeStateChange={handleUpgradeStateChange}
/>
<WatermarkSection
{...{ data, setData }}
isAllowed={isTrial || isDatarooms}
handleUpgradeStateChange={handleUpgradeStateChange}
/>
<AgreementSection
{...{ data, setData }}
isAllowed={isTrial || isDatarooms}
Expand Down
309 changes: 309 additions & 0 deletions components/links/link-sheet/watermark-panel/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
import React, { useEffect, useState } from "react";

import { motion } from "framer-motion";
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 {
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>
<SheetHeader>
<SheetTitle>Watermark Configuration</SheetTitle>
<SheetDescription>
Configure the watermark settings for your document.
</SheetDescription>
</SheetHeader>

<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">
{["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>

<SheetFooter>
<Button onClick={validateAndSave}>Save Watermark</Button>
</SheetFooter>
</SheetContent>
</Sheet>
);
}
Loading