Raggiunto primo stato presentabile

This commit is contained in:
2022-03-05 04:45:12 +01:00
parent b0732c9c4f
commit bbd6b9eb7e
19 changed files with 1326 additions and 468 deletions
+81 -23
View File
@@ -9,77 +9,135 @@
<script type="module" src="./src/index.js"></script>
</head>
<body>
<header>
<div class="nav-cell left">
<nav>
<div class="nav-group left">
<div class="nav-item" id="clock">14:23</div>
</div>
<div class="nav-cell center">
<div class="nav-item">Posti DM</div>
<div class="nav-group center">
<div class="nav-item">
<a href="/">Posti DM</a>
</div>
</div>
<div class="nav-cell right">
<div class="nav-item">Login</div>
<div class="nav-group right">
<div id="login-label" class="nav-item">
<a href="/login.html">Login</a>
</div>
<div id="logged-label" class="nav-item hidden">
<!-- @username -->
</div>
<div id="logout-label" class="nav-item hidden">
<button id="logout-button">Logout</button>
</div>
</div>
</header>
</nav>
<main>
<div class="room-name">AulaStud</div>
<div class="room-main">
<div class="panel room-diagram">
<h3>Aula Studenti</h3>
<div class="room-diagram">
<!-- <div class="options">
<input type="checkbox" id="diagram-or-list-checkbox" checked />
<label for="diagram-or-list-checkbox">Visualizzazione Posti</label>
</div> -->
<div class="room-grid">
<div
class="posto libero"
data-index="1"
data-seat-id="aula-stud/posto-1"
style="grid-column: 3 / span 4; grid-row: 1 / span 2"
></div>
<div
class="posto libero"
data-index="2"
data-seat-id="aula-stud/posto-2"
style="grid-column: 7 / span 4; grid-row: 1 / span 2"
></div>
<div
class="posto libero"
data-index="3"
data-seat-id="aula-stud/posto-3"
style="grid-column: 1 / span 2; grid-row: 3 / span 4"
></div>
<div
class="posto libero"
data-index="4"
data-seat-id="aula-stud/posto-4"
style="grid-column: 1 / span 2; grid-row: 7 / span 4"
></div>
<div
class="posto libero"
data-index="5"
class="posto mio"
data-seat-id="aula-stud/posto-5"
style="grid-column: 3 / span 4; grid-row: 11 / span 2"
></div>
<div
class="posto occupato"
data-index="6"
data-seat-id="aula-stud/posto-6"
style="grid-column: 7 / span 4; grid-row: 11 / span 2"
></div>
<div
class="posto occupato"
data-index="7"
data-seat-id="aula-stud/posto-7"
style="grid-column: 8 / span 2; grid-row: 5 / span 4"
></div>
<div
class="posto occupato"
data-index="8"
data-seat-id="aula-stud/posto-8"
style="grid-column: 14 / span 2; grid-row: 1 / span 4"
></div>
<div
class="posto occupato"
data-index="9"
data-seat-id="aula-stud/posto-9"
style="grid-column: 14 / span 2; grid-row: 5 / span 4"
></div>
<div
class="posto occupato"
data-index="10"
data-seat-id="aula-stud/posto-10"
style="grid-column: 14 / span 2; grid-row: 9 / span 4"
></div>
<div
class="posto occupato"
data-index="11"
data-seat-id="aula-stud/posto-11"
style="grid-column: 14 / span 2; grid-row: 13 / span 4"
></div>
</div>
<div class="room-seat-list hidden">
<div class="seat-item">
<div class="name">Posto 1</div>
<button>Occupa</button>
</div>
<div class="seat-item">
<div class="name">Posto 2</div>
<button>Occupa</button>
</div>
<div class="seat-item">
<div class="name">Posto 3</div>
<button>Occupa</button>
</div>
<div class="seat-item">
<div class="name">Posto 4</div>
<button>Occupa</button>
</div>
</div>
<p>
Clicca su un posto per occuparlo (i posti liberi sono
<span
style="
text-decoration: solid underline #8888 4px;
text-decoration-skip-ink: none;
"
>grigi</span
>, quelli
<span style="text-decoration: solid underline #d88 4px">rossi</span>
sono occupati dagli altri, mentre il tuo è in
<span style="text-decoration: solid underline #8d8 4px">verde</span>)
</p>
</div>
</main>
<div id="tooltip">Some text</div>
<script type="module" src="./src/pages/index.js"></script>
<!-- <main>
<div class="room-name">AulaStud</div>
<div class="room-main">
<div class="panel room-diagram">
...
</div>
<div class="panel room-bookings">
<div class="seat-list">
<div class="seat">
@@ -137,6 +195,6 @@
<button disabled>18:00 &mdash; 19:30</button>
</div>
</div>
</main>
</main> -->
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Posti DM</title>
<script type="module" src="./src/index.js"></script>
</head>
<body>
<nav>
<div class="nav-group left">
<div class="nav-item" id="clock">14:23</div>
</div>
<div class="nav-group center">
<div class="nav-item">
<a href="/">Posti DM</a>
</div>
</div>
</nav>
<main>
<div class="form">
<h3 class="fill-row">Login</h3>
<p id="error-string" class="error fill-row hidden"></p>
<label for="login-username"> Username </label>
<input type="text" id="input-login-username" />
<label for="login-password"> Password </label>
<input type="password" id="input-login-password" />
<button id="button-login" class="fill-row">Login</button>
</div>
</main>
<script type="module" src="./src/pages/login.js"></script>
</body>
</html>
+13
View File
@@ -0,0 +1,13 @@
export function createClock(elClock) {
const renderClock = () => {
const pad = s => (s + '').padStart(2, '0')
const now = new Date(),
hours = now.getHours(),
minutes = now.getMinutes(),
seconds = now.getSeconds()
elClock.innerText = `${pad(hours)}:${pad(minutes)}:${pad(seconds)}`
}
setInterval(renderClock, 1000)
renderClock()
}
+51
View File
@@ -0,0 +1,51 @@
function renderGridLinesCanvas($canvas, [rows, cols]) {
$canvas.width = $canvas.offsetWidth * 2
$canvas.height = $canvas.offsetHeight * 2
const g = $canvas.getContext('2d')
const width = $canvas.width / 2
const height = $canvas.height / 2
g.scale(2, 2)
g.strokeStyle = '#ddd'
g.lineWidth = 2
g.setLineDash([4, 4])
for (let i = 0; i < cols + 1; i++) {
const x = 2 + ((width - 4) / cols) * i
g.beginPath()
g.moveTo(x, 0)
g.lineTo(x, height)
g.stroke()
}
for (let j = 0; j < rows + 1; j++) {
const y = 2 + ((height - 4) / rows) * j
g.beginPath()
g.moveTo(0, y)
g.lineTo(width, y)
g.stroke()
}
}
export function createGridLineCanvas($roomGrid) {
const $canvas = document.createElement('canvas')
$roomGrid.append($canvas)
$canvas.style.position = 'absolute'
$canvas.style.inset = '0'
$canvas.style.width = '100%'
$canvas.style.height = '100%'
$canvas.style.zIndex = '-1'
const [rows, cols] = getComputedStyle($roomGrid)
.getPropertyValue('aspect-ratio')
.split('/')
.reverse()
.map(s => parseInt(s))
const render = () => renderGridLinesCanvas($canvas, [rows, cols])
window.addEventListener('resize', render)
render()
}
+100
View File
@@ -0,0 +1,100 @@
import { createRoomEventStream, Database, getLoggedUser } from '../index.js'
import { addTooltipElementListener, resetTooltip, setTooltipText } from './tooltip.js'
async function renderWidget(elSeatMap, seats) {
const user = await getLoggedUser()
Object.values(seats).forEach(seat => {
const { id, occupiedBy } = seat
elSeatMap[id].classList.remove('libero')
elSeatMap[id].classList.remove('occupato')
elSeatMap[id].classList.remove('mio')
if (occupiedBy.length > 0) {
const [seatOccupant] = occupiedBy
if (user && seatOccupant === user.id) {
elSeatMap[id].classList.add('mio')
} else {
elSeatMap[id].classList.add('occupato')
}
} else {
elSeatMap[id].classList.add('libero')
}
})
}
export async function createSeatWidget($roomGrid, roomId) {
const user = await getLoggedUser()
const elSeats = [...$roomGrid.querySelectorAll('[data-seat-id]')]
const elSeatMap = {}
elSeats.forEach($seat => {
const seatId = $seat.dataset.seatId
$seat.dataset.index = seatId.split('-')[2]
elSeatMap[seatId] = $seat
})
// Full widget state
let seats = await Database.getSeats(roomId)
// First render
renderWidget(elSeatMap, seats)
// Setup event listeners
Object.entries(elSeatMap).forEach(([seatId, $seat]) => {
addTooltipElementListener($hovering => {
if ($seat.contains($hovering)) {
const occupiedBy = seats[seatId].occupiedBy
if (occupiedBy.length > 0) {
if (user && occupiedBy[0] === user.id) {
setTooltipText(`Questo è il tuo posto!`)
} else {
setTooltipText(`Occupato da @${occupiedBy[0]}`)
}
} else {
setTooltipText('Libero')
}
return true
} else {
resetTooltip()
}
})
$seat.addEventListener('click', () => {
if (!user) {
location.href = '/login.html'
return
}
const occupiedBy = seats[seatId].occupiedBy
if (occupiedBy.length === 0) {
const confirmResponse = confirm('Occupare il posto?')
if (confirmResponse) {
Database.occupySeat(seatId)
}
} else if (occupiedBy.length === 1 && occupiedBy[0] === user.id) {
const answer = confirm('Lasciare veramente il posto?')
if (answer) {
Database.leaveSeat(seatId)
}
} else {
alert('Posto già occupato!')
}
})
})
// Refresh room diagram on message from server
createRoomEventStream(roomId).addEventListener('message', async e => {
console.log(e.data)
seats = await Database.getSeats(roomId)
renderWidget(elSeatMap, seats)
})
}
+32
View File
@@ -0,0 +1,32 @@
const LISTENERS = []
const $tooltip = document.querySelector('#tooltip')
export function attachTooltip() {
document.body.addEventListener('mousemove', e => {
$tooltip.style.setProperty('--x', 10 + e.pageX + 'px')
$tooltip.style.setProperty('--y', 10 + e.pageY + 'px')
LISTENERS.find(listener => listener(e.target))
})
}
export function addTooltipElementListener(callbackFn) {
LISTENERS.push(callbackFn)
}
export function setTooltip(mountFn) {
$tooltip.classList.remove('hidden')
mountFn($tooltip)
}
export function setTooltipText(text) {
$tooltip.classList.remove('hidden')
setTooltip($t => {
$t.innerText = text
})
}
export function resetTooltip() {
$tooltip.classList.add('hidden')
$tooltip.innerHTML = ''
}
+33
View File
@@ -1 +1,34 @@
import './style.scss'
let USER = false
export async function getLoggedUser() {
if (USER === false) {
console.log('Caching user data...')
USER = await (await fetch('/api/user')).json()
}
return USER
}
export function createRoomEventStream(roomId) {
return new EventSource(`/api/room_events?id=${roomId}`)
}
export const Database = {
async getSeats(roomId) {
const seatList = await (await fetch(`/api/room/seats?id=${roomId}`)).json()
const seats = {}
seatList.forEach(seat => {
seats[seat.id] = seat
})
return seats
},
async occupySeat(seatId) {
await (await fetch(`/api/seat/occupy?id=${seatId}`, { method: 'POST' })).json()
},
async leaveSeat(seatId) {
await (await fetch(`/api/seat/leave?id=${seatId}`, { method: 'POST' })).json()
},
}
+52
View File
@@ -0,0 +1,52 @@
import { createGridLineCanvas } from '../components/gridlines.js'
import { getLoggedUser } from '../index.js'
import { createSeatWidget } from '../components/seats-widget.js'
import { createClock } from '../components/clock.js'
import { attachTooltip } from '../components/tooltip.js'
const elClock = document.querySelector('#clock')
const elLoggedLabel = document.querySelector('#logged-label')
const elLoginLabel = document.querySelector('#login-label')
const elLogoutLabel = document.querySelector('#logout-label')
const elLogoutButton = document.querySelector('#logout-button')
const elRoomGrid = document.querySelector('.room-grid')
async function logout() {
await fetch('/api/logout', { method: 'POST' })
location.href = '/'
}
async function main() {
const urlSearchParams = new URLSearchParams(window.location.search)
const params = Object.fromEntries(urlSearchParams.entries())
console.log(params)
elLogoutButton.addEventListener('click', () => logout())
const user = await getLoggedUser()
if (user) {
elLoginLabel.classList.add('hidden')
elLoggedLabel.innerText =
'@' + user.id + (user.permissions.length > 0 ? ` (${user.permissions.join(', ')})` : '')
elLoggedLabel.classList.remove('hidden')
elLogoutLabel.classList.remove('hidden')
}
// Widgets
// createClock(elClock)
createGridLineCanvas(elRoomGrid)
createSeatWidget(elRoomGrid, 'aula-stud')
// Use tooltips only on desktop
if (matchMedia('(pointer: fine)').matches) {
attachTooltip()
}
}
main()
+79
View File
@@ -0,0 +1,79 @@
import { getLoggedUser } from '../index.js'
//
// Page Element
//
const elErrorString = document.querySelector('#error-string')
const elLoginUsernameInput = document.querySelector('#input-login-username')
const elLoginPasswordInput = document.querySelector('#input-login-password')
const elLoginButton = document.querySelector('#button-login')
//
// Display error message
//
let errorTimeoutHandle = null
function displayErrorString(e) {
if (errorTimeoutHandle) clearTimeout(errorTimeoutHandle)
elErrorString.classList.toggle('hidden', false)
elErrorString.innerText = e.toString()
errorTimeoutHandle = setTimeout(() => {
elErrorString.classList.toggle('hidden', true)
elErrorString.innerText = ''
}, 5 * 1000)
}
//
// Handle Login Button
//
async function login() {
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type': 'application/json',
},
body: JSON.stringify({
username: elLoginUsernameInput.value,
password: elLoginPasswordInput.value,
}),
})
if (!response.ok) {
displayErrorString(await response.text())
return
}
location.href = '/'
} catch (e) {
displayErrorString(e)
}
}
//
// Main
//
async function main() {
const user = await getLoggedUser()
console.log(user)
if (user) {
location.href = '/'
}
elLoginButton.addEventListener('click', () => login())
elLoginPasswordInput.addEventListener('keydown', e => {
if (e.key === 'Enter') login()
})
elLoginUsernameInput.focus()
}
main()
-1
View File
@@ -35,7 +35,6 @@ html:focus-within {
// Set core body defaults
body {
min-height: 100vh;
text-rendering: optimizeSpeed;
line-height: 1.5;
}
+456 -208
View File
@@ -1,249 +1,497 @@
@use './reset.scss';
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600&display=swap');
$media-small-device-size: 750px;
body {
background: #f0f0f0;
color: #333;
background: #f8f8f8;
// background: #fff;
font-family: 'Inter', sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding-bottom: 6rem;
}
header {
width: 100%;
height: 3rem;
//
// Utility Classes
//
@mixin panel {
background: #fff;
box-shadow: 0 0 8px 0px #0001, 0 0 16px 8px #0001;
border-radius: 0.25rem;
@media screen and (max-width: $media-small-device-size) {
width: 100%;
max-width: 100%;
border-radius: 0;
}
}
@mixin padded-panel {
@include panel;
padding: 2rem;
@media screen and (max-width: $media-small-device-size) {
padding: 1rem;
}
}
.hidden {
display: none !important;
}
//
// Typography
//
// Headings
$base-font-size: 18px;
$heading-scale: 1.33;
@function pow($number, $exponent) {
$value: 1;
@if $exponent > 0 {
@for $i from 1 through $exponent {
$value: $value * $number;
}
}
@return $value;
}
@for $i from 1 through 5 {
h#{$i} {
font-family: 'Inter', serif;
$factor: pow($heading-scale, 5 - $i);
font-size: $base-font-size * $factor;
font-weight: 600;
}
}
//
// Base Elements
//
a,
a:visited {
color: royalblue;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
//
// General Page Structure
//
nav {
width: 100%;
height: 4rem;
position: relative;
background: #fff;
border-bottom: 1px solid #ddd;
@include panel;
border-radius: 0;
.nav-group,
.nav-item {
height: 3rem;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.left {
position: absolute;
left: 0;
padding-left: 1rem;
display: flex;
gap: 1rem;
}
.center {
position: absolute;
left: 50%;
transform: translate(-50%, 0);
display: flex;
gap: 1rem;
}
.left,
.center,
.right {
position: absolute;
right: 0;
padding-right: 1rem;
display: flex;
gap: 1rem;
}
}
.panel {
border: 1px solid #ddd;
background: #fff;
}
main {
display: flex;
flex-direction: column;
gap: 2rem;
align-items: center;
padding: 2rem 1rem 2rem;
.room-name {
display: flex;
align-items: center;
justify-content: center;
font-size: 26px;
font-weight: bold;
}
.room-main {
display: grid;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
grid-template-areas:
'diagram bookings'
'timeline timeline';
max-width: 800px;
.room-diagram {
grid-area: diagram;
}
.room-bookings {
grid-area: bookings;
}
.room-timeline {
grid-area: timeline;
}
gap: 1rem;
.room-diagram {
padding: 1rem;
// --posto-libero-bg: #ddf;
// --posto-libero-bg-1: #88f;
// --posto-libero-bg-2: #66d;
--posto-libero-bg: #8d8;
--posto-occupato-bg: #d88;
display: grid;
grid-template-columns: repeat(15, 1fr);
grid-template-rows: repeat(16, 1fr);
gap: 0.25rem;
aspect-ratio: 15 / 16;
width: calc(15 * 2rem);
max-width: 500px;
.posto {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
&.libero {
background: var(--posto-libero-bg);
}
&.occupato {
background: var(--posto-occupato-bg);
}
border: 2px solid #0004;
cursor: pointer;
&:hover {
border-color: #0006;
border-width: 4px;
}
line-height: 1;
&::before {
font-size: 12px;
content: 'Posto';
}
&::after {
font-size: 12px;
content: attr(data-index);
}
}
}
.room-bookings {
display: flex;
flex-direction: column;
min-width: 15rem;
width: 100%;
padding: 0.75rem 0;
.seat-list {
display: flex;
flex-direction: column;
.seat {
padding: 0.25rem 1rem;
display: grid;
grid-template-columns: 1fr auto;
height: 2.5rem;
&:hover {
background: #0001;
}
.name {
display: flex;
align-items: center;
}
&.selected {
background: #0002;
.name {
font-style: italic;
}
}
}
}
}
.room-timeline {
padding: 1rem;
display: flex;
flex-direction: row;
gap: 0.5rem;
align-items: center;
overflow-x: auto;
button {
min-width: 8rem;
height: 3rem;
}
}
}
}
@media screen and (max-width: 1000px) {
header {
@media screen and (max-width: $media-small-device-size) {
height: unset;
display: flex;
flex-direction: column;
.nav-cell {
position: relative;
padding: 0 1rem;
.nav-group {
height: 4rem;
}
}
main {
.room-main {
display: flex;
flex-direction: column;
@media screen and (min-width: $media-small-device-size) {
.left,
.center,
.right {
position: absolute;
}
.room-diagram {
width: 200px;
}
.left {
left: 0;
padding-left: 1rem;
}
.room-timeline {
flex-direction: column;
}
.center {
left: 50%;
transform: translate(-50%, 0);
font-size: 22px;
}
.right {
right: 0;
padding-right: 1rem;
}
}
}
main {
width: 100%;
@media screen and (min-width: $media-small-device-size) {
max-width: 75ch;
}
padding-top: 2rem;
display: flex;
flex-direction: column;
align-items: center;
gap: 2rem;
}
.form {
@include padded-panel;
display: grid;
grid-template-columns: auto 1fr;
gap: 0.5rem 1rem;
.error {
color: #d00;
}
.fill-row {
grid-column: 1 / span 2;
place-self: center;
}
}
.room-diagram {
--posto-libero-bg: #ddd;
--posto-occupato-bg: #d88;
--posto-mio-occupato-bg: #8d8;
@include padded-panel;
display: flex;
flex-direction: column;
align-items: center;
gap: 1rem;
@media screen and (min-width: $media-small-device-size) {
width: 450px;
}
.room-grid {
// To absolutly position a canvas underneat and render grid lines
position: relative;
z-index: 0;
display: grid;
grid-template-columns: repeat(15, 1fr);
grid-template-rows: repeat(16, 1fr);
gap: 4px;
padding: 4px;
aspect-ratio: 15 / 16;
width: 100%;
.posto {
display: flex;
align-items: center;
justify-content: center;
flex-direction: column;
&.mio {
background: var(--posto-mio-occupato-bg);
}
&.libero {
background: var(--posto-libero-bg);
}
&.occupato {
background: var(--posto-occupato-bg);
}
border: 4px solid #0004;
cursor: pointer;
&:hover {
border-color: #0008;
border-width: 4px;
}
line-height: 1;
// @media screen and (min-width: $media-small-device-size) {
&::before {
font-size: 12px;
content: 'Posto';
}
&::after {
font-size: 12px;
content: attr(data-index);
}
// }
}
}
.room-seat-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
width: 100%;
.seat-item {
display: grid;
grid-template-columns: 1fr auto;
}
}
}
body {
position: relative;
#tooltip {
position: absolute;
left: 0;
right: 0;
padding: 0.25rem;
width: fit-content;
font-size: 14px;
line-height: 1;
opacity: 0.8;
background: #666;
color: #ddd;
transform: translate(var(--x, -100px), var(--y, -100px));
z-index: 100;
user-select: none;
pointer-events: none;
}
}
// .panel {
// border: 1px solid #ddd;
// background: #fff;
// }
// main {
// display: flex;
// flex-direction: column;
// gap: 2rem;
// align-items: center;
// padding: 2rem 1rem 2rem;
// .room-name {
// display: flex;
// align-items: center;
// justify-content: center;
// font-size: 26px;
// font-weight: bold;
// }
// .room-main {
// display: grid;
// grid-template-columns: auto 1fr;
// grid-template-rows: auto auto;
// grid-template-areas:
// 'diagram bookings'
// 'timeline timeline';
// max-width: 800px;
// .room-diagram {
// grid-area: diagram;
// }
// .room-bookings {
// grid-area: bookings;
// }
// .room-timeline {
// grid-area: timeline;
// }
// gap: 1rem;
// .room-diagram {
// padding: 1rem;
// // --posto-libero-bg: #ddf;
// // --posto-libero-bg-1: #88f;
// // --posto-libero-bg-2: #66d;
// --posto-libero-bg: #8d8;
// --posto-occupato-bg: #d88;
// display: grid;
// grid-template-columns: repeat(15, 1fr);
// grid-template-rows: repeat(16, 1fr);
// gap: 0.25rem;
// aspect-ratio: 15 / 16;
// width: calc(15 * 2rem);
// max-width: 500px;
// .posto {
// display: flex;
// align-items: center;
// justify-content: center;
// flex-direction: column;
// &.libero {
// background: var(--posto-libero-bg);
// }
// &.occupato {
// background: var(--posto-occupato-bg);
// }
// border: 2px solid #0004;
// cursor: pointer;
// &:hover {
// border-color: #0006;
// border-width: 4px;
// }
// line-height: 1;
// &::before {
// font-size: 12px;
// content: 'Posto';
// }
// &::after {
// font-size: 12px;
// content: attr(data-index);
// }
// }
// }
// .room-bookings {
// display: flex;
// flex-direction: column;
// min-width: 15rem;
// width: 100%;
// padding: 0.75rem 0;
// .seat-list {
// display: flex;
// flex-direction: column;
// .seat {
// padding: 0.25rem 1rem;
// display: grid;
// grid-template-columns: 1fr auto;
// height: 2.5rem;
// &:hover {
// background: #0001;
// }
// .name {
// display: flex;
// align-items: center;
// }
// &.selected {
// background: #0002;
// .name {
// font-style: italic;
// }
// }
// }
// }
// }
// .room-timeline {
// padding: 1rem;
// display: flex;
// flex-direction: row;
// gap: 0.5rem;
// align-items: center;
// overflow-x: auto;
// button {
// min-width: 8rem;
// height: 3rem;
// }
// }
// }
// }
// @media screen and (max-width: $media-small-device-size) {
// header {
// height: unset;
// display: flex;
// flex-direction: column;
// .nav-cell {
// position: relative;
// padding: 0 1rem;
// }
// }
// main {
// .room-main {
// display: flex;
// flex-direction: column;
// .room-diagram {
// width: 200px;
// }
// .room-timeline {
// flex-direction: column;
// }
// }
// }
// }
img {
transform: rotate(180deg);
}