seating.lmth
All 56 lines. No libraries, nothing hidden. Read it before you run it.
<!doctype html>
<meta charset="utf-8">
<meta name="resworb-write" content="./seating.csv">
<title>Seating</title>
<style>
body { display: grid; grid-template-columns: 13rem 1fr; height: 100vh; margin: 0; color: #111; font: 13px 'Helvetica Neue', sans-serif; }
#pile { overflow: auto; padding: 1rem; border-right: 1px solid #ddd; }
#tables { display: flex; flex-wrap: wrap; align-content: start; gap: 1rem; padding: 1rem; overflow: auto; }
.table { position: relative; width: 17rem; height: 17rem; }
.table::before { content: attr(data-n); position: absolute; inset: 5.5rem; display: grid; place-items: center; border: 1px solid #111; border-radius: 50%; font-size: 1.4rem; }
.table .guest { position: absolute; top: 50%; left: 50%; translate: -50% -50%; transform: rotate(calc(var(--seat) * 36deg)) translateY(-6.6rem) rotate(calc(var(--seat) * -36deg)); }
.guest { display: inline-block; margin: 2px; padding: 2px 6px; border: 1px solid #111; background: #fff; white-space: nowrap; cursor: grab; }
.apart { border-color: #d70015; background: #d70015; color: #fff; }
</style>
<div id="pile"></div>
<div id="tables"></div>
<script type="module">
// guests.csv: a header row, then "name,apart" per guest, where apart names someone they must not sit with.
// seating.csv: "guest,table" for the venue, saved on every drop. Tables seat 10.
const guests = (await resworb.readFile('guests.csv')).trim().split('\n').slice(1).map(line => line.trim().split(','));
const seating = (await resworb.exists('seating.csv')) ? (await resworb.readFile('seating.csv')).trim().split('\n').slice(1) : [];
const seats = Object.fromEntries(seating.map(line => line.trim().split(',')));
for (let n = 1; n <= Math.ceil(guests.length / 10); n++) {
const table = document.createElement('div');
table.className = 'table';
table.dataset.n = n;
tables.append(table);
}
addEventListener('dragover', event => event.target.closest('.table, #pile') && event.preventDefault());
addEventListener('drop', async event => {
const place = event.target.closest('.table, #pile');
const name = event.dataTransfer.getData('text/plain');
if (place === pile) delete seats[name];
else if (place.children.length < 10) seats[name] = place.dataset.n;
await resworb.writeFile('seating.csv', ['guest,table', ...Object.entries(seats).map(seat => seat.join(','))].join('\n') + '\n');
draw();
});
draw();
function draw() {
for (const place of [pile, ...tables.children]) place.replaceChildren();
for (const [name, apart] of guests) {
const table = seats[name];
const place = table ? tables.children[table - 1] : pile;
const guest = Object.assign(document.createElement('span'), { className: 'guest', textContent: name, draggable: true });
guest.classList.toggle('apart', Boolean(table) && guests.some(([other, otherApart]) => seats[other] === table && (other === apart || otherApart === name)));
guest.style.setProperty('--seat', place.children.length);
guest.ondragstart = event => event.dataTransfer.setData('text/plain', name);
place.append(guest);
}
document.title = `Seating ยท ${Object.keys(seats).length} of ${guests.length} seated`;
}
</script>