Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
28a86dcbff | ||
| 7cd826b052 | |||
|
|
da20e34dc9 | ||
|
|
a43b5a96bb | ||
|
|
48aaa262c1 | ||
| 660839d854 | |||
|
|
6d23d601a8 | ||
|
|
7405f2cb88 | ||
|
|
d415615ad7 | ||
|
|
6cd05ca4b4 | ||
| 97b2093b86 | |||
|
|
c0aa618dfa |
@@ -42,8 +42,8 @@ jobs:
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
cache-from: type=local,src=/cache
|
||||
cache-to: type=local,dest=/cache,mode=max
|
||||
cache-from: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache
|
||||
cache-to: type=registry,ref=${{ secrets.REGISTRY_ENDPOINT }}/${{ github.repository_owner }}/litek:buildcache,mode=max
|
||||
# platforms: linux/amd64,linux/arm64
|
||||
platforms: linux/amd64
|
||||
|
||||
|
||||
@@ -11,7 +11,8 @@ WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
|
||||
# 安装依赖
|
||||
RUN pnpm install --frozen-lockfile
|
||||
RUN --mount=type=cache,target=/root/.local/share/pnpm/store \
|
||||
pnpm install --frozen-lockfile
|
||||
|
||||
# 复制源代码
|
||||
COPY . .
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
{
|
||||
"name": "litek",
|
||||
"private": true,
|
||||
"version": "0.0.2",
|
||||
"version": "0.0.7",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
"preview": "vite preview",
|
||||
"release:patch": "npm version patch && git push --follow-tags",
|
||||
"release:minor": "npm version minor && git push --follow-tags",
|
||||
"release:major": "npm version major && git push --follow-tags"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-dialog": "^1.1.15",
|
||||
@@ -19,9 +22,11 @@
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.548.0",
|
||||
"nanoid": "^5.1.6",
|
||||
"next-themes": "^0.4.6",
|
||||
"react": "^19.1.1",
|
||||
"react-dom": "^19.1.1",
|
||||
"react-router-dom": "^7.9.4",
|
||||
"sonner": "^2.0.7",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.16",
|
||||
"uuid": "^13.0.0"
|
||||
|
||||
28
pnpm-lock.yaml
generated
28
pnpm-lock.yaml
generated
@@ -38,6 +38,9 @@ importers:
|
||||
nanoid:
|
||||
specifier: ^5.1.6
|
||||
version: 5.1.6
|
||||
next-themes:
|
||||
specifier: ^0.4.6
|
||||
version: 0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
react:
|
||||
specifier: ^19.1.1
|
||||
version: 19.2.0
|
||||
@@ -47,6 +50,9 @@ importers:
|
||||
react-router-dom:
|
||||
specifier: ^7.9.4
|
||||
version: 7.9.4(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
sonner:
|
||||
specifier: ^2.0.7
|
||||
version: 2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0)
|
||||
tailwind-merge:
|
||||
specifier: ^3.3.1
|
||||
version: 3.3.1
|
||||
@@ -1280,6 +1286,12 @@ packages:
|
||||
natural-compare@1.4.0:
|
||||
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
|
||||
|
||||
next-themes@0.4.6:
|
||||
resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==}
|
||||
peerDependencies:
|
||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||
|
||||
node-releases@2.0.26:
|
||||
resolution: {integrity: sha512-S2M9YimhSjBSvYnlr5/+umAnPHE++ODwt5e2Ij6FoX45HA/s4vHdkDx1eax2pAPeAOqu4s9b7ppahsyEFdVqQA==}
|
||||
|
||||
@@ -1472,6 +1484,12 @@ packages:
|
||||
resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
sonner@2.0.7:
|
||||
resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==}
|
||||
peerDependencies:
|
||||
react: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
react-dom: ^18.0.0 || ^19.0.0 || ^19.0.0-rc
|
||||
|
||||
source-map-js@1.2.1:
|
||||
resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -2702,6 +2720,11 @@ snapshots:
|
||||
|
||||
natural-compare@1.4.0: {}
|
||||
|
||||
next-themes@0.4.6(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
node-releases@2.0.26: {}
|
||||
|
||||
optionator@0.9.4:
|
||||
@@ -2854,6 +2877,11 @@ snapshots:
|
||||
|
||||
shebang-regex@3.0.0: {}
|
||||
|
||||
sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0):
|
||||
dependencies:
|
||||
react: 19.2.0
|
||||
react-dom: 19.2.0(react@19.2.0)
|
||||
|
||||
source-map-js@1.2.1: {}
|
||||
|
||||
strip-json-comments@3.1.1: {}
|
||||
|
||||
70
src/components/tool/base64.tsx
Normal file
70
src/components/tool/base64.tsx
Normal file
@@ -0,0 +1,70 @@
|
||||
import { useState, type FC } from "react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
import { ArrowLeftIcon, ArrowRightIcon } from "lucide-react";
|
||||
|
||||
const Tool: FC = () => {
|
||||
const [decoded, setDecoded] = useState<string>("");
|
||||
const [encoded, setEncoded] = useState<string>("");
|
||||
|
||||
const encode = () => {
|
||||
try {
|
||||
const encoded64 = btoa(decoded);
|
||||
setEncoded(encoded64);
|
||||
setDecoded("");
|
||||
toast.success("encoded successfully");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.error("encoding failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const decode = () => {
|
||||
try {
|
||||
const decoded64 = atob(encoded);
|
||||
setDecoded(decoded64);
|
||||
setEncoded("");
|
||||
toast.success("decoded successfully");
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.error("decoding failed");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-[50vh] flex flex-row gap-4 pt-[20vh]">
|
||||
<Textarea
|
||||
className="flex-1 resize-none"
|
||||
placeholder="Enter the original text"
|
||||
value={decoded}
|
||||
onChange={(e) => setDecoded(e.target.value)}
|
||||
/>
|
||||
<div className="flex flex-col gap-2 justify-center">
|
||||
<Button onClick={encode}>
|
||||
<ArrowRightIcon className="size-4" />
|
||||
Encode
|
||||
</Button>
|
||||
<Button onClick={decode}>
|
||||
<ArrowLeftIcon className="size-4" />
|
||||
Decode
|
||||
</Button>
|
||||
</div>
|
||||
<Textarea
|
||||
className="flex-1 resize-none"
|
||||
placeholder="Enter the Base64 encoded text"
|
||||
value={encoded}
|
||||
onChange={(e) => setEncoded(e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tool;
|
||||
|
||||
@@ -1,7 +1,17 @@
|
||||
import { Hash } from 'lucide-react'
|
||||
import type { ReactNode } from 'react';
|
||||
import { FileJson, Hash, Binary } from 'lucide-react'
|
||||
|
||||
import type { Tool } from "./type";
|
||||
import { UUID } from './uuid'
|
||||
import UUID from './uuid'
|
||||
import JSON from './json'
|
||||
import Base64 from './base64'
|
||||
|
||||
export interface Tool {
|
||||
path: string;
|
||||
name: string;
|
||||
icon: ReactNode;
|
||||
description: string;
|
||||
component: ReactNode;
|
||||
}
|
||||
|
||||
export const tools: Tool[] = [
|
||||
{
|
||||
@@ -10,5 +20,19 @@ export const tools: Tool[] = [
|
||||
description: "Generate a UUID",
|
||||
icon: <Hash />,
|
||||
component: <UUID />,
|
||||
},
|
||||
{
|
||||
path: "json",
|
||||
name: "JSON Formatter",
|
||||
description: "Format and validate JSON",
|
||||
icon: <FileJson />,
|
||||
component: <JSON />,
|
||||
},
|
||||
{
|
||||
path: "base64",
|
||||
name: "Base64 Encoder/Decoder",
|
||||
description: "Encode and decode Base64",
|
||||
icon: <Binary />,
|
||||
component: <Base64 />,
|
||||
}
|
||||
];
|
||||
48
src/components/tool/json.tsx
Normal file
48
src/components/tool/json.tsx
Normal file
@@ -0,0 +1,48 @@
|
||||
import { useState, type FC } from "react";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { toast } from "sonner";
|
||||
|
||||
const Tool: FC = () => {
|
||||
const [json, setJson] = useState<string>("");
|
||||
|
||||
const validateJson = () => {
|
||||
try {
|
||||
JSON.parse(json);
|
||||
return true;
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof Error) {
|
||||
toast.error(error.message);
|
||||
} else {
|
||||
toast.error("Invalid JSON");
|
||||
}
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const minifyJson = () => {
|
||||
if (!validateJson()) return;
|
||||
const formattedJson = JSON.stringify(JSON.parse(json), null, 0);
|
||||
setJson(formattedJson);
|
||||
toast.success("Minified successfully");
|
||||
};
|
||||
|
||||
const prettifyJson = () => {
|
||||
if (!validateJson()) return;
|
||||
const formattedJson = JSON.stringify(JSON.parse(json), null, 2);
|
||||
setJson(formattedJson);
|
||||
toast.success("JSON prettified successfully");
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col gap-4">
|
||||
<Textarea className="flex-1 w-full resize-none" placeholder="Enter your JSON here" value={json} onChange={(e) => setJson(e.target.value)} />
|
||||
<div className="flex flex-row gap-2">
|
||||
<Button onClick={minifyJson}>Minify</Button>
|
||||
<Button onClick={prettifyJson}>Pretty</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tool;
|
||||
@@ -1,9 +0,0 @@
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
export interface Tool {
|
||||
path: string;
|
||||
name: string;
|
||||
icon: ReactNode;
|
||||
description: string;
|
||||
component: ReactNode;
|
||||
}
|
||||
@@ -3,7 +3,7 @@ import { type FC } from "react";
|
||||
import * as uuid from 'uuid'
|
||||
import { nanoid } from 'nanoid'
|
||||
|
||||
export const UUID: FC = () => {
|
||||
const Tool: FC = () => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4">
|
||||
<span className="text-sm text-muted-foreground">Refresh the page to generate new UUID</span>
|
||||
@@ -20,3 +20,5 @@ export const UUID: FC = () => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tool;
|
||||
66
src/components/ui/alert.tsx
Normal file
66
src/components/ui/alert.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
import * as React from "react"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const alertVariants = cva(
|
||||
"relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current",
|
||||
{
|
||||
variants: {
|
||||
variant: {
|
||||
default: "bg-card text-card-foreground",
|
||||
destructive:
|
||||
"text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
variant: "default",
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
function Alert({
|
||||
className,
|
||||
variant,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & VariantProps<typeof alertVariants>) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert"
|
||||
role="alert"
|
||||
className={cn(alertVariants({ variant }), className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-title"
|
||||
className={cn(
|
||||
"col-start-2 line-clamp-1 min-h-4 font-medium tracking-tight",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function AlertDescription({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"div">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="alert-description"
|
||||
className={cn(
|
||||
"text-muted-foreground col-start-2 grid justify-items-start gap-1 text-sm [&_p]:leading-relaxed",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Alert, AlertTitle, AlertDescription }
|
||||
38
src/components/ui/sonner.tsx
Normal file
38
src/components/ui/sonner.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
CircleCheckIcon,
|
||||
InfoIcon,
|
||||
Loader2Icon,
|
||||
OctagonXIcon,
|
||||
TriangleAlertIcon,
|
||||
} from "lucide-react"
|
||||
import { useTheme } from "next-themes"
|
||||
import { Toaster as Sonner, type ToasterProps } from "sonner"
|
||||
|
||||
const Toaster = ({ ...props }: ToasterProps) => {
|
||||
const { theme = "system" } = useTheme()
|
||||
|
||||
return (
|
||||
<Sonner
|
||||
theme={theme as ToasterProps["theme"]}
|
||||
className="toaster group"
|
||||
icons={{
|
||||
success: <CircleCheckIcon className="size-4" />,
|
||||
info: <InfoIcon className="size-4" />,
|
||||
warning: <TriangleAlertIcon className="size-4" />,
|
||||
error: <OctagonXIcon className="size-4" />,
|
||||
loading: <Loader2Icon className="size-4 animate-spin" />,
|
||||
}}
|
||||
style={
|
||||
{
|
||||
"--normal-bg": "var(--popover)",
|
||||
"--normal-text": "var(--popover-foreground)",
|
||||
"--normal-border": "var(--border)",
|
||||
"--border-radius": "var(--radius)",
|
||||
} as React.CSSProperties
|
||||
}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Toaster }
|
||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -7,12 +7,12 @@ import { AppSidebar } from "@/components/sidebar";
|
||||
export const Layout: FC = () => (
|
||||
<SidebarProvider>
|
||||
<AppSidebar />
|
||||
<div className="p-4 flex flex-col w-full h-[100vh]">
|
||||
<div className="p-4 flex flex-col w-full h-[100vh] overflow-hidden">
|
||||
<nav className="flex items-center justify-between">
|
||||
<SidebarTrigger className="size-10" />
|
||||
<div role="actions" />
|
||||
</nav>
|
||||
<main className="flex-1 overflow-auto p-4">
|
||||
<main className="flex-1 overflow-auto p-4 overflow-hidden">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
|
||||
import { Toaster } from '@/components/ui/sonner'
|
||||
|
||||
import './index.css'
|
||||
import { AppRouter } from './router'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<AppRouter />
|
||||
<Toaster />
|
||||
</StrictMode>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user