564 lines
18 KiB
TypeScript
564 lines
18 KiB
TypeScript
"use client";
|
||
|
||
import { DashboardLayout } from "@/components/layout/sidebar";
|
||
import { PageHeader } from "@/components/layout/page-header";
|
||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
||
import { Button } from "@/components/ui/button";
|
||
import { Input } from "@/components/ui/input";
|
||
import { Separator } from "@/components/ui/separator";
|
||
import {
|
||
Calculator,
|
||
Clock,
|
||
Link2,
|
||
ExternalLink,
|
||
Github,
|
||
Globe,
|
||
Cloud,
|
||
Database,
|
||
Play,
|
||
Square,
|
||
RotateCcw,
|
||
Copy,
|
||
Check
|
||
} from "lucide-react";
|
||
import { useState, useEffect, useCallback, useRef } from "react";
|
||
|
||
// ============================================================================
|
||
// Types
|
||
// ============================================================================
|
||
|
||
interface QuickLink {
|
||
id: string;
|
||
name: string;
|
||
url: string;
|
||
icon: React.ReactNode;
|
||
color: string;
|
||
}
|
||
|
||
// ============================================================================
|
||
// Constants
|
||
// ============================================================================
|
||
|
||
const QUICK_LINKS: QuickLink[] = [
|
||
{
|
||
id: "github",
|
||
name: "GitHub",
|
||
url: "https://github.com",
|
||
icon: <Github className="w-4 h-4" />,
|
||
color: "bg-slate-800"
|
||
},
|
||
{
|
||
id: "vercel",
|
||
name: "Vercel",
|
||
url: "https://vercel.com",
|
||
icon: <Cloud className="w-4 h-4" />,
|
||
color: "bg-black"
|
||
},
|
||
{
|
||
id: "supabase",
|
||
name: "Supabase",
|
||
url: "https://supabase.com",
|
||
icon: <Database className="w-4 h-4" />,
|
||
color: "bg-emerald-600"
|
||
},
|
||
{
|
||
id: "gantt",
|
||
name: "Gantt Board",
|
||
url: "https://gantt-board.vercel.app",
|
||
icon: <Clock className="w-4 h-4" />,
|
||
color: "bg-blue-600"
|
||
},
|
||
{
|
||
id: "google",
|
||
name: "Google",
|
||
url: "https://google.com",
|
||
icon: <Globe className="w-4 h-4" />,
|
||
color: "bg-red-500"
|
||
},
|
||
];
|
||
|
||
// ============================================================================
|
||
// Calculator Component
|
||
// ============================================================================
|
||
|
||
function CalculatorTool() {
|
||
const [display, setDisplay] = useState("0");
|
||
const [previousValue, setPreviousValue] = useState<number | null>(null);
|
||
const [operation, setOperation] = useState<string | null>(null);
|
||
const [waitingForOperand, setWaitingForOperand] = useState(false);
|
||
const [copied, setCopied] = useState(false);
|
||
|
||
const clear = useCallback(() => {
|
||
setDisplay("0");
|
||
setPreviousValue(null);
|
||
setOperation(null);
|
||
setWaitingForOperand(false);
|
||
}, []);
|
||
|
||
const inputDigit = useCallback((digit: string) => {
|
||
if (waitingForOperand) {
|
||
setDisplay(digit);
|
||
setWaitingForOperand(false);
|
||
} else {
|
||
setDisplay(display === "0" ? digit : display + digit);
|
||
}
|
||
}, [display, waitingForOperand]);
|
||
|
||
const inputDecimal = useCallback(() => {
|
||
if (waitingForOperand) {
|
||
setDisplay("0.");
|
||
setWaitingForOperand(false);
|
||
} else if (display.indexOf(".") === -1) {
|
||
setDisplay(display + ".");
|
||
}
|
||
}, [display, waitingForOperand]);
|
||
|
||
const performOperation = useCallback((nextOperation: string) => {
|
||
const inputValue = parseFloat(display);
|
||
|
||
if (previousValue === null) {
|
||
setPreviousValue(inputValue);
|
||
} else if (operation) {
|
||
const currentValue = previousValue || 0;
|
||
const newValue = calculate(currentValue, inputValue, operation);
|
||
setPreviousValue(newValue);
|
||
setDisplay(String(newValue));
|
||
}
|
||
|
||
setWaitingForOperand(true);
|
||
setOperation(nextOperation);
|
||
}, [display, operation, previousValue]);
|
||
|
||
const calculate = (firstValue: number, secondValue: number, op: string): number => {
|
||
switch (op) {
|
||
case "+": return firstValue + secondValue;
|
||
case "-": return firstValue - secondValue;
|
||
case "×": return firstValue * secondValue;
|
||
case "÷": return secondValue !== 0 ? firstValue / secondValue : 0;
|
||
default: return secondValue;
|
||
}
|
||
};
|
||
|
||
const performCalculation = useCallback(() => {
|
||
const inputValue = parseFloat(display);
|
||
|
||
if (previousValue !== null && operation) {
|
||
const newValue = calculate(previousValue, inputValue, operation);
|
||
setDisplay(String(newValue));
|
||
setPreviousValue(null);
|
||
setOperation(null);
|
||
setWaitingForOperand(true);
|
||
}
|
||
}, [display, operation, previousValue]);
|
||
|
||
const copyResult = useCallback(() => {
|
||
navigator.clipboard.writeText(display);
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 1500);
|
||
}, [display]);
|
||
|
||
const buttons = [
|
||
{ label: "C", onClick: clear, className: "bg-red-500/10 text-red-500 hover:bg-red-500/20" },
|
||
{ label: "±", onClick: () => setDisplay(String(parseFloat(display) * -1)), className: "bg-muted" },
|
||
{ label: "%", onClick: () => setDisplay(String(parseFloat(display) / 100)), className: "bg-muted" },
|
||
{ label: "÷", onClick: () => performOperation("÷"), className: "bg-primary/10 text-primary" },
|
||
{ label: "7", onClick: () => inputDigit("7") },
|
||
{ label: "8", onClick: () => inputDigit("8") },
|
||
{ label: "9", onClick: () => inputDigit("9") },
|
||
{ label: "×", onClick: () => performOperation("×"), className: "bg-primary/10 text-primary" },
|
||
{ label: "4", onClick: () => inputDigit("4") },
|
||
{ label: "5", onClick: () => inputDigit("5") },
|
||
{ label: "6", onClick: () => inputDigit("6") },
|
||
{ label: "-", onClick: () => performOperation("-"), className: "bg-primary/10 text-primary" },
|
||
{ label: "1", onClick: () => inputDigit("1") },
|
||
{ label: "2", onClick: () => inputDigit("2") },
|
||
{ label: "3", onClick: () => inputDigit("3") },
|
||
{ label: "+", onClick: () => performOperation("+"), className: "bg-primary/10 text-primary" },
|
||
{ label: "0", onClick: () => inputDigit("0"), className: "col-span-2" },
|
||
{ label: ".", onClick: inputDecimal },
|
||
{ label: "=", onClick: performCalculation, className: "bg-primary text-primary-foreground hover:bg-primary/90" },
|
||
];
|
||
|
||
return (
|
||
<Card className="h-full">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="flex items-center gap-2 text-base">
|
||
<Calculator className="w-4 h-4" />
|
||
Calculator
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent>
|
||
<div className="bg-muted rounded-lg p-4 mb-4">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-2xl sm:text-3xl font-mono font-bold truncate">
|
||
{display}
|
||
</span>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={copyResult}
|
||
className="shrink-0"
|
||
aria-label="Copy result"
|
||
>
|
||
{copied ? <Check className="w-4 h-4 text-green-500" /> : <Copy className="w-4 h-4" />}
|
||
</Button>
|
||
</div>
|
||
{operation && (
|
||
<span className="text-xs text-muted-foreground">
|
||
{previousValue} {operation}
|
||
</span>
|
||
)}
|
||
</div>
|
||
<div className="grid grid-cols-4 gap-2">
|
||
{buttons.map((btn) => (
|
||
<Button
|
||
key={btn.label}
|
||
variant="outline"
|
||
onClick={btn.onClick}
|
||
className={`h-10 sm:h-12 text-base font-medium ${btn.className || ""} ${btn.label === "0" ? "col-span-2" : ""}`}
|
||
>
|
||
{btn.label}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Timer Component
|
||
// ============================================================================
|
||
|
||
function TimerTool() {
|
||
const [seconds, setSeconds] = useState(0);
|
||
const [isRunning, setIsRunning] = useState(false);
|
||
const intervalRef = useRef<NodeJS.Timeout | null>(null);
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (intervalRef.current) {
|
||
clearInterval(intervalRef.current);
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
const start = useCallback(() => {
|
||
if (!isRunning) {
|
||
setIsRunning(true);
|
||
intervalRef.current = setInterval(() => {
|
||
setSeconds((s) => s + 1);
|
||
}, 1000);
|
||
}
|
||
}, [isRunning]);
|
||
|
||
const stop = useCallback(() => {
|
||
if (intervalRef.current) {
|
||
clearInterval(intervalRef.current);
|
||
intervalRef.current = null;
|
||
}
|
||
setIsRunning(false);
|
||
}, []);
|
||
|
||
const reset = useCallback(() => {
|
||
stop();
|
||
setSeconds(0);
|
||
}, [stop]);
|
||
|
||
const formatTime = (totalSeconds: number) => {
|
||
const hours = Math.floor(totalSeconds / 3600);
|
||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||
const secs = totalSeconds % 60;
|
||
return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}:${secs.toString().padStart(2, "0")}`;
|
||
};
|
||
|
||
return (
|
||
<Card className="h-full">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="flex items-center gap-2 text-base">
|
||
<Clock className="w-4 h-4" />
|
||
Timer
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="flex flex-col items-center">
|
||
<div className="text-5xl sm:text-6xl font-mono font-bold mb-6 tracking-wider">
|
||
{formatTime(seconds)}
|
||
</div>
|
||
<div className="flex gap-2 w-full">
|
||
<Button
|
||
onClick={start}
|
||
disabled={isRunning}
|
||
className="flex-1"
|
||
variant={isRunning ? "outline" : "default"}
|
||
>
|
||
<Play className="w-4 h-4 mr-2" />
|
||
Start
|
||
</Button>
|
||
<Button
|
||
onClick={stop}
|
||
disabled={!isRunning}
|
||
variant="secondary"
|
||
className="flex-1"
|
||
>
|
||
<Square className="w-4 h-4 mr-2" />
|
||
Stop
|
||
</Button>
|
||
<Button
|
||
onClick={reset}
|
||
variant="outline"
|
||
className="flex-1"
|
||
>
|
||
<RotateCcw className="w-4 h-4 mr-2" />
|
||
Reset
|
||
</Button>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Quick Links Component
|
||
// ============================================================================
|
||
|
||
function QuickLinksTool() {
|
||
const [customLinks, setCustomLinks] = useState<QuickLink[]>([]);
|
||
const [newLinkName, setNewLinkName] = useState("");
|
||
const [newLinkUrl, setNewLinkUrl] = useState("");
|
||
|
||
const addCustomLink = useCallback(() => {
|
||
if (newLinkName && newLinkUrl) {
|
||
const url = newLinkUrl.startsWith("http") ? newLinkUrl : `https://${newLinkUrl}`;
|
||
setCustomLinks((prev) => [
|
||
...prev,
|
||
{
|
||
id: Date.now().toString(),
|
||
name: newLinkName,
|
||
url,
|
||
icon: <Link2 className="w-4 h-4" />,
|
||
color: "bg-muted",
|
||
},
|
||
]);
|
||
setNewLinkName("");
|
||
setNewLinkUrl("");
|
||
}
|
||
}, [newLinkName, newLinkUrl]);
|
||
|
||
const removeCustomLink = useCallback((id: string) => {
|
||
setCustomLinks((prev) => prev.filter((link) => link.id !== id));
|
||
}, []);
|
||
|
||
return (
|
||
<Card className="h-full">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="flex items-center gap-2 text-base">
|
||
<Link2 className="w-4 h-4" />
|
||
Quick Links
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{/* Default Links */}
|
||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||
{QUICK_LINKS.map((link) => (
|
||
<a
|
||
key={link.id}
|
||
href={link.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className={`flex items-center gap-2 p-2 rounded-lg ${link.color} text-white hover:opacity-90 transition-opacity`}
|
||
>
|
||
{link.icon}
|
||
<span className="text-sm font-medium truncate">{link.name}</span>
|
||
</a>
|
||
))}
|
||
</div>
|
||
|
||
<Separator />
|
||
|
||
{/* Custom Links */}
|
||
{customLinks.length > 0 && (
|
||
<div className="space-y-2">
|
||
<p className="text-xs text-muted-foreground">Custom Links</p>
|
||
<div className="space-y-1">
|
||
{customLinks.map((link) => (
|
||
<div key={link.id} className="flex items-center gap-2">
|
||
<a
|
||
href={link.url}
|
||
target="_blank"
|
||
rel="noopener noreferrer"
|
||
className="flex items-center gap-2 flex-1 p-2 rounded-lg bg-muted hover:bg-accent transition-colors"
|
||
>
|
||
{link.icon}
|
||
<span className="text-sm truncate">{link.name}</span>
|
||
<ExternalLink className="w-3 h-3 ml-auto text-muted-foreground" />
|
||
</a>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
className="h-8 w-8 shrink-0"
|
||
onClick={() => removeCustomLink(link.id)}
|
||
>
|
||
×
|
||
</Button>
|
||
</div>
|
||
))}
|
||
</div>
|
||
</div>
|
||
)}
|
||
|
||
{/* Add Custom Link */}
|
||
<div className="space-y-2">
|
||
<p className="text-xs text-muted-foreground">Add Custom Link</p>
|
||
<div className="flex gap-2">
|
||
<Input
|
||
placeholder="Name"
|
||
value={newLinkName}
|
||
onChange={(e) => setNewLinkName(e.target.value)}
|
||
className="flex-1"
|
||
/>
|
||
<Input
|
||
placeholder="URL"
|
||
value={newLinkUrl}
|
||
onChange={(e) => setNewLinkUrl(e.target.value)}
|
||
className="flex-1"
|
||
/>
|
||
<Button onClick={addCustomLink} disabled={!newLinkName || !newLinkUrl}>
|
||
Add
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Password Generator Component
|
||
// ============================================================================
|
||
|
||
function PasswordGeneratorTool() {
|
||
const [password, setPassword] = useState("");
|
||
const [length, setLength] = useState(16);
|
||
const [includeUppercase, setIncludeUppercase] = useState(true);
|
||
const [includeNumbers, setIncludeNumbers] = useState(true);
|
||
const [includeSymbols, setIncludeSymbols] = useState(true);
|
||
const [copied, setCopied] = useState(false);
|
||
|
||
const generatePassword = useCallback(() => {
|
||
const lowercase = "abcdefghijklmnopqrstuvwxyz";
|
||
const uppercase = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||
const numbers = "0123456789";
|
||
const symbols = "!@#$%^&*()_+-=[]{}|;:,.<>?";
|
||
|
||
let chars = lowercase;
|
||
if (includeUppercase) chars += uppercase;
|
||
if (includeNumbers) chars += numbers;
|
||
if (includeSymbols) chars += symbols;
|
||
|
||
let result = "";
|
||
for (let i = 0; i < length; i++) {
|
||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
||
}
|
||
setPassword(result);
|
||
}, [length, includeUppercase, includeNumbers, includeSymbols]);
|
||
|
||
const copyPassword = useCallback(() => {
|
||
if (password) {
|
||
navigator.clipboard.writeText(password);
|
||
setCopied(true);
|
||
setTimeout(() => setCopied(false), 1500);
|
||
}
|
||
}, [password]);
|
||
|
||
return (
|
||
<Card className="h-full">
|
||
<CardHeader className="pb-3">
|
||
<CardTitle className="flex items-center gap-2 text-base">
|
||
<RotateCcw className="w-4 h-4" />
|
||
Password Generator
|
||
</CardTitle>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
<div className="flex gap-2">
|
||
<Input
|
||
value={password}
|
||
readOnly
|
||
placeholder="Click generate..."
|
||
className="font-mono text-sm"
|
||
/>
|
||
<Button onClick={copyPassword} disabled={!password} variant="outline">
|
||
{copied ? <Check className="w-4 h-4" /> : <Copy className="w-4 h-4" />}
|
||
</Button>
|
||
</div>
|
||
|
||
<div className="space-y-3">
|
||
<div className="flex items-center justify-between">
|
||
<span className="text-sm">Length: {length}</span>
|
||
<input
|
||
type="range"
|
||
min="8"
|
||
max="32"
|
||
value={length}
|
||
onChange={(e) => setLength(Number(e.target.value))}
|
||
className="w-24 sm:w-32"
|
||
/>
|
||
</div>
|
||
|
||
<div className="flex flex-wrap gap-2">
|
||
<Button
|
||
variant={includeUppercase ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => setIncludeUppercase(!includeUppercase)}
|
||
>
|
||
ABC
|
||
</Button>
|
||
<Button
|
||
variant={includeNumbers ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => setIncludeNumbers(!includeNumbers)}
|
||
>
|
||
123
|
||
</Button>
|
||
<Button
|
||
variant={includeSymbols ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => setIncludeSymbols(!includeSymbols)}
|
||
>
|
||
!@#
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
|
||
<Button onClick={generatePassword} className="w-full">
|
||
<RotateCcw className="w-4 h-4 mr-2" />
|
||
Generate Password
|
||
</Button>
|
||
</CardContent>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
// ============================================================================
|
||
// Main Page
|
||
// ============================================================================
|
||
|
||
export default function ToolsPage() {
|
||
return (
|
||
<DashboardLayout>
|
||
<div className="space-y-6">
|
||
<PageHeader
|
||
title="Tools"
|
||
description="Handy utilities for everyday tasks"
|
||
/>
|
||
|
||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 sm:gap-6">
|
||
<CalculatorTool />
|
||
<TimerTool />
|
||
<QuickLinksTool />
|
||
<PasswordGeneratorTool />
|
||
</div>
|
||
</div>
|
||
</DashboardLayout>
|
||
);
|
||
}
|