Skip to content
Open
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
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
"next": "15.0.0-rc.1",
"next-plausible": "^3.12.2",
"react": "19.0.0-rc-cd22717c-20241013",
"react-dom": "19.0.0-rc-cd22717c-20241013"
"react-dom": "19.0.0-rc-cd22717c-20241013",
"sonner": "^1.7.0"
},
"devDependencies": {
"@types/eslint": "^8.56.10",
Expand All @@ -31,9 +32,9 @@
"concurrently": "^9.1.0",
"eslint": "^8",
"eslint-config-next": "15.0.0-rc.1",
"postcss": "^8",
"prettier": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.8",
"postcss": "^8",
"tailwindcss": "^3.4.1",
"typescript": "^5"
},
Expand All @@ -42,5 +43,6 @@
"@types/react": "npm:[email protected]",
"@types/react-dom": "npm:[email protected]"
}
}
},
"packageManager": "[email protected]"
}
22 changes: 18 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 12 additions & 9 deletions src/app/(tools)/rounded-border/rounded-tool.tsx
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
"use client";
import { usePlausible } from "next-plausible";
import { useEffect, useMemo, useRef, useState } from "react";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { UploadBox } from "@/components/shared/upload-box";
import { OptionSelector } from "@/components/shared/option-selector";

import { BorderRadiusSelector } from "@/components/border-radius-selector";
import { FileDropzone } from "@/components/shared/file-dropzone";
import { OptionSelector } from "@/components/shared/option-selector";
import { UploadBox } from "@/components/shared/upload-box";
import {
useFileUploader,
type FileUploaderResult,
} from "@/hooks/use-file-uploader";
import { FileDropzone } from "@/components/shared/file-dropzone";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { usePlausible } from "next-plausible";
import { useEffect, useMemo, useRef, useState } from "react";

type Radius = number;

type BackgroundOption = "white" | "black" | "transparent";

const acceptedFileTypes = ["image/*", ".jpg", ".jpeg", ".png", ".webp", ".svg"];

function useImageConverter(props: {
canvas: HTMLCanvasElement | null;
imageContent: string;
Expand Down Expand Up @@ -176,7 +179,7 @@ function RoundedToolCore(props: { fileUploaderProps: FileUploaderResult }) {
title="Add rounded borders to your images. Quick and easy."
subtitle="Allows pasting images from clipboard"
description="Upload Image"
accept="image/*"
accept={acceptedFileTypes.join(", ")}
onChange={handleFileUploadEvent}
/>
);
Expand Down Expand Up @@ -240,12 +243,12 @@ function RoundedToolCore(props: { fileUploaderProps: FileUploaderResult }) {
}

export function RoundedTool() {
const fileUploaderProps = useFileUploader();
const fileUploaderProps = useFileUploader(acceptedFileTypes);

return (
<FileDropzone
setCurrentFile={fileUploaderProps.handleFileUpload}
acceptedFileTypes={["image/*", ".jpg", ".jpeg", ".png", ".webp", ".svg"]}
acceptedFileTypes={acceptedFileTypes}
dropText="Drop image file"
>
<RoundedToolCore fileUploaderProps={fileUploaderProps} />
Expand Down
16 changes: 9 additions & 7 deletions src/app/(tools)/square-image/square-tool.tsx
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
"use client";

import { usePlausible } from "next-plausible";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { UploadBox } from "@/components/shared/upload-box";
import { OptionSelector } from "@/components/shared/option-selector";
import { FileDropzone } from "@/components/shared/file-dropzone";
import { OptionSelector } from "@/components/shared/option-selector";
import { UploadBox } from "@/components/shared/upload-box";
import {
type FileUploaderResult,
useFileUploader,
} from "@/hooks/use-file-uploader";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { usePlausible } from "next-plausible";
import { useEffect, useState } from "react";

const acceptedFileTypes = ["image/*", ".jpg", ".jpeg", ".png", ".webp", ".svg"];

function SquareToolCore(props: { fileUploaderProps: FileUploaderResult }) {
const { imageContent, imageMetadata, handleFileUploadEvent, cancel } =
props.fileUploaderProps;
Expand Down Expand Up @@ -72,7 +74,7 @@ function SquareToolCore(props: { fileUploaderProps: FileUploaderResult }) {
title="Create square images with custom backgrounds. Fast and free."
subtitle="Allows pasting images from clipboard"
description="Upload Image"
accept="image/*"
accept={acceptedFileTypes.join(", ")}
onChange={handleFileUploadEvent}
/>
);
Expand Down Expand Up @@ -138,12 +140,12 @@ function SquareToolCore(props: { fileUploaderProps: FileUploaderResult }) {
}

export function SquareTool() {
const fileUploaderProps = useFileUploader();
const fileUploaderProps = useFileUploader(acceptedFileTypes);

return (
<FileDropzone
setCurrentFile={fileUploaderProps.handleFileUpload}
acceptedFileTypes={["image/*", ".jpg", ".jpeg", ".png", ".webp", ".svg"]}
acceptedFileTypes={acceptedFileTypes}
dropText="Drop image file"
>
<SquareToolCore fileUploaderProps={fileUploaderProps} />
Expand Down
12 changes: 7 additions & 5 deletions src/app/(tools)/svg-to-png/svg-tool.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
"use client";
import { useLocalStorage } from "@/hooks/use-local-storage";
import { usePlausible } from "next-plausible";
import { useEffect, useMemo, useRef, useState } from "react";
import { useLocalStorage } from "@/hooks/use-local-storage";

import { UploadBox } from "@/components/shared/upload-box";
import { SVGScaleSelector } from "@/components/svg-scale-selector";

export type Scale = "custom" | number;

const acceptedFileTypes = ["image/svg+xml", ".svg"];

function scaleSvg(svgContent: string, scale: number) {
const parser = new DOMParser();
const svgDoc = parser.parseFromString(svgContent, "image/svg+xml");
Expand Down Expand Up @@ -131,11 +133,11 @@ function SaveAsPngButton({
);
}

import { FileDropzone } from "@/components/shared/file-dropzone";
import {
type FileUploaderResult,
useFileUploader,
} from "@/hooks/use-file-uploader";
import { FileDropzone } from "@/components/shared/file-dropzone";

function SVGToolCore(props: { fileUploaderProps: FileUploaderResult }) {
const { rawContent, imageMetadata, handleFileUploadEvent, cancel } =
Expand All @@ -155,7 +157,7 @@ function SVGToolCore(props: { fileUploaderProps: FileUploaderResult }) {
<UploadBox
title="Make SVGs into PNGs. Also makes them bigger. (100% free btw.)"
description="Upload SVG"
accept=".svg"
accept={acceptedFileTypes.join(", ")}
onChange={handleFileUploadEvent}
/>
);
Expand Down Expand Up @@ -217,11 +219,11 @@ function SVGToolCore(props: { fileUploaderProps: FileUploaderResult }) {
}

export function SVGTool() {
const fileUploaderProps = useFileUploader();
const fileUploaderProps = useFileUploader(acceptedFileTypes);
return (
<FileDropzone
setCurrentFile={fileUploaderProps.handleFileUpload}
acceptedFileTypes={["image/svg+xml", ".svg"]}
acceptedFileTypes={acceptedFileTypes}
dropText="Drop SVG file"
>
<SVGToolCore fileUploaderProps={fileUploaderProps} />
Expand Down
4 changes: 3 additions & 1 deletion src/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Metadata } from "next";
import PlausibleProvider from "next-plausible";
import localFont from "next/font/local";
import { Toaster } from "sonner";
import "./globals.css";
import PlausibleProvider from "next-plausible";

const geistSans = localFont({
src: "./fonts/GeistVF.woff",
Expand Down Expand Up @@ -36,6 +37,7 @@ export default function RootLayout({
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
<Toaster richColors closeButton theme="dark" />
</body>
</html>
);
Expand Down
31 changes: 20 additions & 11 deletions src/components/shared/file-dropzone.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import React, { useCallback, useState, useRef } from "react";
import { validateFileType } from "@/lib/file-utils";
import React, { useCallback, useRef, useState } from "react";
import { toast } from "sonner";

interface FileDropzoneProps {
children: React.ReactNode;
Expand Down Expand Up @@ -53,18 +55,25 @@ export function FileDropzone({
const droppedFile = files[0];

if (!droppedFile) {
alert("How did you do a drop with no files???");
throw new Error("No files dropped");
toast.error("Error dropping file!", {
description:
"Please try dropping the same file again or drop a different one.",
});

throw new Error("No file loaded");
}

if (
!acceptedFileTypes.includes(droppedFile.type) &&
!acceptedFileTypes.some((type) =>
droppedFile.name.toLowerCase().endsWith(type.replace("*", "")),
)
) {
alert("Invalid file type. Please upload a supported file type.");
throw new Error("Invalid file");
const verify = validateFileType({
acceptedFileTypes,
file: droppedFile,
});

if (!verify.isValid) {
toast.error("Error uploading file!", {
description: verify.error,
});

return;
}

// Happy path
Expand Down
31 changes: 26 additions & 5 deletions src/hooks/use-file-uploader.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useCallback } from "react";
import { type ChangeEvent, useState } from "react";
import { validateFileType } from "@/lib/file-utils";
import { type ChangeEvent, useCallback, useState } from "react";
import { toast } from "sonner";
import { useClipboardPaste } from "./use-clipboard-paste";

const parseSvgFile = (content: string, fileName: string) => {
Expand Down Expand Up @@ -73,7 +74,9 @@ export type FileUploaderResult = {
* - handleFileUpload: Function to handle file input change events
* - cancel: Function to reset the upload state
*/
export const useFileUploader = (): FileUploaderResult => {
export const useFileUploader = (
acceptedFileTypes: string[],
): FileUploaderResult => {
const [imageContent, setImageContent] = useState<string>("");
const [rawContent, setRawContent] = useState<string>("");
const [imageMetadata, setImageMetadata] = useState<{
Expand Down Expand Up @@ -114,9 +117,27 @@ export const useFileUploader = (): FileUploaderResult => {

const handleFileUploadEvent = (event: ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
processFile(file);

if (!file) {
toast.error("Error loading file!", {
description:
"Please try uploading the file again or pick a different one.",
});

throw new Error("No file loaded");
}

const verify = validateFileType({ acceptedFileTypes, file });

if (!verify.isValid) {
toast.error("Error uploading file!", {
description: verify.error,
});

return;
}

processFile(file);
};

const handleFilePaste = useCallback((file: File) => {
Expand Down
Loading