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

Compare commits

...

28 commits

Author SHA1 Message Date
dependabot[bot]
fd790c1b94
Merge cbb7397827 into 998b000089 2024-12-11 08:21:08 +00:00
998b000089
Alter icon fill 2024-10-03 14:23:01 +02:00
873794eee5
Display AXUM_HOST in logs when starting server 2024-10-03 12:59:06 +02:00
a266d3e6f0
Fix IntelliJ project structure 2024-10-03 12:41:52 +02:00
492acaae12
Remove self-hostable from manifest 2024-09-23 06:45:35 +02:00
083267782d
Tweak tasks sorting 2024-09-23 06:42:07 +02:00
63b59bf0eb
Sort in progress tasks before unfinished ones 2024-09-23 06:37:26 +02:00
0f5fdb038b
Fix dates 2024-09-23 06:35:20 +02:00
74b3336ea1
Not sure why this is uncommitted but ok 2024-09-17 04:20:33 +02:00
07e2cb391d
i forgor again 2024-09-14 01:47:31 +02:00
1b6306b03c
Pass lang down 2024-09-14 00:19:13 +02:00
cd05b15c7b
Pass lang down 2024-09-13 23:50:42 +02:00
a39ec41a19
Pass lang down 2024-09-13 23:43:28 +02:00
8de6ddebb4
fix npmrc missing 2024-09-13 19:05:46 +02:00
1dd07abac1
Upgrade rust 2024-09-13 18:56:28 +02:00
37a2253975
Upgrade node 2024-09-13 18:54:46 +02:00
da867e6258
Bump version to 0.4.0 2024-09-13 18:53:45 +02:00
08614b0d69
autorefresh every 30 sec 2024-09-13 18:52:46 +02:00
6af857346a
I forgor 2024-09-13 18:28:32 +02:00
d96cb1564e
Update! 2024-09-13 17:29:34 +02:00
db35005190
Create task v3 2024-09-13 03:47:29 +02:00
48582f93df
Allow deadcode for logs 2024-09-13 03:31:40 +02:00
0d1235b742
Apply clippy lints 2024-09-13 03:31:20 +02:00
c4392f012b
Remove explicit pub(self) 2024-09-13 03:30:12 +02:00
92e10304e2
Use latest to denote the latest struct version 2024-09-13 03:29:50 +02:00
fe0f54e358
Update project metadata 2024-09-13 03:22:25 +02:00
e8144719a2
this is probably a very bad idea but whatever 2024-09-13 02:47:50 +02:00
e5a605562d
Patch up dependencies 2024-09-13 02:36:32 +02:00
86 changed files with 5137 additions and 890 deletions

View file

@ -1,2 +0,0 @@
[Desktop Entry]
Icon=/home/steffo/Workspaces/Steffo99/todocolors/todoblue/public/favicon.ico

View file

@ -1,10 +1,26 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<svg viewBox="0 0 2000 2000" xmlns="http://www.w3.org/2000/svg"> <svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
<svg height="100%" id="emblematic-background" viewBox="0 0 2000 2000" width="100%"> <svg height="100%" id="emblematic-background" version="1.1" viewBox="0 0 512 512" width="100%" xmlns="http://www.w3.org/2000/svg">
<rect fill="#0d193b" height="2000" width="2000"/> <defs>
<linearGradient gradientUnits="userSpaceOnUse" id="background" x2="512" y1="512">
<stop offset="0" stop-color="#051436"/>
<stop offset=".75" stop-color="#001553"/>
<stop offset="1" stop-color="#010a4e"/>
</linearGradient>
</defs>
<rect fill="url(#background)" height="512" width="512"/>
</svg> </svg>
<svg height="63%" id="emblematic-icon" preserveAspectRatio="xMidYMid meet" transform="translate(370.0, 370.0)" viewBox="0 0 512 512" width="63%" xmlns="http://www.w3.org/2000/svg"> <svg height="63%" id="emblematic-icon" preserveAspectRatio="xMidYMid meet" viewBox="0 0 512 512" width="63%" x="94.72" xmlns="http://www.w3.org/2000/svg" y="94.72">
<!--! Font Awesome Free 6.1.1 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc. --> <defs>
<path d="M0 256C0 114.6 114.6 0 256 0C397.4 0 512 114.6 512 256C512 397.4 397.4 512 256 512C114.6 512 0 397.4 0 256zM371.8 211.8C382.7 200.9 382.7 183.1 371.8 172.2C360.9 161.3 343.1 161.3 332.2 172.2L224 280.4L179.8 236.2C168.9 225.3 151.1 225.3 140.2 236.2C129.3 247.1 129.3 264.9 140.2 275.8L204.2 339.8C215.1 350.7 232.9 350.7 243.8 339.8L371.8 211.8z" fill="#9FC0FF"/> <filter color-interpolation-filters="sRGB" id="emblematic-filter">
<feFlood flood-color="rgb(1,8,40)" in="SourceGraphic" result="flood"/>
<feGaussianBlur in="SourceGraphic" result="blur" stdDeviation="10"/>
<feOffset dx="-4" dy="8" in="blur" result="offset"/>
<feComposite in="flood" in2="offset" operator="in" result="comp1"/>
<feComposite in="SourceGraphic" in2="comp1" result="comp2"/>
</filter>
</defs>
<!--! Font Awesome Pro 6.5.2 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license (Commercial License) Copyright 2024 Fonticons, Inc. -->
<path d="M256 512A256 256 0 1 0 256 0a256 256 0 1 0 0 512zM369 209L241 337c-9.4 9.4-24.6 9.4-33.9 0l-64-64c-9.4-9.4-9.4-24.6 0-33.9s24.6-9.4 33.9 0l47 47L335 175c9.4-9.4 24.6-9.4 33.9 0s9.4 24.6 0 33.9z" fill="#85c4ff" filter="url(#emblematic-filter)"/>
</svg> </svg>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,14 +1,8 @@
<component name="ProjectRunConfigurationManager"> <component name="ProjectRunConfigurationManager">
<configuration default="false" name="Run server" type="CargoCommandRunConfiguration" factoryName="Cargo Command"> <configuration default="false" name="Run server" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
<option name="buildProfileId" value="dev" />
<option name="command" value="run" /> <option name="command" value="run" />
<option name="workingDirectory" value="file://$PROJECT_DIR$/todored" /> <option name="workingDirectory" value="file://$PROJECT_DIR$/todored" />
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<envs> <envs>
<env name="AXUM_HOST" value="0.0.0.0:8080" /> <env name="AXUM_HOST" value="0.0.0.0:8080" />
<env name="REDIS_CONN" value="redis://127.0.0.1:6379/" /> <env name="REDIS_CONN" value="redis://127.0.0.1:6379/" />
@ -16,6 +10,13 @@
<env name="TODORED_RATE_LIMIT_CONNECTIONS_PER_MINUTE" value="0" /> <env name="TODORED_RATE_LIMIT_CONNECTIONS_PER_MINUTE" value="0" />
<env name="TODORED_RATE_LIMIT_MESSAGES_PER_MINUTE" value="0" /> <env name="TODORED_RATE_LIMIT_MESSAGES_PER_MINUTE" value="0" />
</envs> </envs>
<option name="emulateTerminal" value="true" />
<option name="channel" value="DEFAULT" />
<option name="requiredFeatures" value="true" />
<option name="allFeatures" value="false" />
<option name="withSudo" value="false" />
<option name="buildTarget" value="REMOTE" />
<option name="backtrace" value="SHORT" />
<option name="isRedirectInput" value="false" /> <option name="isRedirectInput" value="false" />
<option name="redirectInputPath" value="" /> <option name="redirectInputPath" value="" />
<method v="2"> <method v="2">

View file

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

BIN
.media/logo-512x512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 429 KiB

View file

Before

Width:  |  Height:  |  Size: 677 KiB

After

Width:  |  Height:  |  Size: 677 KiB

View file

@ -1,8 +1,12 @@
# ![](media/icon.png) Todocolors <div align="center">
A self-hostable multiplayer todo app with Redis, Rust, WebSockets and Next.js. # ![](.media/icon.png) Todocolors
> Warning: A self-hostable multiplayer todo app
</div>
> [!Warning]
> >
> This project is currently a prototype. > This project is currently a prototype.
> >
@ -10,11 +14,15 @@ A self-hostable multiplayer todo app with Redis, Rust, WebSockets and Next.js.
> >
> The code is a bit better now, but still may get rewritten from scratch for the next iteration of the project! > The code is a bit better now, but still may get rewritten from scratch for the next iteration of the project!
> >
> Use and contribute at your own risk. > Use and contribute at your own risk.ù
## Links
[![Website](https://img.shields.io/website?url=https%3A%2F%2Ftodo.steffo.eu%2F)](https://todo.steffo.eu/)
## Screenshots ## 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') ![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')
## Architecture ## Architecture

2
todoblue/.gitignore vendored
View file

@ -1,5 +1,7 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.npmrc
# dependencies # dependencies
/node_modules /node_modules
/.pnp /.pnp

View file

@ -1,9 +1,9 @@
FROM node:20 AS base FROM node AS base
FROM base AS builder FROM base AS builder
WORKDIR /usr/src/todoblue WORKDIR /usr/src/todoblue
COPY ./package.json ./yarn.lock ./ COPY ./package.json ./yarn.lock .npmrc ./
RUN yarn install --frozen-lockfile RUN yarn install --frozen-lockfile
COPY ./ ./ COPY ./ ./

View file

@ -1,36 +1,36 @@
{ {
"name": "todoblue", "name": "todoblue",
"version": "0.3.0", "version": "0.4.0",
"license": "EUPL-1.2", "license": "EUPL-1.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev", "dev": "next dev",
"build": "next build", "build": "next build",
"start": "next start", "start": "next start",
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@fortawesome/fontawesome-svg-core": "^6.4.0", "@awesome.me/kit-dfe340c874": "^1.0.10",
"@fortawesome/free-regular-svg-icons": "^6.4.0", "@fortawesome/fontawesome-svg-core": "*",
"@fortawesome/free-solid-svg-icons": "^6.4.0", "@fortawesome/react-fontawesome": "*",
"@fortawesome/react-fontawesome": "^0.2.0", "@steffo/bluelib": "*",
"@steffo/bluelib": "^9.0.1", "any-date-parser": "^1.5.4",
"@types/node": "20.4.5", "classnames": "*",
"@types/react": "18.2.17", "client-only": "*",
"@types/react-dom": "18.2.7", "i18next": "*",
"classnames": "^2.3.2", "i18next-resources-to-backend": "*",
"client-only": "^0.0.1", "negotiator": "*",
"i18next": "^23.4.2", "next": "*",
"i18next-resources-to-backend": "^1.1.4", "react": "*",
"negotiator": "^0.6.3", "react-dom": "*",
"next": "13.5.0", "server-only": "*",
"react": "18.2.0", "use-local-storage": "*"
"react-dom": "18.2.0", },
"server-only": "^0.0.1", "devDependencies": {
"typescript": "5.1.6", "@types/negotiator": "*",
"use-local-storage": "^3.0.0" "@types/node": "*",
}, "@types/react": "*",
"devDependencies": { "@types/react-dom": "*",
"@types/negotiator": "^0.6.1" "typescript": "*"
} }
} }

View file

@ -6,7 +6,7 @@
"display": "standalone", "display": "standalone",
"theme_color": "#0d193b", "theme_color": "#0d193b",
"background_color": "#0c193b", "background_color": "#0c193b",
"description": "Self-hostable multiplayer todo app", "description": "Multiplayer todo app",
"categories": ["productivity"], "categories": ["productivity"],
"icons": [ "icons": [
{ {

View file

@ -30,27 +30,6 @@
"taskStatusInProgress": "In progress", "taskStatusInProgress": "In progress",
"taskStatusComplete": "Complete", "taskStatusComplete": "Complete",
"taskStatusJournaled": "Journaled", "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", "taskButtonJournal": "Move this task to the Journal",
"taskButtonUnjournal": "Remove this task from the Journal", "taskButtonUnjournal": "Remove this task from the Journal",
"taskButtonDelete": "Delete this task", "taskButtonDelete": "Delete this task",

View file

@ -30,27 +30,6 @@
"taskStatusInProgress": "In corso", "taskStatusInProgress": "In corso",
"taskStatusComplete": "Completato", "taskStatusComplete": "Completato",
"taskStatusJournaled": "Salvato nel Diario", "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", "taskButtonJournal": "Sposta questa attività nel Diario",
"taskButtonUnjournal": "Rimuovi questa attività dal Diario", "taskButtonUnjournal": "Rimuovi questa attività dal Diario",
"taskButtonDelete": "Elimina questa attività", "taskButtonDelete": "Elimina questa attività",

View file

@ -51,7 +51,7 @@ export function StarredProvider({children}: {children: ReactNode}) {
}, []) }, [])
const isStarred = useCallback((value: string) => { const isStarred = useCallback((value: string) => {
return starred.indexOf(value) >= 0 return starred.includes(value)
}, [starred]) }, [starred])
return ( return (

View file

@ -1,7 +1,7 @@
"use client"; "use client";
import {useClientTranslation} from "@/app/(i18n)/client" import {useClientTranslation} from "@/app/(i18n)/client"
import {faLock} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import classNames from "classnames" import classNames from "classnames"
import {useRouter} from "next/navigation" import {useRouter} from "next/navigation"
@ -71,7 +71,7 @@ export function CreatePrivateBoardPanel({lang}: {lang: string}) {
onSubmit={createBoardValidated} onSubmit={createBoardValidated}
> >
<h3> <h3>
<FontAwesomeIcon icon={faLock} size={"1x"}/> <FontAwesomeIcon icon={fas.faLock} size={"1x"}/>
{" "} {" "}
{t("createPrivateBoardTitle")} {t("createPrivateBoardTitle")}
</h3> </h3>

View file

@ -2,7 +2,7 @@
import {useClientTranslation} from "@/app/(i18n)/client" import {useClientTranslation} from "@/app/(i18n)/client"
import {useLowerKebabifier} from "@/app/(utils)/(kebab)" import {useLowerKebabifier} from "@/app/(utils)/(kebab)"
import {faGlobe} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {useRouter} from "next/navigation" import {useRouter} from "next/navigation"
@ -32,7 +32,7 @@ export function CreatePublicBoardPanel({lang}: {lang: string}) {
onSubmit={createBoardValidated} onSubmit={createBoardValidated}
> >
<h3> <h3>
<FontAwesomeIcon icon={faGlobe}/> <FontAwesomeIcon icon={fas.faGlobe}/>
{" "} {" "}
{t("createPublicBoardTitle")} {t("createPublicBoardTitle")}
</h3> </h3>

View file

@ -2,7 +2,7 @@
import {useClientTranslation} from "@/app/(i18n)/client" import {useClientTranslation} from "@/app/(i18n)/client"
import {useLowerKebabifier} from "@/app/(utils)/(kebab)" import {useLowerKebabifier} from "@/app/(utils)/(kebab)"
import {faKey} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {useRouter} from "next/navigation" import {useRouter} from "next/navigation"
@ -32,7 +32,7 @@ export function KnownBoardsPanel({lang}: {lang: string}) {
onSubmit={moveToBoardValidated} onSubmit={moveToBoardValidated}
> >
<h3> <h3>
<FontAwesomeIcon icon={faKey}/> <FontAwesomeIcon icon={fas.faKey}/>
{" "} {" "}
{t("existingKnownBoardsTitle")} {t("existingKnownBoardsTitle")}
</h3> </h3>

View file

@ -2,7 +2,7 @@
import {useClientTranslation} from "@/app/(i18n)/client" import {useClientTranslation} from "@/app/(i18n)/client"
import {useStarredConsumer} from "@/app/[lang]/(layout)/(contextStarred)" import {useStarredConsumer} from "@/app/[lang]/(layout)/(contextStarred)"
import {faStar} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import Link from "next/link" import Link from "next/link"
@ -66,7 +66,7 @@ export function StarredBoardsPanel({lang}: {lang: string}) {
"box": true, "box": true,
})}> })}>
<h3> <h3>
<FontAwesomeIcon icon={faStar}/> <FontAwesomeIcon icon={fas.faStar}/>
{" "} {" "}
{t("existingStarredBoardsTitle")} {t("existingStarredBoardsTitle")}
</h3> </h3>

View file

@ -1,13 +1,11 @@
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)/TaskIcon"
import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)/TaskImportance" import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)/TaskImportance"
import {TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)/TaskPriority"
export type Task = { export type Task = {
text: string, text: string,
icon: TaskIcon, icon: string,
importance: TaskImportance, importance: TaskImportance,
priority: TaskPriority, deadline: number | null,
created_on: number | null, created_on: number | null,
started_on: number | null, started_on: number | null,
completed_on: number | null, completed_on: number | null,

File diff suppressed because it is too large Load diff

View file

@ -1,10 +0,0 @@
/**
* **Enum** of priority levels a {@link Task} may have.
*/
export enum TaskPriority {
Highest = "Highest",
High = "High",
Normal = "Normal",
Low = "Low",
Lowest = "Lowest",
}

View file

@ -1,7 +1,4 @@
export {TaskIcon} from "./TaskIcon"
export {TaskImportance} from "./TaskImportance" export {TaskImportance} from "./TaskImportance"
export {TaskPriority} from "./TaskPriority"
export {TASK_ICON_TO_FONTAWESOME_SOLID, TASK_ICON_TO_FONTAWESOME_REGULAR} from "./taskIconMappings"
export type {Task} from "./Task" export type {Task} from "./Task"
export type {TaskId} from "./TaskId" export type {TaskId} from "./TaskId"

View file

@ -1,52 +0,0 @@
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, IconDefinition} from "@fortawesome/free-solid-svg-icons"
import {TaskIcon} from "./TaskIcon"
export const TASK_ICON_TO_FONTAWESOME_SOLID: {[T in TaskIcon]: IconDefinition} = {
[TaskIcon.User]: faUserSolid,
[TaskIcon.Image]: faImageSolid,
[TaskIcon.Envelope]: faEnvelopeSolid,
[TaskIcon.Star]: faStarSolid,
[TaskIcon.Heart]: faHeartSolid,
[TaskIcon.Comment]: faCommentSolid,
[TaskIcon.FaceSmile]: faFaceSmileSolid,
[TaskIcon.File]: faFileSolid,
[TaskIcon.Bell]: faBellSolid,
[TaskIcon.Bookmark]: faBookmarkSolid,
[TaskIcon.Eye]: faEyeSolid,
[TaskIcon.Hand]: faHandSolid,
[TaskIcon.PaperPlane]: faPaperPlaneSolid,
[TaskIcon.Handshake]: faHandshakeSolid,
[TaskIcon.Sun]: faSunSolid,
[TaskIcon.Clock]: faClockSolid,
[TaskIcon.Circle]: faCircleSolid,
[TaskIcon.Square]: faSquareSolid,
[TaskIcon.Building]: faBuildingSolid,
[TaskIcon.Flag]: faFlagSolid,
[TaskIcon.Moon]: faMoonSolid,
}
export const TASK_ICON_TO_FONTAWESOME_REGULAR: {[T in TaskIcon]: IconDefinition} = {
[TaskIcon.User]: faUserRegular,
[TaskIcon.Image]: faImageRegular,
[TaskIcon.Envelope]: faEnvelopeRegular,
[TaskIcon.Star]: faStarRegular,
[TaskIcon.Heart]: faHeartRegular,
[TaskIcon.Comment]: faCommentRegular,
[TaskIcon.FaceSmile]: faFaceSmileRegular,
[TaskIcon.File]: faFileRegular,
[TaskIcon.Bell]: faBellRegular,
[TaskIcon.Bookmark]: faBookmarkRegular,
[TaskIcon.Eye]: faEyeRegular,
[TaskIcon.Hand]: faHandRegular,
[TaskIcon.PaperPlane]: faPaperPlaneRegular,
[TaskIcon.Handshake]: faHandshakeRegular,
[TaskIcon.Sun]: faSunRegular,
[TaskIcon.Clock]: faClockRegular,
[TaskIcon.Circle]: faCircleRegular,
[TaskIcon.Square]: faSquareRegular,
[TaskIcon.Building]: faBuildingRegular,
[TaskIcon.Flag]: faFlagRegular,
[TaskIcon.Moon]: faMoonRegular,
}

View file

@ -1,8 +1,8 @@
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)" import {ICONS} from "@/app/[lang]/board/[board]/(api)/(task)/TaskIcon"
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)" import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
import {TaskEditorIcon} from "@/app/[lang]/board/[board]/(page)/(edit)/(task)/TaskEditorIcon" import {TaskEditorIcon} from "@/app/[lang]/board/[board]/(page)/(edit)/(task)/TaskEditorIcon"
import {TaskEditorInput} from "@/app/[lang]/board/[board]/(page)/(edit)/(task)/TaskEditorInput" import {TaskEditorInput} from "@/app/[lang]/board/[board]/(page)/(edit)/(task)/TaskEditorInput"
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/taskToString" import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/convertTTS"
import {TaskContainer} from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer" import {TaskContainer} from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer"
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus" import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
import {useTaskEditor} from "@/app/[lang]/board/[board]/(page)/useTaskEditor" import {useTaskEditor} from "@/app/[lang]/board/[board]/(page)/useTaskEditor"
@ -12,57 +12,34 @@ import {SyntheticEvent, useCallback} from "react"
import style from "./TaskEditor.module.css" import style from "./TaskEditor.module.css"
const ICON_SEQUENCE = [ export function TaskEditor({lang, t, className, editorHook: {input, setInput, task, setTask}}: { lang: string, t: TFunction, className?: string, editorHook: ReturnType<typeof useTaskEditor> }) {
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 {isReady, sendRequest} = useBoardConsumer()
const nextIcon = useCallback((e: SyntheticEvent<HTMLButtonElement>) => { const nextIcon = useCallback((e: SyntheticEvent<HTMLButtonElement>) => {
if("key" in e && typeof e["key"] === "string") { if("key" in e && typeof e["key"] === "string") {
if(![" "].includes(e.key)) { if(![" "].includes(e.key)) {
return; return
} }
} }
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
let index = ICON_SEQUENCE.indexOf(task.icon) + 1; setTask({...task, icon: ICONS[Math.floor(Math.random() * ICONS.length)]})
if(index === ICON_SEQUENCE.length) index = 0;
setTask({...task, icon: ICON_SEQUENCE[index]})
}, [task]) }, [task])
const submitTask = useCallback((e: SyntheticEvent<HTMLInputElement>) => { const submitTask = useCallback((e: SyntheticEvent<HTMLInputElement>) => {
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
if(task.text === "") return if(task.text === "") {
return
}
sendRequest({"Task": [null, task]}) sendRequest({"Task": [null, task]})
setInput(taskToString({...task, text: ""})) setInput(taskToString({...task, text: ""}, lang))
}, [sendRequest, task, setInput]) }, [sendRequest, task, setInput])
if(!isReady) return null if(!isReady) {
return null
}
return ( return (
<div <div
className={cn(className, style.taskEditorContainer)} className={cn(className, style.taskEditorContainer)}
@ -70,7 +47,7 @@ export function TaskEditor({t, className, editorHook: {input, setInput, task, se
<TaskContainer <TaskContainer
role={"form"} role={"form"}
importance={task.importance} importance={task.importance}
priority={task.priority} deadline={task.deadline}
status={TaskSimplifiedStatus.NonExistent} status={TaskSimplifiedStatus.NonExistent}
onSubmit={submitTask} onSubmit={submitTask}
> >

View file

@ -1,38 +1,13 @@
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)"
import {TaskIconComponent} from "@/app/[lang]/board/[board]/(page)/(task)/TaskIconComponent" import {TaskIconComponent} from "@/app/[lang]/board/[board]/(page)/(task)/TaskIconComponent"
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus" import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
import {TFunction} from "i18next" import {TFunction} from "i18next"
import {SyntheticEvent} from "react" import {SyntheticEvent} from "react"
const ICON_TO_KEY = { export function TaskEditorIcon({t, icon, nextIcon}: {t: TFunction, icon: string, nextIcon: (e: SyntheticEvent<HTMLButtonElement>) => void}) {
[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 ( return (
<TaskIconComponent <TaskIconComponent
title={t(ICON_TO_KEY[icon])} title={icon}
icon={icon} icon={icon}
status={TaskSimplifiedStatus.NonExistent} status={TaskSimplifiedStatus.NonExistent}
onInteract={nextIcon} onInteract={nextIcon}

View file

@ -6,14 +6,14 @@ import {TFunction} from "i18next"
import style from "./BoardEditor.module.css" import style from "./BoardEditor.module.css"
export function BoardEditor({className, t, editorHook}: {className?: string, t: TFunction, editorHook: ReturnType<typeof useTaskEditor>}) { export function BoardEditor({className, lang, t, editorHook}: {className?: string, lang: string, t: TFunction, editorHook: ReturnType<typeof useTaskEditor>}) {
const {boardState: {locked}} = useBoardConsumer() const {boardState: {locked}} = useBoardConsumer()
if(locked) return null; if(locked) return null;
return ( return (
<section className={cn(style.boardEditor, className)}> <section className={cn(style.boardEditor, className)}>
<TaskEditor t={t} editorHook={editorHook}/> <TaskEditor lang={lang} t={t} editorHook={editorHook}/>
</section> </section>
) )
} }

View file

@ -0,0 +1,65 @@
import {Task, TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
import {DEADLINE_GLYPH_RE} from "@/app/[lang]/board/[board]/(page)/(edit)/taskDeadline"
import {ICON_DEFAULT, ICON_GLYPH_RE} from "@/app/[lang]/board/[board]/(page)/(edit)/taskIcon"
import {IMPORTANCE_GLYPH_RE} from "@/app/[lang]/board/[board]/(page)/(edit)/taskImportance"
import {default as dateParser} from "any-date-parser"
type Attempt = {
year?: number,
month?: number,
day?: number,
hour?: number,
minute?: number,
second?: number,
millisecond?: number,
}
const VALUE_TO_TASK_IMPORTANCE = {
"1": TaskImportance.Highest,
"2": TaskImportance.High,
"3": TaskImportance.Normal,
"4": TaskImportance.Low,
"5": TaskImportance.Lowest,
}
export function convertSTT(text: string, lang: string): Task {
const importanceMatch = IMPORTANCE_GLYPH_RE.exec(text)
const iconMatch = ICON_GLYPH_RE.exec(text)
const deadlineMatch = DEADLINE_GLYPH_RE.exec(text)
const importance: TaskImportance = VALUE_TO_TASK_IMPORTANCE[importanceMatch?.[1]?.trim() as "1"|"2"|"3"|"4"|"5" ?? "3"]
const icon: string = iconMatch?.[1]?.trim() ?? ICON_DEFAULT
const now = new Date()
const deadlineGroup: string | undefined = deadlineMatch?.[1]?.trim()
const deadlineAttempt: Attempt | undefined = deadlineGroup === undefined ? undefined : dateParser.attempt(deadlineGroup, lang) ?? undefined
const deadlineDate: Date | undefined = deadlineAttempt === undefined ? undefined : new Date(
deadlineAttempt.year ?? now.getFullYear(),
(deadlineAttempt.month ?? (now.getMonth() + 1)) - 1,
deadlineAttempt.day ?? now.getDate(),
deadlineAttempt.hour ?? now.getHours(),
deadlineAttempt.minute ?? now.getMinutes(),
deadlineAttempt.second ?? now.getSeconds(),
deadlineAttempt.millisecond ?? now.getMilliseconds(),
)
const deadline: number | null = (deadlineDate?.getTime?.()) ?? null
console.debug("[convertSTT]", "\ngroup:", deadlineGroup, "\ndate:", deadlineDate, "\ntimestamp:", deadline)
// TODO: Splice so the regexes aren't executed twice
text = text.replace(IMPORTANCE_GLYPH_RE, "")
text = text.replace(ICON_GLYPH_RE, "")
text = text.replace(DEADLINE_GLYPH_RE, "")
text = text.trim()
return {
text,
importance,
icon,
deadline,
created_on: + new Date(),
started_on: null,
completed_on: null,
journaled_on: null,
}
}

View file

@ -0,0 +1,49 @@
import {Task, TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
import {DEADLINE_DEFAULT, DEADLINE_GLYPH_END, DEADLINE_GLYPH_START} from "@/app/[lang]/board/[board]/(page)/(edit)/taskDeadline"
import {ICON_DEFAULT, ICON_GLYPH} from "@/app/[lang]/board/[board]/(page)/(edit)/taskIcon"
import {IMPORTANCE_DEFAULT, IMPORTANCE_GLYPH} from "@/app/[lang]/board/[board]/(page)/(edit)/taskImportance"
const TASK_IMPORTANCE_TO_VALUE = {
[TaskImportance.Highest]: "1",
[TaskImportance.High]: "2",
[TaskImportance.Normal]: "3",
[TaskImportance.Low]: "4",
[TaskImportance.Lowest]: "5",
}
export function taskToString(t: Task, lang: string): string {
const intlDate = Intl.DateTimeFormat(lang, {
year: "numeric",
month: "short",
day: "2-digit",
hour: "2-digit",
minute: "2-digit",
second: "2-digit",
})
let s = ""
if(t.deadline !== DEADLINE_DEFAULT) {
s += DEADLINE_GLYPH_START
s += intlDate.format(new Date(t.deadline))
s += DEADLINE_GLYPH_END
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]
s += " "
}
s += t.text
return s
}

View file

@ -1,8 +0,0 @@
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)"
export const ICON_GLYPH = "#"
export const ICON_GLYPH_RE = /#([A-Za-z]+)\s?/
export const ICON_DEFAULT = TaskIcon.Circle

View file

@ -1,8 +0,0 @@
import {TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
export const PRIORITY_GLYPH = "/"
export const PRIORITY_GLYPH_RE = /\/([1-5])\s?/
export const PRIORITY_DEFAULT = TaskPriority.Normal

View file

@ -1,75 +0,0 @@
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,
"3": TaskImportance.Normal,
"4": TaskImportance.Low,
"5": TaskImportance.Lowest,
}
const VALUE_TO_TASK_PRIORITY = {
"1": TaskPriority.Highest,
"2": TaskPriority.High,
"3": TaskPriority.Normal,
"4": TaskPriority.Low,
"5": TaskPriority.Lowest,
}
const VALUE_TO_TASK_ICON = {
"bookmark": TaskIcon.Bookmark,
"circle": TaskIcon.Circle,
"square": TaskIcon.Square,
"heart": TaskIcon.Heart,
"star": TaskIcon.Star,
"sun": TaskIcon.Sun,
"moon": TaskIcon.Moon,
"eye": TaskIcon.Eye,
"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,
"clock": TaskIcon.Clock,
"image": TaskIcon.Image,
}
export function stringToTask(text: string): Task {
const priorityMatch = PRIORITY_GLYPH_RE.exec(text)
const importanceMatch = IMPORTANCE_GLYPH_RE.exec(text)
const iconMatch = ICON_GLYPH_RE.exec(text)
const priority: TaskPriority = VALUE_TO_TASK_PRIORITY[priorityMatch?.[1]?.trim() as "1"|"2"|"3"|"4"|"5" ?? "3"]
const importance: TaskImportance = VALUE_TO_TASK_IMPORTANCE[importanceMatch?.[1]?.trim() as "1"|"2"|"3"|"4"|"5" ?? "3"]
// @ts-ignore
const icon: TaskIcon = VALUE_TO_TASK_ICON[iconMatch?.[1]?.toLowerCase()?.trim()] ?? TaskIcon.Circle
// TODO: Splice so the regexes aren't executed twice
text = text.replace(PRIORITY_GLYPH_RE, "")
text = text.replace(IMPORTANCE_GLYPH_RE, "")
text = text.replace(ICON_GLYPH_RE, "")
text = text.trim()
return {
text,
priority,
importance,
icon,
created_on: + new Date(),
started_on: null,
completed_on: null,
journaled_on: null,
}
}

View file

@ -0,0 +1,6 @@
export const DEADLINE_GLYPH_START = "["
export const DEADLINE_GLYPH_END = "]"
export const DEADLINE_GLYPH_RE = /\[(.+?)]\s?/
export const DEADLINE_DEFAULT = null

View file

@ -0,0 +1,5 @@
export const ICON_GLYPH = "#"
export const ICON_GLYPH_RE = /#([A-Za-z0-9-]+)\s?/
export const ICON_DEFAULT = "Circle"

View file

@ -1,47 +0,0 @@
import {Task, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
import {ICON_DEFAULT, ICON_GLYPH} from "@/app/[lang]/board/[board]/(page)/(edit)/icon"
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",
[TaskImportance.Normal]: "3",
[TaskImportance.Low]: "4",
[TaskImportance.Lowest]: "5",
}
const TASK_PRIORITY_TO_VALUE = {
[TaskPriority.Highest]: "1",
[TaskPriority.High]: "2",
[TaskPriority.Normal]: "3",
[TaskPriority.Low]: "4",
[TaskPriority.Lowest]: "5",
}
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]
s += " "
}
if(t.priority !== PRIORITY_DEFAULT) {
s += PRIORITY_GLYPH
s += TASK_PRIORITY_TO_VALUE[t.priority]
s += " "
}
s += t.text
return s
}

View file

@ -1,6 +1,6 @@
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)" import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css" import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
import {faUsers} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {TFunction} from "i18next" import {TFunction} from "i18next"
@ -16,7 +16,7 @@ export function ConnectedClientsButton({t}: {t: TFunction}) {
title={t("privacyButtonTitle")} title={t("privacyButtonTitle")}
className={cn(style.block, style.singleBlock)} className={cn(style.block, style.singleBlock)}
> >
<FontAwesomeIcon icon={faUsers} size={"2xs"}/> <FontAwesomeIcon icon={fas.faUsers} size={"2xs"}/>
&nbsp; &nbsp;
{clients.length} {clients.length}
</div> </div>

View file

@ -1,6 +1,6 @@
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)" import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css" import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
import {faObjectGroup} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {TFunction} from "i18next" import {TFunction} from "i18next"
@ -17,7 +17,7 @@ export function CycleGroupingButton({t, next}: {t: TFunction, next: () => void})
onClick={next} onClick={next}
className={cn(style.block, style.singleBlock)} className={cn(style.block, style.singleBlock)}
> >
<FontAwesomeIcon icon={faObjectGroup}/> <FontAwesomeIcon icon={fas.faObjectGroup}/>
</button> </button>
) )
} }

View file

@ -1,6 +1,6 @@
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)" import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css" import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
import {faArrowDownWideShort} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {TFunction} from "i18next" import {TFunction} from "i18next"
@ -17,7 +17,7 @@ export function CycleSortingButton({t, next}: {t: TFunction, next: () => void})
onClick={next} onClick={next}
className={cn(style.block, style.singleBlock)} className={cn(style.block, style.singleBlock)}
> >
<FontAwesomeIcon icon={faArrowDownWideShort}/> <FontAwesomeIcon icon={fas.faArrowDownWideShort}/>
</button> </button>
) )
} }

View file

@ -1,5 +1,5 @@
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css" import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
import {faHouse} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {TFunction} from "i18next" import {TFunction} from "i18next"
@ -17,7 +17,7 @@ export function NavigateHomeButton({t}: {t: TFunction}) {
onClick={goHome} onClick={goHome}
className={cn(style.block, style.singleBlock)} className={cn(style.block, style.singleBlock)}
> >
<FontAwesomeIcon icon={faHouse}/> <FontAwesomeIcon icon={fas.faHouse}/>
</button> </button>
) )
} }

View file

@ -1,7 +1,7 @@
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)" import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css" import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
import {useBoardMetadataEditor} from "@/app/[lang]/board/[board]/(page)/useBoardMetadataEditor" import {useBoardMetadataEditor} from "@/app/[lang]/board/[board]/(page)/useBoardMetadataEditor"
import {faFloppyDisk, faPencil} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {TFunction} from "i18next" import {TFunction} from "i18next"
@ -19,7 +19,7 @@ export function ToggleEditingButton({t, metadataHook}: {t: TFunction, metadataHo
onClick={metadataHook.toggleEditingMetadata} onClick={metadataHook.toggleEditingMetadata}
className={cn(style.block, style.singleBlock)} className={cn(style.block, style.singleBlock)}
> >
<FontAwesomeIcon icon={metadataHook.isEditingMetadata ? faFloppyDisk : faPencil}/> <FontAwesomeIcon icon={fas[metadataHook.isEditingMetadata ? "faFloppyDisk" : "faPencil"]}/>
</button> </button>
) )
} }

View file

@ -1,7 +1,7 @@
import {LockBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)" import {LockBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)" import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css" import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
import {faLock, faLockOpen} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {TFunction} from "i18next" import {TFunction} from "i18next"
@ -25,7 +25,7 @@ export function ToggleLockedButton({t}: {t: TFunction}) {
onClick={toggleLock} onClick={toggleLock}
className={cn(style.block, style.singleBlock)} className={cn(style.block, style.singleBlock)}
> >
<FontAwesomeIcon icon={locked ? faLock : faLockOpen}/> <FontAwesomeIcon icon={fas[locked ? "faLock" : "faLockOpen"]}/>
</button> </button>
) )
} }

View file

@ -1,8 +1,9 @@
"use client";
import {useStarredConsumer} from "@/app/[lang]/(layout)/(contextStarred)" import {useStarredConsumer} from "@/app/[lang]/(layout)/(contextStarred)"
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)" import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css" import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
import {faStar as faStarRegular} from "@fortawesome/free-regular-svg-icons" import {far, fas} from "@awesome.me/kit-dfe340c874/icons"
import {faStar as faStarSolid} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {TFunction} from "i18next" import {TFunction} from "i18next"
@ -19,7 +20,7 @@ export function ToggleStarredButton({t}: {t: TFunction}) {
onClick={() => toggleStarred(boardName)} onClick={() => toggleStarred(boardName)}
className={cn(style.block, style.singleBlock)} className={cn(style.block, style.singleBlock)}
> >
<FontAwesomeIcon icon={thisIsStarred ? faStarSolid : faStarRegular}/> <FontAwesomeIcon icon={(thisIsStarred ? fas : far)["faStar"]}/>
</button> </button>
) )
} }

View file

@ -1,7 +1,7 @@
import {TrimBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)" import {TrimBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)" import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css" import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
import {faScissors} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {TFunction} from "i18next" import {TFunction} from "i18next"
@ -25,7 +25,7 @@ export function TrimButton({t}: {t: TFunction}) {
onClick={requestTrim} onClick={requestTrim}
className={cn(style.block, style.singleBlock)} className={cn(style.block, style.singleBlock)}
> >
<FontAwesomeIcon icon={faScissors}/> <FontAwesomeIcon icon={fas.faScissors}/>
</button> </button>
) )
} }

View file

@ -1,4 +1,4 @@
import {IconDefinition} from "@fortawesome/free-solid-svg-icons" import {IconDefinition} from "@fortawesome/fontawesome-svg-core"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {SyntheticEvent} from "react" import {SyntheticEvent} from "react"

View file

@ -3,6 +3,9 @@
align-items: center; align-items: center;
min-width: 240px; min-width: 240px;
min-height: 48px; min-height: 48px;
border-style: solid;
border-width: 0;
padding: 8px;
} }
.taskImportanceHighest { .taskImportanceHighest {
@ -45,29 +48,32 @@
font-weight: 200; font-weight: 200;
} }
.taskPriorityHighest { .taskDeadlineHour {
border: 4px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.45); border-left-width: 4px;
padding: 4px; padding-left: 4px;
} }
.taskPriorityHigh { .taskDeadlineDay {
border: 3px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.25); border-left-width: 3px;
padding: 5px; padding-left: 5px;
} }
.taskPriorityNormal { .taskDeadlineWeek {
border: 2px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.15); border-left-width: 2px;
padding: 6px; padding-left: 6px;
} }
.taskPriorityLow { .taskDeadlineMonth {
border: 1px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.15); border-left-width: 1px;
padding: 7px; padding-left: 7px;
} }
.taskPriorityLowest { .taskDeadlineFuture {
border: 0; border-color: hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.25);
padding: 8px; }
.taskDeadlinePast {
border-color: hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 1.00);
} }
@keyframes inProgress { @keyframes inProgress {

View file

@ -1,5 +1,6 @@
import {TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)" import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus" import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
import {useTimeDelta} from "@/app/[lang]/board/[board]/(page)/(task)/useTimeDelta"
import cn from "classnames" import cn from "classnames"
import {ComponentPropsWithoutRef} from "react" import {ComponentPropsWithoutRef} from "react"
import style from "./TaskContainer.module.css" import style from "./TaskContainer.module.css"
@ -9,12 +10,14 @@ export type TaskContainerProps = {
className?: string, className?: string,
role: "article" | "form", role: "article" | "form",
importance: TaskImportance, importance: TaskImportance,
priority: TaskPriority, deadline: number | null,
status: TaskSimplifiedStatus, status: TaskSimplifiedStatus,
} & ComponentPropsWithoutRef<"article"> } & ComponentPropsWithoutRef<"article">
export function TaskContainer({role, className, importance, priority, status, ...props}: TaskContainerProps) { export function TaskContainer({role, className, importance, deadline, status, ...props}: TaskContainerProps) {
const {delta, deltaAbs} = useTimeDelta(deadline ?? undefined, 30 * 1000)
const fullProps = { const fullProps = {
className: cn({ className: cn({
"panel": true, "panel": true,
@ -25,11 +28,13 @@ export function TaskContainer({role, className, importance, priority, status, ..
[style.taskImportanceNormal]: importance === TaskImportance.Normal, [style.taskImportanceNormal]: importance === TaskImportance.Normal,
[style.taskImportanceLow]: importance === TaskImportance.Low, [style.taskImportanceLow]: importance === TaskImportance.Low,
[style.taskImportanceLowest]: importance === TaskImportance.Lowest, [style.taskImportanceLowest]: importance === TaskImportance.Lowest,
[style.taskPriorityHighest]: priority === TaskPriority.Highest, [style.taskDeadlineNone]: deadline === null,
[style.taskPriorityHigh]: priority === TaskPriority.High, [style.taskDeadlineHour]: deltaAbs !== undefined && deltaAbs < 60 * 60 * 1000,
[style.taskPriorityNormal]: priority === TaskPriority.Normal, [style.taskDeadlineDay]: deltaAbs !== undefined && 60 * 60 * 1000 <= deltaAbs && deltaAbs < 24 * 60 * 60 * 1000,
[style.taskPriorityLow]: priority === TaskPriority.Low, [style.taskDeadlineWeek]: deltaAbs !== undefined && 24 * 60 * 60 * 1000 <= deltaAbs && deltaAbs < 7 * 24 * 60 * 60 * 1000,
[style.taskPriorityLowest]: priority === TaskPriority.Lowest, [style.taskDeadlineMonth]: deltaAbs !== undefined && deltaAbs >= 7 * 24 * 60 * 60 * 1000,
[style.taskDeadlineFuture]: delta !== undefined && delta >= 0,
[style.taskDeadlinePast]: delta !== undefined && delta < 0,
[style.taskStatusNonExistent]: status === TaskSimplifiedStatus.NonExistent, [style.taskStatusNonExistent]: status === TaskSimplifiedStatus.NonExistent,
[style.taskStatusUnfinished]: status === TaskSimplifiedStatus.Unfinished, [style.taskStatusUnfinished]: status === TaskSimplifiedStatus.Unfinished,
[style.taskStatusInProgress]: status === TaskSimplifiedStatus.InProgress, [style.taskStatusInProgress]: status === TaskSimplifiedStatus.InProgress,

View file

@ -2,3 +2,7 @@
border-radius: var(--b-border-radius); border-radius: var(--b-border-radius);
flex-grow: 1; flex-grow: 1;
} }
.taskDescriptionSource {
font-family: monospace;
}

View file

@ -4,14 +4,16 @@ import style from "./TaskDescription.module.css"
type TaskDescriptionProps = { type TaskDescriptionProps = {
className?: string, className?: string,
isSource?: boolean,
text: string, text: string,
} }
export function TaskDescription({className, text}: TaskDescriptionProps) { export function TaskDescription({className, isSource, text}: TaskDescriptionProps) {
return ( return (
<div className={cn({ <div className={cn({
"taskDescription": true, "taskDescription": true,
[style.taskDescription]: true, [style.taskDescription]: true,
[style.taskDescriptionSource]: isSource,
}, className)} tabIndex={0}> }, className)} tabIndex={0}>
{text} {text}
</div> </div>

View file

@ -1,5 +1,6 @@
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 {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
import {fal, far, fas} from "@awesome.me/kit-dfe340c874/icons"
import {IconPack} from "@fortawesome/fontawesome-svg-core"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import cn from "classnames" import cn from "classnames"
import {SyntheticEvent} from "react" import {SyntheticEvent} from "react"
@ -9,11 +10,18 @@ import style from "./TaskIconComponent.module.css"
type TaskIconProps = { type TaskIconProps = {
className?: string, className?: string,
title: string, title: string,
icon: TaskIcon, icon: string,
status: TaskSimplifiedStatus, status: TaskSimplifiedStatus,
onInteract?: (e: SyntheticEvent<HTMLButtonElement>) => void, onInteract?: (e: SyntheticEvent<HTMLButtonElement>) => void,
} }
const STATUS_TO_PREFIX: {[t in TaskSimplifiedStatus]: IconPack} = {
[TaskSimplifiedStatus.Unfinished]: fas,
[TaskSimplifiedStatus.InProgress]: fas,
[TaskSimplifiedStatus.Complete]: fal,
[TaskSimplifiedStatus.Journaled]: far,
[TaskSimplifiedStatus.NonExistent]: fas,
}
export function TaskIconComponent({className, title, icon, status, onInteract}: TaskIconProps) { export function TaskIconComponent({className, title, icon, status, onInteract}: TaskIconProps) {
const clickable = !!onInteract; const clickable = !!onInteract;
@ -23,6 +31,7 @@ export function TaskIconComponent({className, title, icon, status, onInteract}:
className={cn({ className={cn({
[style.taskIconComponent]: true, [style.taskIconComponent]: true,
[style.taskIconComponentClickable]: clickable, [style.taskIconComponentClickable]: clickable,
"fade": status === TaskSimplifiedStatus.Complete,
}, className)} }, className)}
type={"button"} type={"button"}
title={title} title={title}
@ -32,7 +41,7 @@ export function TaskIconComponent({className, title, icon, status, onInteract}:
> >
<FontAwesomeIcon <FontAwesomeIcon
size={"lg"} size={"lg"}
icon={[TaskSimplifiedStatus.Complete, TaskSimplifiedStatus.Journaled].includes(status) ? TASK_ICON_TO_FONTAWESOME_SOLID[icon] : TASK_ICON_TO_FONTAWESOME_REGULAR[icon]} icon={STATUS_TO_PREFIX[status][`fa${icon}`]}
beatFade={status === TaskSimplifiedStatus.InProgress} beatFade={status === TaskSimplifiedStatus.InProgress}
/> />
</button> </button>

View file

@ -0,0 +1,32 @@
import {useCallback, useEffect, useState} from "react"
export type UseTimeDelta = {
now?: number
delta?: number,
deltaAbs?: number,
refresh: () => void,
}
export function useTimeDelta(target?: number, refreshAfterMs?: number): UseTimeDelta {
const [now, setNow] = useState<number>(() => + new Date())
const refresh = useCallback(() => {
setNow(+new Date())
}, [])
const delta = target === undefined ? undefined : target - now
const deltaAbs = delta === undefined ? undefined : Math.abs(delta)
useEffect(() => {
if(refreshAfterMs) {
const timeout = setTimeout(refresh, refreshAfterMs)
return () => {
clearTimeout(timeout)
}
}
}, [refresh])
return {now, delta, deltaAbs, refresh}
}

View file

@ -1,8 +1,9 @@
import {ColumningMode} from "@/app/[lang]/board/[board]/(page)/(view)/(columning)/ColumningMode" import {ColumningMode} from "@/app/[lang]/board/[board]/(page)/(view)/(columning)/ColumningMode"
import {faTableColumns, faTableList} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {IconDefinition} from "@fortawesome/fontawesome-svg-core"
export const COLUMNING_MODE_TO_ICON = { export const COLUMNING_MODE_TO_ICON: {[m in ColumningMode]: IconDefinition} = {
[ColumningMode.SingleColumn]: faTableList, [ColumningMode.SingleColumn]: fas.faTableList,
[ColumningMode.MultiColumn]: faTableColumns, [ColumningMode.MultiColumn]: fas.faTableColumns,
} }

View file

@ -1,6 +1,5 @@
export enum GroupingMode { export enum GroupingMode {
ByImportance, ByImportance,
ByPriority,
ByStatus, ByStatus,
ByIcon, ByIcon,
Journal, Journal,

View file

@ -1,4 +1,5 @@
import {TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)" import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
import {ICONS} from "@/app/[lang]/board/[board]/(api)/(task)/TaskIcon"
import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode" import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode"
import {TaskGroup} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/TaskGroup" import {TaskGroup} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/TaskGroup"
@ -11,60 +12,25 @@ const TASK_IMPORTANCE_TO_VALUE = {
[TaskImportance.Lowest]: 5, [TaskImportance.Lowest]: 5,
} }
const TASK_PRIORITY_TO_VALUE = {
[TaskPriority.Highest]: 1,
[TaskPriority.High]: 2,
[TaskPriority.Normal]: 3,
[TaskPriority.Low]: 4,
[TaskPriority.Lowest]: 5,
}
const TASK_STATUS_TO_VALUE = { const TASK_STATUS_TO_VALUE = {
"Journaled": 3, "Journaled": 3,
"Complete": 2, "Complete": 2,
"InProgress": 1, "InProgress": 0,
"Unfinished": 0, "Unfinished": 1,
}
const TASK_ICON_TO_VALUE = {
[TaskIcon.Bookmark]: 1,
[TaskIcon.Circle]: 2,
[TaskIcon.Square]: 3,
[TaskIcon.Heart]: 4,
[TaskIcon.Star]: 5,
[TaskIcon.Sun]: 6,
[TaskIcon.Moon]: 7,
[TaskIcon.Eye]: 8,
[TaskIcon.Hand]: 9,
[TaskIcon.Handshake]: 10,
[TaskIcon.FaceSmile]: 11,
[TaskIcon.User]: 12,
[TaskIcon.Comment]: 13,
[TaskIcon.Envelope]: 14,
[TaskIcon.File]: 15,
[TaskIcon.PaperPlane]: 16,
[TaskIcon.Building]: 17,
[TaskIcon.Flag]: 18,
[TaskIcon.Bell]: 19,
[TaskIcon.Clock]: 20,
[TaskIcon.Image]: 21,
} }
export const GROUPING_MODE_TO_GROUP_SORTER_FUNCTION = { export const GROUPING_MODE_TO_GROUP_SORTER_FUNCTION = {
[GroupingMode.ByImportance]: function sortGroupsByImportance(a: TaskGroup<TaskImportance>, b: TaskGroup<TaskImportance>): number { [GroupingMode.ByImportance]: function sortGroupsByImportance(a: TaskGroup<TaskImportance>, b: TaskGroup<TaskImportance>): number {
return TASK_IMPORTANCE_TO_VALUE[a.k] - TASK_IMPORTANCE_TO_VALUE[b.k] return TASK_IMPORTANCE_TO_VALUE[a.k] - TASK_IMPORTANCE_TO_VALUE[b.k]
}, },
[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<string>, b: TaskGroup<string>): number { [GroupingMode.ByStatus]: function sortGroupsByStatus(a: TaskGroup<string>, b: TaskGroup<string>): number {
// @ts-ignore // @ts-ignore
return TASK_STATUS_TO_VALUE[a.k] - TASK_STATUS_TO_VALUE[b.k] return TASK_STATUS_TO_VALUE[a.k] - TASK_STATUS_TO_VALUE[b.k]
}, },
[GroupingMode.ByIcon]: function sortGroupsByIcon(a: TaskGroup<TaskIcon>, b: TaskGroup<TaskIcon>): number { [GroupingMode.ByIcon]: function sortGroupsByIcon(a: TaskGroup<string>, b: TaskGroup<string>): number {
return TASK_ICON_TO_VALUE[a.k] - TASK_ICON_TO_VALUE[b.k] return ICONS.indexOf(a.k as any) - ICONS.indexOf(b.k as any)
}, },
[GroupingMode.Journal]: function sortGroupsAlphabetically(a: TaskGroup<TaskIcon>, b: TaskGroup<TaskIcon>): number { [GroupingMode.Journal]: function sortGroupsAlphabetically(a: TaskGroup<string>, b: TaskGroup<string>): number {
return b.k.localeCompare(a.k) return b.k.localeCompare(a.k)
} }
} }

View file

@ -1,24 +1,19 @@
import {TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)" import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId" import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode" import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode"
export const GROUPING_MODE_TO_TASK_GROUPER_FUNCTION = { export const GROUPING_MODE_TO_TASK_GROUPER_FUNCTION = {
[GroupingMode.ByImportance]: function groupTasksByImportance(t: TaskWithId): TaskImportance | null { [GroupingMode.ByImportance]: function groupTasksByImportance(t: TaskWithId): TaskImportance | null {
if(t[1].journaled_on) return null if(t[1].journaled_on) return null
return t[1].importance return t[1].importance
}, },
[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): string | null { [GroupingMode.ByStatus]: function groupTasksByStatus(t: TaskWithId): string | null {
if(t[1].journaled_on) return null if(t[1].journaled_on) return null
else if(t[1].completed_on) return "Complete" else if(t[1].completed_on) return "Complete"
else if(t[1].started_on) return "InProgress" else if(t[1].started_on) return "InProgress"
else return "Unfinished" else return "Unfinished"
}, },
[GroupingMode.ByIcon]: function groupTasksByIcon(t: TaskWithId): TaskIcon | null { [GroupingMode.ByIcon]: function groupTasksByIcon(t: TaskWithId): string | null {
if(t[1].journaled_on) return null if(t[1].journaled_on) return null
return t[1].icon return t[1].icon
}, },

View file

@ -1,6 +1,7 @@
import {TASK_ICON_TO_FONTAWESOME_REGULAR, TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)" import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
import style from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer.module.css" import style from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer.module.css"
import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode" import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode"
import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {TFunction} from "i18next" import {TFunction} from "i18next"
import {ReactNode} from "react" import {ReactNode} from "react"
@ -48,41 +49,6 @@ export const GROUPING_MODE_TO_TITLE_COMPONENT: {[key in GroupingMode]: (t: Title
} }
}, },
[GroupingMode.ByPriority]: function TitleFromPriority({t, k}: { t: TFunction, k: TaskPriority }): ReactNode {
switch(k) {
case TaskPriority.Highest:
return (
<span className={style.taskPriorityHighestColumnHeader}>
{t("taskPriorityHighest")}
</span>
)
case TaskPriority.High:
return (
<span className={style.taskPriorityHighColumnHeader}>
{t("taskPriorityHigh")}
</span>
)
case TaskPriority.Normal:
return (
<span className={style.taskPriorityNormalColumnHeader}>
{t("taskPriorityNormal")}
</span>
)
case TaskPriority.Low:
return (
<span className={style.taskPriorityLowColumnHeader}>
{t("taskPriorityLow")}
</span>
)
case TaskPriority.Lowest:
return (
<span className={style.taskPriorityLowestColumnHeader}>
{t("taskPriorityLowest")}
</span>
)
}
},
[GroupingMode.ByStatus]: function TitleFromStatus({t, k}: { t: TFunction, k: string }): ReactNode { [GroupingMode.ByStatus]: function TitleFromStatus({t, k}: { t: TFunction, k: string }): ReactNode {
switch(k) { switch(k) {
case "Unfinished": case "Unfinished":
@ -106,12 +72,12 @@ export const GROUPING_MODE_TO_TITLE_COMPONENT: {[key in GroupingMode]: (t: Title
} }
}, },
[GroupingMode.ByIcon]: function TitleFromIcon({k}: { t: TFunction, k: TaskIcon }): ReactNode { [GroupingMode.ByIcon]: function TitleFromIcon({k}: { t: TFunction, k: string }): ReactNode {
return ( return (
<span> <span>
<FontAwesomeIcon icon={TASK_ICON_TO_FONTAWESOME_REGULAR[k]}/> <FontAwesomeIcon icon={fas[`fa${k}`]}/>
&nbsp; &nbsp;
{TaskIcon[k]} {k}
</span> </span>
) )
}, },

View file

@ -1,6 +1,6 @@
export enum SortingMode { export enum SortingMode {
ByPriority,
ByImportance, ByImportance,
ByDeadline,
ByIcon, ByIcon,
ByText, ByText,
ByStatus, ByStatus,

View file

@ -1,4 +1,5 @@
import {TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)" import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
import {ICONS} from "@/app/[lang]/board/[board]/(api)/(task)/TaskIcon"
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId" import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
import {SortingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(sorting)/SortingMode" import {SortingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(sorting)/SortingMode"
@ -11,38 +12,6 @@ const TASK_IMPORTANCE_TO_VALUE = {
[TaskImportance.Lowest]: 5, [TaskImportance.Lowest]: 5,
} }
const TASK_PRIORITY_TO_VALUE = {
[TaskPriority.Highest]: 1,
[TaskPriority.High]: 2,
[TaskPriority.Normal]: 3,
[TaskPriority.Low]: 4,
[TaskPriority.Lowest]: 5,
}
const TASK_ICON_TO_VALUE = {
[TaskIcon.Bookmark]: 1,
[TaskIcon.Circle]: 2,
[TaskIcon.Square]: 3,
[TaskIcon.Heart]: 4,
[TaskIcon.Star]: 5,
[TaskIcon.Sun]: 6,
[TaskIcon.Moon]: 7,
[TaskIcon.Eye]: 8,
[TaskIcon.Hand]: 9,
[TaskIcon.Handshake]: 10,
[TaskIcon.FaceSmile]: 11,
[TaskIcon.User]: 12,
[TaskIcon.Comment]: 13,
[TaskIcon.Envelope]: 14,
[TaskIcon.File]: 15,
[TaskIcon.PaperPlane]: 16,
[TaskIcon.Building]: 17,
[TaskIcon.Flag]: 18,
[TaskIcon.Bell]: 19,
[TaskIcon.Clock]: 20,
[TaskIcon.Image]: 21,
}
export function getTaskSorter(sortingModes: SortingMode[]) { export function getTaskSorter(sortingModes: SortingMode[]) {
return (a: TaskWithId, b: TaskWithId) => { return (a: TaskWithId, b: TaskWithId) => {
@ -61,13 +30,13 @@ const SORTING_MODE_TO_SORTING_FUNCTION = {
return a[1].text.localeCompare(b[1].text) return a[1].text.localeCompare(b[1].text)
}, },
[SortingMode.ByIcon]: function sortTasksByIcon(a: TaskWithId, b: TaskWithId) { [SortingMode.ByIcon]: function sortTasksByIcon(a: TaskWithId, b: TaskWithId) {
return TASK_ICON_TO_VALUE[a[1].icon] - TASK_ICON_TO_VALUE[b[1].icon] return ICONS.indexOf(a[1].icon as any) - ICONS.indexOf(b[1].icon as any)
}, },
[SortingMode.ByImportance]: function sortTasksByImportance(a: TaskWithId, b: TaskWithId) { [SortingMode.ByImportance]: function sortTasksByImportance(a: TaskWithId, b: TaskWithId) {
return TASK_IMPORTANCE_TO_VALUE[a[1].importance] - TASK_IMPORTANCE_TO_VALUE[b[1].importance] return TASK_IMPORTANCE_TO_VALUE[a[1].importance] - TASK_IMPORTANCE_TO_VALUE[b[1].importance]
}, },
[SortingMode.ByPriority]: function sortTasksByPriority(a: TaskWithId, b: TaskWithId) { [SortingMode.ByDeadline]: function sortTasksByPriority(a: TaskWithId, b: TaskWithId) {
return TASK_PRIORITY_TO_VALUE[a[1].priority] - TASK_PRIORITY_TO_VALUE[b[1].priority] return (b[1].deadline ?? -1) - (a[1].deadline ?? -1)
}, },
[SortingMode.ByStatus]: function sortTasksByStatus(a: TaskWithId, b: TaskWithId) { [SortingMode.ByStatus]: function sortTasksByStatus(a: TaskWithId, b: TaskWithId) {
if(a[1].journaled_on && !b[1].journaled_on) return 1; if(a[1].journaled_on && !b[1].journaled_on) return 1;

View file

@ -1,5 +1,6 @@
import {ModifyTaskBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)" import {ModifyTaskBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)" import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/convertTTS"
import {TaskActions} from "@/app/[lang]/board/[board]/(page)/(task)/TaskActions" import {TaskActions} from "@/app/[lang]/board/[board]/(page)/(task)/TaskActions"
import {TaskContainer} from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer" import {TaskContainer} from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer"
import {TaskDescription} from "@/app/[lang]/board/[board]/(page)/(task)/TaskDescription" import {TaskDescription} from "@/app/[lang]/board/[board]/(page)/(task)/TaskDescription"
@ -12,7 +13,7 @@ import {TFunction} from "i18next"
import {Dispatch, KeyboardEvent, PointerEvent, SetStateAction, SyntheticEvent, useCallback, useState} from "react" 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>>}) { export function TaskViewer({lang, t, taskWithId: [id, task], setEditorInput}: {lang: string, t: TFunction, taskWithId: TaskWithId, setEditorInput: Dispatch<SetStateAction<string>>}) {
const [isFlipped, setFlipped] = useState<boolean>(false) const [isFlipped, setFlipped] = useState<boolean>(false)
const {sendRequest, boardState: {locked}} = useBoardConsumer() const {sendRequest, boardState: {locked}} = useBoardConsumer()
@ -103,6 +104,7 @@ export function TaskViewer({t, taskWithId: [id, task], setEditorInput}: {t: TFun
goAwayButton = ( goAwayButton = (
<TaskViewerRecreateButton <TaskViewerRecreateButton
t={t} t={t}
lang={lang}
taskWithId={[id, task]} taskWithId={[id, task]}
setEditorInput={setEditorInput} setEditorInput={setEditorInput}
/> />
@ -129,7 +131,7 @@ export function TaskViewer({t, taskWithId: [id, task], setEditorInput}: {t: TFun
<TaskContainer <TaskContainer
role={"article"} role={"article"}
importance={task.importance} importance={task.importance}
priority={task.priority} deadline={task.deadline}
status={status} status={status}
onKeyDown={toggleFlipOnKeyDown} onKeyDown={toggleFlipOnKeyDown}
onPointerEnter={flipOnPointerEnter} onPointerEnter={flipOnPointerEnter}
@ -138,12 +140,13 @@ export function TaskViewer({t, taskWithId: [id, task], setEditorInput}: {t: TFun
> >
<TaskViewerIcon <TaskViewerIcon
t={t} t={t}
icon={task.icon} icon={task.icon as any}
status={status} status={status}
onInteract={status === TaskSimplifiedStatus.Journaled ? undefined : toggleStatus} onInteract={status === TaskSimplifiedStatus.Journaled ? undefined : toggleStatus}
/> />
<TaskDescription <TaskDescription
text={task.text} isSource={isFlipped}
text={isFlipped ? taskToString(task, lang) : task.text}
/> />
{sideElements} {sideElements}
</TaskContainer> </TaskContainer>

View file

@ -1,4 +1,3 @@
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)"
import {TaskIconComponent} from "@/app/[lang]/board/[board]/(page)/(task)/TaskIconComponent" import {TaskIconComponent} from "@/app/[lang]/board/[board]/(page)/(task)/TaskIconComponent"
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus" import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
import {TFunction} from "i18next" import {TFunction} from "i18next"
@ -15,7 +14,7 @@ const TASK_STATUS_TO_I18N_KEY: {[key in TaskSimplifiedStatus]: string} = {
export type TaskViewerIconProps = { export type TaskViewerIconProps = {
t: TFunction, t: TFunction,
status: TaskSimplifiedStatus, status: TaskSimplifiedStatus,
icon: TaskIcon, icon: string,
onInteract?: Parameters<typeof TaskIconComponent>[0]["onInteract"] onInteract?: Parameters<typeof TaskIconComponent>[0]["onInteract"]
} }

View file

@ -2,7 +2,7 @@ import {UpdateTaskBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)"
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)" import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
import {TaskButton} from "@/app/[lang]/board/[board]/(page)/(task)/TaskButton" import {TaskButton} from "@/app/[lang]/board/[board]/(page)/(task)/TaskButton"
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId" import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
import {faBookBookmark} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {TFunction} from "i18next" import {TFunction} from "i18next"
import {SyntheticEvent, useCallback} from "react" import {SyntheticEvent, useCallback} from "react"
@ -25,7 +25,7 @@ export function TaskViewerJournalButton({t, taskWithId: [id, task]}: {t: TFuncti
return ( return (
<TaskButton <TaskButton
title={t("taskButtonJournal")} title={t("taskButtonJournal")}
icon={faBookBookmark} icon={fas.faBookBookmark}
onInteract={locked ? undefined : toggleJournalTask} onInteract={locked ? undefined : toggleJournalTask}
/> />
) )

View file

@ -1,21 +1,22 @@
import {DeleteTaskBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)" import {DeleteTaskBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)" import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/taskToString" import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/convertTTS"
import {TaskButton} from "@/app/[lang]/board/[board]/(page)/(task)/TaskButton" import {TaskButton} from "@/app/[lang]/board/[board]/(page)/(task)/TaskButton"
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId" import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
import {faTrashArrowUp} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {TFunction} from "i18next" import {TFunction} from "i18next"
import {Dispatch, SetStateAction, SyntheticEvent, useCallback} from "react" import {Dispatch, SetStateAction, SyntheticEvent, useCallback} from "react"
export type TaskViewerRecreateButtonProps = { export type TaskViewerRecreateButtonProps = {
t: TFunction, t: TFunction,
lang: string,
taskWithId: TaskWithId, taskWithId: TaskWithId,
setEditorInput: Dispatch<SetStateAction<string>>, setEditorInput: Dispatch<SetStateAction<string>>,
} }
export function TaskViewerRecreateButton({t, taskWithId: [id, task], setEditorInput}: TaskViewerRecreateButtonProps) { export function TaskViewerRecreateButton({t, lang, taskWithId: [id, task], setEditorInput}: TaskViewerRecreateButtonProps) {
const {sendRequest, boardState: {locked}} = useBoardConsumer() const {sendRequest, boardState: {locked}} = useBoardConsumer()
const recreateTask = useCallback((e: SyntheticEvent<HTMLButtonElement>) => { const recreateTask = useCallback((e: SyntheticEvent<HTMLButtonElement>) => {
@ -26,7 +27,7 @@ export function TaskViewerRecreateButton({t, taskWithId: [id, task], setEditorIn
} }
e.preventDefault() e.preventDefault()
e.stopPropagation() e.stopPropagation()
setEditorInput(taskToString(task)) setEditorInput(taskToString(task, lang))
const request: DeleteTaskBoardRequest = {"Task": [id, null]}; const request: DeleteTaskBoardRequest = {"Task": [id, null]};
sendRequest(request) sendRequest(request)
}, [task, setEditorInput, sendRequest]) }, [task, setEditorInput, sendRequest])
@ -34,7 +35,7 @@ export function TaskViewerRecreateButton({t, taskWithId: [id, task], setEditorIn
return ( return (
<TaskButton <TaskButton
title={t("taskButtonRecreate")} title={t("taskButtonRecreate")}
icon={faTrashArrowUp} icon={fas.faTrashArrowUp}
onInteract={locked ? undefined : recreateTask} onInteract={locked ? undefined : recreateTask}
/> />
) )

View file

@ -4,38 +4,39 @@ import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)"
import {SortingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(sorting)" import {SortingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(sorting)"
import {BoardViewer} from "@/app/[lang]/board/[board]/(page)/(view)/BoardViewer" import {BoardViewer} from "@/app/[lang]/board/[board]/(page)/(view)/BoardViewer"
import {SplashWithIcon} from "@/app/[lang]/board/[board]/(page)/(view)/SplashWithIcon" import {SplashWithIcon} from "@/app/[lang]/board/[board]/(page)/(view)/SplashWithIcon"
import {faAsterisk, faGear, faLink, faLinkSlash} from "@fortawesome/free-solid-svg-icons" import {fas} from "@awesome.me/kit-dfe340c874/icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome" import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {TFunction} from "i18next" import {TFunction} from "i18next"
import {Dispatch, SetStateAction} from "react" import {Dispatch, SetStateAction} from "react"
export function BoardMain({t, className, columning, sorting, grouping, setEditorInput}: {t: TFunction, className?: string, columning: ColumningMode, grouping: GroupingMode, sorting: SortingMode[], setEditorInput: Dispatch<SetStateAction<string>>}) { export function BoardMain({lang, t, className, columning, sorting, grouping, setEditorInput}: {lang: string, t: TFunction, className?: string, columning: ColumningMode, grouping: GroupingMode, sorting: SortingMode[], setEditorInput: Dispatch<SetStateAction<string>>}) {
const {webSocketState, webSocketBackoffMs, boardState} = useBoardConsumer() const {webSocketState, webSocketBackoffMs, boardState} = useBoardConsumer()
switch(webSocketState) { switch(webSocketState) {
case undefined: case undefined:
return <SplashWithIcon return <SplashWithIcon
icon={<FontAwesomeIcon size={"4x"} icon={faGear} beatFade/>} icon={<FontAwesomeIcon size={"4x"} icon={fas.faGear} beatFade/>}
text={t("boardPreparing")} text={t("boardPreparing")}
className={className} className={className}
/> />
case WebSocket.CONNECTING: case WebSocket.CONNECTING:
return <SplashWithIcon return <SplashWithIcon
icon={<FontAwesomeIcon size={"4x"} icon={faLink} beatFade/>} icon={<FontAwesomeIcon size={"4x"} icon={fas.faLink} beatFade/>}
text={t("boardConnecting")} text={t("boardConnecting")}
className={className} className={className}
/> />
case WebSocket.OPEN: case WebSocket.OPEN:
if(Object.keys(boardState.tasks).length === 0) { if(Object.keys(boardState.tasks).length === 0) {
return <SplashWithIcon return <SplashWithIcon
icon={<FontAwesomeIcon size={"4x"} icon={faAsterisk}/>} icon={<FontAwesomeIcon size={"4x"} icon={fas.faAsterisk}/>}
text={t("boardEmpty")} text={t("boardEmpty")}
className={className} className={className}
/> />
} }
else { else {
return <BoardViewer return <BoardViewer
lang={lang}
t={t} t={t}
columning={columning} columning={columning}
sorting={sorting} sorting={sorting}
@ -46,13 +47,13 @@ export function BoardMain({t, className, columning, sorting, grouping, setEditor
} }
case WebSocket.CLOSING: case WebSocket.CLOSING:
return <SplashWithIcon return <SplashWithIcon
icon={<FontAwesomeIcon size={"4x"} icon={faLinkSlash} beatFade/>} icon={<FontAwesomeIcon size={"4x"} icon={fas.faLinkSlash} beatFade/>}
text={t("boardDisconnecting")} text={t("boardDisconnecting")}
className={className} className={className}
/> />
case WebSocket.CLOSED: case WebSocket.CLOSED:
return <SplashWithIcon return <SplashWithIcon
icon={<FontAwesomeIcon size={"4x"} icon={faLinkSlash}/>} icon={<FontAwesomeIcon size={"4x"} icon={fas.faLinkSlash}/>}
text={t("boardDisconnected", { retryingInSeconds: Math.ceil((webSocketBackoffMs ?? 0) / 1000).toString() })} text={t("boardDisconnected", { retryingInSeconds: Math.ceil((webSocketBackoffMs ?? 0) / 1000).toString() })}
className={className} className={className}
/> />

View file

@ -12,7 +12,7 @@ import {Dispatch, SetStateAction} from "react"
import style from "./BoardViewer.module.css" import style from "./BoardViewer.module.css"
export function BoardViewer({className, t, columning, grouping, sorting, setEditorInput}: {className?: string, t: TFunction, columning: ColumningMode, grouping: GroupingMode, sorting: SortingMode[], setEditorInput: Dispatch<SetStateAction<string>>}) { export function BoardViewer({className, lang, t, columning, grouping, sorting, setEditorInput}: {className?: string, t: TFunction, lang: string, columning: ColumningMode, grouping: GroupingMode, sorting: SortingMode[], setEditorInput: Dispatch<SetStateAction<string>>}) {
const {boardState: {tasks}} = useBoardConsumer() const {boardState: {tasks}} = useBoardConsumer()
const {taskGroups} = useBoardTasksArranger(tasks, grouping, sorting); const {taskGroups} = useBoardTasksArranger(tasks, grouping, sorting);
@ -22,12 +22,12 @@ export function BoardViewer({className, t, columning, grouping, sorting, setEdit
[style.boardMainTaskGroupsMultiColumn]: columning === ColumningMode.MultiColumn, [style.boardMainTaskGroupsMultiColumn]: columning === ColumningMode.MultiColumn,
[style.boardMainTaskGroupsSingleColumn]: columning === ColumningMode.SingleColumn, [style.boardMainTaskGroupsSingleColumn]: columning === ColumningMode.SingleColumn,
})}> })}>
{taskGroups.map((tg) => <BoardViewerColumn t={t} taskGroup={tg} key={tg.k} grouping={grouping} setEditorInput={setEditorInput}/>)} {taskGroups.map((tg) => <BoardViewerColumn lang={lang} t={t} taskGroup={tg} key={tg.k} grouping={grouping} setEditorInput={setEditorInput}/>)}
</main> </main>
) )
} }
export function BoardViewerColumn({t, grouping, taskGroup, setEditorInput}: {t: TFunction, grouping: GroupingMode, taskGroup: TaskGroup<string | number>, setEditorInput: Dispatch<SetStateAction<string>>}) { export function BoardViewerColumn({lang, t, grouping, taskGroup, setEditorInput}: {lang: string, t: TFunction, grouping: GroupingMode, taskGroup: TaskGroup<string | number>, setEditorInput: Dispatch<SetStateAction<string>>}) {
const ColumnTitle = GROUPING_MODE_TO_TITLE_COMPONENT[grouping] const ColumnTitle = GROUPING_MODE_TO_TITLE_COMPONENT[grouping]
return ( return (
@ -36,7 +36,7 @@ export function BoardViewerColumn({t, grouping, taskGroup, setEditorInput}: {t:
<ColumnTitle t={t} k={taskGroup.k}/> <ColumnTitle t={t} k={taskGroup.k}/>
</h3> </h3>
<div className={style.boardColumnContents}> <div className={style.boardColumnContents}>
{taskGroup.tasks.map((task: TaskWithId) => <TaskViewer t={t} taskWithId={task} key={task[0]} setEditorInput={setEditorInput}/>)} {taskGroup.tasks.map((task: TaskWithId) => <TaskViewer lang={lang} t={t} taskWithId={task} key={task[0]} setEditorInput={setEditorInput}/>)}
</div> </div>
</div> </div>
) )

View file

@ -15,7 +15,7 @@ export function BoardPage({lang}: {lang: string}) {
const internationalization = useClientTranslation(lang, "board") const internationalization = useClientTranslation(lang, "board")
const metadataHook = useBoardMetadataEditor() const metadataHook = useBoardMetadataEditor()
const layoutHook = useBoardLayoutEditor() const layoutHook = useBoardLayoutEditor()
const editorHook = useTaskEditor() const editorHook = useTaskEditor(lang)
const t = internationalization.t as TFunction const t = internationalization.t as TFunction
@ -28,6 +28,7 @@ export function BoardPage({lang}: {lang: string}) {
layoutHook={layoutHook} layoutHook={layoutHook}
/> />
<BoardMain <BoardMain
lang={lang}
t={t} t={t}
className={style.pageMain} className={style.pageMain}
columning={layoutHook.columningHook.value} columning={layoutHook.columningHook.value}
@ -36,6 +37,7 @@ export function BoardPage({lang}: {lang: string}) {
setEditorInput={editorHook.setInput} setEditorInput={editorHook.setInput}
/> />
<BoardEditor <BoardEditor
lang={lang}
t={t} t={t}
className={style.pageEditor} className={style.pageEditor}
editorHook={editorHook} editorHook={editorHook}

View file

@ -19,7 +19,6 @@ export function useBoardLayoutEditor() {
ColumningMode.MultiColumn, ColumningMode.MultiColumn,
]) ])
const groupingHook = useCycler(useLocalStorage<number | undefined>(localStorageKeyGrouping, undefined), [ const groupingHook = useCycler(useLocalStorage<number | undefined>(localStorageKeyGrouping, undefined), [
GroupingMode.ByPriority,
GroupingMode.ByImportance, GroupingMode.ByImportance,
GroupingMode.ByStatus, GroupingMode.ByStatus,
GroupingMode.ByIcon, GroupingMode.ByIcon,
@ -27,46 +26,33 @@ export function useBoardLayoutEditor() {
]) ])
const sortingHook = useCycler(useLocalStorage<number | undefined>(localStorageKeySorting, undefined), [ const sortingHook = useCycler(useLocalStorage<number | undefined>(localStorageKeySorting, undefined), [
[ [
SortingMode.ByDeadline,
SortingMode.ByImportance,
SortingMode.ByStatus, SortingMode.ByStatus,
SortingMode.ByPriority,
SortingMode.ByImportance,
SortingMode.ByText, SortingMode.ByText,
SortingMode.ByIcon, SortingMode.ByIcon,
SortingMode.ByCreation, SortingMode.ByCreation,
], ],
[ [
SortingMode.ByStatus, SortingMode.ByImportance,
SortingMode.ByImportance, SortingMode.ByDeadline,
SortingMode.ByPriority, SortingMode.ByStatus,
SortingMode.ByText, SortingMode.ByText,
SortingMode.ByIcon, SortingMode.ByIcon,
SortingMode.ByCreation, SortingMode.ByCreation,
], ],
[ [
SortingMode.ByStatus, SortingMode.ByStatus,
SortingMode.ByDeadline,
SortingMode.ByImportance,
SortingMode.ByText, SortingMode.ByText,
SortingMode.ByIcon, SortingMode.ByIcon,
SortingMode.ByCreation, SortingMode.ByCreation,
], ],
[ [
SortingMode.ByStatus, SortingMode.ByStatus,
SortingMode.ByCreation,
],
[
SortingMode.ByPriority,
SortingMode.ByImportance, SortingMode.ByImportance,
SortingMode.ByText, SortingMode.ByDeadline,
SortingMode.ByIcon,
SortingMode.ByCreation,
],
[
SortingMode.ByImportance,
SortingMode.ByPriority,
SortingMode.ByText,
SortingMode.ByIcon,
SortingMode.ByCreation,
],
[
SortingMode.ByText, SortingMode.ByText,
SortingMode.ByIcon, SortingMode.ByIcon,
SortingMode.ByCreation, SortingMode.ByCreation,

View file

@ -1,15 +1,15 @@
import {Task} from "@/app/[lang]/board/[board]/(api)/(task)" import {Task} from "@/app/[lang]/board/[board]/(api)/(task)"
import {stringToTask} from "@/app/[lang]/board/[board]/(page)/(edit)/stringToTask" import {convertSTT} from "@/app/[lang]/board/[board]/(page)/(edit)/convertSTT"
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/taskToString" import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/convertTTS"
import {useCallback, useMemo, useState} from "react" import {useCallback, useMemo, useState} from "react"
export function useTaskEditor() { export function useTaskEditor(lang: string) {
const [input, setInput] = useState<string>("") const [input, setInput] = useState<string>("")
const task = useMemo(() => stringToTask(input), [input]) const task = useMemo(() => convertSTT(input, lang), [input, lang])
const setTask = useCallback((t: Task) => { const setTask = useCallback((t: Task) => {
setInput(taskToString(t)) setInput(taskToString(t, lang))
}, []) }, [])
return { return {

View file

@ -2,152 +2,143 @@
# yarn lockfile v1 # yarn lockfile v1
"@babel/runtime@^7.21.5", "@babel/runtime@^7.22.5": "@awesome.me/kit-dfe340c874@^1.0.10":
version "7.22.10" version "1.0.10"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682" resolved "https://npm.fontawesome.com/@awesome.me/kit-dfe340c874/-/1.0.10/kit-dfe340c874-1.0.10.tgz#a09e8b5a92ab9d58e703008f5adde7aaeaf6f629"
integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ== integrity sha512-q8cIvNP/0xULSNs5XqusIa4pTgI9pvMubNZIQQWIDM8yacpYt1wM54jOceq0g3tGTzl1HummO4oN5Xdfbm8gww==
dependencies:
"@fortawesome/fontawesome-common-types" "^6.6.0"
"@babel/runtime@^7.23.2":
version "7.25.6"
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.25.6.tgz#9afc3289f7184d8d7f98b099884c26317b9264d2"
integrity sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==
dependencies: dependencies:
regenerator-runtime "^0.14.0" regenerator-runtime "^0.14.0"
"@fortawesome/fontawesome-common-types@6.4.2": "@fortawesome/fontawesome-common-types@6.6.0", "@fortawesome/fontawesome-common-types@^6.6.0":
version "6.4.2" version "6.6.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5" resolved "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/6.6.0/fontawesome-common-types-6.6.0.tgz#31ab07ca6a06358c5de4d295d4711b675006163f"
integrity sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA== integrity sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==
"@fortawesome/fontawesome-svg-core@^6.4.0": "@fortawesome/fontawesome-svg-core@*":
version "6.4.2" version "6.6.0"
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz#37f4507d5ec645c8b50df6db14eced32a6f9be09" resolved "https://npm.fontawesome.com/@fortawesome/fontawesome-svg-core/-/6.6.0/fontawesome-svg-core-6.6.0.tgz#2a24c32ef92136e98eae2ff334a27145188295ff"
integrity sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg== integrity sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==
dependencies: dependencies:
"@fortawesome/fontawesome-common-types" "6.4.2" "@fortawesome/fontawesome-common-types" "6.6.0"
"@fortawesome/free-regular-svg-icons@^6.4.0": "@fortawesome/react-fontawesome@*":
version "6.4.2" version "0.2.2"
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz#aee79ed76ce5dd04931352f9d83700761b8b1b25" resolved "https://npm.fontawesome.com/@fortawesome/react-fontawesome/-/0.2.2/react-fontawesome-0.2.2.tgz#68b058f9132b46c8599875f6a636dad231af78d4"
integrity sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q== integrity sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==
dependencies:
"@fortawesome/fontawesome-common-types" "6.4.2"
"@fortawesome/free-solid-svg-icons@^6.4.0":
version "6.4.2"
resolved "https://registry.yarnpkg.com/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.4.2.tgz#33a02c4cb6aa28abea7bc082a9626b7922099df4"
integrity sha512-sYwXurXUEQS32fZz9hVCUUv/xu49PEJEyUOsA51l6PU/qVgfbTb2glsTEaJngVVT8VqBATRIdh7XVgV1JF1LkA==
dependencies:
"@fortawesome/fontawesome-common-types" "6.4.2"
"@fortawesome/react-fontawesome@^0.2.0":
version "0.2.0"
resolved "https://registry.yarnpkg.com/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.0.tgz#d90dd8a9211830b4e3c08e94b63a0ba7291ddcf4"
integrity sha512-uHg75Rb/XORTtVt7OS9WoK8uM276Ufi7gCzshVWkUJbHhh3svsUUeqXerrM96Wm7fRiDzfKRwSoahhMIkGAYHw==
dependencies: dependencies:
prop-types "^15.8.1" prop-types "^15.8.1"
"@next/env@13.5.0": "@next/env@14.2.11":
version "13.5.0" version "14.2.11"
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.0.tgz#a61dee2f29b09985847eabcc4c8a815031267a36" resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.11.tgz#91fa6865140e7c89c555651cbe28b57180b26b9e"
integrity sha512-mxhf/BskjPURT+qEjNP7wBvqre2q6OXEIbydF8BrH+duSSJQnB4/vzzuJDoahYwTXiUaXpouAnMWHZdG0HU62g== integrity sha512-HYsQRSIXwiNqvzzYThrBwq6RhXo3E0n8j8nQnAs8i4fCEo2Zf/3eS0IiRA8XnRg9Ha0YnpkyJZIZg1qEwemrHw==
"@next/swc-darwin-arm64@13.5.0": "@next/swc-darwin-arm64@14.2.11":
version "13.5.0" version "14.2.11"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.0.tgz#45ea191e13593088572d0048d4ddfc1fcdb3c8ed" resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.11.tgz#0022b52ccc62e21c34a34311ee0251e31cd5ff49"
integrity sha512-DavPD8oRjSoCRJana5DCAWdRZ4nbS7/pPw13DlnukFfMPJUk5hCAC3+NbqEyekS/X1IBFdZWSV2lJIdzTn4s6w== integrity sha512-eiY9u7wEJZWp/Pga07Qy3ZmNEfALmmSS1HtsJF3y1QEyaExu7boENz11fWqDmZ3uvcyAxCMhTrA1jfVxITQW8g==
"@next/swc-darwin-x64@13.5.0": "@next/swc-darwin-x64@14.2.11":
version "13.5.0" version "14.2.11"
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.0.tgz#582e8df7d563c057581bc118fff1cea79391d6e7" resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.11.tgz#41151d22b5009a2a83bb57e6d98efb8a8995a3cd"
integrity sha512-s5QSKKB0CTKFWp3CNMC5GH1YOipH1Jjr5P3w+RQTC4Aybo6xPqeWp/UyDW0fxmLRq0e1zgnOMgDQRdxAkoThrw== integrity sha512-lnB0zYCld4yE0IX3ANrVMmtAbziBb7MYekcmR6iE9bujmgERl6+FK+b0MBq0pl304lYe7zO4yxJus9H/Af8jbg==
"@next/swc-linux-arm64-gnu@13.5.0": "@next/swc-linux-arm64-gnu@14.2.11":
version "13.5.0" version "14.2.11"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.0.tgz#7ee0a43b6635eca1e80a887304b7bfe31254a4a6" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.11.tgz#8fdc732c39b79dc7519bb561c48a2417e0eb6b24"
integrity sha512-E0fCKA8F2vfgZWwcv4iq642No75EiACSNUBNGvc5lx/ylqAUdNwE/9+x2SHv+LPUXFhZ6hZLR0Qox/oKgZqFlg== integrity sha512-Ulo9TZVocYmUAtzvZ7FfldtwUoQY0+9z3BiXZCLSUwU2bp7GqHA7/bqrfsArDlUb2xeGwn3ZuBbKtNK8TR0A8w==
"@next/swc-linux-arm64-musl@13.5.0": "@next/swc-linux-arm64-musl@14.2.11":
version "13.5.0" version "14.2.11"
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.0.tgz#99a1efd6b68a4d0dfdc24b81f14cd8b8251425a9" resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.11.tgz#04c3730ca4bc2f0c56c73db53dd03cf00126a243"
integrity sha512-jG/blDDLndFRUcafCQO4TOI3VuoIZh3jQriZ7JaVCgAEZe0D1EUrxKdbBarZ74isutHZ6DpNGRDi/0OHFZpJAA== integrity sha512-fH377DnKGyUnkWlmUpFF1T90m0dADBfK11dF8sOQkiELF9M+YwDRCGe8ZyDzvQcUd20Rr5U7vpZRrAxKwd3Rzg==
"@next/swc-linux-x64-gnu@13.5.0": "@next/swc-linux-x64-gnu@14.2.11":
version "13.5.0" version "14.2.11"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.0.tgz#7c85acd45879a20d8fb102b3212e792924d02e93" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.11.tgz#533823c0a96d31122671f670b58da65bdb71f330"
integrity sha512-6JWR7U41uNL6HGwNbGg3Oedt+FN4YuA126sHWKTq3ic5kkhEusIIdVo7+WcswVJl8nTMB1yT3gEPwygQbVYVUA== integrity sha512-a0TH4ZZp4NS0LgXP/488kgvWelNpwfgGTUCDXVhPGH6pInb7yIYNgM4kmNWOxBFt+TIuOH6Pi9NnGG4XWFUyXQ==
"@next/swc-linux-x64-musl@13.5.0": "@next/swc-linux-x64-musl@14.2.11":
version "13.5.0" version "14.2.11"
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.0.tgz#23aad9ab7621f53bb947b727e659d85e74b0e31a" resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.11.tgz#fa81b56095ef4a64bae7f5798e6cdeac9de2906f"
integrity sha512-uY+wrYfD5QUossqznwidOpJYmmcBwojToZx55shihtbTl6afVYzOxsUbRXLdWmZAa36ckxXpqkvuFNS8icQuug== integrity sha512-DYYZcO4Uir2gZxA4D2JcOAKVs8ZxbOFYPpXSVIgeoQbREbeEHxysVsg3nY4FrQy51e5opxt5mOHl/LzIyZBoKA==
"@next/swc-win32-arm64-msvc@13.5.0": "@next/swc-win32-arm64-msvc@14.2.11":
version "13.5.0" version "14.2.11"
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.0.tgz#5a45686335e5f54342faf9d9ed25f55a4107ce7f" resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.11.tgz#3ad4a72282f9b5e72f600522156e5bab6efb63a8"
integrity sha512-lWZ5vJTULxTOdLcRmrllNgAdDRSDwk8oqJMyDxpqS691NG5uhle9ZwRj3g1F1/vHNkDa+B7PmWhQgG0nmlbKZg== integrity sha512-PwqHeKG3/kKfPpM6of1B9UJ+Er6ySUy59PeFu0Un0LBzJTRKKAg2V6J60Yqzp99m55mLa+YTbU6xj61ImTv9mg==
"@next/swc-win32-ia32-msvc@13.5.0": "@next/swc-win32-ia32-msvc@14.2.11":
version "13.5.0" version "14.2.11"
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.0.tgz#b9990965762aaa109bdeb7b49cbdc7e4af7f9014" resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.11.tgz#80a6dd48e2f8035c518c2162bf6058bd61921a1b"
integrity sha512-jirQXnVCU9hi3cHzgd33d4qSBXn1/0gUT/KtXqy9Ux9OTcIcjJT3TcAzoLJLTdhRg7op3MZoSnuFeWl8kmGGNw== integrity sha512-0U7PWMnOYIvM74GY6rbH6w7v+vNPDVH1gUhlwHpfInJnNe5LkmUZqhp7FNWeNa5wbVgRcRi1F1cyxp4dmeLLvA==
"@next/swc-win32-x64-msvc@13.5.0": "@next/swc-win32-x64-msvc@14.2.11":
version "13.5.0" version "14.2.11"
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.0.tgz#4385c5d9c0db39c2623aed566b3ec7fedaf6f190" resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.11.tgz#2150783dfec3b3e0e7c3ecfda2d4f30fbf913d44"
integrity sha512-Q8QYLyWcMMUp3DohI04VyJbLNCfFMNTxYNhujvJD2lowuqnqApUBP2DxI/jCZRMFWgKi76n5u8UboLVeYXn6jA== integrity sha512-gQpS7mcgovWoaTG1FbS5/ojF7CGfql1Q0ZLsMrhcsi2Sr9HEqsUZ70MPJyaYBXbk6iEAP7UXMD9HC8KY1qNwvA==
"@steffo/bluelib@^9.0.1": "@steffo/bluelib@*":
version "9.0.1" version "9.1.0"
resolved "https://registry.yarnpkg.com/@steffo/bluelib/-/bluelib-9.0.1.tgz#15932995ab67ff1b959601036042cd2f14c4d197" resolved "https://registry.yarnpkg.com/@steffo/bluelib/-/bluelib-9.1.0.tgz#48b03411ad66c53a1931e5ff3ae316a3cf3d9bb6"
integrity sha512-r6/38m0LuapcF5Fung5yjITTcXNoWf03I5C2b4dBDGjt7IqBAiaoBbLsRcChZ5hRRn32TJO10yuDlgsPNwLhAg== integrity sha512-S+MT4NLfcFu2oV22UabI8TXn9c3LOpx3FM6Nb4L/eiMTpE/XJf/NK4di8prHBabt8230JIDDv+SGxUzurG2I8g==
"@swc/helpers@0.5.2": "@swc/counter@^0.1.3":
version "0.5.2" version "0.1.3"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d" resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw== integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
"@swc/helpers@0.5.5":
version "0.5.5"
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.5.tgz#12689df71bfc9b21c4f4ca00ae55f2f16c8b77c0"
integrity sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==
dependencies: dependencies:
"@swc/counter" "^0.1.3"
tslib "^2.4.0" tslib "^2.4.0"
"@types/negotiator@^0.6.1": "@types/negotiator@*":
version "0.6.1" version "0.6.3"
resolved "https://registry.yarnpkg.com/@types/negotiator/-/negotiator-0.6.1.tgz#4c75543f6ef87f427f4705e731a933595b7397f5" resolved "https://registry.yarnpkg.com/@types/negotiator/-/negotiator-0.6.3.tgz#29e8fce64e35f57f6fe9c624f8e4ed304357745a"
integrity sha512-c4mvXFByghezQ/eVGN5HvH/jI63vm3B7FiE81BUzDAWmuiohRecCO6ddU60dfq29oKUMiQujsoB2h0JQC7JHKA== integrity sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==
"@types/node@20.4.5": "@types/node@*":
version "20.4.5" version "22.5.4"
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.5.tgz#9dc0a5cb1ccce4f7a731660935ab70b9c00a5d69" resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.4.tgz#83f7d1f65bc2ed223bdbf57c7884f1d5a4fa84e8"
integrity sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg== integrity sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==
dependencies:
undici-types "~6.19.2"
"@types/prop-types@*": "@types/prop-types@*":
version "15.7.5" version "15.7.12"
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf" resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w== integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
"@types/react-dom@18.2.7": "@types/react-dom@*":
version "18.2.7" version "18.3.0"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63" resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
integrity sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA== integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==
dependencies: dependencies:
"@types/react" "*" "@types/react" "*"
"@types/react@*": "@types/react@*":
version "18.2.20" version "18.3.5"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.20.tgz#1605557a83df5c8a2cc4eeb743b3dfc0eb6aaeb2" resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.5.tgz#5f524c2ad2089c0ff372bbdabc77ca2c4dbadf8f"
integrity sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw== integrity sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==
dependencies: dependencies:
"@types/prop-types" "*" "@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2" csstype "^3.0.2"
"@types/react@18.2.17": any-date-parser@^1.5.4:
version "18.2.17" version "1.5.4"
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.17.tgz#baa565b17ddb649c2dac85b5eaf9e9a1fe0f3b4e" resolved "https://registry.yarnpkg.com/any-date-parser/-/any-date-parser-1.5.4.tgz#3c462f3a419b7147a88a8c7cab91da4193ff680b"
integrity sha512-u+e7OlgPPh+aryjOm5UJMX32OvB2E3QASOAqVMY6Ahs90djagxwv2ya0IctglNbNTexC12qCSMZG47KPfy1hAA== integrity sha512-S4gl9UmXNk9XXSQxp5w5harUD6aM0fepyL3dZM/B3znX57sWf792hS2UvvCFIHECfpsqfKbQ+cqWBky4AKyRIg==
dependencies:
"@types/prop-types" "*"
"@types/scheduler" "*"
csstype "^3.0.2"
"@types/scheduler@*":
version "0.16.3"
resolved "https://registry.yarnpkg.com/@types/scheduler/-/scheduler-0.16.3.tgz#cef09e3ec9af1d63d2a6cc5b383a737e24e6dcf5"
integrity sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==
busboy@1.6.0: busboy@1.6.0:
version "1.6.0" version "1.6.0"
@ -156,49 +147,44 @@ busboy@1.6.0:
dependencies: dependencies:
streamsearch "^1.1.0" streamsearch "^1.1.0"
caniuse-lite@^1.0.30001406: caniuse-lite@^1.0.30001579:
version "1.0.30001521" version "1.0.30001660"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001521.tgz#e9930cf499f7c1e80334b6c1fbca52e00d889e56" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz#31218de3463fabb44d0b7607b652e56edf2e2355"
integrity sha512-fnx1grfpEOvDGH+V17eccmNjucGUnCbP6KL+l5KqBIerp26WK/+RQ7CIDE37KGJjaPyqWXXlFUyKiWmvdNNKmQ== integrity sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==
classnames@^2.3.2: classnames@*:
version "2.3.2" version "2.5.1"
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
client-only@0.0.1, client-only@^0.0.1: client-only@*, client-only@0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1" resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
csstype@^3.0.2: csstype@^3.0.2:
version "3.1.2" version "3.1.3"
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b" resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ== integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
glob-to-regexp@^0.4.1: graceful-fs@^4.2.11:
version "0.4.1"
resolved "https://registry.yarnpkg.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz#c75297087c851b9a578bd217dd59a92f59fe546e"
integrity sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==
graceful-fs@^4.1.2:
version "4.2.11" version "4.2.11"
resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3"
integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==
i18next-resources-to-backend@^1.1.4: i18next-resources-to-backend@*:
version "1.1.4" version "1.2.1"
resolved "https://registry.yarnpkg.com/i18next-resources-to-backend/-/i18next-resources-to-backend-1.1.4.tgz#d139ca0cacc270dcc90b7926e192f4cd5aa4db60" resolved "https://registry.yarnpkg.com/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.1.tgz#fded121e63e3139ce839c9901b9449dbbea7351d"
integrity sha512-hMyr9AOmIea17AOaVe1srNxK/l3mbk81P7Uf3fdcjlw3ehZy3UNTd0OP3EEi6yu4J02kf9jzhCcjokz6AFlEOg== integrity sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==
dependencies: dependencies:
"@babel/runtime" "^7.21.5" "@babel/runtime" "^7.23.2"
i18next@^23.4.2: i18next@*:
version "23.4.4" version "23.15.1"
resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.4.4.tgz#ec8fb2b5f3c5d8e3bf3f8ab1b19e743be91300e0" resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.15.1.tgz#c50de337bf12ca5195e697cc0fbe5f32304871d9"
integrity sha512-+c9B0txp/x1m5zn+QlwHaCS9vyFtmIAEXbVSFzwCX7vupm5V7va8F9cJGNJZ46X9ZtoGzhIiRC7eTIIh93TxPA== integrity sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==
dependencies: dependencies:
"@babel/runtime" "^7.22.5" "@babel/runtime" "^7.23.2"
"js-tokens@^3.0.0 || ^4.0.0": "js-tokens@^3.0.0 || ^4.0.0":
version "4.0.0" version "4.0.0"
@ -212,39 +198,38 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
dependencies: dependencies:
js-tokens "^3.0.0 || ^4.0.0" js-tokens "^3.0.0 || ^4.0.0"
nanoid@^3.3.4: nanoid@^3.3.6:
version "3.3.6" version "3.3.7"
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA== integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
negotiator@^0.6.3: negotiator@*:
version "0.6.3" version "0.6.3"
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg== integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
next@13.5.0: next@*:
version "13.5.0" version "14.2.11"
resolved "https://registry.yarnpkg.com/next/-/next-13.5.0.tgz#3a3ce5b8c89c4fff9c6f0b2452bcb03f63d8c84c" resolved "https://registry.yarnpkg.com/next/-/next-14.2.11.tgz#86572882d340c5c1ee7e46d00790fa7ba664d6a9"
integrity sha512-mhguN5JPZXhhrD/nNcezXgKoxN8GT8xZvvGhUQV2ETiaNm+KHRWT1rCbrF5FlbG2XCcLRKOmOe3D5YQgXmJrDQ== integrity sha512-8MDFqHBhdmR2wdfaWc8+lW3A/hppFe1ggQ9vgIu/g2/2QEMYJrPoQP6b+VNk56gIug/bStysAmrpUKtj3XN8Bw==
dependencies: dependencies:
"@next/env" "13.5.0" "@next/env" "14.2.11"
"@swc/helpers" "0.5.2" "@swc/helpers" "0.5.5"
busboy "1.6.0" busboy "1.6.0"
caniuse-lite "^1.0.30001406" caniuse-lite "^1.0.30001579"
postcss "8.4.14" graceful-fs "^4.2.11"
postcss "8.4.31"
styled-jsx "5.1.1" styled-jsx "5.1.1"
watchpack "2.4.0"
zod "3.21.4"
optionalDependencies: optionalDependencies:
"@next/swc-darwin-arm64" "13.5.0" "@next/swc-darwin-arm64" "14.2.11"
"@next/swc-darwin-x64" "13.5.0" "@next/swc-darwin-x64" "14.2.11"
"@next/swc-linux-arm64-gnu" "13.5.0" "@next/swc-linux-arm64-gnu" "14.2.11"
"@next/swc-linux-arm64-musl" "13.5.0" "@next/swc-linux-arm64-musl" "14.2.11"
"@next/swc-linux-x64-gnu" "13.5.0" "@next/swc-linux-x64-gnu" "14.2.11"
"@next/swc-linux-x64-musl" "13.5.0" "@next/swc-linux-x64-musl" "14.2.11"
"@next/swc-win32-arm64-msvc" "13.5.0" "@next/swc-win32-arm64-msvc" "14.2.11"
"@next/swc-win32-ia32-msvc" "13.5.0" "@next/swc-win32-ia32-msvc" "14.2.11"
"@next/swc-win32-x64-msvc" "13.5.0" "@next/swc-win32-x64-msvc" "14.2.11"
object-assign@^4.1.1: object-assign@^4.1.1:
version "4.1.1" version "4.1.1"
@ -252,16 +237,16 @@ object-assign@^4.1.1:
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
picocolors@^1.0.0: picocolors@^1.0.0:
version "1.0.0" version "1.1.0"
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c" resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ== integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
postcss@8.4.14: postcss@8.4.31:
version "8.4.14" version "8.4.31"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf" resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig== integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
dependencies: dependencies:
nanoid "^3.3.4" nanoid "^3.3.6"
picocolors "^1.0.0" picocolors "^1.0.0"
source-map-js "^1.0.2" source-map-js "^1.0.2"
@ -274,47 +259,47 @@ prop-types@^15.8.1:
object-assign "^4.1.1" object-assign "^4.1.1"
react-is "^16.13.1" react-is "^16.13.1"
react-dom@18.2.0: react-dom@*:
version "18.2.0" version "18.3.1"
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d" resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g== integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
scheduler "^0.23.0" scheduler "^0.23.2"
react-is@^16.13.1: react-is@^16.13.1:
version "16.13.1" version "16.13.1"
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
react@18.2.0: react@*:
version "18.2.0" version "18.3.1"
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5" resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ== integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
regenerator-runtime@^0.14.0: regenerator-runtime@^0.14.0:
version "0.14.0" version "0.14.1"
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA== integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
scheduler@^0.23.0: scheduler@^0.23.2:
version "0.23.0" version "0.23.2"
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe" resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw== integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
dependencies: dependencies:
loose-envify "^1.1.0" loose-envify "^1.1.0"
server-only@^0.0.1: server-only@*:
version "0.0.1" version "0.0.1"
resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e" resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e"
integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA== integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==
source-map-js@^1.0.2: source-map-js@^1.0.2:
version "1.0.2" version "1.2.1"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c" resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw== integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
streamsearch@^1.1.0: streamsearch@^1.1.0:
version "1.1.0" version "1.1.0"
@ -329,29 +314,21 @@ styled-jsx@5.1.1:
client-only "0.0.1" client-only "0.0.1"
tslib@^2.4.0: tslib@^2.4.0:
version "2.6.1" version "2.7.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410" resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig== integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
typescript@5.1.6: typescript@*:
version "5.1.6" version "5.6.2"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274" resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA== integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==
use-local-storage@^3.0.0: undici-types@~6.19.2:
version "6.19.8"
resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02"
integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==
use-local-storage@*:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/use-local-storage/-/use-local-storage-3.0.0.tgz#ecf90952374150f0c65baf027eaaf2062a4c20c6" resolved "https://registry.yarnpkg.com/use-local-storage/-/use-local-storage-3.0.0.tgz#ecf90952374150f0c65baf027eaaf2062a4c20c6"
integrity sha512-wlPNnBCG3ULIJMr5A+dvWqLiPWCfsN1Kwijq+sAhT5yV4ex0u6XmZuNwP+RerIOfzBuz1pwSZuzhZMiluGQHfQ== integrity sha512-wlPNnBCG3ULIJMr5A+dvWqLiPWCfsN1Kwijq+sAhT5yV4ex0u6XmZuNwP+RerIOfzBuz1pwSZuzhZMiluGQHfQ==
watchpack@2.4.0:
version "2.4.0"
resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-2.4.0.tgz#fa33032374962c78113f93c7f2fb4c54c9862a5d"
integrity sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==
dependencies:
glob-to-regexp "^0.4.1"
graceful-fs "^4.1.2"
zod@3.21.4:
version "3.21.4"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.21.4.tgz#10882231d992519f0a10b5dd58a38c9dabbb64db"
integrity sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==

8
todocolors.iml Normal file
View file

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View file

@ -8,6 +8,7 @@ services:
--loglevel notice --loglevel notice
volumes: volumes:
- "./data/redis/rdata:/data" - "./data/redis/rdata:/data"
red: red:
image: "ghcr.io/steffo99/todocolors-red" image: "ghcr.io/steffo99/todocolors-red"
restart: unless-stopped restart: unless-stopped
@ -16,9 +17,11 @@ services:
AXUM_XFORWARDED: "TODO-YOUR-PUBLIC-URL-GOES-HERE" AXUM_XFORWARDED: "TODO-YOUR-PUBLIC-URL-GOES-HERE"
TODORED_RATE_LIMIT_CONNECTIONS_PER_MINUTE: 5 TODORED_RATE_LIMIT_CONNECTIONS_PER_MINUTE: 5
TODORED_RATE_LIMIT_MESSAGES_PER_MINUTE: 100 TODORED_RATE_LIMIT_MESSAGES_PER_MINUTE: 100
blue: blue:
image: "ghcr.io/steffo99/todocolors-blue" image: "ghcr.io/steffo99/todocolors-blue"
restart: unless-stopped restart: unless-stopped
caddy: caddy:
image: "caddy" image: "caddy"
restart: unless-stopped restart: unless-stopped

2
todored/Cargo.lock generated
View file

@ -1151,7 +1151,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]] [[package]]
name = "todored" name = "todored"
version = "0.3.0" version = "0.4.0"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",

View file

@ -1,6 +1,6 @@
[package] [package]
name = "todored" name = "todored"
version = "0.3.0" version = "0.4.0"
license = "EUPL-1.2" license = "EUPL-1.2"
edition = "2021" edition = "2021"

View file

@ -1,4 +1,4 @@
FROM --platform=${BUILDPLATFORM} rust:1.71-bullseye AS builder FROM --platform=${BUILDPLATFORM} rust:bullseye AS builder
ARG BUILDPLATFORM ARG BUILDPLATFORM
ARG TARGETPLATFORM ARG TARGETPLATFORM
@ -60,7 +60,7 @@ RUN \
############################################################################# #############################################################################
FROM --platform=${TARGETPLATFORM} rust:1.71-slim-bullseye AS final FROM --platform=${TARGETPLATFORM} rust:slim-bullseye AS final
WORKDIR /usr/src/todored/ WORKDIR /usr/src/todored/
COPY --from=builder \ COPY --from=builder \

View file

@ -37,8 +37,9 @@ async fn main() {
) )
); );
log::info!("Starting web server!"); let host = std::net::SocketAddr::from_str(&config::AXUM_HOST).expect("AXUM_HOST to be a valid SocketAddr");
axum::Server::bind(&std::net::SocketAddr::from_str(&config::AXUM_HOST).expect("AXUM_HOST to be a valid SocketAddr")) log::info!("Starting web server on: {host:?}");
axum::Server::bind(&host)
.serve(router.into_make_service()) .serve(router.into_make_service())
.await .await
.expect("to be able to run the Axum server"); .expect("to be able to run the Axum server");

View file

@ -7,6 +7,7 @@ pub type ResponseError = StatusCode;
pub type Response<T> = Result<T, ResponseError>; pub type Response<T> = Result<T, ResponseError>;
/// Trait to easily [`log`] function outcomes. /// Trait to easily [`log`] function outcomes.
#[allow(dead_code)]
pub(crate) trait LoggableOutcome { pub(crate) trait LoggableOutcome {
fn log_err_to_trace(self, msg: &str) -> Self; fn log_err_to_trace(self, msg: &str) -> Self;
fn log_err_to_debug(self, msg: &str) -> Self; fn log_err_to_debug(self, msg: &str) -> Self;

View file

@ -45,7 +45,7 @@ pub(crate) async fn handler(
log::trace!("Connection rate limit is: {count} / 60 s"); log::trace!("Connection rate limit is: {count} / 60 s");
if count > 0 { if count > 0 {
log::trace!("Checking rate limit..."); log::trace!("Checking rate limit...");
let result = super::limit::rate_limit_by_key(&mut handle_redis, &rate_limit_key, 1, count, 60).await; let result = super::limit::rate_limit_by_key(&mut handle_redis, rate_limit_key, 1, count, 60).await;
if result.is_err() { if result.is_err() {
return Err::<(), StatusCode>(StatusCode::BAD_REQUEST).into_response() return Err::<(), StatusCode>(StatusCode::BAD_REQUEST).into_response()
} }

View file

@ -12,7 +12,7 @@ pub async fn rate_limit_by_key(
) -> Result<usize, CloseCode> { ) -> Result<usize, CloseCode> {
log::trace!("Incrementing rate limit counter for {key:?}..."); log::trace!("Incrementing rate limit counter for {key:?}...");
let response: usize = redis::cmd("INCRBY") let response: usize = redis::cmd("INCRBY")
.arg(&key) .arg(key)
.arg(increment) .arg(increment)
.query_async::<redis::aio::Connection, usize>(rconn).await .query_async::<redis::aio::Connection, usize>(rconn).await
.log_err_to_error("Could not increase rate limit counter") .log_err_to_error("Could not increase rate limit counter")
@ -20,7 +20,7 @@ pub async fn rate_limit_by_key(
log::trace!("Refreshing rate limit counter expiration for {key:?}..."); log::trace!("Refreshing rate limit counter expiration for {key:?}...");
let _ = redis::cmd("EXPIRE") let _ = redis::cmd("EXPIRE")
.arg(&key) .arg(key)
.arg(expiration_s) .arg(expiration_s)
.query_async::<redis::aio::Connection, ()>(rconn).await .query_async::<redis::aio::Connection, ()>(rconn).await
.log_err_to_warn("Could not set expiration for rate limit counter"); .log_err_to_warn("Could not set expiration for rate limit counter");

View file

@ -1,12 +1,12 @@
pub mod structs; pub mod structs;
pub mod stream; pub mod stream;
pub(self) mod axum; mod axum;
pub(self) mod ws; mod ws;
pub(self) mod ws_receive; mod ws_receive;
pub(self) mod redis_xadd; mod redis_xadd;
pub(self) mod redis_xread; mod redis_xread;
pub(self) mod ws_send; mod ws_send;
pub(self) mod limit; mod limit;
pub(crate) use self::axum::handler as board_websocket; pub(crate) use self::axum::handler as board_websocket;

View file

@ -4,7 +4,7 @@ use crate::task::{v1, VersionedBoardChange};
pub async fn xread_to_vbc(r: StreamReadReply) -> (Vec<VersionedBoardChange>, String) { pub async fn xread_to_vbc(r: StreamReadReply) -> (Vec<VersionedBoardChange>, String) {
log::trace!("Making sure that the Redis Stream existed..."); log::trace!("Making sure that the Redis Stream existed...");
let r = r.keys.get(0); let r = r.keys.first();
let mut current_id: String = "0".to_string(); let mut current_id: String = "0".to_string();

View file

@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize};
use uuid::Uuid; use uuid::Uuid;
use crate::outcome::LoggableOutcome; use crate::outcome::LoggableOutcome;
use crate::routes::board::stream::xread_to_vbc; use crate::routes::board::stream::xread_to_vbc;
use crate::task::VersionedBoardChange::V2; use crate::task::VersionedBoardChange;
use crate::task::v2::{BoardChange, BoardState, Task}; use crate::task::latest::{BoardChange, BoardState, Task};
/// A request sent from a client to the server to perform a [`BoardAction`] on a board. /// A request sent from a client to the server to perform a [`BoardAction`] on a board.
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
@ -24,7 +24,7 @@ impl BoardRequest {
match self.action { match self.action {
BoardAction::Title(title) => { BoardAction::Title(title) => {
log::debug!("Setting board Title: {title:?}"); log::debug!("Setting board Title: {title:?}");
let operation = V2(BoardChange::Title(title)); let operation = VersionedBoardChange::new_latest(BoardChange::Title(title));
let _id = operation.store_in_redis_stream(rconn, &self.key).await; let _id = operation.store_in_redis_stream(rconn, &self.key).await;
Ok(()) Ok(())
}, },
@ -32,25 +32,25 @@ impl BoardRequest {
log::debug!("Creating Task: {task:?}"); log::debug!("Creating Task: {task:?}");
let id = Uuid::new_v4(); let id = Uuid::new_v4();
log::trace!("Assigned id {id:?} to Task: {task:?}"); log::trace!("Assigned id {id:?} to Task: {task:?}");
let operation = V2(BoardChange::Task(id, Some(task))); let operation = VersionedBoardChange::new_latest(BoardChange::Task(id, Some(task)));
let _id = operation.store_in_redis_stream(rconn, &self.key).await; let _id = operation.store_in_redis_stream(rconn, &self.key).await;
Ok(()) Ok(())
}, },
BoardAction::Task(Some(id), Some(task)) => { BoardAction::Task(Some(id), Some(task)) => {
log::debug!("Editing Task {id:?}: {task:?}"); log::debug!("Editing Task {id:?}: {task:?}");
let operation = V2(BoardChange::Task(id, Some(task))); let operation = VersionedBoardChange::new_latest(BoardChange::Task(id, Some(task)));
let _id = operation.store_in_redis_stream(rconn, &self.key).await; let _id = operation.store_in_redis_stream(rconn, &self.key).await;
Ok(()) Ok(())
}, },
BoardAction::Task(Some(id), None) => { BoardAction::Task(Some(id), None) => {
log::debug!("Deleting Task {id:?}..."); log::debug!("Deleting Task {id:?}...");
let operation = V2(BoardChange::Task(id, None)); let operation = VersionedBoardChange::new_latest(BoardChange::Task(id, None));
let _id = operation.store_in_redis_stream(rconn, &self.key).await; let _id = operation.store_in_redis_stream(rconn, &self.key).await;
Ok(()) Ok(())
}, },
BoardAction::Lock(lock) => { BoardAction::Lock(lock) => {
log::debug!("Setting board lock to: {lock:?}"); log::debug!("Setting board lock to: {lock:?}");
let operation = V2(BoardChange::Lock(lock)); let operation = VersionedBoardChange::new_latest(BoardChange::Lock(lock));
let _id = operation.store_in_redis_stream(rconn, &self.key).await; let _id = operation.store_in_redis_stream(rconn, &self.key).await;
Ok(()) Ok(())
}, },
@ -81,7 +81,7 @@ impl BoardRequest {
}); });
log::trace!("Storing new full board state in the Redis Stream..."); log::trace!("Storing new full board state in the Redis Stream...");
let id = V2(BoardChange::State(board)) let id = VersionedBoardChange::new_latest(BoardChange::State(board))
.store_in_redis_stream(rconn, &self.key).await .store_in_redis_stream(rconn, &self.key).await
.log_err_to_error("Failed to store trimmed board") .log_err_to_error("Failed to store trimmed board")
.map_err(|_| 1011u16)?; .map_err(|_| 1011u16)?;

View file

@ -3,8 +3,8 @@ use std::sync::Arc;
use deadqueue::unlimited::Queue; use deadqueue::unlimited::Queue;
use futures_util::StreamExt; use futures_util::StreamExt;
use uuid::Uuid; use uuid::Uuid;
use crate::task::VersionedBoardChange::V2; use crate::task::latest::BoardChange;
use crate::task::v2::BoardChange; use crate::task::VersionedBoardChange;
use super::{redis_xread, redis_xadd, ws_receive, ws_send}; use super::{redis_xread, redis_xadd, ws_receive, ws_send};
pub async fn handler( pub async fn handler(
@ -65,7 +65,7 @@ pub async fn handler(
log::trace!("Client UUID is: {client_uuid:?}"); log::trace!("Client UUID is: {client_uuid:?}");
log::trace!("Notifying clients of the new connection..."); log::trace!("Notifying clients of the new connection...");
let _connect_id = V2(BoardChange::Connect(client_uuid)).store_in_redis_stream(&mut main_redis, &redis_key).await; let _connect_id = VersionedBoardChange::new_latest(BoardChange::Connect(client_uuid)).store_in_redis_stream(&mut main_redis, &redis_key).await;
log::trace!("Notified clients of the new connection successfully!"); log::trace!("Notified clients of the new connection successfully!");
log::trace!("Creating synchronization structures..."); log::trace!("Creating synchronization structures...");
@ -121,7 +121,7 @@ pub async fn handler(
redis_xread_abort.abort(); redis_xread_abort.abort();
log::trace!("Notifying clients of the disconnection..."); log::trace!("Notifying clients of the disconnection...");
let _connect_id = V2(BoardChange::Disconnect(client_uuid)).store_in_redis_stream(&mut main_redis, &redis_key).await; let _connect_id = VersionedBoardChange::new_latest(BoardChange::Disconnect(client_uuid)).store_in_redis_stream(&mut main_redis, &redis_key).await;
log::trace!("Notified clients of the disconnection successfully!"); log::trace!("Notified clients of the disconnection successfully!");
log::trace!("Waiting for the last messages to be sent..."); log::trace!("Waiting for the last messages to be sent...");

View file

@ -24,7 +24,7 @@ pub async fn handler(
log::trace!("Connection rate limit is: {count} / 60 s"); log::trace!("Connection rate limit is: {count} / 60 s");
if count > 0 { if count > 0 {
log::trace!("Checking rate limit..."); log::trace!("Checking rate limit...");
let result = super::limit::rate_limit_by_key(&mut rconn, &rate_limit_key, 1, count, 60).await; let result = super::limit::rate_limit_by_key(&mut rconn, rate_limit_key, 1, count, 60).await;
if result.is_err() { if result.is_err() {
log::warn!("Hit rate limit, closing connection."); log::warn!("Hit rate limit, closing connection.");
return Err(1008u16); return Err(1008u16);

View file

@ -1,6 +1,7 @@
pub mod board; pub mod board;
pub mod structs; pub mod structs;
pub(self) mod root;
mod root;
pub(crate) use root::version as version_route; pub(crate) use root::version as version_route;
pub(crate) use root::healthcheck as healthcheck_route; pub(crate) use root::healthcheck as healthcheck_route;

View file

@ -5,23 +5,32 @@ use crate::outcome::LoggableOutcome;
pub mod v1; pub mod v1;
pub mod v2; pub mod v2;
pub mod v3;
pub use v3 as latest;
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub enum VersionedBoardChange { pub enum VersionedBoardChange {
V1(v1::BoardChange), V1(v1::BoardChange),
V2(v2::BoardChange), V2(v2::BoardChange),
V3(v3::BoardChange),
} }
impl VersionedBoardChange { impl VersionedBoardChange {
pub fn to_latest_bc(self) -> v2::BoardChange { pub fn to_latest_bc(self) -> latest::BoardChange {
match self { match self {
VersionedBoardChange::V1(bc) => bc.into(), Self::V1(bc) => Self::V2(v2::BoardChange::from(bc)).to_latest_bc(),
VersionedBoardChange::V2(bc) => bc, Self::V2(bc) => Self::V3(v3::BoardChange::from(bc)).to_latest_bc(),
Self::V3(bc) => bc,
} }
} }
pub fn to_latest_vbc(self) -> VersionedBoardChange { pub fn to_latest(self) -> VersionedBoardChange {
Self::V2(self.to_latest_bc()) Self::V3(self.to_latest_bc())
}
pub fn new_latest(bc: latest::BoardChange) -> VersionedBoardChange {
Self::V3(bc)
} }
pub(crate) async fn store_in_redis_stream(&self, rconn: &mut redis::aio::Connection, key: &str) -> Result<String, ()> { pub(crate) async fn store_in_redis_stream(&self, rconn: &mut redis::aio::Connection, key: &str) -> Result<String, ()> {

View file

@ -8,6 +8,10 @@ use chrono::serde::{ts_milliseconds, ts_milliseconds_option};
use uuid::Uuid; use uuid::Uuid;
use super::v1; use super::v1;
pub use v1::TaskIcon;
pub use v1::TaskImportance;
pub use v1::TaskPriority;
/// A change to a board's contents. /// A change to a board's contents.
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive] #[non_exhaustive]
@ -35,13 +39,13 @@ pub struct Task {
pub text: String, pub text: String,
#[serde(default)] #[serde(default)]
pub icon: v1::TaskIcon, pub icon: TaskIcon,
#[serde(default)] #[serde(default)]
pub importance: v1::TaskImportance, pub importance: TaskImportance,
#[serde(default)] #[serde(default)]
pub priority: v1::TaskPriority, pub priority: TaskPriority,
/// When the task was created. /// When the task was created.
#[serde(default, with = "ts_milliseconds")] #[serde(default, with = "ts_milliseconds")]

165
todored/src/task/v3.rs Normal file
View file

@ -0,0 +1,165 @@
use std::collections::{HashMap, HashSet};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use chrono::serde::{ts_milliseconds, ts_milliseconds_option};
use uuid::Uuid;
use super::v2;
pub use v2::TaskImportance;
pub use v2::TaskPriority;
/// A change to a board's contents.
#[derive(Clone, Debug, Serialize, Deserialize)]
#[non_exhaustive]
pub enum BoardChange {
/// Set the board's title.
Title(String),
/// Create, update, or delete the [`Task`] with the given [`Uuid`].
Task(Uuid, Option<Task>),
/// Add the given client to the connected clients list.
Connect(Uuid),
/// Remove the given client from the connected clients list.
Disconnect(Uuid),
/// Disable editing the board.
///
/// This is only a client-side change; it is not enforced on the server side!
Lock(bool),
/// Unilaterally set the [`BoardState`], overriding everything else sent so far.
State(BoardState),
}
/// A task that can be displayed on the board.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Task {
#[serde(default)]
pub text: String,
#[serde(default)]
pub icon: String,
#[serde(default)]
pub importance: TaskImportance,
#[serde(default, with = "ts_milliseconds_option")]
pub deadline: Option<DateTime<Utc>>,
/// When the task was created.
#[serde(default, with = "ts_milliseconds")]
pub created_on: DateTime<Utc>,
/// When the task was started. If [`None`], the task hasn't been started yet.
#[serde(default, with = "ts_milliseconds_option")]
pub started_on: Option<DateTime<Utc>>,
/// When the task was completed. If [`None`], the task hasn't been completed yet.
#[serde(default, with = "ts_milliseconds_option")]
pub completed_on: Option<DateTime<Utc>>,
/// When the task was journaled. If [`None`], the task hasn't been journaled yet.
#[serde(default, with = "ts_milliseconds_option")]
pub journaled_on: Option<DateTime<Utc>>,
}
impl From<v2::BoardChange> for BoardChange {
fn from(value: v2::BoardChange) -> Self {
match value {
v2::BoardChange::Title(title) => BoardChange::Title(title),
v2::BoardChange::Task(id, opt) => BoardChange::Task(id, opt.map(|task| task.into())),
v2::BoardChange::Connect(id) => BoardChange::Connect(id),
v2::BoardChange::Disconnect(id) => BoardChange::Disconnect(id),
v2::BoardChange::Lock(value) => BoardChange::Lock(value),
v2::BoardChange::State(state) => {
let tasks: HashMap<Uuid, Task> = state.tasks.into_iter().map(|(u, t)| (u, t.into())).collect();
let state = BoardState {
title: state.title,
clients: state.clients,
locked: state.locked,
tasks,
};
BoardChange::State(state)
}
}
}
}
impl From<v2::Task> for Task {
fn from(value: v2::Task) -> Self {
Self {
text: value.text,
icon: match value.icon {
v2::TaskIcon::User => "user",
v2::TaskIcon::Image => "image",
v2::TaskIcon::Envelope => "envelope",
v2::TaskIcon::Star => "star",
v2::TaskIcon::Heart => "heart",
v2::TaskIcon::Comment => "comment",
v2::TaskIcon::FaceSmile => "face-smile",
v2::TaskIcon::File => "file",
v2::TaskIcon::Bell => "bell",
v2::TaskIcon::Bookmark => "bookmark",
v2::TaskIcon::Eye => "eye",
v2::TaskIcon::Hand => "hand",
v2::TaskIcon::PaperPlane => "paper-plane",
v2::TaskIcon::Handshake => "handshake",
v2::TaskIcon::Sun => "sun",
v2::TaskIcon::Clock => "clock",
v2::TaskIcon::Circle => "circle",
v2::TaskIcon::Square => "square",
v2::TaskIcon::Building => "building",
v2::TaskIcon::Flag => "flag",
v2::TaskIcon::Moon => "moon",
}.to_string(),
importance: value.importance,
deadline: None,
created_on: value.created_on,
started_on: value.started_on,
completed_on: value.completed_on,
journaled_on: value.journaled_on,
}
}
}
/// The complete state of a board.
#[derive(Clone, Default, Debug, Serialize, Deserialize)]
pub struct BoardState {
/// The title of the board.
pub title: String,
/// The clients connected to the board.
pub clients: HashSet<Uuid>,
/// The tasks contained in the board.
pub tasks: HashMap<Uuid, Task>,
/// If the board is locked or not.
pub locked: bool,
}
impl BoardState {
/// Apply a [`BoardChange`] to the [`BoardState`], merging the two.
pub fn apply(&mut self, change: BoardChange) {
match change {
BoardChange::Title(title) => {
self.title = title;
}
BoardChange::Task(uuid, Some(task)) => {
self.tasks.insert(uuid, task);
}
BoardChange::Task(uuid, None) => {
self.tasks.remove(&uuid);
}
BoardChange::Connect(uuid) => {
self.clients.insert(uuid);
}
BoardChange::Disconnect(uuid) => {
self.clients.remove(&uuid);
}
BoardChange::Lock(lock) => {
self.locked = lock;
}
BoardChange::State(state) => {
self.title = state.title;
self.clients = state.clients;
self.tasks = state.tasks;
}
}
}
}