- 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>
253 lines
6.4 KiB
TypeScript
253 lines
6.4 KiB
TypeScript
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();
|
|
}; |