...
This commit is contained in:
parent
cf28f78529
commit
af105583f1
@ -16,6 +16,7 @@ function App() {
|
|||||||
<OurCalendar
|
<OurCalendar
|
||||||
webdavConfig={webdavConfig}
|
webdavConfig={webdavConfig}
|
||||||
calendarFile="/calendars/kristof.json"
|
calendarFile="/calendars/kristof.json"
|
||||||
|
circleFile="/calendars/cirlce1.json"
|
||||||
/>
|
/>
|
||||||
{/* <WebdavFileManager /> */}
|
{/* <WebdavFileManager /> */}
|
||||||
<Toaster />
|
<Toaster />
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import * as React from 'react'
|
import * as React from 'react'
|
||||||
import { addDays, format, startOfWeek, endOfWeek, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isSameDay, parseISO } from 'date-fns'
|
import { addDays, format, startOfWeek, endOfWeek, startOfMonth, endOfMonth, eachDayOfInterval, isSameMonth, isSameDay, parseISO } from 'date-fns'
|
||||||
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, MoreHorizontal, Plus, Moon, Sun, Clock } from 'lucide-react'
|
import { Calendar as CalendarIcon, ChevronLeft, ChevronRight, MoreHorizontal, Plus, Moon, Sun, Clock, X } from 'lucide-react'
|
||||||
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'
|
import { DragDropContext, Droppable, Draggable } from 'react-beautiful-dnd'
|
||||||
import ReactMarkdown from 'react-markdown'
|
import ReactMarkdown from 'react-markdown'
|
||||||
import { v4 as uuidv4 } from 'uuid'
|
import { v4 as uuidv4 } from 'uuid'
|
||||||
@ -39,15 +39,111 @@ import {
|
|||||||
timeOptions,
|
timeOptions,
|
||||||
WebDAVConfig
|
WebDAVConfig
|
||||||
} from '@/lib/calendar-data'
|
} from '@/lib/calendar-data'
|
||||||
|
import { CircleDataManager, Member } from '@/lib/circle-data'
|
||||||
|
|
||||||
const dataManager = CalendarDataManager.getInstance();
|
const dataManager = CalendarDataManager.getInstance();
|
||||||
|
const circleManager = CircleDataManager.getInstance();
|
||||||
|
|
||||||
interface CalendarProps {
|
interface CalendarProps {
|
||||||
webdavConfig: WebDAVConfig;
|
webdavConfig: WebDAVConfig;
|
||||||
calendarFile: string;
|
calendarFile: string;
|
||||||
|
circleFile: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function OurCalendar({ webdavConfig, calendarFile }: CalendarProps) {
|
function AttendeeSelector({ selectedAttendees, onAttendeeChange }: {
|
||||||
|
selectedAttendees: string[];
|
||||||
|
onAttendeeChange: (attendees: string[]) => void;
|
||||||
|
}) {
|
||||||
|
const [searchTerm, setSearchTerm] = React.useState('');
|
||||||
|
const [members, setMembers] = React.useState<Member[]>([]);
|
||||||
|
const [suggestions, setSuggestions] = React.useState<Member[]>([]);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
const fetchMembers = async () => {
|
||||||
|
try {
|
||||||
|
const fetchedMembers = await circleManager.fetchMembers();
|
||||||
|
setMembers(fetchedMembers);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error fetching members:', error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
fetchMembers();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (searchTerm.trim() === '') {
|
||||||
|
setSuggestions([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const filtered = members.filter(member =>
|
||||||
|
member.name.toLowerCase().includes(searchTerm.toLowerCase()) &&
|
||||||
|
!selectedAttendees.includes(member.email)
|
||||||
|
);
|
||||||
|
setSuggestions(filtered);
|
||||||
|
}, [searchTerm, members, selectedAttendees]);
|
||||||
|
|
||||||
|
const handleAddAttendee = (member: Member) => {
|
||||||
|
onAttendeeChange([...selectedAttendees, member.email]);
|
||||||
|
setSearchTerm('');
|
||||||
|
setSuggestions([]);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveAttendee = (email: string) => {
|
||||||
|
onAttendeeChange(selectedAttendees.filter(e => e !== email));
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMemberName = (email: string) => {
|
||||||
|
const member = members.find(m => m.email === email);
|
||||||
|
return member ? member.name : email;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<div className="flex flex-wrap gap-2 mb-2">
|
||||||
|
{selectedAttendees.map(email => (
|
||||||
|
<div
|
||||||
|
key={email}
|
||||||
|
className="flex items-center gap-1 bg-blue-100 dark:bg-blue-800 px-2 py-1 rounded"
|
||||||
|
>
|
||||||
|
<span>{getMemberName(email)}</span>
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveAttendee(email)}
|
||||||
|
className="text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200"
|
||||||
|
>
|
||||||
|
<X className="h-3 w-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="relative">
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
placeholder="Type to search members..."
|
||||||
|
className="dark:bg-gray-700 dark:text-white dark:border-gray-600"
|
||||||
|
/>
|
||||||
|
{suggestions.length > 0 && (
|
||||||
|
<div className="absolute z-10 w-full mt-1 bg-white dark:bg-gray-800 border dark:border-gray-600 rounded-md shadow-lg">
|
||||||
|
{suggestions.map(member => (
|
||||||
|
<div
|
||||||
|
key={member.id}
|
||||||
|
className="px-4 py-2 hover:bg-gray-100 dark:hover:bg-gray-700 cursor-pointer"
|
||||||
|
onClick={() => handleAddAttendee(member)}
|
||||||
|
>
|
||||||
|
<div>{member.name}</div>
|
||||||
|
<div className="text-sm text-gray-500 dark:text-gray-400">{member.email}</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function OurCalendar({ webdavConfig, calendarFile, circleFile }: CalendarProps) {
|
||||||
const [currentDate, setCurrentDate] = React.useState(new Date())
|
const [currentDate, setCurrentDate] = React.useState(new Date())
|
||||||
const [events, setEvents] = React.useState<Event[]>([])
|
const [events, setEvents] = React.useState<Event[]>([])
|
||||||
const [selectedEvent, setSelectedEvent] = React.useState<Event | null>(null)
|
const [selectedEvent, setSelectedEvent] = React.useState<Event | null>(null)
|
||||||
@ -60,7 +156,8 @@ export function OurCalendar({ webdavConfig, calendarFile }: CalendarProps) {
|
|||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
dataManager.setConfig(webdavConfig, calendarFile);
|
dataManager.setConfig(webdavConfig, calendarFile);
|
||||||
}, [webdavConfig, calendarFile]);
|
circleManager.setConfig(webdavConfig, circleFile);
|
||||||
|
}, [webdavConfig, calendarFile, circleFile]);
|
||||||
|
|
||||||
const startDate = startOfWeek(startOfMonth(currentDate))
|
const startDate = startOfWeek(startOfMonth(currentDate))
|
||||||
const endDate = endOfWeek(endOfMonth(currentDate))
|
const endDate = endOfWeek(endOfMonth(currentDate))
|
||||||
@ -90,7 +187,6 @@ export function OurCalendar({ webdavConfig, calendarFile }: CalendarProps) {
|
|||||||
throw new Error(`Failed to fetch calendar data: ${response.status} ${response.statusText}`)
|
throw new Error(`Failed to fetch calendar data: ${response.status} ${response.statusText}`)
|
||||||
} else {
|
} else {
|
||||||
const data = await response.json()
|
const data = await response.json()
|
||||||
// Ensure all events have the new fields
|
|
||||||
const updatedEvents = data.events.map((event: Event) => ({
|
const updatedEvents = data.events.map((event: Event) => ({
|
||||||
...event,
|
...event,
|
||||||
attendees: event.attendees || [],
|
attendees: event.attendees || [],
|
||||||
@ -112,11 +208,10 @@ export function OurCalendar({ webdavConfig, calendarFile }: CalendarProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const checkForUpdates = async () => {
|
const checkForUpdates = async () => {
|
||||||
if (selectedEvent) return; // Skip update check while editing
|
if (selectedEvent) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const fetchedEvents = await dataManager.fetchEvents();
|
const fetchedEvents = await dataManager.fetchEvents();
|
||||||
// Ensure all events have the new fields
|
|
||||||
const updatedEvents = fetchedEvents.map(event => ({
|
const updatedEvents = fetchedEvents.map(event => ({
|
||||||
...event,
|
...event,
|
||||||
attendees: event.attendees || [],
|
attendees: event.attendees || [],
|
||||||
@ -271,7 +366,7 @@ export function OurCalendar({ webdavConfig, calendarFile }: CalendarProps) {
|
|||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
className={cn(
|
className={cn(
|
||||||
"border rounded-lg p-2 min-h-[100px] dark:border-gray-600",
|
"border rounded-lg p-2 min-h-[100px] dark:border-gray-600",
|
||||||
!isSameMonth(day, currentDate) && "bg-gray-100 dark:bg-gray-700",
|
!isSameMonth(day, currentDate) && "bg-gray-100 dark:bg-gray-700",
|
||||||
isSameMonth(day, currentDate) && "dark:bg-gray-800"
|
isSameMonth(day, currentDate) && "dark:bg-gray-800"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
@ -333,7 +428,7 @@ function EventForm({ onSubmit, initialData, isDarkMode }: { onSubmit: (event: Ev
|
|||||||
const [duration, setDuration] = React.useState(initialData?.duration?.toString() || '60')
|
const [duration, setDuration] = React.useState(initialData?.duration?.toString() || '60')
|
||||||
const [category, setCategory] = React.useState(initialData?.category || '')
|
const [category, setCategory] = React.useState(initialData?.category || '')
|
||||||
const [content, setContent] = React.useState(initialData?.content || '')
|
const [content, setContent] = React.useState(initialData?.content || '')
|
||||||
const [attendees, setAttendees] = React.useState(initialData?.attendees?.join('\n') || '')
|
const [attendees, setAttendees] = React.useState<string[]>(initialData?.attendees || [])
|
||||||
const [isFullDay, setIsFullDay] = React.useState(initialData?.isFullDay || false)
|
const [isFullDay, setIsFullDay] = React.useState(initialData?.isFullDay || false)
|
||||||
|
|
||||||
const handleSubmit = (e: React.FormEvent) => {
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
@ -349,7 +444,7 @@ function EventForm({ onSubmit, initialData, isDarkMode }: { onSubmit: (event: Ev
|
|||||||
category,
|
category,
|
||||||
color,
|
color,
|
||||||
content,
|
content,
|
||||||
attendees: attendees.split('\n').filter(email => email.trim() !== ''),
|
attendees,
|
||||||
isFullDay
|
isFullDay
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -453,14 +548,10 @@ function EventForm({ onSubmit, initialData, isDarkMode }: { onSubmit: (event: Ev
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label htmlFor="attendees" className="dark:text-white">Attendees (one email per line)</Label>
|
<Label htmlFor="attendees" className="dark:text-white">Attendees</Label>
|
||||||
<Textarea
|
<AttendeeSelector
|
||||||
id="attendees"
|
selectedAttendees={attendees}
|
||||||
value={attendees}
|
onAttendeeChange={setAttendees}
|
||||||
onChange={(e) => setAttendees(e.target.value)}
|
|
||||||
rows={3}
|
|
||||||
className="dark:bg-gray-700 dark:text-white dark:border-gray-600"
|
|
||||||
placeholder="john@example.com jane@example.com"
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
@ -509,11 +600,11 @@ function EventDetailsDialog({ event, onClose, onEdit, onDelete, isDarkMode }: {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
<p><strong>Category:</strong> <span className={`px-2 py-1 rounded ${event.color} text-white`}>{event.category}</span></p>
|
<p><strong>Category:</strong> <span className={`px-2 py-1 rounded ${event.color} text-white`}>{event.category}</span></p>
|
||||||
{(event.attendees || []).length > 0 && (
|
{event.attendees.length > 0 && (
|
||||||
<div>
|
<div>
|
||||||
<strong>Attendees:</strong>
|
<strong>Attendees:</strong>
|
||||||
<ul className="list-disc list-inside">
|
<ul className="list-disc list-inside">
|
||||||
{(event.attendees || []).map((email, index) => (
|
{event.attendees.map((email, index) => (
|
||||||
<li key={index}>{email}</li>
|
<li key={index}>{email}</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -37,13 +37,6 @@ export const defaultMembers: Member[] = [
|
|||||||
email: "mike.w@example.com",
|
email: "mike.w@example.com",
|
||||||
role: "stakeholder",
|
role: "stakeholder",
|
||||||
description: "Key stakeholder representing finance department"
|
description: "Key stakeholder representing finance department"
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "4",
|
|
||||||
name: "Lisa Chen",
|
|
||||||
email: "lisa.chen@example.com",
|
|
||||||
role: "contributor",
|
|
||||||
description: "Technical contributor and documentation specialist"
|
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user