mirror of
https://github.com/Steffo99/todocolors.git
synced 2024-11-22 16:24:19 +00:00
Refactor code
This commit is contained in:
parent
7549d4e1e3
commit
0865ca94e7
22 changed files with 464 additions and 176 deletions
20
todoblue/src/app/board/[board]/BoardBody.tsx
Normal file
20
todoblue/src/app/board/[board]/BoardBody.tsx
Normal 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"}/>
|
||||||
|
}
|
||||||
|
}
|
4
todoblue/src/app/board/[board]/BoardContext.tsx
Normal file
4
todoblue/src/app/board/[board]/BoardContext.tsx
Normal file
|
@ -0,0 +1,4 @@
|
||||||
|
import {UseBoardReturns} from "@/app/board/[board]/useBoard"
|
||||||
|
import {createContext, useContext} from "react"
|
||||||
|
|
||||||
|
export const BoardContext = createContext<UseBoardReturns | null>(null);
|
7
todoblue/src/app/board/[board]/BoardError.module.css
Normal file
7
todoblue/src/app/board/[board]/BoardError.module.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.boardError {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
18
todoblue/src/app/board/[board]/BoardError.tsx
Normal file
18
todoblue/src/app/board/[board]/BoardError.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -2,6 +2,8 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding-top: 4px;
|
padding-top: 4px;
|
||||||
|
grid-row-gap: 4px;
|
||||||
|
grid-column-gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media screen and (max-width: 450px) {
|
@media screen and (max-width: 450px) {
|
||||||
|
@ -12,7 +14,6 @@
|
||||||
;
|
;
|
||||||
grid-template-columns: auto auto;
|
grid-template-columns: auto auto;
|
||||||
grid-template-rows: auto auto;
|
grid-template-rows: auto auto;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,6 +47,19 @@
|
||||||
|
|
||||||
.boardTitle {
|
.boardTitle {
|
||||||
grid-area: title;
|
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 {
|
.boardButtons button {
|
40
todoblue/src/app/board/[board]/BoardHeader.tsx
Normal file
40
todoblue/src/app/board/[board]/BoardHeader.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
7
todoblue/src/app/board/[board]/BoardLoading.module.css
Normal file
7
todoblue/src/app/board/[board]/BoardLoading.module.css
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
.boardLoading {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
17
todoblue/src/app/board/[board]/BoardLoading.tsx
Normal file
17
todoblue/src/app/board/[board]/BoardLoading.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
14
todoblue/src/app/board/[board]/BoardManager.tsx
Normal file
14
todoblue/src/app/board/[board]/BoardManager.tsx
Normal 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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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;
|
|
||||||
}
|
|
|
@ -1,73 +1,15 @@
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
import {BoardBody} from "@/app/board/[board]/BoardBody"
|
||||||
import {useEffect} from "react"
|
import {BoardManager} from "@/app/board/[board]/BoardManager"
|
||||||
import {useBoardWebSocket} from "@/app/board/[board]/useBoardWebSocket"
|
import {BoardHeader} from "@/app/board/[board]/BoardHeader"
|
||||||
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}}) {
|
export default function Page({params: {board}}: {params: {board: string}}) {
|
||||||
const {tasks, title, pushEvent, readyState} = useBoardWebSocket(board);
|
return (
|
||||||
|
<BoardManager name={board}>
|
||||||
useEffect(() => {
|
<BoardHeader/>
|
||||||
console.debug("[Page] Current events: ", tasks)
|
<BoardBody/>
|
||||||
}, [tasks])
|
</BoardManager>
|
||||||
|
)
|
||||||
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>
|
|
||||||
</>
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,44 +1,44 @@
|
||||||
export type TaskIcon =
|
export type TaskIcon =
|
||||||
"User" &
|
"User" |
|
||||||
"Image" &
|
"Image" |
|
||||||
"Envelope" &
|
"Envelope" |
|
||||||
"Star" &
|
"Star" |
|
||||||
"Heart" &
|
"Heart" |
|
||||||
"Comment" &
|
"Comment" |
|
||||||
"FaceSmile" &
|
"FaceSmile" |
|
||||||
"File" &
|
"File" |
|
||||||
"Bell" &
|
"Bell" |
|
||||||
"Bookmark" &
|
"Bookmark" |
|
||||||
"Eye" &
|
"Eye" |
|
||||||
"Hand" &
|
"Hand" |
|
||||||
"PaperPlane" &
|
"PaperPlane" |
|
||||||
"Handshake" &
|
"Handshake" |
|
||||||
"Sun" &
|
"Sun" |
|
||||||
"Clock" &
|
"Clock" |
|
||||||
"Circle" &
|
"Circle" |
|
||||||
"Square" &
|
"Square" |
|
||||||
"Building" &
|
"Building" |
|
||||||
"Flag" &
|
"Flag" |
|
||||||
"Moon";
|
"Moon";
|
||||||
|
|
||||||
export type TaskImportance =
|
export type TaskImportance =
|
||||||
"Highest" &
|
"Highest" |
|
||||||
"High" &
|
"High" |
|
||||||
"Normal" &
|
"Normal" |
|
||||||
"Low" &
|
"Low" |
|
||||||
"Lowest";
|
"Lowest";
|
||||||
|
|
||||||
export type TaskPriority =
|
export type TaskPriority =
|
||||||
"Highest" &
|
"Highest" |
|
||||||
"High" &
|
"High" |
|
||||||
"Normal" &
|
"Normal" |
|
||||||
"Low" &
|
"Low" |
|
||||||
"Lowest";
|
"Lowest";
|
||||||
|
|
||||||
export type TaskStatus =
|
export type TaskStatus =
|
||||||
"Unfinished" &
|
"Unfinished" |
|
||||||
"InProgress" &
|
"InProgress" |
|
||||||
"Complete"
|
"Complete";
|
||||||
|
|
||||||
export type Task = {
|
export type Task = {
|
||||||
text: string,
|
text: string,
|
||||||
|
@ -48,6 +48,10 @@ export type Task = {
|
||||||
status: TaskStatus,
|
status: TaskStatus,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type TaskWithId = Task & {
|
||||||
|
id: string,
|
||||||
|
}
|
||||||
|
|
||||||
export type TitleBoardAction = {
|
export type TitleBoardAction = {
|
||||||
"Title": string,
|
"Title": string,
|
||||||
}
|
}
|
||||||
|
@ -59,4 +63,6 @@ export type TaskBoardAction = {
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
export type BoardAction = TitleBoardAction & TaskBoardAction;
|
export type BoardAction =
|
||||||
|
TitleBoardAction |
|
||||||
|
TaskBoardAction;
|
||||||
|
|
67
todoblue/src/app/board/[board]/useBoard.ts
Normal file
67
todoblue/src/app/board/[board]/useBoard.ts
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
14
todoblue/src/app/board/[board]/useBoardContext.ts
Normal file
14
todoblue/src/app/board/[board]/useBoardContext.ts
Normal 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
|
||||||
|
}
|
48
todoblue/src/app/board/[board]/useBoardState.ts
Normal file
48
todoblue/src/app/board/[board]/useBoardState.ts
Normal 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}
|
||||||
|
}
|
47
todoblue/src/app/board/[board]/useBoardTaskArranger.ts
Normal file
47
todoblue/src/app/board/[board]/useBoardTaskArranger.ts
Normal 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};
|
||||||
|
}
|
31
todoblue/src/app/board/[board]/useBoardTitleEditor.ts
Normal file
31
todoblue/src/app/board/[board]/useBoardTitleEditor.ts
Normal 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}
|
||||||
|
}
|
|
@ -1,57 +1,36 @@
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import {BoardAction, Task} from "@/app/board/[board]/types"
|
import {BoardAction} from "@/app/board/[board]/types"
|
||||||
import {useMemo, useCallback, useState} from "react"
|
import {useBoardState} from "@/app/board/[board]/useBoardState"
|
||||||
import {useWebSocket} from "@/app/board/[board]/useWebSocket"
|
import {useBoardWebSocketURL} from "@/app/board/[board]/useBoardWebSocketURL"
|
||||||
|
import {useCallback} from "react"
|
||||||
|
import {useWebSocket} from "@/app/useWebSocket"
|
||||||
|
|
||||||
|
|
||||||
export function useBoardWebSocket(board: string) {
|
export function useBoardWebSocket(name: string) {
|
||||||
const url = useMemo(() => `ws://127.0.0.1:8080/board/${board}/ws`, [board]);
|
const {webSocketURL} = useBoardWebSocketURL(name)
|
||||||
const [title, setTitle] = useState<string>("Nuovo tabellone");
|
const {state, act} = useBoardState();
|
||||||
const [tasks, setTasks] = useState<{[key: string]: Task}>(() => ({}));
|
|
||||||
|
|
||||||
const onopen = useCallback((sock: WebSocket, event: Event) => {
|
const {websocket, websocketState} = useWebSocket(webSocketURL, {
|
||||||
setTasks(() => ({}))
|
onopen: useCallback((_sock: WebSocket, _event: Event) => {
|
||||||
console.debug("[useBoardWebSocket] Connected to the websocket of board:", board);
|
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 send = useCallback((data: BoardAction) => {
|
||||||
const data: BoardAction = JSON.parse(event.data);
|
if(!websocket || websocketState !== WebSocket.OPEN) {
|
||||||
console.debug("[useBoardWebSocket] Received:", data);
|
console.warn("[useBoardWebSocket] Webbsocket is not yet ready, cannot send:", 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;
|
return;
|
||||||
}
|
}
|
||||||
console.debug("[useBoardWebSocket] Sending:", data);
|
console.debug("[useBoardWebSocket] Sending:", data);
|
||||||
websocket.send(JSON.stringify(data));
|
websocket.send(JSON.stringify(data));
|
||||||
}, [websocket, readyState])
|
}, [websocket, websocketState])
|
||||||
|
|
||||||
return {title, tasks, pushEvent, readyState}
|
return {state, send, websocketState}
|
||||||
}
|
}
|
||||||
|
|
7
todoblue/src/app/board/[board]/useBoardWebSocketURL.ts
Normal file
7
todoblue/src/app/board/[board]/useBoardWebSocketURL.ts
Normal 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}
|
||||||
|
}
|
17
todoblue/src/app/useCycleState.ts
Normal file
17
todoblue/src/app/useCycleState.ts
Normal 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}
|
||||||
|
}
|
|
@ -11,22 +11,22 @@ export interface WebSocketHandlers {
|
||||||
|
|
||||||
export function useWebSocket(url: string, {onclose, onerror, onmessage, onopen}: WebSocketHandlers) {
|
export function useWebSocket(url: string, {onclose, onerror, onmessage, onopen}: WebSocketHandlers) {
|
||||||
const [websocket, setWebsocket] = useState<WebSocket | null>(null)
|
const [websocket, setWebsocket] = useState<WebSocket | null>(null)
|
||||||
const [readyState, setReadyState] = useState<number>(0);
|
const [websocketState, setWebsocketState] = useState<number>(0);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.debug("[useWebSocket] Creating websocket...");
|
console.debug("[useWebSocket] Creating websocket...");
|
||||||
const sock = new WebSocket(url);
|
const sock = new WebSocket(url);
|
||||||
setWebsocket(sock);
|
setWebsocket(sock);
|
||||||
sock.onopen = (ev) => {
|
sock.onopen = (ev) => {
|
||||||
setReadyState(sock.readyState);
|
setWebsocketState(sock.readyState);
|
||||||
onopen?.(sock, ev);
|
onopen?.(sock, ev);
|
||||||
}
|
}
|
||||||
sock.onclose = (ev) => {
|
sock.onclose = (ev) => {
|
||||||
setReadyState(sock.readyState);
|
setWebsocketState(sock.readyState);
|
||||||
onclose?.(sock, ev);
|
onclose?.(sock, ev);
|
||||||
}
|
}
|
||||||
sock.onerror = (ev) => {
|
sock.onerror = (ev) => {
|
||||||
setReadyState(sock.readyState);
|
setWebsocketState(sock.readyState);
|
||||||
onerror?.(sock, ev);
|
onerror?.(sock, ev);
|
||||||
}
|
}
|
||||||
sock.onmessage = (ev) => {
|
sock.onmessage = (ev) => {
|
||||||
|
@ -39,5 +39,5 @@ export function useWebSocket(url: string, {onclose, onerror, onmessage, onopen}:
|
||||||
}
|
}
|
||||||
}, [url, onclose, onerror, onmessage, onopen])
|
}, [url, onclose, onerror, onmessage, onopen])
|
||||||
|
|
||||||
return {websocket, readyState}
|
return {websocket, websocketState}
|
||||||
}
|
}
|
|
@ -114,7 +114,16 @@
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/react" "*"
|
"@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"
|
version "18.2.17"
|
||||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.17.tgz#baa565b17ddb649c2dac85b5eaf9e9a1fe0f3b4e"
|
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.17.tgz#baa565b17ddb649c2dac85b5eaf9e9a1fe0f3b4e"
|
||||||
integrity sha512-u+e7OlgPPh+aryjOm5UJMX32OvB2E3QASOAqVMY6Ahs90djagxwv2ya0IctglNbNTexC12qCSMZG47KPfy1hAA==
|
integrity sha512-u+e7OlgPPh+aryjOm5UJMX32OvB2E3QASOAqVMY6Ahs90djagxwv2ya0IctglNbNTexC12qCSMZG47KPfy1hAA==
|
||||||
|
@ -136,9 +145,9 @@ busboy@1.6.0:
|
||||||
streamsearch "^1.1.0"
|
streamsearch "^1.1.0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.30001406:
|
caniuse-lite@^1.0.30001406:
|
||||||
version "1.0.30001517"
|
version "1.0.30001518"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001517.tgz#90fabae294215c3495807eb24fc809e11dc2f0a8"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001518.tgz#b3ca93904cb4699c01218246c4d77a71dbe97150"
|
||||||
integrity sha512-Vdhm5S11DaFVLlyiKu4hiUTkpZu+y1KA/rZZqVQfOD5YdDT/eQKlkt7NaE0WGOFgX32diqt9MiP9CAiFeRklaA==
|
integrity sha512-rup09/e3I0BKjncL+FesTayKtPrdwKhUufQFd3riFw1hHg8JmIFoInYfB102cFcY/pPgGmdyl/iy+jgiDi2vdA==
|
||||||
|
|
||||||
classnames@^2.3.2:
|
classnames@^2.3.2:
|
||||||
version "2.3.2"
|
version "2.3.2"
|
||||||
|
|
Loading…
Reference in a new issue