Raggiunto primo stato presentabile

main
Antonio De Lucreziis 3 years ago
parent b0732c9c4f
commit bbd6b9eb7e

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

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

@ -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()
}

@ -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()
}

@ -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)
})
}

@ -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 = ''
}

@ -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()
},
}

@ -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()

@ -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()

@ -35,7 +35,6 @@ html:focus-within {
// Set core body defaults
body {
min-height: 100vh;
text-rendering: optimizeSpeed;
line-height: 1.5;
}

@ -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;
}
header {
width: 100%;
height: 3rem;
padding-bottom: 6rem;
}
position: relative;
//
// Utility Classes
//
@mixin panel {
background: #fff;
border-bottom: 1px solid #ddd;
box-shadow: 0 0 8px 0px #0001, 0 0 16px 8px #0001;
.nav-item {
height: 3rem;
border-radius: 0.25rem;
display: flex;
align-items: center;
justify-content: center;
@media screen and (max-width: $media-small-device-size) {
width: 100%;
max-width: 100%;
border-radius: 0;
}
}
.left {
position: absolute;
left: 0;
@mixin padded-panel {
@include panel;
padding-left: 1rem;
padding: 2rem;
display: flex;
gap: 1rem;
@media screen and (max-width: $media-small-device-size) {
padding: 1rem;
}
}
.center {
position: absolute;
left: 50%;
transform: translate(-50%, 0);
.hidden {
display: none !important;
}
display: flex;
gap: 1rem;
//
// 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;
}
}
.right {
position: absolute;
right: 0;
@return $value;
}
padding-right: 1rem;
@for $i from 1 through 5 {
h#{$i} {
font-family: 'Inter', serif;
display: flex;
gap: 1rem;
$factor: pow($heading-scale, 5 - $i);
font-size: $base-font-size * $factor;
font-weight: 600;
}
}
.panel {
border: 1px solid #ddd;
background: #fff;
//
// Base Elements
//
a,
a:visited {
color: royalblue;
text-decoration: none;
&:hover {
text-decoration: underline;
}
}
main {
display: flex;
flex-direction: column;
gap: 2rem;
//
// General Page Structure
//
align-items: center;
nav {
width: 100%;
height: 4rem;
position: relative;
padding: 2rem 1rem 2rem;
@include panel;
border-radius: 0;
.nav-group,
.nav-item {
height: 100%;
.room-name {
display: flex;
align-items: center;
justify-content: center;
}
font-size: 26px;
font-weight: bold;
.left,
.center,
.right {
display: flex;
gap: 1rem;
}
.room-main {
display: grid;
@media screen and (max-width: $media-small-device-size) {
height: unset;
display: flex;
flex-direction: column;
grid-template-columns: auto 1fr;
grid-template-rows: auto auto;
grid-template-areas:
'diagram bookings'
'timeline timeline';
.nav-group {
height: 4rem;
}
}
max-width: 800px;
@media screen and (min-width: $media-small-device-size) {
.left,
.center,
.right {
position: absolute;
}
.room-diagram {
grid-area: diagram;
.left {
left: 0;
padding-left: 1rem;
}
.room-bookings {
grid-area: bookings;
.center {
left: 50%;
transform: translate(-50%, 0);
font-size: 22px;
}
.room-timeline {
grid-area: timeline;
.right {
right: 0;
padding-right: 1rem;
}
}
}
gap: 1rem;
main {
width: 100%;
.room-diagram {
padding: 1rem;
@media screen and (min-width: $media-small-device-size) {
max-width: 75ch;
}
// --posto-libero-bg: #ddf;
// --posto-libero-bg-1: #88f;
// --posto-libero-bg-2: #66d;
--posto-libero-bg: #8d8;
--posto-occupato-bg: #d88;
padding-top: 2rem;
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);
}
}
}
display: flex;
flex-direction: column;
align-items: center;
.room-bookings {
display: flex;
flex-direction: column;
gap: 2rem;
}
min-width: 15rem;
width: 100%;
.form {
@include padded-panel;
padding: 0.75rem 0;
display: grid;
grid-template-columns: auto 1fr;
.seat-list {
display: flex;
flex-direction: column;
gap: 0.5rem 1rem;
.seat {
padding: 0.25rem 1rem;
.error {
color: #d00;
}
display: grid;
grid-template-columns: 1fr auto;
.fill-row {
grid-column: 1 / span 2;
place-self: center;
}
}
height: 2.5rem;
.room-diagram {
--posto-libero-bg: #ddd;
--posto-occupato-bg: #d88;
--posto-mio-occupato-bg: #8d8;
&:hover {
background: #0001;
}
@include padded-panel;
.name {
display: flex;
align-items: center;
}
display: flex;
flex-direction: column;
align-items: center;
&.selected {
background: #0002;
gap: 1rem;
.name {
font-style: italic;
}
}
}
}
}
@media screen and (min-width: $media-small-device-size) {
width: 450px;
}
.room-timeline {
padding: 1rem;
.room-grid {
// To absolutly position a canvas underneat and render grid lines
position: relative;
z-index: 0;
display: flex;
flex-direction: row;
gap: 0.5rem;
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;
}
overflow-x: auto;
line-height: 1;
button {
min-width: 8rem;
height: 3rem;
// @media screen and (min-width: $media-small-device-size) {
&::before {
font-size: 12px;
content: 'Posto';
}
&::after {
font-size: 12px;
content: attr(data-index);
}
// }
}
}
}
@media screen and (max-width: 1000px) {
header {
height: unset;
.room-seat-list {
display: flex;
flex-direction: column;
gap: 0.5rem;
.nav-cell {
position: relative;
padding: 0 1rem;
width: 100%;
.seat-item {
display: grid;
grid-template-columns: 1fr auto;
}
}
}
main {
.room-main {
display: flex;
flex-direction: column;
body {
position: relative;
.room-diagram {
width: 200px;
}
#tooltip {
position: absolute;
left: 0;
right: 0;
padding: 0.25rem;
.room-timeline {
flex-direction: column;
}
}
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);
}

@ -49,7 +49,7 @@ type AuthService struct {
UserFromSessionToken func(session string) (string, error)
// AuthenticationFailed handles failed authentications
AuthenticationFailed http.Handler
AuthenticationFailed func(error) http.Handler
// OtherError handles other errors
OtherError func(error) http.Handler
@ -57,7 +57,7 @@ type AuthService struct {
func (auth *AuthService) Login(w http.ResponseWriter, r *http.Request, userID, password string) {
if err := auth.CheckUserPassword(userID, password); err != nil {
auth.AuthenticationFailed.ServeHTTP(w, r)
auth.AuthenticationFailed(err).ServeHTTP(w, r)
return
}
@ -98,7 +98,7 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand
return
}
auth.AuthenticationFailed.ServeHTTP(w, r)
auth.AuthenticationFailed(err).ServeHTTP(w, r)
return
}
if err != nil {
@ -139,7 +139,7 @@ func (auth *AuthService) Middleware(config *AuthMiddlewareConfig) func(http.Hand
}
if !hasAll {
auth.AuthenticationFailed.ServeHTTP(w, r)
auth.AuthenticationFailed(err).ServeHTTP(w, r)
return
}
}

@ -1,27 +1,33 @@
package db
import "fmt"
import (
"errors"
"fmt"
)
var ErrAlreadyExists = errors.New(`object already exists in database`)
var ErrDoesntExist = errors.New(`object doesn't exist in database`)
// Entities
type User struct {
ID string
Username string
Permissions []string
ID string `json:"id"`
Permissions []string `json:"permissions"`
}
type Room struct {
ID string
SeatIDs []string
ID string `json:"id"`
SeatIDs []string `json:"seatIds"`
// gridRows, gridCols int
}
type Seat struct {
ID string
ID string `json:"id"`
RoomID string `json:"roomId"`
// OccupiedBy is an empty list or a singleton of a userID
OccupiedBy []string
OccupiedBy []string `json:"occupiedBy"`
// x, y, w, h int
}
@ -29,16 +35,27 @@ type Seat struct {
// Database Interfaces
type Store interface {
CreateUser(user *User) error
CreateRoom(room *Room) error
CreateSeat(seat *Seat) error
DeleteUser(user *User) error
DeleteRoom(room *Room) error
DeleteSeat(seat *Seat) error
GetUser(userID string) (*User, error)
GetRoom(roomID string) (*Room, error)
GetSeat(seatID string) (*Seat, error)
GetOccupiedSeats(roomID string) ([]string, error)
GetFreeSeats(roomID string) ([]string, error)
GetRooms() ([]string, error)
GetRoomOccupiedSeats(roomID string) ([]string, error)
GetRoomFreeSeats(roomID string) ([]string, error)
GetUserSeat(userID string) ([]string, error)
OccupySeat(userID, roomID, seatID string) error
FreeSeat(userID, roomID, seatID string) error
OccupySeat(seatID, userID string) error
FreeSeat(seatID string) error
}
// TODO: Create an SQLite implementation
@ -57,47 +74,88 @@ func NewInMemoryStore() Store {
}
db.rooms["aula-stud"] = &Room{
ID: "aula-stud",
SeatIDs: []string{
"aula-stud/posto-1",
"aula-stud/posto-2",
"aula-stud/posto-3",
"aula-stud/posto-4",
},
ID: "aula-stud",
SeatIDs: []string{},
}
db.seats["aula-stud/posto-1"] = &Seat{
ID: "aula-stud/posto-1",
OccupiedBy: []string{},
}
db.seats["aula-stud/posto-2"] = &Seat{
ID: "aula-stud/posto-2",
OccupiedBy: []string{},
}
db.seats["aula-stud/posto-3"] = &Seat{
ID: "aula-stud/posto-3",
OccupiedBy: []string{},
}
db.seats["aula-stud/posto-4"] = &Seat{
ID: "aula-stud/posto-4",
OccupiedBy: []string{},
for i := 1; i <= 11; i++ {
seatID := fmt.Sprintf(`aula-stud/posto-%d`, i)
db.rooms["aula-stud"].SeatIDs = append(db.rooms["aula-stud"].SeatIDs, seatID)
db.seats[seatID] = &Seat{
ID: seatID,
RoomID: "aula-stud",
OccupiedBy: []string{},
}
}
db.seats["aula-stud/posto-7"].OccupiedBy = []string{"aziis98"}
db.seats["aula-stud/posto-8"].OccupiedBy = []string{"jack"}
db.users["aziis98"] = &User{
ID: "aziis98",
Username: "aziis98",
Permissions: []string{"admin"},
}
db.users["bachoseven"] = &User{
ID: "bachoseven",
Username: "bachoseven",
Permissions: []string{"admin"},
}
return db
}
func (db *memDB) CreateUser(user *User) error {
if _, present := db.users[user.ID]; present {
return ErrAlreadyExists
}
db.users[user.ID] = user
return nil
}
func (db *memDB) CreateRoom(room *Room) error {
if _, present := db.rooms[room.ID]; present {
return ErrAlreadyExists
}
db.rooms[room.ID] = room
return nil
}
func (db *memDB) CreateSeat(seat *Seat) error {
if _, present := db.seats[seat.ID]; present {
return ErrAlreadyExists
}
db.seats[seat.ID] = seat
return nil
}
func (db *memDB) DeleteUser(user *User) error {
if _, present := db.users[user.ID]; !present {
return ErrDoesntExist
}
delete(db.users, user.ID)
return nil
}
func (db *memDB) DeleteRoom(room *Room) error {
if _, present := db.rooms[room.ID]; !present {
return ErrDoesntExist
}
delete(db.rooms, room.ID)
return nil
}
func (db *memDB) DeleteSeat(seat *Seat) error {
if _, present := db.seats[seat.ID]; !present {
return ErrDoesntExist
}
delete(db.seats, seat.ID)
return nil
}
func (db *memDB) GetUser(userID string) (*User, error) {
user, present := db.users[userID]
if !present {
@ -125,7 +183,17 @@ func (db *memDB) GetSeat(seatID string) (*Seat, error) {
return seat, nil
}
func (db *memDB) GetOccupiedSeats(roomID string) ([]string, error) {
func (db *memDB) GetRooms() ([]string, error) {
roomIDs := []string{}
for roomID := range db.rooms {
roomIDs = append(roomIDs, roomID)
}
return roomIDs, nil
}
func (db *memDB) GetRoomOccupiedSeats(roomID string) ([]string, error) {
room, err := db.GetRoom(roomID)
if err != nil {
return nil, err
@ -142,7 +210,7 @@ func (db *memDB) GetOccupiedSeats(roomID string) ([]string, error) {
return seats, nil
}
func (db *memDB) GetFreeSeats(roomID string) ([]string, error) {
func (db *memDB) GetRoomFreeSeats(roomID string) ([]string, error) {
room, err := db.GetRoom(roomID)
if err != nil {
return nil, err
@ -169,12 +237,12 @@ func (db *memDB) GetUserSeat(userID string) ([]string, error) {
return []string{}, nil
}
func (db *memDB) OccupySeat(userID string, roomID string, seatID string) error {
func (db *memDB) OccupySeat(seatID, userID string) error {
db.seats[seatID].OccupiedBy = []string{userID}
return nil
}
func (db *memDB) FreeSeat(userID string, roomID string, seatID string) error {
func (db *memDB) FreeSeat(seatID string) error {
db.seats[seatID].OccupiedBy = []string{}
return nil
}

@ -7,9 +7,12 @@ import (
type H map[string]interface{}
func WriteJSON(w http.ResponseWriter, data interface{}) error {
func WriteJSON(w http.ResponseWriter, data interface{}) {
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(data)
err := json.NewEncoder(w).Encode(data)
if err != nil {
panic(err)
}
}
func ReadJSON(r *http.Request, data interface{}) error {

@ -0,0 +1,78 @@
package serverevents
import (
"fmt"
"log"
"net/http"
)
type Config struct {
Connected func(chan string)
Disconnected func(chan string)
}
type Handler interface {
http.Handler
Broadcast(message string)
}
type handler struct {
clients map[chan string]bool
Connected func(chan string)
Disconnected func(chan string)
}
func New(config *Config) Handler {
return &handler{
make(map[chan string]bool),
config.Connected,
config.Disconnected,
}
}
func (sse *handler) Broadcast(message string) {
for client := range sse.clients {
client <- message
}
}
func (sse *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
client := make(chan string)
sse.clients[client] = true
log.Printf(`New connection`)
if sse.Connected != nil {
sse.Connected(client)
}
defer func() {
log.Printf(`Connection closed`)
close(client)
delete(sse.clients, client)
if sse.Disconnected != nil {
sse.Disconnected(client)
}
}()
flusher, _ := w.(http.Flusher)
for {
select {
case message, ok := <-client:
if !ok {
return
}
fmt.Fprintf(w, "data: %s\n\n", message)
flusher.Flush()
case <-r.Context().Done():
return
}
}
}

@ -0,0 +1,34 @@
package serverevents_test
import (
"log"
"net/http"
"testing"
"time"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil/serverevents"
)
func TestSSE(t *testing.T) {
sse := serverevents.New(&serverevents.Config{
Connected: func(client chan string) {
go func() {
log.Printf(`New client connected callback`)
client <- "Messaggio 1 per questo client"
time.Sleep(1 * time.Second)
client <- "Messaggio 2 per questo client"
}()
},
})
go func() {
for {
log.Printf(`Broadcasting message`)
sse.Broadcast("Messaggio per tutti")
time.Sleep(1 * time.Second)
}
}()
http.Handle("/sse", sse)
http.ListenAndServe(":8000", nil)
}

@ -1,102 +0,0 @@
package httputil
import (
"fmt"
"log"
"net/http"
)
type SSEHandler struct {
clients map[chan string]bool
Connected func(chan string)
Disconnected func(chan string)
}
func (sse *SSEHandler) init() {
if sse.clients == nil {
sse.clients = make(map[chan string]bool)
}
}
func (sse *SSEHandler) Broadcast(message string) {
sse.init()
for client := range sse.clients {
client <- message
}
}
func (sse *SSEHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
sse.init()
w.Header().Set("Content-Type", "text/event-stream")
w.Header().Set("Cache-Control", "no-cache")
w.Header().Set("Connection", "keep-alive")
w.Header().Set("Access-Control-Allow-Origin", "*")
client := make(chan string)
sse.clients[client] = true
log.Printf(`New connection`)
if sse.Connected != nil {
go sse.Connected(client)
}
defer func() {
log.Printf(`Connection closed`)
close(client)
delete(sse.clients, client)
if sse.Disconnected != nil {
go sse.Disconnected(client)
}
}()
flusher, _ := w.(http.Flusher)
for {
select {
case message, ok := <-client:
if !ok {
return
}
fmt.Fprintf(w, "data: %s\n\n", message)
flusher.Flush()
case <-r.Context().Done():
return
}
}
}
// func HandleSSE(handler func(broadcast, single chan<- string)) http.Handler {
// broadcast := make(chan string)
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// w.Header().Set("Content-Type", "text/event-stream")
// w.Header().Set("Cache-Control", "no-cache")
// w.Header().Set("Connection", "keep-alive")
// w.Header().Set("Access-Control-Allow-Origin", "*")
// client := make(chan string)
// defer func() {
// close(client)
// }()
// go handler(broadcast, client)
// flusher, _ := w.(http.Flusher)
// for {
// select {
// case message := <-broadcast:
// fmt.Fprintf(w, "data: %s\n\n", message)
// flusher.Flush()
// case message := <-client:
// fmt.Fprintf(w, "data: %s\n\n", message)
// flusher.Flush()
// case <-r.Context().Done():
// return
// }
// }
// })
// }

@ -1,32 +0,0 @@
package httputil_test
import (
"log"
"net/http"
"testing"
"time"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil"
)
func TestSSE(t *testing.T) {
sse := &httputil.SSEHandler{
Connected: func(client chan string) {
log.Printf(`New client connected callback`)
client <- "Messaggio 1 per questo client"
time.Sleep(1 * time.Second)
client <- "Messaggio 2 per questo client"
},
}
go func() {
for {
log.Printf(`Broadcasting message`)
sse.Broadcast("Messaggio per tutti")
time.Sleep(1 * time.Second)
}
}()
http.Handle("/sse", sse)
http.ListenAndServe(":8000", nil)
}

@ -7,71 +7,93 @@ import (
"git.phc.dm.unipi.it/aziis98/posti-dm/server/auth"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/db"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/httputil/serverevents"
"git.phc.dm.unipi.it/aziis98/posti-dm/server/util"
"github.com/alecthomas/repr"
"github.com/go-chi/chi/v5"
)
type Server struct {
sessions map[string]*db.User
authService *auth.AuthService
roomServerEventStreams map[string]serverevents.Handler
Database db.Store
ApiRoute chi.Router
}
func NewServer() *Server {
server := &Server{
sessions: make(map[string]*db.User),
Database: db.NewInMemoryStore(),
ApiRoute: chi.NewRouter(),
}
server.authService = &auth.AuthService{
CheckUserPassword: func(userID, password string) error {
repr.Println("Sessions: ", server.sessions)
if password != "phc" {
return fmt.Errorf(`invalid password`)
}
return nil
},
UserPermissions: func(userID string) ([]string, error) {
user, err := server.Database.GetUser(userID)
if err != nil {
return nil, err
}
sessions := make(map[string]*db.User)
database := db.NewInMemoryStore()
return user.Permissions, nil
server := &Server{
Database: database,
authService: &auth.AuthService{
CheckUserPassword: func(userID, password string) error {
if password != "phc" {
return fmt.Errorf(`invalid password`)
}
// FIXME: al momento quando la password è giusta creiamo tutti gli account necessari
database.CreateUser(&db.User{
ID: userID,
Permissions: []string{},
})
return nil
},
UserPermissions: func(userID string) ([]string, error) {
user, err := database.GetUser(userID)
if err != nil {
return nil, err
}
return user.Permissions, nil
},
SessionTokenFromUser: func(userID string) (string, error) {
user, err := database.GetUser(userID)
if err != nil {
return "", err
}
token := util.RandomHash(10)
sessions[token] = user
repr.Println("Sessions: ", sessions)
return token, nil
},
UserFromSessionToken: func(session string) (string, error) {
user, present := sessions[session]
if !present {
return "", auth.ErrNoUserForSession
}
return user.ID, nil
},
AuthenticationFailed: func(err error) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusUnauthorized)
})
},
OtherError: func(err error) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
})
},
},
SessionTokenFromUser: func(userID string) (string, error) {
user, err := server.Database.GetUser(userID)
if err != nil {
return "", err
}
token := util.RandomHash(10)
server.sessions[token] = user
roomServerEventStreams: make(map[string]serverevents.Handler),
return token, nil
},
UserFromSessionToken: func(session string) (string, error) {
user, present := server.sessions[session]
if !present {
return "", auth.ErrNoUserForSession
}
ApiRoute: chi.NewRouter(),
}
return user.ID, nil
},
AuthenticationFailed: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, `not authenticated`, http.StatusUnauthorized)
}),
OtherError: func(err error) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, err.Error(), http.StatusInternalServerError)
})
},
roomIDs, _ := database.GetRooms()
for _, roomID := range roomIDs {
server.roomServerEventStreams[roomID] = serverevents.New(&serverevents.Config{
Connected: func(client chan string) {
server.ConnectedToRoom(roomID, client)
},
})
}
server.setupRoutes()
@ -81,7 +103,7 @@ func NewServer() *Server {
func (server *Server) setupRoutes() {
api := server.ApiRoute
db := server.Database
database := server.Database
// Authenticated Routes
@ -104,31 +126,116 @@ func (server *Server) setupRoutes() {
server.authService.Logout(w)
})
api.Get("/user", func(w http.ResponseWriter, r *http.Request) {
userID, err := server.authService.UserFromSession(r)
if err != nil {
httputil.WriteJSON(w, nil)
return
}
user, err := database.GetUser(userID)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
}
httputil.WriteJSON(w, user)
})
api.Get("/room/seats", func(w http.ResponseWriter, r *http.Request) {
roomID, ok := r.URL.Query()["id"]
if !ok {
http.Error(w, `missing room id`, http.StatusBadRequest)
}
room, err := database.GetRoom(roomID[0])
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
seats := []*db.Seat{}
for _, seatID := range room.SeatIDs {
seat, err := database.GetSeat(seatID)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
seats = append(seats, seat)
}
httputil.WriteJSON(w, seats)
})
api.HandleFunc("/room_events", func(w http.ResponseWriter, r *http.Request) {
roomID, ok := r.URL.Query()["id"]
if !ok {
http.Error(w, `missing room id`, http.StatusBadRequest)
return
}
server.roomServerEventStreams[roomID[0]].ServeHTTP(w, r)
})
api.With(server.authService.LoggedMiddleware()).
Get("/current-seat", func(w http.ResponseWriter, r *http.Request) {
Post("/seat/occupy", func(w http.ResponseWriter, r *http.Request) {
userID, err := server.authService.UserFromSession(r)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
http.Error(w, err.Error(), http.StatusUnauthorized)
return
}
seatID, ok := r.URL.Query()["id"]
if !ok {
http.Error(w, `missing seat id`, http.StatusBadRequest)
return
}
occupiedSeatID, err := db.GetUserSeat(userID)
seat, err := database.GetSeat(seatID[0])
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
repr.Println(userID, seatID)
if err := database.OccupySeat(seatID[0], userID); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
if len(occupiedSeatID) == 0 {
httputil.WriteJSON(w, nil)
server.roomServerEventStreams[seat.RoomID].Broadcast("refresh")
httputil.WriteJSON(w, "ok")
})
api.With(server.authService.LoggedMiddleware()).
Post("/seat/leave", func(w http.ResponseWriter, r *http.Request) {
seatID, ok := r.URL.Query()["id"]
if !ok {
http.Error(w, `missing seat id`, http.StatusBadRequest)
return
}
seat, err := db.GetSeat(occupiedSeatID[0])
seat, err := database.GetSeat(seatID[0])
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}
if err := database.FreeSeat(seatID[0]); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
httputil.WriteJSON(w, seat)
server.roomServerEventStreams[seat.RoomID].Broadcast("refresh")
httputil.WriteJSON(w, "ok")
})
}
func (server *Server) ConnectedToRoom(roomID string, client chan string) {
// go func() {
// for {
// client <- "hi from server!"
// time.Sleep(1 * time.Second)
// }
// }()
}

Loading…
Cancel
Save