"use client";
import { useState, useEffect, useMemo } from "react";
import { Globe, Plus, X, Search } from "lucide-react";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { ScrollArea } from "@/components/ui/scroll-area";
import { cn } from "@/lib/utils";
type City = {
id: string;
name: string;
country: string;
timezone: string;
};
const availableCities: City[] = [
{ id: "nyc", name: "New York", country: "USA", timezone: "America/New_York" },
{ id: "lon", name: "London", country: "UK", timezone: "Europe/London" },
{ id: "par", name: "Paris", country: "France", timezone: "Europe/Paris" },
{ id: "tok", name: "Tokyo", country: "Japan", timezone: "Asia/Tokyo" },
{ id: "syd", name: "Sydney", country: "Australia", timezone: "Australia/Sydney" },
{ id: "dub", name: "Dubai", country: "UAE", timezone: "Asia/Dubai" },
{ id: "sin", name: "Singapore", country: "Singapore", timezone: "Asia/Singapore" },
{ id: "ber", name: "Berlin", country: "Germany", timezone: "Europe/Berlin" },
{ id: "lax", name: "Los Angeles", country: "USA", timezone: "America/Los_Angeles" },
];
const defaultCities = ["lon", "tok", "syd"];
export const title = "React Dialog Block World Clock";
export default function DialogWorldClock() {
const [open, setOpen] = useState(false);
const [selectedCityIds, setSelectedCityIds] = useState<string[]>(defaultCities);
const [currentTime, setCurrentTime] = useState(new Date());
const [searchQuery, setSearchQuery] = useState("");
const [showSearch, setShowSearch] = useState(false);
useEffect(() => {
const timer = setInterval(() => {
setCurrentTime(new Date());
}, 1000);
return () => clearInterval(timer);
}, []);
const selectedCities = useMemo(() => {
return selectedCityIds
.map((id) => availableCities.find((c) => c.id === id))
.filter(Boolean) as City[];
}, [selectedCityIds]);
const filteredCities = useMemo(() => {
const query = searchQuery.toLowerCase();
return availableCities.filter(
(city) =>
!selectedCityIds.includes(city.id) &&
(city.name.toLowerCase().includes(query) ||
city.country.toLowerCase().includes(query))
);
}, [searchQuery, selectedCityIds]);
const getTimeInTimezone = (timezone: string) => {
return new Intl.DateTimeFormat("en-US", {
timeZone: timezone,
hour: "numeric",
minute: "2-digit",
hour12: true,
}).format(currentTime);
};
const getTimeDifference = (timezone: string) => {
const targetDate = new Date(
currentTime.toLocaleString("en-US", { timeZone: timezone })
);
const localDate = new Date(currentTime.toLocaleString("en-US"));
const diffMs = targetDate.getTime() - localDate.getTime();
const diffHours = Math.round(diffMs / (1000 * 60 * 60));
if (diffHours === 0) return "Local";
if (diffHours > 0) return `+${diffHours}h`;
return `${diffHours}h`;
};
const handleAddCity = (cityId: string) => {
if (!selectedCityIds.includes(cityId)) {
setSelectedCityIds((prev) => [...prev, cityId]);
}
setSearchQuery("");
setShowSearch(false);
};
const handleRemoveCity = (cityId: string) => {
setSelectedCityIds((prev) => prev.filter((id) => id !== cityId));
};
return (
<Dialog open={open} onOpenChange={setOpen}>
<div className="flex min-h-[350px] items-center justify-center">
<DialogTrigger asChild>
<Button variant="outline">
<Globe className="mr-2 h-4 w-4" />
World Clock
</Button>
</DialogTrigger>
</div>
<DialogContent style={{ maxWidth: "384px" }}>
<DialogHeader>
<DialogTitle>World Clock</DialogTitle>
<DialogDescription>Track time across cities.</DialogDescription>
</DialogHeader>
<div className="py-4">
{showSearch ? (
<div className="space-y-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<Input
placeholder="Search cities..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-9"
autoFocus
/>
</div>
<ScrollArea className="h-[200px]">
<div className="space-y-1">
{filteredCities.map((city) => (
<button
key={city.id}
className="w-full flex items-center justify-between p-2 rounded-md hover:bg-muted transition-colors text-left"
onClick={() => handleAddCity(city.id)}
>
<span className="text-sm">{city.name}</span>
<span className="text-xs text-muted-foreground">
{getTimeInTimezone(city.timezone)}
</span>
</button>
))}
{filteredCities.length === 0 && (
<p className="text-sm text-muted-foreground text-center py-4">
No cities found
</p>
)}
</div>
</ScrollArea>
<Button
variant="outline"
className="w-full"
onClick={() => {
setShowSearch(false);
setSearchQuery("");
}}
>
Cancel
</Button>
</div>
) : (
<div className="space-y-2">
{selectedCities.map((city) => (
<div
key={city.id}
className="group flex items-center justify-between p-2 rounded-md hover:bg-muted/50"
>
<div className="flex items-center gap-3">
<div>
<p className="text-sm font-medium">{city.name}</p>
<p className="text-xs text-muted-foreground">
{getTimeDifference(city.timezone)}
</p>
</div>
</div>
<div className="flex items-center gap-2">
<span className="text-lg font-mono tabular-nums">
{getTimeInTimezone(city.timezone)}
</span>
<Button
variant="ghost"
size="icon"
className={cn(
"h-6 w-6 opacity-0 group-hover:opacity-100 transition-opacity"
)}
onClick={() => handleRemoveCity(city.id)}
>
<X className="h-3 w-3" />
</Button>
</div>
</div>
))}
<Button
variant="outline"
size="sm"
className="w-full mt-2"
onClick={() => setShowSearch(true)}
>
<Plus className="mr-2 h-4 w-4" />
Add City
</Button>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}