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> |