mirror of
https://github.com/Steffo99/todocolors.git
synced 2024-11-21 15:54:18 +00:00
Implement rudimentary task journal
This commit is contained in:
parent
33cb947da3
commit
352c8378d0
123 changed files with 2122 additions and 799 deletions
7
.idea/discord.xml
Normal file
7
.idea/discord.xml
Normal file
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="PROJECT_FILES" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
</project>
|
|
@ -1,9 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="DiscordProjectSettings">
|
||||
<option name="show" value="ASK" />
|
||||
<option name="description" value="" />
|
||||
</component>
|
||||
<component name="ProjectRootManager">
|
||||
<output url="file://$PROJECT_DIR$/out" />
|
||||
</component>
|
||||
|
|
|
@ -8,9 +8,7 @@
|
|||
<arguments value="--port=8081" />
|
||||
<node-interpreter value="project" />
|
||||
<envs>
|
||||
<env name="NEXT_PUBLIC_TODOBLUE_OVERRIDE_BASE_URL" value="http://localhost:8080" />
|
||||
<env name="TODOBLUE_SITE_DESCRIPTION" value="Development version of Todoblue" />
|
||||
<env name="TODOBLUE_SITE_NAME" value="Tododeve" />
|
||||
<env name="NEXT_PUBLIC_TODOBLUE_OVERRIDE_BASE_URL" value="http://ethernet.nitro.home.steffo.eu:8080" />
|
||||
</envs>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
|
|
14
README.md
14
README.md
|
@ -15,3 +15,17 @@ A self-hostable multiplayer todo app with Redis, Rust, WebSockets and Next.js.
|
|||
## Screenshots
|
||||
|
||||
![Screenshot of the application, detailing a nonsensical "Plan for conquering the world"](media/screenshot.png 'Screenshot of the application, detailing a nonsensical "Plan for conquering the world')
|
||||
|
||||
## Installation
|
||||
|
||||
To deploy your own instance of Todocolors, use the (Docker) `compose.yml` file included in `todopod/`, tweaking the `network_mode` and `ports` of the `caddy` container as you see appropriate.
|
||||
|
||||
Data will be stored in the `data/redis/rdata/` directory.
|
||||
|
||||
### Further customization
|
||||
|
||||
For more customization, make changes and then build your own Docker images using the provided `Dockerfile` in `todored/` and `todoblue/`.
|
||||
|
||||
## Credits & acknowledgements
|
||||
|
||||
TODO
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "todoblue",
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import {useCallback, useEffect, useReducer, Reducer} from "react"
|
||||
import {Reducer, useCallback, useEffect, useReducer} from "react"
|
||||
|
||||
|
||||
export interface WebSocketHandlerParams<E extends Event> {
|
||||
event: E,
|
||||
|
|
|
@ -7,26 +7,54 @@
|
|||
"cycleColumningButtonTitle": "Change column arrangement",
|
||||
"cycleGroupingButtonTitle": "Change column task grouping",
|
||||
"cycleSortingButtonTitle": "Change column task sorting",
|
||||
"lockButtonTitle": "Lock board, preventing editing",
|
||||
"unlockButtonTitle": "Unlock board, allowing editing",
|
||||
"privacyButtonTitle": "Connected users",
|
||||
"boardPreparing": "Preparing...",
|
||||
"boardConnecting": "Connecting...",
|
||||
"boardEmpty": "This board is empty.",
|
||||
"boardDisconnecting": "Disconnecting...",
|
||||
"boardDisconnected": "Disconnected, reconnecting in {{retryingInSeconds}} s",
|
||||
"columnHeaderTaskImportanceHighest": "Critical",
|
||||
"columnHeaderTaskImportanceHigh": "Important",
|
||||
"columnHeaderTaskImportanceNormal": "Normal",
|
||||
"columnHeaderTaskImportanceLow": "Optional",
|
||||
"columnHeaderTaskImportanceLowest": "Irrelevant",
|
||||
"columnHeaderTaskPriorityHighest": "Immediate",
|
||||
"columnHeaderTaskPriorityHigh": "Urgent",
|
||||
"columnHeaderTaskPriorityNormal": "Normal",
|
||||
"columnHeaderTaskPriorityLow": "Sometime",
|
||||
"columnHeaderTaskPriorityLowest": "Anytime",
|
||||
"columnHeaderTaskStatusUnfinished": "Unfinished",
|
||||
"columnHeaderTaskStatusInProgress": "In progress",
|
||||
"columnHeaderTaskStatusComplete": "Complete",
|
||||
"taskImportanceHighest": "Critical",
|
||||
"taskImportanceHigh": "Important",
|
||||
"taskImportanceNormal": "Normal",
|
||||
"taskImportanceLow": "Optional",
|
||||
"taskImportanceLowest": "Irrelevant",
|
||||
"taskPriorityHighest": "Immediate",
|
||||
"taskPriorityHigh": "Urgent",
|
||||
"taskPriorityNormal": "Normal",
|
||||
"taskPriorityLow": "Sometime",
|
||||
"taskPriorityLowest": "Anytime",
|
||||
"taskStatusNonExistent": "Non-existent",
|
||||
"taskStatusUnfinished": "Unfinished",
|
||||
"taskStatusInProgress": "In progress",
|
||||
"taskStatusComplete": "Complete",
|
||||
"taskStatusJournaled": "Journaled",
|
||||
"taskIconBookmark": "Bookmark",
|
||||
"taskIconCircle": "Circle",
|
||||
"taskIconSquare": "Square",
|
||||
"taskIconHeart": "Heart",
|
||||
"taskIconStar": "Star",
|
||||
"taskIconSun": "Sun",
|
||||
"taskIconMoon": "Moon",
|
||||
"taskIconEye": "Eye",
|
||||
"taskIconHand": "Hand",
|
||||
"taskIconHandshake": "Handshake",
|
||||
"taskIconFaceSmile": "Smile",
|
||||
"taskIconUser": "User",
|
||||
"taskIconComment": "Comment",
|
||||
"taskIconEnvelope": "Envelope",
|
||||
"taskIconFile": "File",
|
||||
"taskIconPaperPlane": "Paper plane",
|
||||
"taskIconBuilding": "Building",
|
||||
"taskIconFlag": "Flag",
|
||||
"taskIconBell": "Bell",
|
||||
"taskIconClock": "Clock",
|
||||
"taskIconImage": "Image",
|
||||
"taskButtonJournal": "Move this task to the Journal",
|
||||
"taskButtonUnjournal": "Remove this task from the Journal",
|
||||
"taskButtonDelete": "Delete this task",
|
||||
"taskButtonRecreate": "Delete and go back to editing this task",
|
||||
"editorPlaceholder": "What do you want to do?",
|
||||
"taskButtonRecreate": "Delete and return to editing this task",
|
||||
"editorPlaceholder": "What to do...?",
|
||||
"noTitlePlaceholder": "Untitled board"
|
||||
}
|
||||
|
|
|
@ -7,26 +7,54 @@
|
|||
"cycleColumningButtonTitle": "Cambia disposizione colonne",
|
||||
"cycleGroupingButtonTitle": "Cambia raggruppamento attività in colonne",
|
||||
"cycleSortingButtonTitle": "Cambia ordinamento attività nelle colonne",
|
||||
"lockButtonTitle": "Blocca il board, impedendone la modifica",
|
||||
"unlockButtonTitle": "Sblocca il board, permettendone la modifica",
|
||||
"privacyButtonTitle": "Utenti connessi",
|
||||
"boardPreparing": "Preparazione...",
|
||||
"boardConnecting": "Connessione...",
|
||||
"boardEmpty": "Il board è vuoto.",
|
||||
"boardEmpty": "Questo board è vuoto.",
|
||||
"boardDisconnecting": "Disconnessione...",
|
||||
"boardDisconnected": "Disconnesso, riconnessione tra {{retryingInSeconds}} s",
|
||||
"columnHeaderTaskImportanceHighest": "Critica",
|
||||
"columnHeaderTaskImportanceHigh": "Importante",
|
||||
"columnHeaderTaskImportanceNormal": "Normale",
|
||||
"columnHeaderTaskImportanceLow": "Opzionale",
|
||||
"columnHeaderTaskImportanceLowest": "Irrelevante",
|
||||
"columnHeaderTaskPriorityHighest": "Immediata",
|
||||
"columnHeaderTaskPriorityHigh": "Urgente",
|
||||
"columnHeaderTaskPriorityNormal": "Normale",
|
||||
"columnHeaderTaskPriorityLow": "Senza fretta",
|
||||
"columnHeaderTaskPriorityLowest": "Una qualche volta",
|
||||
"columnHeaderTaskStatusUnfinished": "Da fare",
|
||||
"columnHeaderTaskStatusInProgress": "In corso",
|
||||
"columnHeaderTaskStatusComplete": "Completata",
|
||||
"taskImportanceHighest": "Critico",
|
||||
"taskImportanceHigh": "Importante",
|
||||
"taskImportanceNormal": "Normale",
|
||||
"taskImportanceLow": "Opzionale",
|
||||
"taskImportanceLowest": "Irrelevante",
|
||||
"taskPriorityHighest": "Immediato",
|
||||
"taskPriorityHigh": "Urgente",
|
||||
"taskPriorityNormal": "Normale",
|
||||
"taskPriorityLow": "Senza fretta",
|
||||
"taskPriorityLowest": "Prima o poi",
|
||||
"taskStatusNonExistent": "Inesistente",
|
||||
"taskStatusUnfinished": "Da fare",
|
||||
"taskStatusInProgress": "In corso",
|
||||
"taskStatusComplete": "Completato",
|
||||
"taskStatusJournaled": "Salvato nel Diario",
|
||||
"taskIconBookmark": "Segnalibro",
|
||||
"taskIconCircle": "Cerchio",
|
||||
"taskIconSquare": "Quadrato",
|
||||
"taskIconHeart": "Cuore",
|
||||
"taskIconStar": "Stella",
|
||||
"taskIconSun": "Sole",
|
||||
"taskIconMoon": "Luna",
|
||||
"taskIconEye": "Occhio",
|
||||
"taskIconHand": "Mano",
|
||||
"taskIconHandshake": "Stretta di mano",
|
||||
"taskIconFaceSmile": "Sorriso",
|
||||
"taskIconUser": "Utente",
|
||||
"taskIconComment": "Commento",
|
||||
"taskIconEnvelope": "Busta",
|
||||
"taskIconFile": "File",
|
||||
"taskIconPaperPlane": "Aeroplano di carta",
|
||||
"taskIconBuilding": "Edificio",
|
||||
"taskIconFlag": "Bandiera",
|
||||
"taskIconBell": "Campana",
|
||||
"taskIconClock": "Orologio",
|
||||
"taskIconImage": "Immagine",
|
||||
"taskButtonJournal": "Sposta questa attività nel Diario",
|
||||
"taskButtonUnjournal": "Rimuovi questa attività dal Diario",
|
||||
"taskButtonDelete": "Elimina questa attività",
|
||||
"taskButtonRecreate": "Elimina questa attività e torna a modificarla",
|
||||
"editorPlaceholder": "Cosa vuoi fare?",
|
||||
"editorPlaceholder": "Cosa fare...?",
|
||||
"noTitlePlaceholder": "Board senza titolo"
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"use client";
|
||||
import "client-only";
|
||||
import "client-only"
|
||||
import {createInstance, i18n} from "i18next"
|
||||
import resourcesToBackend from "i18next-resources-to-backend"
|
||||
import {useEffect, useState} from "react"
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import "server-only";
|
||||
import {createInstance, i18n} from "i18next"
|
||||
import resourcesToBackend from "i18next-resources-to-backend"
|
||||
import "server-only"
|
||||
|
||||
|
||||
async function init(lang: string, ns: string): Promise<i18n> {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {KEBABIFIER} from "@/app/(utils)/(kebab)/kebabifier"
|
||||
import {ReturnTypeOfUseState} from "@/app/(utils)/ReturnTypeOfUseState"
|
||||
import {Dispatch, useCallback, useState} from "react"
|
||||
import {useCallback, useState} from "react"
|
||||
|
||||
|
||||
/**
|
||||
|
|
|
@ -7,6 +7,7 @@ import classNames from "classnames"
|
|||
import {useRouter} from "next/navigation"
|
||||
import {default as React, SyntheticEvent, useCallback, useEffect, useState} from "react"
|
||||
|
||||
|
||||
export function CreatePrivateBoardPanel({lang}: {lang: string}) {
|
||||
const {t} = useClientTranslation(lang, "root")
|
||||
const router = useRouter();
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useLowerKebabifier} from "@/app/(utils)/(kebab)"
|
||||
import {faGlobe} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {useRouter} from "next/navigation"
|
||||
import {default as React, SyntheticEvent, useCallback, useState} from "react"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
|
||||
|
||||
export function CreatePublicBoardPanel({lang}: {lang: string}) {
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useLowerKebabifier} from "@/app/(utils)/(kebab)"
|
||||
import {faKey} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {useRouter} from "next/navigation"
|
||||
import {default as React, SyntheticEvent, useCallback, useState} from "react"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
|
||||
|
||||
export function KnownBoardsPanel({lang}: {lang: string}) {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {useServerTranslation} from "@/app/(i18n)/server"
|
||||
import style from "./RootHeader.module.css"
|
||||
import {default as React} from "react"
|
||||
import style from "./RootHeader.module.css"
|
||||
|
||||
|
||||
export async function RootHeader({lang}: {lang: string}) {
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import {CreateBoardChapter} from "@/app/[lang]/(page)/CreateBoardChapter"
|
||||
import {ExistingBoardChapter} from "@/app/[lang]/(page)/ExistingBoardChapter"
|
||||
import style from "./RootMain.module.css"
|
||||
import {default as React} from "react"
|
||||
import style from "./RootMain.module.css"
|
||||
|
||||
|
||||
export async function RootMain({lang}: {lang: string}) {
|
||||
return (
|
||||
<main className={style.rootMain}>
|
||||
<CreateBoardChapter lang={lang}/>
|
||||
<ExistingBoardChapter lang={lang}/>
|
||||
<CreateBoardChapter lang={lang}/>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
import {ConnectBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)/ConnectBoardChange"
|
||||
import {DisconnectBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)/DisconnectBoardChange"
|
||||
import {LockBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)/LockBoardChange"
|
||||
import {StateBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)/StateBoardChange"
|
||||
import {TaskBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)/TaskBoardChange"
|
||||
import {TitleBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)/TitleBoardChange"
|
||||
|
||||
|
||||
export type BoardChange = TaskBoardChange | TitleBoardChange | ConnectBoardChange | DisconnectBoardChange | LockBoardChange | StateBoardChange;
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* **Object** signaling the connection of a new client to the board.
|
||||
*/
|
||||
export type ConnectBoardSignal = {
|
||||
export type ConnectBoardChange = {
|
||||
"Connect": string,
|
||||
}
|
|
@ -4,7 +4,7 @@ import {TaskId} from "@/app/[lang]/board/[board]/(api)/(task)"
|
|||
/**
|
||||
* **Object** signaling the deletion of the {@link Task} with the given {@link TaskId} from the board.
|
||||
*/
|
||||
export type DeleteTaskBoardSignal = {
|
||||
export type DeleteTaskBoardChange = {
|
||||
"Task": [
|
||||
TaskId,
|
||||
null
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* **Object** signaling the disconnection of a client from the board.
|
||||
*/
|
||||
export type DisconnectBoardSignal = {
|
||||
export type DisconnectBoardChange = {
|
||||
"Disconnect": string,
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export type LockBoardChange = {
|
||||
"Lock": boolean,
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
import {BoardState} from "@/app/[lang]/board/[board]/(api)/(state)/BoardState"
|
||||
|
||||
|
||||
export type StateBoardChange = {
|
||||
"State": BoardState,
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
import {DeleteTaskBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)/DeleteTaskBoardChange"
|
||||
import {UpdateTaskBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)/UpdateTaskBoardChange"
|
||||
|
||||
|
||||
/**
|
||||
* **Object** signaling an alteration to a {@link Task} related to the board.
|
||||
*/
|
||||
export type TaskBoardChange = UpdateTaskBoardChange | DeleteTaskBoardChange;
|
|
@ -1,6 +1,6 @@
|
|||
/**
|
||||
* **Object** signaling a change to the title of the board.
|
||||
*/
|
||||
export type TitleBoardSignal = {
|
||||
export type TitleBoardChange = {
|
||||
"Title": string,
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import {Task, TaskId} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
|
||||
|
||||
/**
|
||||
* **Object** signaling the creation of a new {@link Task} with the given {@link TaskId} or a modification of the {@link Task} already on the board with the given {@link TaskId}.
|
||||
*/
|
||||
export type UpdateTaskBoardSignal = {
|
||||
export type UpdateTaskBoardChange = {
|
||||
"Task": [
|
||||
TaskId,
|
||||
Task,
|
|
@ -0,0 +1,6 @@
|
|||
export type {UpdateTaskBoardChange} from "./UpdateTaskBoardChange"
|
||||
export type {DeleteTaskBoardChange} from "./DeleteTaskBoardChange"
|
||||
export type {TaskBoardChange} from "./TaskBoardChange"
|
||||
export type {TitleBoardChange} from "./TitleBoardChange"
|
||||
export type {LockBoardChange} from "./LockBoardChange"
|
||||
export type {BoardChange} from "./BoardChange"
|
|
@ -1,8 +1,10 @@
|
|||
import {LockBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)/LockBoardRequest"
|
||||
import {TaskBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)/TaskBoardRequest"
|
||||
import {TitleBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)/TitleBoardRequest"
|
||||
import {TrimBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)/TrimBoardRequest"
|
||||
|
||||
|
||||
/**
|
||||
* **Object** requesting the server to perform a certain action on the board.
|
||||
*/
|
||||
export type BoardRequest = TitleBoardRequest | TaskBoardRequest;
|
||||
export type BoardRequest = TitleBoardRequest | TaskBoardRequest | LockBoardRequest | TrimBoardRequest;
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import {Task} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
|
||||
|
||||
/**
|
||||
* **Object** requesting the creation of a new {@link Task} on the board.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export type LockBoardRequest = {
|
||||
"Lock": boolean,
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import {Task, TaskId} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
|
||||
|
||||
/**
|
||||
* **Object** requesting the modification of the {@link Task} already on the board with the given {@link TaskId}.
|
||||
*/
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
export type TrimBoardRequest = {
|
||||
"Trim": null,
|
||||
}
|
|
@ -2,4 +2,6 @@ export type {ModifyTaskBoardRequest} from "./ModifyTaskBoardRequest"
|
|||
export type {DeleteTaskBoardRequest} from "./DeleteTaskBoardRequest"
|
||||
export type {TaskBoardRequest} from "./TaskBoardRequest"
|
||||
export type {TitleBoardRequest} from "./TitleBoardRequest"
|
||||
export type {LockBoardRequest} from "./LockBoardRequest"
|
||||
export type {TrimBoardRequest} from "./TrimBoardRequest"
|
||||
export type {BoardRequest} from "./BoardRequest"
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import {ConnectBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/ConnectBoardSignal"
|
||||
import {DisconnectBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/DisconnectBoardSignal"
|
||||
import {TaskBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/TaskBoardSignal"
|
||||
import {TitleBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/TitleBoardSignal"
|
||||
|
||||
|
||||
export type BoardSignal = TaskBoardSignal | TitleBoardSignal | ConnectBoardSignal | DisconnectBoardSignal;
|
|
@ -1,8 +0,0 @@
|
|||
import {DeleteTaskBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/DeleteTaskBoardSignal"
|
||||
import {UpdateTaskBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/UpdateTaskBoardSignal"
|
||||
|
||||
|
||||
/**
|
||||
* **Object** signaling an alteration to a {@link Task} related to the board.
|
||||
*/
|
||||
export type TaskBoardSignal = UpdateTaskBoardSignal | DeleteTaskBoardSignal;
|
|
@ -1,5 +0,0 @@
|
|||
export type {UpdateTaskBoardSignal} from "./UpdateTaskBoardSignal"
|
||||
export type {DeleteTaskBoardSignal} from "./DeleteTaskBoardSignal"
|
||||
export type {TaskBoardSignal} from "./TaskBoardSignal"
|
||||
export type {TitleBoardSignal} from "./TitleBoardSignal"
|
||||
export type {BoardSignal} from "./BoardSignal"
|
|
@ -6,6 +6,7 @@ import {Task} from "@/app/[lang]/board/[board]/(api)/(task)"
|
|||
*/
|
||||
export type BoardState = {
|
||||
title: string,
|
||||
tasksById: {[key: string]: Task},
|
||||
connectedClients: string[],
|
||||
tasks: {[key: string]: Task},
|
||||
clients: string[],
|
||||
locked: boolean,
|
||||
}
|
||||
|
|
|
@ -1,59 +1,61 @@
|
|||
import {BoardSignal, DeleteTaskBoardSignal, TaskBoardSignal, TitleBoardSignal, UpdateTaskBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)"
|
||||
import {ConnectBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/ConnectBoardSignal"
|
||||
import {DisconnectBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/DisconnectBoardSignal"
|
||||
import {ConnectBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)/ConnectBoardChange"
|
||||
import {DisconnectBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)/DisconnectBoardChange"
|
||||
import {StateBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)/StateBoardChange"
|
||||
import {BoardState} from "@/app/[lang]/board/[board]/(api)/(state)/BoardState"
|
||||
import {DEFAULT_BOARD_STATE} from "@/app/[lang]/board/[board]/(api)/(state)/defaultBoardState"
|
||||
import {BoardChange, DeleteTaskBoardChange, LockBoardChange, TaskBoardChange, TitleBoardChange, UpdateTaskBoardChange} from "../(change)"
|
||||
|
||||
|
||||
/**
|
||||
* **Reducer** updating a {@link BoardState} with a single new {@link BoardSignal}.
|
||||
* **Reducer** updating a {@link BoardState} with a single new {@link BoardChange}.
|
||||
*
|
||||
* If `null` is passed as signal, the state is reset to the default.
|
||||
* If `null` is passed as change, the state is reset to the default.
|
||||
*
|
||||
* If an unknown signal is passed, the state is not altered.
|
||||
*
|
||||
* @param state The state to update.
|
||||
* @param action The action to perform on the state.
|
||||
* @param change The action to perform on the state.
|
||||
*/
|
||||
export function boardReducer(state: BoardState, action: BoardSignal | null) {
|
||||
if(action === null) {
|
||||
export function boardReducer(state: BoardState, change: BoardChange | null) {
|
||||
if(change === null) {
|
||||
console.debug("[boardReducer] Initializing state...");
|
||||
return DEFAULT_BOARD_STATE
|
||||
}
|
||||
else if("Title" in action) {
|
||||
const titleAction = action as TitleBoardSignal;
|
||||
const title = titleAction["Title"]
|
||||
else if("Title" in change) {
|
||||
const titleChange = change as TitleBoardChange;
|
||||
const title = titleChange["Title"]
|
||||
console.debug("[boardReducer] Setting board title to:", title)
|
||||
return {...state, title}
|
||||
}
|
||||
else if("Task" in action) {
|
||||
const taskAction = action as TaskBoardSignal;
|
||||
const task = taskAction["Task"][1]
|
||||
const tasksById = {...state.tasksById}
|
||||
else if("Task" in change) {
|
||||
const taskChange = change as TaskBoardChange;
|
||||
const task = taskChange["Task"][1]
|
||||
const tasks = {...state.tasks}
|
||||
if(task === null) {
|
||||
const deleteAction = taskAction as DeleteTaskBoardSignal;
|
||||
const id = deleteAction["Task"][0]
|
||||
const deleteChange = taskChange as DeleteTaskBoardChange;
|
||||
const id = deleteChange["Task"][0]
|
||||
console.debug("[boardReducer] Deleting task:", id)
|
||||
delete tasksById[id]
|
||||
delete tasks[id]
|
||||
}
|
||||
else {
|
||||
const updateAction = taskAction as UpdateTaskBoardSignal;
|
||||
const id = updateAction["Task"][0] as string
|
||||
const updateChange = taskChange as UpdateTaskBoardChange;
|
||||
const id = updateChange["Task"][0] as string
|
||||
console.debug("[boardReducer] Putting task:", id)
|
||||
tasksById[id] = task
|
||||
tasks[id] = task
|
||||
}
|
||||
return {...state, tasksById}
|
||||
return {...state, tasks}
|
||||
}
|
||||
else if("Connect" in action) {
|
||||
const connectAction = action as ConnectBoardSignal;
|
||||
const id = connectAction["Connect"];
|
||||
const connectedClients = [...state.connectedClients, id]
|
||||
else if("Connect" in change) {
|
||||
const connectChange = change as ConnectBoardChange;
|
||||
const id = connectChange["Connect"];
|
||||
const connectedClients = [...state.clients, id]
|
||||
console.debug("[boardReducer] Adding new client:", id)
|
||||
return {...state, connectedClients}
|
||||
}
|
||||
else if("Disconnect" in action) {
|
||||
const disconnectAction = action as DisconnectBoardSignal;
|
||||
const id = disconnectAction["Disconnect"];
|
||||
const connectedClients = [...state.connectedClients]
|
||||
else if("Disconnect" in change) {
|
||||
const disconnectChange = change as DisconnectBoardChange;
|
||||
const id = disconnectChange["Disconnect"];
|
||||
const connectedClients = [...state.clients]
|
||||
const clientIndex = connectedClients.indexOf(id)
|
||||
if(clientIndex !== -1) {
|
||||
connectedClients.splice(clientIndex, 1);
|
||||
|
@ -61,12 +63,24 @@ export function boardReducer(state: BoardState, action: BoardSignal | null) {
|
|||
return {...state, connectedClients}
|
||||
}
|
||||
else {
|
||||
console.warn("[boardReducer] Received DisconnectBoardSignal without the client being connected in first place.")
|
||||
console.warn("[boardReducer] Received DisconnectBoardChange without the client being connected in first place.")
|
||||
return state
|
||||
}
|
||||
}
|
||||
if("Lock" in change) {
|
||||
const lockChange = change as LockBoardChange;
|
||||
const locked = lockChange["Lock"]
|
||||
console.debug("[boardReducer] Setting locked status to:", locked)
|
||||
return {...state, locked}
|
||||
}
|
||||
if("State" in change) {
|
||||
const stateChange = change as StateBoardChange;
|
||||
// noinspection UnnecessaryLocalVariableJS
|
||||
const state = stateChange["State"]
|
||||
return {...state}
|
||||
}
|
||||
else {
|
||||
console.warn("[boardReducer] Received unknown signal, ignoring:", action)
|
||||
console.warn("[boardReducer] Received unknown signal, ignoring:", change)
|
||||
return state
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,4 @@ import {BoardState} from "@/app/[lang]/board/[board]/(api)/(state)/BoardState"
|
|||
/**
|
||||
* **Object** denoting the {@link BoardState} of a board where no {@link BoardAction}s have been performed.
|
||||
*/
|
||||
export const DEFAULT_BOARD_STATE: BoardState = {title: "", tasksById: {}, connectedClients: []}
|
||||
export const DEFAULT_BOARD_STATE: BoardState = {title: "", tasks: {}, clients: [], locked: false}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
"use client";
|
||||
|
||||
import {BoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)"
|
||||
import {boardReducer} from "@/app/[lang]/board/[board]/(api)/(state)/boardReducer"
|
||||
import {BoardState} from "@/app/[lang]/board/[board]/(api)/(state)/BoardState"
|
||||
import {DEFAULT_BOARD_STATE} from "@/app/[lang]/board/[board]/(api)/(state)/defaultBoardState"
|
||||
import {Reducer, useReducer} from "react"
|
||||
import {BoardChange} from "../(change)"
|
||||
|
||||
|
||||
export function useBoardState() {
|
||||
const [boardState, processBoardSignal] = useReducer<Reducer<BoardState, BoardSignal | null>>(boardReducer, DEFAULT_BOARD_STATE)
|
||||
const [boardState, processBoardChange] = useReducer<Reducer<BoardState, BoardChange | null>>(boardReducer, DEFAULT_BOARD_STATE)
|
||||
|
||||
return {boardState, processBoardSignal}
|
||||
return {boardState, processBoardChange}
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)/TaskIcon"
|
||||
import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)/TaskImportance"
|
||||
import {TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)/TaskPriority"
|
||||
import {TaskStatus} from "@/app/[lang]/board/[board]/(api)/(task)/TaskStatus"
|
||||
|
||||
|
||||
export type Task = {
|
||||
|
@ -9,5 +8,8 @@ export type Task = {
|
|||
icon: TaskIcon,
|
||||
importance: TaskImportance,
|
||||
priority: TaskPriority,
|
||||
status: TaskStatus,
|
||||
created_on: number | null,
|
||||
started_on: number | null,
|
||||
completed_on: number | null,
|
||||
journaled_on: number | null,
|
||||
}
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
/**
|
||||
* **Enum** of possible stati a {@link Task} can be in.
|
||||
*/
|
||||
export enum TaskStatus {
|
||||
Unfinished = "Unfinished",
|
||||
InProgress = "InProgress",
|
||||
Complete = "Complete",
|
||||
}
|
|
@ -1,7 +1,6 @@
|
|||
export {TaskIcon} from "./TaskIcon"
|
||||
export {TaskImportance} from "./TaskImportance"
|
||||
export {TaskPriority} from "./TaskPriority"
|
||||
export {TaskStatus} from "./TaskStatus"
|
||||
export {TASK_ICON_TO_FONTAWESOME_SOLID, TASK_ICON_TO_FONTAWESOME_REGULAR} from "./taskIconMappings"
|
||||
|
||||
export type {Task} from "./Task"
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import {TaskIcon} from "./TaskIcon"
|
||||
import {faBell as faBellRegular, faBookmark as faBookmarkRegular, faBuilding as faBuildingRegular, faCircle as faCircleRegular, faClock as faClockRegular, faComment as faCommentRegular, faEnvelope as faEnvelopeRegular, faEye as faEyeRegular, faFaceSmile as faFaceSmileRegular, faFile as faFileRegular, faFlag as faFlagRegular, faHand as faHandRegular, faHandshake as faHandshakeRegular, faHeart as faHeartRegular, faImage as faImageRegular, faMoon as faMoonRegular, faPaperPlane as faPaperPlaneRegular, faSquare as faSquareRegular, faStar as faStarRegular, faSun as faSunRegular, faUser as faUserRegular} from "@fortawesome/free-regular-svg-icons"
|
||||
import {faBell as faBellSolid, faBookmark as faBookmarkSolid, faBuilding as faBuildingSolid, faCircle as faCircleSolid, faClock as faClockSolid, faComment as faCommentSolid, faEnvelope as faEnvelopeSolid, faEye as faEyeSolid, faFaceSmile as faFaceSmileSolid, faFile as faFileSolid, faFlag as faFlagSolid, faHand as faHandSolid, faHandshake as faHandshakeSolid, faHeart as faHeartSolid, faImage as faImageSolid, faMoon as faMoonSolid, faPaperPlane as faPaperPlaneSolid, faSquare as faSquareSolid, faStar as faStarSolid, faSun as faSunSolid, faUser as faUserSolid} from "@fortawesome/free-solid-svg-icons"
|
||||
import {faBell as faBellSolid, faBookmark as faBookmarkSolid, faBuilding as faBuildingSolid, faCircle as faCircleSolid, faClock as faClockSolid, faComment as faCommentSolid, faEnvelope as faEnvelopeSolid, faEye as faEyeSolid, faFaceSmile as faFaceSmileSolid, faFile as faFileSolid, faFlag as faFlagSolid, faHand as faHandSolid, faHandshake as faHandshakeSolid, faHeart as faHeartSolid, faImage as faImageSolid, faMoon as faMoonSolid, faPaperPlane as faPaperPlaneSolid, faSquare as faSquareSolid, faStar as faStarSolid, faSun as faSunSolid, faUser as faUserSolid, IconDefinition} from "@fortawesome/free-solid-svg-icons"
|
||||
import {TaskIcon} from "./TaskIcon"
|
||||
|
||||
|
||||
export const TASK_ICON_TO_FONTAWESOME_SOLID = {
|
||||
export const TASK_ICON_TO_FONTAWESOME_SOLID: {[T in TaskIcon]: IconDefinition} = {
|
||||
[TaskIcon.User]: faUserSolid,
|
||||
[TaskIcon.Image]: faImageSolid,
|
||||
[TaskIcon.Envelope]: faEnvelopeSolid,
|
||||
|
@ -27,7 +27,7 @@ export const TASK_ICON_TO_FONTAWESOME_SOLID = {
|
|||
[TaskIcon.Moon]: faMoonSolid,
|
||||
}
|
||||
|
||||
export const TASK_ICON_TO_FONTAWESOME_REGULAR = {
|
||||
export const TASK_ICON_TO_FONTAWESOME_REGULAR: {[T in TaskIcon]: IconDefinition} = {
|
||||
[TaskIcon.User]: faUserRegular,
|
||||
[TaskIcon.Image]: faImageRegular,
|
||||
[TaskIcon.Envelope]: faEnvelopeRegular,
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
'use client';
|
||||
|
||||
import {useWs, WebSocketHandlerParams} from "@/app/(api)/useWs"
|
||||
import {BoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
|
||||
import {BoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)"
|
||||
import {useBoardState} from "@/app/[lang]/board/[board]/(api)/(state)/useBoardState"
|
||||
import {useBoardWsURL} from "@/app/[lang]/board/[board]/(api)/(ws)/useBoardWsURL"
|
||||
import {useCallback, useMemo} from "react"
|
||||
import {useWs, WebSocketHandlerParams} from "@/app/(api)/useWs"
|
||||
import {BoardChange} from "../(change)"
|
||||
|
||||
|
||||
/**
|
||||
|
@ -16,20 +16,20 @@ import {useWs, WebSocketHandlerParams} from "@/app/(api)/useWs"
|
|||
export function useBoardWs(boardName: string) {
|
||||
const wsFullURL = useBoardWsURL(boardName)
|
||||
|
||||
const {boardState, processBoardSignal} = useBoardState();
|
||||
const {boardState, processBoardChange} = useBoardState();
|
||||
|
||||
const {webSocket, webSocketState, webSocketBackoffMs} = useWs(wsFullURL, {
|
||||
|
||||
onopen: useCallback(({}) => {
|
||||
console.debug("[useBoardWs] Opened connection, resetting BoardState to default:", boardName);
|
||||
processBoardSignal(null);
|
||||
}, [processBoardSignal]),
|
||||
processBoardChange(null);
|
||||
}, [processBoardChange]),
|
||||
|
||||
onmessage: useCallback(({event}: WebSocketHandlerParams<MessageEvent>) => {
|
||||
const action: BoardSignal = JSON.parse(event.data);
|
||||
const action: BoardChange = JSON.parse(event.data);
|
||||
console.debug("[useBoardWs] Received signal:", action);
|
||||
processBoardSignal(action)
|
||||
}, [processBoardSignal]),
|
||||
processBoardChange(action)
|
||||
}, [processBoardChange]),
|
||||
|
||||
onerror: useCallback(({event, closeWebSocket}: WebSocketHandlerParams<Event>) => {
|
||||
console.error("[useBoardWs] Encountered a WebSocket error, closing current connection:", event);
|
||||
|
|
|
@ -0,0 +1,90 @@
|
|||
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {TaskEditorIcon} from "@/app/[lang]/board/[board]/(page)/(edit)/(task)/TaskEditorIcon"
|
||||
import {TaskEditorInput} from "@/app/[lang]/board/[board]/(page)/(edit)/(task)/TaskEditorInput"
|
||||
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/taskToString"
|
||||
import {TaskContainer} from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer"
|
||||
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
|
||||
import {useTaskEditor} from "@/app/[lang]/board/[board]/(page)/useTaskEditor"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
import {SyntheticEvent, useCallback} from "react"
|
||||
import style from "./TaskEditor.module.css"
|
||||
|
||||
|
||||
const ICON_SEQUENCE = [
|
||||
TaskIcon.Bookmark,
|
||||
TaskIcon.Circle,
|
||||
TaskIcon.Square,
|
||||
TaskIcon.Heart,
|
||||
TaskIcon.Star,
|
||||
TaskIcon.Sun,
|
||||
TaskIcon.Moon,
|
||||
TaskIcon.Eye,
|
||||
TaskIcon.Hand,
|
||||
TaskIcon.Handshake,
|
||||
TaskIcon.FaceSmile,
|
||||
TaskIcon.User,
|
||||
TaskIcon.Comment,
|
||||
TaskIcon.Envelope,
|
||||
TaskIcon.File,
|
||||
TaskIcon.PaperPlane,
|
||||
TaskIcon.Building,
|
||||
TaskIcon.Flag,
|
||||
TaskIcon.Bell,
|
||||
TaskIcon.Clock,
|
||||
TaskIcon.Image,
|
||||
]
|
||||
|
||||
|
||||
export function TaskEditor({t, className, editorHook: {input, setInput, task, setTask}}: {t: TFunction, className?: string, editorHook: ReturnType<typeof useTaskEditor>}) {
|
||||
const {isReady, sendRequest} = useBoardConsumer()
|
||||
|
||||
const nextIcon = useCallback((e: SyntheticEvent<HTMLButtonElement>) => {
|
||||
if("key" in e && typeof e["key"] === "string") {
|
||||
if(![" "].includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let index = ICON_SEQUENCE.indexOf(task.icon) + 1;
|
||||
if(index === ICON_SEQUENCE.length) index = 0;
|
||||
setTask({...task, icon: ICON_SEQUENCE[index]})
|
||||
}, [task])
|
||||
|
||||
const submitTask = useCallback((e: SyntheticEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if(task.text === "") return
|
||||
sendRequest({"Task": [null, task]})
|
||||
setInput(taskToString({...task, text: ""}))
|
||||
}, [sendRequest, task, setInput])
|
||||
|
||||
if(!isReady) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(className, style.taskEditorContainer)}
|
||||
>
|
||||
<TaskContainer
|
||||
role={"form"}
|
||||
importance={task.importance}
|
||||
priority={task.priority}
|
||||
status={TaskSimplifiedStatus.NonExistent}
|
||||
onSubmit={submitTask}
|
||||
>
|
||||
<TaskEditorIcon
|
||||
t={t}
|
||||
icon={task.icon}
|
||||
nextIcon={nextIcon}
|
||||
/>
|
||||
<TaskEditorInput
|
||||
t={t}
|
||||
input={input}
|
||||
setInput={setInput}
|
||||
/>
|
||||
</TaskContainer>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskIconComponent} from "@/app/[lang]/board/[board]/(page)/(task)/TaskIconComponent"
|
||||
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
|
||||
import {TFunction} from "i18next"
|
||||
import {SyntheticEvent} from "react"
|
||||
|
||||
|
||||
const ICON_TO_KEY = {
|
||||
[TaskIcon.Bookmark]: "taskIconBookmark",
|
||||
[TaskIcon.Circle]: "taskIconCircle",
|
||||
[TaskIcon.Square]: "taskIconSquare",
|
||||
[TaskIcon.Heart]: "taskIconHeart",
|
||||
[TaskIcon.Star]: "taskIconStar",
|
||||
[TaskIcon.Sun]: "taskIconSun",
|
||||
[TaskIcon.Moon]: "taskIconMoon",
|
||||
[TaskIcon.Eye]: "taskIconEye",
|
||||
[TaskIcon.Hand]: "taskIconHand",
|
||||
[TaskIcon.Handshake]: "taskIconHandshake",
|
||||
[TaskIcon.FaceSmile]: "taskIconFaceSmile",
|
||||
[TaskIcon.User]: "taskIconUser",
|
||||
[TaskIcon.Comment]: "taskIconComment",
|
||||
[TaskIcon.Envelope]: "taskIconEnvelope",
|
||||
[TaskIcon.File]: "taskIconFile",
|
||||
[TaskIcon.PaperPlane]: "taskIconPaperPlane",
|
||||
[TaskIcon.Building]: "taskIconBuilding",
|
||||
[TaskIcon.Flag]: "taskIconFlag",
|
||||
[TaskIcon.Bell]: "taskIconBell",
|
||||
[TaskIcon.Clock]: "taskIconClock",
|
||||
[TaskIcon.Image]: "taskIconImage",
|
||||
}
|
||||
|
||||
export function TaskEditorIcon({t, icon, nextIcon}: {t: TFunction, icon: TaskIcon, nextIcon: (e: SyntheticEvent<HTMLButtonElement>) => void}) {
|
||||
return (
|
||||
<TaskIconComponent
|
||||
title={t(ICON_TO_KEY[icon])}
|
||||
icon={icon}
|
||||
status={TaskSimplifiedStatus.NonExistent}
|
||||
onInteract={nextIcon}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import style from "@/app/[lang]/board/[board]/(page)/(edit)/(task)/TaskEditor.module.css"
|
||||
import {TFunction} from "i18next"
|
||||
import {ChangeEvent, Dispatch, SetStateAction} from "react"
|
||||
|
||||
|
||||
export function TaskEditorInput({t, input, setInput}: {t: TFunction, input: string, setInput: Dispatch<SetStateAction<string>>}) {
|
||||
return (
|
||||
<input
|
||||
className={style.taskEditorInput}
|
||||
type={"text"}
|
||||
placeholder={t("editorPlaceholder")}
|
||||
value={input}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setInput(e.target.value)}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -1,13 +1,19 @@
|
|||
import {TaskEditor} from "@/app/[lang]/board/[board]/(page)/(edit)/TaskEditor"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {TaskEditor} from "@/app/[lang]/board/[board]/(page)/(edit)/(task)/TaskEditor"
|
||||
import {useTaskEditor} from "@/app/[lang]/board/[board]/(page)/useTaskEditor"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
import style from "./BoardEditor.module.css"
|
||||
|
||||
|
||||
export function BoardEditor({className, lang, editorHook}: {className?: string, lang: string, editorHook: ReturnType<typeof useTaskEditor>}) {
|
||||
export function BoardEditor({className, t, editorHook}: {className?: string, t: TFunction, editorHook: ReturnType<typeof useTaskEditor>}) {
|
||||
const {boardState: {locked}} = useBoardConsumer()
|
||||
|
||||
if(locked) return null;
|
||||
|
||||
return (
|
||||
<section className={cn(style.boardEditor, className)}>
|
||||
<TaskEditor lang={lang} editorHook={editorHook}/>
|
||||
<TaskEditor t={t} editorHook={editorHook}/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,49 +0,0 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {TASK_ICON_TO_FONTAWESOME_REGULAR} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {taskClassNames} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/taskClassNames"
|
||||
import {useTaskEditor} from "@/app/[lang]/board/[board]/(page)/useTaskEditor"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {ChangeEvent, useCallback, useMemo, SyntheticEvent} from "react"
|
||||
import style from "./TaskEditor.module.css"
|
||||
|
||||
|
||||
export function TaskEditor({lang, className, editorHook: {input, setInput, task}}: {lang: string, className?: string, editorHook: ReturnType<typeof useTaskEditor>}) {
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
const {isReady, sendRequest} = useBoardConsumer()
|
||||
const inputClassName = useMemo(() => taskClassNames(task), [task])
|
||||
|
||||
const submitTask = useCallback((e: SyntheticEvent<HTMLFormElement>) => {
|
||||
e.preventDefault()
|
||||
sendRequest({"Task": [null, task]})
|
||||
setInput("")
|
||||
}, [sendRequest, task, setInput])
|
||||
|
||||
if(!isReady) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(className, style.taskEditorContainer)}
|
||||
>
|
||||
<form
|
||||
className={cn("panel", style.taskEditor, className, inputClassName)}
|
||||
onSubmit={submitTask}
|
||||
>
|
||||
<div className={style.taskEditorIcon}>
|
||||
<FontAwesomeIcon
|
||||
icon={TASK_ICON_TO_FONTAWESOME_REGULAR[task.icon]}
|
||||
size={"lg"}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
className={style.taskEditorInput}
|
||||
type={"text"}
|
||||
placeholder={t("editorPlaceholder")}
|
||||
value={input}
|
||||
onChange={(e: ChangeEvent<HTMLInputElement>) => setInput(e.target.value)}
|
||||
/>
|
||||
</form>
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,8 +1,9 @@
|
|||
import {Task, TaskIcon, TaskImportance, TaskPriority, TaskStatus} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {Task, TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {ICON_GLYPH_RE} from "@/app/[lang]/board/[board]/(page)/(edit)/icon"
|
||||
import {IMPORTANCE_GLYPH_RE} from "@/app/[lang]/board/[board]/(page)/(edit)/importance"
|
||||
import {PRIORITY_GLYPH_RE} from "@/app/[lang]/board/[board]/(page)/(edit)/priority"
|
||||
|
||||
|
||||
const VALUE_TO_TASK_IMPORTANCE = {
|
||||
"1": TaskImportance.Highest,
|
||||
"2": TaskImportance.High,
|
||||
|
@ -31,11 +32,13 @@ const VALUE_TO_TASK_ICON = {
|
|||
"hand": TaskIcon.Hand,
|
||||
"handshake": TaskIcon.Handshake,
|
||||
"facesmile": TaskIcon.FaceSmile,
|
||||
"smile": TaskIcon.FaceSmile,
|
||||
"user": TaskIcon.User,
|
||||
"comment": TaskIcon.Comment,
|
||||
"envelope": TaskIcon.Envelope,
|
||||
"file": TaskIcon.File,
|
||||
"paperplane": TaskIcon.PaperPlane,
|
||||
"plane": TaskIcon.PaperPlane,
|
||||
"building": TaskIcon.Building,
|
||||
"flag": TaskIcon.Flag,
|
||||
"bell": TaskIcon.Bell,
|
||||
|
@ -61,9 +64,12 @@ export function stringToTask(text: string): Task {
|
|||
|
||||
return {
|
||||
text,
|
||||
status: TaskStatus.Unfinished,
|
||||
priority,
|
||||
importance,
|
||||
icon,
|
||||
created_on: + new Date(),
|
||||
started_on: null,
|
||||
completed_on: null,
|
||||
journaled_on: null,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,6 +3,7 @@ import {ICON_DEFAULT, ICON_GLYPH} from "@/app/[lang]/board/[board]/(page)/(edit)
|
|||
import {IMPORTANCE_DEFAULT, IMPORTANCE_GLYPH} from "@/app/[lang]/board/[board]/(page)/(edit)/importance"
|
||||
import {PRIORITY_DEFAULT, PRIORITY_GLYPH} from "@/app/[lang]/board/[board]/(page)/(edit)/priority"
|
||||
|
||||
|
||||
const TASK_IMPORTANCE_TO_VALUE = {
|
||||
[TaskImportance.Highest]: "1",
|
||||
[TaskImportance.High]: "2",
|
||||
|
@ -22,6 +23,12 @@ const TASK_PRIORITY_TO_VALUE = {
|
|||
export function taskToString(t: Task): string {
|
||||
let s = ""
|
||||
|
||||
if(t.icon !== ICON_DEFAULT) {
|
||||
s += ICON_GLYPH
|
||||
s += t.icon
|
||||
s += " "
|
||||
}
|
||||
|
||||
if(t.importance !== IMPORTANCE_DEFAULT) {
|
||||
s += IMPORTANCE_GLYPH
|
||||
s += TASK_IMPORTANCE_TO_VALUE[t.importance]
|
||||
|
@ -34,12 +41,6 @@ export function taskToString(t: Task): string {
|
|||
s += " "
|
||||
}
|
||||
|
||||
if(t.icon !== ICON_DEFAULT) {
|
||||
s += ICON_GLYPH
|
||||
s += t.icon
|
||||
s += " "
|
||||
}
|
||||
|
||||
s += t.text
|
||||
|
||||
return s
|
||||
|
|
|
@ -1,43 +1,70 @@
|
|||
import {BoardHeaderTitle} from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderTitle"
|
||||
import {ConnectedClientsButton} from "@/app/[lang]/board/[board]/(page)/(header)/ConnectedClientsButton"
|
||||
import {CycleColumningButton} from "@/app/[lang]/board/[board]/(page)/(header)/CycleColumningButton"
|
||||
import {CycleGroupingButton} from "@/app/[lang]/board/[board]/(page)/(header)/CycleGroupingButton"
|
||||
import {CycleSortingButton} from "@/app/[lang]/board/[board]/(page)/(header)/CycleSortingButton"
|
||||
import {NavigateHomeButton} from "@/app/[lang]/board/[board]/(page)/(header)/NavigateHomeButton"
|
||||
import {ConnectedClientsButton} from "@/app/[lang]/board/[board]/(page)/(header)/ConnectedClientsButton"
|
||||
import {ToggleEditingButton} from "@/app/[lang]/board/[board]/(page)/(header)/ToggleEditingButton"
|
||||
import {ToggleLockedButton} from "@/app/[lang]/board/[board]/(page)/(header)/ToggleLockedButton"
|
||||
import {ToggleStarredButton} from "@/app/[lang]/board/[board]/(page)/(header)/ToggleStarredButton"
|
||||
import {TrimButton} from "@/app/[lang]/board/[board]/(page)/(header)/TrimButton"
|
||||
import {useBoardLayoutEditor} from "@/app/[lang]/board/[board]/(page)/useBoardLayoutEditor"
|
||||
import {useBoardMetadataEditor} from "@/app/[lang]/board/[board]/(page)/useBoardMetadataEditor"
|
||||
import style from "./BoardHeader.module.css"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
import style from "./BoardHeader.module.css"
|
||||
|
||||
|
||||
interface BoardHeaderProps {
|
||||
lang: string,
|
||||
t: TFunction,
|
||||
className?: string,
|
||||
metadataHook: ReturnType<typeof useBoardMetadataEditor>,
|
||||
layoutHook: ReturnType<typeof useBoardLayoutEditor>,
|
||||
}
|
||||
|
||||
|
||||
export function BoardHeader({lang, className, metadataHook, layoutHook: {columningHook, groupingHook, sortingHook}}: BoardHeaderProps) {
|
||||
export function BoardHeader({t, className, metadataHook, layoutHook: {columningHook, groupingHook, sortingHook}}: BoardHeaderProps) {
|
||||
return (
|
||||
<header className={cn(style.boardHeader, className)}>
|
||||
<div className={cn(style.buttonsArea, style.leftButtonsArea)}>
|
||||
<NavigateHomeButton lang={lang}/>
|
||||
<ToggleStarredButton lang={lang}/>
|
||||
<ConnectedClientsButton lang={lang}/>
|
||||
<NavigateHomeButton
|
||||
t={t}
|
||||
/>
|
||||
<ToggleStarredButton
|
||||
t={t}
|
||||
/>
|
||||
<ToggleLockedButton
|
||||
t={t}
|
||||
/>
|
||||
<ConnectedClientsButton
|
||||
t={t}
|
||||
/>
|
||||
</div>
|
||||
<BoardHeaderTitle
|
||||
lang={lang}
|
||||
t={t}
|
||||
className={style.titleArea}
|
||||
editorHook={metadataHook}
|
||||
/>
|
||||
<div className={cn(style.buttonsArea, style.rightButtonsArea)}>
|
||||
<ToggleEditingButton lang={lang} metadataHook={metadataHook}/>
|
||||
<CycleColumningButton lang={lang} value={columningHook.value} next={columningHook.next}/>
|
||||
<CycleGroupingButton lang={lang} next={groupingHook.next}/>
|
||||
<CycleSortingButton lang={lang} next={sortingHook.next}/>
|
||||
<TrimButton
|
||||
t={t}
|
||||
/>
|
||||
<ToggleEditingButton
|
||||
t={t}
|
||||
metadataHook={metadataHook}
|
||||
/>
|
||||
<CycleColumningButton
|
||||
t={t}
|
||||
value={columningHook.value} next={columningHook.next}
|
||||
/>
|
||||
<CycleGroupingButton
|
||||
t={t}
|
||||
next={groupingHook.next}
|
||||
/>
|
||||
<CycleSortingButton
|
||||
t={t}
|
||||
next={sortingHook.next}
|
||||
/>
|
||||
</div>
|
||||
</header>
|
||||
)
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {usePageTitleSetter} from "@/app/(utils)/usePageTitleSetter"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderTitle.module.css"
|
||||
import {useBoardMetadataEditor} from "@/app/[lang]/board/[board]/(page)/useBoardMetadataEditor"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
import {useMemo} from "react"
|
||||
|
||||
|
||||
export function BoardHeaderTitle({lang, className, editorHook}: {lang: string, className?: string, editorHook: ReturnType<typeof useBoardMetadataEditor>}) {
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
export function BoardHeaderTitle({t, className, editorHook}: {t: TFunction, className?: string, editorHook: ReturnType<typeof useBoardMetadataEditor>}) {
|
||||
const {isReady, boardState: {title: titleFromState}} = useBoardConsumer()
|
||||
|
||||
const pageTitle = useMemo(() => {
|
||||
|
|
|
@ -1,25 +1,24 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import {faUsers} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
||||
|
||||
export function ConnectedClientsButton({lang}: {lang: string}) {
|
||||
const {isReady, boardState: {connectedClients}} = useBoardConsumer()
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
export function ConnectedClientsButton({t}: {t: TFunction}) {
|
||||
const {isReady, boardState: {clients}} = useBoardConsumer()
|
||||
|
||||
if(!isReady) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
title={t("privacyButtonTitle")}
|
||||
className={cn(style.block, style.doubleBlock)}
|
||||
className={cn(style.block, style.singleBlock)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faUsers}/>
|
||||
<FontAwesomeIcon icon={faUsers} size={"2xs"}/>
|
||||
|
||||
{connectedClients.length}
|
||||
{clients.length}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import cn from "classnames"
|
||||
import {COLUMNING_MODE_TO_ICON, ColumningMode} from "../(view)/(columning)"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
import {COLUMNING_MODE_TO_ICON, ColumningMode} from "../(view)/(columning)"
|
||||
|
||||
|
||||
export function CycleColumningButton({lang, value, next}: {lang: string, value: ColumningMode, next: () => void}) {
|
||||
export function CycleColumningButton({t, value, next}: {t: TFunction, value: ColumningMode, next: () => void}) {
|
||||
const {isReady} = useBoardConsumer()
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
|
||||
if(!isReady) return null;
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import {faObjectGroup} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
||||
|
||||
export function CycleGroupingButton({lang, next}: {lang: string, next: () => void}) {
|
||||
export function CycleGroupingButton({t, next}: {t: TFunction, next: () => void}) {
|
||||
const {isReady} = useBoardConsumer()
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
|
||||
if(!isReady) return null;
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import {faArrowDownWideShort} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
||||
|
||||
export function CycleSortingButton({lang, next}: {lang: string, next: () => void}) {
|
||||
export function CycleSortingButton({t, next}: {t: TFunction, next: () => void}) {
|
||||
const {isReady} = useBoardConsumer()
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
|
||||
if(!isReady) return null;
|
||||
|
||||
|
|
|
@ -1,14 +1,13 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {faHouse} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
import {useRouter} from "next/navigation"
|
||||
import {useCallback} from "react"
|
||||
|
||||
|
||||
export function NavigateHomeButton({lang}: {lang: string}) {
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
export function NavigateHomeButton({t}: {t: TFunction}) {
|
||||
const router = useRouter()
|
||||
const goHome = useCallback(() => router.push("/"), [router])
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {useBoardMetadataEditor} from "@/app/[lang]/board/[board]/(page)/useBoardMetadataEditor"
|
||||
import {faFloppyDisk, faPencil} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
||||
|
||||
export function ToggleEditingButton({lang, metadataHook}: {lang: string, metadataHook: ReturnType<typeof useBoardMetadataEditor>}) {
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
const {isReady} = useBoardConsumer()
|
||||
export function ToggleEditingButton({t, metadataHook}: {t: TFunction, metadataHook: ReturnType<typeof useBoardMetadataEditor>}) {
|
||||
const {isReady, boardState: {locked}} = useBoardConsumer()
|
||||
|
||||
if(!isReady) return null;
|
||||
if(locked) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import {LockBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {faLock, faLockOpen} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
import {useCallback} from "react"
|
||||
|
||||
|
||||
export function ToggleLockedButton({t}: {t: TFunction}) {
|
||||
const {isReady, boardState: {locked}, sendRequest} = useBoardConsumer()
|
||||
|
||||
const toggleLock = useCallback(() => {
|
||||
const request: LockBoardRequest = {"Lock": !locked}
|
||||
sendRequest(request)
|
||||
}, [locked, sendRequest])
|
||||
|
||||
if(!isReady) return null;
|
||||
|
||||
// FIXME
|
||||
return (
|
||||
<button
|
||||
title={locked ? t("lockButtonTitle") : t("unlockButtonTitle")}
|
||||
onClick={toggleLock}
|
||||
className={cn(style.block, style.singleBlock)}
|
||||
>
|
||||
<FontAwesomeIcon icon={locked ? faLock : faLockOpen}/>
|
||||
</button>
|
||||
)
|
||||
}
|
|
@ -1,4 +1,3 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useStarredConsumer} from "@/app/[lang]/(layout)/(contextStarred)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
|
@ -6,10 +5,10 @@ import {faStar as faStarRegular} from "@fortawesome/free-regular-svg-icons"
|
|||
import {faStar as faStarSolid} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
||||
|
||||
export function ToggleStarredButton({lang}: {lang: string}) {
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
export function ToggleStarredButton({t}: {t: TFunction}) {
|
||||
const {boardName} = useBoardConsumer()
|
||||
const {isStarred, toggleStarred} = useStarredConsumer()
|
||||
const thisIsStarred = isStarred(boardName)
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
import {TrimBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {faScissors} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
import {useCallback} from "react"
|
||||
|
||||
|
||||
export function TrimButton({t}: {t: TFunction}) {
|
||||
const {isReady, boardState: {locked}, sendRequest} = useBoardConsumer()
|
||||
|
||||
const requestTrim = useCallback(() => {
|
||||
const request: TrimBoardRequest = {"Trim": null}
|
||||
sendRequest(request)
|
||||
}, [locked, sendRequest])
|
||||
|
||||
if(!isReady) return null;
|
||||
if(!locked) return null;
|
||||
|
||||
return (
|
||||
<button
|
||||
title={t("trimButtonTitle")}
|
||||
onClick={requestTrim}
|
||||
className={cn(style.block, style.singleBlock)}
|
||||
>
|
||||
<FontAwesomeIcon icon={faScissors}/>
|
||||
</button>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
.taskActions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
|
||||
flex-grow: 0;
|
||||
flex-shrink: 1;
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
import cn from "classnames"
|
||||
import {ReactNode} from "react"
|
||||
import style from "./TaskActions.module.css"
|
||||
|
||||
|
||||
type TaskActionsProps = {
|
||||
className?: string,
|
||||
children: ReactNode,
|
||||
}
|
||||
|
||||
|
||||
export function TaskActions({className, children}: TaskActionsProps) {
|
||||
return (
|
||||
<div className={cn(style.taskActions, className)}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.taskButton {
|
||||
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
import {IconDefinition} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {SyntheticEvent} from "react"
|
||||
import style from "./TaskButton.module.css"
|
||||
|
||||
|
||||
type TaskButtonProps = {
|
||||
className?: string,
|
||||
title: string,
|
||||
icon: IconDefinition,
|
||||
onInteract?: (e: SyntheticEvent<HTMLButtonElement>) => void,
|
||||
}
|
||||
|
||||
|
||||
export function TaskButton({className, title, icon, onInteract}: TaskButtonProps) {
|
||||
return (
|
||||
<button
|
||||
className={cn({
|
||||
"fade": !onInteract,
|
||||
[style.taskButton]: true,
|
||||
}, className)}
|
||||
title={title}
|
||||
disabled={!onInteract}
|
||||
onPointerDown={onInteract}
|
||||
onKeyDown={onInteract}
|
||||
tabIndex={0}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
size={"sm"}
|
||||
icon={icon}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,140 @@
|
|||
.taskContainer {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-width: 240px;
|
||||
min-height: 48px;
|
||||
}
|
||||
|
||||
.taskImportanceHighest {
|
||||
--bhsl-current-hue: 18deg;
|
||||
--bhsl-current-saturation: 100%;
|
||||
--bhsl-current-lightness: 75%;
|
||||
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.taskImportanceHigh {
|
||||
--bhsl-current-hue: 250deg;
|
||||
--bhsl-current-saturation: 100%;
|
||||
--bhsl-current-lightness: 78%;
|
||||
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.taskImportanceNormal {
|
||||
--bhsl-current-hue: 212deg;
|
||||
--bhsl-current-saturation: 100%;
|
||||
--bhsl-current-lightness: 81%;
|
||||
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.taskImportanceLow {
|
||||
--bhsl-current-hue: 160deg;
|
||||
--bhsl-current-saturation: 45%;
|
||||
--bhsl-current-lightness: 60%;
|
||||
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.taskImportanceLowest {
|
||||
--bhsl-current-hue: 120deg;
|
||||
--bhsl-current-saturation: 30%;
|
||||
--bhsl-current-lightness: 45%;
|
||||
|
||||
font-weight: 200;
|
||||
}
|
||||
|
||||
.taskPriorityHighest {
|
||||
border: 4px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.45);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.taskPriorityHigh {
|
||||
border: 3px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.25);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.taskPriorityNormal {
|
||||
border: 2px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.15);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.taskPriorityLow {
|
||||
border: 1px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.15);
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.taskPriorityLowest {
|
||||
border: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@keyframes inProgress {
|
||||
0%, 100% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.taskStatusNonExistent {
|
||||
|
||||
}
|
||||
|
||||
.taskStatusInProgress :global(.taskDescription) {
|
||||
animation-name: inProgress;
|
||||
animation-delay: 0s;
|
||||
animation-direction: normal;
|
||||
animation-duration: 1s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1);
|
||||
}
|
||||
|
||||
.taskStatusComplete :global(.taskDescription) {
|
||||
opacity: 0.4;
|
||||
text-decoration: 2px currentColor solid line-through;
|
||||
}
|
||||
|
||||
.taskStatusUnfinishedColumnHeader {
|
||||
|
||||
}
|
||||
|
||||
.taskStatusInProgressColumnHeader {
|
||||
animation-name: inProgress;
|
||||
animation-delay: 0s;
|
||||
animation-direction: normal;
|
||||
animation-duration: 1s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1);
|
||||
}
|
||||
|
||||
.taskStatusCompleteColumnHeader {
|
||||
opacity: 0.4;
|
||||
text-decoration: 2px currentColor solid line-through;
|
||||
}
|
||||
|
||||
.taskStatusJournaled {
|
||||
|
||||
}
|
||||
|
||||
.taskPriorityHighestColumnHeader {
|
||||
text-decoration: underline 4px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 1.0);
|
||||
}
|
||||
|
||||
.taskPriorityHighColumnHeader {
|
||||
text-decoration: underline 3px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.75);
|
||||
}
|
||||
|
||||
.taskPriorityNormalColumnHeader {
|
||||
text-decoration: underline 2px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.50);
|
||||
}
|
||||
|
||||
.taskPriorityLowColumnHeader {
|
||||
text-decoration: underline 1px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.25);
|
||||
}
|
||||
|
||||
.taskPriorityLowestColumnHeader {
|
||||
text-decoration: none;
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
import {TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
|
||||
import cn from "classnames"
|
||||
import {ComponentPropsWithoutRef} from "react"
|
||||
import style from "./TaskContainer.module.css"
|
||||
|
||||
|
||||
export type TaskContainerProps = {
|
||||
className?: string,
|
||||
role: "article" | "form",
|
||||
importance: TaskImportance,
|
||||
priority: TaskPriority,
|
||||
status: TaskSimplifiedStatus,
|
||||
} & ComponentPropsWithoutRef<"article">
|
||||
|
||||
|
||||
export function TaskContainer({role, className, importance, priority, status, ...props}: TaskContainerProps) {
|
||||
const fullProps = {
|
||||
className: cn({
|
||||
"panel": true,
|
||||
[style.taskContainer]: true,
|
||||
[style.taskContainerClickable]: true,
|
||||
[style.taskImportanceHighest]: importance === TaskImportance.Highest,
|
||||
[style.taskImportanceHigh]: importance === TaskImportance.High,
|
||||
[style.taskImportanceNormal]: importance === TaskImportance.Normal,
|
||||
[style.taskImportanceLow]: importance === TaskImportance.Low,
|
||||
[style.taskImportanceLowest]: importance === TaskImportance.Lowest,
|
||||
[style.taskPriorityHighest]: priority === TaskPriority.Highest,
|
||||
[style.taskPriorityHigh]: priority === TaskPriority.High,
|
||||
[style.taskPriorityNormal]: priority === TaskPriority.Normal,
|
||||
[style.taskPriorityLow]: priority === TaskPriority.Low,
|
||||
[style.taskPriorityLowest]: priority === TaskPriority.Lowest,
|
||||
[style.taskStatusNonExistent]: status === TaskSimplifiedStatus.NonExistent,
|
||||
[style.taskStatusUnfinished]: status === TaskSimplifiedStatus.Unfinished,
|
||||
[style.taskStatusInProgress]: status === TaskSimplifiedStatus.InProgress,
|
||||
[style.taskStatusComplete]: status === TaskSimplifiedStatus.Complete,
|
||||
[style.taskStatusJournaled]: status === TaskSimplifiedStatus.Journaled,
|
||||
}, className),
|
||||
...props,
|
||||
}
|
||||
|
||||
if(role === "article") {
|
||||
return (
|
||||
<article {...fullProps}/>
|
||||
)
|
||||
}
|
||||
else if(role === "form") {
|
||||
return (
|
||||
<form {...fullProps}/>
|
||||
)
|
||||
}
|
||||
else {
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
.taskDebug {
|
||||
font-size: x-small;
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
import cn from "classnames"
|
||||
import {useMemo} from "react"
|
||||
import style from "./TaskDebug.module.css"
|
||||
|
||||
|
||||
type TaskDebugProps = {
|
||||
className?: string,
|
||||
task: object,
|
||||
}
|
||||
|
||||
|
||||
export function TaskDebug({className, task}: TaskDebugProps) {
|
||||
const json = useMemo(() => JSON.stringify(task, undefined, 2), [task])
|
||||
|
||||
return (
|
||||
<pre className={cn(style.taskDebug, className)}>
|
||||
<code lang={"json"}>
|
||||
{json}
|
||||
</code>
|
||||
</pre>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
.taskDescription {
|
||||
border-radius: var(--b-border-radius);
|
||||
flex-grow: 1;
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
import cn from "classnames"
|
||||
import style from "./TaskDescription.module.css"
|
||||
|
||||
|
||||
type TaskDescriptionProps = {
|
||||
className?: string,
|
||||
text: string,
|
||||
}
|
||||
|
||||
export function TaskDescription({className, text}: TaskDescriptionProps) {
|
||||
return (
|
||||
<div className={cn({
|
||||
"taskDescription": true,
|
||||
[style.taskDescription]: true,
|
||||
}, className)} tabIndex={0}>
|
||||
{text}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,34 @@
|
|||
.taskIconComponent {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
padding: 0;
|
||||
padding-inline: 0;
|
||||
border-width: 0;
|
||||
background: transparent;
|
||||
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
cursor: unset;
|
||||
}
|
||||
|
||||
.taskIconComponentClickable {
|
||||
cursor: pointer;
|
||||
|
||||
transition-property: filter;
|
||||
transition-duration: 0.5s;
|
||||
}
|
||||
|
||||
.taskIconComponentClickable:hover {
|
||||
filter: drop-shadow(0 0 4px currentColor);
|
||||
|
||||
transition-duration: 0s;
|
||||
}
|
||||
|
||||
.taskIconComponentClickable:active {
|
||||
filter: drop-shadow(0 0 6px currentColor) drop-shadow(0 0 6px currentColor);
|
||||
|
||||
transition-duration: 0s;
|
||||
}
|
|
@ -0,0 +1,40 @@
|
|||
import {TASK_ICON_TO_FONTAWESOME_REGULAR, TASK_ICON_TO_FONTAWESOME_SOLID, TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {SyntheticEvent} from "react"
|
||||
import style from "./TaskIconComponent.module.css"
|
||||
|
||||
|
||||
type TaskIconProps = {
|
||||
className?: string,
|
||||
title: string,
|
||||
icon: TaskIcon,
|
||||
status: TaskSimplifiedStatus,
|
||||
onInteract?: (e: SyntheticEvent<HTMLButtonElement>) => void,
|
||||
}
|
||||
|
||||
|
||||
export function TaskIconComponent({className, title, icon, status, onInteract}: TaskIconProps) {
|
||||
const clickable = !!onInteract;
|
||||
|
||||
return (
|
||||
<button
|
||||
className={cn({
|
||||
[style.taskIconComponent]: true,
|
||||
[style.taskIconComponentClickable]: clickable,
|
||||
}, className)}
|
||||
type={"button"}
|
||||
title={title}
|
||||
onClick={onInteract}
|
||||
onKeyDown={onInteract}
|
||||
tabIndex={clickable ? 0 : -1}
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
size={"lg"}
|
||||
icon={[TaskSimplifiedStatus.Complete, TaskSimplifiedStatus.Journaled].includes(status) ? TASK_ICON_TO_FONTAWESOME_SOLID[icon] : TASK_ICON_TO_FONTAWESOME_REGULAR[icon]}
|
||||
beatFade={status === TaskSimplifiedStatus.InProgress}
|
||||
/>
|
||||
</button>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* A simplification of the `started_on`, `completed_on` and `journaled_on` fields of a {@link Task}.
|
||||
*/
|
||||
export enum TaskSimplifiedStatus {
|
||||
NonExistent = -1,
|
||||
Unfinished = 0,
|
||||
InProgress = 1,
|
||||
Complete = 2,
|
||||
Journaled = 3,
|
||||
}
|
|
@ -3,4 +3,5 @@ export enum GroupingMode {
|
|||
ByPriority,
|
||||
ByStatus,
|
||||
ByIcon,
|
||||
Journal,
|
||||
}
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
|
||||
|
||||
export type TaskGroup<K> = {
|
||||
k: K,
|
||||
tasks: TaskWithId[],
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
import {TaskIcon, TaskImportance, TaskPriority, TaskStatus} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode"
|
||||
import {TaskGroup} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskGroup"
|
||||
import {TaskGroup} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/TaskGroup"
|
||||
|
||||
|
||||
const TASK_IMPORTANCE_TO_VALUE = {
|
||||
[TaskImportance.Highest]: 1,
|
||||
|
@ -19,9 +20,10 @@ const TASK_PRIORITY_TO_VALUE = {
|
|||
}
|
||||
|
||||
const TASK_STATUS_TO_VALUE = {
|
||||
[TaskStatus.Unfinished]: 1,
|
||||
[TaskStatus.InProgress]: 2,
|
||||
[TaskStatus.Complete]: 3,
|
||||
"Journaled": 3,
|
||||
"Complete": 2,
|
||||
"InProgress": 1,
|
||||
"Unfinished": 0,
|
||||
}
|
||||
|
||||
const TASK_ICON_TO_VALUE = {
|
||||
|
@ -55,10 +57,14 @@ export const GROUPING_MODE_TO_GROUP_SORTER_FUNCTION = {
|
|||
[GroupingMode.ByPriority]: function sortGroupsByPriority(a: TaskGroup<TaskPriority>, b: TaskGroup<TaskPriority>): number {
|
||||
return TASK_PRIORITY_TO_VALUE[a.k] - TASK_PRIORITY_TO_VALUE[b.k]
|
||||
},
|
||||
[GroupingMode.ByStatus]: function sortGroupsByStatus(a: TaskGroup<TaskStatus>, b: TaskGroup<TaskStatus>): number {
|
||||
return TASK_STATUS_TO_VALUE[a.k] - TASK_STATUS_TO_VALUE[b.k]
|
||||
[GroupingMode.ByStatus]: function sortGroupsByStatus(a: TaskGroup<string>, b: TaskGroup<string>): number {
|
||||
// @ts-ignore
|
||||
return TASK_STATUS_TO_VALUE[a.k] - TASK_STATUS_TO_VALUE[b.k]
|
||||
},
|
||||
[GroupingMode.ByIcon]: function sortGroupsByIcon(a: TaskGroup<TaskIcon>, b: TaskGroup<TaskIcon>): number {
|
||||
return TASK_ICON_TO_VALUE[a.k] - TASK_ICON_TO_VALUE[b.k]
|
||||
},
|
||||
[GroupingMode.Journal]: function sortGroupsAlphabetically(a: TaskGroup<TaskIcon>, b: TaskGroup<TaskIcon>): number {
|
||||
return b.k.localeCompare(a.k)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,32 @@
|
|||
import {TaskIcon, TaskImportance, TaskPriority, TaskStatus} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskWithId"
|
||||
|
||||
|
||||
export const GROUPING_MODE_TO_TASK_GROUPER_FUNCTION = {
|
||||
[GroupingMode.ByImportance]: function groupTasksByImportance(t: TaskWithId): TaskImportance {
|
||||
[GroupingMode.ByImportance]: function groupTasksByImportance(t: TaskWithId): TaskImportance | null {
|
||||
if(t[1].journaled_on) return null
|
||||
return t[1].importance
|
||||
},
|
||||
[GroupingMode.ByPriority]: function groupTasksByPriority(t: TaskWithId): TaskPriority {
|
||||
[GroupingMode.ByPriority]: function groupTasksByPriority(t: TaskWithId): TaskPriority | null {
|
||||
if(t[1].journaled_on) return null
|
||||
return t[1].priority
|
||||
},
|
||||
[GroupingMode.ByStatus]: function groupTasksByStatus(t: TaskWithId): TaskStatus {
|
||||
return t[1].status
|
||||
[GroupingMode.ByStatus]: function groupTasksByStatus(t: TaskWithId): string | null {
|
||||
if(t[1].journaled_on) return null
|
||||
else if(t[1].completed_on) return "Complete"
|
||||
else if(t[1].started_on) return "InProgress"
|
||||
else return "Unfinished"
|
||||
},
|
||||
[GroupingMode.ByIcon]: function groupTasksByIcon(t: TaskWithId): TaskIcon {
|
||||
[GroupingMode.ByIcon]: function groupTasksByIcon(t: TaskWithId): TaskIcon | null {
|
||||
if(t[1].journaled_on) return null
|
||||
return t[1].icon
|
||||
},
|
||||
[GroupingMode.Journal]: function groupTasksByCompletitionDate(t: TaskWithId): string | null {
|
||||
if(!t[1].journaled_on) return null
|
||||
let date;
|
||||
if(!t[1].completed_on) date = new Date(0)
|
||||
else date = new Date(t[1].completed_on)
|
||||
return date.toISOString().split("T")[0]
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,120 +1,112 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {TASK_ICON_TO_FONTAWESOME_REGULAR, TaskIcon, TaskImportance, TaskPriority, TaskStatus} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TASK_ICON_TO_FONTAWESOME_REGULAR, TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer.module.css"
|
||||
import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode"
|
||||
import taskImportanceStyle from "@/app/[lang]/board/[board]/(page)/(view)/(task)/taskImportance.module.css"
|
||||
import taskPriorityStyle from "@/app/[lang]/board/[board]/(page)/(view)/(task)/taskPriority.module.css"
|
||||
import taskStatusStyle from "@/app/[lang]/board/[board]/(page)/(view)/(task)/taskStatus.module.css"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import {TFunction} from "i18next"
|
||||
import {ReactNode} from "react"
|
||||
|
||||
|
||||
export interface TitleComponentProps {
|
||||
lang: string,
|
||||
t: TFunction,
|
||||
k: any,
|
||||
}
|
||||
|
||||
|
||||
export const GROUPING_MODE_TO_TITLE_COMPONENT: {[key in GroupingMode]: (t: TitleComponentProps) => ReactNode} = {
|
||||
[GroupingMode.ByImportance]: function TitleFromImportance({lang, k}: { lang: string, k: TaskImportance }): ReactNode {
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
|
||||
[GroupingMode.ByImportance]: function TitleFromImportance({t, k}: { t: TFunction, k: TaskImportance }): ReactNode {
|
||||
switch(k) {
|
||||
case TaskImportance.Highest:
|
||||
return (
|
||||
<span className={taskImportanceStyle.taskImportanceHighest}>
|
||||
{t("columnHeaderTaskImportanceHighest")}
|
||||
<span className={style.taskImportanceHighest}>
|
||||
{t("taskImportanceHighest")}
|
||||
</span>
|
||||
)
|
||||
case TaskImportance.High:
|
||||
return (
|
||||
<span className={taskImportanceStyle.taskImportanceHigh}>
|
||||
{t("columnHeaderTaskImportanceHigh")}
|
||||
<span className={style.taskImportanceHigh}>
|
||||
{t("taskImportanceHigh")}
|
||||
</span>
|
||||
)
|
||||
case TaskImportance.Normal:
|
||||
return (
|
||||
<span className={taskImportanceStyle.taskImportanceNormal}>
|
||||
{t("columnHeaderTaskImportanceNormal")}
|
||||
<span className={style.taskImportanceNormal}>
|
||||
{t("taskImportanceNormal")}
|
||||
</span>
|
||||
)
|
||||
case TaskImportance.Low:
|
||||
return (
|
||||
<span className={taskImportanceStyle.taskImportanceLow}>
|
||||
{t("columnHeaderTaskImportanceLow")}
|
||||
<span className={style.taskImportanceLow}>
|
||||
{t("taskImportanceLow")}
|
||||
</span>
|
||||
)
|
||||
case TaskImportance.Lowest:
|
||||
return (
|
||||
<span className={taskImportanceStyle.taskImportanceLowest}>
|
||||
{t("columnHeaderTaskImportanceLowest")}
|
||||
<span className={style.taskImportanceLowest}>
|
||||
{t("taskImportanceLowest")}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
[GroupingMode.ByPriority]: function TitleFromPriority({lang, k}: { lang: string, k: TaskPriority }): ReactNode {
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
|
||||
[GroupingMode.ByPriority]: function TitleFromPriority({t, k}: { t: TFunction, k: TaskPriority }): ReactNode {
|
||||
switch(k) {
|
||||
case TaskPriority.Highest:
|
||||
return (
|
||||
<span className={taskPriorityStyle.taskPriorityHighestColumnHeader}>
|
||||
{t("columnHeaderTaskPriorityHighest")}
|
||||
<span className={style.taskPriorityHighestColumnHeader}>
|
||||
{t("taskPriorityHighest")}
|
||||
</span>
|
||||
)
|
||||
case TaskPriority.High:
|
||||
return (
|
||||
<span className={taskPriorityStyle.taskPriorityHighColumnHeader}>
|
||||
{t("columnHeaderTaskPriorityHigh")}
|
||||
<span className={style.taskPriorityHighColumnHeader}>
|
||||
{t("taskPriorityHigh")}
|
||||
</span>
|
||||
)
|
||||
case TaskPriority.Normal:
|
||||
return (
|
||||
<span className={taskPriorityStyle.taskPriorityNormalColumnHeader}>
|
||||
{t("columnHeaderTaskPriorityNormal")}
|
||||
<span className={style.taskPriorityNormalColumnHeader}>
|
||||
{t("taskPriorityNormal")}
|
||||
</span>
|
||||
)
|
||||
case TaskPriority.Low:
|
||||
return (
|
||||
<span className={taskPriorityStyle.taskPriorityLowColumnHeader}>
|
||||
{t("columnHeaderTaskPriorityLow")}
|
||||
<span className={style.taskPriorityLowColumnHeader}>
|
||||
{t("taskPriorityLow")}
|
||||
</span>
|
||||
)
|
||||
case TaskPriority.Lowest:
|
||||
return (
|
||||
<span className={taskPriorityStyle.taskPriorityLowestColumnHeader}>
|
||||
{t("columnHeaderTaskPriorityLowest")}
|
||||
<span className={style.taskPriorityLowestColumnHeader}>
|
||||
{t("taskPriorityLowest")}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
[GroupingMode.ByStatus]: function TitleFromStatus({lang, k}: { lang: string, k: TaskStatus }): ReactNode {
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
|
||||
[GroupingMode.ByStatus]: function TitleFromStatus({t, k}: { t: TFunction, k: string }): ReactNode {
|
||||
switch(k) {
|
||||
case TaskStatus.Unfinished:
|
||||
case "Unfinished":
|
||||
return (
|
||||
<span className={taskStatusStyle.taskStatusUnfinishedColumnHeader}>
|
||||
{t("columnHeaderTaskStatusUnfinished")}
|
||||
<span className={style.taskStatusUnfinishedColumnHeader}>
|
||||
{t("taskStatusUnfinished")}
|
||||
</span>
|
||||
)
|
||||
case TaskStatus.InProgress:
|
||||
case "InProgress":
|
||||
return (
|
||||
<span className={taskStatusStyle.taskStatusInProgressColumnHeader}>
|
||||
{t("columnHeaderTaskStatusInProgress")}
|
||||
<span className={style.taskStatusInProgressColumnHeader}>
|
||||
{t("taskStatusInProgress")}
|
||||
</span>
|
||||
)
|
||||
case TaskStatus.Complete:
|
||||
case "Complete":
|
||||
return (
|
||||
<span className={taskStatusStyle.taskStatusCompleteColumnHeader}>
|
||||
{t("columnHeaderTaskStatusComplete")}
|
||||
<span className={style.taskStatusCompleteColumnHeader}>
|
||||
{t("taskStatusComplete")}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
[GroupingMode.ByIcon]: function TitleFromIcon({k}: { lang: string, k: TaskIcon }): ReactNode {
|
||||
[GroupingMode.ByIcon]: function TitleFromIcon({k}: { t: TFunction, k: TaskIcon }): ReactNode {
|
||||
return (
|
||||
<span>
|
||||
<FontAwesomeIcon icon={TASK_ICON_TO_FONTAWESOME_REGULAR[k]}/>
|
||||
|
@ -123,4 +115,12 @@ export const GROUPING_MODE_TO_TITLE_COMPONENT: {[key in GroupingMode]: (t: Title
|
|||
</span>
|
||||
)
|
||||
},
|
||||
|
||||
[GroupingMode.Journal]: function TitleFromDatetime({k}: { t: TFunction, k: string }): ReactNode {
|
||||
return (
|
||||
<time dateTime={k}>
|
||||
{k}
|
||||
</time>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -4,4 +4,5 @@ export enum SortingMode {
|
|||
ByIcon,
|
||||
ByText,
|
||||
ByStatus,
|
||||
ByCreation,
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {TaskIcon, TaskImportance, TaskPriority, TaskStatus} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
import {SortingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(sorting)/SortingMode"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskWithId"
|
||||
|
||||
|
||||
const TASK_IMPORTANCE_TO_VALUE = {
|
||||
|
@ -19,12 +19,6 @@ const TASK_PRIORITY_TO_VALUE = {
|
|||
[TaskPriority.Lowest]: 5,
|
||||
}
|
||||
|
||||
const TASK_STATUS_TO_VALUE = {
|
||||
[TaskStatus.Unfinished]: 2,
|
||||
[TaskStatus.InProgress]: 1,
|
||||
[TaskStatus.Complete]: 3,
|
||||
}
|
||||
|
||||
const TASK_ICON_TO_VALUE = {
|
||||
[TaskIcon.Bookmark]: 1,
|
||||
[TaskIcon.Circle]: 2,
|
||||
|
@ -76,6 +70,27 @@ const SORTING_MODE_TO_SORTING_FUNCTION = {
|
|||
return TASK_PRIORITY_TO_VALUE[a[1].priority] - TASK_PRIORITY_TO_VALUE[b[1].priority]
|
||||
},
|
||||
[SortingMode.ByStatus]: function sortTasksByStatus(a: TaskWithId, b: TaskWithId) {
|
||||
return TASK_STATUS_TO_VALUE[a[1].status] - TASK_STATUS_TO_VALUE[b[1].status]
|
||||
}
|
||||
if(a[1].journaled_on && !b[1].journaled_on) return 1;
|
||||
if(!a[1].journaled_on && b[1].journaled_on) return -1;
|
||||
if(a[1].completed_on && !b[1].completed_on) return 1;
|
||||
if(!a[1].completed_on && b[1].completed_on) return -1;
|
||||
if(a[1].started_on && !b[1].started_on) return -1;
|
||||
if(!a[1].started_on && b[1].started_on) return 1;
|
||||
|
||||
// @ts-ignore
|
||||
const journaled_on = a[1].journaled_on - b[1].journaled_on
|
||||
if(journaled_on) return journaled_on
|
||||
// @ts-ignore
|
||||
const completed_on = a[1].completed_on - b[1].completed_on
|
||||
if(completed_on) return completed_on
|
||||
// @ts-ignore
|
||||
const started_on = a[1].started_on - b[1].started_on
|
||||
if(started_on) return started_on
|
||||
|
||||
return 0
|
||||
},
|
||||
[SortingMode.ByCreation]: function sortTasksByCreation(a: TaskWithId, b: TaskWithId) {
|
||||
// @ts-ignore
|
||||
return a[1].created_on - b[1].created_on
|
||||
},
|
||||
}
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskWithId"
|
||||
|
||||
|
||||
export type TaskGroup<K> = {
|
||||
k: K,
|
||||
tasks: TaskWithId[],
|
||||
}
|
|
@ -0,0 +1,151 @@
|
|||
import {ModifyTaskBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {TaskActions} from "@/app/[lang]/board/[board]/(page)/(task)/TaskActions"
|
||||
import {TaskContainer} from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer"
|
||||
import {TaskDescription} from "@/app/[lang]/board/[board]/(page)/(task)/TaskDescription"
|
||||
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
import {TaskViewerIcon} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskViewerIcon"
|
||||
import {TaskViewerJournalButton} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskViewerJournalButton"
|
||||
import {TaskViewerRecreateButton} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskViewerRecreateButton"
|
||||
import {TFunction} from "i18next"
|
||||
import {Dispatch, KeyboardEvent, PointerEvent, SetStateAction, SyntheticEvent, useCallback, useState} from "react"
|
||||
|
||||
|
||||
export function TaskViewer({t, taskWithId: [id, task], setEditorInput}: {t: TFunction, taskWithId: TaskWithId, setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const [isFlipped, setFlipped] = useState<boolean>(false)
|
||||
const {sendRequest, boardState: {locked}} = useBoardConsumer()
|
||||
|
||||
const toggleFlipOnKeyDown = useCallback(
|
||||
(e: KeyboardEvent<HTMLDivElement>) => {
|
||||
if("key" in e && typeof e["key"] === "string") {
|
||||
if(!["Enter", " "].includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setFlipped(prev => !prev)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const flipOnPointerEnter = useCallback(
|
||||
(e: PointerEvent<HTMLDivElement>) => {
|
||||
if(!["mouse", "pen"].includes(e.pointerType)) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setFlipped(true)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const flopOnPointerLeave = useCallback(
|
||||
(e: PointerEvent<HTMLDivElement>) => {
|
||||
if(!["mouse", "pen"].includes(e.pointerType)) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setFlipped(false)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const toggleFlipOnTouch = useCallback(
|
||||
(e: PointerEvent<HTMLDivElement>) => {
|
||||
if(!["", "touch"].includes(e.pointerType)) {
|
||||
return
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setFlipped(prev => !prev)
|
||||
},
|
||||
[]
|
||||
)
|
||||
|
||||
const toggleStatus = useCallback((e: SyntheticEvent<HTMLButtonElement>) => {
|
||||
if("key" in e && typeof e["key"] === "string") {
|
||||
if(![" "].includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let request: ModifyTaskBoardRequest
|
||||
|
||||
if(!task.started_on) {
|
||||
request = {"Task": [id, {...task, started_on: + new Date()}]}
|
||||
}
|
||||
else if(!task.completed_on) {
|
||||
request = {"Task": [id, {...task, completed_on: + new Date()}]}
|
||||
}
|
||||
else {
|
||||
request = {"Task": [id, {...task, started_on: null, completed_on: null}]}
|
||||
}
|
||||
sendRequest(request)
|
||||
}, [id, task, locked, sendRequest])
|
||||
|
||||
let status = TaskSimplifiedStatus.Unfinished
|
||||
if(task.started_on) status = TaskSimplifiedStatus.InProgress
|
||||
if(task.completed_on) status = TaskSimplifiedStatus.Complete
|
||||
if(task.journaled_on) status = TaskSimplifiedStatus.Journaled
|
||||
|
||||
let sideElements;
|
||||
if(!isFlipped) {
|
||||
sideElements = null;
|
||||
}
|
||||
else {
|
||||
let goAwayButton
|
||||
if(status < TaskSimplifiedStatus.Complete) {
|
||||
goAwayButton = (
|
||||
<TaskViewerRecreateButton
|
||||
t={t}
|
||||
taskWithId={[id, task]}
|
||||
setEditorInput={setEditorInput}
|
||||
/>
|
||||
)
|
||||
}
|
||||
else {
|
||||
goAwayButton = (
|
||||
<TaskViewerJournalButton
|
||||
t={t}
|
||||
taskWithId={[id, task]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
sideElements = (
|
||||
<TaskActions>
|
||||
{goAwayButton}
|
||||
</TaskActions>
|
||||
)
|
||||
}
|
||||
|
||||
// noinspection PointlessBooleanExpressionJS
|
||||
return (
|
||||
<TaskContainer
|
||||
role={"article"}
|
||||
importance={task.importance}
|
||||
priority={task.priority}
|
||||
status={status}
|
||||
onKeyDown={toggleFlipOnKeyDown}
|
||||
onPointerEnter={flipOnPointerEnter}
|
||||
onPointerLeave={flopOnPointerLeave}
|
||||
onPointerDown={toggleFlipOnTouch}
|
||||
>
|
||||
<TaskViewerIcon
|
||||
t={t}
|
||||
icon={task.icon}
|
||||
status={status}
|
||||
onInteract={status === TaskSimplifiedStatus.Journaled ? undefined : toggleStatus}
|
||||
/>
|
||||
<TaskDescription
|
||||
text={task.text}
|
||||
/>
|
||||
{sideElements}
|
||||
</TaskContainer>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskIconComponent} from "@/app/[lang]/board/[board]/(page)/(task)/TaskIconComponent"
|
||||
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
|
||||
import {TFunction} from "i18next"
|
||||
|
||||
|
||||
const TASK_STATUS_TO_I18N_KEY: {[key in TaskSimplifiedStatus]: string} = {
|
||||
[TaskSimplifiedStatus.NonExistent]: "taskStatusNonExistent",
|
||||
[TaskSimplifiedStatus.Unfinished]: "taskStatusUnfinished",
|
||||
[TaskSimplifiedStatus.InProgress]: "taskStatusInProgress",
|
||||
[TaskSimplifiedStatus.Complete]: "taskStatusComplete",
|
||||
[TaskSimplifiedStatus.Journaled]: "taskStatusJournaled",
|
||||
}
|
||||
|
||||
export type TaskViewerIconProps = {
|
||||
t: TFunction,
|
||||
status: TaskSimplifiedStatus,
|
||||
icon: TaskIcon,
|
||||
onInteract?: Parameters<typeof TaskIconComponent>[0]["onInteract"]
|
||||
}
|
||||
|
||||
export function TaskViewerIcon({t, status, icon, onInteract}: TaskViewerIconProps) {
|
||||
return (
|
||||
<TaskIconComponent
|
||||
title={t(TASK_STATUS_TO_I18N_KEY[status])}
|
||||
status={status}
|
||||
icon={icon}
|
||||
onInteract={onInteract}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
import {UpdateTaskBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {TaskButton} from "@/app/[lang]/board/[board]/(page)/(task)/TaskButton"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
import {faBookBookmark} from "@fortawesome/free-solid-svg-icons"
|
||||
import {TFunction} from "i18next"
|
||||
import {SyntheticEvent, useCallback} from "react"
|
||||
|
||||
|
||||
export function TaskViewerJournalButton({t, taskWithId: [id, task]}: {t: TFunction, taskWithId: TaskWithId}) {
|
||||
const {sendRequest, boardState: {locked}} = useBoardConsumer()
|
||||
|
||||
const toggleJournalTask = useCallback((e: SyntheticEvent<HTMLButtonElement>) => {
|
||||
if("key" in e && typeof e["key"] === "string") {
|
||||
if(!["Enter", " "].includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
const request: UpdateTaskBoardChange = {"Task": [id, {...task, journaled_on: task.journaled_on ? null : + new Date()}]};
|
||||
sendRequest(request)
|
||||
}, [task, sendRequest])
|
||||
|
||||
return (
|
||||
<TaskButton
|
||||
title={t("taskButtonJournal")}
|
||||
icon={faBookBookmark}
|
||||
onInteract={locked ? undefined : toggleJournalTask}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
import {DeleteTaskBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/taskToString"
|
||||
import {TaskButton} from "@/app/[lang]/board/[board]/(page)/(task)/TaskButton"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
import {faTrashArrowUp} from "@fortawesome/free-solid-svg-icons"
|
||||
import {TFunction} from "i18next"
|
||||
import {Dispatch, SetStateAction, SyntheticEvent, useCallback} from "react"
|
||||
|
||||
|
||||
export type TaskViewerRecreateButtonProps = {
|
||||
t: TFunction,
|
||||
taskWithId: TaskWithId,
|
||||
setEditorInput: Dispatch<SetStateAction<string>>,
|
||||
}
|
||||
|
||||
|
||||
export function TaskViewerRecreateButton({t, taskWithId: [id, task], setEditorInput}: TaskViewerRecreateButtonProps) {
|
||||
const {sendRequest, boardState: {locked}} = useBoardConsumer()
|
||||
|
||||
const recreateTask = useCallback((e: SyntheticEvent<HTMLButtonElement>) => {
|
||||
if("key" in e && typeof e["key"] === "string") {
|
||||
if(!["Enter", " "].includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setEditorInput(taskToString(task))
|
||||
const request: DeleteTaskBoardRequest = {"Task": [id, null]};
|
||||
sendRequest(request)
|
||||
}, [task, setEditorInput, sendRequest])
|
||||
|
||||
return (
|
||||
<TaskButton
|
||||
title={t("taskButtonRecreate")}
|
||||
icon={faTrashArrowUp}
|
||||
onInteract={locked ? undefined : recreateTask}
|
||||
/>
|
||||
)
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
.taskViewerSide {
|
||||
|
||||
}
|
||||
|
||||
.taskViewerSideVisible {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.taskViewerSideHidden {
|
||||
display: none;
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
import cn from "classnames"
|
||||
import {ReactNode} from "react"
|
||||
import style from "./TaskViewerSide.module.css"
|
||||
|
||||
|
||||
export function TaskViewerSide({visible, children}: {visible: boolean, children: ReactNode}) {
|
||||
return (
|
||||
<div className={cn({
|
||||
[style.taskViewerSide]: true,
|
||||
[style.taskViewerSideVisible]: visible,
|
||||
[style.taskViewerSideHidden]: !visible,
|
||||
})}>
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
}
|
|
@ -1,26 +0,0 @@
|
|||
import {Task, TaskImportance, TaskPriority, TaskStatus} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import classNames from "classnames"
|
||||
import importanceStyle from "./taskImportance.module.css"
|
||||
import priorityStyle from "./taskPriority.module.css"
|
||||
import statusStyle from "./taskStatus.module.css"
|
||||
|
||||
|
||||
export function taskClassNames(t: Task) {
|
||||
return classNames({
|
||||
[importanceStyle.taskImportanceHighest]: t.importance === TaskImportance.Highest,
|
||||
[importanceStyle.taskImportanceHigh]: t.importance === TaskImportance.High,
|
||||
[importanceStyle.taskImportanceNormal]: t.importance === TaskImportance.Normal,
|
||||
[importanceStyle.taskImportanceLow]: t.importance === TaskImportance.Low,
|
||||
[importanceStyle.taskImportanceLowest]: t.importance === TaskImportance.Lowest,
|
||||
|
||||
[priorityStyle.taskPriorityHighest]: t.priority === TaskPriority.Highest,
|
||||
[priorityStyle.taskPriorityHigh]: t.priority === TaskPriority.High,
|
||||
[priorityStyle.taskPriorityNormal]: t.priority === TaskPriority.Normal,
|
||||
[priorityStyle.taskPriorityLow]: t.priority === TaskPriority.Low,
|
||||
[priorityStyle.taskPriorityLowest]: t.priority === TaskPriority.Lowest,
|
||||
|
||||
[statusStyle.taskStatusUnfinished]: t.status === TaskStatus.Unfinished,
|
||||
[statusStyle.taskStatusInProgress]: t.status === TaskStatus.InProgress,
|
||||
[statusStyle.taskStatusComplete]: t.status === TaskStatus.Complete,
|
||||
})
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
.taskImportanceHighest {
|
||||
--bhsl-current-hue: 18deg;
|
||||
--bhsl-current-saturation: 100%;
|
||||
--bhsl-current-lightness: 75%;
|
||||
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.taskImportanceHigh {
|
||||
--bhsl-current-hue: 250deg;
|
||||
--bhsl-current-saturation: 100%;
|
||||
--bhsl-current-lightness: 78%;
|
||||
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.taskImportanceNormal {
|
||||
--bhsl-current-hue: 212deg;
|
||||
--bhsl-current-saturation: 100%;
|
||||
--bhsl-current-lightness: 81%;
|
||||
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.taskImportanceLow {
|
||||
--bhsl-current-hue: 160deg;
|
||||
--bhsl-current-saturation: 45%;
|
||||
--bhsl-current-lightness: 60%;
|
||||
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
.taskImportanceLowest {
|
||||
--bhsl-current-hue: 120deg;
|
||||
--bhsl-current-saturation: 30%;
|
||||
--bhsl-current-lightness: 45%;
|
||||
|
||||
font-weight: 200;
|
||||
}
|
|
@ -1,45 +0,0 @@
|
|||
.taskPriorityHighest {
|
||||
border: 4px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.45);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.taskPriorityHigh {
|
||||
border: 3px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.25);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.taskPriorityNormal {
|
||||
border: 2px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.15);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.taskPriorityLow {
|
||||
border: 1px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.15);
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.taskPriorityLowest {
|
||||
border: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
|
||||
.taskPriorityHighestColumnHeader {
|
||||
text-decoration: underline 4px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 1.0);
|
||||
}
|
||||
|
||||
.taskPriorityHighColumnHeader {
|
||||
text-decoration: underline 3px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.75);
|
||||
}
|
||||
|
||||
.taskPriorityNormalColumnHeader {
|
||||
text-decoration: underline 2px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.50);
|
||||
}
|
||||
|
||||
.taskPriorityLowColumnHeader {
|
||||
text-decoration: underline 1px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.25);
|
||||
}
|
||||
|
||||
.taskPriorityLowestColumnHeader {
|
||||
text-decoration: none;
|
||||
}
|
|
@ -1,44 +0,0 @@
|
|||
@keyframes inProgress {
|
||||
0%, 100% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
50% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.taskStatusUnfinished span {
|
||||
|
||||
}
|
||||
|
||||
.taskStatusInProgress span {
|
||||
animation-name: inProgress;
|
||||
animation-delay: 0s;
|
||||
animation-direction: normal;
|
||||
animation-duration: 1s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1);
|
||||
}
|
||||
|
||||
.taskStatusComplete span {
|
||||
opacity: 0.4;
|
||||
text-decoration: 2px currentColor solid line-through;
|
||||
}
|
||||
|
||||
.taskStatusUnfinishedColumnHeader {
|
||||
|
||||
}
|
||||
|
||||
.taskStatusInProgressColumnHeader {
|
||||
animation-name: inProgress;
|
||||
animation-delay: 0s;
|
||||
animation-direction: normal;
|
||||
animation-duration: 1s;
|
||||
animation-iteration-count: infinite;
|
||||
animation-timing-function: cubic-bezier(0.4, 0, 0.6, 1);
|
||||
}
|
||||
|
||||
.taskStatusCompleteColumnHeader {
|
||||
opacity: 0.4;
|
||||
text-decoration: 2px currentColor solid line-through;
|
||||
}
|
|
@ -1,17 +1,16 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {ColumningMode} from "@/app/[lang]/board/[board]/(page)/(view)/(columning)"
|
||||
import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)"
|
||||
import {SortingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(sorting)"
|
||||
import {SplashWithIcon} from "@/app/[lang]/board/[board]/(page)/(view)/SplashWithIcon"
|
||||
import {BoardViewer} from "@/app/[lang]/board/[board]/(page)/(view)/BoardViewer"
|
||||
import {faLink, faLinkSlash, faGear, faAsterisk} from "@fortawesome/free-solid-svg-icons"
|
||||
import {SplashWithIcon} from "@/app/[lang]/board/[board]/(page)/(view)/SplashWithIcon"
|
||||
import {faAsterisk, faGear, faLink, faLinkSlash} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import {TFunction} from "i18next"
|
||||
import {Dispatch, SetStateAction} from "react"
|
||||
|
||||
|
||||
export function BoardMain({lang, className, columning, sorting, grouping, setEditorInput}: {lang: string, className?: string, columning: ColumningMode, grouping: GroupingMode, sorting: SortingMode[], setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
export function BoardMain({t, className, columning, sorting, grouping, setEditorInput}: {t: TFunction, className?: string, columning: ColumningMode, grouping: GroupingMode, sorting: SortingMode[], setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const {webSocketState, webSocketBackoffMs, boardState} = useBoardConsumer()
|
||||
|
||||
switch(webSocketState) {
|
||||
|
@ -28,7 +27,7 @@ export function BoardMain({lang, className, columning, sorting, grouping, setEdi
|
|||
className={className}
|
||||
/>
|
||||
case WebSocket.OPEN:
|
||||
if(Object.keys(boardState.tasksById).length === 0) {
|
||||
if(Object.keys(boardState.tasks).length === 0) {
|
||||
return <SplashWithIcon
|
||||
icon={<FontAwesomeIcon size={"4x"} icon={faAsterisk}/>}
|
||||
text={t("boardEmpty")}
|
||||
|
@ -37,7 +36,7 @@ export function BoardMain({lang, className, columning, sorting, grouping, setEdi
|
|||
}
|
||||
else {
|
||||
return <BoardViewer
|
||||
lang={lang}
|
||||
t={t}
|
||||
columning={columning}
|
||||
sorting={sorting}
|
||||
grouping={grouping}
|
||||
|
|
|
@ -1,19 +1,20 @@
|
|||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
import {ColumningMode} from "@/app/[lang]/board/[board]/(page)/(view)/(columning)"
|
||||
import {GROUPING_MODE_TO_TITLE_COMPONENT, GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)"
|
||||
import {TaskGroup} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/TaskGroup"
|
||||
import {SortingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(sorting)"
|
||||
import {TaskGroup} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskGroup"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskWithId"
|
||||
import {TaskViewer} from "@/app/[lang]/board/[board]/(page)/(view)/TaskViewer"
|
||||
import {TaskViewer} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskViewer"
|
||||
import {useBoardTasksArranger} from "@/app/[lang]/board/[board]/(page)/(view)/useBoardTasksArranger"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
import {Dispatch, SetStateAction} from "react"
|
||||
import style from "./BoardViewer.module.css"
|
||||
|
||||
|
||||
export function BoardViewer({className, lang, columning, grouping, sorting, setEditorInput}: {className?: string, lang: string, columning: ColumningMode, grouping: GroupingMode, sorting: SortingMode[], setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const {boardState: {tasksById}} = useBoardConsumer()
|
||||
const {taskGroups} = useBoardTasksArranger(tasksById, grouping, sorting);
|
||||
export function BoardViewer({className, t, columning, grouping, sorting, setEditorInput}: {className?: string, t: TFunction, columning: ColumningMode, grouping: GroupingMode, sorting: SortingMode[], setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const {boardState: {tasks}} = useBoardConsumer()
|
||||
const {taskGroups} = useBoardTasksArranger(tasks, grouping, sorting);
|
||||
|
||||
return (
|
||||
<main className={cn(className, {
|
||||
|
@ -21,21 +22,21 @@ export function BoardViewer({className, lang, columning, grouping, sorting, setE
|
|||
[style.boardMainTaskGroupsMultiColumn]: columning === ColumningMode.MultiColumn,
|
||||
[style.boardMainTaskGroupsSingleColumn]: columning === ColumningMode.SingleColumn,
|
||||
})}>
|
||||
{taskGroups.map((tg) => <BoardViewerColumn lang={lang} taskGroup={tg} key={tg.k} grouping={grouping} setEditorInput={setEditorInput}/>)}
|
||||
{taskGroups.map((tg) => <BoardViewerColumn t={t} taskGroup={tg} key={tg.k} grouping={grouping} setEditorInput={setEditorInput}/>)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export function BoardViewerColumn({lang, grouping, taskGroup, setEditorInput}: {lang: string, grouping: GroupingMode, taskGroup: TaskGroup<string | number>, setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
export function BoardViewerColumn({t, grouping, taskGroup, setEditorInput}: {t: TFunction, grouping: GroupingMode, taskGroup: TaskGroup<string | number>, setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const ColumnTitle = GROUPING_MODE_TO_TITLE_COMPONENT[grouping]
|
||||
|
||||
return (
|
||||
<div className={style.boardColumn}>
|
||||
<h3>
|
||||
<ColumnTitle lang={lang} k={taskGroup.k}/>
|
||||
<ColumnTitle t={t} k={taskGroup.k}/>
|
||||
</h3>
|
||||
<div className={style.boardColumnContents}>
|
||||
{taskGroup.tasks.map((task: TaskWithId) => <TaskViewer lang={lang} task={task} key={task[0]} setEditorInput={setEditorInput}/>)}
|
||||
{taskGroup.tasks.map((task: TaskWithId) => <TaskViewer t={t} taskWithId={task} key={task[0]} setEditorInput={setEditorInput}/>)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -1,28 +0,0 @@
|
|||
.taskViewer {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
min-width: 240px;
|
||||
min-height: 48px;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.taskIcon {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
width: 28px;
|
||||
height: 100%;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.taskViewerBack {
|
||||
justify-content: end;
|
||||
}
|
||||
|
||||
.taskViewerDebug {
|
||||
font-size: xx-small;
|
||||
}
|
|
@ -1,112 +0,0 @@
|
|||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {DeleteTaskBoardRequest, ModifyTaskBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
|
||||
import {TASK_ICON_TO_FONTAWESOME_REGULAR, TASK_ICON_TO_FONTAWESOME_SOLID, TaskStatus} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/taskToString"
|
||||
import {taskClassNames} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/taskClassNames"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(view)/TaskViewer.module.css"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskWithId"
|
||||
import {faTrashArrowUp} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {Dispatch, SyntheticEvent, SetStateAction, useCallback, useState} from "react"
|
||||
|
||||
|
||||
export function TaskViewer({lang, task, setEditorInput}: {lang: string, task: TaskWithId, setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const [isFlipped, setFlipped] = useState<boolean>(false)
|
||||
|
||||
const toggleFlipped = useCallback((e: SyntheticEvent<HTMLElement>) => {
|
||||
if("key" in e && typeof e["key"] === "string") {
|
||||
if(!["Enter", " "].includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setFlipped(prev => !prev)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn({
|
||||
"panel": true,
|
||||
[style.taskViewer]: true,
|
||||
[style.taskViewerFront]: !isFlipped,
|
||||
[style.taskViewerBack]: isFlipped,
|
||||
}, taskClassNames(task[1]))}
|
||||
onClick={toggleFlipped}
|
||||
onKeyDownCapture={toggleFlipped}
|
||||
>
|
||||
{isFlipped ? <TaskViewerBack lang={lang} task={task} setEditorInput={setEditorInput}/> : <TaskViewerFront lang={lang} task={task}/>}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TaskViewerFront({task}: {lang: string, task: TaskWithId}) {
|
||||
const {sendRequest} = useBoardConsumer()
|
||||
|
||||
const toggleStatus = useCallback((e: SyntheticEvent<HTMLDivElement>) => {
|
||||
if("key" in e && typeof e["key"] === "string") {
|
||||
if(!["Enter", " "].includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
let request: ModifyTaskBoardRequest
|
||||
switch(task[1].status) {
|
||||
case TaskStatus.Unfinished:
|
||||
request = {"Task": [task[0], {...task[1], status: TaskStatus.InProgress}]}
|
||||
break
|
||||
case TaskStatus.InProgress:
|
||||
request = {"Task": [task[0], {...task[1], status: TaskStatus.Complete}]}
|
||||
break
|
||||
case TaskStatus.Complete:
|
||||
request = {"Task": [task[0], {...task[1], status: TaskStatus.Unfinished}]}
|
||||
break
|
||||
}
|
||||
sendRequest(request)
|
||||
}, [task, sendRequest])
|
||||
|
||||
return <>
|
||||
<div className={style.taskIcon} onClick={toggleStatus} onKeyDown={toggleStatus} tabIndex={0}>
|
||||
<FontAwesomeIcon
|
||||
size={"lg"}
|
||||
icon={task[1].status === TaskStatus.Complete ? TASK_ICON_TO_FONTAWESOME_SOLID[task[1].icon] : TASK_ICON_TO_FONTAWESOME_REGULAR[task[1].icon]}
|
||||
beatFade={task[1].status === TaskStatus.InProgress}
|
||||
/>
|
||||
</div>
|
||||
<span className={style.taskDescription} tabIndex={0}>
|
||||
{task[1].text}
|
||||
</span>
|
||||
</>
|
||||
}
|
||||
|
||||
function TaskViewerBack({lang, task, setEditorInput}: {lang: string, task: TaskWithId, setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const {t} = useClientTranslation(lang, "board")
|
||||
const {sendRequest} = useBoardConsumer()
|
||||
|
||||
const recreateTask = useCallback((e: SyntheticEvent<HTMLButtonElement>) => {
|
||||
if("key" in e && typeof e["key"] === "string") {
|
||||
if(!["Enter", " "].includes(e.key)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setEditorInput(taskToString(task[1]))
|
||||
const request: DeleteTaskBoardRequest = {"Task": [task[0], null]};
|
||||
sendRequest(request)
|
||||
}, [task, setEditorInput, sendRequest])
|
||||
|
||||
return <>
|
||||
<div className={style.taskViewerDebug}>
|
||||
{task[0]}
|
||||
</div>
|
||||
<div className={style.taskViewerButtons}>
|
||||
<button title={t("taskButtonRecreate")} onClick={recreateTask} onKeyDown={recreateTask} tabIndex={0}>
|
||||
<FontAwesomeIcon size={"sm"} icon={faTrashArrowUp}/>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
import {Task} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
import {GROUPING_MODE_TO_GROUP_SORTER_FUNCTION, GROUPING_MODE_TO_TASK_GROUPER_FUNCTION, GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)"
|
||||
import {TaskGroup} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/TaskGroup"
|
||||
import {SortingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(sorting)"
|
||||
import {getTaskSorter} from "@/app/[lang]/board/[board]/(page)/(view)/(sorting)/sortingModeToSortingFunction"
|
||||
import {TaskGroup} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskGroup"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(view)/(task)/TaskWithId"
|
||||
import {useMemo} from "react"
|
||||
|
||||
|
||||
export function useBoardTasksArranger(tasksById: {[id: string]: Task}, grouping: GroupingMode, sorting: SortingMode[]) {
|
||||
export function useBoardTasksArranger(tasks: {[id: string]: Task}, grouping: GroupingMode, sorting: SortingMode[]) {
|
||||
const taskGroups = useMemo(() => {
|
||||
const taskGrouperFunction = GROUPING_MODE_TO_TASK_GROUPER_FUNCTION[grouping]
|
||||
const keyedTaskGroups: {[t: string | number]: TaskWithId[]} = {}
|
||||
|
||||
Object.entries(tasksById).forEach((t: TaskWithId) => {
|
||||
Object.entries(tasks).forEach((t: TaskWithId) => {
|
||||
const group = taskGrouperFunction(t)
|
||||
if(group === null) return;
|
||||
const array = keyedTaskGroups[group] ?? []
|
||||
keyedTaskGroups[group] = [...array, t]
|
||||
})
|
||||
|
@ -29,7 +30,7 @@ export function useBoardTasksArranger(tasksById: {[id: string]: Task}, grouping:
|
|||
taskGroups.sort(groupSorterFunction)
|
||||
return taskGroups as TaskGroup<string | number>[]
|
||||
|
||||
}, [tasksById, grouping, sorting])
|
||||
}, [tasks, grouping, sorting])
|
||||
|
||||
return {
|
||||
taskGroups,
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue