From 56e28e29e3761aee7cd16a56af89a5fea3982076 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 16 Apr 2026 03:44:54 +0000 Subject: [PATCH] Add main page with sidebar and dev tool routes (Base64, Timestamp) Agent-Logs-Url: https://github.com/slhmy/dev-tools-web/sessions/927cc0c7-a785-4d4d-9d52-2c1af9e5b22a Co-authored-by: slhmy <31381093+slhmy@users.noreply.github.com> --- package-lock.json | 75 ++++++++--- package.json | 1 + src/App.tsx | 42 ++++--- src/components/app-sidebar.tsx | 205 ++++++------------------------- src/components/ui/breadcrumb.tsx | 7 +- src/components/ui/sheet.tsx | 5 +- src/components/ui/sidebar.tsx | 12 +- src/pages/base64.tsx | 74 +++++++++++ src/pages/home.tsx | 10 ++ src/pages/timestamp.tsx | 120 ++++++++++++++++++ src/types/globals.d.ts | 1 + vite.config.ts | 3 + 12 files changed, 345 insertions(+), 210 deletions(-) create mode 100644 src/pages/base64.tsx create mode 100644 src/pages/home.tsx create mode 100644 src/pages/timestamp.tsx create mode 100644 src/types/globals.d.ts diff --git a/package-lock.json b/package-lock.json index 4b34245..87a0e50 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-router-dom": "^7.14.1", "shadcn": "^4.2.0", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", @@ -66,7 +67,6 @@ "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", "license": "MIT", - "peer": true, "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", @@ -1541,7 +1541,6 @@ "resolved": "https://registry.npmjs.org/@noble/ciphers/-/ciphers-1.3.0.tgz", "integrity": "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==", "license": "MIT", - "peer": true, "engines": { "node": "^14.21.3 || >=16" }, @@ -3849,7 +3848,6 @@ "integrity": "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~7.16.0" } @@ -3860,7 +3858,6 @@ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==", "devOptional": true, "license": "MIT", - "peer": true, "dependencies": { "csstype": "^3.2.2" } @@ -3871,7 +3868,6 @@ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", - "peer": true, "peerDependencies": { "@types/react": "^19.2.0" } @@ -3933,7 +3929,6 @@ "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", @@ -4224,7 +4219,6 @@ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -4449,7 +4443,6 @@ } ], "license": "MIT", - "peer": true, "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", @@ -5189,7 +5182,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -5442,7 +5434,6 @@ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz", "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==", "license": "MIT", - "peer": true, "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.1", @@ -6003,7 +5994,6 @@ "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.14.tgz", "integrity": "sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==", "license": "MIT", - "peer": true, "engines": { "node": ">=16.9.0" } @@ -7474,7 +7464,6 @@ "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "prettier": "bin/prettier.cjs" }, @@ -7765,7 +7754,6 @@ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz", "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==", "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -7775,7 +7763,6 @@ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz", "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==", "license": "MIT", - "peer": true, "dependencies": { "scheduler": "^0.27.0" }, @@ -7840,6 +7827,57 @@ } } }, + "node_modules/react-router": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.14.1.tgz", + "integrity": "sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==", + "license": "MIT", + "dependencies": { + "cookie": "^1.0.1", + "set-cookie-parser": "^2.6.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + }, + "peerDependenciesMeta": { + "react-dom": { + "optional": true + } + } + }, + "node_modules/react-router-dom": { + "version": "7.14.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.14.1.tgz", + "integrity": "sha512-ZkrQuwwhGibjQLqH1eCdyiZyLWglPxzxdl5tgwgKEyCSGC76vmAjleGocRe3J/MLfzMUIKwaFJWpFVJhK3d2xA==", + "license": "MIT", + "dependencies": { + "react-router": "7.14.1" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "react": ">=18", + "react-dom": ">=18" + } + }, + "node_modules/react-router/node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, "node_modules/react-style-singleton": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", @@ -8108,6 +8146,12 @@ "url": "https://opencollective.com/express" } }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, "node_modules/setprototypeof": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", @@ -8634,7 +8678,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "devOptional": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -8834,7 +8877,6 @@ "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.2.tgz", "integrity": "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg==", "license": "MIT", - "peer": true, "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", @@ -9155,7 +9197,6 @@ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/colinhacks" } diff --git a/package.json b/package.json index 57e7a62..06e4fb5 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "radix-ui": "^1.4.3", "react": "^19.2.4", "react-dom": "^19.2.4", + "react-router-dom": "^7.14.1", "shadcn": "^4.2.0", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", diff --git a/src/App.tsx b/src/App.tsx index 4fddc48..093c591 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,20 +1,34 @@ -import { Button } from "@/components/ui/button" +import { BrowserRouter, Route, Routes } from "react-router-dom" + +import { AppSidebar } from "@/components/app-sidebar" +import { + SidebarInset, + SidebarProvider, + SidebarTrigger, +} from "@/components/ui/sidebar" +import { Base64Page } from "@/pages/base64" +import { HomePage } from "@/pages/home" +import { TimestampPage } from "@/pages/timestamp" export function App() { return ( -
-
-
-

Project ready!

-

You may now add components and start building.

-

We've already added the button component for you.

- -
-
- (Press d to toggle dark mode) -
-
-
+ + + + +
+ +
+
+ + } /> + } /> + } /> + +
+
+
+
) } diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index 698a476..dd143b4 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -1,5 +1,6 @@ import * as React from "react" - +import { NavLink } from "react-router-dom" +import { WrenchIcon } from "lucide-react" import { Sidebar, SidebarContent, @@ -13,165 +14,35 @@ import { SidebarMenuSubItem, SidebarRail, } from "@/components/ui/sidebar" -import { GalleryVerticalEndIcon } from "lucide-react" -// This is sample data. -const data = { - navMain: [ - { - title: "Getting Started", - url: "#", - items: [ - { - title: "Installation", - url: "#", - }, - { - title: "Project Structure", - url: "#", - }, - ], - }, - { - title: "Build Your Application", - url: "#", - items: [ - { - title: "Routing", - url: "#", - }, - { - title: "Data Fetching", - url: "#", - isActive: true, - }, - { - title: "Rendering", - url: "#", - }, - { - title: "Caching", - url: "#", - }, - { - title: "Styling", - url: "#", - }, - { - title: "Optimizing", - url: "#", - }, - { - title: "Configuring", - url: "#", - }, - { - title: "Testing", - url: "#", - }, - { - title: "Authentication", - url: "#", - }, - { - title: "Deploying", - url: "#", - }, - { - title: "Upgrading", - url: "#", - }, - { - title: "Examples", - url: "#", - }, - ], - }, - { - title: "API Reference", - url: "#", - items: [ - { - title: "Components", - url: "#", - }, - { - title: "File Conventions", - url: "#", - }, - { - title: "Functions", - url: "#", - }, - { - title: "next.config.js Options", - url: "#", - }, - { - title: "CLI", - url: "#", - }, - { - title: "Edge Runtime", - url: "#", - }, - ], - }, - { - title: "Architecture", - url: "#", - items: [ - { - title: "Accessibility", - url: "#", - }, - { - title: "Fast Refresh", - url: "#", - }, - { - title: "Next.js Compiler", - url: "#", - }, - { - title: "Supported Browsers", - url: "#", - }, - { - title: "Turbopack", - url: "#", - }, - ], - }, - { - title: "Community", - url: "#", - items: [ - { - title: "Contribution Guide", - url: "#", - }, - ], - }, - ], -} +const navItems = [ + { + title: "Encoding", + items: [{ title: "Base64", url: "/base64" }], + }, + { + title: "Date & Time", + items: [{ title: "Timestamp", url: "/timestamp" }], + }, +] export function AppSidebar({ ...props }: React.ComponentProps) { - return ( - + return ( - +
- +
- Documentation - v1.0.0 + Dev Tools + + v{APP_VERSION} +
-
+
@@ -179,24 +50,27 @@ export function AppSidebar({ ...props }: React.ComponentProps) { - {data.navMain.map((item) => ( - + {navItems.map((group) => ( + - - {item.title} - + {group.title} - {item.items?.length ? ( - - {item.items.map((item) => ( - - - {item.title} - - - ))} - - ) : null} + + {group.items.map((item) => ( + + + + isActive ? "font-medium text-foreground" : "" + } + > + {item.title} + + + + ))} + ))} @@ -206,3 +80,4 @@ export function AppSidebar({ ...props }: React.ComponentProps) {
) } + diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx index db51b0a..6278e02 100644 --- a/src/components/ui/breadcrumb.tsx +++ b/src/components/ui/breadcrumb.tsx @@ -82,9 +82,7 @@ function BreadcrumbSeparator({ className={cn("[&>svg]:size-3.5", className)} {...props} > - {children ?? ( - - )} + {children ?? } ) } @@ -104,8 +102,7 @@ function BreadcrumbEllipsis({ )} {...props} > - + More ) diff --git a/src/components/ui/sheet.tsx b/src/components/ui/sheet.tsx index 3035c3b..0c4c3aa 100644 --- a/src/components/ui/sheet.tsx +++ b/src/components/ui/sheet.tsx @@ -70,11 +70,10 @@ function SheetContent({ diff --git a/src/components/ui/sidebar.tsx b/src/components/ui/sidebar.tsx index b8b42d8..53e2a8d 100644 --- a/src/components/ui/sidebar.tsx +++ b/src/components/ui/sidebar.tsx @@ -289,9 +289,9 @@ function SidebarRail({ className, ...props }: React.ComponentProps<"button">) { title="Toggle Sidebar" className={cn( "absolute inset-y-0 z-20 hidden w-4 transition-all ease-linear group-data-[side=left]:-right-4 group-data-[side=right]:left-0 after:absolute after:inset-y-0 after:start-1/2 after:w-[2px] hover:after:bg-sidebar-border sm:flex ltr:-translate-x-1/2 rtl:-translate-x-1/2", - "in-data-[side=left]:cursor-w-resize rtl:in-data-[side=left]:cursor-e-resize in-data-[side=right]:cursor-e-resize rtl:in-data-[side=right]:cursor-w-resize", + "in-data-[side=left]:cursor-w-resize in-data-[side=right]:cursor-e-resize rtl:in-data-[side=left]:cursor-e-resize rtl:in-data-[side=right]:cursor-w-resize", "[[data-side=left][data-state=collapsed]_&]:cursor-e-resize rtl:[[data-side=left][data-state=collapsed]_&]:cursor-w-resize [[data-side=right][data-state=collapsed]_&]:cursor-w-resize rtl:[[data-side=right][data-state=collapsed]_&]:cursor-e-resize", - "group-data-[collapsible=offcanvas]:translate-x-0 rtl:group-data-[collapsible=offcanvas]:-translate-x-0 group-data-[collapsible=offcanvas]:after:start-full hover:group-data-[collapsible=offcanvas]:bg-sidebar", + "group-data-[collapsible=offcanvas]:translate-x-0 group-data-[collapsible=offcanvas]:after:start-full hover:group-data-[collapsible=offcanvas]:bg-sidebar rtl:group-data-[collapsible=offcanvas]:-translate-x-0", "[[data-side=left][data-collapsible=offcanvas]_&]:-end-2", "[[data-side=right][data-collapsible=offcanvas]_&]:-start-2", className @@ -421,7 +421,7 @@ function SidebarGroupAction({ data-slot="sidebar-group-action" data-sidebar="group-action" className={cn( - "absolute top-3.5 end-3 flex aspect-square w-5 items-center justify-center rounded-none p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0", + "absolute end-3 top-3.5 flex aspect-square w-5 items-center justify-center rounded-none p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0", className )} {...props} @@ -553,7 +553,7 @@ function SidebarMenuAction({ data-slot="sidebar-menu-action" data-sidebar="menu-action" className={cn( - "absolute top-1.5 end-1 flex aspect-square w-5 items-center justify-center rounded-none p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0", + "absolute end-1 top-1.5 flex aspect-square w-5 items-center justify-center rounded-none p-0 text-sidebar-foreground ring-sidebar-ring outline-hidden transition-transform group-data-[collapsible=icon]:hidden peer-hover/menu-button:text-sidebar-accent-foreground peer-data-[size=default]/menu-button:top-1.5 peer-data-[size=lg]/menu-button:top-2.5 peer-data-[size=sm]/menu-button:top-1 after:absolute after:-inset-2 hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 md:after:hidden [&>svg]:size-4 [&>svg]:shrink-0", showOnHover && "group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 peer-data-active/menu-button:text-sidebar-accent-foreground aria-expanded:opacity-100 md:opacity-0", className @@ -624,7 +624,7 @@ function SidebarMenuSub({ className, ...props }: React.ComponentProps<"ul">) { data-slot="sidebar-menu-sub" data-sidebar="menu-sub" className={cn( - "mx-3.5 flex min-w-0 translate-x-px rtl:-translate-x-px flex-col gap-1 border-s border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden", + "mx-3.5 flex min-w-0 translate-x-px flex-col gap-1 border-s border-sidebar-border px-2.5 py-0.5 group-data-[collapsible=icon]:hidden rtl:-translate-x-px", className )} {...props} @@ -666,7 +666,7 @@ function SidebarMenuSubButton({ data-size={size} data-active={isActive} className={cn( - "flex h-7 min-w-0 -translate-x-px rtl:translate-x-px items-center gap-2 overflow-hidden rounded-none px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-xs data-[size=sm]:text-xs data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", + "flex h-7 min-w-0 -translate-x-px items-center gap-2 overflow-hidden rounded-none px-2 text-sidebar-foreground ring-sidebar-ring outline-hidden group-data-[collapsible=icon]:hidden hover:bg-sidebar-accent hover:text-sidebar-accent-foreground focus-visible:ring-2 active:bg-sidebar-accent active:text-sidebar-accent-foreground disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[size=md]:text-xs data-[size=sm]:text-xs rtl:translate-x-px data-active:bg-sidebar-accent data-active:text-sidebar-accent-foreground [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0 [&>svg]:text-sidebar-accent-foreground", className )} {...props} diff --git a/src/pages/base64.tsx b/src/pages/base64.tsx new file mode 100644 index 0000000..c1980ac --- /dev/null +++ b/src/pages/base64.tsx @@ -0,0 +1,74 @@ +import { useState } from "react" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" + +export function Base64Page() { + const [input, setInput] = useState("") + const [output, setOutput] = useState("") + const [error, setError] = useState("") + + function encode() { + try { + const bytes = new TextEncoder().encode(input) + const binary = String.fromCharCode(...bytes) + setOutput(btoa(binary)) + setError("") + } catch { + setError("Encoding failed. Make sure the input is valid text.") + } + } + + function decode() { + try { + const binary = atob(input) + const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0)) + setOutput(new TextDecoder().decode(bytes)) + setError("") + } catch { + setError("Decoding failed. Make sure the input is valid Base64.") + } + } + + return ( +
+
+

Base64 Converter

+

+ Encode or decode Base64 strings. +

+
+
+ + setInput(e.target.value)} + placeholder="Enter text or Base64 string..." + /> +
+ + +
+
+ {error &&

{error}

} + {output && ( +
+ +
+ {output} +
+ +
+ )} +
+ ) +} diff --git a/src/pages/home.tsx b/src/pages/home.tsx new file mode 100644 index 0000000..220ecdf --- /dev/null +++ b/src/pages/home.tsx @@ -0,0 +1,10 @@ +export function HomePage() { + return ( +
+

Dev Tools

+

+ Select a tool from the sidebar to get started. +

+
+ ) +} diff --git a/src/pages/timestamp.tsx b/src/pages/timestamp.tsx new file mode 100644 index 0000000..883edcc --- /dev/null +++ b/src/pages/timestamp.tsx @@ -0,0 +1,120 @@ +import { useState } from "react" + +import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" + +export function TimestampPage() { + const [tsInput, setTsInput] = useState("") + const [dateInput, setDateInput] = useState("") + const [tsResult, setTsResult] = useState("") + const [dateResult, setDateResult] = useState("") + const [tsError, setTsError] = useState("") + const [dateError, setDateError] = useState("") + + function convertTimestamp() { + const num = Number(tsInput.trim()) + if (!tsInput.trim() || isNaN(num)) { + setTsError("Enter a valid Unix timestamp (seconds or milliseconds).") + setTsResult("") + return + } + // Detect seconds vs milliseconds + const ms = String(num).length <= 10 ? num * 1000 : num + const date = new Date(ms) + if (isNaN(date.getTime())) { + setTsError("Invalid timestamp.") + setTsResult("") + return + } + setTsResult(date.toISOString()) + setTsError("") + } + + function convertDate() { + const date = new Date(dateInput.trim()) + if (isNaN(date.getTime())) { + setDateError("Enter a valid date string (e.g. 2024-01-01 or ISO 8601).") + setDateResult("") + return + } + setDateResult(String(Math.floor(date.getTime() / 1000))) + setDateError("") + } + + function useNow() { + const now = Math.floor(Date.now() / 1000) + setTsInput(String(now)) + setTsResult(new Date(now * 1000).toISOString()) + setTsError("") + } + + return ( +
+
+

Timestamp Converter

+

+ Convert Unix timestamps to dates and vice versa. +

+
+ + {/* Timestamp → Date */} +
+

Unix Timestamp → Date

+
+ setTsInput(e.target.value)} + placeholder="e.g. 1700000000" + /> + + +
+ {tsError &&

{tsError}

} + {tsResult && ( +
+
+ {tsResult} +
+ +
+ )} +
+ + {/* Date → Timestamp */} +
+

Date → Unix Timestamp

+
+ setDateInput(e.target.value)} + placeholder="e.g. 2024-01-01T00:00:00Z" + /> + +
+ {dateError &&

{dateError}

} + {dateResult && ( +
+
+ {dateResult} +
+ +
+ )} +
+
+ ) +} diff --git a/src/types/globals.d.ts b/src/types/globals.d.ts new file mode 100644 index 0000000..0922288 --- /dev/null +++ b/src/types/globals.d.ts @@ -0,0 +1 @@ +declare const APP_VERSION: string diff --git a/vite.config.ts b/vite.config.ts index d4ecee1..bcb7c0f 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -12,4 +12,7 @@ export default defineConfig({ "@": path.resolve(__dirname, "./src"), }, }, + define: { + APP_VERSION: JSON.stringify(process.env.npm_package_version), + }, })