style(components): standardize main component file formatting
This commit is contained in:
@@ -1,17 +1,17 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from "@/components/ui/button";
|
||||||
import { Textarea } from '@/components/ui/textarea'
|
import { Textarea } from "@/components/ui/textarea";
|
||||||
import { Card } from '@/components/ui/card'
|
import { Card } from "@/components/ui/card";
|
||||||
|
|
||||||
interface AIToolbarProps {
|
interface AIToolbarProps {
|
||||||
isAuthenticated: boolean
|
isAuthenticated: boolean;
|
||||||
isPending: boolean
|
isPending: boolean;
|
||||||
aiPrompt: string
|
aiPrompt: string;
|
||||||
setAiPrompt: (prompt: string) => void
|
setAiPrompt: (prompt: string) => void;
|
||||||
aiLoading: boolean
|
aiLoading: boolean;
|
||||||
onAiCreate: () => void
|
onAiCreate: () => void;
|
||||||
onAiSummarize: () => void
|
onAiSummarize: () => void;
|
||||||
summary: string | null
|
summary: string | null;
|
||||||
summaryUpdated: string | null
|
summaryUpdated: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AIToolbar = ({
|
export const AIToolbar = ({
|
||||||
@@ -23,28 +23,30 @@ export const AIToolbar = ({
|
|||||||
onAiCreate,
|
onAiCreate,
|
||||||
onAiSummarize,
|
onAiSummarize,
|
||||||
summary,
|
summary,
|
||||||
summaryUpdated
|
summaryUpdated,
|
||||||
}: AIToolbarProps) => {
|
}: AIToolbarProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isPending ? (
|
{isPending ? (
|
||||||
<div className='mb-4 p-4 text-center animate-pulse bg-muted'>Loading...</div>
|
<div className="mb-4 p-4 text-center animate-pulse bg-muted">
|
||||||
|
Loading...
|
||||||
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{isAuthenticated ? (
|
{isAuthenticated ? (
|
||||||
<div className="flex flex-col sm:flex-row gap-4 mb-4 items-start">
|
<div className="flex flex-col sm:flex-row gap-4 mb-4 items-start">
|
||||||
<div className='w-full'>
|
<div className="w-full">
|
||||||
<Textarea
|
<Textarea
|
||||||
className="wrap-anywhere field-sizing-content resize-none w-full min-h-[2.5rem] max-h-64 overflow-y-auto sm:overflow-y-visible px-3 py-2 scroll-p-8 placeholder:italic"
|
className="wrap-anywhere field-sizing-content resize-none w-full min-h-[2.5rem] max-h-64 overflow-y-auto sm:overflow-y-visible px-3 py-2 scroll-p-8 placeholder:italic"
|
||||||
style={{ clipPath: "inset(0 round 1rem)" }}
|
style={{ clipPath: "inset(0 round 1rem)" }}
|
||||||
placeholder='Describe event for AI to create'
|
placeholder="Describe event for AI to create"
|
||||||
value={aiPrompt}
|
value={aiPrompt}
|
||||||
onChange={e => setAiPrompt(e.target.value)}
|
onChange={(e) => setAiPrompt(e.target.value)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-row gap-2 pt-1'>
|
<div className="flex flex-row gap-2 pt-1">
|
||||||
<Button onClick={onAiCreate} disabled={aiLoading}>
|
<Button onClick={onAiCreate} disabled={aiLoading}>
|
||||||
{aiLoading ? 'Thinking...' : 'AI Create'}
|
{aiLoading ? "Thinking..." : "AI Create"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -61,20 +63,22 @@ export const AIToolbar = ({
|
|||||||
{/* Summary Panel */}
|
{/* Summary Panel */}
|
||||||
{summary && (
|
{summary && (
|
||||||
<Card className="p-4 mb-4">
|
<Card className="p-4 mb-4">
|
||||||
<div className="text-sm mb-1">
|
<div className="text-sm mb-1">Summary updated {summaryUpdated}</div>
|
||||||
Summary updated {summaryUpdated}
|
|
||||||
</div>
|
|
||||||
<div>{summary}</div>
|
<div>{summary}</div>
|
||||||
</Card>
|
</Card>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* AI Actions Toolbar */}
|
{/* AI Actions Toolbar */}
|
||||||
<p className='text-muted-foreground text-sm pb-2 pl-1'>AI actions</p>
|
<p className="text-muted-foreground text-sm pb-2 pl-1">AI actions</p>
|
||||||
<div className="gap-2 mb-4">
|
<div className="gap-2 mb-4">
|
||||||
<Button variant="secondary" onClick={onAiSummarize} disabled={aiLoading}>
|
<Button
|
||||||
{aiLoading ? 'Summarizing...' : 'AI Summarize'}
|
variant="secondary"
|
||||||
|
onClick={onAiSummarize}
|
||||||
|
disabled={aiLoading}
|
||||||
|
>
|
||||||
|
{aiLoading ? "Summarizing..." : "AI Summarize"}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,41 +1,41 @@
|
|||||||
import { ReactNode } from 'react'
|
import { ReactNode } from "react";
|
||||||
import { toast } from 'sonner'
|
import { toast } from "sonner";
|
||||||
|
|
||||||
interface DragDropContainerProps {
|
interface DragDropContainerProps {
|
||||||
children: ReactNode
|
children: ReactNode;
|
||||||
isDragOver: boolean
|
isDragOver: boolean;
|
||||||
setIsDragOver: (isDragOver: boolean) => void
|
setIsDragOver: (isDragOver: boolean) => void;
|
||||||
onImport: (file: File) => void
|
onImport: (file: File) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DragDropContainer = ({
|
export const DragDropContainer = ({
|
||||||
children,
|
children,
|
||||||
isDragOver,
|
isDragOver,
|
||||||
setIsDragOver,
|
setIsDragOver,
|
||||||
onImport
|
onImport,
|
||||||
}: DragDropContainerProps) => {
|
}: DragDropContainerProps) => {
|
||||||
const handleDragOver = (e: React.DragEvent) => {
|
const handleDragOver = (e: React.DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
setIsDragOver(true)
|
setIsDragOver(true);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDragLeave = (e: React.DragEvent) => {
|
const handleDragLeave = (e: React.DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
setIsDragOver(false)
|
setIsDragOver(false);
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleDrop = (e: React.DragEvent) => {
|
const handleDrop = (e: React.DragEvent) => {
|
||||||
e.preventDefault()
|
e.preventDefault();
|
||||||
setIsDragOver(false)
|
setIsDragOver(false);
|
||||||
if (e.dataTransfer.files?.length) {
|
if (e.dataTransfer.files?.length) {
|
||||||
const file = e.dataTransfer.files[0]
|
const file = e.dataTransfer.files[0];
|
||||||
if (file.name.endsWith('.ics')) {
|
if (file.name.endsWith(".ics")) {
|
||||||
onImport(file)
|
onImport(file);
|
||||||
} else {
|
} else {
|
||||||
toast.warning('Please drop an .ics file')
|
toast.warning("Please drop an .ics file");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -43,13 +43,13 @@ export const DragDropContainer = ({
|
|||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
onDrop={handleDrop}
|
onDrop={handleDrop}
|
||||||
className={`p-4 min-h-[80vh] flex flex-col rounded border-2 border-dashed transition ${
|
className={`p-4 min-h-[80vh] flex flex-col rounded border-2 border-dashed transition ${
|
||||||
isDragOver ? 'border-blue-500 bg-blue-50' : 'border-gray-700'
|
isDragOver ? "border-blue-500 bg-blue-50" : "border-gray-700"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<div className='mt-auto w-full pb-4 text-gray-400'>
|
<div className="mt-auto w-full pb-4 text-gray-400">
|
||||||
<div className='max-w-fit m-auto'>Drag & Drop *.ics here</div>
|
<div className="max-w-fit m-auto">Drag & Drop *.ics here</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from "@/components/ui/button";
|
||||||
import { IcsFilePicker } from '@/components/ics-file-picker'
|
import { IcsFilePicker } from "@/components/ics-file-picker";
|
||||||
import type { CalendarEvent } from '@/lib/types'
|
import type { CalendarEvent } from "@/lib/types";
|
||||||
|
|
||||||
interface EventActionsToolbarProps {
|
interface EventActionsToolbarProps {
|
||||||
events: CalendarEvent[]
|
events: CalendarEvent[];
|
||||||
onAddEvent: () => void
|
onAddEvent: () => void;
|
||||||
onImport: (file: File) => void
|
onImport: (file: File) => void;
|
||||||
onExport: () => void
|
onExport: () => void;
|
||||||
onClearAll: () => void
|
onClearAll: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventActionsToolbar = ({
|
export const EventActionsToolbar = ({
|
||||||
@@ -15,22 +15,28 @@ export const EventActionsToolbar = ({
|
|||||||
onAddEvent,
|
onAddEvent,
|
||||||
onImport,
|
onImport,
|
||||||
onExport,
|
onExport,
|
||||||
onClearAll
|
onClearAll,
|
||||||
}: EventActionsToolbarProps) => {
|
}: EventActionsToolbarProps) => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Control Toolbar */}
|
{/* Control Toolbar */}
|
||||||
<p className='text-muted-foreground text-sm pb-2 pl-1'>Event Actions</p>
|
<p className="text-muted-foreground text-sm pb-2 pl-1">Event Actions</p>
|
||||||
<div className="flex flex-wrap gap-2 mb-4">
|
<div className="flex flex-wrap gap-2 mb-4">
|
||||||
<Button onClick={onAddEvent}>Add Event</Button>
|
<Button onClick={onAddEvent}>Add Event</Button>
|
||||||
<IcsFilePicker onFileSelect={onImport} variant='secondary'>Import .ics</IcsFilePicker>
|
<IcsFilePicker onFileSelect={onImport} variant="secondary">
|
||||||
|
Import .ics
|
||||||
|
</IcsFilePicker>
|
||||||
{events.length > 0 && (
|
{events.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Button variant="secondary" onClick={onExport}>Export .ics</Button>
|
<Button variant="secondary" onClick={onExport}>
|
||||||
<Button variant="destructive" onClick={onClearAll}>Clear All</Button>
|
Export .ics
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={onClearAll}>
|
||||||
|
Clear All
|
||||||
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,35 +1,40 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from "@/components/ui/button";
|
||||||
import { Card, CardHeader, CardContent } from '@/components/ui/card'
|
import { Card, CardHeader, CardContent } from "@/components/ui/card";
|
||||||
import { LucideMapPin, Clock, MoreHorizontal } from 'lucide-react'
|
import { LucideMapPin, Clock, MoreHorizontal } from "lucide-react";
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger, DropdownMenuItem } from '@/components/ui/dropdown-menu'
|
import {
|
||||||
import { RRuleDisplay } from '@/components/rrule-display'
|
DropdownMenu,
|
||||||
import type { CalendarEvent } from '@/lib/types'
|
DropdownMenuContent,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
DropdownMenuItem,
|
||||||
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
import { RRuleDisplay } from "@/components/rrule-display";
|
||||||
|
import type { CalendarEvent } from "@/lib/types";
|
||||||
|
|
||||||
interface EventCardProps {
|
interface EventCardProps {
|
||||||
event: CalendarEvent
|
event: CalendarEvent;
|
||||||
onEdit: (event: CalendarEvent) => void
|
onEdit: (event: CalendarEvent) => void;
|
||||||
onDelete: (eventId: string) => void
|
onDelete: (eventId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
||||||
const formatDateTime = (dateStr: string, allDay: boolean | undefined) => {
|
const formatDateTime = (dateStr: string, allDay: boolean | undefined) => {
|
||||||
return allDay
|
return allDay
|
||||||
? new Date(dateStr).toLocaleDateString()
|
? new Date(dateStr).toLocaleDateString()
|
||||||
: new Date(dateStr).toLocaleString()
|
: new Date(dateStr).toLocaleString();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleEdit = () => {
|
const handleEdit = () => {
|
||||||
onEdit({
|
onEdit({
|
||||||
id: event.id,
|
id: event.id,
|
||||||
title: event.title,
|
title: event.title,
|
||||||
description: event.description || '',
|
description: event.description || "",
|
||||||
location: event.location || '',
|
location: event.location || "",
|
||||||
url: event.url || '',
|
url: event.url || "",
|
||||||
start: event.start,
|
start: event.start,
|
||||||
end: event.end || '',
|
end: event.end || "",
|
||||||
allDay: event.allDay || false
|
allDay: event.allDay || false,
|
||||||
})
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card className="w-full">
|
<Card className="w-full">
|
||||||
@@ -58,9 +63,7 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
|||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem onClick={handleEdit}>
|
<DropdownMenuItem onClick={handleEdit}>Edit</DropdownMenuItem>
|
||||||
Edit
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={() => onDelete(event.id)}
|
onClick={() => onDelete(event.id)}
|
||||||
className="text-destructive"
|
className="text-destructive"
|
||||||
@@ -88,5 +91,5 @@ export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,30 +1,36 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from "@/components/ui/button";
|
||||||
import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
import {
|
||||||
import { Input } from '@/components/ui/input'
|
Dialog,
|
||||||
import { RecurrencePicker } from '@/components/recurrence-picker'
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from "@/components/ui/dialog";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { RecurrencePicker } from "@/components/recurrence-picker";
|
||||||
|
|
||||||
interface EventDialogProps {
|
interface EventDialogProps {
|
||||||
open: boolean
|
open: boolean;
|
||||||
onOpenChange: (open: boolean) => void
|
onOpenChange: (open: boolean) => void;
|
||||||
editingId: string | null
|
editingId: string | null;
|
||||||
title: string
|
title: string;
|
||||||
setTitle: (title: string) => void
|
setTitle: (title: string) => void;
|
||||||
description: string
|
description: string;
|
||||||
setDescription: (description: string) => void
|
setDescription: (description: string) => void;
|
||||||
location: string
|
location: string;
|
||||||
setLocation: (location: string) => void
|
setLocation: (location: string) => void;
|
||||||
url: string
|
url: string;
|
||||||
setUrl: (url: string) => void
|
setUrl: (url: string) => void;
|
||||||
start: string
|
start: string;
|
||||||
setStart: (start: string) => void
|
setStart: (start: string) => void;
|
||||||
end: string
|
end: string;
|
||||||
setEnd: (end: string) => void
|
setEnd: (end: string) => void;
|
||||||
allDay: boolean
|
allDay: boolean;
|
||||||
setAllDay: (allDay: boolean) => void
|
setAllDay: (allDay: boolean) => void;
|
||||||
recurrenceRule: string | undefined
|
recurrenceRule: string | undefined;
|
||||||
setRecurrenceRule: (rule: string | undefined) => void
|
setRecurrenceRule: (rule: string | undefined) => void;
|
||||||
onSave: () => void
|
onSave: () => void;
|
||||||
onReset: () => void
|
onReset: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventDialog = ({
|
export const EventDialog = ({
|
||||||
@@ -48,49 +54,81 @@ export const EventDialog = ({
|
|||||||
recurrenceRule,
|
recurrenceRule,
|
||||||
setRecurrenceRule,
|
setRecurrenceRule,
|
||||||
onSave,
|
onSave,
|
||||||
onReset
|
onReset,
|
||||||
}: EventDialogProps) => {
|
}: EventDialogProps) => {
|
||||||
const handleOpenChange = (val: boolean) => {
|
const handleOpenChange = (val: boolean) => {
|
||||||
if (!val) onReset()
|
if (!val) onReset();
|
||||||
onOpenChange(val)
|
onOpenChange(val);
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{editingId ? 'Edit Event' : 'Add Event'}</DialogTitle>
|
<DialogTitle>{editingId ? "Edit Event" : "Add Event"}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<Input placeholder="Title" value={title} onChange={e => setTitle(e.target.value)} />
|
<Input
|
||||||
|
placeholder="Title"
|
||||||
|
value={title}
|
||||||
|
onChange={(e) => setTitle(e.target.value)}
|
||||||
|
/>
|
||||||
<textarea
|
<textarea
|
||||||
className="border rounded p-2 w-full"
|
className="border rounded p-2 w-full"
|
||||||
placeholder="Description"
|
placeholder="Description"
|
||||||
value={description}
|
value={description}
|
||||||
onChange={e => setDescription(e.target.value)}
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="Location"
|
||||||
|
value={location}
|
||||||
|
onChange={(e) => setLocation(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
placeholder="URL"
|
||||||
|
value={url}
|
||||||
|
onChange={(e) => setUrl(e.target.value)}
|
||||||
/>
|
/>
|
||||||
<Input placeholder="Location" value={location} onChange={e => setLocation(e.target.value)} />
|
|
||||||
<Input placeholder="URL" value={url} onChange={e => setUrl(e.target.value)} />
|
|
||||||
<RecurrencePicker value={recurrenceRule} onChange={setRecurrenceRule} />
|
<RecurrencePicker value={recurrenceRule} onChange={setRecurrenceRule} />
|
||||||
|
|
||||||
<label className="flex items-center gap-2 mt-2">
|
<label className="flex items-center gap-2 mt-2">
|
||||||
<input type="checkbox" checked={allDay} onChange={e => setAllDay(e.target.checked)} />
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={allDay}
|
||||||
|
onChange={(e) => setAllDay(e.target.checked)}
|
||||||
|
/>
|
||||||
All day event
|
All day event
|
||||||
</label>
|
</label>
|
||||||
{!allDay ? (
|
{!allDay ? (
|
||||||
<>
|
<>
|
||||||
<Input type="datetime-local" value={start} onChange={e => setStart(e.target.value)} />
|
<Input
|
||||||
<Input type="datetime-local" value={end} onChange={e => setEnd(e.target.value)} />
|
type="datetime-local"
|
||||||
|
value={start}
|
||||||
|
onChange={(e) => setStart(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="datetime-local"
|
||||||
|
value={end}
|
||||||
|
onChange={(e) => setEnd(e.target.value)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<Input type="date" value={start ? start.split('T')[0] : ''} onChange={e => setStart(e.target.value)} />
|
<Input
|
||||||
<Input type="date" value={end ? end.split('T')[0] : ''} onChange={e => setEnd(e.target.value)} />
|
type="date"
|
||||||
|
value={start ? start.split("T")[0] : ""}
|
||||||
|
onChange={(e) => setStart(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={end ? end.split("T")[0] : ""}
|
||||||
|
onChange={(e) => setEnd(e.target.value)}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button onClick={onSave}>{editingId ? 'Update' : 'Save'}</Button>
|
<Button onClick={onSave}>{editingId ? "Update" : "Save"}</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,27 +1,31 @@
|
|||||||
import { Calendar1Icon } from 'lucide-react'
|
import { Calendar1Icon } from "lucide-react";
|
||||||
import { EventCard } from './event-card'
|
import { EventCard } from "./event-card";
|
||||||
import type { CalendarEvent } from '@/lib/types'
|
import type { CalendarEvent } from "@/lib/types";
|
||||||
|
|
||||||
interface EventsListProps {
|
interface EventsListProps {
|
||||||
events: CalendarEvent[]
|
events: CalendarEvent[];
|
||||||
onEdit: (event: CalendarEvent) => void
|
onEdit: (event: CalendarEvent) => void;
|
||||||
onDelete: (eventId: string) => void
|
onDelete: (eventId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const EventsList = ({ events, onEdit, onDelete }: EventsListProps) => {
|
export const EventsList = ({ events, onEdit, onDelete }: EventsListProps) => {
|
||||||
if (events.length === 0) {
|
if (events.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col items-center justify-center py-8 text-center">
|
<div className="flex flex-col items-center justify-center py-8 text-center">
|
||||||
<Calendar1Icon className='h-12 w-12 text-muted-foreground mb-4' />
|
<Calendar1Icon className="h-12 w-12 text-muted-foreground mb-4" />
|
||||||
<h3 className="text-lg font-medium text-muted-foreground">No events yet</h3>
|
<h3 className="text-lg font-medium text-muted-foreground">
|
||||||
<p className="text-sm text-muted-foreground">Create your first event to get started</p>
|
No events yet
|
||||||
|
</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Create your first event to get started
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
{events.map(event => (
|
{events.map((event) => (
|
||||||
<EventCard
|
<EventCard
|
||||||
key={event.id}
|
key={event.id}
|
||||||
event={event}
|
event={event}
|
||||||
@@ -30,5 +34,5 @@ export const EventsList = ({ events, onEdit, onDelete }: EventsListProps) => {
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import type React from "react"
|
import type React from "react";
|
||||||
|
|
||||||
import { useRef } from "react"
|
import { useRef } from "react";
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button";
|
||||||
import { Calendar } from "lucide-react"
|
import { Calendar } from "lucide-react";
|
||||||
|
|
||||||
interface IcsFilePickerProps {
|
interface IcsFilePickerProps {
|
||||||
onFileSelect?: (file: File) => void
|
onFileSelect?: (file: File) => void;
|
||||||
className?: string
|
className?: string;
|
||||||
children?: React.ReactNode
|
children?: React.ReactNode;
|
||||||
variant?: "default" | "destructive" | "outline" | "secondary" | "ghost" | "link"
|
variant?:
|
||||||
size?: "default" | "sm" | "lg" | "icon"
|
| "default"
|
||||||
|
| "destructive"
|
||||||
|
| "outline"
|
||||||
|
| "secondary"
|
||||||
|
| "ghost"
|
||||||
|
| "link";
|
||||||
|
size?: "default" | "sm" | "lg" | "icon";
|
||||||
}
|
}
|
||||||
|
|
||||||
export function IcsFilePicker({
|
export function IcsFilePicker({
|
||||||
@@ -21,18 +27,18 @@ export function IcsFilePicker({
|
|||||||
variant = "default",
|
variant = "default",
|
||||||
size = "default",
|
size = "default",
|
||||||
}: IcsFilePickerProps) {
|
}: IcsFilePickerProps) {
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
const handleButtonClick = () => {
|
const handleButtonClick = () => {
|
||||||
fileInputRef.current?.click()
|
fileInputRef.current?.click();
|
||||||
}
|
};
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
const file = event.target.files?.[0]
|
const file = event.target.files?.[0];
|
||||||
if (file && onFileSelect) {
|
if (file && onFileSelect) {
|
||||||
onFileSelect(file)
|
onFileSelect(file);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -44,7 +50,12 @@ export function IcsFilePicker({
|
|||||||
className="hidden"
|
className="hidden"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
/>
|
/>
|
||||||
<Button onClick={handleButtonClick} variant={variant} size={size} className={className}>
|
<Button
|
||||||
|
onClick={handleButtonClick}
|
||||||
|
variant={variant}
|
||||||
|
size={size}
|
||||||
|
className={className}
|
||||||
|
>
|
||||||
{children || (
|
{children || (
|
||||||
<>
|
<>
|
||||||
<Calendar className="mr-2 h-4 w-4" />
|
<Calendar className="mr-2 h-4 w-4" />
|
||||||
@@ -53,6 +64,5 @@ export function IcsFilePicker({
|
|||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,56 +1,55 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { Moon, Sun, Monitor } from "lucide-react"
|
import { Moon, Sun, Monitor } from "lucide-react";
|
||||||
import { useTheme } from "next-themes"
|
import { useTheme } from "next-themes";
|
||||||
|
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownMenuTrigger,
|
DropdownMenuTrigger,
|
||||||
} from "@/components/ui/dropdown-menu"
|
} from "@/components/ui/dropdown-menu";
|
||||||
|
|
||||||
type ThemeIconProps = {
|
type ThemeIconProps = {
|
||||||
theme?: string
|
theme?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
const ThemeIcon = ({ theme }: ThemeIconProps) => {
|
const ThemeIcon = ({ theme }: ThemeIconProps) => {
|
||||||
|
const [mounted, setMounted] = React.useState(false);
|
||||||
const [mounted, setMounted] = React.useState(false)
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
setMounted(true)
|
setMounted(true);
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
if (!mounted) {
|
if (!mounted) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (theme) {
|
switch (theme) {
|
||||||
case "light":
|
case "light":
|
||||||
return (
|
return (
|
||||||
<Sun className="absolute h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
<Sun className="absolute h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||||
)
|
);
|
||||||
case "dark":
|
case "dark":
|
||||||
return (
|
return (
|
||||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||||
)
|
);
|
||||||
case "system":
|
case "system":
|
||||||
return (
|
return <Monitor className="absolute h-[1.2rem] w-[1.2rem] scale-100" />;
|
||||||
<Monitor className="absolute h-[1.2rem] w-[1.2rem] scale-100" />
|
|
||||||
)
|
|
||||||
default:
|
default:
|
||||||
return (<>
|
return (
|
||||||
|
<>
|
||||||
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
<Sun className="h-[1.2rem] w-[1.2rem] scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
||||||
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
<Moon className="absolute h-[1.2rem] w-[1.2rem] scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
||||||
</>)
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export function ModeToggle() {
|
export function ModeToggle() {
|
||||||
const { setTheme, theme } = useTheme()
|
const { setTheme, theme } = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
@@ -72,6 +71,5 @@ export function ModeToggle() {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,65 +1,76 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { useState } from "react"
|
import { useState } from "react";
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label";
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import {
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { Checkbox } from "@/components/ui/checkbox";
|
||||||
|
|
||||||
type Recurrence = {
|
type Recurrence = {
|
||||||
freq: "NONE" | "DAILY" | "WEEKLY" | "MONTHLY"
|
freq: "NONE" | "DAILY" | "WEEKLY" | "MONTHLY";
|
||||||
interval: number
|
interval: number;
|
||||||
byDay?: string[]
|
byDay?: string[];
|
||||||
count?: number
|
count?: number;
|
||||||
until?: string
|
until?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
value?: string
|
value?: string;
|
||||||
onChange: (rrule: string | undefined) => void
|
onChange: (rrule: string | undefined) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RecurrencePicker({ value, onChange }: Props) {
|
export function RecurrencePicker({ value, onChange }: Props) {
|
||||||
const [rec, setRec] = useState<Recurrence>(() => {
|
const [rec, setRec] = useState<Recurrence>(() => {
|
||||||
// If existing rrule, parse minimally (for simplicity we only rehydrate FREQ and INTERVAL)
|
// If existing rrule, parse minimally (for simplicity we only rehydrate FREQ and INTERVAL)
|
||||||
if (value) {
|
if (value) {
|
||||||
const parts = Object.fromEntries(value.split(";").map((p) => p.split("=")))
|
const parts = Object.fromEntries(
|
||||||
|
value.split(";").map((p) => p.split("=")),
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
freq: parts.FREQ || "NONE",
|
freq: parts.FREQ || "NONE",
|
||||||
interval: parts.INTERVAL ? Number.parseInt(parts.INTERVAL, 10) : 1,
|
interval: parts.INTERVAL ? Number.parseInt(parts.INTERVAL, 10) : 1,
|
||||||
byDay: parts.BYDAY ? parts.BYDAY.split(",") : [],
|
byDay: parts.BYDAY ? parts.BYDAY.split(",") : [],
|
||||||
count: parts.COUNT ? Number.parseInt(parts.COUNT, 10) : undefined,
|
count: parts.COUNT ? Number.parseInt(parts.COUNT, 10) : undefined,
|
||||||
until: parts.UNTIL,
|
until: parts.UNTIL,
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
return { freq: "NONE", interval: 1 };
|
||||||
return { freq: "NONE", interval: 1 }
|
});
|
||||||
})
|
|
||||||
|
|
||||||
const update = (updates: Partial<Recurrence>) => {
|
const update = (updates: Partial<Recurrence>) => {
|
||||||
const newRec = { ...rec, ...updates }
|
const newRec = { ...rec, ...updates };
|
||||||
setRec(newRec)
|
setRec(newRec);
|
||||||
|
|
||||||
if (newRec.freq === "NONE") {
|
if (newRec.freq === "NONE") {
|
||||||
onChange(undefined)
|
onChange(undefined);
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build RRULE string
|
// Build RRULE string
|
||||||
let rrule = `FREQ=${newRec.freq};INTERVAL=${newRec.interval}`
|
let rrule = `FREQ=${newRec.freq};INTERVAL=${newRec.interval}`;
|
||||||
if (newRec.freq === "WEEKLY" && newRec.byDay?.length) {
|
if (newRec.freq === "WEEKLY" && newRec.byDay?.length) {
|
||||||
rrule += `;BYDAY=${newRec.byDay.join(",")}`
|
rrule += `;BYDAY=${newRec.byDay.join(",")}`;
|
||||||
}
|
}
|
||||||
if (newRec.count) rrule += `;COUNT=${newRec.count}`
|
if (newRec.count) rrule += `;COUNT=${newRec.count}`;
|
||||||
if (newRec.until) rrule += `;UNTIL=${newRec.until.replace(/-/g, "")}T000000Z`
|
if (newRec.until)
|
||||||
|
rrule += `;UNTIL=${newRec.until.replace(/-/g, "")}T000000Z`;
|
||||||
|
|
||||||
onChange(rrule)
|
onChange(rrule);
|
||||||
}
|
};
|
||||||
|
|
||||||
const toggleDay = (day: string) => {
|
const toggleDay = (day: string) => {
|
||||||
const byDay = rec.byDay || []
|
const byDay = rec.byDay || [];
|
||||||
const newByDay = byDay.includes(day) ? byDay.filter((d) => d !== day) : [...byDay, day]
|
const newByDay = byDay.includes(day)
|
||||||
update({ byDay: newByDay })
|
? byDay.filter((d) => d !== day)
|
||||||
}
|
: [...byDay, day];
|
||||||
|
update({ byDay: newByDay });
|
||||||
|
};
|
||||||
|
|
||||||
const dayLabels = {
|
const dayLabels = {
|
||||||
MO: "Mon",
|
MO: "Mon",
|
||||||
@@ -69,13 +80,20 @@ export function RecurrencePicker({ value, onChange }: Props) {
|
|||||||
FR: "Fri",
|
FR: "Fri",
|
||||||
SA: "Sat",
|
SA: "Sat",
|
||||||
SU: "Sun",
|
SU: "Sun",
|
||||||
}
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
<Label htmlFor="frequency" className="pt-4 pb-2 pl-1">Repeats</Label>
|
<Label htmlFor="frequency" className="pt-4 pb-2 pl-1">
|
||||||
|
Repeats
|
||||||
|
</Label>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Select value={rec.freq} onValueChange={(value) => update({ freq: value as Recurrence["freq"] })}>
|
<Select
|
||||||
|
value={rec.freq}
|
||||||
|
onValueChange={(value) =>
|
||||||
|
update({ freq: value as Recurrence["freq"] })
|
||||||
|
}
|
||||||
|
>
|
||||||
<SelectTrigger id="frequency">
|
<SelectTrigger id="frequency">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
@@ -92,7 +110,12 @@ export function RecurrencePicker({ value, onChange }: Props) {
|
|||||||
<>
|
<>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="interval">
|
<Label htmlFor="interval">
|
||||||
Interval (every {rec.interval} {rec.freq === "DAILY" ? "day" : rec.freq === "WEEKLY" ? "week" : "month"}
|
Interval (every {rec.interval}{" "}
|
||||||
|
{rec.freq === "DAILY"
|
||||||
|
? "day"
|
||||||
|
: rec.freq === "WEEKLY"
|
||||||
|
? "week"
|
||||||
|
: "month"}
|
||||||
{rec.interval > 1 ? "s" : ""})
|
{rec.interval > 1 ? "s" : ""})
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
@@ -100,7 +123,9 @@ export function RecurrencePicker({ value, onChange }: Props) {
|
|||||||
type="number"
|
type="number"
|
||||||
min={1}
|
min={1}
|
||||||
value={rec.interval}
|
value={rec.interval}
|
||||||
onChange={(e) => update({ interval: Number.parseInt(e.target.value, 10) || 1 })}
|
onChange={(e) =>
|
||||||
|
update({ interval: Number.parseInt(e.target.value, 10) || 1 })
|
||||||
|
}
|
||||||
className="w-24"
|
className="w-24"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -133,7 +158,13 @@ export function RecurrencePicker({ value, onChange }: Props) {
|
|||||||
type="number"
|
type="number"
|
||||||
placeholder="e.g. 10"
|
placeholder="e.g. 10"
|
||||||
value={rec.count || ""}
|
value={rec.count || ""}
|
||||||
onChange={(e) => update({ count: e.target.value ? Number.parseInt(e.target.value, 10) : undefined })}
|
onChange={(e) =>
|
||||||
|
update({
|
||||||
|
count: e.target.value
|
||||||
|
? Number.parseInt(e.target.value, 10)
|
||||||
|
: undefined,
|
||||||
|
})
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
@@ -149,5 +180,5 @@ export function RecurrencePicker({ value, onChange }: Props) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,38 @@
|
|||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge";
|
||||||
import type { RecurrenceRule } from "@/lib/rfc5545-types"
|
import type { RecurrenceRule } from "@/lib/rfc5545-types";
|
||||||
|
|
||||||
interface RRuleDisplayProps {
|
interface RRuleDisplayProps {
|
||||||
rrule: string | RecurrenceRule
|
rrule: string | RecurrenceRule;
|
||||||
className?: string
|
className?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RRuleDisplay({ rrule, className }: RRuleDisplayProps) {
|
export function RRuleDisplay({ rrule, className }: RRuleDisplayProps) {
|
||||||
const parsedRule = typeof rrule === 'string' ? parseRRuleString(rrule) : rrule
|
const parsedRule =
|
||||||
const humanText = formatRRuleToHuman(parsedRule)
|
typeof rrule === "string" ? parseRRuleString(rrule) : rrule;
|
||||||
|
const humanText = formatRRuleToHuman(parsedRule);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
<span className="text-sm text-muted-foreground">{humanText}</span>
|
<span className="text-sm text-muted-foreground">{humanText}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RRuleDisplayDetailedProps {
|
interface RRuleDisplayDetailedProps {
|
||||||
rrule: string | RecurrenceRule
|
rrule: string | RecurrenceRule;
|
||||||
className?: string
|
className?: string;
|
||||||
showBadges?: boolean
|
showBadges?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RRuleDisplayDetailed({ rrule, className, showBadges = true }: RRuleDisplayDetailedProps) {
|
export function RRuleDisplayDetailed({
|
||||||
const parsedRule = typeof rrule === 'string' ? parseRRuleString(rrule) : rrule
|
rrule,
|
||||||
const humanText = formatRRuleToHuman(parsedRule)
|
className,
|
||||||
const details = getRRuleDetails(parsedRule)
|
showBadges = true,
|
||||||
|
}: RRuleDisplayDetailedProps) {
|
||||||
|
const parsedRule =
|
||||||
|
typeof rrule === "string" ? parseRRuleString(rrule) : rrule;
|
||||||
|
const humanText = formatRRuleToHuman(parsedRule);
|
||||||
|
const details = getRRuleDetails(parsedRule);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={className}>
|
<div className={className}>
|
||||||
@@ -44,201 +50,257 @@ export function RRuleDisplayDetailed({ rrule, className, showBadges = true }: RR
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseRRuleString(rruleString: string): RecurrenceRule {
|
function parseRRuleString(rruleString: string): RecurrenceRule {
|
||||||
const parts = Object.fromEntries(rruleString.split(";").map(p => p.split("=")))
|
const parts = Object.fromEntries(
|
||||||
|
rruleString.split(";").map((p) => p.split("=")),
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
freq: parts.FREQ as RecurrenceRule['freq'],
|
freq: parts.FREQ as RecurrenceRule["freq"],
|
||||||
until: parts.UNTIL ? new Date(parts.UNTIL.replace(/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z?/, '$1-$2-$3T$4:$5:$6Z')).toISOString() : undefined,
|
until: parts.UNTIL
|
||||||
|
? new Date(
|
||||||
|
parts.UNTIL.replace(
|
||||||
|
/(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})Z?/,
|
||||||
|
"$1-$2-$3T$4:$5:$6Z",
|
||||||
|
),
|
||||||
|
).toISOString()
|
||||||
|
: undefined,
|
||||||
count: parts.COUNT ? parseInt(parts.COUNT, 10) : undefined,
|
count: parts.COUNT ? parseInt(parts.COUNT, 10) : undefined,
|
||||||
interval: parts.INTERVAL ? parseInt(parts.INTERVAL, 10) : undefined,
|
interval: parts.INTERVAL ? parseInt(parts.INTERVAL, 10) : undefined,
|
||||||
bySecond: parts.BYSECOND ? parts.BYSECOND.split(",").map((n: string) => parseInt(n, 10)) : undefined,
|
bySecond: parts.BYSECOND
|
||||||
byMinute: parts.BYMINUTE ? parts.BYMINUTE.split(",").map((n: string) => parseInt(n, 10)) : undefined,
|
? parts.BYSECOND.split(",").map((n: string) => parseInt(n, 10))
|
||||||
byHour: parts.BYHOUR ? parts.BYHOUR.split(",").map((n: string) => parseInt(n, 10)) : undefined,
|
: undefined,
|
||||||
|
byMinute: parts.BYMINUTE
|
||||||
|
? parts.BYMINUTE.split(",").map((n: string) => parseInt(n, 10))
|
||||||
|
: undefined,
|
||||||
|
byHour: parts.BYHOUR
|
||||||
|
? parts.BYHOUR.split(",").map((n: string) => parseInt(n, 10))
|
||||||
|
: undefined,
|
||||||
byDay: parts.BYDAY ? parts.BYDAY.split(",") : undefined,
|
byDay: parts.BYDAY ? parts.BYDAY.split(",") : undefined,
|
||||||
byMonthDay: parts.BYMONTHDAY ? parts.BYMONTHDAY.split(",").map((n: string) => parseInt(n, 10)) : undefined,
|
byMonthDay: parts.BYMONTHDAY
|
||||||
byYearDay: parts.BYYEARDAY ? parts.BYYEARDAY.split(",").map((n: string) => parseInt(n, 10)) : undefined,
|
? parts.BYMONTHDAY.split(",").map((n: string) => parseInt(n, 10))
|
||||||
byWeekNo: parts.BYWEEKNO ? parts.BYWEEKNO.split(",").map((n: string) => parseInt(n, 10)) : undefined,
|
: undefined,
|
||||||
byMonth: parts.BYMONTH ? parts.BYMONTH.split(",").map((n: string) => parseInt(n, 10)) : undefined,
|
byYearDay: parts.BYYEARDAY
|
||||||
bySetPos: parts.BYSETPOS ? parts.BYSETPOS.split(",").map((n: string) => parseInt(n, 10)) : undefined,
|
? parts.BYYEARDAY.split(",").map((n: string) => parseInt(n, 10))
|
||||||
wkst: parts.WKST as RecurrenceRule['wkst'],
|
: undefined,
|
||||||
}
|
byWeekNo: parts.BYWEEKNO
|
||||||
|
? parts.BYWEEKNO.split(",").map((n: string) => parseInt(n, 10))
|
||||||
|
: undefined,
|
||||||
|
byMonth: parts.BYMONTH
|
||||||
|
? parts.BYMONTH.split(",").map((n: string) => parseInt(n, 10))
|
||||||
|
: undefined,
|
||||||
|
bySetPos: parts.BYSETPOS
|
||||||
|
? parts.BYSETPOS.split(",").map((n: string) => parseInt(n, 10))
|
||||||
|
: undefined,
|
||||||
|
wkst: parts.WKST as RecurrenceRule["wkst"],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatRRuleToHuman(rule: RecurrenceRule): string {
|
function formatRRuleToHuman(rule: RecurrenceRule): string {
|
||||||
const { freq, interval = 1, count, until, byDay, byMonthDay, byMonth, byHour, byMinute, bySecond } = rule
|
const {
|
||||||
|
freq,
|
||||||
|
interval = 1,
|
||||||
|
count,
|
||||||
|
until,
|
||||||
|
byDay,
|
||||||
|
byMonthDay,
|
||||||
|
byMonth,
|
||||||
|
byHour,
|
||||||
|
byMinute,
|
||||||
|
bySecond,
|
||||||
|
} = rule;
|
||||||
|
|
||||||
let text = ""
|
let text = "";
|
||||||
|
|
||||||
// Base frequency
|
// Base frequency
|
||||||
switch (freq) {
|
switch (freq) {
|
||||||
case 'SECONDLY':
|
case "SECONDLY":
|
||||||
text = interval === 1 ? "Every second" : `Every ${interval} seconds`
|
text = interval === 1 ? "Every second" : `Every ${interval} seconds`;
|
||||||
break
|
break;
|
||||||
case 'MINUTELY':
|
case "MINUTELY":
|
||||||
text = interval === 1 ? "Every minute" : `Every ${interval} minutes`
|
text = interval === 1 ? "Every minute" : `Every ${interval} minutes`;
|
||||||
break
|
break;
|
||||||
case 'HOURLY':
|
case "HOURLY":
|
||||||
text = interval === 1 ? "Every hour" : `Every ${interval} hours`
|
text = interval === 1 ? "Every hour" : `Every ${interval} hours`;
|
||||||
break
|
break;
|
||||||
case 'DAILY':
|
case "DAILY":
|
||||||
text = interval === 1 ? "Daily" : `Every ${interval} days`
|
text = interval === 1 ? "Daily" : `Every ${interval} days`;
|
||||||
break
|
break;
|
||||||
case 'WEEKLY':
|
case "WEEKLY":
|
||||||
text = interval === 1 ? "Weekly" : `Every ${interval} weeks`
|
text = interval === 1 ? "Weekly" : `Every ${interval} weeks`;
|
||||||
break
|
break;
|
||||||
case 'MONTHLY':
|
case "MONTHLY":
|
||||||
text = interval === 1 ? "Monthly" : `Every ${interval} months`
|
text = interval === 1 ? "Monthly" : `Every ${interval} months`;
|
||||||
break
|
break;
|
||||||
case 'YEARLY':
|
case "YEARLY":
|
||||||
text = interval === 1 ? "Yearly" : `Every ${interval} years`
|
text = interval === 1 ? "Yearly" : `Every ${interval} years`;
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add day specifications
|
// Add day specifications
|
||||||
if (byDay?.length) {
|
if (byDay?.length) {
|
||||||
const dayNames = {
|
const dayNames = {
|
||||||
'SU': 'Sunday', 'MO': 'Monday', 'TU': 'Tuesday', 'WE': 'Wednesday',
|
SU: "Sunday",
|
||||||
'TH': 'Thursday', 'FR': 'Friday', 'SA': 'Saturday'
|
MO: "Monday",
|
||||||
}
|
TU: "Tuesday",
|
||||||
|
WE: "Wednesday",
|
||||||
|
TH: "Thursday",
|
||||||
|
FR: "Friday",
|
||||||
|
SA: "Saturday",
|
||||||
|
};
|
||||||
|
|
||||||
const days = byDay.map(day => {
|
const days = byDay.map((day) => {
|
||||||
// Handle numbered days like "2TU" (second Tuesday)
|
// Handle numbered days like "2TU" (second Tuesday)
|
||||||
const match = day.match(/^(-?\d+)?([A-Z]{2})$/)
|
const match = day.match(/^(-?\d+)?([A-Z]{2})$/);
|
||||||
if (match) {
|
if (match) {
|
||||||
const [, num, dayCode] = match
|
const [, num, dayCode] = match;
|
||||||
const dayName = dayNames[dayCode as keyof typeof dayNames]
|
const dayName = dayNames[dayCode as keyof typeof dayNames];
|
||||||
if (num) {
|
if (num) {
|
||||||
const ordinal = getOrdinal(parseInt(num))
|
const ordinal = getOrdinal(parseInt(num));
|
||||||
return `${ordinal} ${dayName}`
|
return `${ordinal} ${dayName}`;
|
||||||
}
|
}
|
||||||
return dayName
|
return dayName;
|
||||||
}
|
}
|
||||||
return day
|
return day;
|
||||||
})
|
});
|
||||||
|
|
||||||
if (freq === 'WEEKLY') {
|
if (freq === "WEEKLY") {
|
||||||
text += ` on ${formatList(days)}`
|
text += ` on ${formatList(days)}`;
|
||||||
} else {
|
} else {
|
||||||
text += ` on ${formatList(days)}`
|
text += ` on ${formatList(days)}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add month day specifications
|
// Add month day specifications
|
||||||
if (byMonthDay?.length) {
|
if (byMonthDay?.length) {
|
||||||
const days = byMonthDay.map(day => {
|
const days = byMonthDay.map((day) => {
|
||||||
if (day < 0) {
|
if (day < 0) {
|
||||||
return `${getOrdinal(Math.abs(day))} to last day`
|
return `${getOrdinal(Math.abs(day))} to last day`;
|
||||||
}
|
}
|
||||||
return getOrdinal(day)
|
return getOrdinal(day);
|
||||||
})
|
});
|
||||||
text += ` on the ${formatList(days)}`
|
text += ` on the ${formatList(days)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add month specifications
|
// Add month specifications
|
||||||
if (byMonth?.length) {
|
if (byMonth?.length) {
|
||||||
const monthNames = [
|
const monthNames = [
|
||||||
'January', 'February', 'March', 'April', 'May', 'June',
|
"January",
|
||||||
'July', 'August', 'September', 'October', 'November', 'December'
|
"February",
|
||||||
]
|
"March",
|
||||||
const months = byMonth.map(month => monthNames[month - 1])
|
"April",
|
||||||
text += ` in ${formatList(months)}`
|
"May",
|
||||||
|
"June",
|
||||||
|
"July",
|
||||||
|
"August",
|
||||||
|
"September",
|
||||||
|
"October",
|
||||||
|
"November",
|
||||||
|
"December",
|
||||||
|
];
|
||||||
|
const months = byMonth.map((month) => monthNames[month - 1]);
|
||||||
|
text += ` in ${formatList(months)}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add time specifications
|
// Add time specifications
|
||||||
if (byHour?.length || byMinute?.length || bySecond?.length) {
|
if (byHour?.length || byMinute?.length || bySecond?.length) {
|
||||||
const timeSpecs = []
|
const timeSpecs = [];
|
||||||
if (byHour?.length) {
|
if (byHour?.length) {
|
||||||
const hours = byHour.map(h => `${h.toString().padStart(2, '0')}:00`)
|
const hours = byHour.map((h) => `${h.toString().padStart(2, "0")}:00`);
|
||||||
timeSpecs.push(`at ${formatList(hours)}`)
|
timeSpecs.push(`at ${formatList(hours)}`);
|
||||||
}
|
}
|
||||||
if (byMinute?.length && !byHour?.length) {
|
if (byMinute?.length && !byHour?.length) {
|
||||||
timeSpecs.push(`at minute ${formatList(byMinute.map(String))}`)
|
timeSpecs.push(`at minute ${formatList(byMinute.map(String))}`);
|
||||||
}
|
}
|
||||||
if (bySecond?.length && !byHour?.length && !byMinute?.length) {
|
if (bySecond?.length && !byHour?.length && !byMinute?.length) {
|
||||||
timeSpecs.push(`at second ${formatList(bySecond.map(String))}`)
|
timeSpecs.push(`at second ${formatList(bySecond.map(String))}`);
|
||||||
}
|
}
|
||||||
if (timeSpecs.length) {
|
if (timeSpecs.length) {
|
||||||
text += ` ${timeSpecs.join(' ')}`
|
text += ` ${timeSpecs.join(" ")}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add end conditions
|
// Add end conditions
|
||||||
if (count) {
|
if (count) {
|
||||||
text += `, ${count} time${count === 1 ? '' : 's'}`
|
text += `, ${count} time${count === 1 ? "" : "s"}`;
|
||||||
} else if (until) {
|
} else if (until) {
|
||||||
const date = new Date(until)
|
const date = new Date(until);
|
||||||
text += `, until ${date.toLocaleDateString()}`
|
text += `, until ${date.toLocaleDateString()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return text
|
return text;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRRuleDetails(rule: RecurrenceRule): string[] {
|
function getRRuleDetails(rule: RecurrenceRule): string[] {
|
||||||
const details: string[] = []
|
const details: string[] = [];
|
||||||
|
|
||||||
if (rule.wkst && rule.wkst !== 'MO') {
|
if (rule.wkst && rule.wkst !== "MO") {
|
||||||
const dayNames = {
|
const dayNames = {
|
||||||
'SU': 'Sunday', 'MO': 'Monday', 'TU': 'Tuesday', 'WE': 'Wednesday',
|
SU: "Sunday",
|
||||||
'TH': 'Thursday', 'FR': 'Friday', 'SA': 'Saturday'
|
MO: "Monday",
|
||||||
}
|
TU: "Tuesday",
|
||||||
details.push(`Week starts ${dayNames[rule.wkst]}`)
|
WE: "Wednesday",
|
||||||
|
TH: "Thursday",
|
||||||
|
FR: "Friday",
|
||||||
|
SA: "Saturday",
|
||||||
|
};
|
||||||
|
details.push(`Week starts ${dayNames[rule.wkst]}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rule.byWeekNo?.length) {
|
if (rule.byWeekNo?.length) {
|
||||||
details.push(`Week ${formatList(rule.byWeekNo.map(String))}`)
|
details.push(`Week ${formatList(rule.byWeekNo.map(String))}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rule.byYearDay?.length) {
|
if (rule.byYearDay?.length) {
|
||||||
details.push(`Day ${formatList(rule.byYearDay.map(String))} of year`)
|
details.push(`Day ${formatList(rule.byYearDay.map(String))} of year`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rule.bySetPos?.length) {
|
if (rule.bySetPos?.length) {
|
||||||
const positions = rule.bySetPos.map(pos => {
|
const positions = rule.bySetPos.map((pos) => {
|
||||||
if (pos < 0) {
|
if (pos < 0) {
|
||||||
return `${getOrdinal(Math.abs(pos))} to last`
|
return `${getOrdinal(Math.abs(pos))} to last`;
|
||||||
}
|
}
|
||||||
return getOrdinal(pos)
|
return getOrdinal(pos);
|
||||||
})
|
});
|
||||||
details.push(`Position ${formatList(positions)}`)
|
details.push(`Position ${formatList(positions)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
return details
|
return details;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getOrdinal(num: number): string {
|
function getOrdinal(num: number): string {
|
||||||
const suffix = ['th', 'st', 'nd', 'rd']
|
const suffix = ["th", "st", "nd", "rd"];
|
||||||
const v = num % 100
|
const v = num % 100;
|
||||||
return num + (suffix[(v - 20) % 10] || suffix[v] || suffix[0])
|
return num + (suffix[(v - 20) % 10] || suffix[v] || suffix[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
function formatList(items: string[]): string {
|
function formatList(items: string[]): string {
|
||||||
if (items.length === 0) return ''
|
if (items.length === 0) return "";
|
||||||
if (items.length === 1) return items[0]
|
if (items.length === 1) return items[0];
|
||||||
if (items.length === 2) return `${items[0]} and ${items[1]}`
|
if (items.length === 2) return `${items[0]} and ${items[1]}`;
|
||||||
return `${items.slice(0, -1).join(', ')}, and ${items[items.length - 1]}`
|
return `${items.slice(0, -1).join(", ")}, and ${items[items.length - 1]}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Hook for easy usage in components
|
// Hook for easy usage in components
|
||||||
export function useRRuleDisplay(rrule?: string) {
|
export function useRRuleDisplay(rrule?: string) {
|
||||||
if (!rrule) return null
|
if (!rrule) return null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const parsedRule = parseRRuleString(rrule)
|
const parsedRule = parseRRuleString(rrule);
|
||||||
return {
|
return {
|
||||||
humanText: formatRRuleToHuman(parsedRule),
|
humanText: formatRRuleToHuman(parsedRule),
|
||||||
details: getRRuleDetails(parsedRule),
|
details: getRRuleDetails(parsedRule),
|
||||||
parsedRule
|
parsedRule,
|
||||||
}
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
humanText: "Invalid recurrence rule",
|
humanText: "Invalid recurrence rule",
|
||||||
details: [],
|
details: [],
|
||||||
parsedRule: null,
|
parsedRule: null,
|
||||||
error: error instanceof Error ? error.message : String(error)
|
error: error instanceof Error ? error.message : String(error),
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import { signOut, useSession } from "@/lib/auth-client"
|
import { signOut, useSession } from "@/lib/auth-client";
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button";
|
||||||
import { useRouter } from "next/navigation"
|
import { useRouter } from "next/navigation";
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner";
|
||||||
|
|
||||||
export default function SignIn() {
|
export default function SignIn() {
|
||||||
const { data: session, isPending } = useSession()
|
const { data: session, isPending } = useSession();
|
||||||
const router = useRouter()
|
const router = useRouter();
|
||||||
|
|
||||||
const handleSignOut = async () => {
|
const handleSignOut = async () => {
|
||||||
try {
|
try {
|
||||||
await signOut()
|
await signOut();
|
||||||
router.push("/")
|
router.push("/");
|
||||||
} catch (_error) {
|
} catch (_error) {
|
||||||
toast.error("Failed to sign out. Please try again.")
|
toast.error("Failed to sign out. Please try again.");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if (isPending) {
|
if (isPending) {
|
||||||
return <div className="h-8 w-16 bg-muted animate-pulse rounded"></div>
|
return <div className="h-8 w-16 bg-muted animate-pulse rounded"></div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session?.user) {
|
if (session?.user) {
|
||||||
@@ -29,7 +29,7 @@ export default function SignIn() {
|
|||||||
Sign Out
|
Sign Out
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -40,5 +40,5 @@ export default function SignIn() {
|
|||||||
>
|
>
|
||||||
Sign In
|
Sign In
|
||||||
</Button>
|
</Button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client"
|
"use client";
|
||||||
|
|
||||||
import * as React from "react"
|
import * as React from "react";
|
||||||
import { ThemeProvider as NextThemesProvider } from "next-themes"
|
import { ThemeProvider as NextThemesProvider } from "next-themes";
|
||||||
|
|
||||||
export function ThemeProvider({
|
export function ThemeProvider({
|
||||||
children,
|
children,
|
||||||
...props
|
...props
|
||||||
}: React.ComponentProps<typeof NextThemesProvider>) {
|
}: React.ComponentProps<typeof NextThemesProvider>) {
|
||||||
return <NextThemesProvider {...props}>{children}</NextThemesProvider>
|
return <NextThemesProvider {...props}>{children}</NextThemesProvider>;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user