mirror of
https://github.com/ershisan99/DevToysWeb.git
synced 2025-12-16 20:49:23 +00:00
renewal
recreate project by using https://github.com/shadcn/next-template App: - support dark mode - add toggle theme button - add clear search button - add search button - add current page indicator - add tool group pages - add settings tool - add 1 tab format option to Json format tool - add paste button to some tools - add file button to some tools - add copy button to some tools - add clear button to some tools - change favicon - change search hit rate - change each page title - change icons from Material Icons to Lucide - change sidebar scroll area - change editor from Ace to Monaco - change parsable separators of number base converter - change default value of format option of number base converter - change default values of some tool forms - change some styles - remove disabled tools - remove real-time search - fix uri encoding tool Dev: - MUI + Emotion -> Radix UI + Tailwind CSS - Next.js 12 Pages -> Next.js 13 App Router - React 17 -> React 18 - many other packages upgraded - use useState instead of recoil - use Next.js typedRoutes instead of pathpida - clean npm scripts - format import statements by Prettier - no component separations between container and presenter - effective component memoizations - add vscode settings - many refactors
This commit is contained in:
30
components/buttons/base.tsx
Normal file
30
components/buttons/base.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import { Button, ButtonProps } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
|
||||
export type BaseButtonProps = ButtonProps & {
|
||||
icon: React.ReactNode;
|
||||
iconOnly?: true;
|
||||
labelText: string;
|
||||
};
|
||||
|
||||
export function BaseButton({ icon, iconOnly, labelText, ...props }: BaseButtonProps) {
|
||||
const button = (
|
||||
<Button className="w-fit border" {...props}>
|
||||
{icon}
|
||||
{!iconOnly && <span className="ml-1">{labelText}</span>}
|
||||
</Button>
|
||||
);
|
||||
|
||||
return iconOnly ? (
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>{button}</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
<p>{labelText}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
}
|
||||
13
components/buttons/clear.tsx
Normal file
13
components/buttons/clear.tsx
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useMemo } from "react";
|
||||
|
||||
import { icons } from "@/components/icons";
|
||||
|
||||
import { BaseButton, BaseButtonProps } from "./base";
|
||||
|
||||
export type ClearButtonProps = Omit<BaseButtonProps, "icon" | "labelText">;
|
||||
|
||||
export function ClearButton({ iconOnly, ...props }: ClearButtonProps) {
|
||||
const icon = useMemo(() => <icons.X size={16} />, []);
|
||||
|
||||
return <BaseButton {...props} {...{ icon, iconOnly }} labelText="Clear" />;
|
||||
}
|
||||
24
components/buttons/copy.tsx
Normal file
24
components/buttons/copy.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
import { icons } from "@/components/icons";
|
||||
|
||||
import { BaseButton, BaseButtonProps } from "./base";
|
||||
|
||||
export type CopyButtonProps = Omit<BaseButtonProps, "icon" | "labelText" | "onClick"> & {
|
||||
text: string;
|
||||
};
|
||||
|
||||
export function CopyButton({ text, iconOnly, ...props }: CopyButtonProps) {
|
||||
const onClick: BaseButtonProps["onClick"] = useCallback(() => {
|
||||
navigator.clipboard.writeText(text).catch(e => {
|
||||
if (e instanceof Error) {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(e.message);
|
||||
}
|
||||
});
|
||||
}, [text]);
|
||||
|
||||
const icon = useMemo(() => <icons.Copy size={16} />, []);
|
||||
|
||||
return <BaseButton {...props} {...{ icon, iconOnly, onClick }} labelText="Copy" />;
|
||||
}
|
||||
66
components/buttons/file.tsx
Normal file
66
components/buttons/file.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
|
||||
import { icons } from "@/components/icons";
|
||||
|
||||
import { BaseButton, BaseButtonProps } from "./base";
|
||||
|
||||
type InputProps = React.ComponentProps<"input">;
|
||||
|
||||
export type FileButtonProps = Pick<InputProps, "accept"> &
|
||||
Omit<BaseButtonProps, "icon" | "labelText" | "onClick"> & {
|
||||
maxFileSizeMb?: number;
|
||||
onFileRead: (text: string) => void;
|
||||
};
|
||||
|
||||
export function FileButton({
|
||||
accept,
|
||||
iconOnly,
|
||||
maxFileSizeMb = 20,
|
||||
onFileRead,
|
||||
...props
|
||||
}: FileButtonProps) {
|
||||
const ref = useRef<HTMLInputElement>(null);
|
||||
|
||||
const onClick = () => ref.current?.click();
|
||||
|
||||
const onChange: NonNullable<InputProps["onChange"]> = useCallback(
|
||||
({ currentTarget }) => {
|
||||
const file = Array.from(currentTarget.files ?? []).at(0);
|
||||
|
||||
if (!file) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: reject if the file is unsupported
|
||||
|
||||
if (file.size > maxFileSizeMb * 2 ** 20) {
|
||||
// eslint-disable-next-line no-alert
|
||||
return alert(`The file is too big. Up to ${maxFileSizeMb}MiB.`);
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onload = ({ target }) => {
|
||||
if (typeof target?.result === "string") {
|
||||
onFileRead(target?.result);
|
||||
}
|
||||
};
|
||||
|
||||
reader.readAsText(file);
|
||||
|
||||
// clear selected file to accept the same file again
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
currentTarget.value = "";
|
||||
},
|
||||
[maxFileSizeMb, onFileRead]
|
||||
);
|
||||
|
||||
const icon = useMemo(() => <icons.File size={16} />, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<BaseButton {...props} {...{ icon, iconOnly, onClick }} labelText="Load a file" />
|
||||
<input hidden type="file" {...{ ref, accept, onChange }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
27
components/buttons/paste.tsx
Normal file
27
components/buttons/paste.tsx
Normal file
@@ -0,0 +1,27 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
|
||||
import { icons } from "@/components/icons";
|
||||
|
||||
import { BaseButton, BaseButtonProps } from "./base";
|
||||
|
||||
export type PasteButtonProps = Omit<BaseButtonProps, "icon" | "labelText" | "onClick"> & {
|
||||
onClipboardRead: (text: string) => void;
|
||||
};
|
||||
|
||||
export function PasteButton({ iconOnly, onClipboardRead, ...props }: PasteButtonProps) {
|
||||
const onClick: BaseButtonProps["onClick"] = useCallback(() => {
|
||||
navigator.clipboard
|
||||
.readText()
|
||||
.then(onClipboardRead)
|
||||
.catch(e => {
|
||||
if (e instanceof Error) {
|
||||
// eslint-disable-next-line no-alert
|
||||
alert(e.message);
|
||||
}
|
||||
});
|
||||
}, [onClipboardRead]);
|
||||
|
||||
const icon = useMemo(() => <icons.Clipboard size={16} />, []);
|
||||
|
||||
return <BaseButton {...props} {...{ icon, iconOnly, onClick }} labelText="Paste" />;
|
||||
}
|
||||
Reference in New Issue
Block a user