Skip to content
Empty file.
198 changes: 198 additions & 0 deletions components/ds/MultiFileUploadComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
"use client";
import { DragEvent, useCallback, useMemo, useRef, useState } from "react";
import UploadIcon from "@/components/icons/UploadIcon";

type Status = "idle" | "loading" | "error" | "unsupported" | "hover";

const MAX_FILE_SIZE = 4 * 1024 * 1024;
interface MultiFileUploadProps {
onFilesSelect: (files: File[]) => void;
maxFileSize?: number;
multiple?: boolean;
acceptedTypes?: string[];
}

const MultiFileUploadComponent = ({
onFilesSelect,
maxFileSize = MAX_FILE_SIZE,
multiple = true,
acceptedTypes = ["image/*"],
}: MultiFileUploadProps) => {
const [status, setStatus] = useState<Status>("idle");
const inputRef = useRef<HTMLInputElement>(null);

const formattedMaxSize = useMemo((): string => {
const sizeInMB = maxFileSize / (1024 * 1024);
return Number.isInteger(sizeInMB)
? `${sizeInMB}MB`
: `${sizeInMB.toFixed(2)}MB`;
}, [maxFileSize]);

const isFileValid = useCallback(
(file: File): boolean => {
// Check file type
const typeValid = acceptedTypes.some((type) => {
if (type.endsWith("/*")) {
const baseType = type.replace("/*", "");
return file.type.startsWith(baseType);
}
return file.type === type;
});

// Check file size
const sizeValid = file.size <= maxFileSize;

return typeValid && sizeValid;
},
[acceptedTypes, maxFileSize]
);

const validateFiles = useCallback(
(files: FileList): { valid: File[]; invalid: boolean } => {
const validFiles: File[] = [];
let hasInvalid = false;

for (let i = 0; i < files.length; i++) {
const file = files[i];
if (isFileValid(file)) {
validFiles.push(file);
} else {
hasInvalid = true;
}
}

return { valid: validFiles, invalid: hasInvalid };
},
[isFileValid]
);

const handleDrop = useCallback(
(event: DragEvent<HTMLDivElement>) => {
event.preventDefault();

const files = event.dataTransfer.files;
if (!files.length) {
setStatus("unsupported");
return;
}

const { valid, invalid } = validateFiles(files);

if (valid.length === 0) {
setStatus(invalid ? "error" : "unsupported");
return;
}

if (invalid) {
setStatus("error");
// Still process valid files
} else {
setStatus("loading");
}

onFilesSelect(valid);
setStatus("idle");
},
[onFilesSelect, validateFiles]
);

const handleDragOver = useCallback(
(event: React.DragEvent<HTMLDivElement>) => {
event.preventDefault();
setStatus("hover");
},
[]
);

const handleDragLeave = useCallback(() => {
setStatus("idle");
}, []);

const handleClick = () => {
inputRef.current?.click();
};

const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const files = event.target.files;
if (files && files.length > 0) {
const { valid, invalid } = validateFiles(files);

if (valid.length === 0) {
setStatus(invalid ? "error" : "unsupported");
return;
}

if (invalid) {
setStatus("error");
// Still process valid files
} else {
setStatus("loading");
}

onFilesSelect(valid);
setStatus("idle");
}
};

const acceptAttribute = acceptedTypes.join(",");
const fileTypeText = multiple ? "images" : "image";

return (
<div
onDrop={handleDrop}
onDragOver={handleDragOver}
onDragLeave={handleDragLeave}
onClick={handleClick}
className="flex flex-col border border-dashed border-border p-6 text-center text-muted-foreground rounded-lg min-h-40 items-center justify-center bg-muted cursor-pointer mb-2"
>
<input
ref={inputRef}
type="file"
accept={acceptAttribute}
multiple={multiple}
className="hidden"
onChange={handleFileChange}
/>
<UploadIcon />
{statusComponents[status](formattedMaxSize, fileTypeText, multiple)}
</div>
);
};

const StatusComponent = ({
title,
message,
}: {
title: string;
message?: string;
}) => (
<div>
<p>{title}</p>
<p>{message || "\u00A0"}</p>
</div>
);

const statusComponents: Record<
Status,
(maxSize: string, fileType: string, multiple: boolean) => JSX.Element
> = {
idle: (maxSize, fileType, multiple) => (
<StatusComponent
title={`Drag and drop your ${fileType} here, or click to select`}
message={`Max size ${maxSize}${multiple ? " per file" : ""}`}
/>
),
loading: () => <StatusComponent title="Loading..." />,
error: (maxSize, fileType, multiple) => (
<StatusComponent
title={`Some files are too big or invalid!`}
message={`${maxSize} max per ${fileType.slice(0, -1)}${multiple ? ", valid files will still be processed" : ""}`}
/>
),
unsupported: (_, fileType) => (
<StatusComponent title={`Please provide valid ${fileType}`} />
),
hover: () => <StatusComponent title="Drop it like it's hot! πŸ”₯" />,
};

export { MultiFileUploadComponent };
28 changes: 28 additions & 0 deletions components/ds/ProgressComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import * as React from "react";
import * as ProgressPrimitive from "@radix-ui/react-progress";

import { cn } from "@/lib/utils";

const Progress = React.forwardRef<
React.ElementRef<typeof ProgressPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof ProgressPrimitive.Root>
>(({ className, value, ...props }, ref) => (
<ProgressPrimitive.Root
ref={ref}
className={cn(
"relative h-4 w-full overflow-hidden rounded-full bg-secondary",
className
)}
{...props}
>
<ProgressPrimitive.Indicator
className="h-full w-full flex-1 bg-primary transition-all"
style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
/>
</ProgressPrimitive.Root>
));
Progress.displayName = ProgressPrimitive.Root.displayName;

export { Progress };
28 changes: 28 additions & 0 deletions components/ds/SliderComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"use client";

import * as React from "react";
import * as SliderPrimitive from "@radix-ui/react-slider";

import { cn } from "@/lib/utils";

const Slider = React.forwardRef<
React.ElementRef<typeof SliderPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof SliderPrimitive.Root>
>(({ className, ...props }, ref) => (
<SliderPrimitive.Root
ref={ref}
className={cn(
"relative flex w-full touch-none select-none items-center",
className
)}
{...props}
>
<SliderPrimitive.Track className="relative h-2 w-full grow overflow-hidden rounded-full bg-secondary">
<SliderPrimitive.Range className="absolute h-full bg-primary" />
</SliderPrimitive.Track>
<SliderPrimitive.Thumb className="block h-5 w-5 rounded-full border-2 border-primary bg-background ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50" />
</SliderPrimitive.Root>
));
Slider.displayName = SliderPrimitive.Root.displayName;

export { Slider };
140 changes: 140 additions & 0 deletions components/seo/WebPConverterSEO.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import Link from "next/link";

export default function WebPConverterSEO() {
return (
<div className="content-wrapper">
<section>
<p>
Convert images to WebP format with our free online converter. Reduce
file sizes while maintaining image quality using WebP's advanced
compression for faster web performance and better user experience.
</p>
</section>

<section>
<p>
Simply upload your images, adjust quality settings, and download the
optimized WebP files. Perfect for web developers looking to improve
site speed and reduce bandwidth usage.
</p>
</section>

<section>
<h2>How to Use the WebP Converter</h2>
<p>
Convert single images or batch process multiple files with our
intuitive WebP converter tool.
</p>
<ul>
<li>
<b>Upload images:</b> <br /> Drag and drop multiple image files or
click to select them.
</li>
<li>
<b>Adjust quality:</b> <br /> Use the quality slider to balance file
size and image quality (1-100%).
</li>
<li>
<b>Convert & download:</b> <br /> Convert all images at once and
download individually or as a zip file.
</li>
</ul>
<p>
Need to optimize images further? Try our{" "}
<Link
href="/utilities/image-resizer"
target="_blank"
rel="noopener noreferrer"
>
Image Resizer
</Link>{" "}
to change dimensions before converting to WebP.
</p>
</section>

<section>
<h2>More Image Utilities</h2>
<p>
Optimize and convert images with Jam's suite of free image tools, all
available with dark mode support.
</p>
<ul>
<li>
<Link href="/utilities/image-resizer">Image Resizer</Link>: Resize
images while maintaining aspect ratio and quality.
</li>
<li>
<Link href="/utilities/image-to-base64">Image to Base64</Link>:
Convert images to base64 data URIs for embedding in code.
</li>
</ul>
</section>

<section>
<h2>Benefits of WebP Format</h2>
<p>
WebP is a modern image format developed by Google that provides
superior compression compared to JPEG and PNG while maintaining
excellent image quality.
</p>
<ul>
<li>
<b>Smaller file sizes:</b> <br /> WebP files are typically 25-50%
smaller than equivalent JPEG or PNG images.
</li>
<li>
<b>Faster loading:</b> <br /> Reduced file sizes mean faster page
load times and better user experience.
</li>
<li>
<b>Browser support:</b> <br /> Supported by all modern browsers
including Chrome, Firefox, Safari, and Edge.
</li>
<li>
<b>Quality preservation:</b> <br /> Advanced compression algorithms
maintain image quality even at higher compression ratios.
</li>
</ul>
</section>

<section>
<h2>FAQs</h2>
<ul>
<li>
<b>What is WebP format?</b> <br /> WebP is a modern image format
that provides excellent compression and quality. It's designed to
replace JPEG and PNG for web use.
</li>
<li>
<b>How much smaller are WebP files?</b> <br /> WebP files are
typically 25-50% smaller than equivalent JPEG files and up to 26%
smaller than PNG files.
</li>
<li>
<b>Do all browsers support WebP?</b> <br /> Yes, all modern browsers
including Chrome, Firefox, Safari, and Edge support WebP format.
</li>
<li>
<b>What quality setting should I use?</b> <br /> For web use, 80-90%
quality provides excellent results with significant file size
reduction. Lower values create smaller files with some quality loss.
</li>
<li>
<b>Can I convert multiple images at once?</b> <br /> Yes, our tool
supports batch processing. Upload multiple images and convert them
all simultaneously.
</li>
<li>
<b>Is there a file size limit?</b> <br /> Each image can be up to
10MB. This accommodates most web images and high-resolution photos.
</li>
<li>
<b>Are my images stored on your servers?</b> <br /> No, all
conversion happens in your browser. Images are not uploaded to our
servers, ensuring privacy and security.
</li>
</ul>
</section>
</div>
);
}
Loading
Loading