diff --git a/api/src/database.types.ts b/api/src/database.types.ts
index e0b0a58..f10b08f 100644
--- a/api/src/database.types.ts
+++ b/api/src/database.types.ts
@@ -18,6 +18,7 @@ export type Database = {
Row: {
availability_data: Json
created_at: string
+ exceptions: Json | null
id: number
updated_at: string
user_id: string
@@ -25,6 +26,7 @@ export type Database = {
Insert: {
availability_data?: Json
created_at?: string
+ exceptions?: Json | null
id?: number
updated_at?: string
user_id: string
@@ -32,6 +34,7 @@ export type Database = {
Update: {
availability_data?: Json
created_at?: string
+ exceptions?: Json | null
id?: number
updated_at?: string
user_id?: string
diff --git a/sql/17_availabilities_table.sql b/sql/17_availabilities_table.sql
index 8f3a12a..2636279 100644
--- a/sql/17_availabilities_table.sql
+++ b/sql/17_availabilities_table.sql
@@ -67,3 +67,10 @@ ALTER TABLE availabilities RENAME COLUMN availabilities TO availability_data;
-- Update the comment for the renamed column
COMMENT ON COLUMN availabilities.availability_data IS
'JSONB object containing availability settings for each day (0-6, where 0 is Monday). Each day has enabled status and time ranges.';
+
+-- Add exceptions column for date-specific availability overrides
+ALTER TABLE availabilities ADD COLUMN exceptions JSONB DEFAULT '[]';
+
+-- Add comment for the exceptions column
+COMMENT ON COLUMN availabilities.exceptions IS
+ 'JSONB object containing date-specific availability exceptions that override regular availability settings. Keys are ISO date strings (YYYY-MM-DD), values contain exception type and optional time ranges.';
diff --git a/ui/src/components/AvailabilityCard.tsx b/ui/src/components/AvailabilityCard.tsx
index 52247a5..f069e3b 100644
--- a/ui/src/components/AvailabilityCard.tsx
+++ b/ui/src/components/AvailabilityCard.tsx
@@ -29,7 +29,6 @@ interface AvailabilityCardProps {
) => void;
}
-const MINUTES_IN_DAY = 24 * 60;
const MAX_END_TIME_MINUTES = 23 * 60 + 30; // 23:30
const DAYS_OF_WEEK_DISPLAY = [
@@ -55,96 +54,6 @@ function minutesToTime(minutes: number): string {
.padStart(2, "0")}`;
}
-// TimeRangeSlider component for visualizing time ranges
-function TimeRangeSlider({ timeRanges }: { timeRanges: TimeRange[] }) {
- const sortedRanges = [...timeRanges].sort(
- (a, b) => timeToMinutes(a.start) - timeToMinutes(b.start)
- );
-
- // Generate hour markers (every 2 hours from 6 AM to 10 PM)
- const hourMarkers = [];
- for (let hour = 6; hour <= 22; hour += 2) {
- hourMarkers.push({
- time: `${hour.toString().padStart(2, "0")}:00`,
- minutes: hour * 60,
- label: `${hour}h`,
- });
- }
-
- const getPositionPercentage = (minutes: number) => {
- // Map 0-1440 minutes (24 hours) to 0-100%
- return (minutes / MINUTES_IN_DAY) * 100;
- };
-
- return (
-
-
- {/* Hour markers - subtle dots */}
- {hourMarkers.map((marker) => (
-
- ))}
-
- {/* Time range bubbles */}
- {sortedRanges.map((range, index) => {
- const startMinutes = timeToMinutes(range.start);
- const endMinutes = timeToMinutes(range.end);
- const leftPercent = getPositionPercentage(startMinutes);
- const widthPercent = getPositionPercentage(endMinutes - startMinutes);
-
- return (
-
- {/* Inner glow effect */}
-
-
- {/* Bubble highlight */}
-
-
- );
- })}
-
- {/* Start and end time markers - elegant */}
-
-
-
-
- {/* Hour labels - refined */}
-
- {hourMarkers.map((marker) => (
-
- {marker.label}
-
- ))}
-
-
- );
-}
-
export function AvailabilityCard({
day,
enabled,
@@ -378,11 +287,6 @@ export function AvailabilityCard({
)}
-
- {/* Time Slider Visualization */}
- {enabled && timeRanges.length > 0 && (
-
- )}
);
}
diff --git a/ui/src/components/AvailabilityVisualization.tsx b/ui/src/components/AvailabilityVisualization.tsx
new file mode 100644
index 0000000..ce1ef46
--- /dev/null
+++ b/ui/src/components/AvailabilityVisualization.tsx
@@ -0,0 +1,127 @@
+import { WeeklyAvailability } from "@ui/hooks/availabilities";
+import { Text } from "@ui/ui-library/text";
+
+// Check if a time slot is available for a given day
+const isTimeSlotAvailable = (
+ day: number,
+ timeSlot: string,
+ draftAvailabilities: WeeklyAvailability
+): boolean => {
+ const dayAvailability = draftAvailabilities[day];
+ if (!dayAvailability.enabled) return false;
+
+ const slotMinutes = timeToMinutes(timeSlot);
+ return dayAvailability.timeRanges.some((range) => {
+ const startMinutes = timeToMinutes(range.start);
+ const endMinutes = timeToMinutes(range.end);
+ return slotMinutes >= startMinutes && slotMinutes < endMinutes;
+ });
+};
+
+// Helper function to convert time string to minutes
+const timeToMinutes = (time: string): number => {
+ const [hours, minutes] = time.split(":").map(Number);
+ return hours * 60 + minutes;
+};
+
+// Helper function to convert minutes to time string
+const minutesToTime = (minutes: number): string => {
+ const hours = Math.floor(minutes / 60);
+ const mins = minutes % 60;
+ return `${hours.toString().padStart(2, "0")}:${mins
+ .toString()
+ .padStart(2, "0")}`;
+};
+
+// Generate time slots for visualization based on duration
+const generateTimeSlots = (intervalMinutes: number = 30) => {
+ const slots = [];
+ for (let hour = 0; hour < 24; hour++) {
+ for (let minute = 0; minute < 60; minute += intervalMinutes) {
+ slots.push(minutesToTime(hour * 60 + minute));
+ }
+ }
+ return slots;
+};
+
+const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6];
+const DAYS_OF_WEEK_DISPLAY = [
+ "Lundi",
+ "Mardi",
+ "Mercredi",
+ "Jeudi",
+ "Vendredi",
+ "Samedi",
+ "Dimanche",
+];
+
+export const AvailabilityVisualization = ({
+ draftAvailabilities,
+ slotDurationMinutes = 30,
+}: {
+ draftAvailabilities: WeeklyAvailability;
+ slotDurationMinutes?: number;
+}) => {
+ const timeSlots = generateTimeSlots(slotDurationMinutes);
+ return (
+
+ {/* Weekly Calendar Header */}
+
+
+
+ Heure
+
+
+ {DAYS_OF_WEEK.map((day) => (
+
+
+ {DAYS_OF_WEEK_DISPLAY[day]}
+
+
+ ))}
+
+
+ {/* Time Slots */}
+
+ {timeSlots.map((timeSlot) => {
+ const timeMinutes = timeToMinutes(timeSlot);
+ const hour = Math.floor(timeMinutes / 60);
+ // Only show hours from 6 AM to 11 PM
+ if (hour < 6 || hour > 23) return null;
+
+ return (
+
+
+
+ {timeSlot}
+
+
+ {DAYS_OF_WEEK.map((day) => (
+
+ {isTimeSlotAvailable(day, timeSlot, draftAvailabilities) ? (
+
+ ) : (
+
+ )}
+
+ ))}
+
+ );
+ })}
+
+
+ );
+};
diff --git a/ui/src/hooks/availabilities.ts b/ui/src/hooks/availabilities.ts
index 0022c9a..ed34d15 100644
--- a/ui/src/hooks/availabilities.ts
+++ b/ui/src/hooks/availabilities.ts
@@ -3,6 +3,7 @@ import { queryClient } from "@ui/lib/api";
import { supabase } from "@ui/hooks/auth";
import { useSession } from "@ui/contexts/SessionContext";
import { useEffect, useState } from "react";
+import { Database } from "@ui/types/database.types";
export type TimeRange = {
start: string;
@@ -18,6 +19,18 @@ export type WeeklyAvailability = {
[key: number]: DayAvailability;
};
+export type Exception = {
+ date: string;
+} & (
+ | {
+ type: "hours";
+ hours: TimeRange;
+ }
+ | {
+ type: "day";
+ }
+);
+
const DAYS_OF_WEEK = [0, 1, 2, 3, 4, 5, 6];
export const DEFAULT_AVAILABILITIES: WeeklyAvailability = DAYS_OF_WEEK.reduce(
@@ -34,7 +47,9 @@ export const DEFAULT_AVAILABILITIES: WeeklyAvailability = DAYS_OF_WEEK.reduce(
export function useAvailabilities() {
const { session } = useSession();
- const { data: availabilities, isLoading } = useQuery({
+ const { data: availabilities, isLoading } = useQuery<
+ Database["public"]["Tables"]["availabilities"]["Row"]
+ >({
queryKey: ["availabilities"],
queryFn: async () => {
const { data, error } = await supabase
@@ -43,20 +58,35 @@ export function useAvailabilities() {
.eq("user_id", session?.user.id)
.limit(1);
if (error) throw error;
- return data?.[0].availability_data as WeeklyAvailability;
+ return data?.[0] as Database["public"]["Tables"]["availabilities"]["Row"];
},
enabled: !!session?.user.id,
});
- console.log("availabilities", availabilities);
-
- const { mutate: updateAvailabilities, isPending: isUpdating } = useMutation({
- mutationFn: async (optionalAvailabilities: WeeklyAvailability) => {
- const newAvailabilities =
- optionalAvailabilities || DEFAULT_AVAILABILITIES;
+ const { mutate: updateAvailabilities } = useMutation<
+ void,
+ Error,
+ {
+ updatedAvailabilities: WeeklyAvailability;
+ newException?: Exception | null;
+ }
+ >({
+ mutationFn: async ({
+ updatedAvailabilities,
+ newException = null,
+ }: {
+ updatedAvailabilities: WeeklyAvailability;
+ newException?: Exception | null;
+ }) => {
+ const newAvailabilities = updatedAvailabilities || DEFAULT_AVAILABILITIES;
+ const newExceptions = [
+ ...((availabilities?.exceptions as Exception[] | null) || []),
+ newException || [],
+ ];
const { error } = await supabase.from("availabilities").upsert(
{
availability_data: newAvailabilities,
+ exceptions: newExceptions,
user_id: session?.user.id,
},
{
@@ -74,16 +104,17 @@ export function useAvailabilities() {
useState(null);
useEffect(() => {
- if (availabilities) {
- setDraftAvailabilities(availabilities);
+ if (availabilities?.availability_data) {
+ setDraftAvailabilities(
+ availabilities.availability_data as WeeklyAvailability
+ );
}
- }, [availabilities]);
+ }, [availabilities?.availability_data]);
return {
isLoading,
updateAvailabilities,
draftAvailabilities: draftAvailabilities || DEFAULT_AVAILABILITIES,
setDraftAvailabilities,
- isUpdating,
};
}
diff --git a/ui/src/pages/availabilities.tsx b/ui/src/pages/availabilities.tsx
index f910299..623b801 100644
--- a/ui/src/pages/availabilities.tsx
+++ b/ui/src/pages/availabilities.tsx
@@ -1,15 +1,22 @@
import { Strong, Text } from "@ui/ui-library/text";
import { AvailabilityCard } from "@ui/components/AvailabilityCard";
import { Button } from "@ui/ui-library/button";
-import { LoadingSpinner } from "@ui/components/LoadingSpinner";
import {
DEFAULT_AVAILABILITIES,
+ Exception,
useAvailabilities,
WeeklyAvailability,
} from "@ui/hooks/availabilities";
import { toast } from "@ui/ui-library/toast/toast-queue";
import { useState } from "react";
import { Checkbox } from "@ui/ui-library/checkbox";
+import { PlusIcon } from "@ui/ui-library/icons";
+import { AvailabilityVisualization } from "@ui/components/AvailabilityVisualization";
+import { SaveIcon } from "lucide-react";
+import { DatePicker, DatePickerInput } from "@ui/ui-library/date-picker";
+import { Label } from "@ui/ui-library/field";
+import { DateValue, getLocalTimeZone, today } from "@internationalized/date";
+import { RadioGroup, Radios, Radio } from "@ui/ui-library/radio-group";
// Custom Modal Component
interface CustomModalProps {
@@ -81,13 +88,12 @@ interface TimeRange {
}
export function AvailabilitiesPage() {
- const {
- updateAvailabilities,
- isUpdating,
- draftAvailabilities,
- setDraftAvailabilities,
- } = useAvailabilities();
+ const { updateAvailabilities, draftAvailabilities, setDraftAvailabilities } =
+ useAvailabilities();
+ const [activeTab, setActiveTab] = useState<"reglages" | "visualisation">(
+ "reglages"
+ );
const [copyModalOpen, setCopyModalOpen] = useState(false);
const [sourceDayData, setSourceDayData] = useState<{
day: number;
@@ -95,6 +101,10 @@ export function AvailabilitiesPage() {
timeRanges: TimeRange[];
} | null>(null);
const [selectedDays, setSelectedDays] = useState([]);
+ const [exceptionModalOpen, setExceptionModalOpen] = useState(false);
+ const [exceptionType, setExceptionType] = useState<"day" | "hours">("day");
+ const [exceptionDate, setExceptionDate] = useState(null);
+ const [exceptionHours, setExceptionHours] = useState(null);
const handleCopyToOtherDays = (
sourceDay: number,
@@ -135,145 +145,195 @@ export function AvailabilitiesPage() {
Disponibilités
- Définissez vos horaires de disponibilité pour chaque jour de la
- semaine
+ {activeTab === "reglages"
+ ? "Définissez vos horaires de disponibilité pour chaque jour de la semaine"
+ : "Visualisez votre planning hebdomadaire"}
-
- {
- updateAvailabilities(DEFAULT_AVAILABILITIES);
- }}
- className="py-1"
- >
- Horaires de bureau (9h-17h)
-
- {
- const newAvailabilities: WeeklyAvailability = {};
- DAYS_OF_WEEK.forEach((day) => {
- newAvailabilities[day] = {
- enabled: false,
- timeRanges: [{ start: "09:00", end: "17:00" }],
- };
- });
- updateAvailabilities(newAvailabilities);
- }}
- className="py-1"
- >
- Tout désactiver
-
-
+ {activeTab === "reglages" && (
+
+
{
+ updateAvailabilities(
+ {
+ updatedAvailabilities: draftAvailabilities,
+ newException: null,
+ },
+ {
+ onSuccess: () => {
+ toast.add({
+ title: "Succès",
+ description: "Disponibilités enregistrées avec succès",
+ type: "success",
+ });
+ },
+ onError: () => {
+ toast.add({
+ title: "Erreur",
+ description:
+ "Erreur lors de l'enregistrement des disponibilités",
+ type: "error",
+ });
+ },
+ }
+ );
+ }}
+ >
+ Enregistrer
+
+
setExceptionModalOpen(true)}
+ className="[--btn-bg:var(--color-blue-800)]"
+ >
+ Ajouter une exception
+
+
{
+ updateAvailabilities({
+ updatedAvailabilities: DEFAULT_AVAILABILITIES,
+ });
+ }}
+ className="py-1"
+ >
+ Horaires de bureau (9h-17h)
+
+
{
+ const newAvailabilities: WeeklyAvailability = {};
+ DAYS_OF_WEEK.forEach((day) => {
+ newAvailabilities[day] = {
+ enabled: false,
+ timeRanges: [{ start: "09:00", end: "17:00" }],
+ };
+ });
+ updateAvailabilities({
+ updatedAvailabilities: newAvailabilities,
+ });
+ }}
+ className="py-1"
+ >
+ Tout désactiver
+
+
+ )}
+
+
+ {/* Tab Navigation */}
+
+ setActiveTab("reglages")}
+ className={`px-6 py-3 font-medium text-sm border-b-2 transition-colors ${
+ activeTab === "reglages"
+ ? "border-primary text-primary"
+ : "border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
+ }`}
+ >
+ Réglages
+
+ setActiveTab("visualisation")}
+ className={`px-6 py-3 font-medium text-sm border-b-2 transition-colors ${
+ activeTab === "visualisation"
+ ? "border-primary text-primary"
+ : "border-transparent text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-300"
+ }`}
+ >
+ Visualisation
+
-
-
-
- {DAYS_OF_WEEK.map((day) => (
-
-
-
{
- setDraftAvailabilities({
- ...draftAvailabilities,
- [day]: {
- ...draftAvailabilities[day],
- enabled,
- },
- });
- }}
- timeRanges={draftAvailabilities[day].timeRanges}
- onTimeRangesChange={(ranges) => {
- setDraftAvailabilities({
- ...draftAvailabilities,
- [day]: {
- ...draftAvailabilities[day],
- timeRanges: ranges,
- },
- });
- }}
- onCopyToOtherDays={handleCopyToOtherDays}
- />
+ {activeTab === "reglages" && (
+
+
+
+ {DAYS_OF_WEEK.map((day) => (
+
+
+
{
+ setDraftAvailabilities({
+ ...draftAvailabilities,
+ [day]: {
+ ...draftAvailabilities[day],
+ enabled,
+ },
+ });
+ }}
+ timeRanges={draftAvailabilities[day].timeRanges}
+ onTimeRangesChange={(ranges) => {
+ setDraftAvailabilities({
+ ...draftAvailabilities,
+ [day]: {
+ ...draftAvailabilities[day],
+ timeRanges: ranges,
+ },
+ });
+ }}
+ onCopyToOtherDays={handleCopyToOtherDays}
+ />
+
+ ))}
+
+
+
+
+
+
+
Fuseau horaire
+
+ Vos disponibilités sont affichées dans votre fuseau horaire
+ local.
+
- ))}
-
-
-
-
-
-
Fuseau horaire
-
- Vos disponibilités sont affichées dans votre fuseau horaire
- local.
-
-
+
+ Votre fuseau horaire
+
+ {Intl.DateTimeFormat().resolvedOptions().timeZone}
+
+
+ {new Date().toLocaleTimeString([], {
+ hour: "2-digit",
+ minute: "2-digit",
+ })}{" "}
+ - Heure locale
+
+
-
- Votre fuseau horaire
-
- {Intl.DateTimeFormat().resolvedOptions().timeZone}
-
-
- {new Date().toLocaleTimeString([], {
- hour: "2-digit",
- minute: "2-digit",
- })}{" "}
- - Heure locale
-
-
-
-
-
Information
-
- Les créneaux horaires seront automatiquement convertis dans le
- fuseau horaire de vos clients lorsqu'ils consulteront vos
- disponibilités.
-
+
+ Information
+
+ Les créneaux horaires seront automatiquement convertis dans
+ le fuseau horaire de vos clients lorsqu'ils
+ consulteront vos disponibilités.
+
+
-
-
+ )}
-
- {isUpdating &&
}
-
{
- updateAvailabilities(draftAvailabilities, {
- onSuccess: () => {
- toast.add({
- title: "Succès",
- description: "Disponibilités enregistrées avec succès",
- type: "success",
- });
- },
- onError: () => {
- toast.add({
- title: "Erreur",
- description:
- "Erreur lors de l'enregistrement des disponibilités",
- type: "error",
- });
- },
- });
- }}
- >
- {isUpdating ? "Enregistrement..." : "Enregistrer les disponibilités"}
-
+ {activeTab === "visualisation" && (
+
+ )}
{/* Copy Modal */}
@@ -326,6 +386,126 @@ export function AvailabilitiesPage() {
+
+ {/* Exception Modal */}
+
setExceptionModalOpen(false)}
+ title="Ajouter une exception"
+ >
+
+
+ Définissez une exception pour une date spécifique qui remplacera vos
+ disponibilités habituelles.
+
+
+
+
{
+ if (value === null) {
+ return;
+ }
+ setExceptionDate(value.toDate(getLocalTimeZone()));
+ }}
+ >
+ Date de l'exception
+
+
+
+
+ {
+ setExceptionType(value as "day" | "hours");
+ }}
+ >
+ Type d'exception
+
+ Indisponible toute la journée
+ Horaires personnalisés
+
+
+
+
+ {/* Custom Time Ranges (shown when custom is selected) */}
+
+
+
+
+ setExceptionModalOpen(false)}
+ >
+ Annuler
+
+ {
+ setExceptionModalOpen(false);
+ const exception: Exception =
+ exceptionType === "hours"
+ ? {
+ date: exceptionDate?.toISOString() || "",
+ type: exceptionType,
+ hours:
+ exceptionHours ||
+ ({
+ start: "09:00",
+ end: "17:00",
+ } as TimeRange),
+ }
+ : {
+ date: exceptionDate?.toISOString() || "",
+ type: exceptionType,
+ };
+ updateAvailabilities({
+ updatedAvailabilities: draftAvailabilities,
+ newException: exception,
+ });
+ toast.add({
+ title: "Succès",
+ description: "Exception ajoutée avec succès",
+ type: "success",
+ });
+ }}
+ >
+ Ajouter l'exception
+
+
+
+
);
}
diff --git a/ui/src/types/database.types.ts b/ui/src/types/database.types.ts
index e0b0a58..f10b08f 100644
--- a/ui/src/types/database.types.ts
+++ b/ui/src/types/database.types.ts
@@ -18,6 +18,7 @@ export type Database = {
Row: {
availability_data: Json
created_at: string
+ exceptions: Json | null
id: number
updated_at: string
user_id: string
@@ -25,6 +26,7 @@ export type Database = {
Insert: {
availability_data?: Json
created_at?: string
+ exceptions?: Json | null
id?: number
updated_at?: string
user_id: string
@@ -32,6 +34,7 @@ export type Database = {
Update: {
availability_data?: Json
created_at?: string
+ exceptions?: Json | null
id?: number
updated_at?: string
user_id?: string
diff --git a/ui/src/ui-library/date-field.tsx b/ui/src/ui-library/date-field.tsx
index 318597d..f2a6bc0 100644
--- a/ui/src/ui-library/date-field.tsx
+++ b/ui/src/ui-library/date-field.tsx
@@ -6,9 +6,9 @@ import {
DateSegment,
DateValue,
composeRenderProps,
-} from 'react-aria-components';
-import { inputField } from './utils';
-import { twMerge } from 'tailwind-merge';
+} from "react-aria-components";
+import { inputField } from "./utils";
+import { twMerge } from "tailwind-merge";
export interface DateFieldProps
extends RACDateFieldProps {}
@@ -24,37 +24,44 @@ export function DateField(props: DateFieldProps) {
inputField,
// RAC does not set disable to date field when it is disable
// So we have to style disable state for none input
- isDisabled && '[&>:not(input)]:opacity-50',
- className,
+ isDisabled && "[&>:not(input)]:opacity-50",
+ className
);
- },
+ }
)}
/>
);
}
-export type DateInputProps = Omit;
+export type DateInputProps = Omit;
export function DateInput(props: DateInputProps) {
return (
- twMerge(
- 'group flex w-full items-center rounded-md border border-input bg-transparent',
-
- '[&:has([data-disabled=true])]:opacity-50',
- '[&:has([data-ui=date-segment][aria-readonly])]:bg-zinc-50',
- 'dark:[&:has([data-ui=date-segment][aria-readonly])]:bg-white/10',
- 'block min-w-[150px]',
- 'text-base/6 sm:text-sm/6',
- 'px-3',
- 'py-[calc(--spacing(2.5)-1px)] sm:py-[calc(--spacing(1.5)-1px)]',
- renderProps.isInvalid && 'border-destructive',
- renderProps.isFocusWithin && 'border-ring ring-1 ring-ring',
- className,
- ),
+ className={composeRenderProps(
+ props.className,
+ (className, { isInvalid, isFocusWithin, isHovered, isDisabled }) =>
+ twMerge(
+ "group flex min-w-[150px] items-center",
+ "w-full rounded-md text-base/6 shadow-sm outline-none sm:text-sm/6 dark:shadow-none",
+ "px-2.5 py-2.5 sm:py-1.5",
+ "ring ring-zinc-950/10 dark:ring-white/10",
+ !isFocusWithin &&
+ !isDisabled &&
+ !isInvalid &&
+ isHovered && [
+ "[&:not(:has([data-ui=date-segment][aria-readonly]))]:ring-zinc-950/20",
+ "dark:[&:not(:has([data-ui=date-segment][aria-readonly]))]:ring-white/20",
+ ],
+ "[&:has([data-disabled=true])]:opacity-50",
+ "[&:has([data-ui=date-segment][aria-readonly])]:bg-zinc-50",
+ "dark:[&:has([data-ui=date-segment][aria-readonly])]:bg-white/5",
+ isInvalid && "ring-red-600 dark:ring-red-600",
+ isFocusWithin ? "ring-ring dark:ring-ring ring-2" : "",
+ className
+ )
)}
>
{(segment) => (
@@ -62,9 +69,9 @@ export function DateInput(props: DateInputProps) {
data-ui="date-segment"
segment={segment}
className={twMerge(
- 'inline rounded-sm px-0.5 caret-transparent outline-0 data-[type=literal]:px-0',
- 'data-placeholder:italic data-placeholder:text-muted',
- 'focus:bg-accent focus:text-[lch(from_var(--color-accent)_calc((49.44_-_l)_*_infinity)_0_0)] focus:data-placeholder:text-[lch(from_var(--color-accent)_calc((49.44_-_l)_*_infinity)_0_0)]',
+ "inline rounded-sm px-0.5 caret-transparent outline-0 data-[type=literal]:px-0",
+ "data-placeholder:text-muted data-placeholder:italic",
+ "focus:bg-accent focus:text-[lch(from_var(--accent)_calc((49.44_-_l)_*_infinity)_0_0)] focus:data-placeholder:text-[lch(from_var(--accent)_calc((49.44_-_l)_*_infinity)_0_0)]"
)}
/>
)}
diff --git a/ui/src/ui-library/date-picker.tsx b/ui/src/ui-library/date-picker.tsx
index 1bc8e6e..c0ab080 100644
--- a/ui/src/ui-library/date-picker.tsx
+++ b/ui/src/ui-library/date-picker.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React from "react";
import {
DatePicker as RACDatePicker,
DatePickerProps as RACDatePickerProps,
@@ -7,15 +7,15 @@ import {
useLocale,
Group,
composeRenderProps,
-} from 'react-aria-components';
-import { Button } from './button';
-import { Calendar, YearRange } from './calendar';
-import { DateInput, DateInputProps } from './date-field';
-import { Dialog } from './dialog';
-import { Popover } from './popover';
-import { inputField } from './utils';
-import { twMerge } from 'tailwind-merge';
-import { CalendarIcon } from './icons';
+} from "react-aria-components";
+import { Button } from "./button";
+import { Calendar, YearRange } from "./calendar";
+import { DateInput, DateInputProps } from "./date-field";
+import { Dialog } from "./dialog";
+import { Popover } from "./popover";
+import { inputField } from "./utils";
+import { twMerge } from "tailwind-merge";
+import { CalendarIcon } from "./icons/outline/calendar";
export interface DatePickerProps
extends RACDatePickerProps {}
@@ -41,22 +41,21 @@ export function DatePickerInput({
data-ui="control"
{...props}
className={[
- 'group',
- 'grid w-auto min-w-52',
- 'grid-cols-[1fr_calc(theme(size.5)+20px)]',
- 'sm:grid-cols-[1fr_calc(theme(size.4)+20px)]',
- ].join(' ')}
+ "group",
+ "grid w-auto min-w-52",
+ "grid-cols-[1fr_auto]",
+ ].join(" ")}
>
twMerge(
- 'col-span-full',
- 'row-start-1',
- 'sm:pe-9',
- 'pe-10',
- className,
- ),
+ "col-span-full",
+ "row-start-1",
+ "sm:pe-8",
+ "pe-9",
+ className
+ )
)}
/>
-
+
@@ -101,12 +102,12 @@ export function DatePickerButton({
- {formattedDate === '' ? (
+ {formattedDate === "" ? (
{children}
) : (
{formattedDate}
@@ -118,7 +119,7 @@ export function DatePickerButton({
-
+
diff --git a/ui/src/ui-library/icons/outline/calendar.tsx b/ui/src/ui-library/icons/outline/calendar.tsx
new file mode 100644
index 0000000..be310fe
--- /dev/null
+++ b/ui/src/ui-library/icons/outline/calendar.tsx
@@ -0,0 +1,28 @@
+import { Icon } from "../../icon";
+
+export function CalendarIcon({
+ "aria-label": arialLabel,
+ ...props
+}: React.JSX.IntrinsicElements["svg"]) {
+ return (
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/xtablo-expo/lib/database.types.ts b/xtablo-expo/lib/database.types.ts
index e0b0a58..f10b08f 100644
--- a/xtablo-expo/lib/database.types.ts
+++ b/xtablo-expo/lib/database.types.ts
@@ -18,6 +18,7 @@ export type Database = {
Row: {
availability_data: Json
created_at: string
+ exceptions: Json | null
id: number
updated_at: string
user_id: string
@@ -25,6 +26,7 @@ export type Database = {
Insert: {
availability_data?: Json
created_at?: string
+ exceptions?: Json | null
id?: number
updated_at?: string
user_id: string
@@ -32,6 +34,7 @@ export type Database = {
Update: {
availability_data?: Json
created_at?: string
+ exceptions?: Json | null
id?: number
updated_at?: string
user_id?: string