| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- "use client";
- import * as React from "react";
- import dynamic from "next/dynamic";
- import { Calendar as CalendarIcon, X } from "lucide-react";
- import { cn } from "@/lib/utils";
- import { Badge } from "@/components/ui/badge";
- import { Button } from "@/components/ui/button";
- import { Input } from "@/components/ui/input";
- import { Label } from "@/components/ui/label";
- import {
- Popover,
- PopoverContent,
- PopoverTrigger,
- } from "@/components/ui/popover";
- import { Skeleton } from "@/components/ui/skeleton";
- import { useSearchDateRangePicker } from "@/lib/frontend/search/useSearchDateRangePicker";
- function CalendarLoading() {
- return (
- <div className="space-y-3">
- <div className="flex gap-4">
- <Skeleton className="h-72 w-72" />
- <Skeleton className="h-72 w-72" />
- </div>
- <p className="text-xs text-muted-foreground text-center">
- Kalender lädt…
- </p>
- </div>
- );
- }
- const Calendar = dynamic(
- () => import("@/components/ui/calendar").then((m) => m.Calendar),
- {
- ssr: false,
- loading: CalendarLoading,
- },
- );
- // Preset chips should look identical in light/dark.
- // We intentionally keep them "white pill" style in both modes (consistent + readable).
- const PRESET_BADGE_CLASS = [
- "bg-white text-black border-border",
- "hover:bg-white/90",
- "dark:bg-white dark:text-black",
- ].join(" ");
- export default function SearchDateRangePicker({
- from,
- to,
- onDateRangeChange,
- isSubmitting,
- className,
- }) {
- const {
- disabled,
- open,
- setOpen,
- activeField,
- setActiveField,
- fromRef,
- toRef,
- month,
- setMonth,
- summary,
- fromDisplay,
- toDisplay,
- presetsRow1,
- presetsRow2,
- calendarKey,
- calendarSelected,
- calendarModifiers,
- calendarModifiersClassNames,
- isRangeInvalid,
- handlePickDay,
- handleClearFrom,
- handleClearTo,
- handleReset,
- applyPreset,
- } = useSearchDateRangePicker({
- from,
- to,
- onDateRangeChange,
- isSubmitting,
- });
- const fromInputId = React.useId();
- const toInputId = React.useId();
- const activeInputClass =
- "border-blue-600 bg-blue-50 dark:border-blue-900 dark:bg-blue-950";
- return (
- <div className={cn("grid gap-2", className)}>
- <Label>Zeitraum</Label>
- <Popover open={open} onOpenChange={setOpen}>
- <PopoverTrigger asChild>
- <Button
- type="button"
- variant="outline"
- disabled={disabled}
- className={cn(
- "w-60 justify-between font-normal",
- !from && !to ? "text-muted-foreground" : "",
- )}
- title="Zeitraum auswählen"
- >
- <span className="truncate">{summary}</span>
- <CalendarIcon className="ml-2 h-4 w-4 opacity-70" />
- </Button>
- </PopoverTrigger>
- <PopoverContent align="start" className="w-fit p-0">
- <div className="w-fit space-y-4 p-4">
- <div className="grid grid-cols-2 gap-4">
- <div className="space-y-1">
- <Label htmlFor={fromInputId}>Von</Label>
- <div className="relative">
- <Input
- id={fromInputId}
- ref={fromRef}
- readOnly
- disabled={disabled}
- value={fromDisplay}
- placeholder="TT.MM.JJJJ"
- className={cn(
- "pr-8",
- activeField === "from" ? activeInputClass : "",
- )}
- onFocus={() => setActiveField("from")}
- onClick={() => setActiveField("from")}
- />
- {from ? (
- <button
- type="button"
- className="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-1 opacity-70 hover:opacity-100"
- onClick={handleClearFrom}
- aria-label="Startdatum löschen"
- title="Startdatum löschen"
- >
- <X className="h-4 w-4" />
- </button>
- ) : null}
- </div>
- </div>
- <div className="space-y-1">
- <Label htmlFor={toInputId}>Bis</Label>
- <div className="relative">
- <Input
- id={toInputId}
- ref={toRef}
- readOnly
- disabled={disabled}
- value={toDisplay}
- placeholder="TT.MM.JJJJ"
- className={cn(
- "pr-8",
- activeField === "to" ? activeInputClass : "",
- )}
- onFocus={() => setActiveField("to")}
- onClick={() => setActiveField("to")}
- />
- {to ? (
- <button
- type="button"
- className="absolute right-2 top-1/2 -translate-y-1/2 rounded-sm p-1 opacity-70 hover:opacity-100"
- onClick={handleClearTo}
- aria-label="Enddatum löschen"
- title="Enddatum löschen"
- >
- <X className="h-4 w-4" />
- </button>
- ) : null}
- </div>
- </div>
- </div>
- <Calendar
- key={calendarKey}
- mode="range"
- numberOfMonths={2}
- captionLayout="dropdown"
- month={month}
- onMonthChange={setMonth}
- selected={calendarSelected}
- modifiers={calendarModifiers}
- modifiersClassNames={calendarModifiersClassNames}
- onDayClick={handlePickDay}
- />
- {isRangeInvalid ? (
- <p className="text-xs text-destructive text-center">
- Das Startdatum darf nicht nach dem Enddatum liegen.
- </p>
- ) : null}
- <div className="space-y-2">
- <div className="text-sm text-muted-foreground">Schnellwahl</div>
- <div className="flex flex-wrap gap-2">
- {presetsRow1.map((p) => (
- <Badge key={p.key} asChild className={PRESET_BADGE_CLASS}>
- <button
- type="button"
- className="cursor-pointer select-none disabled:opacity-60"
- disabled={disabled}
- onClick={() => applyPreset(p)}
- title={p.label}
- >
- {p.label}
- </button>
- </Badge>
- ))}
- </div>
- <div className="flex flex-wrap gap-2">
- {presetsRow2.map((p) => (
- <Badge key={p.key} asChild className={PRESET_BADGE_CLASS}>
- <button
- type="button"
- className="cursor-pointer select-none disabled:opacity-60"
- disabled={disabled}
- onClick={() => applyPreset(p)}
- title={p.label}
- >
- {p.label}
- </button>
- </Badge>
- ))}
- </div>
- </div>
- <div className="flex justify-end">
- <Button
- type="button"
- variant="outline"
- disabled={disabled}
- onClick={handleReset}
- >
- Zurücksetzen
- </Button>
- </div>
- <p className="text-xs text-muted-foreground text-center">
- Tipp: Für einen einzelnen Tag setzen Sie <b>Von</b> und <b>Bis</b>{" "}
- auf dasselbe Datum.
- </p>
- </div>
- </PopoverContent>
- </Popover>
- </div>
- );
- }
|