395 lines
No EOL
14 KiB
HTML
395 lines
No EOL
14 KiB
HTML
<!--
|
|
view.html: "View" page for who-when-where
|
|
|
|
For each users listed under the view, display:
|
|
* Name and profile picture
|
|
* Location (emoji)
|
|
* Availability (green-yellow-red)
|
|
for a certain time period (customisable via the interface).
|
|
|
|
Also displays a summary for each day, per location.
|
|
-->
|
|
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>3W - Who When Where?</title>
|
|
|
|
<link rel="stylesheet" href="main.css">
|
|
|
|
<style>
|
|
table#view_table {
|
|
border-collapse: collapse;
|
|
}
|
|
table#view_table th, table#view_table td {
|
|
padding: 1em 1.2em;
|
|
font-weight: normal;
|
|
}
|
|
table#view_table th:first-child {
|
|
text-align: right;
|
|
}
|
|
|
|
table#view_table thead tr, table#view_table tfoot tr {
|
|
background-color: var(--colour_primary);
|
|
color: var(--colour_primary_text);
|
|
text-align: left;
|
|
}
|
|
table#view_table thead th {
|
|
text-align: center;
|
|
vertical-align: bottom;
|
|
}
|
|
table#view_table thead th:first-child {
|
|
font-weight: bold;
|
|
}
|
|
table#view_table thead th.me {
|
|
font-weight: bold;
|
|
}
|
|
table#view_table thead th img {
|
|
display: block;
|
|
margin: auto;
|
|
margin-bottom: 1em;
|
|
border-radius: 50%;
|
|
max-width: 4em;
|
|
max-height: 4em;
|
|
}
|
|
|
|
table#view_table tbody tr {
|
|
border-bottom: 1px solid #dddddd;
|
|
}
|
|
table#view_table tbody th {
|
|
font-weight: bold;
|
|
text-align: right;
|
|
}
|
|
table#view_table tbody th span {
|
|
font-size: 0.8em;
|
|
font-weight: normal;
|
|
display: inline-block;
|
|
margin-left: 0.33em;
|
|
}
|
|
table#view_table tbody tr:nth-of-type(odd) {
|
|
background-color: var(--colour_lightgrey);
|
|
}
|
|
table#view_table tbody tr:nth-of-type(even) {
|
|
background-color: #ffffff;
|
|
}
|
|
table#view_table tbody td.me {
|
|
cursor: pointer;
|
|
}
|
|
|
|
table#view_table tbody td {
|
|
text-align: center;
|
|
}
|
|
|
|
table#view_table td .where {
|
|
font-size: 2.5em;
|
|
position: relative;
|
|
}
|
|
table#view_table td:has(.modifier) {
|
|
padding-right: 1.55em;
|
|
}
|
|
|
|
|
|
i.modifier {
|
|
font-style: normal;
|
|
font-size: 0.33em;
|
|
width: 0.33em;
|
|
position: absolute;
|
|
right: 0.33em;
|
|
}
|
|
i.modifier-top {
|
|
top: -0em;
|
|
}
|
|
i.modifier-bottom {
|
|
bottom: -0em;
|
|
}
|
|
i.modifier-cross::before {
|
|
display: block;
|
|
width: 1em;
|
|
position: absolute;
|
|
top: -0.25em;
|
|
text-align: center;
|
|
|
|
font-size: 1.5em;
|
|
content: "✗";
|
|
color: rgba(255, 0, 0, 0.7);
|
|
}
|
|
|
|
|
|
table#view_table tfoot {
|
|
font-style: italic;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<table id="view_table">
|
|
<thead>
|
|
<tr id="view_table_thead_headers">
|
|
<th id="view_table_thead_name"></th>
|
|
<!-- For-each group member -->
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<!-- For-each date -->
|
|
</tbody>
|
|
<tfoot>
|
|
<tr>
|
|
<th id="view_table_tfoot_total"></th>
|
|
|
|
<!-- Colspan = number of members -->
|
|
<th id="view_table_tfoot_filler"></th>
|
|
</tr>
|
|
</tfoot>
|
|
</table>
|
|
|
|
<p>
|
|
So far, this group has planned between
|
|
<span id="view_first_filled_date"></span>
|
|
and
|
|
<span id="view_last_filled_date"></span>.
|
|
</p>
|
|
|
|
<p>
|
|
<a href="index.html">Take me home!</a>
|
|
</p>
|
|
|
|
<template id="row_date">
|
|
<tr>
|
|
<th></th>
|
|
<!-- For-each group member = availability -->
|
|
</tr>
|
|
</template>
|
|
<template id="cell_availability">
|
|
<td>
|
|
<span class="where"></span>
|
|
</td>
|
|
</template>
|
|
<template id="modifier_cannot_host">
|
|
<i class="modifier modifier-top modifier-cross" title="Cannot host">🏠</i>
|
|
</template>
|
|
<template id="modifier_cannot_travel">
|
|
<i class="modifier modifier-bottom modifier-cross" title="Cannot travel">🚅</i>
|
|
</template>
|
|
|
|
<script>
|
|
var userUid = 1;
|
|
var viewData = {
|
|
view: {
|
|
name: "Le groupe des abeilles",
|
|
numberOfGroupMembers: 3,
|
|
firstFilledDate: "2023-05-06",
|
|
lastFilledDate: "2025-01-31"
|
|
},
|
|
members: [
|
|
{
|
|
uid: 1,
|
|
displayName: "Louis",
|
|
avatar: "https://static.david-david-studio.com/image/24511.jpg"
|
|
},
|
|
{
|
|
uid: 2,
|
|
displayName: "Mathilde",
|
|
avatar: "https://tinder.com/static/tinder.png"
|
|
},
|
|
{
|
|
uid: 3,
|
|
displayName: "Louis",
|
|
avatar: "https://www.aprifel.com/wp-content/uploads/2019/02/potiron.jpg"
|
|
}
|
|
],
|
|
dates: [
|
|
{
|
|
date: "2024-08-01",
|
|
membersAvailability: [
|
|
{
|
|
memberUid: 1,
|
|
location: "🇸🇪",
|
|
canHost: true,
|
|
canTravel: true
|
|
},
|
|
{
|
|
memberUid: 2,
|
|
location: "🗼",
|
|
canHost: true,
|
|
canTravel: true
|
|
},
|
|
{
|
|
memberUid: 3,
|
|
location: "🥨",
|
|
canHost: true,
|
|
canTravel: true
|
|
}
|
|
]
|
|
},
|
|
{
|
|
date: "2024-08-02",
|
|
membersAvailability: [
|
|
{
|
|
memberUid: 1,
|
|
location: "🗼",
|
|
canHost: false,
|
|
canTravel: true
|
|
},
|
|
{
|
|
memberUid: 2,
|
|
location: "🗼",
|
|
canHost: true,
|
|
canTravel: true
|
|
},
|
|
{
|
|
memberUid: 3,
|
|
location: "🥨",
|
|
canHost: true,
|
|
canTravel: false
|
|
}
|
|
]
|
|
}
|
|
]
|
|
};
|
|
|
|
function data2dom(data) {
|
|
// Retrieve common DOM elements
|
|
var headerRow = document.getElementById("view_table_thead_headers");
|
|
var tbody = document.getElementById("view_table").querySelector("tbody");
|
|
|
|
// Prepared data
|
|
var numberOfDisplayedMembers = data.members.length;
|
|
|
|
// Static: update header
|
|
document.getElementById("view_table_thead_name").textContent = data.view.name;
|
|
|
|
// Static: update footer
|
|
let strViewMembersCount = data.view.numberOfGroupMembers + " group member";
|
|
if(data.view.numberOfGroupMembers > 1) strViewMembersCount += "s";
|
|
document.getElementById("view_table_tfoot_total").textContent = strViewMembersCount;
|
|
document.getElementById("view_table_tfoot_filler").setAttribute("colspan", numberOfDisplayedMembers);
|
|
|
|
// Static: update below table
|
|
document.getElementById("view_first_filled_date").textContent = data.view.firstFilledDate;
|
|
document.getElementById("view_last_filled_date").textContent = data.view.lastFilledDate;
|
|
|
|
// For each member
|
|
data.members.forEach(element => {
|
|
let newCell = document.createElement("th");
|
|
let displayName;
|
|
|
|
if(element.uid == userUid) {
|
|
displayName = "Me (" + element.displayName + ")";
|
|
newCell.classList.add("me");
|
|
}
|
|
else {
|
|
displayName = element.displayName;
|
|
}
|
|
|
|
if(element.avatar) {
|
|
let newAvatar = document.createElement("img");
|
|
newAvatar.setAttribute("src", element.avatar);
|
|
newAvatar.setAttribute("alt", "Photo for " + element.displayName);
|
|
newCell.appendChild(newAvatar);
|
|
}
|
|
let newTextData = document.createTextNode(displayName);
|
|
newCell.appendChild(newTextData);
|
|
|
|
headerRow.appendChild(newCell);
|
|
});
|
|
// For each date
|
|
const rowDateTemplate = document.getElementById("row_date");
|
|
const cellAvailabilityTemplate = document.getElementById("cell_availability");
|
|
const modifierCannotHostTemplate = document.getElementById("modifier_cannot_host");
|
|
const modifierCannotTravelTemplate = document.getElementById("modifier_cannot_travel");
|
|
data.dates.forEach(element => {
|
|
let clonedRow = rowDateTemplate.content.cloneNode(true);
|
|
|
|
let header = clonedRow.querySelector("th");
|
|
let headerName = document.createTextNode(element.date);
|
|
header.appendChild(headerName);
|
|
|
|
let locations = [];
|
|
element.membersAvailability.forEach(availability => {
|
|
// Creating the DOM
|
|
let clonedCell = cellAvailabilityTemplate.content.cloneNode(true);
|
|
if(userUid == availability.memberUid) {
|
|
clonedCell.children[0].classList.add("me");
|
|
}
|
|
let whereSpan = clonedCell.querySelector("span.where");
|
|
let locationText = document.createTextNode(availability.location);
|
|
whereSpan.appendChild(locationText);
|
|
|
|
if(!availability.canHost) {
|
|
let clonedModifier = modifierCannotHostTemplate.content.cloneNode(true);
|
|
whereSpan.appendChild(clonedModifier);
|
|
}
|
|
if(!availability.canTravel) {
|
|
let clonedModifier = modifierCannotTravelTemplate.content.cloneNode(true);
|
|
whereSpan.appendChild(clonedModifier);
|
|
}
|
|
|
|
clonedRow.children[0].appendChild(clonedCell);
|
|
|
|
// Aggregating data per location, for totals
|
|
let existingLocations = locations.filter(e => e.location === availability.location);
|
|
if(existingLocations.length < 1) {
|
|
locations.push({
|
|
location: availability.location,
|
|
membersAvailable: 0,
|
|
hostsAvailable: 0,
|
|
travellersAvailable: 0
|
|
});
|
|
}
|
|
});
|
|
|
|
// Update other locations if member can travel
|
|
element.membersAvailability.forEach(availability => {
|
|
locations.forEach(existingLocation => {
|
|
if(existingLocation.location == availability.location) {
|
|
existingLocation.membersAvailable++;
|
|
if(availability.canHost) {
|
|
existingLocation.hostsAvailable++;
|
|
}
|
|
}
|
|
else if(availability.canTravel) {
|
|
existingLocation.membersAvailable++;
|
|
existingLocation.travellersAvailable++;
|
|
}
|
|
});
|
|
});
|
|
|
|
if(locations.length > 0) {
|
|
header.appendChild(document.createElement("br"));
|
|
}
|
|
locations.sort((a, b) => {
|
|
if(a.membersAvailable > b.membersAvailable) {
|
|
return -1;
|
|
}
|
|
else if(a.membersAvailable < b.membersAvailable) {
|
|
return 1;
|
|
}
|
|
return 0;
|
|
}).forEach(location => {
|
|
let spanLocation = document.createElement("span");
|
|
spanLocation.textContent = location.location + location.membersAvailable;
|
|
|
|
let title = [location.location + ": "];
|
|
if(location.hostsAvailable > 0) {
|
|
title.push(location.hostsAvailable + "🏠");
|
|
}
|
|
if(location.travellersAvailable > 0) {
|
|
title.push(location.travellersAvailable + "🚅");
|
|
}
|
|
let neitherHostNorTraveller = location.membersAvailable - location.hostsAvailable - location.travellersAvailable;
|
|
if(neitherHostNorTraveller > 0) {
|
|
title.push(neitherHostNorTraveller);
|
|
}
|
|
spanLocation.setAttribute("title", title.join(" "));
|
|
|
|
header.appendChild(spanLocation);
|
|
});
|
|
|
|
tbody.appendChild(clonedRow);
|
|
});
|
|
}
|
|
|
|
data2dom(viewData);
|
|
</script>
|
|
</body>
|
|
</html> |