feat: Enhance calendar component with glassmorphism design and modular architecture

- Refactored Calendar.tsx into modular component structure
- Added glassmorphism theming with CSS custom properties
- Implemented reusable calendar subcomponents:
  - CalendarGrid: Month view with improved day/event display
  - CalendarHeader: Navigation and view controls
  - EventList: List view for events
  - TrendingEvents: Location-based trending events
  - UpcomingEvents: Quick upcoming events preview
- Enhanced responsive design for mobile devices
- Added Playwright testing framework for automated testing
- Updated Docker development commands in CLAUDE.md
- Improved accessibility and user experience

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-07-13 12:07:33 -06:00
parent f4f929912d
commit 0956873381
19 changed files with 1516 additions and 489 deletions

View File

@@ -0,0 +1,253 @@
import { Event, CalendarDay } from './types';
/**
* Get the month name for display
*/
export const getMonthName = (date: Date, short: boolean = false): string => {
const monthNames = [
'January', 'February', 'March', 'April', 'May', 'June',
'July', 'August', 'September', 'October', 'November', 'December'
];
if (short) {
return monthNames[date.getMonth()].slice(0, 3);
}
return monthNames[date.getMonth()];
};
/**
* Get day names for calendar header
*/
export const getDayNames = (short: boolean = false): string[] => {
if (short) {
return ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
}
return ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
};
/**
* Check if two dates are on the same day
*/
export const isSameDay = (date1: Date, date2: Date): boolean => {
return date1.toDateString() === date2.toDateString();
};
/**
* Check if a date is today
*/
export const isToday = (date: Date): boolean => {
return isSameDay(date, new Date());
};
/**
* Generate calendar days grid for a given month
*/
export const generateCalendarDays = (currentDate: Date, events: Event[]): CalendarDay[] => {
const year = currentDate.getFullYear();
const month = currentDate.getMonth();
// Get first day of month and number of days
const firstDay = new Date(year, month, 1);
const lastDay = new Date(year, month + 1, 0);
const daysInMonth = lastDay.getDate();
const startingDayOfWeek = firstDay.getDay();
// Calculate previous month days to show
const prevMonth = new Date(year, month - 1, 0);
const daysInPrevMonth = prevMonth.getDate();
const calendarDays: CalendarDay[] = [];
// Calculate total cells needed
const totalCells = Math.ceil((daysInMonth + startingDayOfWeek) / 7) * 7;
for (let i = 0; i < totalCells; i++) {
let dayNumber: number;
let isCurrentMonth: boolean;
let currentDayDate: Date;
if (i < startingDayOfWeek) {
// Previous month days
dayNumber = daysInPrevMonth - (startingDayOfWeek - i - 1);
isCurrentMonth = false;
currentDayDate = new Date(year, month - 1, dayNumber);
} else if (i >= startingDayOfWeek + daysInMonth) {
// Next month days
dayNumber = i - startingDayOfWeek - daysInMonth + 1;
isCurrentMonth = false;
currentDayDate = new Date(year, month + 1, dayNumber);
} else {
// Current month days
dayNumber = i - startingDayOfWeek + 1;
isCurrentMonth = true;
currentDayDate = new Date(year, month, dayNumber);
}
// Get events for this day
const dayEvents = events.filter(event => {
const eventDate = new Date(event.start_time);
return isSameDay(eventDate, currentDayDate);
});
calendarDays.push({
day: dayNumber,
isCurrentMonth,
isToday: isToday(currentDayDate),
date: currentDayDate,
events: dayEvents
});
}
return calendarDays;
};
/**
* Get events for a specific day
*/
export const getEventsForDay = (events: Event[], date: Date): Event[] => {
return events.filter(event => {
const eventDate = new Date(event.start_time);
return isSameDay(eventDate, date);
});
};
/**
* Format date for display
*/
export const formatDate = (date: Date, options?: Intl.DateTimeFormatOptions): string => {
const defaultOptions: Intl.DateTimeFormatOptions = {
year: 'numeric',
month: 'long',
day: 'numeric'
};
return new Intl.DateTimeFormat('en-US', { ...defaultOptions, ...options }).format(date);
};
/**
* Format time for display
*/
export const formatTime = (date: Date): string => {
return new Intl.DateTimeFormat('en-US', {
hour: 'numeric',
minute: '2-digit',
hour12: true
}).format(date);
};
/**
* Format date and time for display
*/
export const formatDateTime = (dateString: string) => {
const date = new Date(dateString);
return {
date: formatDate(date),
time: formatTime(date),
dayOfWeek: date.toLocaleDateString('en-US', { weekday: 'long' })
};
};
/**
* Get category color classes
*/
export const getCategoryColor = (category?: string): string => {
const colors = {
music: 'from-purple-500 to-pink-500',
arts: 'from-blue-500 to-cyan-500',
community: 'from-green-500 to-emerald-500',
business: 'from-gray-500 to-slate-500',
food: 'from-orange-500 to-red-500',
sports: 'from-indigo-500 to-purple-500',
default: 'from-gray-400 to-gray-500'
};
return colors[category as keyof typeof colors] || colors.default;
};
/**
* Get category icon
*/
export const getCategoryIcon = (category?: string): string => {
const icons = {
music: '🎵',
arts: '🎨',
community: '🤝',
business: '💼',
food: '🍷',
sports: '⚽',
default: '📅'
};
return icons[category as keyof typeof icons] || icons.default;
};
/**
* Group events by date
*/
export const groupEventsByDate = (events: Event[]): Record<string, Event[]> => {
const grouped: Record<string, Event[]> = {};
events.forEach(event => {
const dateKey = new Date(event.start_time).toDateString();
if (!grouped[dateKey]) {
grouped[dateKey] = [];
}
grouped[dateKey].push(event);
});
return grouped;
};
/**
* Sort events by start time
*/
export const sortEventsByDate = (events: Event[]): Event[] => {
return events.sort((a, b) =>
new Date(a.start_time).getTime() - new Date(b.start_time).getTime()
);
};
/**
* Filter future events
*/
export const getFutureEvents = (events: Event[]): Event[] => {
const today = new Date();
return events.filter(event => new Date(event.start_time) >= today);
};
/**
* Get relative date text (Today, Tomorrow, etc.)
*/
export const getRelativeDateText = (date: Date): string => {
const today = new Date();
const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
if (isSameDay(date, today)) {
return 'Today';
} else if (isSameDay(date, tomorrow)) {
return 'Tomorrow';
} else {
return formatDate(date, {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
};
/**
* Truncate text with ellipsis
*/
export const truncateText = (text: string, maxLength: number): string => {
if (text.length <= maxLength) return text;
return text.substring(0, maxLength) + '...';
};
/**
* Get CSS variable value
*/
export const getCSSVariable = (variableName: string): string => {
if (typeof window === 'undefined') return '';
return getComputedStyle(document.documentElement).getPropertyValue(variableName).trim();
};