Files
Website/src/components/ui/datetime-picker/DateTimePicker.svelte
2025-03-01 20:00:46 +01:00

130 lines
4.7 KiB
Svelte

<!--
- This file is a part of the SteamWar software.
-
- Copyright (C) 2025 SteamWar.de-Serverteam
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU Affero General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU Affero General Public License for more details.
-
- You should have received a copy of the GNU Affero General Public License
- along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<script lang="ts">
import { Calendar } from "@components/ui/calendar";
import { Button } from "@components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "$lib/components/ui/popover";
import { ScrollArea } from "$lib/components/ui/scroll-area";
import { CalendarIcon } from "lucide-svelte";
import { cn } from "@components/utils";
import type {ZonedDateTime} from "@internationalized/date";
let {
value = $bindable(),
onChange
}: {
value: ZonedDateTime
onChange?: ((date: ZonedDateTime | undefined) => void) | undefined
} = $props();
let isOpen = $state(false);
const hours = Array.from({ length: 24 }, (_, i) => i);
function handleDateSelect(selectedDate: ZonedDateTime) {
if (selectedDate) {
value = selectedDate;
if (onChange) {
onChange(value);
}
}
}
function handleTimeChange(type: "hour" | "minute", change: number) {
if (change !== undefined) {
if (type === "hour") {
value = value.set({ hour: change });
} else if (type === "minute") {
value = value.set({ minute: change });
}
if (onChange) {
onChange(value);
}
}
}
</script>
<Popover bind:open={isOpen}>
<PopoverTrigger>
<Button
variant="outline"
class={cn(
"w-full justify-start text-left font-normal",
!value && "text-muted-foreground"
)}
>
<CalendarIcon class="mr-2 h-4 w-4" />
{#if value}
{new Intl.DateTimeFormat("de-DE", {
day: "2-digit",
month: "2-digit",
year: "numeric",
hour: "2-digit",
minute: "2-digit",
}).format(value.toDate())}
{:else}
<span>DD.MM.YYYY, hh:mm</span>
{/if}
</Button>
</PopoverTrigger>
<PopoverContent class="w-auto p-0">
<div class="sm:flex">
<Calendar
mode="single"
bind:value
onValueChange={(date) => handleDateSelect(date)}
initialFocus
/>
<div class="flex flex-col sm:flex-row sm:h-[300px] divide-y sm:divide-y-0 sm:divide-x">
<ScrollArea class="w-64 sm:w-auto">
<div class="flex sm:flex-col p-2">
{#each [...hours].reverse() as hour}
<Button
size="icon"
variant={value && value.hour === hour ? "default" : "ghost"}
class="sm:w-full shrink-0 aspect-square"
onclick={() => handleTimeChange("hour", hour)}
>
{hour}
</Button>
{/each}
</div>
</ScrollArea>
<ScrollArea class="w-64 sm:w-auto">
<div class="flex sm:flex-col p-2">
{#each Array.from({ length: 60 }, (_, i) => i) as minute}
<Button
size="icon"
variant={value && value.minute === minute ? "default" : "ghost"}
class="sm:w-full shrink-0 aspect-square"
onclick={() => handleTimeChange("minute", minute)}
>
{minute.toString().padStart(2, '0')}
</Button>
{/each}
</div>
</ScrollArea>
</div>
</div>
</PopoverContent>
</Popover>