implement calendar event display with day indicators and overlay
- Add ExpandedEvent type to shared package for recurring event instances - Implement EventController and EventService with full CRUD operations - Server-side recurring event expansion via recurrenceExpander - Calendar grid shows orange dot indicator for days with events - Tap on day opens modal overlay with EventCards - EventCard component with Feather icons (calendar, clock, repeat, edit, trash) - EventsStore with Zustand for client-side event state management - Load events for visible grid range including adjacent month days - Add textPrimary, borderPrimary, eventIndicator to theme - Update test responses for multiple events on Saturdays
This commit is contained in:
@@ -1,23 +1,154 @@
|
||||
import { View, Text, Pressable } from "react-native";
|
||||
import { CalendarEvent } from "@caldav/shared";
|
||||
import { ExpandedEvent } from "@caldav/shared";
|
||||
import { Feather } from "@expo/vector-icons";
|
||||
import currentTheme from "../Themes";
|
||||
|
||||
type EventCardProps = {
|
||||
event: CalendarEvent;
|
||||
onPress?: (event: CalendarEvent) => void;
|
||||
event: ExpandedEvent;
|
||||
onEdit: () => void;
|
||||
onDelete: () => void;
|
||||
};
|
||||
|
||||
const EventCard = ({ event: _event, onPress: _onPress }: EventCardProps) => {
|
||||
// TODO: Display event title, time, and description preview
|
||||
// TODO: Handle onPress to navigate to EventDetailScreen
|
||||
// TODO: Style based on event type or time of day
|
||||
throw new Error("Not implemented");
|
||||
function formatDate(date: Date): string {
|
||||
const d = new Date(date);
|
||||
return d.toLocaleDateString("de-DE", {
|
||||
weekday: "short",
|
||||
day: "2-digit",
|
||||
month: "2-digit",
|
||||
year: "numeric",
|
||||
});
|
||||
}
|
||||
|
||||
function formatTime(date: Date): string {
|
||||
const d = new Date(date);
|
||||
return d.toLocaleTimeString("de-DE", {
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function formatDuration(start: Date, end: Date): string {
|
||||
const startDate = new Date(start);
|
||||
const endDate = new Date(end);
|
||||
const diffMs = endDate.getTime() - startDate.getTime();
|
||||
const diffMins = Math.round(diffMs / 60000);
|
||||
|
||||
if (diffMins < 60) {
|
||||
return `${diffMins} min`;
|
||||
}
|
||||
|
||||
const hours = Math.floor(diffMins / 60);
|
||||
const mins = diffMins % 60;
|
||||
|
||||
if (mins === 0) {
|
||||
return hours === 1 ? "1 Std" : `${hours} Std`;
|
||||
}
|
||||
|
||||
return `${hours} Std ${mins} min`;
|
||||
}
|
||||
|
||||
export const EventCard = ({ event, onEdit, onDelete }: EventCardProps) => {
|
||||
return (
|
||||
<Pressable>
|
||||
<View>
|
||||
<Text>EventCard - Not Implemented</Text>
|
||||
<View
|
||||
className="rounded-xl overflow-hidden mb-3"
|
||||
style={{ borderWidth: 2, borderColor: currentTheme.borderPrimary }}
|
||||
>
|
||||
{/* Header with title */}
|
||||
<View
|
||||
className="px-3 py-2"
|
||||
style={{
|
||||
backgroundColor: currentTheme.chatBot,
|
||||
borderBottomWidth: 2,
|
||||
borderBottomColor: currentTheme.borderPrimary,
|
||||
}}
|
||||
>
|
||||
<Text className="font-bold text-base">{event.title}</Text>
|
||||
</View>
|
||||
</Pressable>
|
||||
|
||||
{/* Content */}
|
||||
<View className="px-3 py-2 bg-white">
|
||||
{/* Date */}
|
||||
<View className="flex-row items-center mb-1">
|
||||
<Feather
|
||||
name="calendar"
|
||||
size={16}
|
||||
color={currentTheme.textPrimary}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text style={{ color: currentTheme.textPrimary }}>
|
||||
{formatDate(event.occurrenceStart)}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Time with duration */}
|
||||
<View className="flex-row items-center mb-1">
|
||||
<Feather
|
||||
name="clock"
|
||||
size={16}
|
||||
color={currentTheme.textPrimary}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text style={{ color: currentTheme.textPrimary }}>
|
||||
{formatTime(event.occurrenceStart)} -{" "}
|
||||
{formatTime(event.occurrenceEnd)} (
|
||||
{formatDuration(event.occurrenceStart, event.occurrenceEnd)})
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Recurring indicator */}
|
||||
{event.isRecurring && (
|
||||
<View className="flex-row items-center mb-1">
|
||||
<Feather
|
||||
name="repeat"
|
||||
size={16}
|
||||
color={currentTheme.textPrimary}
|
||||
style={{ marginRight: 8 }}
|
||||
/>
|
||||
<Text style={{ color: currentTheme.textPrimary }}>
|
||||
Wiederkehrend
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Description */}
|
||||
{event.description && (
|
||||
<Text
|
||||
style={{ color: currentTheme.textPrimary }}
|
||||
className="text-sm mt-1"
|
||||
>
|
||||
{event.description}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<View className="flex-row justify-end mt-3 gap-3">
|
||||
<Pressable
|
||||
onPress={onEdit}
|
||||
className="w-10 h-10 rounded-full items-center justify-center"
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: currentTheme.borderPrimary,
|
||||
}}
|
||||
>
|
||||
<Feather name="edit-2" size={18} color={currentTheme.textPrimary} />
|
||||
</Pressable>
|
||||
<Pressable
|
||||
onPress={onDelete}
|
||||
className="w-10 h-10 rounded-full items-center justify-center"
|
||||
style={{
|
||||
borderWidth: 1,
|
||||
borderColor: currentTheme.borderPrimary,
|
||||
}}
|
||||
>
|
||||
<Feather
|
||||
name="trash-2"
|
||||
size={18}
|
||||
color={currentTheme.textPrimary}
|
||||
/>
|
||||
</Pressable>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user