fix parent, teacher and admin pages
This commit is contained in:
@@ -0,0 +1,663 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Teacher Calendar</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #f4efe6;
|
||||
--panel: #fffaf2;
|
||||
--panel-strong: #f0e3ce;
|
||||
--ink: #1d2a39;
|
||||
--muted: #6b7280;
|
||||
--accent: #0f766e;
|
||||
--accent-soft: #d4f4ef;
|
||||
--line: #d8ccb8;
|
||||
--shadow: 0 18px 45px rgba(38, 32, 24, 0.12);
|
||||
--today: #f59e0b;
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
font-family: Georgia, "Times New Roman", serif;
|
||||
color: var(--ink);
|
||||
background:
|
||||
radial-gradient(circle at top left, rgba(15, 118, 110, 0.15), transparent 35%),
|
||||
radial-gradient(circle at top right, rgba(245, 158, 11, 0.18), transparent 28%),
|
||||
linear-gradient(180deg, #f7f1e7 0%, #efe7da 100%);
|
||||
}
|
||||
|
||||
.shell {
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
padding: 32px 20px 48px;
|
||||
}
|
||||
|
||||
.hero {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.9fr);
|
||||
align-items: end;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.hero-card,
|
||||
.summary-card,
|
||||
.calendar-card,
|
||||
.agenda-card {
|
||||
background: rgba(255, 250, 242, 0.92);
|
||||
border: 1px solid rgba(216, 204, 184, 0.9);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
.hero-card {
|
||||
border-radius: 28px;
|
||||
padding: 28px;
|
||||
}
|
||||
|
||||
.eyebrow {
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.18em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0 0 8px;
|
||||
font-size: clamp(2rem, 4vw, 3.6rem);
|
||||
line-height: 0.94;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.hero p,
|
||||
.summary-meta,
|
||||
.agenda-empty,
|
||||
.legend span,
|
||||
.calendar-note,
|
||||
.month-caption {
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
border-radius: 24px;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.stat {
|
||||
background: var(--panel-strong);
|
||||
border-radius: 18px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.stat strong {
|
||||
display: block;
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.layout {
|
||||
display: grid;
|
||||
gap: 22px;
|
||||
grid-template-columns: minmax(0, 1.65fr) minmax(300px, 0.95fr);
|
||||
}
|
||||
|
||||
.calendar-card,
|
||||
.agenda-card {
|
||||
border-radius: 28px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.calendar-header,
|
||||
.agenda-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 22px 24px;
|
||||
border-bottom: 1px solid var(--line);
|
||||
background: rgba(240, 227, 206, 0.45);
|
||||
}
|
||||
|
||||
.calendar-header h2,
|
||||
.agenda-header h2 {
|
||||
margin: 0;
|
||||
font-size: 1.35rem;
|
||||
}
|
||||
|
||||
.calendar-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
button {
|
||||
appearance: none;
|
||||
border: 0;
|
||||
border-radius: 999px;
|
||||
cursor: pointer;
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.nav-button,
|
||||
.today-button {
|
||||
padding: 10px 14px;
|
||||
background: #e8ddcb;
|
||||
color: var(--ink);
|
||||
}
|
||||
|
||||
.today-button {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.calendar-body {
|
||||
padding: 18px 18px 22px;
|
||||
}
|
||||
|
||||
.weekdays,
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, minmax(0, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.weekdays {
|
||||
padding: 0 6px 12px;
|
||||
font-size: 0.82rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.day {
|
||||
min-height: 132px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
padding: 12px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid rgba(216, 204, 184, 0.85);
|
||||
background: rgba(255, 255, 255, 0.74);
|
||||
transition: transform 140ms ease, box-shadow 140ms ease, border-color 140ms ease;
|
||||
}
|
||||
|
||||
.day:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 14px 28px rgba(54, 46, 33, 0.08);
|
||||
}
|
||||
|
||||
.day.muted {
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
.day.today {
|
||||
border-color: rgba(245, 158, 11, 0.9);
|
||||
box-shadow: inset 0 0 0 1px rgba(245, 158, 11, 0.2);
|
||||
}
|
||||
|
||||
.day.selected {
|
||||
border-color: var(--accent);
|
||||
box-shadow: inset 0 0 0 1px rgba(15, 118, 110, 0.22);
|
||||
background: #fffdfa;
|
||||
}
|
||||
|
||||
.day-number {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
border-radius: 50%;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.today .day-number {
|
||||
background: rgba(245, 158, 11, 0.18);
|
||||
color: #9a5600;
|
||||
}
|
||||
|
||||
.event-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.event-pill {
|
||||
display: block;
|
||||
padding: 7px 9px;
|
||||
border-radius: 12px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.2;
|
||||
color: #12202f;
|
||||
background: #dbe7f5;
|
||||
border-left: 4px solid #8aa6cf;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.event-pill.more {
|
||||
background: var(--panel-strong);
|
||||
border-left-color: #9c8360;
|
||||
color: #5c4730;
|
||||
}
|
||||
|
||||
.legend {
|
||||
display: flex;
|
||||
gap: 14px;
|
||||
flex-wrap: wrap;
|
||||
margin-top: 16px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.legend-item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.legend-swatch {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
}
|
||||
|
||||
.agenda-body {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.agenda-date {
|
||||
margin: 0 0 14px;
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
.agenda-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.agenda-item {
|
||||
padding: 16px;
|
||||
border-radius: 18px;
|
||||
border: 1px solid rgba(216, 204, 184, 0.85);
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.agenda-item h3 {
|
||||
margin: 0 0 8px;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.agenda-item p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
white-space: pre-line;
|
||||
}
|
||||
|
||||
.agenda-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
margin-bottom: 10px;
|
||||
color: #5e5c52;
|
||||
}
|
||||
|
||||
.agenda-dot {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 999px;
|
||||
background: #8aa6cf;
|
||||
}
|
||||
|
||||
.calendar-note {
|
||||
margin-top: 10px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.hero,
|
||||
.layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.shell {
|
||||
padding: 16px 12px 28px;
|
||||
}
|
||||
|
||||
.weekdays,
|
||||
.grid {
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.day {
|
||||
min-height: 108px;
|
||||
padding: 10px 8px;
|
||||
border-radius: 16px;
|
||||
}
|
||||
|
||||
.event-pill {
|
||||
padding: 6px 7px;
|
||||
font-size: 0.72rem;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.calendar-header,
|
||||
.agenda-header {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="shell">
|
||||
<section class="hero">
|
||||
<div class="hero-card hero">
|
||||
<div>
|
||||
<div class="eyebrow">Teacher Portal</div>
|
||||
<h1>School Calendar</h1>
|
||||
<p>Monthly view for teacher-visible events, no-school days, and staff-only items.</p>
|
||||
</div>
|
||||
</div>
|
||||
<aside class="summary-card">
|
||||
<div class="eyebrow">Current Scope</div>
|
||||
<div class="summary-meta">School Year {{ $schoolYear }} · Default Semester {{ $defaultSemester }}</div>
|
||||
<div class="summary-grid">
|
||||
<div class="stat">
|
||||
<strong id="stat-total">0</strong>
|
||||
<span>Total events</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<strong id="stat-no-school">0</strong>
|
||||
<span>No-school days</span>
|
||||
</div>
|
||||
<div class="stat">
|
||||
<strong id="stat-staff">0</strong>
|
||||
<span>Staff-only events</span>
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
|
||||
<section class="layout">
|
||||
<article class="calendar-card">
|
||||
<div class="calendar-header">
|
||||
<div>
|
||||
<h2 id="month-label">Month</h2>
|
||||
<div class="month-caption" id="month-caption">Teacher schedule overview</div>
|
||||
</div>
|
||||
<div class="calendar-toolbar">
|
||||
<button class="nav-button" id="prev-month" type="button">Prev</button>
|
||||
<button class="today-button" id="jump-today" type="button">Today</button>
|
||||
<button class="nav-button" id="next-month" type="button">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="calendar-body">
|
||||
<div class="weekdays">
|
||||
<div>Sun</div>
|
||||
<div>Mon</div>
|
||||
<div>Tue</div>
|
||||
<div>Wed</div>
|
||||
<div>Thu</div>
|
||||
<div>Fri</div>
|
||||
<div>Sat</div>
|
||||
</div>
|
||||
<div class="grid" id="calendar-grid"></div>
|
||||
<div class="legend">
|
||||
<div class="legend-item"><span class="legend-swatch" style="background:#dbe7f5"></span><span>General event</span></div>
|
||||
<div class="legend-item"><span class="legend-swatch" style="background:#ffc107"></span><span>No school</span></div>
|
||||
<div class="legend-item"><span class="legend-swatch" style="background:#28a745"></span><span>Staff only</span></div>
|
||||
<div class="legend-item"><span class="legend-swatch" style="background:#6f42c1"></span><span>Meeting</span></div>
|
||||
</div>
|
||||
<div class="calendar-note">Select any day to inspect the event list for that date.</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<aside class="agenda-card">
|
||||
<div class="agenda-header">
|
||||
<h2>Day Agenda</h2>
|
||||
<div class="month-caption" id="agenda-count">0 events</div>
|
||||
</div>
|
||||
<div class="agenda-body">
|
||||
<h3 class="agenda-date" id="agenda-date"></h3>
|
||||
<div class="agenda-list" id="agenda-list"></div>
|
||||
<p class="agenda-empty" id="agenda-empty">No events scheduled for this day.</p>
|
||||
</div>
|
||||
</aside>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const rawEvents = @json($events);
|
||||
|
||||
const events = rawEvents.map((event) => {
|
||||
const start = String(event.start || '').slice(0, 10);
|
||||
return {
|
||||
...event,
|
||||
start,
|
||||
backgroundColor: event.backgroundColor || '#8aa6cf',
|
||||
};
|
||||
});
|
||||
|
||||
const eventsByDate = events.reduce((map, event) => {
|
||||
if (!event.start) {
|
||||
return map;
|
||||
}
|
||||
if (!map[event.start]) {
|
||||
map[event.start] = [];
|
||||
}
|
||||
map[event.start].push(event);
|
||||
return map;
|
||||
}, {});
|
||||
|
||||
Object.values(eventsByDate).forEach((list) => {
|
||||
list.sort((left, right) => String(left.title || '').localeCompare(String(right.title || '')));
|
||||
});
|
||||
|
||||
const monthLabel = document.getElementById('month-label');
|
||||
const monthCaption = document.getElementById('month-caption');
|
||||
const calendarGrid = document.getElementById('calendar-grid');
|
||||
const agendaDate = document.getElementById('agenda-date');
|
||||
const agendaList = document.getElementById('agenda-list');
|
||||
const agendaEmpty = document.getElementById('agenda-empty');
|
||||
const agendaCount = document.getElementById('agenda-count');
|
||||
const today = new Date();
|
||||
const todayKey = toDateKey(today);
|
||||
|
||||
const availableDates = Object.keys(eventsByDate).sort();
|
||||
const initialDate = availableDates.includes(todayKey) ? todayKey : (availableDates[0] || todayKey);
|
||||
|
||||
let selectedDate = initialDate;
|
||||
let visibleMonth = new Date(initialDate + 'T12:00:00');
|
||||
visibleMonth.setDate(1);
|
||||
|
||||
document.getElementById('prev-month').addEventListener('click', () => {
|
||||
visibleMonth = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() - 1, 1);
|
||||
renderCalendar();
|
||||
});
|
||||
|
||||
document.getElementById('next-month').addEventListener('click', () => {
|
||||
visibleMonth = new Date(visibleMonth.getFullYear(), visibleMonth.getMonth() + 1, 1);
|
||||
renderCalendar();
|
||||
});
|
||||
|
||||
document.getElementById('jump-today').addEventListener('click', () => {
|
||||
selectedDate = todayKey;
|
||||
visibleMonth = new Date(today.getFullYear(), today.getMonth(), 1);
|
||||
renderCalendar();
|
||||
renderAgenda();
|
||||
});
|
||||
|
||||
function renderCalendar() {
|
||||
calendarGrid.innerHTML = '';
|
||||
|
||||
const year = visibleMonth.getFullYear();
|
||||
const month = visibleMonth.getMonth();
|
||||
const firstDay = new Date(year, month, 1);
|
||||
const lastDay = new Date(year, month + 1, 0);
|
||||
const leading = firstDay.getDay();
|
||||
const totalCells = Math.ceil((leading + lastDay.getDate()) / 7) * 7;
|
||||
const startDate = new Date(year, month, 1 - leading);
|
||||
|
||||
monthLabel.textContent = new Intl.DateTimeFormat('en-US', { month: 'long', year: 'numeric' }).format(firstDay);
|
||||
monthCaption.textContent = `${countMonthEvents(year, month)} event${countMonthEvents(year, month) === 1 ? '' : 's'} in view`;
|
||||
|
||||
for (let i = 0; i < totalCells; i += 1) {
|
||||
const cellDate = new Date(startDate.getFullYear(), startDate.getMonth(), startDate.getDate() + i);
|
||||
const dateKey = toDateKey(cellDate);
|
||||
const dayEvents = eventsByDate[dateKey] || [];
|
||||
const isCurrentMonth = cellDate.getMonth() === month;
|
||||
const isToday = dateKey === todayKey;
|
||||
const isSelected = dateKey === selectedDate;
|
||||
|
||||
const cell = document.createElement('button');
|
||||
cell.type = 'button';
|
||||
cell.className = `day${isCurrentMonth ? '' : ' muted'}${isToday ? ' today' : ''}${isSelected ? ' selected' : ''}`;
|
||||
cell.addEventListener('click', () => {
|
||||
selectedDate = dateKey;
|
||||
if (!isCurrentMonth) {
|
||||
visibleMonth = new Date(cellDate.getFullYear(), cellDate.getMonth(), 1);
|
||||
}
|
||||
renderCalendar();
|
||||
renderAgenda();
|
||||
});
|
||||
|
||||
const number = document.createElement('span');
|
||||
number.className = 'day-number';
|
||||
number.textContent = String(cellDate.getDate());
|
||||
cell.appendChild(number);
|
||||
|
||||
const stack = document.createElement('div');
|
||||
stack.className = 'event-stack';
|
||||
|
||||
dayEvents.slice(0, 3).forEach((event) => {
|
||||
const pill = document.createElement('span');
|
||||
pill.className = 'event-pill';
|
||||
pill.textContent = event.title || 'Untitled';
|
||||
pill.style.borderLeftColor = event.backgroundColor || '#8aa6cf';
|
||||
pill.style.background = withAlpha(event.backgroundColor || '#dbe7f5', 0.18);
|
||||
stack.appendChild(pill);
|
||||
});
|
||||
|
||||
if (dayEvents.length > 3) {
|
||||
const more = document.createElement('span');
|
||||
more.className = 'event-pill more';
|
||||
more.textContent = `+${dayEvents.length - 3} more`;
|
||||
stack.appendChild(more);
|
||||
}
|
||||
|
||||
cell.appendChild(stack);
|
||||
calendarGrid.appendChild(cell);
|
||||
}
|
||||
}
|
||||
|
||||
function renderAgenda() {
|
||||
const dayEvents = eventsByDate[selectedDate] || [];
|
||||
agendaDate.textContent = new Intl.DateTimeFormat('en-US', {
|
||||
weekday: 'long',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
year: 'numeric',
|
||||
}).format(new Date(selectedDate + 'T12:00:00'));
|
||||
agendaList.innerHTML = '';
|
||||
agendaCount.textContent = `${dayEvents.length} event${dayEvents.length === 1 ? '' : 's'}`;
|
||||
agendaEmpty.style.display = dayEvents.length === 0 ? 'block' : 'none';
|
||||
|
||||
dayEvents.forEach((event) => {
|
||||
const item = document.createElement('article');
|
||||
item.className = 'agenda-item';
|
||||
|
||||
const tag = document.createElement('div');
|
||||
tag.className = 'agenda-tag';
|
||||
const dot = document.createElement('span');
|
||||
dot.className = 'agenda-dot';
|
||||
dot.style.background = event.backgroundColor || '#8aa6cf';
|
||||
tag.appendChild(dot);
|
||||
tag.append(document.createTextNode(buildTagLabel(event)));
|
||||
|
||||
const title = document.createElement('h3');
|
||||
title.textContent = event.title || 'Untitled';
|
||||
|
||||
const description = document.createElement('p');
|
||||
description.textContent = event.description || 'No extra details provided.';
|
||||
|
||||
item.appendChild(tag);
|
||||
item.appendChild(title);
|
||||
item.appendChild(description);
|
||||
agendaList.appendChild(item);
|
||||
});
|
||||
}
|
||||
|
||||
function buildTagLabel(event) {
|
||||
const props = event.extendedProps || {};
|
||||
if (String(event.id || '').startsWith('pm-')) {
|
||||
return 'Meeting';
|
||||
}
|
||||
if (props.no_school) {
|
||||
return 'No School';
|
||||
}
|
||||
if (props.notify_teacher && props.notify_admin && !props.notify_parent) {
|
||||
return 'Staff Only';
|
||||
}
|
||||
return 'School Event';
|
||||
}
|
||||
|
||||
function countMonthEvents(year, month) {
|
||||
return events.filter((event) => {
|
||||
const date = new Date(event.start + 'T12:00:00');
|
||||
return date.getFullYear() === year && date.getMonth() === month;
|
||||
}).length;
|
||||
}
|
||||
|
||||
function toDateKey(date) {
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}`;
|
||||
}
|
||||
|
||||
function withAlpha(hex, alpha) {
|
||||
const normalized = hex.replace('#', '');
|
||||
if (normalized.length !== 6) {
|
||||
return 'rgba(219, 231, 245, 0.18)';
|
||||
}
|
||||
const r = parseInt(normalized.slice(0, 2), 16);
|
||||
const g = parseInt(normalized.slice(2, 4), 16);
|
||||
const b = parseInt(normalized.slice(4, 6), 16);
|
||||
return `rgba(${r}, ${g}, ${b}, ${alpha})`;
|
||||
}
|
||||
|
||||
document.getElementById('stat-total').textContent = String(events.length);
|
||||
document.getElementById('stat-no-school').textContent = String(events.filter((event) => event.extendedProps && event.extendedProps.no_school).length);
|
||||
document.getElementById('stat-staff').textContent = String(events.filter((event) => {
|
||||
const props = event.extendedProps || {};
|
||||
return props.notify_teacher && props.notify_admin && !props.notify_parent;
|
||||
}).length);
|
||||
|
||||
renderCalendar();
|
||||
renderAgenda();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user