Skip to content

Commit

Permalink
feat: add watermarking (#576)
Browse files Browse the repository at this point in the history
* feat: add watermarking

* fix: proper export

* fix

* fix: imports

* fix: duplicate

* fix: asset prefix

* feat: limit to trial and datarooms plan

* feat: improve watermarking

* feat: add ip address and cleanup types
  • Loading branch information
mfts authored Aug 31, 2024
1 parent c9622a3 commit 13cd7c5
Show file tree
Hide file tree
Showing 28 changed files with 1,182 additions and 20 deletions.
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"></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

0 comments on commit 13cd7c5

Please sign in to comment.