97 lines
5.4 KiB
JavaScript
97 lines
5.4 KiB
JavaScript
import { api, protectedFetch } from "./api.js?v=__ASSET_VERSION__";
|
|
import { initSideNav, loadNavUser } from "./nav.js?v=__ASSET_VERSION__";
|
|
const root = document.getElementById("workspace-content");
|
|
const n = (tag, text = "", cls = "") => Object.assign(document.createElement(tag), {textContent:text,className:cls});
|
|
const link = (text, href, cls = "") => Object.assign(n("a",text,cls), {href});
|
|
const now = new Date();
|
|
const initial = new URLSearchParams(location.search).get("month");
|
|
let month = /^20\d{2}-(0[1-9]|1[0-2])$/.test(initial || "") ? initial : `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}`;
|
|
let events = [], generation = 0;
|
|
const labels = {roast:"Roasted",cupping:"Cupped",brew:"Brewed",plan:"Plan created"};
|
|
const toolbar = n("div","","calendar-toolbar");
|
|
const previous = n("button","← Previous","ghost-btn"), next = n("button","Next →","ghost-btn"), current = n("button","This month","ghost-btn");
|
|
const heading = n("h2");
|
|
const filter = n("select","","field-input");
|
|
filter.setAttribute("aria-label","Activity type");
|
|
for (const [value,label] of [["","All activity"],...Object.entries(labels)]) filter.append(Object.assign(n("option",label),{value}));
|
|
toolbar.append(previous,heading,next,current,filter);
|
|
const status = n("p","","muted"); status.setAttribute("role","status");
|
|
const content = n("div");
|
|
const shortcuts = n("div","","calendar-shortcuts");
|
|
shortcuts.append(link("Plan a roast","/plans","primary-btn"),link("Log a roast","/roasts","ghost-btn"),link("Cup coffee","/cupping","ghost-btn"),link("Log a brew","/brews","ghost-btn"));
|
|
root.replaceChildren(shortcuts,toolbar,status,content);
|
|
function render() {
|
|
const selected = events.filter(e=>!filter.value || e.kind===filter.value);
|
|
status.textContent = `${selected.length} ${selected.length===1?"activity":"activities"} this month`;
|
|
content.replaceChildren();
|
|
const [year,m] = month.split("-").map(Number);
|
|
heading.textContent = new Date(year,m-1,1).toLocaleDateString(undefined,{month:"long",year:"numeric"});
|
|
const grid = n("div","","activity-calendar");
|
|
for (const day of ["Sun","Mon","Tue","Wed","Thu","Fri","Sat"]) grid.append(n("div",day,"calendar-weekday"));
|
|
const offset = new Date(year,m-1,1).getDay(), count = new Date(year,m,0).getDate();
|
|
for(let i=0;i<offset;i++) grid.append(n("div","","calendar-day calendar-padding"));
|
|
const today = `${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}-${String(now.getDate()).padStart(2,"0")}`;
|
|
for(let day=1;day<=count;day++) {
|
|
const key = month+"-"+String(day).padStart(2,"0");
|
|
const cell = n("section","","calendar-day"+(key===today?" calendar-today":""));
|
|
const title = n("h3",String(day));
|
|
title.setAttribute("aria-label",new Date(year,m-1,day).toLocaleDateString(undefined,{dateStyle:"full"}));
|
|
cell.append(title);
|
|
for(const event of selected.filter(e=>e.date===key)) {
|
|
const a = link(labels[event.kind]+" · "+event.title,event.href,"calendar-event event-"+event.kind);
|
|
a.title = a.textContent;
|
|
cell.append(a);
|
|
}
|
|
grid.append(cell);
|
|
}
|
|
for(let i=(offset+count)%7;i>0 && i<7;i++) grid.append(n("div","","calendar-day calendar-padding"));
|
|
content.append(grid);
|
|
const agenda = n("section","","calendar-agenda");
|
|
agenda.append(n("h2","Activity log"));
|
|
if(!selected.length) agenda.append(n("p","No activity this month. Log a roast, cupping or brew to see it here."));
|
|
let lastDate;
|
|
for(const event of [...selected].reverse()) {
|
|
if(event.date!==lastDate) {
|
|
agenda.append(n("h3",new Date(event.date+"T12:00:00").toLocaleDateString(undefined,{weekday:"long",month:"short",day:"numeric"})));
|
|
lastDate=event.date;
|
|
}
|
|
const row = n("div","","calendar-log-row");
|
|
row.append(n("span",labels[event.kind],"calendar-kind event-"+event.kind),link(event.title,event.href));
|
|
if(event.planHref) row.append(link("View plan",event.planHref,"muted"));
|
|
if(event.dateSource==="logged") row.append(n("small","Date logged","muted"));
|
|
agenda.append(row);
|
|
}
|
|
content.append(agenda);
|
|
}
|
|
async function load() {
|
|
const token = ++generation;
|
|
filter.disabled = true;
|
|
events = [];
|
|
status.textContent="Loading activity…";
|
|
content.replaceChildren();
|
|
const [year,m] = month.split("-").map(Number);
|
|
heading.textContent=new Date(year,m-1,1).toLocaleDateString(undefined,{month:"long",year:"numeric"});
|
|
previous.disabled=month==="2000-01"; next.disabled=month==="2099-12";
|
|
history.replaceState(null,"","/home?month="+month);
|
|
try {
|
|
const data = await api("/api/activity?"+new URLSearchParams({month,timeZone:Intl.DateTimeFormat().resolvedOptions().timeZone}));
|
|
if(token!==generation)return;
|
|
events=data.events; filter.disabled = false; render();
|
|
} catch(error) {
|
|
if(token!==generation)return;
|
|
status.textContent="Could not load activity. "+error.message;
|
|
const retry=n("button","Retry","ghost-btn");retry.onclick=load;content.append(retry);
|
|
}
|
|
}
|
|
function move(delta) {
|
|
const [year,m]=month.split("-").map(Number), date=new Date(year,m-1+delta,1);
|
|
month=`${date.getFullYear()}-${String(date.getMonth()+1).padStart(2,"0")}`; load();
|
|
}
|
|
previous.onclick=()=>move(-1);next.onclick=()=>move(1);
|
|
current.onclick=()=>{month=`${now.getFullYear()}-${String(now.getMonth()+1).padStart(2,"0")}`;load();};
|
|
filter.onchange=()=>{if(!status.textContent.startsWith("Loading"))render();};
|
|
initSideNav();
|
|
await loadNavUser();
|
|
document.getElementById("btn-logout").onclick=async()=>{await protectedFetch("/api/auth/logout",{method:"POST"});location.assign("/");};
|
|
await load();
|