295 lines
13 KiB
JavaScript
295 lines
13 KiB
JavaScript
import calendarUtil from "../calendarUtil.js";
|
||
|
||
class DesktopSidebar extends Shadow {
|
||
static DAY_NAMES = ["S", "M", "T", "W", "T", "F", "S"];
|
||
|
||
constructor(currentDate, calendars, selectedCalendars, events, weekStartsOn, actions) {
|
||
super()
|
||
this.currentDate = currentDate
|
||
this.calendars = calendars
|
||
this.selectedCalendars = selectedCalendars
|
||
this.events = events
|
||
this.weekStartsOn = weekStartsOn
|
||
this.actions = actions
|
||
|
||
// Persist mini calendar month across parent rerenders
|
||
if (this.miniDate === undefined) {
|
||
this.miniDate = new Date(currentDate)
|
||
}
|
||
if (this.selectedDate === undefined) {
|
||
this.selectedDate = new Date(currentDate)
|
||
}
|
||
}
|
||
|
||
render() {
|
||
const { onSelectDate, onToggleCalendar, onNewCalendar, onEditCalendar } = this.actions
|
||
const ordered = Array.from({ length: 7 }, (_, i) =>
|
||
DesktopSidebar.DAY_NAMES[(this.weekStartsOn + i) % 7]
|
||
);
|
||
const weeks = this.buildMonthWeeks(this.miniDate);
|
||
const today = new Date();
|
||
|
||
VStack(() => {
|
||
// ── Mini calendar ─────────────────────────────────────────
|
||
VStack(() => {
|
||
// Month / year + nav arrows
|
||
HStack(() => {
|
||
p(this.miniDate.toLocaleDateString(undefined, { month: "long", year: "numeric" }))
|
||
.margin(0)
|
||
.fontWeight("600")
|
||
.fontSize(0.78, em)
|
||
.color("var(--headertext)")
|
||
.flex(1)
|
||
|
||
HStack(() => {
|
||
button("‹")
|
||
.onClick((done) => {if(!done) return; this.miniDate = calendarUtil.addMonths(this.miniDate, -1); this.rerender(); })
|
||
.padding("0.18em 0.42em")
|
||
.border("none")
|
||
.background("transparent")
|
||
.color("var(--headertext)")
|
||
.cursor("pointer")
|
||
.borderRadius(0.3, em)
|
||
.fontSize(0.85, em)
|
||
button("›")
|
||
.onClick((done) => {if(!done) return; this.miniDate = calendarUtil.addMonths(this.miniDate, 1); this.rerender(); })
|
||
.padding("0.18em 0.42em")
|
||
.border("none")
|
||
.background("transparent")
|
||
.color("var(--headertext)")
|
||
.cursor("pointer")
|
||
.borderRadius(0.3, em)
|
||
.fontSize(0.85, em)
|
||
})
|
||
.gap(0)
|
||
})
|
||
.alignItems("center")
|
||
.marginBottom(0.35, em)
|
||
|
||
// Day-name header row
|
||
HStack(() => {
|
||
ordered.forEach(name => {
|
||
p(name)
|
||
.margin(0)
|
||
.fontSize(0.64, em)
|
||
.fontWeight("600")
|
||
.color("var(--headertext)")
|
||
.opacity(0.42)
|
||
.flex(1)
|
||
.textAlign("center")
|
||
.paddingVertical(0.18, em)
|
||
})
|
||
})
|
||
.width(100, pct)
|
||
|
||
// Date cells
|
||
VStack(() => {
|
||
weeks.forEach(({ days }) => {
|
||
HStack(() => {
|
||
days.forEach(({ day, isCurrentMonth, calendarColors }) => {
|
||
const isToday = calendarUtil.isSameDay(day, today);
|
||
const isSelected = calendarUtil.isSameDay(day, this.selectedDate);
|
||
|
||
VStack(() => {
|
||
p(day.getDate())
|
||
.margin(0)
|
||
.fontSize(0.72, em)
|
||
.fontWeight(isSelected ? "700" : "400")
|
||
.color(isSelected || isToday ? "white" : "var(--headertext)")
|
||
.background(
|
||
isToday ? "var(--quillred)"
|
||
: isSelected ? "var(--lightaccent)"
|
||
: "transparent"
|
||
)
|
||
.width(1.52, em)
|
||
.height(1.52, em)
|
||
.borderRadius(25, pct)
|
||
.textAlign("center")
|
||
.lineHeight("1.52em")
|
||
.opacity(isCurrentMonth ? 1 : 0.27)
|
||
.boxSizing("border-box")
|
||
|
||
if (calendarColors.length > 0) {
|
||
HStack(() => {
|
||
calendarColors.slice(0, 3).forEach(color => {
|
||
VStack(() => {})
|
||
.width(0.24, em)
|
||
.height(0.24, em)
|
||
.borderRadius(50, pct)
|
||
.background(color)
|
||
.flexShrink(0)
|
||
})
|
||
})
|
||
.gap(0.14, em)
|
||
.justifyContent("center")
|
||
.marginTop(0.1, em)
|
||
} else {
|
||
VStack(() => {}).height(0.34, em)
|
||
}
|
||
})
|
||
.flex(1)
|
||
.alignItems("center")
|
||
.paddingVertical(0.16, em)
|
||
.cursor("pointer")
|
||
.onClick((done) => {if(!done) return;
|
||
this.selectedDate = day;
|
||
this.miniDate = new Date(day);
|
||
onSelectDate(day);
|
||
this.rerender();
|
||
})
|
||
})
|
||
})
|
||
.width(100, pct)
|
||
})
|
||
})
|
||
.width(100, pct)
|
||
})
|
||
.marginTop(30, px)
|
||
.padding(1, em)
|
||
.paddingBottom(0.75, em)
|
||
|
||
// ── Divider ───────────────────────────────────────────────
|
||
VStack(() => {})
|
||
.height(1, px)
|
||
.background("var(--divider)")
|
||
.width(100, pct)
|
||
|
||
// ── Calendars list ────────────────────────────────────────
|
||
VStack(() => {
|
||
p("CALENDARS")
|
||
.margin(0)
|
||
.marginBottom(0.45, em)
|
||
.fontSize(0.63, em)
|
||
.fontWeight("700")
|
||
.letterSpacing("0.07em")
|
||
.color("var(--headertext)")
|
||
.opacity(0.38)
|
||
.paddingHorizontal(1, em)
|
||
|
||
this.calendars.forEach(cal => {
|
||
const isSelected = this.selectedCalendars.some(c => c.id === cal.id);
|
||
HStack(() => {
|
||
HStack(() => {
|
||
VStack(() => {})
|
||
.width(0.65, em)
|
||
.height(0.65, em)
|
||
.borderRadius(50, pct)
|
||
.background(isSelected ? cal.color : "transparent")
|
||
.border(`2px solid ${cal.color}`)
|
||
.boxSizing("border-box")
|
||
.flexShrink(0)
|
||
p(cal.name)
|
||
.margin(0)
|
||
.fontSize(0.78, em)
|
||
.color("var(--headertext)")
|
||
.opacity(isSelected ? 1 : 0.4)
|
||
.flex(1)
|
||
.overflow("hidden")
|
||
.whiteSpace("nowrap")
|
||
.textOverflow("ellipsis")
|
||
})
|
||
.flex(1)
|
||
.gap(0.55, em)
|
||
.alignItems("center")
|
||
.cursor("pointer")
|
||
.onClick((done) => { if (!done) return; if (onToggleCalendar) onToggleCalendar(cal); })
|
||
if (onEditCalendar && cal.owner_id === global.profile.id) {
|
||
button("···")
|
||
.border("none")
|
||
.background("transparent")
|
||
.color("var(--headertext)")
|
||
.opacity(0.4)
|
||
.fontSize(0.72, em)
|
||
.cursor("pointer")
|
||
.padding("0 0.2em")
|
||
.flexShrink(0)
|
||
.onClick((done) => { if (done) onEditCalendar(cal) })
|
||
}
|
||
})
|
||
.paddingHorizontal(1, em)
|
||
.paddingVertical(0.32, em)
|
||
.alignItems("center")
|
||
.borderRadius(0.4, em)
|
||
})
|
||
|
||
button("+ New Calendar")
|
||
.paddingVertical(0.52, em)
|
||
.paddingHorizontal(0.8, em)
|
||
.marginHorizontal(1, em)
|
||
.marginVertical(0.5, em)
|
||
.width("auto")
|
||
.background("transparent")
|
||
.border("1px solid var(--divider)")
|
||
.color("var(--headertext)")
|
||
.borderRadius(0.5, em)
|
||
.fontSize(0.83, em)
|
||
.fontWeight("600")
|
||
.cursor("pointer")
|
||
.onHover(function(hovering) {
|
||
this.style.background = hovering ? "var(--divider)" : "transparent";
|
||
})
|
||
.onClick((done) => { if (done) onNewCalendar() })
|
||
})
|
||
.paddingTop(0.7, em)
|
||
.paddingBottom(0.5, em)
|
||
})
|
||
.width(220, px)
|
||
.minWidth(220, px)
|
||
.height(100, pct)
|
||
.borderRight("1px solid var(--divider)")
|
||
.boxSizing("border-box")
|
||
.overflowY("auto")
|
||
.flexShrink(0)
|
||
}
|
||
|
||
buildMonthWeeks(date) {
|
||
const month = date.getMonth();
|
||
const allDays = calendarUtil.buildMonthGrid(date, this.weekStartsOn);
|
||
const gridStart = allDays[0];
|
||
const gridEnd = calendarUtil.endOfDay(allDays[41]);
|
||
|
||
const colorsByDay = new Map();
|
||
allDays.forEach(day => colorsByDay.set(calendarUtil.toDateInput(day), []));
|
||
|
||
const colorByCalId = new Map((this.selectedCalendars || []).map(c => [c.id, c.color]));
|
||
const expanded = calendarUtil.expandRecurringEvents(this.events || [], gridStart, gridEnd);
|
||
expanded.filter(ev => {
|
||
const end = ev.all_day ? calendarUtil.endOfDay(ev.time_end) : ev.time_end;
|
||
return calendarUtil.rangesOverlap(ev.time_start, end, gridStart, gridEnd);
|
||
}).forEach(ev => {
|
||
const colors = (ev.calendars || [])
|
||
.map(id => colorByCalId.get(id))
|
||
.filter(Boolean);
|
||
if (colors.length === 0) return;
|
||
const effectiveEnd = ev.all_day ? calendarUtil.endOfDay(ev.time_end) : ev.time_end;
|
||
let cursor = calendarUtil.startOfDay(ev.time_start > gridStart ? ev.time_start : gridStart);
|
||
const end = effectiveEnd < gridEnd ? effectiveEnd : gridEnd;
|
||
while (cursor < end) {
|
||
const key = calendarUtil.toDateInput(cursor);
|
||
const arr = colorsByDay.get(key);
|
||
if (arr) {
|
||
colors.forEach(color => {
|
||
if (!arr.includes(color) && arr.length < 3) arr.push(color);
|
||
});
|
||
}
|
||
cursor = calendarUtil.addDays(cursor, 1);
|
||
}
|
||
});
|
||
|
||
const weeks = [];
|
||
for (let w = 0; w < 6; w++) {
|
||
weeks.push({
|
||
days: allDays.slice(w * 7, w * 7 + 7).map(day => ({
|
||
day,
|
||
isCurrentMonth: day.getMonth() === month,
|
||
calendarColors: colorsByDay.get(calendarUtil.toDateInput(day)) || []
|
||
}))
|
||
});
|
||
}
|
||
return weeks;
|
||
}
|
||
|
||
}
|
||
|
||
register(DesktopSidebar)
|