mirror of
https://github.com/nfonteyne/octane-website.git
synced 2026-09-03 23:24:48 +02:00
update: better colors for calendar light mode and see rejected rehersal in calendar
This commit is contained in:
parent
a3d62200ce
commit
d0e9dfb62e
4 changed files with 55 additions and 18 deletions
|
|
@ -15,6 +15,11 @@
|
|||
--radius: 14px;
|
||||
--radius-sm: 8px;
|
||||
--shadow: 0 1px 2px rgba(32, 31, 43, 0.04), 0 4px 16px rgba(32, 31, 43, 0.06);
|
||||
/* Higher than the dark-mode value below — pale accent tints on a white
|
||||
surface read as barely-there in light mode, so light needs a stronger mix
|
||||
to hit the same perceived contrast dark mode gets "for free" against a
|
||||
dark surface. */
|
||||
--cal-marker-mix: 38%;
|
||||
font-family: "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
|
||||
}
|
||||
|
||||
|
|
@ -34,6 +39,7 @@
|
|||
--success: #4fd190;
|
||||
--success-bg: #163828;
|
||||
--shadow: 0 1px 2px rgba(0, 0, 0, 0.2), 0 8px 24px rgba(0, 0, 0, 0.35);
|
||||
--cal-marker-mix: 24%;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -51,6 +57,7 @@
|
|||
--danger-bg: #3a201f;
|
||||
--success: #4fd190;
|
||||
--success-bg: #163828;
|
||||
--cal-marker-mix: 24%;
|
||||
}
|
||||
|
||||
:root[data-theme="light"] {
|
||||
|
|
@ -67,6 +74,7 @@
|
|||
--danger-bg: #fdecea;
|
||||
--success: #1e8e5a;
|
||||
--success-bg: #e8f7ef;
|
||||
--cal-marker-mix: 38%;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
|
|
@ -1567,6 +1575,10 @@ button.calendar-filters-close { display: none; }
|
|||
.vote-badge.accept { border-left: 3px solid var(--success); }
|
||||
.vote-badge.reject { border-left: 3px solid var(--danger); }
|
||||
|
||||
.vote-badge.rehearsal-status-confirmed { border-left: 3px solid var(--accent); }
|
||||
.vote-badge.rehearsal-status-suggested { border-left: 3px solid var(--success); }
|
||||
.vote-badge.rehearsal-status-rejected { border-left: 3px solid var(--danger); }
|
||||
|
||||
.rehearsal-voted-section { margin-top: 0.75rem; }
|
||||
|
||||
.rehearsal-voted-section > summary {
|
||||
|
|
@ -1595,16 +1607,22 @@ button.calendar-filters-close { display: none; }
|
|||
|
||||
/* Concert/rehearsal markers take priority over the availability heat colors
|
||||
above (same specificity, later in the cascade) — a scheduled date matters
|
||||
more than who's free that day. */
|
||||
.cal-cell.has-rehearsal { background: color-mix(in srgb, var(--accent) 20%, var(--surface)); border-color: var(--accent); }
|
||||
.cal-cell.rehearsal-confirmed { background: color-mix(in srgb, var(--success) 20%, var(--surface)); border-color: var(--success); }
|
||||
.cal-cell.rehearsal-suggested { background: color-mix(in srgb, var(--accent) 20%, var(--surface)); border-color: var(--accent); }
|
||||
.cal-cell.has-concert { background: color-mix(in srgb, var(--accent-2) 20%, var(--surface)); border-color: var(--accent-2); }
|
||||
.cal-cell.has-rehearsal.has-concert {
|
||||
background: linear-gradient(135deg, color-mix(in srgb, var(--accent) 20%, var(--surface)), color-mix(in srgb, var(--accent-2) 20%, var(--surface)));
|
||||
}
|
||||
more than who's free that day.
|
||||
Colors: confirmed rehearsals are purple (accent) — the "settled, book it"
|
||||
state; still-pending suggestions are green — an invitation to vote, not yet
|
||||
settled; a rejected rehearsal (host said no, or too many refusals) is red. */
|
||||
.cal-cell.rehearsal-confirmed { background: color-mix(in srgb, var(--accent) var(--cal-marker-mix), var(--surface)); border-color: var(--accent); }
|
||||
.cal-cell.rehearsal-suggested { background: color-mix(in srgb, var(--success) var(--cal-marker-mix), var(--surface)); border-color: var(--success); }
|
||||
.cal-cell.rehearsal-rejected { background: color-mix(in srgb, var(--danger) var(--cal-marker-mix), var(--surface)); border-color: var(--danger); }
|
||||
.cal-cell.has-concert { background: color-mix(in srgb, var(--accent-2) var(--cal-marker-mix), var(--surface)); border-color: var(--accent-2); }
|
||||
.cal-cell.rehearsal-confirmed.has-concert {
|
||||
background: linear-gradient(135deg, color-mix(in srgb, var(--success) 20%, var(--surface)), color-mix(in srgb, var(--accent-2) 20%, var(--surface)));
|
||||
background: linear-gradient(135deg, color-mix(in srgb, var(--accent) var(--cal-marker-mix), var(--surface)), color-mix(in srgb, var(--accent-2) var(--cal-marker-mix), var(--surface)));
|
||||
}
|
||||
.cal-cell.rehearsal-suggested.has-concert {
|
||||
background: linear-gradient(135deg, color-mix(in srgb, var(--success) var(--cal-marker-mix), var(--surface)), color-mix(in srgb, var(--accent-2) var(--cal-marker-mix), var(--surface)));
|
||||
}
|
||||
.cal-cell.rehearsal-rejected.has-concert {
|
||||
background: linear-gradient(135deg, color-mix(in srgb, var(--danger) var(--cal-marker-mix), var(--surface)), color-mix(in srgb, var(--accent-2) var(--cal-marker-mix), var(--surface)));
|
||||
}
|
||||
|
||||
/* ---------- Concerts: upcoming list + detail actions ---------- */
|
||||
|
|
|
|||
|
|
@ -106,6 +106,7 @@ function concertLinksHtml(concert) {
|
|||
|
||||
function rehearsalStatusLabel(r) {
|
||||
if (r.status === 'confirmed') return 'Répétition confirmée';
|
||||
if (r.status === 'rejected') return 'Répétition refusée';
|
||||
const accepted = r.votes.filter((v) => v.vote === 'accept').length;
|
||||
return `Suggestion de répétition (${accepted}/${r.confirmThreshold} votes)`;
|
||||
}
|
||||
|
|
@ -216,10 +217,14 @@ function renderCalendar() {
|
|||
|
||||
const rehearsal = rehearsalsByDate.get(isoDate(date));
|
||||
if (rehearsal) {
|
||||
const isConfirmed = rehearsal.status === 'confirmed';
|
||||
cell.classList.add('has-rehearsal', isConfirmed ? 'rehearsal-confirmed' : 'rehearsal-suggested');
|
||||
const statusTitles = {
|
||||
confirmed: 'Répétition confirmée',
|
||||
rejected: 'Répétition refusée',
|
||||
suggested: 'Répétition proposée (en attente de votes)',
|
||||
};
|
||||
cell.classList.add('has-rehearsal', `rehearsal-${rehearsal.status}`);
|
||||
const badge = document.createElement('span');
|
||||
badge.title = isConfirmed ? 'Répétition confirmée' : 'Répétition proposée (en attente de votes)';
|
||||
badge.title = statusTitles[rehearsal.status] || statusTitles.suggested;
|
||||
badge.textContent = '🎸';
|
||||
badges.appendChild(badge);
|
||||
}
|
||||
|
|
@ -522,7 +527,7 @@ function rehearsalRowTemplate(r) {
|
|||
return `
|
||||
<div class="rehearsal-row" data-rehearsal-id="${r.id}">
|
||||
<div>
|
||||
<span class="vote-badge ${r.status === 'confirmed' ? 'accept' : ''}">${escapeHtml(rehearsalStatusLabel(r))}</span>
|
||||
<span class="vote-badge rehearsal-status-${r.status}">${escapeHtml(rehearsalStatusLabel(r))}</span>
|
||||
<div class="card-title">${formatDatetime(r.startsAt)} – ${formatTime(r.endsAt)}</div>
|
||||
<div class="card-subtitle">${r.location ? `${escapeHtml(r.location)} · ` : ''}Proposée par ${escapeHtml(r.proposedByName)}</div>
|
||||
${rehearsalAcceptedByHtml(r)}
|
||||
|
|
|
|||
|
|
@ -40,8 +40,11 @@ function foldLine(line) {
|
|||
return chunks.map((chunk, i) => (i === 0 ? chunk : ` ${chunk}`)).join('\r\n');
|
||||
}
|
||||
|
||||
// Returns null for a rejected rehearsal — those are dropped entirely from the
|
||||
// shared feed (see buildRehearsalsFeed) rather than shown as tentative/cancelled.
|
||||
function rehearsalToEvent(rehearsal, { threshold, hostUserId, baseUrl, allUsers }) {
|
||||
const status = computeRehearsalStatus(rehearsal.votes, threshold, hostUserId);
|
||||
if (status === 'rejected') return null;
|
||||
const icsStatus = status === 'confirmed' ? 'CONFIRMED' : 'TENTATIVE';
|
||||
const summary = status === 'confirmed' ? 'Répétition' : 'Répétition (proposition)';
|
||||
const voteUrl = `${baseUrl}/calendar.html?rehearsalId=${rehearsal.id}`;
|
||||
|
|
@ -82,7 +85,10 @@ function buildRehearsalsFeed(rehearsals, { threshold, hostUserId, baseUrl, allUs
|
|||
'CALSCALE:GREGORIAN',
|
||||
'METHOD:PUBLISH',
|
||||
'X-WR-CALNAME:Répétitions Octane',
|
||||
...rehearsals.flatMap((r) => rehearsalToEvent(r, { threshold, hostUserId, baseUrl, allUsers })),
|
||||
...rehearsals
|
||||
.map((r) => rehearsalToEvent(r, { threshold, hostUserId, baseUrl, allUsers }))
|
||||
.filter(Boolean)
|
||||
.flat(),
|
||||
'END:VCALENDAR',
|
||||
];
|
||||
return lines.map(foldLine).join('\r\n');
|
||||
|
|
|
|||
|
|
@ -1,11 +1,19 @@
|
|||
// Shared between the API (rehearsals list) and the ICS feed so both agree on
|
||||
// when a proposed rehearsal counts as confirmed. When a host is configured,
|
||||
// their acceptance is mandatory: without the host there is nowhere to play,
|
||||
// so a rehearsal can never be "confirmed" on vote count alone.
|
||||
// when a proposed rehearsal counts as confirmed, rejected, or still a
|
||||
// suggestion. When a host is configured, their acceptance is mandatory:
|
||||
// without the host there is nowhere to play, so an explicit host refusal (or
|
||||
// too many "reject" votes) kills the rehearsal outright rather than leaving
|
||||
// it as a pending suggestion.
|
||||
function computeRehearsalStatus(votes, threshold, hostUserId) {
|
||||
const acceptCount = votes.filter((v) => v.vote === 'accept').length;
|
||||
const rejectCount = votes.filter((v) => v.vote === 'reject').length;
|
||||
const hostVote = hostUserId ? votes.find((v) => v.userId === hostUserId) : null;
|
||||
|
||||
if ((hostVote && hostVote.vote === 'reject') || rejectCount >= threshold) {
|
||||
return 'rejected';
|
||||
}
|
||||
if (acceptCount < threshold) return 'suggested';
|
||||
if (hostUserId && !votes.some((v) => v.userId === hostUserId && v.vote === 'accept')) {
|
||||
if (hostUserId && !(hostVote && hostVote.vote === 'accept')) {
|
||||
return 'suggested';
|
||||
}
|
||||
return 'confirmed';
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue