refactor(client): ♻️ split source files for readability

This commit is contained in:
Julien Oculi 2024-06-25 14:58:06 +02:00
parent 479234ef1d
commit 95c471e5d8
3 changed files with 294 additions and 305 deletions

178
client/app.js Normal file
View file

@ -0,0 +1,178 @@
// Input handlers
document
.querySelector('#move-input')
.addEventListener('change', (event) =>
assignCommands('backward', 'forward', event)
)
document
.querySelector('#rotate-input')
.addEventListener('change', (event) =>
assignCommands('left', 'right', event)
)
// Buttons handlers
for (const button of document
.querySelector('#touch-controls')
.querySelectorAll('button')) {
button.addEventListener('click', () => {
const { command, value } = button.dataset
// Check datas are set for button
if (command === undefined || value === undefined) {
alert(
`Pas de command ou de valeur assigné au bouton "${button.title}"`
)
throw new Error(`no command or value assigned to ${button.title}`)
}
// Check command is valid
if (
!['forward', 'backward', 'left', 'right', 'stop'].includes(command)
) {
alert(
`La command "${command}" n'est pas valide en tant que ('forward', 'backward', 'left', 'right', 'stop')`
)
throw new Error(
`specified command "${command}" is not in ['forward', 'backward', 'left', 'right', 'stop']`
)
}
// Check value is valid finite number
if (!Number.isFinite(Number(value))) {
alert(`La valeur "${value}" n'est convertible en entier fini`)
throw new Error(
`value "${value}" cannot be casted to finite integer`
)
}
sendCommand(command, Number(value))
})
}
// Keyboard handler
document.addEventListener('keydown', ({ code }) => {
if (code === 'ArrowUp') {
return sendCommand('forward', 2)
}
if (code === 'ArrowDown') {
return sendCommand('backward', 2)
}
if (code === 'ArrowLeft') {
return sendCommand('left', 5)
}
if (code === 'ArrowRight') {
return sendCommand('right', 5)
}
if (code === 'Space') {
return sendCommand('stop', 0)
}
})
// Settings handler
document
.querySelector('#settings')
.addEventListener('submit', async (event) => {
// Don't interrupt event if not form submitting
if (!(event instanceof SubmitEvent)) return true
if (event.target === null) return true
// Disable form sending
event.preventDefault()
const form = new FormData(event.target)
const ipAddress = form.get('ip-address')
if (ipAddress === null) return
const endpoint = `http://${ipAddress}`
await testEndpoint(endpoint)
setEndpoint(endpoint)
})
/**
* A command string.
* @typedef {('forward' | 'backward' | 'left' | 'right' | 'stop')} Command
*/
/**
* Send command to the robot.
*
* @param {Command} command Command to send.
* @param {number} value Value of the command if needed.
*
* @returns {Promise<void>}
*/
async function sendCommand(command, value) {
const endpoint = getEndpoint()
const url = new URL(`/get?command=${command}&value=${value}`, endpoint)
const response = await fetch(url)
const text = await response.text()
console.log(text)
}
/**
* Get robot endpoint address.
*
* @returns {string}
*/
function getEndpoint() {
const endpoint = sessionStorage.getItem('robot-endpoint')
if (endpoint === null) {
alert("Aucune adresse IP n'a été renseignée !")
throw new Error('no given ip address')
}
return endpoint
}
/**
* Set robot endpoint address.
*
* @param {string} ip Robot ip.
*
* @returns void
*/
function setEndpoint(ip) {
sessionStorage.setItem('robot-endpoint', ip)
}
/**
* Test connection to endpoint.
*
* @param {string} endpoint Endpoint to test for.
*
* @returns {void}
* @throws {Error} Endpoint unreachable.
*/
async function testEndpoint(endpoint) {
try {
const response = await fetch(endpoint)
if (response.ok) return
} catch (cause) {
alert(`Impossible de joindre l'adresse "${endpoint}"`)
throw new Error(`unable to connect to robot at ${endpoint}`, {
cause,
})
}
}
/**
* Assign a command to an input.
* Do negative command if value < 0 else do positive command.
*
* @param {Command} negativeCommand Command if value < 0.
* @param {Command} positiveCommand Command if value >= 0.
* @param {event} event Event of the input.
*
* @returns {Promise<void>}
*/
function assignCommands(negativeCommand, positiveCommand, event) {
if (event.target === null) return
/** @type {HTMLInputElement} */
const target = event.target
const value = target.valueAsNumber
if (value < 0) {
return sendCommand(negativeCommand, Math.abs(value))
}
return sendCommand(positiveCommand, value)
}

View file

@ -3,6 +3,8 @@
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="stylesheet" href="style.css" />
<script type="module" src="app.js"></script>
<title>Contrôle du robot</title>
</head>
<body>
@ -104,309 +106,4 @@
</section>
</main>
</body>
<script type="module">
// Input handlers
document
.querySelector('#move-input')
.addEventListener('change', (event) =>
assignCommands('backward', 'forward', event)
)
document
.querySelector('#rotate-input')
.addEventListener('change', (event) =>
assignCommands('left', 'right', event)
)
// Buttons handlers
for (const button of document
.querySelector('#touch-controls')
.querySelectorAll('button')) {
button.addEventListener('click', () => {
const { command, value } = button.dataset
// Check datas are set for button
if (command === undefined || value === undefined) {
alert(
`Pas de command ou de valeur assigné au bouton "${button.title}"`
)
throw new Error(
`no command or value assigned to ${button.title}`
)
}
// Check command is valid
if (
!['forward', 'backward', 'left', 'right', 'stop'].includes(
command
)
) {
alert(
`La command "${command}" n'est pas valide en tant que ('forward', 'backward', 'left', 'right', 'stop')`
)
throw new Error(
`specified command "${command}" is not in ['forward', 'backward', 'left', 'right', 'stop']`
)
}
// Check value is valid finite number
if (!Number.isFinite(Number(value))) {
alert(
`La valeur "${value}" n'est convertible en entier fini`
)
throw new Error(
`value "${value}" cannot be casted to finite integer`
)
}
sendCommand(command, Number(value))
})
}
// Keyboard handler
document.addEventListener('keydown', ({ code }) => {
if (code === 'ArrowUp') {
return sendCommand('forward', 2)
}
if (code === 'ArrowDown') {
return sendCommand('backward', 2)
}
if (code === 'ArrowLeft') {
return sendCommand('left', 5)
}
if (code === 'ArrowRight') {
return sendCommand('right', 5)
}
if (code === 'Space') {
return sendCommand('stop', 0)
}
})
// Settings handler
document
.querySelector('#settings')
.addEventListener('submit', async (event) => {
// Don't interrupt event if not form submitting
if (!(event instanceof SubmitEvent)) return true
if (event.target === null) return true
// Disable form sending
event.preventDefault()
const form = new FormData(event.target)
const ipAddress = form.get('ip-address')
if (ipAddress === null) return
const endpoint = `http://${ipAddress}`
await testEndpoint(endpoint)
setEndpoint(endpoint)
})
/**
* A command string.
* @typedef {('forward' | 'backward' | 'left' | 'right' | 'stop')} Command
*/
/**
* Send command to the robot.
*
* @param {Command} command Command to send.
* @param {number} value Value of the command if needed.
*
* @returns {Promise<void>}
*/
async function sendCommand(command, value) {
const endpoint = getEndpoint()
const url = new URL(
`/get?command=${command}&value=${value}`,
endpoint
)
const response = await fetch(url)
const text = await response.text()
console.log(text)
}
/**
* Get robot endpoint address.
*
* @returns {string}
*/
function getEndpoint() {
const endpoint = sessionStorage.getItem('robot-endpoint')
if (endpoint === null) {
alert("Aucune adresse IP n'a été renseignée !")
throw new Error('no given ip address')
}
return endpoint
}
/**
* Set robot endpoint address.
*
* @param {string} ip Robot ip.
*
* @returns void
*/
function setEndpoint(ip) {
sessionStorage.setItem('robot-endpoint', ip)
}
/**
* Test connection to endpoint.
*
* @param {string} endpoint Endpoint to test for.
*
* @returns {void}
* @throws {Error} Endpoint unreachable.
*/
async function testEndpoint(endpoint) {
try {
const response = await fetch(endpoint)
if (response.ok) return
} catch (cause) {
alert(`Impossible de joindre l'adresse "${endpoint}"`)
throw new Error(`unable to connect to robot at ${endpoint}`, {
cause,
})
}
}
/**
* Assign a command to an input.
* Do negative command if value < 0 else do positive command.
*
* @param {Command} negativeCommand Command if value < 0.
* @param {Command} positiveCommand Command if value >= 0.
* @param {event} event Event of the input.
*
* @returns {Promise<void>}
*/
function assignCommands(negativeCommand, positiveCommand, event) {
if (event.target === null) return
/** @type {HTMLInputElement} */
const target = event.target
const value = target.valueAsNumber
if (value < 0) {
return sendCommand(negativeCommand, Math.abs(value))
}
return sendCommand(positiveCommand, value)
}
</script>
<style>
:root {
--translucent-high: rgba(255, 255, 255, 0.8);
--translucent-low: rgba(255, 255, 255, 0.4);
--padding: 0.5rem;
--padding-half: calc(var(--padding) / 2);
--border-size: 0.2rem;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
background-color: rgb(247, 234, 219);
height: 100dvh;
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont,
'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans',
'Helvetica Neue', sans-serif;
}
h1,
h2 {
margin: 0;
}
h1 {
text-align: center;
}
main {
padding: var(--padding);
height: 100%;
text-wrap: pretty;
}
.touch-controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--padding);
padding: var(--padding);
button {
height: 5.5rem;
font-size: 3rem;
border-radius: var(--padding);
}
}
button {
border: solid var(--border-size) var(--translucent-high);
background: var(--translucent-high);
border-radius: var(--padding-half);
transition: all 0.2s ease;
height: 100%;
font-size: 110%;
&:active {
border-color: var(--translucent-low);
background-color: var(--translucent-low);
}
}
.input-controls,
.settings {
padding: var(--padding);
display: flex;
gap: var(--padding);
border: solid var(--border-size) var(--translucent-high);
border-radius: var(--padding-half);
align-items: center;
justify-content: space-around;
}
.inputs {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(700px, 1fr));
gap: var(--padding);
width: 100%;
}
.infos {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(800px, 1fr));
flex-wrap: wrap;
gap: var(--padding);
justify-content: space-between;
}
label {
display: grid;
gap: var(--padding);
}
input {
border: solid var(--border-size) var(--translucent-high);
background: var(--translucent-low);
border-radius: var(--padding-half);
transition: all 0.2s ease;
height: 100%;
padding: var(--padding-half);
font-family: 'Courier New', Courier, monospace;
outline: none;
&:focus {
background-color: var(--translucent-high);
}
}
</style>
</html>

114
client/style.css Normal file
View file

@ -0,0 +1,114 @@
:root {
--translucent-high: rgba(255, 255, 255, 0.8);
--translucent-low: rgba(255, 255, 255, 0.4);
--padding: 0.5rem;
--padding-half: calc(var(--padding) / 2);
--border-size: 0.2rem;
}
* {
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
background-color: rgb(247, 234, 219);
height: 100dvh;
margin: 0;
padding: 0;
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI',
Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue',
sans-serif;
}
h1,
h2 {
margin: 0;
}
h1 {
text-align: center;
}
main {
padding: var(--padding);
height: 100%;
text-wrap: pretty;
}
.touch-controls {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: var(--padding);
padding: var(--padding);
button {
height: 5.5rem;
font-size: 3rem;
border-radius: var(--padding);
}
}
button {
border: solid var(--border-size) var(--translucent-high);
background: var(--translucent-high);
border-radius: var(--padding-half);
transition: all 0.2s ease;
height: 100%;
font-size: 110%;
&:active {
border-color: var(--translucent-low);
background-color: var(--translucent-low);
}
}
.input-controls,
.settings {
padding: var(--padding);
display: flex;
gap: var(--padding);
border: solid var(--border-size) var(--translucent-high);
border-radius: var(--padding-half);
align-items: center;
justify-content: space-around;
}
.inputs {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(700px, 1fr));
gap: var(--padding);
width: 100%;
}
.infos {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(800px, 1fr));
flex-wrap: wrap;
gap: var(--padding);
justify-content: space-between;
}
label {
display: grid;
gap: var(--padding);
}
input {
border: solid var(--border-size) var(--translucent-high);
background: var(--translucent-low);
border-radius: var(--padding-half);
transition: all 0.2s ease;
height: 100%;
padding: var(--padding-half);
font-family: 'Courier New', Courier, monospace;
outline: none;
&:focus {
background-color: var(--translucent-high);
}
}