Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
153 changes: 141 additions & 12 deletions internal/air/static/css/calendar-grid.css
Original file line number Diff line number Diff line change
Expand Up @@ -178,23 +178,61 @@
background: white;
}

.today-dot {
/* Pulsing today indicator */
.today-indicator {
position: absolute;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
width: 6px;
height: 6px;
top: 8px;
right: 8px;
width: 8px;
height: 8px;
background: var(--accent);
border-radius: 50%;
animation: todayPulse 2s ease-in-out infinite;
}

.event-dot {
width: 8px;
height: 8px;
@keyframes todayPulse {
0%, 100% {
transform: scale(1);
opacity: 1;
box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.4);
}
50% {
transform: scale(1.2);
opacity: 0.8;
box-shadow: 0 0 0 6px rgba(139, 92, 246, 0);
}
}

/* Event count badge */
.event-count-badge {
position: absolute;
bottom: 8px;
right: 8px;
min-width: 20px;
height: 20px;
padding: 0 6px;
background: var(--accent);
border-radius: 50%;
margin-top: 4px;
border-radius: 10px;
font-size: 11px;
font-weight: 600;
color: white;
display: flex;
align-items: center;
justify-content: center;
}

.calendar-day.selected .event-count-badge {
background: white;
color: var(--accent);
}

/* Legacy support */
.today-dot {
display: none;
}

.event-dot {
display: none;
}

.calendar-events-panel {
Expand Down Expand Up @@ -233,13 +271,43 @@
border: 1px solid var(--border);
border-radius: 14px;
transition: all 0.2s;
position: relative;
}

.event-card:hover {
border-color: var(--border-light);
transform: translateY(-2px);
}

/* Edit button - top right corner */
.event-card .event-edit-btn {
position: absolute !important;
top: 8px;
right: 8px;
width: 28px;
height: 28px;
display: flex;
align-items: center;
justify-content: center;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-muted);
cursor: pointer;
opacity: 0;
transition: all 0.2s;
}

.event-card:hover .event-edit-btn {
opacity: 1;
}

.event-edit-btn:hover {
background: var(--bg-hover);
color: var(--text-primary);
border-color: var(--border-light);
}

.event-card.focus-time {
background: linear-gradient(135deg, rgba(34, 197, 94, 0.1) 0%, rgba(16, 185, 129, 0.05) 100%);
border-color: rgba(34, 197, 94, 0.3);
Expand All @@ -262,10 +330,46 @@
margin-bottom: 10px;
}

.event-time-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 6px;
}

.event-time {
font-size: 12px;
color: var(--text-muted);
margin-bottom: 6px;
}

.event-relative-time {
font-size: 11px;
font-weight: 600;
padding: 2px 8px;
border-radius: 10px;
background: var(--bg-surface);
color: var(--text-muted);
}

.event-relative-time.starting-now {
background: linear-gradient(135deg, #ef4444 0%, #f97316 100%);
color: white;
animation: urgentPulse 1s ease-in-out infinite;
}

.event-relative-time.starting-soon {
background: linear-gradient(135deg, #f59e0b 0%, #eab308 100%);
color: white;
}

.event-relative-time.upcoming {
background: rgba(139, 92, 246, 0.15);
color: var(--accent);
}

@keyframes urgentPulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}

.event-title {
Expand Down Expand Up @@ -316,6 +420,31 @@
color: var(--text-muted);
}

/* Join Meeting button */
.join-meeting-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
background: linear-gradient(135deg, #4285f4 0%, #34a853 100%);
border-radius: 8px;
font-size: 13px;
font-weight: 600;
color: white;
text-decoration: none;
transition: all 0.2s;
box-shadow: 0 2px 8px rgba(66, 133, 244, 0.3);
}

.join-meeting-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(66, 133, 244, 0.4);
}

.join-meeting-btn:active {
transform: translateY(0);
}

.event-actions {
display: flex;
gap: 8px;
Expand Down
3 changes: 2 additions & 1 deletion internal/air/static/js/calendar-data.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ async loadEvents() {

const data = await AirAPI.getEvents({
start: Math.floor(start.getTime() / 1000),
end: Math.floor(end.getTime() / 1000)
end: Math.floor(end.getTime() / 1000),
limit: 200 // Fetch more events to cover the full month
});

this.events = data.events || [];
Expand Down
103 changes: 93 additions & 10 deletions internal/air/static/js/calendar-ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,21 +45,23 @@ renderCalendarGrid() {
for (let day = 1; day <= daysInMonth; day++) {
const isToday = isCurrentMonth && day === todayDate;
const dateStr = `${year}-${month + 1}-${day}`;
const hasEvents = this.events.some(e => {
// Count events for this day
const dayEvents = this.events.filter(e => {
const eventDate = new Date(e.start_time * 1000);
return eventDate.getFullYear() === year &&
eventDate.getMonth() === month &&
eventDate.getDate() === day;
});
const eventCount = dayEvents.length;

let classes = 'calendar-day';
if (isToday) classes += ' today';
if (hasEvents) classes += ' has-event';
if (eventCount > 0) classes += ' has-event';

html += `<div class="${classes}" data-date="${dateStr}">
${day}
${isToday ? '<span class="today-dot"></span>' : ''}
${hasEvents ? '<div class="event-dot"></div>' : ''}
${isToday ? '<span class="today-indicator"></span>' : ''}
${eventCount > 0 ? `<div class="event-count-badge">${eventCount}</div>` : ''}
</div>`;
}

Expand Down Expand Up @@ -95,8 +97,24 @@ onDayClick(dateStr, dayEl) {
document.querySelectorAll('.calendar-day.selected').forEach(el => el.classList.remove('selected'));
dayEl.classList.add('selected');

// Update events panel header
// Check if clicked date is today
const today = new Date();
const isToday = clickedDate.getFullYear() === today.getFullYear() &&
clickedDate.getMonth() === today.getMonth() &&
clickedDate.getDate() === today.getDate();

// Update events panel header title and date
const headerEl = document.querySelector('.events-header h3');
const dateEl = document.querySelector('.events-date');

if (headerEl) {
headerEl.textContent = isToday ? "Today's Schedule" : clickedDate.toLocaleDateString('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric'
});
}

if (dateEl) {
dateEl.textContent = clickedDate.toLocaleDateString('en-US', {
weekday: 'short',
Expand Down Expand Up @@ -258,6 +276,7 @@ renderEventCard(event) {
const endTime = this.formatEventTime(event.end_time);
const isFocusTime = event.title?.toLowerCase().includes('focus');
const hasConferencing = event.conferencing && event.conferencing.url;
const relativeTime = this.getRelativeTime(event.start_time);

const participantsHtml = event.participants && event.participants.length > 0
? `<div class="event-attendees">
Expand All @@ -271,23 +290,79 @@ renderEventCard(event) {
: '';

return `
<div class="event-card${isFocusTime ? ' focus-time' : ''}" data-event-id="${event.id}">
<div class="event-time">${event.is_all_day ? 'All Day' : `${startTime} - ${endTime}`}</div>
<div class="event-card${isFocusTime ? ' focus-time' : ''}${relativeTime.class ? ' ' + relativeTime.class : ''}" data-event-id="${event.id}">
<button class="event-edit-btn" style="position: absolute; top: 8px; right: 8px;" onclick="event.stopPropagation(); CalendarManager.openEditModal('${event.id}')" title="Edit event">
<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<circle cx="12" cy="12" r="3"></circle>
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1 0 2.83 2 2 0 0 1-2.83 0l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-2 2 2 2 0 0 1-2-2v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83 0 2 2 0 0 1 0-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1-2-2 2 2 0 0 1 2-2h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 0-2.83 2 2 0 0 1 2.83 0l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 2-2 2 2 0 0 1 2 2v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 0 2 2 0 0 1 0 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 2 2 2 2 0 0 1-2 2h-.09a1.65 1.65 0 0 0-1.51 1z"></path>
</svg>
</button>
<div class="event-time-row">
<div class="event-time">${event.is_all_day ? 'All Day' : `${startTime} - ${endTime}`}</div>
${relativeTime.text ? `<div class="event-relative-time ${relativeTime.class}">${relativeTime.text}</div>` : ''}
</div>
<div class="event-title">${isFocusTime ? '🧘 ' : ''}${this.escapeHtml(event.title || '(No Title)')}</div>
${event.description ? `<div class="event-desc">${this.escapeHtml(this.stripHtml(event.description).substring(0, 100))}</div>` : ''}
${event.location ? `<div class="event-location">📍 ${this.escapeHtml(event.location)}</div>` : ''}
${participantsHtml}
${hasConferencing ? `
<div class="event-meta">
<a href="${event.conferencing.url}" target="_blank" class="event-tag">
📹 ${event.conferencing.provider || 'Video Call'}
<div class="event-actions">
<a href="${event.conferencing.url}" target="_blank" class="join-meeting-btn" onclick="event.stopPropagation()">
📹 Join Meeting
</a>
</div>
` : ''}
</div>
`;
},

getRelativeTime(timestamp) {
const now = Date.now() / 1000;
const diff = timestamp - now;
const diffMins = Math.floor(diff / 60);
const diffHours = Math.floor(diff / 3600);

// Past events
if (diff < 0) {
return { text: '', class: '' };
}

// Starting now (within 5 minutes)
if (diffMins <= 5) {
return { text: 'Starting now', class: 'starting-now' };
}

// Starting soon (within 30 minutes)
if (diffMins <= 30) {
return { text: `in ${diffMins} min`, class: 'starting-soon' };
}

// Within the hour
if (diffMins < 60) {
return { text: `in ${diffMins} min`, class: 'upcoming' };
}

// Within a few hours
if (diffHours <= 3) {
return { text: `in ${diffHours} hr${diffHours > 1 ? 's' : ''}`, class: 'upcoming' };
}

// Later today or tomorrow
const eventDate = new Date(timestamp * 1000);
const today = new Date();
if (eventDate.toDateString() === today.toDateString()) {
return { text: 'Today', class: '' };
}

const tomorrow = new Date(today);
tomorrow.setDate(tomorrow.getDate() + 1);
if (eventDate.toDateString() === tomorrow.toDateString()) {
return { text: 'Tomorrow', class: '' };
}

return { text: '', class: '' };
},

formatEventTime(timestamp) {
if (!timestamp) return '';
const date = new Date(timestamp * 1000);
Expand Down Expand Up @@ -316,4 +391,12 @@ stripHtml(html) {
text = text.replace(/\s+/g, ' ').trim();
return text;
},

openEditModal(eventId) {
// Find the event by ID
const event = this.events.find(e => e.id === eventId);
if (event && typeof EventModal !== 'undefined') {
EventModal.open(event);
}
},
});
8 changes: 4 additions & 4 deletions internal/air/templates/pages/calendar.gohtml
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,11 @@
</button>

<div class="folder-group">
<div class="folder-item active">
<div class="folder-item">
<span class="folder-icon">📆</span>
<span>Today</span>
</div>
<div class="folder-item">
<div class="folder-item active">
<span class="folder-icon">📅</span>
<span>Week</span>
</div>
Expand Down Expand Up @@ -79,7 +79,7 @@
<div class="calendar-header">
<div class="calendar-nav">
<button class="calendar-nav-btn">←</button>
<h2 class="calendar-title">December 2024</h2>
<h2 class="calendar-title">Loading...</h2>
<button class="calendar-nav-btn">→</button>
</div>
<button class="today-btn">Today</button>
Expand Down Expand Up @@ -139,7 +139,7 @@
<div class="calendar-events-panel" data-testid="events-panel">
<div class="events-header">
<h3>Today's Schedule</h3>
<span class="events-date">Wed, Dec 25</span>
<span class="events-date">Loading...</span>
</div>
<div class="events-list" id="eventsList">
<!-- Skeleton loaders - replaced by JavaScript -->
Expand Down
Loading