feat: replace calendar date inputs with a wheel-style date picker
Adds DateWheelPicker (src/components/ui/date-wheel-picker.tsx) — an iOS-style bottom-sheet with scroll-snap day/month/year columns — and swaps out the 6 remaining <input type="date"> usages in PetForm (birthday, adoptedAt) and HealthDashboard (weight log, vet visit, vaccine given/due dates). Closes the last open DEBUG-List.md item. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
f14be58c06
commit
f14af09b58
6 files changed
+288
-30
No files matched your search
+20
-14
@@ -1,34 +1,40 @@
|
||||
Debug:
|
||||
|
||||
~~\-Die Suchen seite ist noch nicht auf Deutsch übersetzt.~~ ✅ ERLEDIGT
|
||||
~~-Die Suchen seite ist noch nicht auf Deutsch übersetzt.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Die Erwähnfunktion mittels @ ist impelmentiert, aber es fehlt die Benachrichtung das man Erwähnt wurde ! Zudem ist die Auswahl nach dem tippen @ schlecht designed man kann gerade so den Avatar erkennen aber nicht den namen in der aufploppenden suche, bitte um FIX !~~ ✅ ERLEDIGT (Dropdown-Design + Benachrichtigung + Eingabefeld-Breite)
|
||||
~~-Die Erwähnfunktion mittels @ ist impelmentiert, aber es fehlt die Benachrichtung das man Erwähnt wurde ! Zudem ist die Auswahl nach dem tippen @ schlecht designed man kann gerade so den Avatar erkennen aber nicht den namen in der aufploppenden suche, bitte um FIX !~~ ✅ ERLEDIGT (Dropdown-Design + Benachrichtigung + Eingabefeld-Breite)
|
||||
|
||||
~~\-Messages ist noch nicht auf Deutsch übersetzt.~~ ✅ ERLEDIGT
|
||||
~~-Messages ist noch nicht auf Deutsch übersetzt.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Kommentar Update; wenn man einen Kommentar postet, muss dieser zwingend sofort im Kommentar-Verlauf sichtbar werden (Optische Bestätigung das der Kommentar angekommen ist für den Nutzer), es muss sich als Nutzer Live anfühlen auch wenn der Server noch am arbeiten ist.~~ ✅ ERLEDIGT
|
||||
~~-Kommentar Update; wenn man einen Kommentar postet, muss dieser zwingend sofort im Kommentar-Verlauf sichtbar werden (Optische Bestätigung das der Kommentar angekommen ist für den Nutzer), es muss sich als Nutzer Live anfühlen auch wenn der Server noch am arbeiten ist.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Benachrichtung wenn man "Paw-Back this post" nutzt für den Original-User des Posts.~~ ✅ ERLEDIGT
|
||||
~~-Benachrichtung wenn man "Paw-Back this post" nutzt für den Original-User des Posts.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Die Mobil Ansicht ist noch nicht ausgereift, das Menü ist teilweise nicht mehr erkennbar im unteren Segemet ( Startseite, Entdecken, +, Benachrichtigungen, Profil).~~ ✅ ERLEDIGT
|
||||
~~-Die Mobil Ansicht ist noch nicht ausgereift, das Menü ist teilweise nicht mehr erkennbar im unteren Segemet ( Startseite, Entdecken, +, Benachrichtigungen, Profil).~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Mobilansicht - Add Story Button ist sehr linksbündig, keine Aufteilung vorhanden.~~ ✅ ERLEDIGT
|
||||
~~-Mobilansicht - Add Story Button ist sehr linksbündig, keine Aufteilung vorhanden.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Mobilansicht - Kein PawFeed Logo sichtbar, soll nur auf der Startseite erscheinen beim scrollen verschwinden.~~ ✅ ERLEDIGT
|
||||
~~-Mobilansicht - Kein PawFeed Logo sichtbar, soll nur auf der Startseite erscheinen beim scrollen verschwinden.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Mobilansicht - wenn man bis man hin und her scrollt soll man bei langem hochscrollen einen refresh auslösen.~~ ✅ ERLEDIGT
|
||||
~~-Mobilansicht - wenn man bis man hin und her scrollt soll man bei langem hochscrollen einen refresh auslösen.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Mobilansicht - Impressum/Datenschutz in das Profil rein legen, damit man es erreichen kann.~~ ✅ ERLEDIGT
|
||||
~~-Mobilansicht - Impressum/Datenschutz in das Profil rein legen, damit man es erreichen kann.~~ ✅ ERLEDIGT
|
||||
|
||||
~~-Mobilansicht - Nachrichten nicht aufrufbar weil kein Button vorhanden. kann gerne als Mail-Icon mitscrollend oben rechts in die ecke damit es immer erreichbar ist.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Mobilansicht - Explore Grid - bei langen Rassenamen drückt der Rassenamen aus dem Grid heraus. Anpassen entweder Grid auf 2Reihig oder Rassenamen werden mit Absatz angezeigt.~~ ✅ ERLEDIGT
|
||||
~~-Mobilansicht - Explore Grid - bei langen Rassenamen drückt der Rassenamen aus dem Grid heraus. Anpassen entweder Grid auf 2Reihig oder Rassenamen werden mit Absatz angezeigt.~~ ✅ ERLEDIGT
|
||||
|
||||
|
||||
|
||||
~~\-Kommentarbereich dynamisch: bei wenigen Kommentaren nimmt der Bereich den halben Screen ein, soll sich anpassen und erst ab einer gewissen Höhe scrollbar werden.~~ ✅ ERLEDIGT
|
||||
~~-Kommentarbereich dynamisch: bei wenigen Kommentaren nimmt der Bereich den halben Screen ein, soll sich anpassen und erst ab einer gewissen Höhe scrollbar werden.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-Mobilansicht - Chat-Eingabe nicht direkt sichtbar: Innerhalb eines Chats muss man scrollen um zur Texteingabe zu gelangen. Die Eingabeleiste soll fixiert am unteren Bildschirmrand bleiben (sticky/fixed), sodass man ohne Scrollen direkt antworten kann.~~ ✅ ERLEDIGT
|
||||
~~-Mobilansicht - Chat-Eingabe nicht direkt sichtbar: Innerhalb eines Chats muss man scrollen um zur Texteingabe zu gelangen. Die Eingabeleiste soll fixiert am unteren Bildschirmrand bleiben (sticky/fixed), sodass man ohne Scrollen direkt antworten kann.~~ ✅ ERLEDIGT
|
||||
|
||||
~~\-User Delete Funktion, für den Unwahrscheinlichen fall ein User möchte sein Konto löschen wird ein Workflow benötigt, der nach einer Bestätigung mittels Tiernamen Eingabe (Auswahl eins von möglichen) den Kompletten Account löscht, sowie alle Hochgeladen Bilder/Videos und erzeugten Posts, Rückstandslos.~~ ✅ ERLEDIGT
|
||||
~~-User Delete Funktion, für den Unwahrscheinlichen fall ein User möchte sein Konto löschen wird ein Workflow benötigt, der nach einer Bestätigung mittels Tiernamen Eingabe (Auswahl eins von möglichen) den Kompletten Account löscht, sowie alle Hochgeladen Bilder/Videos und erzeugten Posts, Rückstandslos.~~ ✅ ERLEDIGT
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
~~-Alle Kalender-Optionen in allen Karten so abändern, das statt eines Kalenders, ein Data-Wheel-Picker erscheint, das macht die Anwendung einfacher.~~ ✅ ERLEDIGT
|
||||
|
||||
@@ -80,6 +80,10 @@
|
||||
"changesSaved": "Änderungen gespeichert",
|
||||
"error": "Etwas ist schiefgelaufen. Bitte versuche es erneut."
|
||||
},
|
||||
"DateWheelPicker": {
|
||||
"selectDate": "Datum auswählen",
|
||||
"apply": "Übernehmen"
|
||||
},
|
||||
"AvatarUpload": {
|
||||
"uploadPhoto": "Foto hochladen",
|
||||
"changePhoto": "Foto ändern",
|
||||
|
||||
@@ -80,6 +80,10 @@
|
||||
"changesSaved": "Changes saved",
|
||||
"error": "Something went wrong. Please try again."
|
||||
},
|
||||
"DateWheelPicker": {
|
||||
"selectDate": "Select date",
|
||||
"apply": "Apply"
|
||||
},
|
||||
"AvatarUpload": {
|
||||
"uploadPhoto": "Upload photo",
|
||||
"changePhoto": "Change photo",
|
||||
|
||||
@@ -10,6 +10,7 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { DateWheelPicker } from "@/components/ui/date-wheel-picker";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
@@ -113,7 +114,7 @@ function WeightTab({ petId }: { petId: string }) {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="w-date" className="text-xs">{t("weight.labelDate")}</Label>
|
||||
<Input id="w-date" type="date" value={date} onChange={(e) => setDate(e.target.value)} />
|
||||
<DateWheelPicker id="w-date" value={date} onChange={setDate} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="w-kg" className="text-xs">{t("weight.labelWeight")}</Label>
|
||||
@@ -243,7 +244,7 @@ function VetVisitsTab({ petId }: { petId: string }) {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="v-date" className="text-xs">{t("vet.labelDate")}</Label>
|
||||
<Input id="v-date" type="date" value={date} onChange={(e) => setDate(e.target.value)} />
|
||||
<DateWheelPicker id="v-date" value={date} onChange={setDate} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="v-vet" className="text-xs">{t("vet.labelVet")}</Label>
|
||||
@@ -373,11 +374,11 @@ function VaccinesTab({ petId }: { petId: string }) {
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="vac-given" className="text-xs">{t("vaccines.labelDateGiven")}</Label>
|
||||
<Input id="vac-given" type="date" value={dateGiven} onChange={(e) => setDateGiven(e.target.value)} />
|
||||
<DateWheelPicker id="vac-given" value={dateGiven} onChange={setDateGiven} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<Label htmlFor="vac-due" className="text-xs">{t("vaccines.labelNextDue")}</Label>
|
||||
<Input id="vac-due" type="date" value={nextDueDate} onChange={(e) => setNextDueDate(e.target.value)} />
|
||||
<DateWheelPicker id="vac-due" value={nextDueDate} onChange={setNextDueDate} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
} from "@/components/ui/select";
|
||||
import { Skeleton } from "@/components/ui/skeleton";
|
||||
import { AvatarUpload } from "@/components/pet/AvatarUpload";
|
||||
import { DateWheelPicker } from "@/components/ui/date-wheel-picker";
|
||||
|
||||
/**
|
||||
* PetForm — reusable create/edit form for pet profiles.
|
||||
@@ -148,8 +149,6 @@ export function PetForm({
|
||||
const petName = watch("name");
|
||||
const bioValue = watch("bio") ?? "";
|
||||
const adoptionStoryValue = watch("adoptionStory") ?? "";
|
||||
const birthdayValue = watch("birthday") ?? "";
|
||||
const adoptedAtValue = watch("adoptedAt") ?? "";
|
||||
|
||||
const onSubmit = (data: PetFormValues) => {
|
||||
if (mode === "create") {
|
||||
@@ -350,21 +349,33 @@ export function PetForm({
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="birthday">{t("birthday")}</Label>
|
||||
<Input
|
||||
id="birthday"
|
||||
type="date"
|
||||
value={birthdayValue}
|
||||
{...register("birthday")}
|
||||
<Controller
|
||||
name="birthday"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<DateWheelPicker
|
||||
id="birthday"
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
maxYear={new Date().getFullYear()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t("birthdayHint")}</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<Label htmlFor="adopted-at">{t("adoptedAt")}</Label>
|
||||
<Input
|
||||
id="adopted-at"
|
||||
type="date"
|
||||
value={adoptedAtValue}
|
||||
{...register("adoptedAt")}
|
||||
<Controller
|
||||
name="adoptedAt"
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<DateWheelPicker
|
||||
id="adopted-at"
|
||||
value={field.value ?? ""}
|
||||
onChange={field.onChange}
|
||||
maxYear={new Date().getFullYear()}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<p className="text-xs text-muted-foreground">{t("adoptedAtHint")}</p>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useLocale, useTranslations } from "next-intl";
|
||||
import { CalendarDays } from "lucide-react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
SheetHeader,
|
||||
SheetTitle,
|
||||
SheetFooter,
|
||||
} from "@/components/ui/sheet";
|
||||
|
||||
const ITEM_HEIGHT = 40;
|
||||
const VISIBLE_ITEMS = 5;
|
||||
const WHEEL_HEIGHT = ITEM_HEIGHT * VISIBLE_ITEMS;
|
||||
const WHEEL_PADDING = (WHEEL_HEIGHT - ITEM_HEIGHT) / 2;
|
||||
|
||||
interface DraftDate {
|
||||
year: number;
|
||||
month: number; // 1-12
|
||||
day: number;
|
||||
}
|
||||
|
||||
function daysInMonth(year: number, month: number): number {
|
||||
return new Date(year, month, 0).getDate();
|
||||
}
|
||||
|
||||
function parseValue(value: string): DraftDate {
|
||||
if (value) {
|
||||
const [year, month, day] = value.split("-").map(Number);
|
||||
if (year && month && day) return { year, month, day };
|
||||
}
|
||||
const now = new Date();
|
||||
return { year: now.getFullYear(), month: now.getMonth() + 1, day: now.getDate() };
|
||||
}
|
||||
|
||||
function formatValue({ year, month, day }: DraftDate): string {
|
||||
return `${String(year).padStart(4, "0")}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")}`;
|
||||
}
|
||||
|
||||
interface WheelItem {
|
||||
value: number;
|
||||
label: string;
|
||||
}
|
||||
|
||||
// Scroll-snap wheel column — the item nearest the center line after the
|
||||
// scroll settles becomes the selected value. Mirrors the iOS date-picker
|
||||
// interaction the DEBUG-List asked for instead of a calendar grid.
|
||||
function WheelColumn({
|
||||
items,
|
||||
selectedValue,
|
||||
onSelectedChange,
|
||||
}: {
|
||||
items: WheelItem[];
|
||||
selectedValue: number;
|
||||
onSelectedChange: (value: number) => void;
|
||||
}) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const settleTimeout = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const selectedIndex = Math.max(
|
||||
0,
|
||||
items.findIndex((item) => item.value === selectedValue)
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
const target = selectedIndex * ITEM_HEIGHT;
|
||||
if (Math.abs(el.scrollTop - target) > 1) {
|
||||
el.scrollTo({ top: target, behavior: "instant" as ScrollBehavior });
|
||||
}
|
||||
// Only re-syncs when the external value or item set changes, not on
|
||||
// every local scroll frame — otherwise user-driven scrolling gets fought.
|
||||
}, [selectedIndex, items]);
|
||||
|
||||
const handleScroll = () => {
|
||||
const el = containerRef.current;
|
||||
if (!el) return;
|
||||
if (settleTimeout.current) clearTimeout(settleTimeout.current);
|
||||
settleTimeout.current = setTimeout(() => {
|
||||
const index = Math.round(el.scrollTop / ITEM_HEIGHT);
|
||||
const clamped = Math.min(Math.max(index, 0), items.length - 1);
|
||||
const item = items[clamped];
|
||||
if (item && item.value !== selectedValue) {
|
||||
onSelectedChange(item.value);
|
||||
}
|
||||
}, 80);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
onScroll={handleScroll}
|
||||
className="flex-1 snap-y snap-mandatory overflow-y-auto scroll-smooth [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden"
|
||||
style={{ height: WHEEL_HEIGHT, paddingTop: WHEEL_PADDING, paddingBottom: WHEEL_PADDING }}
|
||||
>
|
||||
{items.map((item) => (
|
||||
<div
|
||||
key={item.value}
|
||||
className={cn(
|
||||
"flex snap-center items-center justify-center text-base transition-colors",
|
||||
item.value === selectedValue ? "font-semibold text-foreground" : "text-muted-foreground"
|
||||
)}
|
||||
style={{ height: ITEM_HEIGHT }}
|
||||
>
|
||||
{item.label}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export interface DateWheelPickerProps {
|
||||
id?: string;
|
||||
value: string; // "YYYY-MM-DD" or ""
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
minYear?: number;
|
||||
maxYear?: number;
|
||||
disabled?: boolean;
|
||||
className?: string;
|
||||
"aria-invalid"?: boolean;
|
||||
}
|
||||
|
||||
export function DateWheelPicker({
|
||||
id,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
minYear,
|
||||
maxYear,
|
||||
disabled,
|
||||
className,
|
||||
"aria-invalid": ariaInvalid,
|
||||
}: DateWheelPickerProps) {
|
||||
const locale = useLocale();
|
||||
const t = useTranslations("DateWheelPicker");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [wasOpen, setWasOpen] = useState(false);
|
||||
const [draft, setDraft] = useState<DraftDate>(() => parseValue(value));
|
||||
|
||||
// Re-seed the draft from the committed value on the closed->open transition.
|
||||
// Adjusting state during render (guarded by a change check) instead of in a
|
||||
// useEffect avoids an extra commit-then-rerender cascade.
|
||||
if (open !== wasOpen) {
|
||||
setWasOpen(open);
|
||||
if (open) setDraft(parseValue(value));
|
||||
}
|
||||
|
||||
const now = new Date();
|
||||
const yearFrom = minYear ?? now.getFullYear() - 100;
|
||||
const yearTo = maxYear ?? now.getFullYear() + 10;
|
||||
|
||||
const years = useMemo<WheelItem[]>(
|
||||
() => Array.from({ length: yearTo - yearFrom + 1 }, (_, i) => ({ value: yearFrom + i, label: String(yearFrom + i) })),
|
||||
[yearFrom, yearTo]
|
||||
);
|
||||
|
||||
const monthFormatter = useMemo(() => new Intl.DateTimeFormat(locale, { month: "long" }), [locale]);
|
||||
const months = useMemo<WheelItem[]>(
|
||||
() => Array.from({ length: 12 }, (_, i) => ({ value: i + 1, label: monthFormatter.format(new Date(2000, i, 1)) })),
|
||||
[monthFormatter]
|
||||
);
|
||||
|
||||
const dayCount = daysInMonth(draft.year, draft.month);
|
||||
const days = useMemo<WheelItem[]>(
|
||||
() => Array.from({ length: dayCount }, (_, i) => ({ value: i + 1, label: String(i + 1) })),
|
||||
[dayCount]
|
||||
);
|
||||
|
||||
// Derived, not stored: switching to a shorter month (e.g. 31 -> February)
|
||||
// clamps the displayed/committed day without mutating draft.day, so
|
||||
// scrolling back to a longer month restores the original day pick.
|
||||
const effectiveDay = Math.min(draft.day, dayCount);
|
||||
|
||||
const dateFormatter = useMemo(
|
||||
() => new Intl.DateTimeFormat(locale, { day: "2-digit", month: "long", year: "numeric" }),
|
||||
[locale]
|
||||
);
|
||||
|
||||
const displayLabel = value ? dateFormatter.format(new Date(`${value}T00:00:00`)) : null;
|
||||
|
||||
const handleConfirm = () => {
|
||||
onChange(formatValue({ ...draft, day: effectiveDay }));
|
||||
setOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Sheet open={open} onOpenChange={setOpen}>
|
||||
<Button
|
||||
id={id}
|
||||
type="button"
|
||||
variant="outline"
|
||||
disabled={disabled}
|
||||
aria-invalid={ariaInvalid}
|
||||
onClick={() => setOpen(true)}
|
||||
className={cn(
|
||||
"h-8 w-full justify-start gap-2 px-2.5 py-1 text-base font-normal md:text-sm",
|
||||
!displayLabel && "text-muted-foreground",
|
||||
className
|
||||
)}
|
||||
>
|
||||
<CalendarDays className="h-4 w-4 shrink-0 opacity-60" data-icon="inline-start" />
|
||||
<span className="truncate">{displayLabel ?? placeholder ?? t("selectDate")}</span>
|
||||
</Button>
|
||||
<SheetContent side="bottom">
|
||||
<SheetHeader>
|
||||
<SheetTitle>{displayLabel ?? placeholder ?? t("selectDate")}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="relative px-4">
|
||||
<div
|
||||
className="pointer-events-none absolute inset-x-4 top-1/2 -translate-y-1/2 rounded-lg bg-muted/60"
|
||||
style={{ height: ITEM_HEIGHT }}
|
||||
/>
|
||||
<div className="flex">
|
||||
<WheelColumn items={days} selectedValue={effectiveDay} onSelectedChange={(day) => setDraft((d) => ({ ...d, day }))} />
|
||||
<WheelColumn items={months} selectedValue={draft.month} onSelectedChange={(month) => setDraft((d) => ({ ...d, month }))} />
|
||||
<WheelColumn items={years} selectedValue={draft.year} onSelectedChange={(year) => setDraft((d) => ({ ...d, year }))} />
|
||||
</div>
|
||||
</div>
|
||||
<SheetFooter>
|
||||
<Button type="button" onClick={handleConfirm}>
|
||||
{t("apply")}
|
||||
</Button>
|
||||
</SheetFooter>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user