Cleanup test files and move to another folder

This commit is contained in:
Janus C. H. Knudsen 2025-11-05 00:07:19 +01:00
parent 8456d8aa28
commit 9c765b35ab
28 changed files with 0 additions and 1981 deletions

View file

@ -0,0 +1,189 @@
// js/core/CalendarConfig.js
import { eventBus } from './EventBus.js';
import { EventTypes } from '../types/EventTypes.js';
/**
* Calendar configuration management
*/
export class CalendarConfig {
constructor() {
this.config = {
// View settings
view: 'week', // 'day' | 'week' | 'month'
weekDays: 7, // 4-7 days for week view
firstDayOfWeek: 1, // 0 = Sunday, 1 = Monday
// Time settings
dayStartHour: 7, // Calendar starts at 7 AM
dayEndHour: 19, // Calendar ends at 7 PM
workStartHour: 8, // Work hours start
workEndHour: 17, // Work hours end
snapInterval: 15, // Minutes: 5, 10, 15, 30, 60
// Display settings
hourHeight: 60, // Pixels per hour
showCurrentTime: true,
showWorkHours: true,
// Interaction settings
allowDrag: true,
allowResize: true,
allowCreate: true,
// API settings
apiEndpoint: '/api/events',
dateFormat: 'YYYY-MM-DD',
timeFormat: 'HH:mm',
// Feature flags
enableSearch: true,
enableTouch: true,
// Event defaults
defaultEventDuration: 60, // Minutes
minEventDuration: null, // Will be same as snapInterval
maxEventDuration: 480 // 8 hours
};
// Set computed values
this.config.minEventDuration = this.config.snapInterval;
// Load from data attributes
this.loadFromDOM();
}
/**
* Load configuration from DOM data attributes
*/
loadFromDOM() {
const calendar = document.querySelector('swp-calendar');
if (!calendar) return;
// Read data attributes
const attrs = calendar.dataset;
if (attrs.view) this.config.view = attrs.view;
if (attrs.weekDays) this.config.weekDays = parseInt(attrs.weekDays);
if (attrs.snapInterval) this.config.snapInterval = parseInt(attrs.snapInterval);
if (attrs.dayStartHour) this.config.dayStartHour = parseInt(attrs.dayStartHour);
if (attrs.dayEndHour) this.config.dayEndHour = parseInt(attrs.dayEndHour);
if (attrs.hourHeight) this.config.hourHeight = parseInt(attrs.hourHeight);
}
/**
* Get a config value
* @param {string} key
* @returns {*}
*/
get(key) {
return this.config[key];
}
/**
* Set a config value
* @param {string} key
* @param {*} value
*/
set(key, value) {
const oldValue = this.config[key];
this.config[key] = value;
// Update computed values
if (key === 'snapInterval') {
this.config.minEventDuration = value;
}
// Emit config update event
eventBus.emit(EventTypes.CONFIG_UPDATE, {
key,
value,
oldValue
});
}
/**
* Update multiple config values
* @param {Object} updates
*/
update(updates) {
Object.entries(updates).forEach(([key, value]) => {
this.set(key, value);
});
}
/**
* Get all config
* @returns {Object}
*/
getAll() {
return { ...this.config };
}
/**
* Calculate derived values
*/
get minuteHeight() {
return this.config.hourHeight / 60;
}
get totalHours() {
return this.config.dayEndHour - this.config.dayStartHour;
}
get totalMinutes() {
return this.totalHours * 60;
}
get slotsPerHour() {
return 60 / this.config.snapInterval;
}
get totalSlots() {
return this.totalHours * this.slotsPerHour;
}
get slotHeight() {
return this.config.hourHeight / this.slotsPerHour;
}
/**
* Validate snap interval
* @param {number} interval
* @returns {boolean}
*/
isValidSnapInterval(interval) {
return [5, 10, 15, 30, 60].includes(interval);
}
/**
* Get view-specific settings
* @param {string} view
* @returns {Object}
*/
getViewSettings(view = this.config.view) {
const settings = {
day: {
columns: 1,
showAllDay: true,
scrollToHour: 8
},
week: {
columns: this.config.weekDays,
showAllDay: true,
scrollToHour: 8
},
month: {
columns: 7,
showAllDay: false,
scrollToHour: null
}
};
return settings[view] || settings.week;
}
}
// Create singleton instance
export const calendarConfig = new CalendarConfig();

View file

@ -0,0 +1,385 @@
// js/managers/DataManager.js
import { eventBus } from '../core/EventBus.js';
import { EventTypes } from '../types/EventTypes.js';
/**
* Manages data fetching and API communication
* Currently uses mock data until backend is implemented
*/
export class DataManager {
constructor() {
this.baseUrl = '/api/events';
this.useMockData = true; // Toggle this when backend is ready
this.cache = new Map();
this.init();
}
init() {
this.subscribeToEvents();
}
subscribeToEvents() {
// Listen for period changes to fetch new data
eventBus.on(EventTypes.PERIOD_CHANGE, (e) => {
this.fetchEventsForPeriod(e.detail);
});
// Listen for event updates
eventBus.on(EventTypes.EVENT_UPDATE, (e) => {
this.updateEvent(e.detail);
});
// Listen for event creation
eventBus.on(EventTypes.EVENT_CREATE, (e) => {
this.createEvent(e.detail);
});
// Listen for event deletion
eventBus.on(EventTypes.EVENT_DELETE, (e) => {
this.deleteEvent(e.detail.eventId);
});
}
/**
* Fetch events for a specific period
* @param {Object} period - Contains start, end, view
*/
async fetchEventsForPeriod(period) {
const cacheKey = `${period.start}-${period.end}-${period.view}`;
// Check cache first
if (this.cache.has(cacheKey)) {
const cachedData = this.cache.get(cacheKey);
eventBus.emit(EventTypes.DATA_FETCH_SUCCESS, cachedData);
return cachedData;
}
// Emit loading start
eventBus.emit(EventTypes.DATA_FETCH_START, { period });
try {
let data;
if (this.useMockData) {
// Simulate network delay
await this.delay(300);
data = this.getMockData(period);
} else {
// Real API call
const params = new URLSearchParams({
start: period.start,
end: period.end,
view: period.view
});
const response = await fetch(`${this.baseUrl}?${params}`);
if (!response.ok) throw new Error('Failed to fetch events');
data = await response.json();
}
// Cache the data
this.cache.set(cacheKey, data);
// Emit success
eventBus.emit(EventTypes.DATA_FETCH_SUCCESS, data);
return data;
} catch (error) {
eventBus.emit(EventTypes.DATA_FETCH_ERROR, { error: error.message });
throw error;
}
}
/**
* Create a new event
*/
async createEvent(eventData) {
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'create' });
try {
if (this.useMockData) {
await this.delay(200);
const newEvent = {
id: `evt-${Date.now()}`,
...eventData,
syncStatus: 'synced'
};
// Clear cache to force refresh
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'create',
event: newEvent
});
return newEvent;
} else {
// Real API call
const response = await fetch(this.baseUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(eventData)
});
if (!response.ok) throw new Error('Failed to create event');
const newEvent = await response.json();
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'create',
event: newEvent
});
return newEvent;
}
} catch (error) {
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
action: 'create',
error: error.message
});
throw error;
}
}
/**
* Update an existing event
*/
async updateEvent(updateData) {
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'update' });
try {
if (this.useMockData) {
await this.delay(200);
// Clear cache to force refresh
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'update',
eventId: updateData.eventId,
changes: updateData.changes
});
return true;
} else {
// Real API call
const response = await fetch(`${this.baseUrl}/${updateData.eventId}`, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(updateData.changes)
});
if (!response.ok) throw new Error('Failed to update event');
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'update',
eventId: updateData.eventId
});
return true;
}
} catch (error) {
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
action: 'update',
error: error.message,
eventId: updateData.eventId
});
throw error;
}
}
/**
* Delete an event
*/
async deleteEvent(eventId) {
eventBus.emit(EventTypes.DATA_SYNC_START, { action: 'delete' });
try {
if (this.useMockData) {
await this.delay(200);
// Clear cache to force refresh
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'delete',
eventId
});
return true;
} else {
// Real API call
const response = await fetch(`${this.baseUrl}/${eventId}`, {
method: 'DELETE'
});
if (!response.ok) throw new Error('Failed to delete event');
this.cache.clear();
eventBus.emit(EventTypes.DATA_SYNC_SUCCESS, {
action: 'delete',
eventId
});
return true;
}
} catch (error) {
eventBus.emit(EventTypes.DATA_SYNC_ERROR, {
action: 'delete',
error: error.message,
eventId
});
throw error;
}
}
/**
* Generate mock data for testing
*/
getMockData(period) {
const events = [];
const types = ['meeting', 'meal', 'work', 'milestone'];
const titles = {
meeting: ['Team Standup', 'Client Meeting', 'Project Review', 'Sprint Planning', 'Design Review'],
meal: ['Breakfast', 'Lunch', 'Coffee Break', 'Dinner'],
work: ['Deep Work Session', 'Code Review', 'Documentation', 'Testing'],
milestone: ['Project Deadline', 'Release Day', 'Demo Day']
};
// Parse dates
const startDate = new Date(period.start);
const endDate = new Date(period.end);
// Generate some events for each day
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
// Skip weekends for most events
const dayOfWeek = d.getDay();
const isWeekend = dayOfWeek === 0 || dayOfWeek === 6;
if (isWeekend) {
// Maybe one or two events on weekends
if (Math.random() > 0.7) {
const type = 'meal';
const title = titles[type][Math.floor(Math.random() * titles[type].length)];
const hour = 12 + Math.floor(Math.random() * 4);
events.push({
id: `evt-${events.length + 1}`,
title,
type,
start: `${this.formatDate(d)}T${hour}:00:00`,
end: `${this.formatDate(d)}T${hour + 1}:00:00`,
allDay: false,
syncStatus: 'synced'
});
}
} else {
// Regular workday events
// Morning standup
if (Math.random() > 0.3) {
events.push({
id: `evt-${events.length + 1}`,
title: 'Team Standup',
type: 'meeting',
start: `${this.formatDate(d)}T09:00:00`,
end: `${this.formatDate(d)}T09:30:00`,
allDay: false,
syncStatus: 'synced'
});
}
// Lunch
events.push({
id: `evt-${events.length + 1}`,
title: 'Lunch',
type: 'meal',
start: `${this.formatDate(d)}T12:00:00`,
end: `${this.formatDate(d)}T13:00:00`,
allDay: false,
syncStatus: 'synced'
});
// Random afternoon events
const numAfternoonEvents = Math.floor(Math.random() * 3) + 1;
for (let i = 0; i < numAfternoonEvents; i++) {
const type = types[Math.floor(Math.random() * types.length)];
const title = titles[type][Math.floor(Math.random() * titles[type].length)];
const startHour = 13 + Math.floor(Math.random() * 4);
const duration = 1 + Math.floor(Math.random() * 2);
events.push({
id: `evt-${events.length + 1}`,
title,
type,
start: `${this.formatDate(d)}T${startHour}:${Math.random() > 0.5 ? '00' : '30'}:00`,
end: `${this.formatDate(d)}T${startHour + duration}:00:00`,
allDay: false,
syncStatus: Math.random() > 0.9 ? 'pending' : 'synced'
});
}
}
}
// Add a multi-day event
if (period.view === 'week') {
const midWeek = new Date(startDate);
midWeek.setDate(midWeek.getDate() + 2);
events.push({
id: `evt-${events.length + 1}`,
title: 'Project Sprint',
type: 'milestone',
start: `${this.formatDate(startDate)}T00:00:00`,
end: `${this.formatDate(midWeek)}T23:59:59`,
allDay: true,
syncStatus: 'synced'
});
}
return {
events,
meta: {
start: period.start,
end: period.end,
view: period.view,
total: events.length
}
};
}
/**
* Utility methods
*/
formatDate(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Clear all cached data
*/
clearCache() {
this.cache.clear();
}
/**
* Toggle between mock and real data
*/
setUseMockData(useMock) {
this.useMockData = useMock;
this.clearCache();
}
}

View file

@ -0,0 +1,231 @@
// js/utils/DateUtils.js
/**
* Date and time utility functions
*/
export class DateUtils {
/**
* Get start of week for a given date
* @param {Date} date
* @param {number} firstDayOfWeek - 0 = Sunday, 1 = Monday
* @returns {Date}
*/
static getWeekStart(date, firstDayOfWeek = 1) {
const d = new Date(date);
const day = d.getDay();
const diff = (day - firstDayOfWeek + 7) % 7;
d.setDate(d.getDate() - diff);
d.setHours(0, 0, 0, 0);
return d;
}
/**
* Get end of week for a given date
* @param {Date} date
* @param {number} firstDayOfWeek
* @returns {Date}
*/
static getWeekEnd(date, firstDayOfWeek = 1) {
const start = this.getWeekStart(date, firstDayOfWeek);
const end = new Date(start);
end.setDate(end.getDate() + 6);
end.setHours(23, 59, 59, 999);
return end;
}
/**
* Format date to YYYY-MM-DD
* @param {Date} date
* @returns {string}
*/
static formatDate(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
/**
* Format time to HH:MM
* @param {Date} date
* @returns {string}
*/
static formatTime(date) {
return `${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
}
/**
* Format time to 12-hour format
* @param {Date} date
* @returns {string}
*/
static formatTime12(date) {
const hours = date.getHours();
const minutes = date.getMinutes();
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
return `${displayHours}:${String(minutes).padStart(2, '0')} ${period}`;
}
/**
* Convert minutes since midnight to time string
* @param {number} minutes
* @returns {string}
*/
static minutesToTime(minutes) {
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
const period = hours >= 12 ? 'PM' : 'AM';
const displayHours = hours % 12 || 12;
return `${displayHours}:${String(mins).padStart(2, '0')} ${period}`;
}
/**
* Convert time string to minutes since midnight
* @param {string} timeStr - Format: "HH:MM" or "HH:MM:SS"
* @returns {number}
*/
static timeToMinutes(timeStr) {
const [time] = timeStr.split('T').pop().split('.');
const [hours, minutes] = time.split(':').map(Number);
return hours * 60 + minutes;
}
/**
* Get minutes since start of day
* @param {Date|string} date
* @returns {number}
*/
static getMinutesSinceMidnight(date) {
const d = typeof date === 'string' ? new Date(date) : date;
return d.getHours() * 60 + d.getMinutes();
}
/**
* Calculate duration in minutes between two dates
* @param {Date|string} start
* @param {Date|string} end
* @returns {number}
*/
static getDurationMinutes(start, end) {
const startDate = typeof start === 'string' ? new Date(start) : start;
const endDate = typeof end === 'string' ? new Date(end) : end;
return Math.floor((endDate - startDate) / 60000);
}
/**
* Check if date is today
* @param {Date} date
* @returns {boolean}
*/
static isToday(date) {
const today = new Date();
return date.toDateString() === today.toDateString();
}
/**
* Check if two dates are on the same day
* @param {Date} date1
* @param {Date} date2
* @returns {boolean}
*/
static isSameDay(date1, date2) {
return date1.toDateString() === date2.toDateString();
}
/**
* Check if event spans multiple days
* @param {Date|string} start
* @param {Date|string} end
* @returns {boolean}
*/
static isMultiDay(start, end) {
const startDate = typeof start === 'string' ? new Date(start) : start;
const endDate = typeof end === 'string' ? new Date(end) : end;
return !this.isSameDay(startDate, endDate);
}
/**
* Get day name
* @param {Date} date
* @param {string} format - 'short' or 'long'
* @returns {string}
*/
static getDayName(date, format = 'short') {
const days = {
short: ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'],
long: ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday']
};
return days[format][date.getDay()];
}
/**
* Add days to date
* @param {Date} date
* @param {number} days
* @returns {Date}
*/
static addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
/**
* Add minutes to date
* @param {Date} date
* @param {number} minutes
* @returns {Date}
*/
static addMinutes(date, minutes) {
const result = new Date(date);
result.setMinutes(result.getMinutes() + minutes);
return result;
}
/**
* Snap time to nearest interval
* @param {Date} date
* @param {number} intervalMinutes
* @returns {Date}
*/
static snapToInterval(date, intervalMinutes) {
const minutes = date.getMinutes();
const snappedMinutes = Math.round(minutes / intervalMinutes) * intervalMinutes;
const result = new Date(date);
result.setMinutes(snappedMinutes);
result.setSeconds(0);
result.setMilliseconds(0);
return result;
}
/**
* Get current time in minutes since day start
* @param {number} dayStartHour
* @returns {number}
*/
static getCurrentTimeMinutes(dayStartHour = 0) {
const now = new Date();
const minutesSinceMidnight = now.getHours() * 60 + now.getMinutes();
return minutesSinceMidnight - (dayStartHour * 60);
}
/**
* Format duration to human readable string
* @param {number} minutes
* @returns {string}
*/
static formatDuration(minutes) {
if (minutes < 60) {
return `${minutes} min`;
}
const hours = Math.floor(minutes / 60);
const mins = minutes % 60;
if (mins === 0) {
return `${hours} hour${hours > 1 ? 's' : ''}`;
}
return `${hours} hour${hours > 1 ? 's' : ''} ${mins} min`;
}
}

View file

@ -0,0 +1,73 @@
// js/types/EventTypes.js
/**
* Calendar event type constants
*/
export const EventTypes = {
// View events
VIEW_CHANGE: 'calendar:viewchange',
VIEW_RENDERED: 'calendar:viewrendered',
PERIOD_CHANGE: 'calendar:periodchange',
// Event CRUD
EVENT_CREATE: 'calendar:eventcreate',
EVENT_UPDATE: 'calendar:eventupdate',
EVENT_DELETE: 'calendar:eventdelete',
EVENT_RENDERED: 'calendar:eventrendered',
EVENT_SELECTED: 'calendar:eventselected',
// Interaction events
DRAG_START: 'calendar:dragstart',
DRAG_MOVE: 'calendar:dragmove',
DRAG_END: 'calendar:dragend',
DRAG_CANCEL: 'calendar:dragcancel',
RESIZE_START: 'calendar:resizestart',
RESIZE_MOVE: 'calendar:resizemove',
RESIZE_END: 'calendar:resizeend',
RESIZE_CANCEL: 'calendar:resizecancel',
// UI events
POPUP_SHOW: 'calendar:popupshow',
POPUP_HIDE: 'calendar:popuphide',
SEARCH_START: 'calendar:searchstart',
SEARCH_UPDATE: 'calendar:searchupdate',
SEARCH_CLEAR: 'calendar:searchclear',
// Grid events
GRID_CLICK: 'calendar:gridclick',
GRID_DBLCLICK: 'calendar:griddblclick',
GRID_RENDERED: 'calendar:gridrendered',
// Data events
DATA_FETCH_START: 'calendar:datafetchstart',
DATA_FETCH_SUCCESS: 'calendar:datafetchsuccess',
DATA_FETCH_ERROR: 'calendar:datafetcherror',
DATA_SYNC_START: 'calendar:datasyncstart',
DATA_SYNC_SUCCESS: 'calendar:datasyncsuccess',
DATA_SYNC_ERROR: 'calendar:datasyncerror',
// State events
STATE_UPDATE: 'calendar:stateupdate',
CONFIG_UPDATE: 'calendar:configupdate',
// Time events
TIME_UPDATE: 'calendar:timeupdate',
// Navigation events
NAV_PREV: 'calendar:navprev',
NAV_NEXT: 'calendar:navnext',
NAV_TODAY: 'calendar:navtoday',
// Loading events
LOADING_START: 'calendar:loadingstart',
LOADING_END: 'calendar:loadingend',
// Error events
ERROR: 'calendar:error',
// Init events
READY: 'calendar:ready',
DESTROY: 'calendar:destroy'
};

View file

@ -0,0 +1,115 @@
// js/core/EventBus.js
/**
* Central event dispatcher for calendar using DOM CustomEvents
* Provides logging and debugging capabilities
*/
export class EventBus {
constructor() {
this.eventLog = [];
this.debug = false; // Set to true for console logging
this.listeners = new Set(); // Track listeners for cleanup
}
/**
* Subscribe to an event via DOM addEventListener
* @param {string} eventType
* @param {Function} handler
* @param {Object} options
* @returns {Function} Unsubscribe function
*/
on(eventType, handler, options = {}) {
document.addEventListener(eventType, handler, options);
// Track for cleanup
this.listeners.add({ eventType, handler, options });
// Return unsubscribe function
return () => this.off(eventType, handler);
}
/**
* Subscribe to an event once
* @param {string} eventType
* @param {Function} handler
*/
once(eventType, handler) {
return this.on(eventType, handler, { once: true });
}
/**
* Unsubscribe from an event
* @param {string} eventType
* @param {Function} handler
*/
off(eventType, handler) {
document.removeEventListener(eventType, handler);
// Remove from tracking
for (const listener of this.listeners) {
if (listener.eventType === eventType && listener.handler === handler) {
this.listeners.delete(listener);
break;
}
}
}
/**
* Emit an event via DOM CustomEvent
* @param {string} eventType
* @param {*} detail
* @returns {boolean} Whether event was cancelled
*/
emit(eventType, detail = {}) {
const event = new CustomEvent(eventType, {
detail,
bubbles: true,
cancelable: true
});
this.eventLog.push({
type: eventType,
detail,
timestamp: Date.now()
});
// Emit on document (only DOM events now)
return !document.dispatchEvent(event);
}
/**
* Get event history
* @param {string} eventType Optional filter by type
* @returns {Array}
*/
getEventLog(eventType = null) {
if (eventType) {
return this.eventLog.filter(e => e.type === eventType);
}
return this.eventLog;
}
/**
* Enable/disable debug mode
* @param {boolean} enabled
*/
setDebug(enabled) {
this.debug = enabled;
}
/**
* Clean up all tracked listeners
*/
destroy() {
for (const listener of this.listeners) {
document.removeEventListener(listener.eventType, listener.handler);
}
this.listeners.clear();
this.eventLog = [];
}
}
// Create singleton instance
export const eventBus = new EventBus();

View file

@ -0,0 +1,334 @@
// js/managers/GridManager.js
import { eventBus } from '../core/EventBus.js';
import { calendarConfig } from '../core/CalendarConfig.js';
import { EventTypes } from '../types/EventTypes.js';
import { DateUtils } from '../utils/DateUtils.js';
/**
* Manages the calendar grid structure
*/
export class GridManager {
constructor() {
this.container = null;
this.timeAxis = null;
this.weekHeader = null;
this.timeGrid = null;
this.dayColumns = null;
this.currentWeek = null;
this.init();
}
init() {
this.findElements();
this.subscribeToEvents();
}
findElements() {
this.container = document.querySelector('swp-calendar-container');
this.timeAxis = document.querySelector('swp-time-axis');
this.weekHeader = document.querySelector('swp-calendar-header');
this.timeGrid = document.querySelector('swp-time-grid');
this.scrollableContent = document.querySelector('swp-scrollable-content');
}
subscribeToEvents() {
// Re-render grid on config changes
eventBus.on(EventTypes.CONFIG_UPDATE, (e) => {
if (['dayStartHour', 'dayEndHour', 'hourHeight', 'view', 'weekDays'].includes(e.detail.key)) {
this.render();
}
});
// Re-render on view change
eventBus.on(EventTypes.VIEW_CHANGE, () => {
this.render();
});
// Re-render on period change
eventBus.on(EventTypes.PERIOD_CHANGE, (e) => {
this.currentWeek = e.detail.week;
this.renderHeaders();
});
// Handle grid clicks
this.setupGridInteractions();
}
/**
* Render the complete grid structure
*/
render() {
this.renderTimeAxis();
this.renderHeaders();
this.renderGrid();
this.renderGridLines();
// Emit grid rendered event
eventBus.emit(EventTypes.GRID_RENDERED);
}
/**
* Render time axis (left side hours)
*/
renderTimeAxis() {
if (!this.timeAxis) return;
const startHour = calendarConfig.get('dayStartHour');
const endHour = calendarConfig.get('dayEndHour');
this.timeAxis.innerHTML = '';
for (let hour = startHour; hour <= endHour; hour++) {
const marker = document.createElement('swp-hour-marker');
marker.textContent = this.formatHour(hour);
marker.dataset.hour = hour;
this.timeAxis.appendChild(marker);
}
}
/**
* Render week headers
*/
renderHeaders() {
if (!this.weekHeader || !this.currentWeek) return;
const view = calendarConfig.get('view');
const weekDays = calendarConfig.get('weekDays');
this.weekHeader.innerHTML = '';
if (view === 'week') {
const dates = this.getWeekDates(this.currentWeek);
const daysToShow = dates.slice(0, weekDays);
daysToShow.forEach((date, index) => {
const header = document.createElement('swp-day-header');
header.innerHTML = `
<swp-day-name>${this.getDayName(date)}</swp-day-name>
<swp-day-date>${date.getDate()}</swp-day-date>
`;
header.dataset.date = this.formatDate(date);
header.dataset.dayIndex = index;
// Mark today
if (this.isToday(date)) {
header.dataset.today = 'true';
}
this.weekHeader.appendChild(header);
});
}
}
/**
* Render the main grid structure
*/
renderGrid() {
if (!this.timeGrid) return;
// Clear existing columns
let dayColumns = this.timeGrid.querySelector('swp-day-columns');
if (!dayColumns) {
dayColumns = document.createElement('swp-day-columns');
this.timeGrid.appendChild(dayColumns);
}
dayColumns.innerHTML = '';
const view = calendarConfig.get('view');
const columnsCount = view === 'week' ? calendarConfig.get('weekDays') : 1;
// Create columns
for (let i = 0; i < columnsCount; i++) {
const column = document.createElement('swp-day-column');
column.dataset.columnIndex = i;
if (this.currentWeek) {
const dates = this.getWeekDates(this.currentWeek);
if (dates[i]) {
column.dataset.date = this.formatDate(dates[i]);
}
}
// Add events container
const eventsLayer = document.createElement('swp-events-layer');
column.appendChild(eventsLayer);
dayColumns.appendChild(column);
}
this.dayColumns = dayColumns;
this.updateGridStyles();
}
/**
* Render grid lines
*/
renderGridLines() {
let gridLines = this.timeGrid.querySelector('swp-grid-lines');
if (!gridLines) {
gridLines = document.createElement('swp-grid-lines');
this.timeGrid.insertBefore(gridLines, this.timeGrid.firstChild);
}
const totalHours = calendarConfig.totalHours;
const hourHeight = calendarConfig.get('hourHeight');
// Set CSS variables
this.timeGrid.style.setProperty('--total-hours', totalHours);
this.timeGrid.style.setProperty('--hour-height', `${hourHeight}px`);
// Grid lines are handled by CSS
}
/**
* Update grid CSS variables
*/
updateGridStyles() {
const root = document.documentElement;
const config = calendarConfig.getAll();
// Set CSS variables
root.style.setProperty('--hour-height', `${config.hourHeight}px`);
root.style.setProperty('--minute-height', `${config.hourHeight / 60}px`);
root.style.setProperty('--snap-interval', config.snapInterval);
root.style.setProperty('--day-start-hour', config.dayStartHour);
root.style.setProperty('--day-end-hour', config.dayEndHour);
root.style.setProperty('--work-start-hour', config.workStartHour);
root.style.setProperty('--work-end-hour', config.workEndHour);
// Set grid height
const totalHeight = calendarConfig.totalHours * config.hourHeight;
if (this.timeGrid) {
this.timeGrid.style.height = `${totalHeight}px`;
}
}
/**
* Setup grid interaction handlers
*/
setupGridInteractions() {
if (!this.timeGrid) return;
// Click handler
this.timeGrid.addEventListener('click', (e) => {
// Ignore if clicking on an event
if (e.target.closest('swp-event')) return;
const column = e.target.closest('swp-day-column');
if (!column) return;
const position = this.getClickPosition(e, column);
eventBus.emit(EventTypes.GRID_CLICK, {
date: column.dataset.date,
time: position.time,
minutes: position.minutes,
columnIndex: parseInt(column.dataset.columnIndex)
});
});
// Double click handler
this.timeGrid.addEventListener('dblclick', (e) => {
// Ignore if clicking on an event
if (e.target.closest('swp-event')) return;
const column = e.target.closest('swp-day-column');
if (!column) return;
const position = this.getClickPosition(e, column);
eventBus.emit(EventTypes.GRID_DBLCLICK, {
date: column.dataset.date,
time: position.time,
minutes: position.minutes,
columnIndex: parseInt(column.dataset.columnIndex)
});
});
}
/**
* Get click position in grid
*/
getClickPosition(event, column) {
const rect = column.getBoundingClientRect();
const y = event.clientY - rect.top + this.scrollableContent.scrollTop;
const minuteHeight = calendarConfig.minuteHeight;
const snapInterval = calendarConfig.get('snapInterval');
const dayStartHour = calendarConfig.get('dayStartHour');
// Calculate minutes from start of day
let minutes = Math.floor(y / minuteHeight);
// Snap to interval
minutes = Math.round(minutes / snapInterval) * snapInterval;
// Add day start offset
const totalMinutes = (dayStartHour * 60) + minutes;
return {
minutes: totalMinutes,
time: this.minutesToTime(totalMinutes),
y: minutes * minuteHeight
};
}
/**
* Utility methods
*/
formatHour(hour) {
const period = hour >= 12 ? 'PM' : 'AM';
const displayHour = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);
return `${displayHour} ${period}`;
}
formatDate(date) {
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
}
getDayName(date) {
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
return days[date.getDay()];
}
getWeekDates(weekStart) {
const dates = [];
for (let i = 0; i < 7; i++) {
const date = new Date(weekStart);
date.setDate(weekStart.getDate() + i);
dates.push(date);
}
return dates;
}
isToday(date) {
const today = new Date();
return date.toDateString() === today.toDateString();
}
minutesToTime(totalMinutes) {
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
const period = hours >= 12 ? 'PM' : 'AM';
const displayHour = hours > 12 ? hours - 12 : (hours === 0 ? 12 : hours);
return `${displayHour}:${minutes.toString().padStart(2, '0')} ${period}`;
}
/**
* Scroll to specific hour
*/
scrollToHour(hour) {
if (!this.scrollableContent) return;
const hourHeight = calendarConfig.get('hourHeight');
const dayStartHour = calendarConfig.get('dayStartHour');
const scrollTop = (hour - dayStartHour) * hourHeight;
this.scrollableContent.scrollTop = scrollTop;
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,536 @@
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar Plantempus - Month View</title>
<style>
/* Use existing Calendar Plantempus variables and styling */
:root {
--hour-height: 60px;
--minute-height: 1px;
--snap-interval: 15;
--day-column-min-width: 140px;
--week-days: 7;
--header-height: 80px;
--color-primary: #2196f3;
--color-secondary: #ff9800;
--color-success: #4caf50;
--color-warning: #ff5722;
--color-error: #f44336;
--color-text: #2c3e50;
--color-text-secondary: #6c757d;
--color-border: #e0e0e0;
--color-background: #ffffff;
--color-surface: #f8f9fa;
--color-hover: #f0f0f0;
--transition-fast: 0.15s ease;
--border-radius: 4px;
--box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--color-surface);
color: var(--color-text);
line-height: 1.5;
}
/* Month grid container - matches existing swp-calendar-container */
.month-container {
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
overflow: hidden;
margin: 20px;
}
/* Month grid layout with week numbers */
.month-grid {
display: grid;
grid-template-columns: 40px repeat(7, 1fr); /* Small column for week numbers + 7 days */
grid-template-rows: 40px repeat(6, 1fr);
min-height: 600px;
}
/* Week number header */
.week-header {
grid-column: 1;
grid-row: 1;
background: var(--color-surface);
border-right: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-secondary);
height: 40px;
}
/* Day headers - only day names, right aligned, smaller height */
.month-day-header {
background: var(--color-surface);
border-right: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
display: flex;
align-items: center;
justify-content: flex-end;
padding: 8px 12px;
font-weight: 600;
color: var(--color-text-secondary);
font-size: 0.875rem;
height: 40px;
}
.month-day-header:last-child {
border-right: none;
}
/* Week number cells */
.week-number {
grid-column: 1;
background: var(--color-surface);
border-right: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-secondary);
}
/* Month day cells - similar to existing day columns */
.month-day-cell {
border-right: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
padding: 8px;
background: var(--color-background);
transition: background-color var(--transition-fast);
position: relative;
min-height: 100px;
cursor: pointer;
}
.month-day-cell:hover {
background: var(--color-hover);
}
.month-day-cell:last-child {
border-right: none;
}
.month-day-cell.other-month {
background: var(--color-surface);
color: var(--color-text-secondary);
}
.month-day-cell.today {
background: #f0f8ff;
border-left: 3px solid var(--color-primary);
}
.month-day-cell.weekend {
background: #fafbfc;
}
.month-day-number {
font-weight: 600;
margin-bottom: 6px;
font-size: 0.875rem;
}
.month-day-cell.today .month-day-number {
color: var(--color-primary);
font-weight: 700;
}
/* Month events styling - compact version of existing events */
.month-events {
display: flex;
flex-direction: column;
gap: 2px;
max-height: 70px;
overflow: hidden;
}
.month-event {
background: #e3f2fd;
color: var(--color-primary);
padding: 1px 4px;
border-radius: 2px;
font-size: 10px;
font-weight: 500;
cursor: pointer;
transition: all var(--transition-fast);
border-left: 2px solid var(--color-primary);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.2;
}
.month-event:hover {
transform: translateY(-1px);
}
/* Event categories - using existing color scheme */
.month-event.category-meeting {
background: #e8f5e8;
color: var(--color-success);
border-left-color: var(--color-success);
}
.month-event.category-deadline {
background: #ffebee;
color: var(--color-error);
border-left-color: var(--color-error);
}
.month-event.category-work {
background: #fff8e1;
color: var(--color-secondary);
border-left-color: var(--color-secondary);
}
.month-event.category-personal {
background: #f3e5f5;
color: #7b1fa2;
border-left-color: #9c27b0;
}
.month-event-more {
background: var(--color-surface);
color: var(--color-text-secondary);
padding: 1px 4px;
border-radius: 2px;
font-size: 9px;
text-align: center;
cursor: pointer;
border: 1px dashed var(--color-border);
margin-top: 1px;
}
.month-event-more:hover {
background: var(--color-hover);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.month-container {
margin: 10px;
}
.month-grid {
grid-template-columns: 30px repeat(7, 1fr);
min-height: 400px;
}
.month-day-cell {
min-height: 60px;
padding: 4px;
}
.month-day-header {
padding: 8px 4px;
font-size: 0.75rem;
}
.month-event {
font-size: 9px;
padding: 1px 3px;
}
.month-events {
max-height: 40px;
}
.week-number {
font-size: 0.6rem;
}
}
</style>
</head>
<body>
<div class="month-container">
<div class="month-grid">
<!-- Week number header -->
<div class="week-header">Uge</div>
<!-- Day headers - only day names, right aligned -->
<div class="month-day-header">Man</div>
<div class="month-day-header">Tir</div>
<div class="month-day-header">Ons</div>
<div class="month-day-header">Tor</div>
<div class="month-day-header">Fre</div>
<div class="month-day-header">Lør</div>
<div class="month-day-header">Søn</div>
<!-- Week 1 -->
<div class="week-number">1</div>
<div class="month-day-cell other-month">
<div class="month-day-number">30</div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">31</div>
<div class="month-events">
<div class="month-event category-personal">Nytår</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">1</div>
<div class="month-events">
<div class="month-event category-personal">Nytårsdag</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">2</div>
<div class="month-events">
<div class="month-event category-work">Tilbage på arbejde</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">3</div>
<div class="month-events">
<div class="month-event category-meeting">Team møde</div>
<div class="month-event category-work">Review</div>
</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">4</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">5</div>
<div class="month-events">
<div class="month-event category-personal">Familie</div>
</div>
</div>
<!-- Week 2 -->
<div class="week-number">2</div>
<div class="month-day-cell">
<div class="month-day-number">6</div>
<div class="month-events">
<div class="month-event category-meeting">Stand-up</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">7</div>
<div class="month-events">
<div class="month-event category-deadline">Rapport</div>
<div class="month-event category-meeting">1:1</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">8</div>
<div class="month-events">
<div class="month-event category-work">Code review</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">9</div>
<div class="month-events">
<div class="month-event category-meeting">Planning</div>
<div class="month-event category-work">Design</div>
<div class="month-event-more">+1</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">10</div>
<div class="month-events">
<div class="month-event category-work">Sprint review</div>
</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">11</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">12</div>
<div class="month-events">
<div class="month-event category-personal">Brunch</div>
</div>
</div>
<!-- Week 3 -->
<div class="week-number">3</div>
<div class="month-day-cell">
<div class="month-day-number">13</div>
<div class="month-events">
<div class="month-event category-meeting">All hands</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">14</div>
<div class="month-events">
<div class="month-event category-work">Deploy</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">15</div>
<div class="month-events">
<div class="month-event category-deadline">Presentation</div>
<div class="month-event category-meeting">Retro</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">16</div>
<div class="month-events">
<div class="month-event category-work">Bug triage</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">17</div>
<div class="month-events">
<div class="month-event category-meeting">Planning</div>
</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">18</div>
<div class="month-events">
<div class="month-event category-personal">Koncert</div>
</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">19</div>
</div>
<!-- Week 4 - Today -->
<div class="week-number">4</div>
<div class="month-day-cell today">
<div class="month-day-number">20</div>
<div class="month-events">
<div class="month-event category-meeting">Status møde</div>
<div class="month-event category-work">Refactoring</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">21</div>
<div class="month-events">
<div class="month-event category-deadline">Beta release</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">22</div>
<div class="month-events">
<div class="month-event category-meeting">Architecture</div>
<div class="month-event category-work">Performance</div>
<div class="month-event category-deadline">Docs</div>
<div class="month-event-more">+2</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">23</div>
<div class="month-events">
<div class="month-event category-work">Cleanup</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">24</div>
<div class="month-events">
<div class="month-event category-meeting">Weekly sync</div>
</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">25</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">26</div>
<div class="month-events">
<div class="month-event category-personal">Fødselsdag</div>
</div>
</div>
<!-- Week 5 -->
<div class="week-number">5</div>
<div class="month-day-cell">
<div class="month-day-number">27</div>
<div class="month-events">
<div class="month-event category-meeting">Q1 planning</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">28</div>
<div class="month-events">
<div class="month-event category-work">Tech talks</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">29</div>
<div class="month-events">
<div class="month-event category-deadline">Monthly report</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">30</div>
<div class="month-events">
<div class="month-event category-meeting">Team building</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">31</div>
<div class="month-events">
<div class="month-event category-deadline">End of month</div>
</div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">1</div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">2</div>
</div>
<!-- Week 6 -->
<div class="week-number">6</div>
<div class="month-day-cell other-month">
<div class="month-day-number">3</div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">4</div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">5</div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">6</div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">7</div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">8</div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">9</div>
</div>
</div>
</div>
<script>
// Add click handlers for events and days
document.addEventListener('click', function(e) {
if (e.target.classList.contains('month-event')) {
console.log('Event clicked:', e.target.textContent);
}
if (e.target.classList.contains('month-event-more')) {
console.log('Show more events');
}
if (e.target.classList.contains('month-day-cell') || e.target.closest('.month-day-cell')) {
const cell = e.target.closest('.month-day-cell');
const dayNumber = cell.querySelector('.month-day-number').textContent;
}
});
</script>
</body>
</html>

View file

@ -0,0 +1,848 @@
<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Calendar Plantempus - Month View Expanded</title>
<style>
/* Use existing Calendar Plantempus variables and styling */
:root {
/* Event duration sizing */
--pixels-per-hour: 30px;
--min-event-height: 15px; /* 30 min minimum */
--color-primary: #2196f3;
--color-secondary: #ff9800;
--color-success: #4caf50;
--color-warning: #ff5722;
--color-error: #f44336;
--color-text: #2c3e50;
--color-text-secondary: #6c757d;
--color-border: #e0e0e0;
--color-background: #ffffff;
--color-surface: #f8f9fa;
--color-hover: #f0f0f0;
--transition-fast: 0.15s ease;
--border-radius: 4px;
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background-color: var(--color-surface);
color: var(--color-text);
line-height: 1.5;
}
/* Month grid container */
.month-container {
background: var(--color-background);
border: 1px solid var(--color-border);
border-radius: var(--border-radius);
overflow: hidden;
margin: 20px;
}
/* Month grid layout - rows auto-size based on content */
.month-grid {
display: grid;
grid-template-columns: 40px repeat(7, 1fr);
grid-template-rows: 40px repeat(6, auto);
min-height: 600px;
}
/* Week number header */
.week-header {
grid-column: 1;
grid-row: 1;
background: var(--color-surface);
border-right: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
display: flex;
align-items: center;
justify-content: center;
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-secondary);
height: 40px;
}
/* Day headers */
.month-day-header {
background: var(--color-surface);
border-right: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
display: flex;
align-items: center;
justify-content: flex-end;
padding: 8px 12px;
font-weight: 600;
color: var(--color-text-secondary);
font-size: 0.875rem;
height: 40px;
}
.month-day-header:last-child {
border-right: none;
}
/* Week number cells */
.week-number {
grid-column: 1;
background: var(--color-surface);
border-right: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
display: flex;
align-items: start;
justify-content: center;
padding-top: 8px;
font-size: 0.75rem;
font-weight: 600;
color: var(--color-text-secondary);
}
/* Month day cells */
.month-day-cell {
border-right: 1px solid var(--color-border);
border-bottom: 1px solid var(--color-border);
padding: 8px;
background: var(--color-background);
transition: background-color var(--transition-fast);
min-height: 80px;
cursor: pointer;
}
.month-day-cell:hover {
background: var(--color-hover);
}
.month-day-cell:last-child {
border-right: none;
}
.month-day-cell.other-month {
background: var(--color-surface);
color: var(--color-text-secondary);
}
.month-day-cell.today {
background: #f0f8ff;
border-left: 3px solid var(--color-primary);
}
.month-day-cell.weekend {
background: #fafbfc;
}
.month-day-number {
font-weight: 600;
margin-bottom: 6px;
font-size: 0.875rem;
}
.month-day-cell.today .month-day-number {
color: var(--color-primary);
font-weight: 700;
}
/* Events container */
.month-events {
display: flex;
flex-direction: column;
gap: 3px;
}
/* Individual events with duration-based minimum height */
.month-event {
background: #e3f2fd;
color: var(--color-text);
padding: 6px 8px;
border-radius: 4px;
cursor: pointer;
transition: all var(--transition-fast);
border-left: 3px solid var(--color-primary);
/* Min-height set inline based on duration */
display: flex;
flex-direction: column;
gap: 2px;
}
/* Event time range */
.event-time-range {
font-size: 11px;
color: var(--color-text-secondary);
font-weight: 500;
}
/* Event title */
.event-title {
font-size: 12px;
font-weight: 600;
color: var(--color-text);
line-height: 1.3;
}
/* Event subtitle/description */
.event-subtitle {
font-size: 11px;
color: var(--color-text-secondary);
font-weight: 400;
line-height: 1.3;
}
.month-event:hover {
transform: translateX(2px);
background: #d1e7fd;
}
/* Event categories */
.month-event.category-meeting {
background: #e8f5e8;
border-left-color: var(--color-success);
}
.month-event.category-meeting:hover {
background: #d4edd4;
}
.month-event.category-deadline {
background: #ffebee;
border-left-color: var(--color-error);
}
.month-event.category-deadline:hover {
background: #ffd6dc;
}
.month-event.category-work {
background: #fff8e1;
border-left-color: var(--color-secondary);
}
.month-event.category-work:hover {
background: #ffedcc;
}
.month-event.category-personal {
background: #f3e5f5;
border-left-color: #9c27b0;
}
.month-event.category-personal:hover {
background: #e8d1ec;
}
/* All-day events */
.month-event.all-day {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-left: none;
}
.month-event.all-day .event-time-range,
.month-event.all-day .event-title,
.month-event.all-day .event-subtitle {
color: white;
}
.month-event.all-day:hover {
background: linear-gradient(135deg, #5a72e8 0%, #6b42a0 100%);
}
/* Short events (30 min) - compact layout */
.month-event.short-event {
flex-direction: row;
align-items: center;
gap: 8px;
}
.month-event.short-event .event-time-range {
flex-shrink: 0;
}
.month-event.short-event .event-title {
font-size: 11px;
}
.month-event.short-event .event-subtitle {
display: none;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.month-container {
margin: 10px;
}
.month-grid {
grid-template-columns: 30px repeat(7, 1fr);
min-height: 500px;
}
.month-day-cell {
padding: 4px;
min-height: 60px;
}
.month-day-header {
padding: 8px 4px;
font-size: 0.75rem;
}
.month-event {
padding: 4px 6px;
}
.event-title {
font-size: 11px;
}
.event-time-range,
.event-subtitle {
font-size: 10px;
}
.week-number {
font-size: 0.6rem;
}
:root {
--pixels-per-hour: 20px;
}
}
</style>
</head>
<body>
<div class="month-container">
<div class="month-grid">
<!-- Week number header -->
<div class="week-header">Uge</div>
<!-- Day headers -->
<div class="month-day-header">Man</div>
<div class="month-day-header">Tir</div>
<div class="month-day-header">Ons</div>
<div class="month-day-header">Tor</div>
<div class="month-day-header">Fre</div>
<div class="month-day-header">Lør</div>
<div class="month-day-header">Søn</div>
<!-- Week 1 -->
<div class="week-number">1</div>
<div class="month-day-cell other-month">
<div class="month-day-number">30</div>
<div class="month-events"></div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">31</div>
<div class="month-events">
<div class="month-event category-personal" style="min-height: 60px;">
<div class="event-time-range">20:00 - 22:00</div>
<div class="event-title">Nytårsaften</div>
<div class="event-subtitle">Fest med familie og venner</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">1</div>
<div class="month-events">
<div class="month-event category-personal all-day" style="min-height: 30px;">
<div class="event-time-range">Hele dagen</div>
<div class="event-title">Nytårsdag</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">2</div>
<div class="month-events">
<div class="month-event category-work" style="min-height: 30px;">
<div class="event-time-range">09:00 - 10:00</div>
<div class="event-title">Tilbage på arbejde</div>
<div class="event-subtitle">Opstart efter ferien</div>
</div>
<div class="month-event category-meeting short-event" style="min-height: 15px;">
<div class="event-time-range">10:00 - 10:30</div>
<div class="event-title">Kick-off møde</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">3</div>
<div class="month-events">
<div class="month-event category-meeting" style="min-height: 45px;">
<div class="event-time-range">09:00 - 10:30</div>
<div class="event-title">Team møde</div>
<div class="event-subtitle">Q1 planning og mål</div>
</div>
<div class="month-event category-work" style="min-height: 60px;">
<div class="event-time-range">11:00 - 13:00</div>
<div class="event-title">Projekt review</div>
<div class="event-subtitle">Gennemgang af December projekter og status på Q1 initiativer</div>
</div>
<div class="month-event category-deadline short-event" style="min-height: 15px;">
<div class="event-time-range">15:00 - 15:30</div>
<div class="event-title">Sprint deadline</div>
</div>
</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">4</div>
<div class="month-events"></div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">5</div>
<div class="month-events">
<div class="month-event category-personal" style="min-height: 75px;">
<div class="event-time-range">18:00 - 20:30</div>
<div class="event-title">Familie middag</div>
<div class="event-subtitle">Hos mormor og morfar</div>
</div>
</div>
</div>
<!-- Week 2 -->
<div class="week-number">2</div>
<div class="month-day-cell">
<div class="month-day-number">6</div>
<div class="month-events">
<div class="month-event category-meeting short-event" style="min-height: 7.5px;">
<div class="event-time-range">09:15 - 09:30</div>
<div class="event-title">Stand-up</div>
</div>
<div class="month-event category-work" style="min-height: 45px;">
<div class="event-time-range">10:00 - 11:30</div>
<div class="event-title">Code review</div>
<div class="event-subtitle">Frontend refactoring PR</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">7</div>
<div class="month-events">
<div class="month-event category-deadline" style="min-height: 30px;">
<div class="event-time-range">12:00 - 13:00</div>
<div class="event-title">Rapport deadline</div>
<div class="event-subtitle">Månedlig status rapport</div>
</div>
<div class="month-event category-meeting" style="min-height: 30px;">
<div class="event-time-range">14:00 - 15:00</div>
<div class="event-title">1:1 med chef</div>
<div class="event-subtitle">Karriere udvikling</div>
</div>
<div class="month-event category-work short-event" style="min-height: 15px;">
<div class="event-time-range">15:30 - 16:00</div>
<div class="event-title">Design review</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">8</div>
<div class="month-events">
<div class="month-event category-work" style="min-height: 30px;">
<div class="event-time-range">10:00 - 11:00</div>
<div class="event-title">Feature demo</div>
<div class="event-subtitle">Ny dashboard funktionalitet</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">9</div>
<div class="month-events">
<div class="month-event category-meeting" style="min-height: 60px;">
<div class="event-time-range">09:00 - 11:00</div>
<div class="event-title">Planning møde</div>
<div class="event-subtitle">Sprint 23 planning og estimering</div>
</div>
<div class="month-event category-work" style="min-height: 45px;">
<div class="event-time-range">11:00 - 12:30</div>
<div class="event-title">Design session</div>
<div class="event-subtitle">UX workshop for ny feature</div>
</div>
<div class="month-event category-deadline short-event" style="min-height: 15px;">
<div class="event-time-range">14:00 - 14:30</div>
<div class="event-title">Release notes</div>
</div>
<div class="month-event category-meeting" style="min-height: 30px;">
<div class="event-time-range">15:00 - 16:00</div>
<div class="event-title">Tech sync</div>
<div class="event-subtitle">Arkitektur diskussion</div>
</div>
<div class="month-event category-personal short-event" style="min-height: 15px;">
<div class="event-time-range">17:00 - 17:30</div>
<div class="event-title">Tandlæge</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">10</div>
<div class="month-events">
<div class="month-event category-work" style="min-height: 60px;">
<div class="event-time-range">13:00 - 15:00</div>
<div class="event-title">Sprint review</div>
<div class="event-subtitle">Demo af Sprint 22 leverancer til stakeholders</div>
</div>
<div class="month-event category-meeting" style="min-height: 30px;">
<div class="event-time-range">15:00 - 16:00</div>
<div class="event-title">Retrospective</div>
<div class="event-subtitle">Sprint 22 læringer</div>
</div>
</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">11</div>
<div class="month-events"></div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">12</div>
<div class="month-events">
<div class="month-event category-personal" style="min-height: 60px;">
<div class="event-time-range">11:00 - 13:00</div>
<div class="event-title">Brunch</div>
<div class="event-subtitle">Med gamle venner fra uni</div>
</div>
</div>
</div>
<!-- Week 3 -->
<div class="week-number">3</div>
<div class="month-day-cell">
<div class="month-day-number">13</div>
<div class="month-events">
<div class="month-event category-meeting all-day" style="min-height: 30px;">
<div class="event-time-range">Hele dagen</div>
<div class="event-title">All hands møde</div>
<div class="event-subtitle">Quarterly business update</div>
</div>
<div class="month-event category-work" style="min-height: 60px;">
<div class="event-time-range">14:00 - 16:00</div>
<div class="event-title">Architecture planning</div>
<div class="event-subtitle">Microservices migration strategi</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">14</div>
<div class="month-events">
<div class="month-event category-work" style="min-height: 90px;">
<div class="event-time-range">10:00 - 13:00</div>
<div class="event-title">Feature deployment</div>
<div class="event-subtitle">Production release og monitoring af ny payment feature</div>
</div>
<div class="month-event category-meeting" style="min-height: 45px;">
<div class="event-time-range">15:00 - 16:30</div>
<div class="event-title">Client call</div>
<div class="event-subtitle">Requirements gathering</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">15</div>
<div class="month-events">
<div class="month-event category-deadline" style="min-height: 60px;">
<div class="event-time-range">10:00 - 12:00</div>
<div class="event-title">Client presentation</div>
<div class="event-subtitle">Q4 results og Q1 roadmap præsentation</div>
</div>
<div class="month-event category-meeting" style="min-height: 30px;">
<div class="event-time-range">14:00 - 15:00</div>
<div class="event-title">Team retrospective</div>
<div class="event-subtitle">Monthly team health check</div>
</div>
<div class="month-event category-work" style="min-height: 30px;">
<div class="event-time-range">16:00 - 17:00</div>
<div class="event-title">Bug triage</div>
<div class="event-subtitle">Priority 1 issues</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">16</div>
<div class="month-events">
<div class="month-event category-work" style="min-height: 45px;">
<div class="event-time-range">11:00 - 12:30</div>
<div class="event-title">Performance review</div>
<div class="event-subtitle">Database optimization results</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">17</div>
<div class="month-events">
<div class="month-event category-meeting" style="min-height: 90px;">
<div class="event-time-range">09:00 - 12:00</div>
<div class="event-title">Sprint planning</div>
<div class="event-subtitle">Sprint 24 - omfattende planning session med hele teamet</div>
</div>
<div class="month-event category-work" style="min-height: 60px;">
<div class="event-time-range">13:00 - 15:00</div>
<div class="event-title">Code cleanup</div>
<div class="event-subtitle">Technical debt reduction</div>
</div>
</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">18</div>
<div class="month-events">
<div class="month-event category-personal" style="min-height: 90px;">
<div class="event-time-range">19:00 - 22:00</div>
<div class="event-title">Koncert</div>
<div class="event-subtitle">Royal Arena - med forband og afterparty</div>
</div>
</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">19</div>
<div class="month-events"></div>
</div>
<!-- Week 4 - Today -->
<div class="week-number">4</div>
<div class="month-day-cell today">
<div class="month-day-number">20</div>
<div class="month-events">
<div class="month-event category-meeting short-event" style="min-height: 15px;">
<div class="event-time-range">09:00 - 09:30</div>
<div class="event-title">Status møde</div>
</div>
<div class="month-event category-work" style="min-height: 45px;">
<div class="event-time-range">10:30 - 12:00</div>
<div class="event-title">Refactoring session</div>
<div class="event-subtitle">Legacy code modernization</div>
</div>
<div class="month-event category-deadline" style="min-height: 30px;">
<div class="event-time-range">12:00 - 13:00</div>
<div class="event-title">Documentation due</div>
<div class="event-subtitle">API documentation update</div>
</div>
<div class="month-event category-meeting" style="min-height: 45px;">
<div class="event-time-range">14:00 - 15:30</div>
<div class="event-title">Architecture review</div>
<div class="event-subtitle">System design review med senior architects</div>
</div>
<div class="month-event category-work" style="min-height: 30px;">
<div class="event-time-range">15:30 - 16:30</div>
<div class="event-title">Performance testing</div>
<div class="event-subtitle">Load testing results</div>
</div>
<div class="month-event category-personal short-event" style="min-height: 15px;">
<div class="event-time-range">17:00 - 17:30</div>
<div class="event-title">Gym</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">21</div>
<div class="month-events">
<div class="month-event category-deadline" style="min-height: 120px;">
<div class="event-time-range">14:00 - 18:00</div>
<div class="event-title">Beta release</div>
<div class="event-subtitle">Full release process including deployment, smoke testing, monitoring setup og stakeholder notification</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">22</div>
<div class="month-events">
<div class="month-event category-meeting" style="min-height: 45px;">
<div class="event-time-range">09:00 - 10:30</div>
<div class="event-title">Architecture review</div>
<div class="event-subtitle">Final design approval</div>
</div>
<div class="month-event category-work" style="min-height: 60px;">
<div class="event-time-range">11:00 - 13:00</div>
<div class="event-title">Performance testing</div>
<div class="event-subtitle">Full regression test suite execution</div>
</div>
<div class="month-event category-deadline" style="min-height: 30px;">
<div class="event-time-range">14:00 - 15:00</div>
<div class="event-title">Documentation deadline</div>
<div class="event-subtitle">User guide completion</div>
</div>
<div class="month-event category-meeting" style="min-height: 45px;">
<div class="event-time-range">15:00 - 16:30</div>
<div class="event-title">Stakeholder meeting</div>
<div class="event-subtitle">Project status update</div>
</div>
<div class="month-event category-work short-event" style="min-height: 15px;">
<div class="event-time-range">16:30 - 17:00</div>
<div class="event-title">Deploy to staging</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">23</div>
<div class="month-events">
<div class="month-event category-work all-day" style="min-height: 30px;">
<div class="event-time-range">Hele dagen</div>
<div class="event-title">Code cleanup day</div>
<div class="event-subtitle">Team-wide technical debt reduction</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">24</div>
<div class="month-events">
<div class="month-event category-meeting" style="min-height: 30px;">
<div class="event-time-range">14:00 - 15:00</div>
<div class="event-title">Weekly sync</div>
<div class="event-subtitle">Cross-team alignment</div>
</div>
</div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">25</div>
<div class="month-events"></div>
</div>
<div class="month-day-cell weekend">
<div class="month-day-number">26</div>
<div class="month-events">
<div class="month-event category-personal all-day" style="min-height: 30px;">
<div class="event-time-range">Hele dagen</div>
<div class="event-title">Fødselsdag</div>
</div>
<div class="month-event category-personal" style="min-height: 120px;">
<div class="event-time-range">18:00 - 22:00</div>
<div class="event-title">Fødselsdagsfest</div>
<div class="event-subtitle">Stor fest med familie og venner, middag og underholdning</div>
</div>
</div>
</div>
<!-- Week 5 -->
<div class="week-number">5</div>
<div class="month-day-cell">
<div class="month-day-number">27</div>
<div class="month-events">
<div class="month-event category-meeting all-day" style="min-height: 30px;">
<div class="event-time-range">Hele dagen</div>
<div class="event-title">Q1 planning - Day 1</div>
<div class="event-subtitle">Quarterly planning workshop</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">28</div>
<div class="month-events">
<div class="month-event category-meeting all-day" style="min-height: 30px;">
<div class="event-time-range">Hele dagen</div>
<div class="event-title">Q1 planning - Day 2</div>
<div class="event-subtitle">OKR setting og roadmap</div>
</div>
<div class="month-event category-work" style="min-height: 60px;">
<div class="event-time-range">14:00 - 16:00</div>
<div class="event-title">Tech talks</div>
<div class="event-subtitle">Knowledge sharing session om nye teknologier</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">29</div>
<div class="month-events">
<div class="month-event category-meeting all-day" style="min-height: 30px;">
<div class="event-time-range">Hele dagen</div>
<div class="event-title">Q1 planning - Day 3</div>
<div class="event-subtitle">Final alignment og præsentation</div>
</div>
<div class="month-event category-deadline" style="min-height: 60px;">
<div class="event-time-range">16:00 - 18:00</div>
<div class="event-title">Monthly report</div>
<div class="event-subtitle">Januar status rapport og metrics sammensætning</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">30</div>
<div class="month-events">
<div class="month-event category-meeting" style="min-height: 90px;">
<div class="event-time-range">15:00 - 18:00</div>
<div class="event-title">Team building</div>
<div class="event-subtitle">Off-site team building aktivitet med middag</div>
</div>
</div>
</div>
<div class="month-day-cell">
<div class="month-day-number">31</div>
<div class="month-events">
<div class="month-event category-deadline all-day" style="min-height: 30px;">
<div class="event-time-range">Hele dagen</div>
<div class="event-title">End of month</div>
</div>
<div class="month-event category-work" style="min-height: 60px;">
<div class="event-time-range">14:00 - 16:00</div>
<div class="event-title">Month wrap-up</div>
<div class="event-subtitle">Januar review og Februar forberedelse</div>
</div>
</div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">1</div>
<div class="month-events"></div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">2</div>
<div class="month-events"></div>
</div>
<!-- Week 6 -->
<div class="week-number">6</div>
<div class="month-day-cell other-month">
<div class="month-day-number">3</div>
<div class="month-events"></div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">4</div>
<div class="month-events"></div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">5</div>
<div class="month-events"></div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">6</div>
<div class="month-events"></div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">7</div>
<div class="month-events"></div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">8</div>
<div class="month-events"></div>
</div>
<div class="month-day-cell other-month">
<div class="month-day-number">9</div>
<div class="month-events"></div>
</div>
</div>
</div>
<script>
// Click handlers
document.addEventListener('click', function(e) {
if (e.target.closest('.month-event')) {
const event = e.target.closest('.month-event');
console.log('Event clicked:', event.querySelector('.event-title').textContent);
// Remove previous selection
document.querySelectorAll('.month-event').forEach(el => {
el.style.outline = '';
});
// Highlight selected event
event.style.outline = '2px solid var(--color-primary)';
}
if (e.target.closest('.month-day-cell')) {
const cell = e.target.closest('.month-day-cell');
const dayNumber = cell.querySelector('.month-day-number').textContent;
console.log('Day clicked:', dayNumber);
}
});
</script>
</body>
</html>