1
Fork 0
mirror of https://github.com/Steffo99/todocolors.git synced 2024-10-16 07:17:28 +00:00

Make some frontend progress

This commit is contained in:
Steffo 2023-08-02 04:14:49 +02:00
parent 88b3ab1674
commit 3c6b9ba36e
Signed by: steffo
GPG key ID: 2A24051445686895
19 changed files with 473 additions and 111 deletions

View file

@ -17,6 +17,7 @@
"@types/node": "20.4.5",
"@types/react": "18.2.17",
"@types/react-dom": "18.2.7",
"classnames": "^2.3.2",
"next": "13.4.12",
"react": "18.2.0",
"react-dom": "18.2.0",

View file

@ -0,0 +1,38 @@
"use client";
import {useBoardCreator} from "@/app/useBoardCreator"
import {faKey} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {default as React} from "react"
export function CreatePrivateBoard() {
const {createBoard} = useBoardCreator();
return (
<form
className={"panel box form-flex"}
onSubmit={e => {
e.preventDefault();
createBoard(crypto.randomUUID().toString());
}}
>
<h3>
<FontAwesomeIcon icon={faKey} size={"1x"}/>
{" "}
Privato
</h3>
<p>
Crea un nuovo tabellone privato utilizzando un codice segreto autogenerato!
<br/>
<small>Esso sarà accessibile solo da chi ne conosce il link.</small>
</p>
<label className={"float-bottom"}>
<span/>
<button>
Crea
</button>
<span/>
</label>
</form>
)
}

View file

@ -0,0 +1,57 @@
"use client"
import {useBoardCreator} from "@/app/useBoardCreator"
import {useLowerKebabState} from "@/app/useKebabState"
import {faGlobe} from "@fortawesome/free-solid-svg-icons"
import {default as React} from "react"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
export function CreatePublicBoard() {
const [code, setCode] = useLowerKebabState("")
const {createBoard} = useBoardCreator();
return (
<form
className={"panel box form-flex"}
onSubmit={e => {
e.preventDefault();
createBoard(code);
}}
>
<h3>
<FontAwesomeIcon icon={faGlobe}/>
{" "}
Pubblico
</h3>
<p>
Crea un nuovo tabellone pubblico, con un codice personalizzato!
<br/>
<small>Se un tabellone con quel codice esiste già, sarai reindirizzato ad esso.</small>
</p>
<label className={"float-bottom"}>
<span>
Codice
</span>
<input
type={"text"}
placeholder={"garasauto-planning-2023"}
value={code}
onChange={(
e => setCode(e.target.value)
)}
/>
<span/>
</label>
<label>
<span/>
<button
onClick={_ => createBoard(code)}
>
Crea
</button>
<span/>
</label>
</form>
)
}

View file

@ -0,0 +1,65 @@
.boardHeader {
display: grid;
align-items: center;
padding-top: 4px;
}
@media screen and (max-width: 450px) {
.boardHeader {
grid-template-areas:
"left right"
"title title"
;
grid-template-columns: auto auto;
grid-template-rows: auto auto;
}
}
@media screen and (min-width: 451px) {
.boardHeader {
grid-template-areas:
"left title right"
;
grid-template-columns: auto 1fr auto;
grid-template-rows: auto;
}
}
.boardButtons {
width: auto;
min-width: unset;
display: flex;
gap: 4px;
}
.boardButtonsLeft {
grid-area: left;
justify-self: start;
}
.boardButtonsRight {
grid-area: right;
justify-self: end;
}
.boardTitle {
grid-area: title;
}
.boardButtons button {
display: inline-flex;
justify-content: center;
align-items: center;
background-color: hsla(
var(--bhsl-background-hue),
var(--bhsl-background-saturation),
var(--bhsl-background-lightness),
100%
);
width: 36px;
height: 36px;
}

View file

@ -1,15 +1,73 @@
'use client';
import {default as React} from "react";
import {useBoard} from "@/app/board/[board]/useBoard"
"use client";
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {useEffect} from "react"
import {useBoardWebSocket} from "@/app/board/[board]/useBoardWebSocket"
import style from "./page.module.css";
import classNames from "classnames"
import {faHouse, faPencil, faTableColumns, faArrowDownWideShort} from "@fortawesome/free-solid-svg-icons"
export default function Page({params: {board}}: {params: {board: string}}) {
const wsUrl = React.useMemo(() => `ws://127.0.0.1:8080/board/${board}/ws`, [board])
useBoard(wsUrl);
const {tasks, title, pushEvent, readyState} = useBoardWebSocket(board);
return (
<p>
TODO
</p>
)
useEffect(() => {
console.debug("[Page] Current events: ", tasks)
}, [tasks])
return <>
<header className={style.boardHeader}>
<div className={classNames(style.boardButtons, style.boardButtonsLeft)}>
<button title={"Home"}>
<FontAwesomeIcon icon={faHouse}/>
</button>
<button title={"Modifica titolo"}>
<FontAwesomeIcon icon={faPencil}/>
</button>
</div>
<h1 className={style.boardTitle}>
{title}
</h1>
<div className={classNames(style.boardButtons, style.boardButtonsRight)}>
<button title={"Cambia raggruppamento orizzontale"}>
<FontAwesomeIcon icon={faTableColumns}/>
</button>
<button title={"Cambia ordinamento verticale"}>
<FontAwesomeIcon icon={faArrowDownWideShort}/>
</button>
</div>
</header>
<main>
<div className={"chapter-5"}>
<div>
<h2>
Gruppo A
</h2>
</div>
<div>
<h2>
Gruppo B
</h2>
</div>
<div>
<h2>
Gruppo C
</h2>
</div>
<div>
<h2>
Gruppo D
</h2>
</div>
<div>
<h2>
Gruppo E
</h2>
</div>
</div>
</main>
<footer>
sos
</footer>
</>
}

View file

@ -0,0 +1,62 @@
export type TaskIcon =
"User" &
"Image" &
"Envelope" &
"Star" &
"Heart" &
"Comment" &
"FaceSmile" &
"File" &
"Bell" &
"Bookmark" &
"Eye" &
"Hand" &
"PaperPlane" &
"Handshake" &
"Sun" &
"Clock" &
"Circle" &
"Square" &
"Building" &
"Flag" &
"Moon";
export type TaskImportance =
"Highest" &
"High" &
"Normal" &
"Low" &
"Lowest";
export type TaskPriority =
"Highest" &
"High" &
"Normal" &
"Low" &
"Lowest";
export type TaskStatus =
"Unfinished" &
"InProgress" &
"Complete"
export type Task = {
text: string,
icon: TaskIcon,
importance: TaskImportance,
priority: TaskPriority,
status: TaskStatus,
}
export type TitleBoardAction = {
"Title": string,
}
export type TaskBoardAction = {
"Task": [
string,
Task,
]
}
export type BoardAction = TitleBoardAction & TaskBoardAction;

View file

@ -1,18 +0,0 @@
'use client';
import {default as React} from "react";
import {useWs} from "@/app/board/[board]/useWs"
export function useBoard(url: string) {
const socket = useWs(url, {
onopen: React.useCallback((sock: WebSocket, event: Event) => {
console.debug("[useBoard] Connected to the server!");
sock.send('{"Title": "sus"}')
}, []),
onmessage: React.useCallback((sock: WebSocket, event: MessageEvent) => {
const data = JSON.parse(event.data);
console.debug("[useBoard] Received ServerOperation: ", data);
}, [])
});
}

View file

@ -0,0 +1,57 @@
'use client';
import {BoardAction, Task} from "@/app/board/[board]/types"
import {useMemo, useCallback, useState} from "react"
import {useWebSocket} from "@/app/board/[board]/useWebSocket"
export function useBoardWebSocket(board: string) {
const url = useMemo(() => `ws://127.0.0.1:8080/board/${board}/ws`, [board]);
const [title, setTitle] = useState<string>("Nuovo tabellone");
const [tasks, setTasks] = useState<{[key: string]: Task}>(() => ({}));
const onopen = useCallback((sock: WebSocket, event: Event) => {
setTasks(() => ({}))
console.debug("[useBoardWebSocket] Connected to the websocket of board:", board);
}, [])
const onmessage = useCallback((sock: WebSocket, event: MessageEvent) => {
const data: BoardAction = JSON.parse(event.data);
console.debug("[useBoardWebSocket] Received:", data);
if(data["Title"] !== undefined) {
setTitle(data["Title"]);
}
else if(data["Task"] !== undefined) {
const id = data["Task"][0]
const task = data["Task"][1]
setTasks((prevTasks) => {
const tasks = {...prevTasks}
if(task === null) {
delete tasks[id]
}
else {
tasks[id] = task
}
return tasks
})
}
}, [])
const {websocket} = useWebSocket(url, {onopen, onmessage});
const readyState = websocket?.readyState;
const pushEvent = useCallback((data: any) => {
if(!websocket) {
console.warn("[useBoardWebSocket] Socket does not exist yet, cannot send:", data)
return;
}
if(readyState != 1) {
console.warn("[useBoardWebSocket] Socket isn't ready yet, cannot send:", data);
return;
}
console.debug("[useBoardWebSocket] Sending:", data);
websocket.send(JSON.stringify(data));
}, [websocket, readyState])
return {title, tasks, pushEvent, readyState}
}

View file

@ -0,0 +1,43 @@
'use client';
import {useState, useEffect, useCallback} from "react"
export interface WebSocketHandlers {
onclose?: (sock: WebSocket, event: CloseEvent) => void,
onerror?: (sock: WebSocket, event: Event) => void,
onmessage?: (sock: WebSocket, event: MessageEvent) => void,
onopen?: (sock: WebSocket, event: Event) => void,
}
export function useWebSocket(url: string, {onclose, onerror, onmessage, onopen}: WebSocketHandlers) {
const [websocket, setWebsocket] = useState<WebSocket | null>(null)
const [readyState, setReadyState] = useState<number>(0);
useEffect(() => {
console.debug("[useWebSocket] Creating websocket...");
const sock = new WebSocket(url);
setWebsocket(sock);
sock.onopen = (ev) => {
setReadyState(sock.readyState);
onopen?.(sock, ev);
}
sock.onclose = (ev) => {
setReadyState(sock.readyState);
onclose?.(sock, ev);
}
sock.onerror = (ev) => {
setReadyState(sock.readyState);
onerror?.(sock, ev);
}
sock.onmessage = (ev) => {
onmessage?.(sock, ev);
}
return () => {
console.debug("[useWebSocket] Closing websocket...");
sock.close();
setWebsocket(null);
}
}, [url, onclose, onerror, onmessage, onopen])
return {websocket, readyState}
}

View file

@ -1,30 +0,0 @@
'use client';
import {default as React} from "react";
export interface UseWsHandlers {
onclose?: (sock: WebSocket, event: CloseEvent) => void,
onerror?: (sock: WebSocket, event: Event) => void,
onmessage?: (sock: WebSocket, event: MessageEvent) => void,
onopen?: (sock: WebSocket, event: Event) => void,
}
export function useWs(url: string, {onclose, onerror, onmessage, onopen}: UseWsHandlers) {
const [websocket, setWebsocket] = React.useState<WebSocket | null>(null)
React.useEffect(() => {
const sock = new WebSocket(url);
sock.onclose = onclose ? (ev) => onclose(sock, ev) : null;
sock.onerror = onerror ? (ev) => onerror(sock, ev) : null;
sock.onmessage = onmessage ? (ev) => onmessage(sock, ev) : null;
sock.onopen = onopen ? (ev) => onopen(sock, ev) : null;
setWebsocket(sock);
return () => {
sock.close();
setWebsocket(null);
}
}, [url, onclose, onerror, onmessage, onopen])
return websocket
}

View file

@ -6,9 +6,13 @@ import "@steffo/bluelib/dist/glass.root.css"
import "@steffo/bluelib/dist/layouts-center.root.css"
import "@steffo/bluelib/dist/colors-royalblue.root.css"
import "@steffo/bluelib/dist/fonts-fira-ghpages.root.css"
import '@fortawesome/fontawesome-svg-core/styles.css';
import { config } from '@fortawesome/fontawesome-svg-core';
config.autoAddCss = false; /* eslint-disable import/first */
import type {Metadata as NextMetadata} from "next"
import {ReactNode} from "react"
import {default as React, ReactNode} from "react"
export const metadata: NextMetadata = {
@ -21,6 +25,13 @@ export default function RootLayout({children}: { children: ReactNode }) {
<html lang="en">
<body className={"theme-bluelib layout-center"}>
{children}
<footer>
<p>
© <a href="https://steffo.eu">Stefano Pigozzi</a> -
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPL 3.0</a> -
<a href="https://github.com/Steffo99/todocolors">GitHub</a>
</p>
</footer>
</body>
</html>
)

View file

@ -1,3 +1,5 @@
import {CreatePrivateBoard} from "@/app/CreatePrivateBoard"
import {CreatePublicBoard} from "@/app/CreatePublicBoard"
import {default as React} from "react";
export default function Page() {
@ -12,55 +14,9 @@ export default function Page() {
<h2>
Crea un nuovo tabellone
</h2>
<form className={"panel box form-flex"}>
<h3>
Pubblico
</h3>
<p>
Crea un nuovo tabellone pubblico, con un codice personalizzato!
<br/>
<small>Se un tabellone con quel codice esiste già, sarai reindirizzato ad esso.</small>
</p>
<label className={"float-bottom"}>
<span>
Codice
</span>
<input type={"text"} placeholder={"garas-auto-planning-2023"}/>
<span/>
</label>
<label>
<span/>
<button>
Crea
</button>
<span/>
</label>
</form>
<div className={"panel box form-flex"}>
<h3>
Privato
</h3>
<p>
Crea un nuovo tabellone privato utilizzando un codice segreto autogenerato!
<br/>
<small>Esso sarà accessibile solo da chi ne conosce il link.</small>
</p>
<label className={"float-bottom"}>
<span/>
<button>
Crea
</button>
<span/>
</label>
</div>
<CreatePublicBoard/>
<CreatePrivateBoard/>
</div>
</main>
<footer>
<p>
© <a href="https://steffo.eu">Stefano Pigozzi</a> -
<a href="https://www.gnu.org/licenses/agpl-3.0.en.html">AGPL 3.0</a> -
<a href="https://github.com/Steffo99/todocolors">GitHub</a>
</p>
</footer>
</>
}

View file

@ -0,0 +1,13 @@
"use client";
import {useCallback} from "react"
import {useRouter} from "next/navigation"
export function useBoardCreator() {
const router = useRouter();
const createBoard = useCallback((board: String) => {
router.push(`/board/${board}`);
}, [])
return {createBoard}
}

View file

@ -0,0 +1,38 @@
"use client";
import {useCallback, useState} from "react"
const KEBABIFIER = /[^a-zA-Z0-9-]/g
export function useAnyKebabState(initial: string): [string, (inputString: string) => void] {
const [state, setInnerState] = useState<string>(initial);
const setState = useCallback((inputString: string) => {
const kebabifiedString = inputString.replaceAll(KEBABIFIER, "-");
setInnerState(kebabifiedString);
}, [])
return [state, setState]
}
export function useLowerKebabState(initial: string): [string, (inputString: string) => void] {
const [state, setInnerState] = useState<string>(initial);
const setState = useCallback((inputString: string) => {
const kebabifiedString = inputString.toLowerCase().replaceAll(KEBABIFIER, "-");
setInnerState(kebabifiedString);
}, [])
return [state, setState]
}
export function useUpperKebabState(initial: string): [string, (inputString: string) => void] {
const [state, setInnerState] = useState<string>(initial);
const setState = useCallback((inputString: string) => {
const kebabifiedString = inputString.toUpperCase().replaceAll(KEBABIFIER, "-");
setInnerState(kebabifiedString);
}, [])
return [state, setState]
}

View file

@ -2,10 +2,7 @@
<module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/node_modules" />
</content>
<content url="file://$MODULE_DIR$" />
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>

View file

@ -140,6 +140,11 @@ caniuse-lite@^1.0.30001406:
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8"
integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==
classnames@^2.3.2:
version "2.3.2"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
client-only@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"

2
todored/Cargo.lock generated
View file

@ -1071,11 +1071,13 @@ dependencies = [
"axum",
"deadqueue",
"futures-util",
"lazy_static",
"log",
"micronfig",
"pkg-version",
"pretty_env_logger",
"redis",
"regex",
"serde",
"serde_json",
"tokio",

View file

@ -9,11 +9,13 @@ edition = "2021"
axum = { version = "0.6.19", features = ["ws"] }
deadqueue = "0.2.4"
futures-util = "0.3.28"
lazy_static = "1.4.0"
log = "0.4.19"
micronfig = "0.2.0"
pkg-version = "1.0.0"
pretty_env_logger = "0.5.0"
redis = { version = "0.23.1", features = ["r2d2", "ahash", "cluster", "tokio-comp", "connection-manager"] }
regex = "1.9.1"
serde = { version = "1.0.178", features = ["derive"] }
serde_json = "1.0.104"
tokio = { version = "1.29.1", features = ["macros", "rt-multi-thread"] }

View file

@ -1,6 +1,9 @@
use axum::Extension;
use axum::extract::{Path, WebSocketUpgrade};
use uuid::Uuid;
use crate::kebab::Skewer;
use crate::routes::board::structs::{BoardAction, BoardRequest};
use crate::task::{Task, TaskIcon, TaskImportance, TaskPriority, TaskStatus};
use super::ws;
pub(crate) async fn handler(
@ -12,6 +15,8 @@ pub(crate) async fn handler(
let board = board.to_kebab_lowercase();
log::trace!("Kebabified board name to: {board:?}");
log::info!("{}", serde_json::ser::to_string(&BoardAction::Task(Some(Uuid::new_v4()), Some(Task { text: "Hello world".to_string(), icon: TaskIcon::Bell, importance: TaskImportance::High, priority: TaskPriority::Highest, status: TaskStatus::Complete }))).unwrap());
log::trace!("Received websocket request, upgrading...");
upgrade_request.on_upgrade(|websocket| ws::handler(board, rclient, websocket))
}