1
Fork 0
mirror of https://github.com/Steffo99/todocolors.git synced 2024-11-22 08:14:18 +00:00

Refactor code

This commit is contained in:
Steffo 2023-08-03 02:23:15 +02:00
parent 7549d4e1e3
commit 0865ca94e7
Signed by: steffo
GPG key ID: 2A24051445686895
22 changed files with 464 additions and 176 deletions

View file

@ -0,0 +1,20 @@
import {BoardError} from "@/app/board/[board]/BoardError"
import {BoardLoading} from "@/app/board/[board]/BoardLoading"
import {useBoardContext} from "@/app/board/[board]/useBoardContext"
export function BoardBody() {
const {websocketState} = useBoardContext()
switch(websocketState) {
case undefined:
return <BoardLoading text={"Caricamento..."}/>
case WebSocket.CONNECTING:
return <BoardLoading text={"Connessione..."}/>
case WebSocket.OPEN:
return <>nothing here</>
case WebSocket.CLOSING:
case WebSocket.CLOSED:
return <BoardError text={"Errore"}/>
}
}

View file

@ -0,0 +1,4 @@
import {UseBoardReturns} from "@/app/board/[board]/useBoard"
import {createContext, useContext} from "react"
export const BoardContext = createContext<UseBoardReturns | null>(null);

View file

@ -0,0 +1,7 @@
.boardError {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
}

View file

@ -0,0 +1,18 @@
import style from "./BoardError.module.css"
import {faExclamationCircle} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import classNames from "classnames"
export function BoardError({text}: {text: string}) {
return (
<main className={classNames("red", style.boardError)}>
<div>
<FontAwesomeIcon size={"4x"} icon={faExclamationCircle}/>
</div>
<div>
{text}
</div>
</main>
)
}

View file

@ -2,6 +2,8 @@
display: grid;
align-items: center;
padding-top: 4px;
grid-row-gap: 4px;
grid-column-gap: 10px;
}
@media screen and (max-width: 450px) {
@ -12,7 +14,6 @@
;
grid-template-columns: auto auto;
grid-template-rows: auto auto;
}
}
@ -46,6 +47,19 @@
.boardTitle {
grid-area: title;
height: 80px;
}
.boardTitle div {
padding-top: 0.125em;
padding-right: 0.75ex;
padding-left: 0.75ex;
padding-bottom: calc(0.125em + 2px);
}
.boardTitle input {
text-align: center;
width: 100%;
}
.boardButtons button {

View file

@ -0,0 +1,40 @@
import style from "./BoardHeader.module.css"
import {useBoardContext} from "@/app/board/[board]/useBoardContext"
import {faArrowDownWideShort, faHouse, faPencil, faTableColumns} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames"
export function BoardHeader() {
const {title, isEditingTitle, editTitle, setEditTitle, toggleEditingTitle, nextGrouper, nextSorter, websocketState} = useBoardContext();
const isReady = websocketState === WebSocket.OPEN
return (
<header className={style.boardHeader}>
<div className={cn(style.boardButtons, style.boardButtonsLeft)}>
<button title={"Home"}>
<FontAwesomeIcon icon={faHouse}/>
</button>
<button className={cn({fade: !isReady})} disabled={!isReady} title={"Modifica titolo"} onClick={isReady ? toggleEditingTitle : undefined}>
<FontAwesomeIcon icon={faPencil}/>
</button>
</div>
<h1 className={cn({fade: !isReady, [style.boardTitle]: true})}>
{isEditingTitle ?
<input type={"text"} placeholder={"Titolo"} onChange={(e) => setEditTitle(e.target.value)} value={editTitle}/>
:
<div>{title}</div>
}
</h1>
<div className={cn(style.boardButtons, style.boardButtonsRight)}>
<button title={"Cambia raggruppamento orizzontale"} onClick={nextGrouper}>
<FontAwesomeIcon icon={faTableColumns}/>
</button>
<button title={"Cambia ordinamento verticale"} onClick={nextSorter}>
<FontAwesomeIcon icon={faArrowDownWideShort}/>
</button>
</div>
</header>
)
}

View file

@ -0,0 +1,7 @@
.boardLoading {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: 8px;
}

View file

@ -0,0 +1,17 @@
import style from "./BoardLoading.module.css"
import {faSpinner} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
export function BoardLoading({text}: {text: string}) {
return (
<main className={style.boardLoading}>
<div>
<FontAwesomeIcon size={"4x"} icon={faSpinner} pulse/>
</div>
<div>
{text}
</div>
</main>
)
}

View file

@ -0,0 +1,14 @@
import {BoardContext} from "@/app/board/[board]/BoardContext"
import {useBoard} from "@/app/board/[board]/useBoard"
import {ReactNode} from "react"
export function BoardManager({name, children}: {name: string, children: ReactNode}) {
const context = useBoard(name);
return (
<BoardContext.Provider value={context}>
{children}
</BoardContext.Provider>
)
}

View file

@ -1,20 +0,0 @@
import {Task} from "@/app/board/[board]/types"
export function groupAndSortTasks(tasks: Task[], grouping: (a: Task) => string, sorting: (a: Task, b: Task) => number) {
const groups: {[group: string]: Task[]} = {}
for(const task of tasks) {
const group = grouping(task);
if(!groups[group]) {
groups[group] = [];
}
groups[group].push(task);
}
for(const group of Object.keys(groups)) {
groups[group].sort(sorting);
}
return groups;
}

View file

@ -1,73 +1,15 @@
"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"
import {BoardBody} from "@/app/board/[board]/BoardBody"
import {BoardManager} from "@/app/board/[board]/BoardManager"
import {BoardHeader} from "@/app/board/[board]/BoardHeader"
export default function Page({params: {board}}: {params: {board: string}}) {
const {tasks, title, pushEvent, readyState} = useBoardWebSocket(board);
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>
</>
return (
<BoardManager name={board}>
<BoardHeader/>
<BoardBody/>
</BoardManager>
)
}

View file

@ -1,44 +1,44 @@
export type TaskIcon =
"User" &
"Image" &
"Envelope" &
"Star" &
"Heart" &
"Comment" &
"FaceSmile" &
"File" &
"Bell" &
"Bookmark" &
"Eye" &
"Hand" &
"PaperPlane" &
"Handshake" &
"Sun" &
"Clock" &
"Circle" &
"Square" &
"Building" &
"Flag" &
"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" &
"Highest" |
"High" |
"Normal" |
"Low" |
"Lowest";
export type TaskPriority =
"Highest" &
"High" &
"Normal" &
"Low" &
"Highest" |
"High" |
"Normal" |
"Low" |
"Lowest";
export type TaskStatus =
"Unfinished" &
"InProgress" &
"Complete"
"Unfinished" |
"InProgress" |
"Complete";
export type Task = {
text: string,
@ -48,6 +48,10 @@ export type Task = {
status: TaskStatus,
}
export type TaskWithId = Task & {
id: string,
}
export type TitleBoardAction = {
"Title": string,
}
@ -59,4 +63,6 @@ export type TaskBoardAction = {
]
}
export type BoardAction = TitleBoardAction & TaskBoardAction;
export type BoardAction =
TitleBoardAction |
TaskBoardAction;

View file

@ -0,0 +1,67 @@
"use client";
import {TaskWithId} from "@/app/board/[board]/types"
import {useBoardWebSocket} from "@/app/board/[board]/useBoardWebSocket"
import {GroupSortingFunction, TaskGroup, TaskGroupingFunction, TaskSortingFunction, useBoardTaskArranger} from "@/app/board/[board]/useBoardTaskArranger"
import {useBoardTitleEditor} from "@/app/board/[board]/useBoardTitleEditor"
import {useCycleState} from "@/app/useCycleState"
import {Dispatch, SetStateAction} from "react"
function groupTasksByIcon(a: TaskWithId) {return a.icon}
function sortGroupsByKey(a: TaskGroup, b: TaskGroup) {return a.key.localeCompare(b.key)}
const TASK_GROUPERS: [TaskGroupingFunction, GroupSortingFunction][] = [
[groupTasksByIcon, sortGroupsByKey],
]
function sortTasksByText(a: TaskWithId, b: TaskWithId) {return a.text.localeCompare(b.text)}
const TASK_SORTERS: TaskSortingFunction[] = [
sortTasksByText,
];
export interface UseBoardReturns {
title: string,
taskGroups: TaskGroup[],
websocketState: number,
isEditingTitle: boolean,
stopEditingTitle: () => void,
startEditingTitle: () => void,
toggleEditingTitle: () => void,
moveGrouper: (n: number) => void,
nextGrouper: () => void,
previousGrouper: () => void,
moveSorter: (n: number) => void,
nextSorter: () => void,
previousSorter: () => void,
editTitle: string,
setEditTitle: Dispatch<SetStateAction<string>>,
}
export function useBoard(name: string): UseBoardReturns {
const {state: {title, tasksById}, send, websocketState} = useBoardWebSocket(name);
const {value: [taskGrouper, groupSorter], move: moveGrouper, next: nextGrouper, previous: previousGrouper} = useCycleState(TASK_GROUPERS);
const {value: taskSorter, move: moveSorter, next: nextSorter, previous: previousSorter} = useCycleState(TASK_SORTERS);
const {taskGroups} = useBoardTaskArranger(tasksById, taskGrouper, groupSorter, taskSorter);
const {isEditingTitle, stopEditingTitle, startEditingTitle, toggleEditingTitle, editTitle, setEditTitle} = useBoardTitleEditor(title, send);
return {
title,
taskGroups,
websocketState,
isEditingTitle,
stopEditingTitle,
startEditingTitle,
toggleEditingTitle,
moveGrouper,
nextGrouper,
previousGrouper,
moveSorter,
nextSorter,
previousSorter,
editTitle,
setEditTitle,
}
}

View file

@ -0,0 +1,14 @@
import {BoardContext} from "@/app/board/[board]/BoardContext"
import {UseBoardReturns} from "@/app/board/[board]/useBoard"
import {useContext} from "react"
export function useBoardContext(): UseBoardReturns {
const context = useContext(BoardContext);
if(context === null) {
throw Error()
}
return context
}

View file

@ -0,0 +1,48 @@
"use client";
import {BoardAction, Task, TaskBoardAction, TitleBoardAction} from "@/app/board/[board]/types"
import {Reducer, useReducer} from "react"
type BoardState = {
title: string,
tasksById: {[key: string]: Task},
}
function boardReducer(state: BoardState, action: BoardAction | null) {
if(action === null) {
console.debug("[boardReducer] Initializing state...");
return {title: "", tasksById: {}}
}
else if(Object.hasOwn(action, "Title")) {
const titleAction = action as TitleBoardAction;
const title = titleAction["Title"]
console.debug("[boardReducer] Setting board title to:", title)
return {...state, title}
}
else if(Object.hasOwn(action, "Task")) {
const taskAction = action as TaskBoardAction;
const id = taskAction["Task"][0]
const task = taskAction["Task"][1]
const tasksById = {...state.tasksById}
if(task === null) {
console.debug("[boardReducer] Deleting task:", id)
delete tasksById[id]
}
else {
console.debug("[boardReducer] Putting task:", id)
tasksById[id] = task
}
return {...state, tasksById}
}
else {
console.warn("[boardReducer] Received unknown action, ignoring:", action)
return state
}
}
export function useBoardState() {
const [state, act] = useReducer<Reducer<BoardState, BoardAction | null>>(boardReducer, {title: "", tasksById: {}})
return {state, act}
}

View file

@ -0,0 +1,47 @@
"use client";
import {Task, TaskWithId} from "@/app/board/[board]/types"
import {useMemo} from "react"
export type TaskGroup = {
key: string,
tasks: TaskWithId[],
}
export type TaskGroupingFunction = (a: TaskWithId) => string
export type TaskSortingFunction = (a: TaskWithId, b: TaskWithId) => number;
export type GroupSortingFunction = (a: TaskGroup, b: TaskGroup) => number;
export function arrangeBoardTasks(tasksById: { [p: string]: Task }, taskGrouper: TaskGroupingFunction, groupSorter: GroupSortingFunction, taskSorter: TaskSortingFunction): TaskGroup[] {
const groupsByKey: {[group: string]: TaskWithId[]} = {}
for(const [id, task] of Object.entries(tasksById)) {
const taskWithId = {...task, id};
const group = taskGrouper(taskWithId);
if(!groupsByKey[group]) {
groupsByKey[group] = [];
}
groupsByKey[group].push(taskWithId);
}
for(const group of Object.keys(groupsByKey)) {
groupsByKey[group].sort(taskSorter);
}
const groups: TaskGroup[] = []
for(const [key, tasks] of Object.entries(groupsByKey)) {
groups.push({key, tasks})
}
groups.sort(groupSorter)
return groups;
}
export function useBoardTaskArranger(tasksById: { [p: string]: Task }, taskGrouper: TaskGroupingFunction, groupSorter: GroupSortingFunction, taskSorter: TaskSortingFunction) {
const taskGroups = useMemo(() => arrangeBoardTasks(tasksById, taskGrouper, groupSorter, taskSorter), [tasksById, taskGrouper, taskSorter, groupSorter])
return {taskGroups};
}

View file

@ -0,0 +1,31 @@
"use client";
import {BoardAction} from "@/app/board/[board]/types"
import {useCallback, useState} from "react"
export function useBoardTitleEditor(title: string, send: (action: BoardAction) => void) {
const [isEditingTitle, setEditingTitle] = useState<boolean>(false)
const [editTitle, setEditTitle] = useState<string>(title)
const startEditingTitle = useCallback(() => {
console.debug("[useEditableTitle] Starting title edit...");
setEditingTitle(true);
setEditTitle(title);
}, [title])
const stopEditingTitle = useCallback(() => {
console.debug("[useEditableTitle] Ending title edit...");
setEditingTitle(false);
if(editTitle) {
console.debug("[useEditableTitle] Sending title change event...");
send({"Title": editTitle})
}
}, [send, editTitle])
const toggleEditingTitle = useCallback(() => {
return isEditingTitle ? stopEditingTitle() : startEditingTitle()
}, [isEditingTitle, stopEditingTitle, startEditingTitle])
return {isEditingTitle, startEditingTitle, stopEditingTitle, toggleEditingTitle, editTitle, setEditTitle}
}

View file

@ -1,57 +1,36 @@
'use client';
import {BoardAction, Task} from "@/app/board/[board]/types"
import {useMemo, useCallback, useState} from "react"
import {useWebSocket} from "@/app/board/[board]/useWebSocket"
import {BoardAction} from "@/app/board/[board]/types"
import {useBoardState} from "@/app/board/[board]/useBoardState"
import {useBoardWebSocketURL} from "@/app/board/[board]/useBoardWebSocketURL"
import {useCallback} from "react"
import {useWebSocket} from "@/app/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}>(() => ({}));
export function useBoardWebSocket(name: string) {
const {webSocketURL} = useBoardWebSocketURL(name)
const {state, act} = useBoardState();
const onopen = useCallback((sock: WebSocket, event: Event) => {
setTasks(() => ({}))
console.debug("[useBoardWebSocket] Connected to the websocket of board:", board);
}, [])
const {websocket, websocketState} = useWebSocket(webSocketURL, {
onopen: useCallback((_sock: WebSocket, _event: Event) => {
console.debug("[useBoard] Connected to board:", name);
act(null);
}, []),
onmessage: useCallback((_sock: WebSocket, event: MessageEvent) => {
const action: BoardAction = JSON.parse(event.data);
console.debug("[useBoard] Received:", action);
act(action)
}, []),
});
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);
const send = useCallback((data: BoardAction) => {
if(!websocket || websocketState !== WebSocket.OPEN) {
console.warn("[useBoardWebSocket] Webbsocket is not yet ready, cannot send:", data);
return;
}
console.debug("[useBoardWebSocket] Sending:", data);
websocket.send(JSON.stringify(data));
}, [websocket, readyState])
}, [websocket, websocketState])
return {title, tasks, pushEvent, readyState}
return {state, send, websocketState}
}

View file

@ -0,0 +1,7 @@
import {useMemo} from "react"
export function useBoardWebSocketURL(name: string) {
const webSocketURL = useMemo(() => `ws://127.0.0.1:8080/board/${name}/ws`, [name]);
return {webSocketURL}
}

View file

@ -0,0 +1,17 @@
import {useCallback, useMemo, useState} from "react"
export function useCycleState(items: any[]) {
const [index, setIndex] = useState<number>(0);
const value = useMemo(() => items[index], [index])
const move = useCallback((num: number) => {
setIndex((prevIndex) => (prevIndex + num) % items.length);
}, [items])
const next = useCallback(() => move(1), [move]);
const previous = useCallback(() => move(-1), [move]);
return {index, value, move, next, previous}
}

View file

@ -11,22 +11,22 @@ export interface WebSocketHandlers {
export function useWebSocket(url: string, {onclose, onerror, onmessage, onopen}: WebSocketHandlers) {
const [websocket, setWebsocket] = useState<WebSocket | null>(null)
const [readyState, setReadyState] = useState<number>(0);
const [websocketState, setWebsocketState] = useState<number>(0);
useEffect(() => {
console.debug("[useWebSocket] Creating websocket...");
const sock = new WebSocket(url);
setWebsocket(sock);
sock.onopen = (ev) => {
setReadyState(sock.readyState);
setWebsocketState(sock.readyState);
onopen?.(sock, ev);
}
sock.onclose = (ev) => {
setReadyState(sock.readyState);
setWebsocketState(sock.readyState);
onclose?.(sock, ev);
}
sock.onerror = (ev) => {
setReadyState(sock.readyState);
setWebsocketState(sock.readyState);
onerror?.(sock, ev);
}
sock.onmessage = (ev) => {
@ -39,5 +39,5 @@ export function useWebSocket(url: string, {onclose, onerror, onmessage, onopen}:
}
}, [url, onclose, onerror, onmessage, onopen])
return {websocket, readyState}
return {websocket, websocketState}
}

View file

@ -114,7 +114,16 @@
dependencies:
"@types/react" "*"
"@types/react@*", "@types/react@18.2.17":
"@types/react@*":
version "18.2.18"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.18.tgz#c8b233919eef1bdc294f6f34b37f9727ad677516"
integrity sha512-da4NTSeBv/P34xoZPhtcLkmZuJ+oYaCxHmyHzwaDQo9RQPBeXV+06gEk2FpqEcsX9XrnNLvRpVh6bdavDSjtiQ==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/react@18.2.17":
version "18.2.17"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.17.tgz#baa565b17ddb649c2dac85b5eaf9e9a1fe0f3b4e"
integrity sha512-u+e7OlgPPh+aryjOm5UJMX32OvB2E3QASOAqVMY6Ahs90djagxwv2ya0IctglNbNTexC12qCSMZG47KPfy1hAA==
@ -136,9 +145,9 @@ busboy@1.6.0:
streamsearch "^1.1.0"
caniuse-lite@^1.0.30001406:
version "1.0.30001517"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8"
integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==
version "1.0.30001518"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001518.tgz#b3ca93904cb4699c01218246c4d77a71dbe97150"
integrity sha512-rup09/e3I0BKjncL+FesTayKtPrdwKhUufQFd3riFw1hHg8JmIFoInYfB102cFcY/pPgGmdyl/iy+jgiDi2vdA==
classnames@^2.3.2:
version "2.3.2"