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),
+ },
})