mission-control/app/tools/page.tsx

564 lines
18 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"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>
);
}