From c2566ec1600ff654efb5ce8cace1015c6d571a06 Mon Sep 17 00:00:00 2001 From: Shoubhit Dash Date: Sun, 10 Nov 2024 13:22:16 +0530 Subject: [PATCH 01/13] persist user preferences between sessions --- .../(tools)/rounded-border/rounded-tool.tsx | 8 +++-- src/app/(tools)/square-image/square-tool.tsx | 7 +++-- src/app/(tools)/svg-to-png/svg-tool.tsx | 3 +- src/hooks/use-local-storage.ts | 30 +++++++++++++++++++ 4 files changed, 42 insertions(+), 6 deletions(-) create mode 100644 src/hooks/use-local-storage.ts diff --git a/src/app/(tools)/rounded-border/rounded-tool.tsx b/src/app/(tools)/rounded-border/rounded-tool.tsx index 20fb159..0f43646 100644 --- a/src/app/(tools)/rounded-border/rounded-tool.tsx +++ b/src/app/(tools)/rounded-border/rounded-tool.tsx @@ -2,6 +2,7 @@ import { usePlausible } from "next-plausible"; import { useMemo, useState } from "react"; import { ChangeEvent } from "react"; +import { useLocalStorage } from "@/hooks/use-local-storage"; import React from "react"; type Radius = 2 | 4 | 8 | 16 | 32 | 64; @@ -188,8 +189,11 @@ export function RoundedTool() { const { imageContent, imageMetadata, handleFileUpload, cancel } = useFileUploader(); - const [radius, setRadius] = useState(2); - const [background, setBackground] = useState("transparent"); + const [radius, setRadius] = useLocalStorage("roundedTool_radius", 2); + const [background, setBackground] = useLocalStorage( + "roundedTool_background", + "transparent" + ); if (!imageMetadata) return ( diff --git a/src/app/(tools)/square-image/square-tool.tsx b/src/app/(tools)/square-image/square-tool.tsx index 0d70be0..7739add 100644 --- a/src/app/(tools)/square-image/square-tool.tsx +++ b/src/app/(tools)/square-image/square-tool.tsx @@ -2,12 +2,13 @@ import React, { useState, useEffect, ChangeEvent } from "react"; import { usePlausible } from "next-plausible"; +import { useLocalStorage } from "@/hooks/use-local-storage"; export const SquareTool: React.FC = () => { const [imageFile, setImageFile] = useState(null); - const [backgroundColor, setBackgroundColor] = useState<"black" | "white">( - "white" - ); + const [backgroundColor, setBackgroundColor] = useLocalStorage< + "black" | "white" + >("squareTool_backgroundColor", "white"); const [previewUrl, setPreviewUrl] = useState(null); const [canvasDataUrl, setCanvasDataUrl] = useState(null); const [imageMetadata, setImageMetadata] = useState<{ diff --git a/src/app/(tools)/svg-to-png/svg-tool.tsx b/src/app/(tools)/svg-to-png/svg-tool.tsx index 9b62c44..6a8ddb3 100644 --- a/src/app/(tools)/svg-to-png/svg-tool.tsx +++ b/src/app/(tools)/svg-to-png/svg-tool.tsx @@ -1,6 +1,7 @@ "use client"; import { usePlausible } from "next-plausible"; import { useMemo, useState } from "react"; +import { useLocalStorage } from "@/hooks/use-local-storage"; import { ChangeEvent } from "react"; @@ -175,7 +176,7 @@ export function SVGTool() { const { svgContent, imageMetadata, handleFileUpload, cancel } = useFileUploader(); - const [scale, setScale] = useState(1); + const [scale, setScale] = useLocalStorage("svgTool_scale", 1); if (!imageMetadata) return ( diff --git a/src/hooks/use-local-storage.ts b/src/hooks/use-local-storage.ts new file mode 100644 index 0000000..8c1e39d --- /dev/null +++ b/src/hooks/use-local-storage.ts @@ -0,0 +1,30 @@ +import { useState } from "react"; + +export function useLocalStorage(key: string, initialValue: T) { + const [storedValue, setStoredValue] = useState(() => { + if (typeof window === "undefined") return initialValue; + + try { + const item = window.localStorage.getItem(key); + return item ? JSON.parse(item) : initialValue; + } catch (error) { + console.warn(`Error reading localStorage key "${key}":`, error); + return initialValue; + } + }); + + const setValue = (value: T | ((val: T) => T)) => { + try { + const valueToStore = + value instanceof Function ? value(storedValue) : value; + setStoredValue(valueToStore); + if (typeof window !== "undefined") { + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } + } catch (error) { + console.warn(`Error setting localStorage key "${key}":`, error); + } + }; + + return [storedValue, setValue] as const; +} From 970c7685ba932c4c74468dd7adc2a0e1e3387043 Mon Sep 17 00:00:00 2001 From: Theo Browne Date: Sat, 9 Nov 2024 23:55:16 -0800 Subject: [PATCH 02/13] start linting --- .eslintrc.cjs | 41 ++++++++ .eslintrc.json | 3 - .prettierrc | 3 + package.json | 15 ++- pnpm-lock.yaml | 99 +++++++++++++++++++ .../(tools)/rounded-border/rounded-tool.tsx | 26 ++--- tsconfig.json | 43 +++++--- 7 files changed, 198 insertions(+), 32 deletions(-) create mode 100644 .eslintrc.cjs delete mode 100644 .eslintrc.json create mode 100644 .prettierrc diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 0000000..8ef3022 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,41 @@ +/** @type {import("eslint").Linter.Config} */ +const config = { + parser: "@typescript-eslint/parser", + parserOptions: { + project: true, + }, + plugins: ["@typescript-eslint"], + extends: [ + "next/core-web-vitals", + "plugin:@typescript-eslint/recommended-type-checked", + "plugin:@typescript-eslint/stylistic-type-checked", + ], + rules: { + "@typescript-eslint/array-type": "off", + "@typescript-eslint/consistent-type-definitions": "off", + "@typescript-eslint/consistent-type-imports": [ + "warn", + { + prefer: "type-imports", + fixStyle: "inline-type-imports", + }, + ], + "@typescript-eslint/no-unused-vars": [ + "warn", + { + argsIgnorePattern: "^_", + }, + ], + "@typescript-eslint/require-await": "off", + "@typescript-eslint/no-misused-promises": [ + "error", + { + checksVoidReturn: { + attributes: false, + }, + }, + ], + "@next/next/no-img-element": "off", + }, +}; +module.exports = config; diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 3722418..0000000 --- a/.eslintrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "extends": ["next/core-web-vitals", "next/typescript"] -} diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..b4bfed3 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "plugins": ["prettier-plugin-tailwindcss"] +} diff --git a/package.json b/package.json index e58b9c3..69213cc 100644 --- a/package.json +++ b/package.json @@ -3,10 +3,16 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbo", "build": "next build", + "check": "next lint && tsc --noEmit", + "dev": "next dev --turbo", + "lint": "next lint", + "lint:fix": "next lint --fix", + "preview": "next build && next start", "start": "next start", - "lint": "next lint" + "typecheck": "tsc --noEmit", + "format:write": "prettier --write \"**/*.{ts,tsx,js,jsx,mdx}\" --cache", + "format:check": "prettier --check \"**/*.{ts,tsx,js,jsx,mdx}\" --cache" }, "dependencies": { "babel-plugin-react-compiler": "0.0.0-experimental-734b737-20241003", @@ -16,11 +22,16 @@ "react-dom": "19.0.0-rc-cd22717c-20241013" }, "devDependencies": { + "@types/eslint": "^8.56.10", "@types/node": "^20", "@types/react": "npm:types-react@19.0.0-rc.1", "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1", + "@typescript-eslint/eslint-plugin": "^8.1.0", + "@typescript-eslint/parser": "^8.1.0", "eslint": "^8", "eslint-config-next": "15.0.0-rc.1", + "prettier": "^3.3.3", + "prettier-plugin-tailwindcss": "^0.6.8", "postcss": "^8", "tailwindcss": "^3.4.1", "typescript": "^5" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2585321..6c0d7d6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -28,6 +28,9 @@ importers: specifier: 19.0.0-rc-cd22717c-20241013 version: 19.0.0-rc-cd22717c-20241013(react@19.0.0-rc-cd22717c-20241013) devDependencies: + '@types/eslint': + specifier: ^8.56.10 + version: 8.56.12 '@types/node': specifier: ^20 version: 20.16.10 @@ -37,6 +40,12 @@ importers: '@types/react-dom': specifier: npm:types-react-dom@19.0.0-rc.1 version: types-react-dom@19.0.0-rc.1 + '@typescript-eslint/eslint-plugin': + specifier: ^8.1.0 + version: 8.8.0(@typescript-eslint/parser@8.8.0(eslint@8.57.1)(typescript@5.6.2))(eslint@8.57.1)(typescript@5.6.2) + '@typescript-eslint/parser': + specifier: ^8.1.0 + version: 8.8.0(eslint@8.57.1)(typescript@5.6.2) eslint: specifier: ^8 version: 8.57.1 @@ -46,6 +55,12 @@ importers: postcss: specifier: ^8 version: 8.4.47 + prettier: + specifier: ^3.3.3 + version: 3.3.3 + prettier-plugin-tailwindcss: + specifier: ^0.6.8 + version: 0.6.8(prettier@3.3.3) tailwindcss: specifier: ^3.4.1 version: 3.4.13 @@ -325,6 +340,12 @@ packages: '@swc/helpers@0.5.13': resolution: {integrity: sha512-UoKGxQ3r5kYI9dALKJapMmuK+1zWM/H17Z1+iwnNmzcJRnfFuevZs375TA5rW31pu4BS4NoSy1fRsexDXfWn5w==} + '@types/eslint@8.56.12': + resolution: {integrity: sha512-03ruubjWyOHlmljCVoxSuNDdmfZDzsrrz0P2LeJsOXr+ZwFQ+0yQIwNCwt/GYhV7Z31fgtXJTAEs+FYlEL851g==} + + '@types/estree@1.0.6': + resolution: {integrity: sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==} + '@types/istanbul-lib-coverage@2.0.6': resolution: {integrity: sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==} @@ -334,6 +355,9 @@ packages: '@types/istanbul-reports@1.1.2': resolution: {integrity: sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + '@types/json5@0.0.29': resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} @@ -1402,6 +1426,66 @@ packages: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} + prettier-plugin-tailwindcss@0.6.8: + resolution: {integrity: sha512-dGu3kdm7SXPkiW4nzeWKCl3uoImdd5CTZEJGxyypEPL37Wj0HT2pLqjrvSei1nTeuQfO4PUfjeW5cTUNRLZ4sA==} + engines: {node: '>=14.21.3'} + peerDependencies: + '@ianvs/prettier-plugin-sort-imports': '*' + '@prettier/plugin-pug': '*' + '@shopify/prettier-plugin-liquid': '*' + '@trivago/prettier-plugin-sort-imports': '*' + '@zackad/prettier-plugin-twig-melody': '*' + prettier: ^3.0 + prettier-plugin-astro: '*' + prettier-plugin-css-order: '*' + prettier-plugin-import-sort: '*' + prettier-plugin-jsdoc: '*' + prettier-plugin-marko: '*' + prettier-plugin-multiline-arrays: '*' + prettier-plugin-organize-attributes: '*' + prettier-plugin-organize-imports: '*' + prettier-plugin-sort-imports: '*' + prettier-plugin-style-order: '*' + prettier-plugin-svelte: '*' + peerDependenciesMeta: + '@ianvs/prettier-plugin-sort-imports': + optional: true + '@prettier/plugin-pug': + optional: true + '@shopify/prettier-plugin-liquid': + optional: true + '@trivago/prettier-plugin-sort-imports': + optional: true + '@zackad/prettier-plugin-twig-melody': + optional: true + prettier-plugin-astro: + optional: true + prettier-plugin-css-order: + optional: true + prettier-plugin-import-sort: + optional: true + prettier-plugin-jsdoc: + optional: true + prettier-plugin-marko: + optional: true + prettier-plugin-multiline-arrays: + optional: true + prettier-plugin-organize-attributes: + optional: true + prettier-plugin-organize-imports: + optional: true + prettier-plugin-sort-imports: + optional: true + prettier-plugin-style-order: + optional: true + prettier-plugin-svelte: + optional: true + + prettier@3.3.3: + resolution: {integrity: sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==} + engines: {node: '>=14'} + hasBin: true + pretty-format@24.9.0: resolution: {integrity: sha512-00ZMZUiHaJrNfk33guavqgvfJS30sLYf0f8+Srklv0AMPodGGHcoHgksZ3OThYnIvOd+8yMCn0YiEOogjlgsnA==} engines: {node: '>= 6'} @@ -1980,6 +2064,13 @@ snapshots: dependencies: tslib: 2.7.0 + '@types/eslint@8.56.12': + dependencies: + '@types/estree': 1.0.6 + '@types/json-schema': 7.0.15 + + '@types/estree@1.0.6': {} + '@types/istanbul-lib-coverage@2.0.6': {} '@types/istanbul-lib-report@3.0.3': @@ -1991,6 +2082,8 @@ snapshots: '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-lib-report': 3.0.3 + '@types/json-schema@7.0.15': {} + '@types/json5@0.0.29': {} '@types/node@20.16.10': @@ -3277,6 +3370,12 @@ snapshots: prelude-ls@1.2.1: {} + prettier-plugin-tailwindcss@0.6.8(prettier@3.3.3): + dependencies: + prettier: 3.3.3 + + prettier@3.3.3: {} + pretty-format@24.9.0: dependencies: '@jest/types': 24.9.0 diff --git a/src/app/(tools)/rounded-border/rounded-tool.tsx b/src/app/(tools)/rounded-border/rounded-tool.tsx index 20fb159..8eb6916 100644 --- a/src/app/(tools)/rounded-border/rounded-tool.tsx +++ b/src/app/(tools)/rounded-border/rounded-tool.tsx @@ -1,7 +1,7 @@ "use client"; import { usePlausible } from "next-plausible"; import { useMemo, useState } from "react"; -import { ChangeEvent } from "react"; +import type { ChangeEvent } from "react"; import React from "react"; type Radius = 2 | 4 | 8 | 16 | 32 | 64; @@ -21,7 +21,7 @@ function useImageConverter(props: { width: props.imageMetadata.width, height: props.imageMetadata.height, }; - }, [props.imageContent, props.imageMetadata]); + }, [props.imageMetadata]); const convertToPng = async () => { const ctx = props.canvas?.getContext("2d"); @@ -129,7 +129,7 @@ const ImageRenderer: React.FC = ({ }, [imageContent, radius]); return ( -
+
( - null + null, ); const { convertToPng, canvasProps } = useImageConverter({ canvas: canvasRef, @@ -174,9 +174,9 @@ function SaveAsPngButton({ @@ -193,10 +193,10 @@ export function RoundedTool() { if (!imageMetadata) return ( -
+

Round the corners of any image

-