diff --git a/frontend/public/locales/en-GB/translation.json b/frontend/public/locales/en-GB/translation.json index e363636a2..7d01af4f5 100644 --- a/frontend/public/locales/en-GB/translation.json +++ b/frontend/public/locales/en-GB/translation.json @@ -44,6 +44,7 @@ "editYourNewFiles": "Edit your new file(s)", "close": "Close", "fileSelected": "Selected: {{filename}}", + "chooseFile": "Choose File", "filesSelected": "{{count}} files selected", "files": { "title": "Files", @@ -814,26 +815,180 @@ "submit": "Add Attachments" }, "watermark": { - "tags": "Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo", "title": "Add Watermark", - "header": "Add Watermark", - "customColor": "Custom Text Colour", - "selectText": { - "1": "Select PDF to add watermark to:", - "2": "Watermark Text:", - "3": "Font Size:", - "4": "Rotation (0-360):", - "5": "Width Spacer (Space between each watermark horizontally):", - "6": "Height Spacer (Space between each watermark vertically):", - "7": "Opacity (0% - 100%):", - "8": "Watermark Type:", - "9": "Watermark Image:", - "10": "Convert PDF to PDF-Image" - }, + "desc": "Add text or image watermarks to PDF files", + "completed": "Watermark added", "submit": "Add Watermark", - "type": { - "1": "Text", - "2": "Image" + "filenamePrefix": "watermarked", + "error": { + "failed": "An error occurred while adding watermark to the PDF." + }, + "watermarkType": { + "text": "Text", + "image": "Image" + }, + "settings": { + "type": "Watermark Type", + "text": { + "label": "Watermark Text", + "placeholder": "Enter watermark text" + }, + "image": { + "label": "Watermark Image", + "choose": "Choose Image", + "selected": "Selected: {{filename}}" + }, + "fontSize": "Font Size", + "size": "Size", + "alphabet": "Font/Language", + "color": "Watermark Colour", + "rotation": "Rotation (degrees)", + "opacity": "Opacity (%)", + "spacing": { + "horizontal": "Horizontal Spacing", + "vertical": "Vertical Spacing" + }, + "convertToImage": "Flatten PDF pages to images" + }, + "alphabet": { + "roman": "Roman/Latin", + "arabic": "Arabic", + "japanese": "Japanese", + "korean": "Korean", + "chinese": "Chinese", + "thai": "Thai" + }, + "steps": { + "type": "Watermark Type", + "wording": "Wording", + "textStyle": "Style", + "formatting": "Formatting", + "file": "Watermark File" + }, + "results": { + "title": "Watermark Results" + }, + "tooltip": { + "language": { + "title": "Language Support", + "text": "Choose the appropriate language setting to ensure proper font rendering for your text." + }, + "appearance": { + "title": "Appearance Settings", + "text": "Control how your watermark looks and blends with the document.", + "bullet1": "Rotation: -360° to 360° for angled watermarks", + "bullet2": "Opacity: 0-100% for transparency control", + "bullet3": "Lower opacity creates subtle watermarks" + }, + "spacing": { + "title": "Spacing Control", + "text": "Adjust the spacing between repeated watermarks across the page.", + "bullet1": "Width spacing: Horizontal distance between watermarks", + "bullet2": "Height spacing: Vertical distance between watermarks", + "bullet3": "Higher values create more spread out patterns" + }, + "type": { + "header": { + "title": "Watermark Type Selection" + }, + "description": { + "title": "Choose Your Watermark", + "text": "Select between text or image watermarks based on your needs." + }, + "text": { + "title": "Text Watermarks", + "text": "Perfect for adding copyright notices, company names, or confidentiality labels. Supports multiple languages and custom colours.", + "bullet1": "Customisable fonts and languages", + "bullet2": "Adjustable colours and transparency", + "bullet3": "Ideal for legal or branding text" + }, + "image": { + "title": "Image Watermarks", + "text": "Use logos, stamps, or any image as a watermark. Great for branding and visual identification.", + "bullet1": "Upload any image format", + "bullet2": "Maintains image quality", + "bullet3": "Perfect for logos and stamps" + } + }, + "wording": { + "header": { + "title": "Text Content" + }, + "text": { + "title": "Watermark Text", + "text": "Enter the text that will appear as your watermark across the document.", + "bullet1": "Keep it concise for better readability", + "bullet2": "Common examples: 'CONFIDENTIAL', 'DRAFT', company name", + "bullet3": "Emoji characters are not supported and will be filtered out" + } + }, + "textStyle": { + "header": { + "title": "Text Style" + }, + "color": { + "title": "Colour Selection", + "text": "Choose a colour that provides good contrast with your document content.", + "bullet1": "Light grey (#d3d3d3) for subtle watermarks", + "bullet2": "Black or dark colours for high contrast", + "bullet3": "Custom colours for branding purposes" + }, + "language": { + "title": "Language Support", + "text": "Choose the appropriate language setting to ensure proper font rendering." + } + }, + "file": { + "header": { + "title": "Image Upload" + }, + "upload": { + "title": "Image Selection", + "text": "Upload an image file to use as your watermark.", + "bullet1": "Supports common formats: PNG, JPG, GIF, BMP", + "bullet2": "PNG with transparency works best", + "bullet3": "Higher resolution images maintain quality better" + }, + "recommendations": { + "title": "Best Practices", + "text": "Tips for optimal image watermark results.", + "bullet1": "Use logos or stamps with transparent backgrounds", + "bullet2": "Simple designs work better than complex images", + "bullet3": "Consider the final document size when choosing resolution" + } + }, + "formatting": { + "header": { + "title": "Formatting & Layout" + }, + "size": { + "title": "Size Control", + "text": "Adjust the size of your watermark (text or image).", + "bullet1": "Larger sizes create more prominent watermarks" + }, + "appearance": { + "title": "Appearance Settings", + "text": "Control how your watermark looks and blends with the document.", + "bullet1": "Rotation: -360° to 360° for angled watermarks", + "bullet2": "Opacity: 0-100% for transparency control", + "bullet3": "Lower opacity creates subtle watermarks" + }, + "spacing": { + "title": "Spacing Control", + "text": "Adjust the spacing between repeated watermarks across the page.", + "bullet1": "Horizontal spacing: Distance between watermarks left to right", + "bullet2": "Vertical spacing: Distance between watermarks top to bottom", + "bullet3": "Higher values create more spread out patterns" + }, + "security": { + "title": "Security Option", + "text": "Convert the final PDF to an image-based format for enhanced security.", + "bullet1": "Prevents text selection and copying", + "bullet2": "Makes watermarks harder to remove", + "bullet3": "Results in larger file sizes", + "bullet4": "Best for sensitive or copyrighted content" + } + } } }, "permissions": { diff --git a/frontend/public/locales/en-US/translation.json b/frontend/public/locales/en-US/translation.json index 967d7746d..af7188944 100644 --- a/frontend/public/locales/en-US/translation.json +++ b/frontend/public/locales/en-US/translation.json @@ -43,6 +43,7 @@ "download": "Download", "editYourNewFiles": "Edit your new file(s)", "close": "Close", + "chooseFile": "Choose File", "fileSelected": "Selected: {{filename}}", "filesSelected": "{{count}} files selected", "files": { @@ -717,29 +718,6 @@ "upload": "Add image", "submit": "Add image" }, - "watermark": { - "tags": "Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo", - "title": "Add Watermark", - "header": "Add Watermark", - "customColor": "Custom Text Color", - "selectText": { - "1": "Select PDF to add watermark to:", - "2": "Watermark Text:", - "3": "Font Size:", - "4": "Rotation (0-360):", - "5": "Width Spacer (Space between each watermark horizontally):", - "6": "Height Spacer (Space between each watermark vertically):", - "7": "Opacity (0% - 100%):", - "8": "Watermark Type:", - "9": "Watermark Image:", - "10": "Convert PDF to PDF-Image" - }, - "submit": "Add Watermark", - "type": { - "1": "Text", - "2": "Image" - } - }, "permissions": { "tags": "read,write,edit,print", "title": "Change Permissions", @@ -1742,6 +1720,245 @@ } } }, + "watermark": { + "tags": "Text,repeating,label,own,copyright,trademark,img,jpg,picture,photo", + "title": "Add Watermark", + "desc": "Add text or image watermarks to PDF files", + "header": "Add Watermark", + "completed": "Watermark added", + "submit": "Add Watermark", + "filenamePrefix": "watermarked", + "error": { + "failed": "An error occurred while adding watermark to the PDF." + }, + "watermarkType": { + "text": "Text", + "image": "Image" + }, + "settings": { + "type": "Watermark Type", + "text": { + "label": "Watermark Text", + "placeholder": "Enter watermark text" + }, + "image": { + "label": "Watermark Image", + "choose": "Choose Image", + "selected": "Selected: {{filename}}" + }, + "fontSize": "Font Size", + "alphabet": "Font/Language", + "color": "Watermark Color", + "rotation": "Rotation (degrees)", + "opacity": "Opacity (%)", + "spacing": { + "horizontal": "Horizontal Spacing", + "vertical": "Vertical Spacing" + }, + "convertToImage": "Flatten PDF pages to images" + }, + "alphabet": { + "roman": "Roman/Latin", + "arabic": "Arabic", + "japanese": "Japanese", + "korean": "Korean", + "chinese": "Chinese", + "thai": "Thai" + }, + "steps": { + "type": "Watermark Type", + "wording": "Wording", + "textStyle": "Style", + "file": "Watermark File", + "formatting": "Formatting" + }, + "results": { + "title": "Watermark Results" + }, + "tooltip": { + "language": { + "title": "Language Support", + "text": "Choose the appropriate language setting to ensure proper font rendering for your text." + }, + "appearance": { + "title": "Appearance Settings", + "text": "Control how your watermark looks and blends with the document.", + "bullet1": "Rotation: -360° to 360° for angled watermarks", + "bullet2": "Opacity: 0-100% for transparency control", + "bullet3": "Lower opacity creates subtle watermarks" + }, + "spacing": { + "title": "Spacing Control", + "text": "Adjust the spacing between repeated watermarks across the page.", + "bullet1": "Width spacing: Horizontal distance between watermarks", + "bullet2": "Height spacing: Vertical distance between watermarks", + "bullet3": "Higher values create more spread out patterns" + }, + "type": { + "header": { + "title": "Watermark Type Selection" + }, + "description": { + "title": "Choose Your Watermark", + "text": "Select between text or image watermarks based on your needs." + }, + "text": { + "title": "Text Watermarks", + "text": "Perfect for adding copyright notices, company names, or confidentiality labels. Supports multiple languages and custom colors.", + "bullet1": "Customizable fonts and languages", + "bullet2": "Adjustable colors and transparency", + "bullet3": "Ideal for legal or branding text" + }, + "image": { + "title": "Image Watermarks", + "text": "Use logos, stamps, or any image as a watermark. Great for branding and visual identification.", + "bullet1": "Upload any image format", + "bullet2": "Maintains image quality", + "bullet3": "Perfect for logos and stamps" + } + }, + "content": { + "header": { + "title": "Content Configuration" + }, + "text": { + "title": "Text Settings", + "text": "Configure your text watermark appearance and language support.", + "bullet1": "Enter your watermark text", + "bullet2": "Adjust font size (8-72pt)", + "bullet3": "Select language/script support", + "bullet4": "Choose custom colors" + }, + "language": { + "title": "Language Support", + "text": "Choose the appropriate language setting to ensure proper font rendering for your text.", + "bullet1": "Roman/Latin for Western languages", + "bullet2": "Arabic for Arabic script", + "bullet3": "Japanese, Korean, Chinese for Asian languages", + "bullet4": "Thai for Thai script" + } + }, + "style": { + "header": { + "title": "Style & Positioning" + }, + "appearance": { + "title": "Appearance Settings", + "text": "Control how your watermark looks and blends with the document.", + "bullet1": "Rotation: -360° to 360° for angled watermarks", + "bullet2": "Opacity: 0-100% for transparency control", + "bullet3": "Lower opacity creates subtle watermarks" + }, + "spacing": { + "title": "Spacing Control", + "text": "Adjust the spacing between repeated watermarks across the page.", + "bullet1": "Width spacing: Horizontal distance between watermarks", + "bullet2": "Height spacing: Vertical distance between watermarks", + "bullet3": "Higher values create more spread out patterns" + } + }, + "wording": { + "header": { + "title": "Text Content" + }, + "text": { + "title": "Watermark Text", + "text": "Enter the text that will appear as your watermark across the document.", + "bullet1": "Keep it concise for better readability", + "bullet2": "Common examples: 'CONFIDENTIAL', 'DRAFT', company name", + "bullet3": "Emoji characters are not supported and will be filtered out" + } + }, + "textStyle": { + "header": { + "title": "Text Style" + }, + "language": { + "title": "Language Support", + "text": "Choose the appropriate language setting to ensure proper font rendering.", + "bullet1": "Roman/Latin for Western languages", + "bullet2": "Arabic for Arabic script", + "bullet3": "Japanese, Korean, Chinese for Asian languages", + "bullet4": "Thai for Thai script" + }, + "color": { + "title": "Color Selection", + "text": "Choose a color that provides good contrast with your document content.", + "bullet1": "Light gray (#d3d3d3) for subtle watermarks", + "bullet2": "Black or dark colors for high contrast", + "bullet3": "Custom colors for branding purposes" + } + }, + "file": { + "header": { + "title": "Image Upload" + }, + "upload": { + "title": "Image Selection", + "text": "Upload an image file to use as your watermark.", + "bullet1": "Supports common formats: PNG, JPG, GIF, BMP", + "bullet2": "PNG with transparency works best", + "bullet3": "Higher resolution images maintain quality better" + }, + "recommendations": { + "title": "Best Practices", + "text": "Tips for optimal image watermark results.", + "bullet1": "Use logos or stamps with transparent backgrounds", + "bullet2": "Simple designs work better than complex images", + "bullet3": "Consider the final document size when choosing resolution" + } + }, + "formatting": { + "header": { + "title": "Formatting & Layout" + }, + "size": { + "title": "Size Control", + "text": "Adjust the size of your watermark (text or image).", + "bullet1": "Larger sizes create more prominent watermarks" + }, + "appearance": { + "title": "Appearance Settings", + "text": "Control how your watermark looks and blends with the document.", + "bullet1": "Rotation: -360° to 360° for angled watermarks", + "bullet2": "Opacity: 0-100% for transparency control", + "bullet3": "Lower opacity creates subtle watermarks" + }, + "spacing": { + "title": "Spacing Control", + "text": "Adjust the spacing between repeated watermarks across the page.", + "bullet1": "Horizontal spacing: Distance between watermarks left to right", + "bullet2": "Vertical spacing: Distance between watermarks top to bottom", + "bullet3": "Higher values create more spread out patterns" + }, + "security": { + "title": "Security Option", + "text": "Flatten PDF pages to images for enhanced security.", + "bullet1": "Prevents text selection and copying", + "bullet2": "Makes watermarks harder to remove", + "bullet3": "Results in larger file sizes", + "bullet4": "Best for sensitive or copyrighted content" + } + }, + "advanced": { + "header": { + "title": "Advanced Options" + }, + "conversion": { + "title": "PDF to Image Conversion", + "text": "Convert the final PDF to an image-based format for enhanced security.", + "bullet1": "Prevents text selection and copying", + "bullet2": "Makes watermarks harder to remove", + "bullet3": "Results in larger file sizes", + "bullet4": "Best for sensitive or copyrighted content" + }, + "security": { + "title": "Security Considerations", + "text": "Image-based PDFs provide additional protection against unauthorized editing and content extraction." + } + } + } + }, "removePassword": { "title": "Remove Password", "desc": "Remove password protection from your PDF document.", diff --git a/frontend/src/components/shared/FileUploadButton.tsx b/frontend/src/components/shared/FileUploadButton.tsx new file mode 100644 index 000000000..27f58400f --- /dev/null +++ b/frontend/src/components/shared/FileUploadButton.tsx @@ -0,0 +1,45 @@ +import React, { useRef } from "react"; +import { FileButton, Button } from "@mantine/core"; +import { useTranslation } from "react-i18next"; + +interface FileUploadButtonProps { + file?: File; + onChange: (file: File | null) => void; + accept?: string; + disabled?: boolean; + placeholder?: string; + variant?: "outline" | "filled" | "light" | "default" | "subtle" | "gradient"; + fullWidth?: boolean; +} + +const FileUploadButton = ({ + file, + onChange, + accept = "*/*", + disabled = false, + placeholder, + variant = "outline", + fullWidth = true +}: FileUploadButtonProps) => { + const { t } = useTranslation(); + const resetRef = useRef<() => void>(null); + + const defaultPlaceholder = t('chooseFile', 'Choose File'); + + return ( + + {(props) => ( + + {file ? file.name : (placeholder || defaultPlaceholder)} + + )} + + ); +}; + +export default FileUploadButton; diff --git a/frontend/src/components/shared/Tooltip.tsx b/frontend/src/components/shared/Tooltip.tsx index 0acc78057..c415eddf5 100644 --- a/frontend/src/components/shared/Tooltip.tsx +++ b/frontend/src/components/shared/Tooltip.tsx @@ -2,7 +2,8 @@ import React, { useState, useRef, useEffect } from 'react'; import { createPortal } from 'react-dom'; import { isClickOutside, addEventListenerWithCleanup } from '../../utils/genericUtils'; import { useTooltipPosition } from '../../hooks/useTooltipPosition'; -import { TooltipContent, TooltipTip } from './tooltip/TooltipContent'; +import { TooltipTip } from '../../types/tips'; +import { TooltipContent } from './tooltip/TooltipContent'; import { useSidebarContext } from '../../contexts/SidebarContext'; import styles from './tooltip/Tooltip.module.css' diff --git a/frontend/src/components/shared/tooltip/TooltipContent.tsx b/frontend/src/components/shared/tooltip/TooltipContent.tsx index e3515e0e6..0e347cd1c 100644 --- a/frontend/src/components/shared/tooltip/TooltipContent.tsx +++ b/frontend/src/components/shared/tooltip/TooltipContent.tsx @@ -1,12 +1,6 @@ import React from 'react'; import styles from './Tooltip.module.css'; - -export interface TooltipTip { - title?: string; - description?: string; - bullets?: string[]; - body?: React.ReactNode; -} +import { TooltipTip } from '../../../types/tips'; interface TooltipContentProps { content?: React.ReactNode; diff --git a/frontend/src/components/tools/addWatermark/WatermarkFormatting.tsx b/frontend/src/components/tools/addWatermark/WatermarkFormatting.tsx new file mode 100644 index 000000000..9a267f638 --- /dev/null +++ b/frontend/src/components/tools/addWatermark/WatermarkFormatting.tsx @@ -0,0 +1,83 @@ +import React from "react"; +import { Stack, Checkbox, Group } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters"; +import NumberInputWithUnit from "../shared/NumberInputWithUnit"; + +interface WatermarkFormattingProps { + parameters: AddWatermarkParameters; + onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + disabled?: boolean; +} + +const WatermarkFormatting = ({ parameters, onParameterChange, disabled = false }: WatermarkFormattingProps) => { + const { t } = useTranslation(); + + return ( + + {/* Size - single row */} + onParameterChange('fontSize', typeof value === 'number' ? value : 12)} + unit={parameters.watermarkType === 'text' ? 'pt' : 'px'} + min={1} + disabled={disabled} + /> + + {/* Position & Appearance - 2 per row */} + + onParameterChange('rotation', typeof value === 'number' ? value : 0)} + unit="°" + min={-360} + max={360} + disabled={disabled} + /> + onParameterChange('opacity', typeof value === 'number' ? value : 50)} + unit="%" + min={0} + max={100} + disabled={disabled} + /> + + + {/* Spacing - 2 per row */} + + onParameterChange('widthSpacer', typeof value === 'number' ? value : 50)} + unit="px" + min={0} + max={200} + disabled={disabled} + /> + onParameterChange('heightSpacer', typeof value === 'number' ? value : 50)} + unit="px" + min={0} + max={200} + disabled={disabled} + /> + + + {/* Advanced Options */} + onParameterChange('convertPDFToImage', event.currentTarget.checked)} + disabled={disabled} + /> + + ); +}; + +export default WatermarkFormatting; \ No newline at end of file diff --git a/frontend/src/components/tools/addWatermark/WatermarkImageFile.tsx b/frontend/src/components/tools/addWatermark/WatermarkImageFile.tsx new file mode 100644 index 000000000..6f38ae206 --- /dev/null +++ b/frontend/src/components/tools/addWatermark/WatermarkImageFile.tsx @@ -0,0 +1,29 @@ +import React from "react"; +import { Stack } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters"; +import FileUploadButton from "../../shared/FileUploadButton"; + +interface WatermarkImageFileProps { + parameters: AddWatermarkParameters; + onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + disabled?: boolean; +} + +const WatermarkImageFile = ({ parameters, onParameterChange, disabled = false }: WatermarkImageFileProps) => { + const { t } = useTranslation(); + + return ( + + onParameterChange('watermarkImage', file)} + accept="image/*" + disabled={disabled} + placeholder={t('watermark.settings.image.choose', 'Choose Image')} + /> + + ); +}; + +export default WatermarkImageFile; diff --git a/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx b/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx new file mode 100644 index 000000000..2de9335b0 --- /dev/null +++ b/frontend/src/components/tools/addWatermark/WatermarkStyleSettings.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import { Stack, Text, NumberInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters"; + +interface WatermarkStyleSettingsProps { + parameters: AddWatermarkParameters; + onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + disabled?: boolean; +} + +const WatermarkStyleSettings = ({ parameters, onParameterChange, disabled = false }: WatermarkStyleSettingsProps) => { + const { t } = useTranslation(); + + return ( + + {/* Appearance Settings */} + + {t('watermark.settings.rotation', 'Rotation (degrees)')} + onParameterChange('rotation', value || 0)} + min={-360} + max={360} + disabled={disabled} + /> + + {t('watermark.settings.opacity', 'Opacity (%)')} + onParameterChange('opacity', value || 50)} + min={0} + max={100} + disabled={disabled} + /> + + + {/* Spacing Settings */} + + {t('watermark.settings.spacing.width', 'Width Spacing')} + onParameterChange('widthSpacer', value || 50)} + min={0} + max={200} + disabled={disabled} + /> + + {t('watermark.settings.spacing.height', 'Height Spacing')} + onParameterChange('heightSpacer', value || 50)} + min={0} + max={200} + disabled={disabled} + /> + + + + ); +}; + +export default WatermarkStyleSettings; \ No newline at end of file diff --git a/frontend/src/components/tools/addWatermark/WatermarkTextStyle.tsx b/frontend/src/components/tools/addWatermark/WatermarkTextStyle.tsx new file mode 100644 index 000000000..00fd21d09 --- /dev/null +++ b/frontend/src/components/tools/addWatermark/WatermarkTextStyle.tsx @@ -0,0 +1,46 @@ +import React from "react"; +import { Stack, Text, Select, ColorInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters"; +import { alphabetOptions } from "../../../constants/addWatermarkConstants"; + +interface WatermarkTextStyleProps { + parameters: AddWatermarkParameters; + onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + disabled?: boolean; +} + +const WatermarkTextStyle = ({ parameters, onParameterChange, disabled = false }: WatermarkTextStyleProps) => { + const { t } = useTranslation(); + + + return ( + + + + {t("watermark.settings.color", "Colour")} + + onParameterChange("customColor", value)} + disabled={disabled} + format="hex" + /> + + + + + {t("watermark.settings.alphabet", "Alphabet")} + + value && onParameterChange("alphabet", value)} + data={alphabetOptions} + disabled={disabled} + /> + + + ); +}; + +export default WatermarkTextStyle; diff --git a/frontend/src/components/tools/addWatermark/WatermarkTypeSettings.tsx b/frontend/src/components/tools/addWatermark/WatermarkTypeSettings.tsx new file mode 100644 index 000000000..f97454c5c --- /dev/null +++ b/frontend/src/components/tools/addWatermark/WatermarkTypeSettings.tsx @@ -0,0 +1,44 @@ +import React from "react"; +import { Button, Stack, Text } from "@mantine/core"; +import { useTranslation } from "react-i18next"; + +interface WatermarkTypeSettingsProps { + watermarkType?: 'text' | 'image'; + onWatermarkTypeChange: (type: 'text' | 'image') => void; + disabled?: boolean; +} + +const WatermarkTypeSettings = ({ watermarkType, onWatermarkTypeChange, disabled = false }: WatermarkTypeSettingsProps) => { + const { t } = useTranslation(); + + return ( + + + onWatermarkTypeChange('text')} + disabled={disabled} + style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }} + > + + {t('watermark.watermarkType.text', 'Text')} + + + onWatermarkTypeChange('image')} + disabled={disabled} + style={{ flex: 1, height: 'auto', minHeight: '40px', fontSize: '11px' }} + > + + {t('watermark.watermarkType.image', 'Image')} + + + + + ); +}; + +export default WatermarkTypeSettings; diff --git a/frontend/src/components/tools/addWatermark/WatermarkWording.tsx b/frontend/src/components/tools/addWatermark/WatermarkWording.tsx new file mode 100644 index 000000000..621a0f399 --- /dev/null +++ b/frontend/src/components/tools/addWatermark/WatermarkWording.tsx @@ -0,0 +1,34 @@ +import React from "react"; +import { Stack, Text, TextInput } from "@mantine/core"; +import { useTranslation } from "react-i18next"; +import { AddWatermarkParameters } from "../../../hooks/tools/addWatermark/useAddWatermarkParameters"; +import { removeEmojis } from "../../../utils/textUtils"; + +interface WatermarkWordingProps { + parameters: AddWatermarkParameters; + onParameterChange: (key: keyof AddWatermarkParameters, value: any) => void; + disabled?: boolean; +} + +const WatermarkWording = ({ parameters, onParameterChange, disabled = false }: WatermarkWordingProps) => { + const { t } = useTranslation(); + + const handleTextChange = (e: React.ChangeEvent) => { + const value = e.target.value; + const filteredValue = removeEmojis(value); + onParameterChange('watermarkText', filteredValue); + }; + + return ( + + + + ); +}; + +export default WatermarkWording; diff --git a/frontend/src/components/tools/shared/NumberInputWithUnit.tsx b/frontend/src/components/tools/shared/NumberInputWithUnit.tsx new file mode 100644 index 000000000..4e30c40d1 --- /dev/null +++ b/frontend/src/components/tools/shared/NumberInputWithUnit.tsx @@ -0,0 +1,57 @@ +import React, { useState, useEffect } from "react"; +import { Stack, Text, NumberInput } from "@mantine/core"; + +interface NumberInputWithUnitProps { + label: string; + value: number; + onChange: (value: number | string) => void; + unit: string; + min?: number; + max?: number; + disabled?: boolean; +} + +const NumberInputWithUnit = ({ + label, + value, + onChange, + unit, + min, + max, + disabled = false +}: NumberInputWithUnitProps) => { + const [localValue, setLocalValue] = useState(value); + + // Sync local value when external value changes + useEffect(() => { + setLocalValue(value); + }, [value]); + + const handleBlur = () => { + onChange(localValue); + }; + + return ( + + + {label} + + + {unit} + + } + rightSectionWidth={unit.length * 8 + 20} // Dynamic width based on unit length + /> + + ); +}; + +export default NumberInputWithUnit; \ No newline at end of file diff --git a/frontend/src/components/tools/shared/ReviewToolStep.tsx b/frontend/src/components/tools/shared/ReviewToolStep.tsx index 4b82f914e..475715704 100644 --- a/frontend/src/components/tools/shared/ReviewToolStep.tsx +++ b/frontend/src/components/tools/shared/ReviewToolStep.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { Button, Stack, Text } from '@mantine/core'; import { useTranslation } from 'react-i18next'; import DownloadIcon from '@mui/icons-material/Download'; @@ -14,24 +14,32 @@ export interface ReviewToolStepProps { onFileClick?: (file: File) => void; } -export function createReviewToolStep( - createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement, - props: ReviewToolStepProps -): React.ReactElement { +function ReviewStepContent({ operation, onFileClick }: { operation: ToolOperationHook; onFileClick?: (file: File) => void }) { const { t } = useTranslation(); - const { operation } = props; + const stepRef = useRef(null); const previewFiles = operation.files?.map((file, index) => ({ file, thumbnail: operation.thumbnails[index] })) || []; - return createStep(t("review", "Review"), { - isVisible: props.isVisible, - _excludeFromCount: true, - _noPadding: true - }, ( - + // Auto-scroll to bottom when content appears + useEffect(() => { + if (stepRef.current && (previewFiles.length > 0 || operation.downloadUrl || operation.errorMessage)) { + const scrollableContainer = stepRef.current.closest('[style*="overflow: auto"]') as HTMLElement; + if (scrollableContainer) { + setTimeout(() => { + scrollableContainer.scrollTo({ + top: scrollableContainer.scrollHeight, + behavior: 'smooth' + }); + }, 100); // Small delay to ensure content is rendered + } + } + }, [previewFiles.length, operation.downloadUrl, operation.errorMessage]); + + return ( + ( {previewFiles.length > 0 && ( )} @@ -61,5 +69,23 @@ export function createReviewToolStep( + ); +} + +export function createReviewToolStep( + createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement, + props: ReviewToolStepProps +): React.ReactElement { + const { t } = useTranslation(); + + return createStep(t("review", "Review"), { + isVisible: props.isVisible, + _excludeFromCount: true, + _noPadding: true + }, ( + )); } diff --git a/frontend/src/components/tools/shared/ToolStep.tsx b/frontend/src/components/tools/shared/ToolStep.tsx index 2bbd508c4..927180038 100644 --- a/frontend/src/components/tools/shared/ToolStep.tsx +++ b/frontend/src/components/tools/shared/ToolStep.tsx @@ -3,12 +3,13 @@ import { Text, Stack, Box, Flex, Divider } from '@mantine/core'; import ExpandMoreIcon from '@mui/icons-material/ExpandMore'; import ChevronRightIcon from '@mui/icons-material/ChevronRight'; import { Tooltip } from '../../shared/Tooltip'; -import { TooltipTip } from '../../shared/tooltip/TooltipContent'; +import { TooltipTip } from '../../../types/tips'; import { createFilesToolStep, FilesToolStepProps } from './FilesToolStep'; import { createReviewToolStep, ReviewToolStepProps } from './ReviewToolStep'; interface ToolStepContextType { visibleStepCount: number; + forceStepNumbers?: boolean; } const ToolStepContext = createContext(null); @@ -82,10 +83,11 @@ const ToolStep = ({ const parent = useContext(ToolStepContext); - // Auto-detect if we should show numbers based on sibling count + // Auto-detect if we should show numbers based on sibling count or force option const shouldShowNumber = useMemo(() => { - if (showNumber !== undefined) return showNumber; - return parent ? parent.visibleStepCount >= 3 : false; + if (showNumber !== undefined) return showNumber; // Individual step override + if (parent?.forceStepNumbers) return true; // Flow-level force + return parent ? parent.visibleStepCount >= 3 : false; // Auto-detect }, [showNumber, parent]); const stepNumber = _stepNumber; @@ -196,7 +198,7 @@ export function createToolSteps() { } // Context provider wrapper for tools using the factory -export function ToolStepProvider({ children }: { children: React.ReactNode }) { +export function ToolStepProvider({ children, forceStepNumbers }: { children: React.ReactNode; forceStepNumbers?: boolean }) { // Count visible steps from children that are ToolStep elements const visibleStepCount = useMemo(() => { let count = 0; @@ -212,8 +214,9 @@ export function ToolStepProvider({ children }: { children: React.ReactNode }) { }, [children]); const contextValue = useMemo(() => ({ - visibleStepCount - }), [visibleStepCount]); + visibleStepCount, + forceStepNumbers + }), [visibleStepCount, forceStepNumbers]); return ( diff --git a/frontend/src/components/tools/shared/createToolFlow.tsx b/frontend/src/components/tools/shared/createToolFlow.tsx index 76f53cfe2..3cb46ed60 100644 --- a/frontend/src/components/tools/shared/createToolFlow.tsx +++ b/frontend/src/components/tools/shared/createToolFlow.tsx @@ -49,6 +49,7 @@ export interface ToolFlowConfig { steps: MiddleStepConfig[]; executeButton?: ExecuteButtonConfig; review: ReviewStepConfig; + forceStepNumbers?: boolean; } /** @@ -60,7 +61,7 @@ export function createToolFlow(config: ToolFlowConfig) { return ( - + {/* Files Step */} {steps.createFilesStep({ selectedFiles: config.files.selectedFiles, diff --git a/frontend/src/components/tooltips/useWatermarkTips.ts b/frontend/src/components/tooltips/useWatermarkTips.ts new file mode 100644 index 000000000..d9ffbd7d9 --- /dev/null +++ b/frontend/src/components/tooltips/useWatermarkTips.ts @@ -0,0 +1,176 @@ +import { useTranslation } from 'react-i18next'; +import { TooltipContent, TooltipTip } from '../../types/tips'; + +// Shared tooltip content to reduce duplication +const useSharedWatermarkContent = () => { + const { t } = useTranslation(); + + const languageSupportTip: TooltipTip = { + title: t("watermark.tooltip.language.title", "Language Support"), + description: t("watermark.tooltip.language.text", "Choose the appropriate language setting to ensure proper font rendering for your text.") + }; + + const appearanceTip: TooltipTip = { + title: t("watermark.tooltip.appearance.title", "Appearance Settings"), + description: t("watermark.tooltip.appearance.text", "Control how your watermark looks and blends with the document."), + bullets: [ + t("watermark.tooltip.appearance.bullet1", "Rotation: -360° to 360° for angled watermarks"), + t("watermark.tooltip.appearance.bullet2", "Opacity: 0-100% for transparency control"), + t("watermark.tooltip.appearance.bullet3", "Lower opacity creates subtle watermarks") + ] + }; + + const spacingTip: TooltipTip = { + title: t("watermark.tooltip.spacing.title", "Spacing Control"), + description: t("watermark.tooltip.spacing.text", "Adjust the spacing between repeated watermarks across the page."), + bullets: [ + t("watermark.tooltip.spacing.bullet1", "Width spacing: Horizontal distance between watermarks"), + t("watermark.tooltip.spacing.bullet2", "Height spacing: Vertical distance between watermarks"), + t("watermark.tooltip.spacing.bullet3", "Higher values create more spread out patterns") + ] + }; + + return { languageSupportTip, appearanceTip, spacingTip }; +}; + +export const useWatermarkTypeTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("watermark.tooltip.type.header.title", "Watermark Type Selection") + }, + tips: [ + { + title: t("watermark.tooltip.type.description.title", "Choose Your Watermark"), + description: t("watermark.tooltip.type.description.text", "Select between text or image watermarks based on your needs.") + }, + { + title: t("watermark.tooltip.type.text.title", "Text Watermarks"), + description: t("watermark.tooltip.type.text.text", "Perfect for adding copyright notices, company names, or confidentiality labels. Supports multiple languages and custom colors."), + bullets: [ + t("watermark.tooltip.type.text.bullet1", "Customizable fonts and languages"), + t("watermark.tooltip.type.text.bullet2", "Adjustable colors and transparency"), + t("watermark.tooltip.type.text.bullet3", "Ideal for legal or branding text") + ] + }, + { + title: t("watermark.tooltip.type.image.title", "Image Watermarks"), + description: t("watermark.tooltip.type.image.text", "Use logos, stamps, or any image as a watermark. Great for branding and visual identification."), + bullets: [ + t("watermark.tooltip.type.image.bullet1", "Upload any image format"), + t("watermark.tooltip.type.image.bullet2", "Maintains image quality"), + t("watermark.tooltip.type.image.bullet3", "Perfect for logos and stamps") + ] + } + ] + }; +}; + + + +export const useWatermarkWordingTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("watermark.tooltip.wording.header.title", "Text Content") + }, + tips: [ + { + title: t("watermark.tooltip.wording.text.title", "Watermark Text"), + description: t("watermark.tooltip.wording.text.text", "Enter the text that will appear as your watermark across the document."), + bullets: [ + t("watermark.tooltip.wording.text.bullet1", "Keep it concise for better readability"), + t("watermark.tooltip.wording.text.bullet2", "Common examples: 'CONFIDENTIAL', 'DRAFT', company name"), + t("watermark.tooltip.wording.text.bullet3", "Emoji characters are not supported and will be filtered out") + ] + } + ] + }; +}; + +export const useWatermarkTextStyleTips = (): TooltipContent => { + const { t } = useTranslation(); + const { languageSupportTip } = useSharedWatermarkContent(); + + return { + header: { + title: t("watermark.tooltip.textStyle.header.title", "Text Style") + }, + tips: [ + { + title: t("watermark.tooltip.textStyle.color.title", "Color Selection"), + description: t("watermark.tooltip.textStyle.color.text", "Choose a color that provides good contrast with your document content."), + bullets: [ + t("watermark.tooltip.textStyle.color.bullet1", "Light gray (#d3d3d3) for subtle watermarks"), + t("watermark.tooltip.textStyle.color.bullet2", "Black or dark colors for high contrast"), + t("watermark.tooltip.textStyle.color.bullet3", "Custom colors for branding purposes") + ] + }, + languageSupportTip + ] + }; +}; + +export const useWatermarkFileTips = (): TooltipContent => { + const { t } = useTranslation(); + + return { + header: { + title: t("watermark.tooltip.file.header.title", "Image Upload") + }, + tips: [ + { + title: t("watermark.tooltip.file.upload.title", "Image Selection"), + description: t("watermark.tooltip.file.upload.text", "Upload an image file to use as your watermark."), + bullets: [ + t("watermark.tooltip.file.upload.bullet1", "Supports common formats: PNG, JPG, GIF, BMP"), + t("watermark.tooltip.file.upload.bullet2", "PNG with transparency works best"), + t("watermark.tooltip.file.upload.bullet3", "Higher resolution images maintain quality better") + ] + }, + { + title: t("watermark.tooltip.file.recommendations.title", "Best Practices"), + description: t("watermark.tooltip.file.recommendations.text", "Tips for optimal image watermark results."), + bullets: [ + t("watermark.tooltip.file.recommendations.bullet1", "Use logos or stamps with transparent backgrounds"), + t("watermark.tooltip.file.recommendations.bullet2", "Simple designs work better than complex images"), + t("watermark.tooltip.file.recommendations.bullet3", "Consider the final document size when choosing resolution") + ] + } + ] + }; +}; + +export const useWatermarkFormattingTips = (): TooltipContent => { + const { t } = useTranslation(); + const { appearanceTip, spacingTip } = useSharedWatermarkContent(); + + return { + header: { + title: t("watermark.tooltip.formatting.header.title", "Formatting & Layout") + }, + tips: [ + { + title: t("watermark.tooltip.formatting.size.title", "Size Control"), + description: t("watermark.tooltip.formatting.size.text", "Adjust the size of your watermark (text or image)."), + bullets: [ + t("watermark.tooltip.formatting.size.bullet1", "Larger sizes create more prominent watermarks") + ] + }, + appearanceTip, + spacingTip, + { + title: t("watermark.tooltip.formatting.security.title", "Security Option"), + description: t("watermark.tooltip.formatting.security.text", "Convert the final PDF to an image-based format for enhanced security."), + bullets: [ + t("watermark.tooltip.formatting.security.bullet1", "Prevents text selection and copying"), + t("watermark.tooltip.formatting.security.bullet2", "Makes watermarks harder to remove"), + t("watermark.tooltip.formatting.security.bullet3", "Results in larger file sizes"), + t("watermark.tooltip.formatting.security.bullet4", "Best for sensitive or copyrighted content") + ] + } + ] + }; +}; \ No newline at end of file diff --git a/frontend/src/constants/addWatermarkConstants.ts b/frontend/src/constants/addWatermarkConstants.ts new file mode 100644 index 000000000..65d26e5ca --- /dev/null +++ b/frontend/src/constants/addWatermarkConstants.ts @@ -0,0 +1,28 @@ +import { AddWatermarkParameters } from "../hooks/tools/addWatermark/useAddWatermarkParameters"; + +export interface AlphabetOption { + value: string; + label: string; +} + +export const alphabetOptions: AlphabetOption[] = [ + { value: "roman", label: "Roman" }, + { value: "arabic", label: "العربية" }, + { value: "japanese", label: "日本語" }, + { value: "korean", label: "한국어" }, + { value: "chinese", label: "简体中文" }, + { value: "thai", label: "ไทย" }, +]; + +export const defaultWatermarkParameters: AddWatermarkParameters = { + watermarkType: undefined, + watermarkText: '', + fontSize: 12, + rotation: 0, + opacity: 50, + widthSpacer: 50, + heightSpacer: 50, + alphabet: 'roman', + customColor: '#d3d3d3', + convertPDFToImage: false +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/addWatermark/useAddWatermarkOperation.ts b/frontend/src/hooks/tools/addWatermark/useAddWatermarkOperation.ts new file mode 100644 index 000000000..a5f28bc06 --- /dev/null +++ b/frontend/src/hooks/tools/addWatermark/useAddWatermarkOperation.ts @@ -0,0 +1,46 @@ +import { useTranslation } from 'react-i18next'; +import { useToolOperation } from '../shared/useToolOperation'; +import { createStandardErrorHandler } from '../../../utils/toolErrorHandler'; +import { AddWatermarkParameters } from './useAddWatermarkParameters'; + +const buildFormData = (parameters: AddWatermarkParameters, file: File): FormData => { + const formData = new FormData(); + formData.append("fileInput", file); + + // Required: watermarkType as string + formData.append("watermarkType", parameters.watermarkType || "text"); + + // Add watermark content based on type + if (parameters.watermarkType === 'text') { + formData.append("watermarkText", parameters.watermarkText); + } else if (parameters.watermarkType === 'image' && parameters.watermarkImage) { + formData.append("watermarkImage", parameters.watermarkImage); + } + + // Required parameters with correct formatting + formData.append("fontSize", parameters.fontSize.toString()); + formData.append("rotation", parameters.rotation.toString()); + formData.append("opacity", (parameters.opacity / 100).toString()); // Convert percentage to decimal + formData.append("widthSpacer", parameters.widthSpacer.toString()); + formData.append("heightSpacer", parameters.heightSpacer.toString()); + + // Backend-expected parameters from user input + formData.append("alphabet", parameters.alphabet); + formData.append("customColor", parameters.customColor); + formData.append("convertPDFToImage", parameters.convertPDFToImage.toString()); + + return formData; +}; + +export const useAddWatermarkOperation = () => { + const { t } = useTranslation(); + + return useToolOperation({ + operationType: 'watermark', + endpoint: '/api/v1/security/add-watermark', + buildFormData, + filePrefix: t('watermark.filenamePrefix', 'watermarked') + '_', + multiFileEndpoint: false, // Individual API calls per file + getErrorMessage: createStandardErrorHandler(t('watermark.error.failed', 'An error occurred while adding watermark to the PDF.')) + }); +}; \ No newline at end of file diff --git a/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts b/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts new file mode 100644 index 000000000..aa2f3bdda --- /dev/null +++ b/frontend/src/hooks/tools/addWatermark/useAddWatermarkParameters.ts @@ -0,0 +1,50 @@ +import { useState, useCallback } from 'react'; +import { defaultWatermarkParameters } from '../../../constants/addWatermarkConstants'; + +export interface AddWatermarkParameters { + watermarkType?: 'text' | 'image'; + watermarkText: string; + watermarkImage?: File; + fontSize: number; // Used for both text size and image size + rotation: number; + opacity: number; + widthSpacer: number; + heightSpacer: number; + alphabet: string; + customColor: string; + convertPDFToImage: boolean; +} + + +export const useAddWatermarkParameters = () => { + const [parameters, setParameters] = useState(defaultWatermarkParameters); + + const updateParameter = useCallback(( + key: K, + value: AddWatermarkParameters[K] + ) => { + setParameters(prev => ({ ...prev, [key]: value })); + }, []); + + const resetParameters = useCallback(() => { + setParameters(defaultWatermarkParameters); + }, []); + + const validateParameters = useCallback((): boolean => { + if (!parameters.watermarkType) { + return false; + } + if (parameters.watermarkType === 'text') { + return parameters.watermarkText.trim().length > 0; + } else { + return parameters.watermarkImage !== undefined; + } + }, [parameters]); + + return { + parameters, + updateParameter, + resetParameters, + validateParameters + }; +}; \ No newline at end of file diff --git a/frontend/src/tools/AddWatermark.tsx b/frontend/src/tools/AddWatermark.tsx new file mode 100644 index 000000000..3f2357a7e --- /dev/null +++ b/frontend/src/tools/AddWatermark.tsx @@ -0,0 +1,212 @@ +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { useEndpointEnabled } from "../hooks/useEndpointConfig"; +import { useFileContext } from "../contexts/FileContext"; +import { useToolFileSelection } from "../contexts/FileSelectionContext"; + +import { createToolFlow } from "../components/tools/shared/createToolFlow"; + +import WatermarkTypeSettings from "../components/tools/addWatermark/WatermarkTypeSettings"; +import WatermarkWording from "../components/tools/addWatermark/WatermarkWording"; +import WatermarkTextStyle from "../components/tools/addWatermark/WatermarkTextStyle"; +import WatermarkImageFile from "../components/tools/addWatermark/WatermarkImageFile"; +import WatermarkFormatting from "../components/tools/addWatermark/WatermarkFormatting"; + +import { useAddWatermarkParameters } from "../hooks/tools/addWatermark/useAddWatermarkParameters"; +import { useAddWatermarkOperation } from "../hooks/tools/addWatermark/useAddWatermarkOperation"; +import { + useWatermarkTypeTips, + useWatermarkWordingTips, + useWatermarkTextStyleTips, + useWatermarkFileTips, + useWatermarkFormattingTips, +} from "../components/tooltips/useWatermarkTips"; +import { BaseToolProps } from "../types/tool"; + +const AddWatermark = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { + const { t } = useTranslation(); + const { setCurrentMode } = useFileContext(); + const { selectedFiles } = useToolFileSelection(); + + const [collapsedType, setCollapsedType] = useState(false); + const [collapsedStyle, setCollapsedStyle] = useState(true); + const [collapsedFormatting, setCollapsedFormatting] = useState(true); + + const watermarkParams = useAddWatermarkParameters(); + const watermarkOperation = useAddWatermarkOperation(); + const watermarkTypeTips = useWatermarkTypeTips(); + const watermarkWordingTips = useWatermarkWordingTips(); + const watermarkTextStyleTips = useWatermarkTextStyleTips(); + const watermarkFileTips = useWatermarkFileTips(); + const watermarkFormattingTips = useWatermarkFormattingTips(); + + // Endpoint validation + const { enabled: endpointEnabled, loading: endpointLoading } = useEndpointEnabled("add-watermark"); + + useEffect(() => { + watermarkOperation.resetResults(); + onPreviewFile?.(null); + }, [watermarkParams.parameters]); + + // Auto-collapse type step after selection + useEffect(() => { + if (watermarkParams.parameters.watermarkType && !collapsedType) { + setCollapsedType(true); + } + }, [watermarkParams.parameters.watermarkType]); + + const handleAddWatermark = async () => { + try { + await watermarkOperation.executeOperation(watermarkParams.parameters, selectedFiles); + if (watermarkOperation.files && onComplete) { + onComplete(watermarkOperation.files); + } + } catch (error) { + if (onError) { + onError(error instanceof Error ? error.message : t("watermark.error.failed", "Add watermark operation failed")); + } + } + }; + + const handleThumbnailClick = (file: File) => { + onPreviewFile?.(file); + sessionStorage.setItem("previousMode", "watermark"); + setCurrentMode("viewer"); + }; + + const handleSettingsReset = () => { + watermarkOperation.resetResults(); + onPreviewFile?.(null); + setCurrentMode("watermark"); + }; + + const hasFiles = selectedFiles.length > 0; + const hasResults = watermarkOperation.files.length > 0 || watermarkOperation.downloadUrl !== null; + + // Dynamic step structure based on watermark type + const getSteps = () => { + const steps = []; + + steps.push({ + title: t("watermark.steps.type", "Watermark Type"), + isCollapsed: hasResults ? true : collapsedType, + isVisible: hasFiles || hasResults, + onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedType(!collapsedType), + tooltip: watermarkTypeTips, + content: ( + watermarkParams.updateParameter("watermarkType", type)} + disabled={endpointLoading} + /> + ), + }); + + if (hasFiles || hasResults) { + // Text watermark path + if (watermarkParams.parameters.watermarkType === "text") { + // Step 2: Wording + steps.push({ + title: t("watermark.steps.wording", "Wording"), + isCollapsed: hasResults, + tooltip: watermarkWordingTips, + content: ( + + ), + }); + + // Step 3: Style + steps.push({ + title: t("watermark.steps.textStyle", "Style"), + isCollapsed: hasResults ? true : collapsedStyle, + onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedStyle(!collapsedStyle), + tooltip: watermarkTextStyleTips, + content: ( + + ), + }); + + // Step 4: Formatting + steps.push({ + title: t("watermark.steps.formatting", "Formatting"), + isCollapsed: hasResults ? true : collapsedFormatting, + onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedFormatting(!collapsedFormatting), + tooltip: watermarkFormattingTips, + content: ( + + ), + }); + } + + // Image watermark path + if (watermarkParams.parameters.watermarkType === "image") { + // Step 2: Watermark File + steps.push({ + title: t("watermark.steps.file", "Watermark File"), + isCollapsed: hasResults, + tooltip: watermarkFileTips, + content: ( + + ), + }); + + // Step 3: Formatting + steps.push({ + title: t("watermark.steps.formatting", "Formatting"), + isCollapsed: hasResults ? true : collapsedFormatting, + onCollapsedClick: hasResults ? handleSettingsReset : () => setCollapsedFormatting(!collapsedFormatting), + tooltip: watermarkFormattingTips, + content: ( + + ), + }); + } + } + + return steps; + }; + + return createToolFlow({ + files: { + selectedFiles, + isCollapsed: hasFiles || hasResults, + }, + steps: getSteps(), + executeButton: { + text: t("watermark.submit", "Add Watermark"), + isVisible: !hasResults, + loadingText: t("loading"), + onClick: handleAddWatermark, + disabled: !watermarkParams.validateParameters() || !hasFiles || !endpointEnabled, + }, + review: { + isVisible: hasResults, + operation: watermarkOperation, + title: t("watermark.results.title", "Watermark Results"), + onFileClick: handleThumbnailClick, + }, + forceStepNumbers: true, + }); +}; + +export default AddWatermark; diff --git a/frontend/src/types/fileContext.ts b/frontend/src/types/fileContext.ts index 7a8c4d2ed..0b5552c4f 100644 --- a/frontend/src/types/fileContext.ts +++ b/frontend/src/types/fileContext.ts @@ -17,6 +17,7 @@ export type ModeType = | 'sanitize' | 'addPassword' | 'changePermissions' + | 'watermark' | 'removePassword'; export type ViewType = 'viewer' | 'pageEditor' | 'fileEditor'; @@ -108,12 +109,12 @@ export interface FileContextActions { removeFiles: (fileIds: string[], deleteFromStorage?: boolean) => void; replaceFile: (oldFileId: string, newFile: File) => Promise; clearAllFiles: () => void; - + // File pinning pinFile: (file: File) => void; unpinFile: (file: File) => void; isFilePinned: (file: File) => boolean; - + // File consumption (replace unpinned files with outputs) consumeFiles: (inputFiles: File[], outputFiles: File[]) => Promise; diff --git a/frontend/src/types/tips.ts b/frontend/src/types/tips.ts index 58519e114..5bcd767d7 100644 --- a/frontend/src/types/tips.ts +++ b/frontend/src/types/tips.ts @@ -1,13 +1,15 @@ +export interface TooltipTip { + title?: string; + description?: string; + bullets?: string[]; + body?: React.ReactNode; +} + export interface TooltipContent { header?: { title: string; logo?: string | React.ReactNode; }; - tips?: Array<{ - title?: string; - description?: string; - bullets?: string[]; - body?: React.ReactNode; - }>; + tips?: TooltipTip[]; content?: React.ReactNode; } \ No newline at end of file diff --git a/frontend/src/utils/textUtils.ts b/frontend/src/utils/textUtils.ts new file mode 100644 index 000000000..e033d505a --- /dev/null +++ b/frontend/src/utils/textUtils.ts @@ -0,0 +1,9 @@ +/** + * Filters out emoji characters from a text string + * @param text - The input text string + * @returns The filtered text without emoji characters + */ +export const removeEmojis = (text: string): string => { + // Filter out emoji characters (Unicode ranges for emojis) + return text.replace(/[\u{1F600}-\u{1F64F}]|[\u{1F300}-\u{1F5FF}]|[\u{1F680}-\u{1F6FF}]|[\u{1F1E0}-\u{1F1FF}]|[\u{2600}-\u{26FF}]|[\u{2700}-\u{27BF}]/gu, ''); +}; \ No newline at end of file