Compare commits
1 commit
fd790c1b94
...
168055e8f3
Author | SHA1 | Date | |
---|---|---|---|
|
168055e8f3 |
2
.directory
Normal file
|
@ -0,0 +1,2 @@
|
|||
[Desktop Entry]
|
||||
Icon=/home/steffo/Workspaces/Steffo99/todocolors/todoblue/public/favicon.ico
|
|
@ -1,26 +1,10 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<svg viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg height="100%" id="emblematic-background" version="1.1" viewBox="0 0 512 512" width="100%" xmlns="http://www.w3.org/2000/svg">
|
||||
<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 viewBox="0 0 2000 2000" xmlns="http://www.w3.org/2000/svg">
|
||||
<svg height="100%" id="emblematic-background" viewBox="0 0 2000 2000" width="100%">
|
||||
<rect fill="#0d193b" height="2000" width="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">
|
||||
<defs>
|
||||
<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 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">
|
||||
<!--! 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. -->
|
||||
<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"/>
|
||||
</svg>
|
||||
</svg>
|
||||
|
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1 KiB |
|
@ -1,15 +1,7 @@
|
|||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Run server" type="CargoCommandRunConfiguration" factoryName="Cargo Command">
|
||||
<option name="buildProfileId" value="dev" />
|
||||
<option name="command" value="run" />
|
||||
<option name="workingDirectory" value="file://$PROJECT_DIR$/todored" />
|
||||
<envs>
|
||||
<env name="AXUM_HOST" value="0.0.0.0:8080" />
|
||||
<env name="REDIS_CONN" value="redis://127.0.0.1:6379/" />
|
||||
<env name="RUST_LOG" value="todored" />
|
||||
<env name="TODORED_RATE_LIMIT_CONNECTIONS_PER_MINUTE" value="0" />
|
||||
<env name="TODORED_RATE_LIMIT_MESSAGES_PER_MINUTE" value="0" />
|
||||
</envs>
|
||||
<option name="emulateTerminal" value="true" />
|
||||
<option name="channel" value="DEFAULT" />
|
||||
<option name="requiredFeatures" value="true" />
|
||||
|
@ -17,6 +9,13 @@
|
|||
<option name="withSudo" value="false" />
|
||||
<option name="buildTarget" value="REMOTE" />
|
||||
<option name="backtrace" value="SHORT" />
|
||||
<envs>
|
||||
<env name="AXUM_HOST" value="0.0.0.0:8080" />
|
||||
<env name="REDIS_CONN" value="redis://127.0.0.1:6379/" />
|
||||
<env name="RUST_LOG" value="todored" />
|
||||
<env name="TODORED_RATE_LIMIT_CONNECTIONS_PER_MINUTE" value="0" />
|
||||
<env name="TODORED_RATE_LIMIT_MESSAGES_PER_MINUTE" value="0" />
|
||||
</envs>
|
||||
<option name="isRedirectInput" value="false" />
|
||||
<option name="redirectInputPath" value="" />
|
||||
<method v="2">
|
||||
|
|
Before Width: | Height: | Size: 429 KiB |
18
README.md
|
@ -1,12 +1,8 @@
|
|||
<div align="center">
|
||||
# ![](media/icon.png) Todocolors
|
||||
|
||||
# ![](.media/icon.png) Todocolors
|
||||
A self-hostable multiplayer todo app with Redis, Rust, WebSockets and Next.js.
|
||||
|
||||
A self-hostable multiplayer todo app
|
||||
|
||||
</div>
|
||||
|
||||
> [!Warning]
|
||||
> Warning:
|
||||
>
|
||||
> This project is currently a prototype.
|
||||
>
|
||||
|
@ -14,15 +10,11 @@ A self-hostable multiplayer todo app
|
|||
>
|
||||
> 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.ù
|
||||
|
||||
## Links
|
||||
|
||||
[![Website](https://img.shields.io/website?url=https%3A%2F%2Ftodo.steffo.eu%2F)](https://todo.steffo.eu/)
|
||||
> Use and contribute at your own risk.
|
||||
|
||||
## 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
|
||||
|
||||
|
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
Before Width: | Height: | Size: 677 KiB After Width: | Height: | Size: 677 KiB |
2
todoblue/.gitignore
vendored
|
@ -1,7 +1,5 @@
|
|||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
.npmrc
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
FROM node AS base
|
||||
FROM node:20 AS base
|
||||
|
||||
FROM base AS builder
|
||||
WORKDIR /usr/src/todoblue
|
||||
|
||||
COPY ./package.json ./yarn.lock .npmrc ./
|
||||
COPY ./package.json ./yarn.lock ./
|
||||
RUN yarn install --frozen-lockfile
|
||||
|
||||
COPY ./ ./
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "todoblue",
|
||||
"version": "0.4.0",
|
||||
"version": "0.3.0",
|
||||
"license": "EUPL-1.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
|
@ -10,27 +10,27 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@awesome.me/kit-dfe340c874": "^1.0.10",
|
||||
"@fortawesome/fontawesome-svg-core": "*",
|
||||
"@fortawesome/react-fontawesome": "*",
|
||||
"@steffo/bluelib": "*",
|
||||
"any-date-parser": "^1.5.4",
|
||||
"classnames": "*",
|
||||
"client-only": "*",
|
||||
"i18next": "*",
|
||||
"i18next-resources-to-backend": "*",
|
||||
"negotiator": "*",
|
||||
"next": "*",
|
||||
"react": "*",
|
||||
"react-dom": "*",
|
||||
"server-only": "*",
|
||||
"use-local-storage": "*"
|
||||
"@fortawesome/fontawesome-svg-core": "^6.4.0",
|
||||
"@fortawesome/free-regular-svg-icons": "^6.4.0",
|
||||
"@fortawesome/free-solid-svg-icons": "^6.4.0",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@steffo/bluelib": "^9.0.1",
|
||||
"@types/node": "20.4.5",
|
||||
"@types/react": "18.2.17",
|
||||
"@types/react-dom": "18.2.7",
|
||||
"classnames": "^2.3.2",
|
||||
"client-only": "^0.0.1",
|
||||
"i18next": "^23.4.2",
|
||||
"i18next-resources-to-backend": "^1.1.4",
|
||||
"negotiator": "^0.6.3",
|
||||
"next": "13.5.0",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"server-only": "^0.0.1",
|
||||
"typescript": "5.1.6",
|
||||
"use-local-storage": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/negotiator": "*",
|
||||
"@types/node": "*",
|
||||
"@types/react": "*",
|
||||
"@types/react-dom": "*",
|
||||
"typescript": "*"
|
||||
"@types/negotiator": "^0.6.1"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
"display": "standalone",
|
||||
"theme_color": "#0d193b",
|
||||
"background_color": "#0c193b",
|
||||
"description": "Multiplayer todo app",
|
||||
"description": "Self-hostable multiplayer todo app",
|
||||
"categories": ["productivity"],
|
||||
"icons": [
|
||||
{
|
||||
|
|
|
@ -30,6 +30,27 @@
|
|||
"taskStatusInProgress": "In progress",
|
||||
"taskStatusComplete": "Complete",
|
||||
"taskStatusJournaled": "Journaled",
|
||||
"taskIconBookmark": "Bookmark",
|
||||
"taskIconCircle": "Circle",
|
||||
"taskIconSquare": "Square",
|
||||
"taskIconHeart": "Heart",
|
||||
"taskIconStar": "Star",
|
||||
"taskIconSun": "Sun",
|
||||
"taskIconMoon": "Moon",
|
||||
"taskIconEye": "Eye",
|
||||
"taskIconHand": "Hand",
|
||||
"taskIconHandshake": "Handshake",
|
||||
"taskIconFaceSmile": "Smile",
|
||||
"taskIconUser": "User",
|
||||
"taskIconComment": "Comment",
|
||||
"taskIconEnvelope": "Envelope",
|
||||
"taskIconFile": "File",
|
||||
"taskIconPaperPlane": "Paper plane",
|
||||
"taskIconBuilding": "Building",
|
||||
"taskIconFlag": "Flag",
|
||||
"taskIconBell": "Bell",
|
||||
"taskIconClock": "Clock",
|
||||
"taskIconImage": "Image",
|
||||
"taskButtonJournal": "Move this task to the Journal",
|
||||
"taskButtonUnjournal": "Remove this task from the Journal",
|
||||
"taskButtonDelete": "Delete this task",
|
||||
|
|
|
@ -30,6 +30,27 @@
|
|||
"taskStatusInProgress": "In corso",
|
||||
"taskStatusComplete": "Completato",
|
||||
"taskStatusJournaled": "Salvato nel Diario",
|
||||
"taskIconBookmark": "Segnalibro",
|
||||
"taskIconCircle": "Cerchio",
|
||||
"taskIconSquare": "Quadrato",
|
||||
"taskIconHeart": "Cuore",
|
||||
"taskIconStar": "Stella",
|
||||
"taskIconSun": "Sole",
|
||||
"taskIconMoon": "Luna",
|
||||
"taskIconEye": "Occhio",
|
||||
"taskIconHand": "Mano",
|
||||
"taskIconHandshake": "Stretta di mano",
|
||||
"taskIconFaceSmile": "Sorriso",
|
||||
"taskIconUser": "Utente",
|
||||
"taskIconComment": "Commento",
|
||||
"taskIconEnvelope": "Busta",
|
||||
"taskIconFile": "File",
|
||||
"taskIconPaperPlane": "Aeroplano di carta",
|
||||
"taskIconBuilding": "Edificio",
|
||||
"taskIconFlag": "Bandiera",
|
||||
"taskIconBell": "Campana",
|
||||
"taskIconClock": "Orologio",
|
||||
"taskIconImage": "Immagine",
|
||||
"taskButtonJournal": "Sposta questa attività nel Diario",
|
||||
"taskButtonUnjournal": "Rimuovi questa attività dal Diario",
|
||||
"taskButtonDelete": "Elimina questa attività",
|
||||
|
|
|
@ -51,7 +51,7 @@ export function StarredProvider({children}: {children: ReactNode}) {
|
|||
}, [])
|
||||
|
||||
const isStarred = useCallback((value: string) => {
|
||||
return starred.includes(value)
|
||||
return starred.indexOf(value) >= 0
|
||||
}, [starred])
|
||||
|
||||
return (
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
"use client";
|
||||
|
||||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faLock} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import classNames from "classnames"
|
||||
import {useRouter} from "next/navigation"
|
||||
|
@ -71,7 +71,7 @@ export function CreatePrivateBoardPanel({lang}: {lang: string}) {
|
|||
onSubmit={createBoardValidated}
|
||||
>
|
||||
<h3>
|
||||
<FontAwesomeIcon icon={fas.faLock} size={"1x"}/>
|
||||
<FontAwesomeIcon icon={faLock} size={"1x"}/>
|
||||
{" "}
|
||||
{t("createPrivateBoardTitle")}
|
||||
</h3>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useLowerKebabifier} from "@/app/(utils)/(kebab)"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faGlobe} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {useRouter} from "next/navigation"
|
||||
|
@ -32,7 +32,7 @@ export function CreatePublicBoardPanel({lang}: {lang: string}) {
|
|||
onSubmit={createBoardValidated}
|
||||
>
|
||||
<h3>
|
||||
<FontAwesomeIcon icon={fas.faGlobe}/>
|
||||
<FontAwesomeIcon icon={faGlobe}/>
|
||||
{" "}
|
||||
{t("createPublicBoardTitle")}
|
||||
</h3>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useLowerKebabifier} from "@/app/(utils)/(kebab)"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faKey} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {useRouter} from "next/navigation"
|
||||
|
@ -32,7 +32,7 @@ export function KnownBoardsPanel({lang}: {lang: string}) {
|
|||
onSubmit={moveToBoardValidated}
|
||||
>
|
||||
<h3>
|
||||
<FontAwesomeIcon icon={fas.faKey}/>
|
||||
<FontAwesomeIcon icon={faKey}/>
|
||||
{" "}
|
||||
{t("existingKnownBoardsTitle")}
|
||||
</h3>
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import {useClientTranslation} from "@/app/(i18n)/client"
|
||||
import {useStarredConsumer} from "@/app/[lang]/(layout)/(contextStarred)"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faStar} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import Link from "next/link"
|
||||
|
@ -66,7 +66,7 @@ export function StarredBoardsPanel({lang}: {lang: string}) {
|
|||
"box": true,
|
||||
})}>
|
||||
<h3>
|
||||
<FontAwesomeIcon icon={fas.faStar}/>
|
||||
<FontAwesomeIcon icon={faStar}/>
|
||||
{" "}
|
||||
{t("existingStarredBoardsTitle")}
|
||||
</h3>
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)/TaskIcon"
|
||||
import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)/TaskImportance"
|
||||
import {TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)/TaskPriority"
|
||||
|
||||
|
||||
export type Task = {
|
||||
text: string,
|
||||
icon: string,
|
||||
icon: TaskIcon,
|
||||
importance: TaskImportance,
|
||||
deadline: number | null,
|
||||
priority: TaskPriority,
|
||||
created_on: number | null,
|
||||
started_on: number | null,
|
||||
completed_on: number | null,
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* **Enum** of priority levels a {@link Task} may have.
|
||||
*/
|
||||
export enum TaskPriority {
|
||||
Highest = "Highest",
|
||||
High = "High",
|
||||
Normal = "Normal",
|
||||
Low = "Low",
|
||||
Lowest = "Lowest",
|
||||
}
|
|
@ -1,4 +1,7 @@
|
|||
export {TaskIcon} from "./TaskIcon"
|
||||
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 {TaskId} from "./TaskId"
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
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,
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
import {ICONS} from "@/app/[lang]/board/[board]/(api)/(task)/TaskIcon"
|
||||
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {TaskEditorIcon} from "@/app/[lang]/board/[board]/(page)/(edit)/(task)/TaskEditorIcon"
|
||||
import {TaskEditorInput} from "@/app/[lang]/board/[board]/(page)/(edit)/(task)/TaskEditorInput"
|
||||
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/convertTTS"
|
||||
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/taskToString"
|
||||
import {TaskContainer} from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer"
|
||||
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
|
||||
import {useTaskEditor} from "@/app/[lang]/board/[board]/(page)/useTaskEditor"
|
||||
|
@ -12,33 +12,56 @@ import {SyntheticEvent, useCallback} from "react"
|
|||
import style from "./TaskEditor.module.css"
|
||||
|
||||
|
||||
export function TaskEditor({lang, t, className, editorHook: {input, setInput, task, setTask}}: { lang: string, t: TFunction, className?: string, editorHook: ReturnType<typeof useTaskEditor> }) {
|
||||
const ICON_SEQUENCE = [
|
||||
TaskIcon.Bookmark,
|
||||
TaskIcon.Circle,
|
||||
TaskIcon.Square,
|
||||
TaskIcon.Heart,
|
||||
TaskIcon.Star,
|
||||
TaskIcon.Sun,
|
||||
TaskIcon.Moon,
|
||||
TaskIcon.Eye,
|
||||
TaskIcon.Hand,
|
||||
TaskIcon.Handshake,
|
||||
TaskIcon.FaceSmile,
|
||||
TaskIcon.User,
|
||||
TaskIcon.Comment,
|
||||
TaskIcon.Envelope,
|
||||
TaskIcon.File,
|
||||
TaskIcon.PaperPlane,
|
||||
TaskIcon.Building,
|
||||
TaskIcon.Flag,
|
||||
TaskIcon.Bell,
|
||||
TaskIcon.Clock,
|
||||
TaskIcon.Image,
|
||||
]
|
||||
|
||||
|
||||
export function TaskEditor({t, className, editorHook: {input, setInput, task, setTask}}: {t: TFunction, className?: string, editorHook: ReturnType<typeof useTaskEditor>}) {
|
||||
const {isReady, sendRequest} = useBoardConsumer()
|
||||
|
||||
const nextIcon = useCallback((e: SyntheticEvent<HTMLButtonElement>) => {
|
||||
if("key" in e && typeof e["key"] === "string") {
|
||||
if(![" "].includes(e.key)) {
|
||||
return
|
||||
return;
|
||||
}
|
||||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setTask({...task, icon: ICONS[Math.floor(Math.random() * ICONS.length)]})
|
||||
let index = ICON_SEQUENCE.indexOf(task.icon) + 1;
|
||||
if(index === ICON_SEQUENCE.length) index = 0;
|
||||
setTask({...task, icon: ICON_SEQUENCE[index]})
|
||||
}, [task])
|
||||
|
||||
const submitTask = useCallback((e: SyntheticEvent<HTMLInputElement>) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if(task.text === "") {
|
||||
return
|
||||
}
|
||||
if(task.text === "") return
|
||||
sendRequest({"Task": [null, task]})
|
||||
setInput(taskToString({...task, text: ""}, lang))
|
||||
setInput(taskToString({...task, text: ""}))
|
||||
}, [sendRequest, task, setInput])
|
||||
|
||||
if(!isReady) {
|
||||
return null
|
||||
}
|
||||
if(!isReady) return null
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -47,7 +70,7 @@ export function TaskEditor({lang, t, className, editorHook: {input, setInput, ta
|
|||
<TaskContainer
|
||||
role={"form"}
|
||||
importance={task.importance}
|
||||
deadline={task.deadline}
|
||||
priority={task.priority}
|
||||
status={TaskSimplifiedStatus.NonExistent}
|
||||
onSubmit={submitTask}
|
||||
>
|
||||
|
|
|
@ -1,13 +1,38 @@
|
|||
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskIconComponent} from "@/app/[lang]/board/[board]/(page)/(task)/TaskIconComponent"
|
||||
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
|
||||
import {TFunction} from "i18next"
|
||||
import {SyntheticEvent} from "react"
|
||||
|
||||
|
||||
export function TaskEditorIcon({t, icon, nextIcon}: {t: TFunction, icon: string, nextIcon: (e: SyntheticEvent<HTMLButtonElement>) => void}) {
|
||||
const ICON_TO_KEY = {
|
||||
[TaskIcon.Bookmark]: "taskIconBookmark",
|
||||
[TaskIcon.Circle]: "taskIconCircle",
|
||||
[TaskIcon.Square]: "taskIconSquare",
|
||||
[TaskIcon.Heart]: "taskIconHeart",
|
||||
[TaskIcon.Star]: "taskIconStar",
|
||||
[TaskIcon.Sun]: "taskIconSun",
|
||||
[TaskIcon.Moon]: "taskIconMoon",
|
||||
[TaskIcon.Eye]: "taskIconEye",
|
||||
[TaskIcon.Hand]: "taskIconHand",
|
||||
[TaskIcon.Handshake]: "taskIconHandshake",
|
||||
[TaskIcon.FaceSmile]: "taskIconFaceSmile",
|
||||
[TaskIcon.User]: "taskIconUser",
|
||||
[TaskIcon.Comment]: "taskIconComment",
|
||||
[TaskIcon.Envelope]: "taskIconEnvelope",
|
||||
[TaskIcon.File]: "taskIconFile",
|
||||
[TaskIcon.PaperPlane]: "taskIconPaperPlane",
|
||||
[TaskIcon.Building]: "taskIconBuilding",
|
||||
[TaskIcon.Flag]: "taskIconFlag",
|
||||
[TaskIcon.Bell]: "taskIconBell",
|
||||
[TaskIcon.Clock]: "taskIconClock",
|
||||
[TaskIcon.Image]: "taskIconImage",
|
||||
}
|
||||
|
||||
export function TaskEditorIcon({t, icon, nextIcon}: {t: TFunction, icon: TaskIcon, nextIcon: (e: SyntheticEvent<HTMLButtonElement>) => void}) {
|
||||
return (
|
||||
<TaskIconComponent
|
||||
title={icon}
|
||||
title={t(ICON_TO_KEY[icon])}
|
||||
icon={icon}
|
||||
status={TaskSimplifiedStatus.NonExistent}
|
||||
onInteract={nextIcon}
|
||||
|
|
|
@ -6,14 +6,14 @@ import {TFunction} from "i18next"
|
|||
import style from "./BoardEditor.module.css"
|
||||
|
||||
|
||||
export function BoardEditor({className, lang, t, editorHook}: {className?: string, lang: string, t: TFunction, editorHook: ReturnType<typeof useTaskEditor>}) {
|
||||
export function BoardEditor({className, t, editorHook}: {className?: string, t: TFunction, editorHook: ReturnType<typeof useTaskEditor>}) {
|
||||
const {boardState: {locked}} = useBoardConsumer()
|
||||
|
||||
if(locked) return null;
|
||||
|
||||
return (
|
||||
<section className={cn(style.boardEditor, className)}>
|
||||
<TaskEditor lang={lang} t={t} editorHook={editorHook}/>
|
||||
<TaskEditor t={t} editorHook={editorHook}/>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,65 +0,0 @@
|
|||
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,
|
||||
}
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
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
|
|
@ -0,0 +1,8 @@
|
|||
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
|
|
@ -0,0 +1,75 @@
|
|||
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,
|
||||
}
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
export const DEADLINE_GLYPH_START = "["
|
||||
export const DEADLINE_GLYPH_END = "]"
|
||||
|
||||
export const DEADLINE_GLYPH_RE = /\[(.+?)]\s?/
|
||||
|
||||
export const DEADLINE_DEFAULT = null
|
|
@ -1,5 +0,0 @@
|
|||
export const ICON_GLYPH = "#"
|
||||
|
||||
export const ICON_GLYPH_RE = /#([A-Za-z0-9-]+)\s?/
|
||||
|
||||
export const ICON_DEFAULT = "Circle"
|
|
@ -0,0 +1,47 @@
|
|||
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
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faUsers} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
@ -16,7 +16,7 @@ export function ConnectedClientsButton({t}: {t: TFunction}) {
|
|||
title={t("privacyButtonTitle")}
|
||||
className={cn(style.block, style.singleBlock)}
|
||||
>
|
||||
<FontAwesomeIcon icon={fas.faUsers} size={"2xs"}/>
|
||||
<FontAwesomeIcon icon={faUsers} size={"2xs"}/>
|
||||
|
||||
{clients.length}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faObjectGroup} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
@ -17,7 +17,7 @@ export function CycleGroupingButton({t, next}: {t: TFunction, next: () => void})
|
|||
onClick={next}
|
||||
className={cn(style.block, style.singleBlock)}
|
||||
>
|
||||
<FontAwesomeIcon icon={fas.faObjectGroup}/>
|
||||
<FontAwesomeIcon icon={faObjectGroup}/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faArrowDownWideShort} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
@ -17,7 +17,7 @@ export function CycleSortingButton({t, next}: {t: TFunction, next: () => void})
|
|||
onClick={next}
|
||||
className={cn(style.block, style.singleBlock)}
|
||||
>
|
||||
<FontAwesomeIcon icon={fas.faArrowDownWideShort}/>
|
||||
<FontAwesomeIcon icon={faArrowDownWideShort}/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faHouse} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
@ -17,7 +17,7 @@ export function NavigateHomeButton({t}: {t: TFunction}) {
|
|||
onClick={goHome}
|
||||
className={cn(style.block, style.singleBlock)}
|
||||
>
|
||||
<FontAwesomeIcon icon={fas.faHouse}/>
|
||||
<FontAwesomeIcon icon={faHouse}/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {useBoardMetadataEditor} from "@/app/[lang]/board/[board]/(page)/useBoardMetadataEditor"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faFloppyDisk, faPencil} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
@ -19,7 +19,7 @@ export function ToggleEditingButton({t, metadataHook}: {t: TFunction, metadataHo
|
|||
onClick={metadataHook.toggleEditingMetadata}
|
||||
className={cn(style.block, style.singleBlock)}
|
||||
>
|
||||
<FontAwesomeIcon icon={fas[metadataHook.isEditingMetadata ? "faFloppyDisk" : "faPencil"]}/>
|
||||
<FontAwesomeIcon icon={metadataHook.isEditingMetadata ? faFloppyDisk : faPencil}/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {LockBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faLock, faLockOpen} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
@ -25,7 +25,7 @@ export function ToggleLockedButton({t}: {t: TFunction}) {
|
|||
onClick={toggleLock}
|
||||
className={cn(style.block, style.singleBlock)}
|
||||
>
|
||||
<FontAwesomeIcon icon={fas[locked ? "faLock" : "faLockOpen"]}/>
|
||||
<FontAwesomeIcon icon={locked ? faLock : faLockOpen}/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
"use client";
|
||||
|
||||
import {useStarredConsumer} from "@/app/[lang]/(layout)/(contextStarred)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {far, fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faStar as faStarRegular} from "@fortawesome/free-regular-svg-icons"
|
||||
import {faStar as faStarSolid} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
@ -20,7 +19,7 @@ export function ToggleStarredButton({t}: {t: TFunction}) {
|
|||
onClick={() => toggleStarred(boardName)}
|
||||
className={cn(style.block, style.singleBlock)}
|
||||
>
|
||||
<FontAwesomeIcon icon={(thisIsStarred ? fas : far)["faStar"]}/>
|
||||
<FontAwesomeIcon icon={thisIsStarred ? faStarSolid : faStarRegular}/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import {TrimBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faScissors} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {TFunction} from "i18next"
|
||||
|
@ -25,7 +25,7 @@ export function TrimButton({t}: {t: TFunction}) {
|
|||
onClick={requestTrim}
|
||||
className={cn(style.block, style.singleBlock)}
|
||||
>
|
||||
<FontAwesomeIcon icon={fas.faScissors}/>
|
||||
<FontAwesomeIcon icon={faScissors}/>
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import {IconDefinition} from "@fortawesome/fontawesome-svg-core"
|
||||
import {IconDefinition} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {SyntheticEvent} from "react"
|
||||
|
|
|
@ -3,9 +3,6 @@
|
|||
align-items: center;
|
||||
min-width: 240px;
|
||||
min-height: 48px;
|
||||
border-style: solid;
|
||||
border-width: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
.taskImportanceHighest {
|
||||
|
@ -48,32 +45,29 @@
|
|||
font-weight: 200;
|
||||
}
|
||||
|
||||
.taskDeadlineHour {
|
||||
border-left-width: 4px;
|
||||
padding-left: 4px;
|
||||
.taskPriorityHighest {
|
||||
border: 4px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.45);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.taskDeadlineDay {
|
||||
border-left-width: 3px;
|
||||
padding-left: 5px;
|
||||
.taskPriorityHigh {
|
||||
border: 3px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.25);
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.taskDeadlineWeek {
|
||||
border-left-width: 2px;
|
||||
padding-left: 6px;
|
||||
.taskPriorityNormal {
|
||||
border: 2px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.15);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.taskDeadlineMonth {
|
||||
border-left-width: 1px;
|
||||
padding-left: 7px;
|
||||
.taskPriorityLow {
|
||||
border: 1px solid hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.15);
|
||||
padding: 7px;
|
||||
}
|
||||
|
||||
.taskDeadlineFuture {
|
||||
border-color: hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 0.25);
|
||||
}
|
||||
|
||||
.taskDeadlinePast {
|
||||
border-color: hsl(var(--bhsl-current-hue) var(--bhsl-current-saturation) var(--bhsl-current-lightness) / 1.00);
|
||||
.taskPriorityLowest {
|
||||
border: 0;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@keyframes inProgress {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
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 {ComponentPropsWithoutRef} from "react"
|
||||
import style from "./TaskContainer.module.css"
|
||||
|
@ -10,14 +9,12 @@ export type TaskContainerProps = {
|
|||
className?: string,
|
||||
role: "article" | "form",
|
||||
importance: TaskImportance,
|
||||
deadline: number | null,
|
||||
priority: TaskPriority,
|
||||
status: TaskSimplifiedStatus,
|
||||
} & ComponentPropsWithoutRef<"article">
|
||||
|
||||
|
||||
export function TaskContainer({role, className, importance, deadline, status, ...props}: TaskContainerProps) {
|
||||
const {delta, deltaAbs} = useTimeDelta(deadline ?? undefined, 30 * 1000)
|
||||
|
||||
export function TaskContainer({role, className, importance, priority, status, ...props}: TaskContainerProps) {
|
||||
const fullProps = {
|
||||
className: cn({
|
||||
"panel": true,
|
||||
|
@ -28,13 +25,11 @@ export function TaskContainer({role, className, importance, deadline, status, ..
|
|||
[style.taskImportanceNormal]: importance === TaskImportance.Normal,
|
||||
[style.taskImportanceLow]: importance === TaskImportance.Low,
|
||||
[style.taskImportanceLowest]: importance === TaskImportance.Lowest,
|
||||
[style.taskDeadlineNone]: deadline === null,
|
||||
[style.taskDeadlineHour]: deltaAbs !== undefined && deltaAbs < 60 * 60 * 1000,
|
||||
[style.taskDeadlineDay]: deltaAbs !== undefined && 60 * 60 * 1000 <= deltaAbs && deltaAbs < 24 * 60 * 60 * 1000,
|
||||
[style.taskDeadlineWeek]: deltaAbs !== undefined && 24 * 60 * 60 * 1000 <= deltaAbs && deltaAbs < 7 * 24 * 60 * 60 * 1000,
|
||||
[style.taskDeadlineMonth]: deltaAbs !== undefined && deltaAbs >= 7 * 24 * 60 * 60 * 1000,
|
||||
[style.taskDeadlineFuture]: delta !== undefined && delta >= 0,
|
||||
[style.taskDeadlinePast]: delta !== undefined && delta < 0,
|
||||
[style.taskPriorityHighest]: priority === TaskPriority.Highest,
|
||||
[style.taskPriorityHigh]: priority === TaskPriority.High,
|
||||
[style.taskPriorityNormal]: priority === TaskPriority.Normal,
|
||||
[style.taskPriorityLow]: priority === TaskPriority.Low,
|
||||
[style.taskPriorityLowest]: priority === TaskPriority.Lowest,
|
||||
[style.taskStatusNonExistent]: status === TaskSimplifiedStatus.NonExistent,
|
||||
[style.taskStatusUnfinished]: status === TaskSimplifiedStatus.Unfinished,
|
||||
[style.taskStatusInProgress]: status === TaskSimplifiedStatus.InProgress,
|
||||
|
|
|
@ -2,7 +2,3 @@
|
|||
border-radius: var(--b-border-radius);
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.taskDescriptionSource {
|
||||
font-family: monospace;
|
||||
}
|
||||
|
|
|
@ -4,16 +4,14 @@ import style from "./TaskDescription.module.css"
|
|||
|
||||
type TaskDescriptionProps = {
|
||||
className?: string,
|
||||
isSource?: boolean,
|
||||
text: string,
|
||||
}
|
||||
|
||||
export function TaskDescription({className, isSource, text}: TaskDescriptionProps) {
|
||||
export function TaskDescription({className, text}: TaskDescriptionProps) {
|
||||
return (
|
||||
<div className={cn({
|
||||
"taskDescription": true,
|
||||
[style.taskDescription]: true,
|
||||
[style.taskDescriptionSource]: isSource,
|
||||
}, className)} tabIndex={0}>
|
||||
{text}
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
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 {fal, far, fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {IconPack} from "@fortawesome/fontawesome-svg-core"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import cn from "classnames"
|
||||
import {SyntheticEvent} from "react"
|
||||
|
@ -10,18 +9,11 @@ import style from "./TaskIconComponent.module.css"
|
|||
type TaskIconProps = {
|
||||
className?: string,
|
||||
title: string,
|
||||
icon: string,
|
||||
icon: TaskIcon,
|
||||
status: TaskSimplifiedStatus,
|
||||
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) {
|
||||
const clickable = !!onInteract;
|
||||
|
@ -31,7 +23,6 @@ export function TaskIconComponent({className, title, icon, status, onInteract}:
|
|||
className={cn({
|
||||
[style.taskIconComponent]: true,
|
||||
[style.taskIconComponentClickable]: clickable,
|
||||
"fade": status === TaskSimplifiedStatus.Complete,
|
||||
}, className)}
|
||||
type={"button"}
|
||||
title={title}
|
||||
|
@ -41,7 +32,7 @@ export function TaskIconComponent({className, title, icon, status, onInteract}:
|
|||
>
|
||||
<FontAwesomeIcon
|
||||
size={"lg"}
|
||||
icon={STATUS_TO_PREFIX[status][`fa${icon}`]}
|
||||
icon={[TaskSimplifiedStatus.Complete, TaskSimplifiedStatus.Journaled].includes(status) ? TASK_ICON_TO_FONTAWESOME_SOLID[icon] : TASK_ICON_TO_FONTAWESOME_REGULAR[icon]}
|
||||
beatFade={status === TaskSimplifiedStatus.InProgress}
|
||||
/>
|
||||
</button>
|
||||
|
|
|
@ -1,32 +0,0 @@
|
|||
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}
|
||||
}
|
|
@ -1,9 +1,8 @@
|
|||
import {ColumningMode} from "@/app/[lang]/board/[board]/(page)/(view)/(columning)/ColumningMode"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {IconDefinition} from "@fortawesome/fontawesome-svg-core"
|
||||
import {faTableColumns, faTableList} from "@fortawesome/free-solid-svg-icons"
|
||||
|
||||
|
||||
export const COLUMNING_MODE_TO_ICON: {[m in ColumningMode]: IconDefinition} = {
|
||||
[ColumningMode.SingleColumn]: fas.faTableList,
|
||||
[ColumningMode.MultiColumn]: fas.faTableColumns,
|
||||
export const COLUMNING_MODE_TO_ICON = {
|
||||
[ColumningMode.SingleColumn]: faTableList,
|
||||
[ColumningMode.MultiColumn]: faTableColumns,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
export enum GroupingMode {
|
||||
ByImportance,
|
||||
ByPriority,
|
||||
ByStatus,
|
||||
ByIcon,
|
||||
Journal,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {ICONS} from "@/app/[lang]/board/[board]/(api)/(task)/TaskIcon"
|
||||
import {TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode"
|
||||
import {TaskGroup} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/TaskGroup"
|
||||
|
||||
|
@ -12,25 +11,60 @@ const TASK_IMPORTANCE_TO_VALUE = {
|
|||
[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 = {
|
||||
"Journaled": 3,
|
||||
"Complete": 2,
|
||||
"InProgress": 0,
|
||||
"Unfinished": 1,
|
||||
"InProgress": 1,
|
||||
"Unfinished": 0,
|
||||
}
|
||||
|
||||
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 = {
|
||||
[GroupingMode.ByImportance]: function sortGroupsByImportance(a: TaskGroup<TaskImportance>, b: TaskGroup<TaskImportance>): number {
|
||||
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 {
|
||||
// @ts-ignore
|
||||
return TASK_STATUS_TO_VALUE[a.k] - TASK_STATUS_TO_VALUE[b.k]
|
||||
},
|
||||
[GroupingMode.ByIcon]: function sortGroupsByIcon(a: TaskGroup<string>, b: TaskGroup<string>): number {
|
||||
return ICONS.indexOf(a.k as any) - ICONS.indexOf(b.k as any)
|
||||
[GroupingMode.ByIcon]: function sortGroupsByIcon(a: TaskGroup<TaskIcon>, b: TaskGroup<TaskIcon>): number {
|
||||
return TASK_ICON_TO_VALUE[a.k] - TASK_ICON_TO_VALUE[b.k]
|
||||
},
|
||||
[GroupingMode.Journal]: function sortGroupsAlphabetically(a: TaskGroup<string>, b: TaskGroup<string>): number {
|
||||
[GroupingMode.Journal]: function sortGroupsAlphabetically(a: TaskGroup<TaskIcon>, b: TaskGroup<TaskIcon>): number {
|
||||
return b.k.localeCompare(a.k)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,19 +1,24 @@
|
|||
import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode"
|
||||
|
||||
|
||||
export const GROUPING_MODE_TO_TASK_GROUPER_FUNCTION = {
|
||||
[GroupingMode.ByImportance]: function groupTasksByImportance(t: TaskWithId): TaskImportance | null {
|
||||
if(t[1].journaled_on) return null
|
||||
return t[1].importance
|
||||
},
|
||||
[GroupingMode.ByPriority]: function groupTasksByPriority(t: TaskWithId): TaskPriority | null {
|
||||
if(t[1].journaled_on) return null
|
||||
return t[1].priority
|
||||
},
|
||||
[GroupingMode.ByStatus]: function groupTasksByStatus(t: TaskWithId): string | null {
|
||||
if(t[1].journaled_on) return null
|
||||
else if(t[1].completed_on) return "Complete"
|
||||
else if(t[1].started_on) return "InProgress"
|
||||
else return "Unfinished"
|
||||
},
|
||||
[GroupingMode.ByIcon]: function groupTasksByIcon(t: TaskWithId): string | null {
|
||||
[GroupingMode.ByIcon]: function groupTasksByIcon(t: TaskWithId): TaskIcon | null {
|
||||
if(t[1].journaled_on) return null
|
||||
return t[1].icon
|
||||
},
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TASK_ICON_TO_FONTAWESOME_REGULAR, TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import style from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer.module.css"
|
||||
import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)/GroupingMode"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import {TFunction} from "i18next"
|
||||
import {ReactNode} from "react"
|
||||
|
@ -49,6 +48,41 @@ 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 {
|
||||
switch(k) {
|
||||
case "Unfinished":
|
||||
|
@ -72,12 +106,12 @@ export const GROUPING_MODE_TO_TITLE_COMPONENT: {[key in GroupingMode]: (t: Title
|
|||
}
|
||||
},
|
||||
|
||||
[GroupingMode.ByIcon]: function TitleFromIcon({k}: { t: TFunction, k: string }): ReactNode {
|
||||
[GroupingMode.ByIcon]: function TitleFromIcon({k}: { t: TFunction, k: TaskIcon }): ReactNode {
|
||||
return (
|
||||
<span>
|
||||
<FontAwesomeIcon icon={fas[`fa${k}`]}/>
|
||||
<FontAwesomeIcon icon={TASK_ICON_TO_FONTAWESOME_REGULAR[k]}/>
|
||||
|
||||
{k}
|
||||
{TaskIcon[k]}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
export enum SortingMode {
|
||||
ByPriority,
|
||||
ByImportance,
|
||||
ByDeadline,
|
||||
ByIcon,
|
||||
ByText,
|
||||
ByStatus,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import {TaskImportance} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {ICONS} from "@/app/[lang]/board/[board]/(api)/(task)/TaskIcon"
|
||||
import {TaskIcon, TaskImportance, TaskPriority} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
import {SortingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(sorting)/SortingMode"
|
||||
|
||||
|
@ -12,6 +11,38 @@ const TASK_IMPORTANCE_TO_VALUE = {
|
|||
[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[]) {
|
||||
return (a: TaskWithId, b: TaskWithId) => {
|
||||
|
@ -30,13 +61,13 @@ const SORTING_MODE_TO_SORTING_FUNCTION = {
|
|||
return a[1].text.localeCompare(b[1].text)
|
||||
},
|
||||
[SortingMode.ByIcon]: function sortTasksByIcon(a: TaskWithId, b: TaskWithId) {
|
||||
return ICONS.indexOf(a[1].icon as any) - ICONS.indexOf(b[1].icon as any)
|
||||
return TASK_ICON_TO_VALUE[a[1].icon] - TASK_ICON_TO_VALUE[b[1].icon]
|
||||
},
|
||||
[SortingMode.ByImportance]: function sortTasksByImportance(a: TaskWithId, b: TaskWithId) {
|
||||
return TASK_IMPORTANCE_TO_VALUE[a[1].importance] - TASK_IMPORTANCE_TO_VALUE[b[1].importance]
|
||||
},
|
||||
[SortingMode.ByDeadline]: function sortTasksByPriority(a: TaskWithId, b: TaskWithId) {
|
||||
return (b[1].deadline ?? -1) - (a[1].deadline ?? -1)
|
||||
[SortingMode.ByPriority]: function sortTasksByPriority(a: TaskWithId, b: TaskWithId) {
|
||||
return TASK_PRIORITY_TO_VALUE[a[1].priority] - TASK_PRIORITY_TO_VALUE[b[1].priority]
|
||||
},
|
||||
[SortingMode.ByStatus]: function sortTasksByStatus(a: TaskWithId, b: TaskWithId) {
|
||||
if(a[1].journaled_on && !b[1].journaled_on) return 1;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {ModifyTaskBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/convertTTS"
|
||||
import {TaskActions} from "@/app/[lang]/board/[board]/(page)/(task)/TaskActions"
|
||||
import {TaskContainer} from "@/app/[lang]/board/[board]/(page)/(task)/TaskContainer"
|
||||
import {TaskDescription} from "@/app/[lang]/board/[board]/(page)/(task)/TaskDescription"
|
||||
|
@ -13,7 +12,7 @@ import {TFunction} from "i18next"
|
|||
import {Dispatch, KeyboardEvent, PointerEvent, SetStateAction, SyntheticEvent, useCallback, useState} from "react"
|
||||
|
||||
|
||||
export function TaskViewer({lang, t, taskWithId: [id, task], setEditorInput}: {lang: string, t: TFunction, taskWithId: TaskWithId, setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
export function TaskViewer({t, taskWithId: [id, task], setEditorInput}: {t: TFunction, taskWithId: TaskWithId, setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const [isFlipped, setFlipped] = useState<boolean>(false)
|
||||
const {sendRequest, boardState: {locked}} = useBoardConsumer()
|
||||
|
||||
|
@ -104,7 +103,6 @@ export function TaskViewer({lang, t, taskWithId: [id, task], setEditorInput}: {l
|
|||
goAwayButton = (
|
||||
<TaskViewerRecreateButton
|
||||
t={t}
|
||||
lang={lang}
|
||||
taskWithId={[id, task]}
|
||||
setEditorInput={setEditorInput}
|
||||
/>
|
||||
|
@ -131,7 +129,7 @@ export function TaskViewer({lang, t, taskWithId: [id, task], setEditorInput}: {l
|
|||
<TaskContainer
|
||||
role={"article"}
|
||||
importance={task.importance}
|
||||
deadline={task.deadline}
|
||||
priority={task.priority}
|
||||
status={status}
|
||||
onKeyDown={toggleFlipOnKeyDown}
|
||||
onPointerEnter={flipOnPointerEnter}
|
||||
|
@ -140,13 +138,12 @@ export function TaskViewer({lang, t, taskWithId: [id, task], setEditorInput}: {l
|
|||
>
|
||||
<TaskViewerIcon
|
||||
t={t}
|
||||
icon={task.icon as any}
|
||||
icon={task.icon}
|
||||
status={status}
|
||||
onInteract={status === TaskSimplifiedStatus.Journaled ? undefined : toggleStatus}
|
||||
/>
|
||||
<TaskDescription
|
||||
isSource={isFlipped}
|
||||
text={isFlipped ? taskToString(task, lang) : task.text}
|
||||
text={task.text}
|
||||
/>
|
||||
{sideElements}
|
||||
</TaskContainer>
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import {TaskIcon} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {TaskIconComponent} from "@/app/[lang]/board/[board]/(page)/(task)/TaskIconComponent"
|
||||
import {TaskSimplifiedStatus} from "@/app/[lang]/board/[board]/(page)/(task)/TaskSimplifiedStatus"
|
||||
import {TFunction} from "i18next"
|
||||
|
@ -14,7 +15,7 @@ const TASK_STATUS_TO_I18N_KEY: {[key in TaskSimplifiedStatus]: string} = {
|
|||
export type TaskViewerIconProps = {
|
||||
t: TFunction,
|
||||
status: TaskSimplifiedStatus,
|
||||
icon: string,
|
||||
icon: TaskIcon,
|
||||
onInteract?: Parameters<typeof TaskIconComponent>[0]["onInteract"]
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,7 @@ import {UpdateTaskBoardChange} from "@/app/[lang]/board/[board]/(api)/(change)"
|
|||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {TaskButton} from "@/app/[lang]/board/[board]/(page)/(task)/TaskButton"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faBookBookmark} from "@fortawesome/free-solid-svg-icons"
|
||||
import {TFunction} from "i18next"
|
||||
import {SyntheticEvent, useCallback} from "react"
|
||||
|
||||
|
@ -25,7 +25,7 @@ export function TaskViewerJournalButton({t, taskWithId: [id, task]}: {t: TFuncti
|
|||
return (
|
||||
<TaskButton
|
||||
title={t("taskButtonJournal")}
|
||||
icon={fas.faBookBookmark}
|
||||
icon={faBookBookmark}
|
||||
onInteract={locked ? undefined : toggleJournalTask}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -1,22 +1,21 @@
|
|||
import {DeleteTaskBoardRequest} from "@/app/[lang]/board/[board]/(api)/(request)"
|
||||
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
|
||||
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/convertTTS"
|
||||
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/taskToString"
|
||||
import {TaskButton} from "@/app/[lang]/board/[board]/(page)/(task)/TaskButton"
|
||||
import {TaskWithId} from "@/app/[lang]/board/[board]/(page)/(task)/TaskWithId"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faTrashArrowUp} from "@fortawesome/free-solid-svg-icons"
|
||||
import {TFunction} from "i18next"
|
||||
import {Dispatch, SetStateAction, SyntheticEvent, useCallback} from "react"
|
||||
|
||||
|
||||
export type TaskViewerRecreateButtonProps = {
|
||||
t: TFunction,
|
||||
lang: string,
|
||||
taskWithId: TaskWithId,
|
||||
setEditorInput: Dispatch<SetStateAction<string>>,
|
||||
}
|
||||
|
||||
|
||||
export function TaskViewerRecreateButton({t, lang, taskWithId: [id, task], setEditorInput}: TaskViewerRecreateButtonProps) {
|
||||
export function TaskViewerRecreateButton({t, taskWithId: [id, task], setEditorInput}: TaskViewerRecreateButtonProps) {
|
||||
const {sendRequest, boardState: {locked}} = useBoardConsumer()
|
||||
|
||||
const recreateTask = useCallback((e: SyntheticEvent<HTMLButtonElement>) => {
|
||||
|
@ -27,7 +26,7 @@ export function TaskViewerRecreateButton({t, lang, taskWithId: [id, task], setEd
|
|||
}
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setEditorInput(taskToString(task, lang))
|
||||
setEditorInput(taskToString(task))
|
||||
const request: DeleteTaskBoardRequest = {"Task": [id, null]};
|
||||
sendRequest(request)
|
||||
}, [task, setEditorInput, sendRequest])
|
||||
|
@ -35,7 +34,7 @@ export function TaskViewerRecreateButton({t, lang, taskWithId: [id, task], setEd
|
|||
return (
|
||||
<TaskButton
|
||||
title={t("taskButtonRecreate")}
|
||||
icon={fas.faTrashArrowUp}
|
||||
icon={faTrashArrowUp}
|
||||
onInteract={locked ? undefined : recreateTask}
|
||||
/>
|
||||
)
|
||||
|
|
|
@ -4,39 +4,38 @@ import {GroupingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(grouping)"
|
|||
import {SortingMode} from "@/app/[lang]/board/[board]/(page)/(view)/(sorting)"
|
||||
import {BoardViewer} from "@/app/[lang]/board/[board]/(page)/(view)/BoardViewer"
|
||||
import {SplashWithIcon} from "@/app/[lang]/board/[board]/(page)/(view)/SplashWithIcon"
|
||||
import {fas} from "@awesome.me/kit-dfe340c874/icons"
|
||||
import {faAsterisk, faGear, faLink, faLinkSlash} from "@fortawesome/free-solid-svg-icons"
|
||||
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
|
||||
import {TFunction} from "i18next"
|
||||
import {Dispatch, SetStateAction} from "react"
|
||||
|
||||
|
||||
export function BoardMain({lang, t, className, columning, sorting, grouping, setEditorInput}: {lang: string, t: TFunction, className?: string, columning: ColumningMode, grouping: GroupingMode, sorting: SortingMode[], setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
export function BoardMain({t, className, columning, sorting, grouping, setEditorInput}: {t: TFunction, className?: string, columning: ColumningMode, grouping: GroupingMode, sorting: SortingMode[], setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const {webSocketState, webSocketBackoffMs, boardState} = useBoardConsumer()
|
||||
|
||||
switch(webSocketState) {
|
||||
case undefined:
|
||||
return <SplashWithIcon
|
||||
icon={<FontAwesomeIcon size={"4x"} icon={fas.faGear} beatFade/>}
|
||||
icon={<FontAwesomeIcon size={"4x"} icon={faGear} beatFade/>}
|
||||
text={t("boardPreparing")}
|
||||
className={className}
|
||||
/>
|
||||
case WebSocket.CONNECTING:
|
||||
return <SplashWithIcon
|
||||
icon={<FontAwesomeIcon size={"4x"} icon={fas.faLink} beatFade/>}
|
||||
icon={<FontAwesomeIcon size={"4x"} icon={faLink} beatFade/>}
|
||||
text={t("boardConnecting")}
|
||||
className={className}
|
||||
/>
|
||||
case WebSocket.OPEN:
|
||||
if(Object.keys(boardState.tasks).length === 0) {
|
||||
return <SplashWithIcon
|
||||
icon={<FontAwesomeIcon size={"4x"} icon={fas.faAsterisk}/>}
|
||||
icon={<FontAwesomeIcon size={"4x"} icon={faAsterisk}/>}
|
||||
text={t("boardEmpty")}
|
||||
className={className}
|
||||
/>
|
||||
}
|
||||
else {
|
||||
return <BoardViewer
|
||||
lang={lang}
|
||||
t={t}
|
||||
columning={columning}
|
||||
sorting={sorting}
|
||||
|
@ -47,13 +46,13 @@ export function BoardMain({lang, t, className, columning, sorting, grouping, set
|
|||
}
|
||||
case WebSocket.CLOSING:
|
||||
return <SplashWithIcon
|
||||
icon={<FontAwesomeIcon size={"4x"} icon={fas.faLinkSlash} beatFade/>}
|
||||
icon={<FontAwesomeIcon size={"4x"} icon={faLinkSlash} beatFade/>}
|
||||
text={t("boardDisconnecting")}
|
||||
className={className}
|
||||
/>
|
||||
case WebSocket.CLOSED:
|
||||
return <SplashWithIcon
|
||||
icon={<FontAwesomeIcon size={"4x"} icon={fas.faLinkSlash}/>}
|
||||
icon={<FontAwesomeIcon size={"4x"} icon={faLinkSlash}/>}
|
||||
text={t("boardDisconnected", { retryingInSeconds: Math.ceil((webSocketBackoffMs ?? 0) / 1000).toString() })}
|
||||
className={className}
|
||||
/>
|
||||
|
|
|
@ -12,7 +12,7 @@ import {Dispatch, SetStateAction} from "react"
|
|||
import style from "./BoardViewer.module.css"
|
||||
|
||||
|
||||
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>>}) {
|
||||
export function BoardViewer({className, t, columning, grouping, sorting, setEditorInput}: {className?: string, t: TFunction, columning: ColumningMode, grouping: GroupingMode, sorting: SortingMode[], setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const {boardState: {tasks}} = useBoardConsumer()
|
||||
const {taskGroups} = useBoardTasksArranger(tasks, grouping, sorting);
|
||||
|
||||
|
@ -22,12 +22,12 @@ export function BoardViewer({className, lang, t, columning, grouping, sorting, s
|
|||
[style.boardMainTaskGroupsMultiColumn]: columning === ColumningMode.MultiColumn,
|
||||
[style.boardMainTaskGroupsSingleColumn]: columning === ColumningMode.SingleColumn,
|
||||
})}>
|
||||
{taskGroups.map((tg) => <BoardViewerColumn lang={lang} t={t} taskGroup={tg} key={tg.k} grouping={grouping} setEditorInput={setEditorInput}/>)}
|
||||
{taskGroups.map((tg) => <BoardViewerColumn t={t} taskGroup={tg} key={tg.k} grouping={grouping} setEditorInput={setEditorInput}/>)}
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export function BoardViewerColumn({lang, t, grouping, taskGroup, setEditorInput}: {lang: string, t: TFunction, grouping: GroupingMode, taskGroup: TaskGroup<string | number>, setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
export function BoardViewerColumn({t, grouping, taskGroup, setEditorInput}: {t: TFunction, grouping: GroupingMode, taskGroup: TaskGroup<string | number>, setEditorInput: Dispatch<SetStateAction<string>>}) {
|
||||
const ColumnTitle = GROUPING_MODE_TO_TITLE_COMPONENT[grouping]
|
||||
|
||||
return (
|
||||
|
@ -36,7 +36,7 @@ export function BoardViewerColumn({lang, t, grouping, taskGroup, setEditorInput}
|
|||
<ColumnTitle t={t} k={taskGroup.k}/>
|
||||
</h3>
|
||||
<div className={style.boardColumnContents}>
|
||||
{taskGroup.tasks.map((task: TaskWithId) => <TaskViewer lang={lang} t={t} taskWithId={task} key={task[0]} setEditorInput={setEditorInput}/>)}
|
||||
{taskGroup.tasks.map((task: TaskWithId) => <TaskViewer t={t} taskWithId={task} key={task[0]} setEditorInput={setEditorInput}/>)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
|
|
@ -15,7 +15,7 @@ export function BoardPage({lang}: {lang: string}) {
|
|||
const internationalization = useClientTranslation(lang, "board")
|
||||
const metadataHook = useBoardMetadataEditor()
|
||||
const layoutHook = useBoardLayoutEditor()
|
||||
const editorHook = useTaskEditor(lang)
|
||||
const editorHook = useTaskEditor()
|
||||
|
||||
const t = internationalization.t as TFunction
|
||||
|
||||
|
@ -28,7 +28,6 @@ export function BoardPage({lang}: {lang: string}) {
|
|||
layoutHook={layoutHook}
|
||||
/>
|
||||
<BoardMain
|
||||
lang={lang}
|
||||
t={t}
|
||||
className={style.pageMain}
|
||||
columning={layoutHook.columningHook.value}
|
||||
|
@ -37,7 +36,6 @@ export function BoardPage({lang}: {lang: string}) {
|
|||
setEditorInput={editorHook.setInput}
|
||||
/>
|
||||
<BoardEditor
|
||||
lang={lang}
|
||||
t={t}
|
||||
className={style.pageEditor}
|
||||
editorHook={editorHook}
|
||||
|
|
|
@ -19,6 +19,7 @@ export function useBoardLayoutEditor() {
|
|||
ColumningMode.MultiColumn,
|
||||
])
|
||||
const groupingHook = useCycler(useLocalStorage<number | undefined>(localStorageKeyGrouping, undefined), [
|
||||
GroupingMode.ByPriority,
|
||||
GroupingMode.ByImportance,
|
||||
GroupingMode.ByStatus,
|
||||
GroupingMode.ByIcon,
|
||||
|
@ -26,24 +27,8 @@ export function useBoardLayoutEditor() {
|
|||
])
|
||||
const sortingHook = useCycler(useLocalStorage<number | undefined>(localStorageKeySorting, undefined), [
|
||||
[
|
||||
SortingMode.ByDeadline,
|
||||
SortingMode.ByImportance,
|
||||
SortingMode.ByStatus,
|
||||
SortingMode.ByText,
|
||||
SortingMode.ByIcon,
|
||||
SortingMode.ByCreation,
|
||||
],
|
||||
[
|
||||
SortingMode.ByImportance,
|
||||
SortingMode.ByDeadline,
|
||||
SortingMode.ByStatus,
|
||||
SortingMode.ByText,
|
||||
SortingMode.ByIcon,
|
||||
SortingMode.ByCreation,
|
||||
],
|
||||
[
|
||||
SortingMode.ByStatus,
|
||||
SortingMode.ByDeadline,
|
||||
SortingMode.ByPriority,
|
||||
SortingMode.ByImportance,
|
||||
SortingMode.ByText,
|
||||
SortingMode.ByIcon,
|
||||
|
@ -52,7 +37,36 @@ export function useBoardLayoutEditor() {
|
|||
[
|
||||
SortingMode.ByStatus,
|
||||
SortingMode.ByImportance,
|
||||
SortingMode.ByDeadline,
|
||||
SortingMode.ByPriority,
|
||||
SortingMode.ByText,
|
||||
SortingMode.ByIcon,
|
||||
SortingMode.ByCreation,
|
||||
],
|
||||
[
|
||||
SortingMode.ByStatus,
|
||||
SortingMode.ByText,
|
||||
SortingMode.ByIcon,
|
||||
SortingMode.ByCreation,
|
||||
],
|
||||
[
|
||||
SortingMode.ByStatus,
|
||||
SortingMode.ByCreation,
|
||||
],
|
||||
[
|
||||
SortingMode.ByPriority,
|
||||
SortingMode.ByImportance,
|
||||
SortingMode.ByText,
|
||||
SortingMode.ByIcon,
|
||||
SortingMode.ByCreation,
|
||||
],
|
||||
[
|
||||
SortingMode.ByImportance,
|
||||
SortingMode.ByPriority,
|
||||
SortingMode.ByText,
|
||||
SortingMode.ByIcon,
|
||||
SortingMode.ByCreation,
|
||||
],
|
||||
[
|
||||
SortingMode.ByText,
|
||||
SortingMode.ByIcon,
|
||||
SortingMode.ByCreation,
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import {Task} from "@/app/[lang]/board/[board]/(api)/(task)"
|
||||
import {convertSTT} from "@/app/[lang]/board/[board]/(page)/(edit)/convertSTT"
|
||||
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/convertTTS"
|
||||
import {stringToTask} from "@/app/[lang]/board/[board]/(page)/(edit)/stringToTask"
|
||||
import {taskToString} from "@/app/[lang]/board/[board]/(page)/(edit)/taskToString"
|
||||
import {useCallback, useMemo, useState} from "react"
|
||||
|
||||
|
||||
export function useTaskEditor(lang: string) {
|
||||
export function useTaskEditor() {
|
||||
const [input, setInput] = useState<string>("")
|
||||
const task = useMemo(() => convertSTT(input, lang), [input, lang])
|
||||
const task = useMemo(() => stringToTask(input), [input])
|
||||
|
||||
const setTask = useCallback((t: Task) => {
|
||||
setInput(taskToString(t, lang))
|
||||
setInput(taskToString(t))
|
||||
}, [])
|
||||
|
||||
return {
|
||||
|
|
|
@ -2,143 +2,152 @@
|
|||
# yarn lockfile v1
|
||||
|
||||
|
||||
"@awesome.me/kit-dfe340c874@^1.0.10":
|
||||
version "1.0.10"
|
||||
resolved "https://npm.fontawesome.com/@awesome.me/kit-dfe340c874/-/1.0.10/kit-dfe340c874-1.0.10.tgz#a09e8b5a92ab9d58e703008f5adde7aaeaf6f629"
|
||||
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==
|
||||
"@babel/runtime@^7.21.5", "@babel/runtime@^7.22.5":
|
||||
version "7.22.10"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.22.10.tgz#ae3e9631fd947cb7e3610d3e9d8fef5f76696682"
|
||||
integrity sha512-21t/fkKLMZI4pqP2wlmsQAWnYW1PDyKyyUV4vCi+B25ydmdaYTKXPwCj0BzSUnZf4seIiYvSA3jcZ3gdsMFkLQ==
|
||||
dependencies:
|
||||
regenerator-runtime "^0.14.0"
|
||||
|
||||
"@fortawesome/fontawesome-common-types@6.6.0", "@fortawesome/fontawesome-common-types@^6.6.0":
|
||||
version "6.6.0"
|
||||
resolved "https://npm.fontawesome.com/@fortawesome/fontawesome-common-types/-/6.6.0/fontawesome-common-types-6.6.0.tgz#31ab07ca6a06358c5de4d295d4711b675006163f"
|
||||
integrity sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==
|
||||
"@fortawesome/fontawesome-common-types@6.4.2":
|
||||
version "6.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.4.2.tgz#1766039cad33f8ad87f9467b98e0d18fbc8f01c5"
|
||||
integrity sha512-1DgP7f+XQIJbLFCTX1V2QnxVmpLdKdzzo2k8EmvDOePfchaIGQ9eCHj2up3/jNEbZuBqel5OxiaOJf37TWauRA==
|
||||
|
||||
"@fortawesome/fontawesome-svg-core@*":
|
||||
version "6.6.0"
|
||||
resolved "https://npm.fontawesome.com/@fortawesome/fontawesome-svg-core/-/6.6.0/fontawesome-svg-core-6.6.0.tgz#2a24c32ef92136e98eae2ff334a27145188295ff"
|
||||
integrity sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==
|
||||
"@fortawesome/fontawesome-svg-core@^6.4.0":
|
||||
version "6.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.4.2.tgz#37f4507d5ec645c8b50df6db14eced32a6f9be09"
|
||||
integrity sha512-gjYDSKv3TrM2sLTOKBc5rH9ckje8Wrwgx1CxAPbN5N3Fm4prfi7NsJVWd1jklp7i5uSCVwhZS5qlhMXqLrpAIg==
|
||||
dependencies:
|
||||
"@fortawesome/fontawesome-common-types" "6.6.0"
|
||||
"@fortawesome/fontawesome-common-types" "6.4.2"
|
||||
|
||||
"@fortawesome/react-fontawesome@*":
|
||||
version "0.2.2"
|
||||
resolved "https://npm.fontawesome.com/@fortawesome/react-fontawesome/-/0.2.2/react-fontawesome-0.2.2.tgz#68b058f9132b46c8599875f6a636dad231af78d4"
|
||||
integrity sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==
|
||||
"@fortawesome/free-regular-svg-icons@^6.4.0":
|
||||
version "6.4.2"
|
||||
resolved "https://registry.yarnpkg.com/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.4.2.tgz#aee79ed76ce5dd04931352f9d83700761b8b1b25"
|
||||
integrity sha512-0+sIUWnkgTVVXVAPQmW4vxb9ZTHv0WstOa3rBx9iPxrrrDH6bNLsDYuwXF9b6fGm+iR7DKQvQshUH/FJm3ed9Q==
|
||||
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:
|
||||
prop-types "^15.8.1"
|
||||
|
||||
"@next/env@14.2.11":
|
||||
version "14.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-14.2.11.tgz#91fa6865140e7c89c555651cbe28b57180b26b9e"
|
||||
integrity sha512-HYsQRSIXwiNqvzzYThrBwq6RhXo3E0n8j8nQnAs8i4fCEo2Zf/3eS0IiRA8XnRg9Ha0YnpkyJZIZg1qEwemrHw==
|
||||
"@next/env@13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/env/-/env-13.5.0.tgz#a61dee2f29b09985847eabcc4c8a815031267a36"
|
||||
integrity sha512-mxhf/BskjPURT+qEjNP7wBvqre2q6OXEIbydF8BrH+duSSJQnB4/vzzuJDoahYwTXiUaXpouAnMWHZdG0HU62g==
|
||||
|
||||
"@next/swc-darwin-arm64@14.2.11":
|
||||
version "14.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.11.tgz#0022b52ccc62e21c34a34311ee0251e31cd5ff49"
|
||||
integrity sha512-eiY9u7wEJZWp/Pga07Qy3ZmNEfALmmSS1HtsJF3y1QEyaExu7boENz11fWqDmZ3uvcyAxCMhTrA1jfVxITQW8g==
|
||||
"@next/swc-darwin-arm64@13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-arm64/-/swc-darwin-arm64-13.5.0.tgz#45ea191e13593088572d0048d4ddfc1fcdb3c8ed"
|
||||
integrity sha512-DavPD8oRjSoCRJana5DCAWdRZ4nbS7/pPw13DlnukFfMPJUk5hCAC3+NbqEyekS/X1IBFdZWSV2lJIdzTn4s6w==
|
||||
|
||||
"@next/swc-darwin-x64@14.2.11":
|
||||
version "14.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.11.tgz#41151d22b5009a2a83bb57e6d98efb8a8995a3cd"
|
||||
integrity sha512-lnB0zYCld4yE0IX3ANrVMmtAbziBb7MYekcmR6iE9bujmgERl6+FK+b0MBq0pl304lYe7zO4yxJus9H/Af8jbg==
|
||||
"@next/swc-darwin-x64@13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-darwin-x64/-/swc-darwin-x64-13.5.0.tgz#582e8df7d563c057581bc118fff1cea79391d6e7"
|
||||
integrity sha512-s5QSKKB0CTKFWp3CNMC5GH1YOipH1Jjr5P3w+RQTC4Aybo6xPqeWp/UyDW0fxmLRq0e1zgnOMgDQRdxAkoThrw==
|
||||
|
||||
"@next/swc-linux-arm64-gnu@14.2.11":
|
||||
version "14.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.11.tgz#8fdc732c39b79dc7519bb561c48a2417e0eb6b24"
|
||||
integrity sha512-Ulo9TZVocYmUAtzvZ7FfldtwUoQY0+9z3BiXZCLSUwU2bp7GqHA7/bqrfsArDlUb2xeGwn3ZuBbKtNK8TR0A8w==
|
||||
"@next/swc-linux-arm64-gnu@13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-13.5.0.tgz#7ee0a43b6635eca1e80a887304b7bfe31254a4a6"
|
||||
integrity sha512-E0fCKA8F2vfgZWwcv4iq642No75EiACSNUBNGvc5lx/ylqAUdNwE/9+x2SHv+LPUXFhZ6hZLR0Qox/oKgZqFlg==
|
||||
|
||||
"@next/swc-linux-arm64-musl@14.2.11":
|
||||
version "14.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.11.tgz#04c3730ca4bc2f0c56c73db53dd03cf00126a243"
|
||||
integrity sha512-fH377DnKGyUnkWlmUpFF1T90m0dADBfK11dF8sOQkiELF9M+YwDRCGe8ZyDzvQcUd20Rr5U7vpZRrAxKwd3Rzg==
|
||||
"@next/swc-linux-arm64-musl@13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-13.5.0.tgz#99a1efd6b68a4d0dfdc24b81f14cd8b8251425a9"
|
||||
integrity sha512-jG/blDDLndFRUcafCQO4TOI3VuoIZh3jQriZ7JaVCgAEZe0D1EUrxKdbBarZ74isutHZ6DpNGRDi/0OHFZpJAA==
|
||||
|
||||
"@next/swc-linux-x64-gnu@14.2.11":
|
||||
version "14.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.11.tgz#533823c0a96d31122671f670b58da65bdb71f330"
|
||||
integrity sha512-a0TH4ZZp4NS0LgXP/488kgvWelNpwfgGTUCDXVhPGH6pInb7yIYNgM4kmNWOxBFt+TIuOH6Pi9NnGG4XWFUyXQ==
|
||||
"@next/swc-linux-x64-gnu@13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-13.5.0.tgz#7c85acd45879a20d8fb102b3212e792924d02e93"
|
||||
integrity sha512-6JWR7U41uNL6HGwNbGg3Oedt+FN4YuA126sHWKTq3ic5kkhEusIIdVo7+WcswVJl8nTMB1yT3gEPwygQbVYVUA==
|
||||
|
||||
"@next/swc-linux-x64-musl@14.2.11":
|
||||
version "14.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.11.tgz#fa81b56095ef4a64bae7f5798e6cdeac9de2906f"
|
||||
integrity sha512-DYYZcO4Uir2gZxA4D2JcOAKVs8ZxbOFYPpXSVIgeoQbREbeEHxysVsg3nY4FrQy51e5opxt5mOHl/LzIyZBoKA==
|
||||
"@next/swc-linux-x64-musl@13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-13.5.0.tgz#23aad9ab7621f53bb947b727e659d85e74b0e31a"
|
||||
integrity sha512-uY+wrYfD5QUossqznwidOpJYmmcBwojToZx55shihtbTl6afVYzOxsUbRXLdWmZAa36ckxXpqkvuFNS8icQuug==
|
||||
|
||||
"@next/swc-win32-arm64-msvc@14.2.11":
|
||||
version "14.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.11.tgz#3ad4a72282f9b5e72f600522156e5bab6efb63a8"
|
||||
integrity sha512-PwqHeKG3/kKfPpM6of1B9UJ+Er6ySUy59PeFu0Un0LBzJTRKKAg2V6J60Yqzp99m55mLa+YTbU6xj61ImTv9mg==
|
||||
"@next/swc-win32-arm64-msvc@13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-13.5.0.tgz#5a45686335e5f54342faf9d9ed25f55a4107ce7f"
|
||||
integrity sha512-lWZ5vJTULxTOdLcRmrllNgAdDRSDwk8oqJMyDxpqS691NG5uhle9ZwRj3g1F1/vHNkDa+B7PmWhQgG0nmlbKZg==
|
||||
|
||||
"@next/swc-win32-ia32-msvc@14.2.11":
|
||||
version "14.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.11.tgz#80a6dd48e2f8035c518c2162bf6058bd61921a1b"
|
||||
integrity sha512-0U7PWMnOYIvM74GY6rbH6w7v+vNPDVH1gUhlwHpfInJnNe5LkmUZqhp7FNWeNa5wbVgRcRi1F1cyxp4dmeLLvA==
|
||||
"@next/swc-win32-ia32-msvc@13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-13.5.0.tgz#b9990965762aaa109bdeb7b49cbdc7e4af7f9014"
|
||||
integrity sha512-jirQXnVCU9hi3cHzgd33d4qSBXn1/0gUT/KtXqy9Ux9OTcIcjJT3TcAzoLJLTdhRg7op3MZoSnuFeWl8kmGGNw==
|
||||
|
||||
"@next/swc-win32-x64-msvc@14.2.11":
|
||||
version "14.2.11"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.11.tgz#2150783dfec3b3e0e7c3ecfda2d4f30fbf913d44"
|
||||
integrity sha512-gQpS7mcgovWoaTG1FbS5/ojF7CGfql1Q0ZLsMrhcsi2Sr9HEqsUZ70MPJyaYBXbk6iEAP7UXMD9HC8KY1qNwvA==
|
||||
"@next/swc-win32-x64-msvc@13.5.0":
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-13.5.0.tgz#4385c5d9c0db39c2623aed566b3ec7fedaf6f190"
|
||||
integrity sha512-Q8QYLyWcMMUp3DohI04VyJbLNCfFMNTxYNhujvJD2lowuqnqApUBP2DxI/jCZRMFWgKi76n5u8UboLVeYXn6jA==
|
||||
|
||||
"@steffo/bluelib@*":
|
||||
version "9.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@steffo/bluelib/-/bluelib-9.1.0.tgz#48b03411ad66c53a1931e5ff3ae316a3cf3d9bb6"
|
||||
integrity sha512-S+MT4NLfcFu2oV22UabI8TXn9c3LOpx3FM6Nb4L/eiMTpE/XJf/NK4di8prHBabt8230JIDDv+SGxUzurG2I8g==
|
||||
"@steffo/bluelib@^9.0.1":
|
||||
version "9.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@steffo/bluelib/-/bluelib-9.0.1.tgz#15932995ab67ff1b959601036042cd2f14c4d197"
|
||||
integrity sha512-r6/38m0LuapcF5Fung5yjITTcXNoWf03I5C2b4dBDGjt7IqBAiaoBbLsRcChZ5hRRn32TJO10yuDlgsPNwLhAg==
|
||||
|
||||
"@swc/counter@^0.1.3":
|
||||
version "0.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@swc/counter/-/counter-0.1.3.tgz#cc7463bd02949611c6329596fccd2b0ec782b0e9"
|
||||
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==
|
||||
"@swc/helpers@0.5.2":
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/@swc/helpers/-/helpers-0.5.2.tgz#85ea0c76450b61ad7d10a37050289eded783c27d"
|
||||
integrity sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==
|
||||
dependencies:
|
||||
"@swc/counter" "^0.1.3"
|
||||
tslib "^2.4.0"
|
||||
|
||||
"@types/negotiator@*":
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/negotiator/-/negotiator-0.6.3.tgz#29e8fce64e35f57f6fe9c624f8e4ed304357745a"
|
||||
integrity sha512-JkXTOdKs5MF086b/pt8C3+yVp3iDUwG635L7oCH6HvJvvr6lSUU5oe/gLXnPEfYRROHjJIPgCV6cuAg8gGkntQ==
|
||||
"@types/negotiator@^0.6.1":
|
||||
version "0.6.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/negotiator/-/negotiator-0.6.1.tgz#4c75543f6ef87f427f4705e731a933595b7397f5"
|
||||
integrity sha512-c4mvXFByghezQ/eVGN5HvH/jI63vm3B7FiE81BUzDAWmuiohRecCO6ddU60dfq29oKUMiQujsoB2h0JQC7JHKA==
|
||||
|
||||
"@types/node@*":
|
||||
version "22.5.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.5.4.tgz#83f7d1f65bc2ed223bdbf57c7884f1d5a4fa84e8"
|
||||
integrity sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==
|
||||
dependencies:
|
||||
undici-types "~6.19.2"
|
||||
"@types/node@20.4.5":
|
||||
version "20.4.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/node/-/node-20.4.5.tgz#9dc0a5cb1ccce4f7a731660935ab70b9c00a5d69"
|
||||
integrity sha512-rt40Nk13II9JwQBdeYqmbn2Q6IVTA5uPhvSO+JVqdXw/6/4glI6oR9ezty/A9Hg5u7JH4OmYmuQ+XvjKm0Datg==
|
||||
|
||||
"@types/prop-types@*":
|
||||
version "15.7.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.12.tgz#12bb1e2be27293c1406acb6af1c3f3a1481d98c6"
|
||||
integrity sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==
|
||||
version "15.7.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||
|
||||
"@types/react-dom@*":
|
||||
version "18.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.0.tgz#0cbc818755d87066ab6ca74fbedb2547d74a82b0"
|
||||
integrity sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==
|
||||
"@types/react-dom@18.2.7":
|
||||
version "18.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.2.7.tgz#67222a08c0a6ae0a0da33c3532348277c70abb63"
|
||||
integrity sha512-GRaAEriuT4zp9N4p1i8BDBYmEyfo+xQ3yHjJU4eiK5NDa1RmUZG+unZABUTK4/Ox/M+GaHwb6Ow8rUITrtjszA==
|
||||
dependencies:
|
||||
"@types/react" "*"
|
||||
|
||||
"@types/react@*":
|
||||
version "18.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.5.tgz#5f524c2ad2089c0ff372bbdabc77ca2c4dbadf8f"
|
||||
integrity sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==
|
||||
version "18.2.20"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.20.tgz#1605557a83df5c8a2cc4eeb743b3dfc0eb6aaeb2"
|
||||
integrity sha512-WKNtmsLWJM/3D5mG4U84cysVY31ivmyw85dE84fOCk5Hx78wezB/XEjVPWl2JTZ5FkEeaTJf+VgUAUn3PE7Isw==
|
||||
dependencies:
|
||||
"@types/prop-types" "*"
|
||||
"@types/scheduler" "*"
|
||||
csstype "^3.0.2"
|
||||
|
||||
any-date-parser@^1.5.4:
|
||||
version "1.5.4"
|
||||
resolved "https://registry.yarnpkg.com/any-date-parser/-/any-date-parser-1.5.4.tgz#3c462f3a419b7147a88a8c7cab91da4193ff680b"
|
||||
integrity sha512-S4gl9UmXNk9XXSQxp5w5harUD6aM0fepyL3dZM/B3znX57sWf792hS2UvvCFIHECfpsqfKbQ+cqWBky4AKyRIg==
|
||||
"@types/react@18.2.17":
|
||||
version "18.2.17"
|
||||
resolved "https://registry.yarnpkg.com/@types/react/-/react-18.2.17.tgz#baa565b17ddb649c2dac85b5eaf9e9a1fe0f3b4e"
|
||||
integrity sha512-u+e7OlgPPh+aryjOm5UJMX32OvB2E3QASOAqVMY6Ahs90djagxwv2ya0IctglNbNTexC12qCSMZG47KPfy1hAA==
|
||||
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:
|
||||
version "1.6.0"
|
||||
|
@ -147,44 +156,49 @@ busboy@1.6.0:
|
|||
dependencies:
|
||||
streamsearch "^1.1.0"
|
||||
|
||||
caniuse-lite@^1.0.30001579:
|
||||
version "1.0.30001660"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001660.tgz#31218de3463fabb44d0b7607b652e56edf2e2355"
|
||||
integrity sha512-GacvNTTuATm26qC74pt+ad1fW15mlQ/zuTzzY1ZoIzECTP8HURDfF43kNxPgf7H1jmelCBQTTbBNxdSXOA7Bqg==
|
||||
caniuse-lite@^1.0.30001406:
|
||||
version "1.0.30001521"
|
||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001521.tgz#e9930cf499f7c1e80334b6c1fbca52e00d889e56"
|
||||
integrity sha512-fnx1grfpEOvDGH+V17eccmNjucGUnCbP6KL+l5KqBIerp26WK/+RQ7CIDE37KGJjaPyqWXXlFUyKiWmvdNNKmQ==
|
||||
|
||||
classnames@*:
|
||||
version "2.5.1"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.5.1.tgz#ba774c614be0f016da105c858e7159eae8e7687b"
|
||||
integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
|
||||
classnames@^2.3.2:
|
||||
version "2.3.2"
|
||||
resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924"
|
||||
integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw==
|
||||
|
||||
client-only@*, client-only@0.0.1:
|
||||
client-only@0.0.1, client-only@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/client-only/-/client-only-0.0.1.tgz#38bba5d403c41ab150bff64a95c85013cf73bca1"
|
||||
integrity sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==
|
||||
|
||||
csstype@^3.0.2:
|
||||
version "3.1.3"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
|
||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.2.tgz#1d4bf9d572f11c14031f0436e1c10bc1f571f50b"
|
||||
integrity sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==
|
||||
|
||||
graceful-fs@^4.2.11:
|
||||
glob-to-regexp@^0.4.1:
|
||||
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"
|
||||
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==
|
||||
|
||||
i18next-resources-to-backend@*:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/i18next-resources-to-backend/-/i18next-resources-to-backend-1.2.1.tgz#fded121e63e3139ce839c9901b9449dbbea7351d"
|
||||
integrity sha512-okHbVA+HZ7n1/76MsfhPqDou0fptl2dAlhRDu2ideXloRRduzHsqDOznJBef+R3DFZnbvWoBW+KxJ7fnFjd6Yw==
|
||||
i18next-resources-to-backend@^1.1.4:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.yarnpkg.com/i18next-resources-to-backend/-/i18next-resources-to-backend-1.1.4.tgz#d139ca0cacc270dcc90b7926e192f4cd5aa4db60"
|
||||
integrity sha512-hMyr9AOmIea17AOaVe1srNxK/l3mbk81P7Uf3fdcjlw3ehZy3UNTd0OP3EEi6yu4J02kf9jzhCcjokz6AFlEOg==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.2"
|
||||
"@babel/runtime" "^7.21.5"
|
||||
|
||||
i18next@*:
|
||||
version "23.15.1"
|
||||
resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.15.1.tgz#c50de337bf12ca5195e697cc0fbe5f32304871d9"
|
||||
integrity sha512-wB4abZ3uK7EWodYisHl/asf8UYEhrI/vj/8aoSsrj/ZDxj4/UXPOa1KvFt1Fq5hkUHquNqwFlDprmjZ8iySgYA==
|
||||
i18next@^23.4.2:
|
||||
version "23.4.4"
|
||||
resolved "https://registry.yarnpkg.com/i18next/-/i18next-23.4.4.tgz#ec8fb2b5f3c5d8e3bf3f8ab1b19e743be91300e0"
|
||||
integrity sha512-+c9B0txp/x1m5zn+QlwHaCS9vyFtmIAEXbVSFzwCX7vupm5V7va8F9cJGNJZ46X9ZtoGzhIiRC7eTIIh93TxPA==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.23.2"
|
||||
"@babel/runtime" "^7.22.5"
|
||||
|
||||
"js-tokens@^3.0.0 || ^4.0.0":
|
||||
version "4.0.0"
|
||||
|
@ -198,38 +212,39 @@ loose-envify@^1.1.0, loose-envify@^1.4.0:
|
|||
dependencies:
|
||||
js-tokens "^3.0.0 || ^4.0.0"
|
||||
|
||||
nanoid@^3.3.6:
|
||||
version "3.3.7"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8"
|
||||
integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==
|
||||
nanoid@^3.3.4:
|
||||
version "3.3.6"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.6.tgz#443380c856d6e9f9824267d960b4236ad583ea4c"
|
||||
integrity sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==
|
||||
|
||||
negotiator@*:
|
||||
negotiator@^0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||
|
||||
next@*:
|
||||
version "14.2.11"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-14.2.11.tgz#86572882d340c5c1ee7e46d00790fa7ba664d6a9"
|
||||
integrity sha512-8MDFqHBhdmR2wdfaWc8+lW3A/hppFe1ggQ9vgIu/g2/2QEMYJrPoQP6b+VNk56gIug/bStysAmrpUKtj3XN8Bw==
|
||||
next@13.5.0:
|
||||
version "13.5.0"
|
||||
resolved "https://registry.yarnpkg.com/next/-/next-13.5.0.tgz#3a3ce5b8c89c4fff9c6f0b2452bcb03f63d8c84c"
|
||||
integrity sha512-mhguN5JPZXhhrD/nNcezXgKoxN8GT8xZvvGhUQV2ETiaNm+KHRWT1rCbrF5FlbG2XCcLRKOmOe3D5YQgXmJrDQ==
|
||||
dependencies:
|
||||
"@next/env" "14.2.11"
|
||||
"@swc/helpers" "0.5.5"
|
||||
"@next/env" "13.5.0"
|
||||
"@swc/helpers" "0.5.2"
|
||||
busboy "1.6.0"
|
||||
caniuse-lite "^1.0.30001579"
|
||||
graceful-fs "^4.2.11"
|
||||
postcss "8.4.31"
|
||||
caniuse-lite "^1.0.30001406"
|
||||
postcss "8.4.14"
|
||||
styled-jsx "5.1.1"
|
||||
watchpack "2.4.0"
|
||||
zod "3.21.4"
|
||||
optionalDependencies:
|
||||
"@next/swc-darwin-arm64" "14.2.11"
|
||||
"@next/swc-darwin-x64" "14.2.11"
|
||||
"@next/swc-linux-arm64-gnu" "14.2.11"
|
||||
"@next/swc-linux-arm64-musl" "14.2.11"
|
||||
"@next/swc-linux-x64-gnu" "14.2.11"
|
||||
"@next/swc-linux-x64-musl" "14.2.11"
|
||||
"@next/swc-win32-arm64-msvc" "14.2.11"
|
||||
"@next/swc-win32-ia32-msvc" "14.2.11"
|
||||
"@next/swc-win32-x64-msvc" "14.2.11"
|
||||
"@next/swc-darwin-arm64" "13.5.0"
|
||||
"@next/swc-darwin-x64" "13.5.0"
|
||||
"@next/swc-linux-arm64-gnu" "13.5.0"
|
||||
"@next/swc-linux-arm64-musl" "13.5.0"
|
||||
"@next/swc-linux-x64-gnu" "13.5.0"
|
||||
"@next/swc-linux-x64-musl" "13.5.0"
|
||||
"@next/swc-win32-arm64-msvc" "13.5.0"
|
||||
"@next/swc-win32-ia32-msvc" "13.5.0"
|
||||
"@next/swc-win32-x64-msvc" "13.5.0"
|
||||
|
||||
object-assign@^4.1.1:
|
||||
version "4.1.1"
|
||||
|
@ -237,16 +252,16 @@ object-assign@^4.1.1:
|
|||
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
|
||||
|
||||
picocolors@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.0.tgz#5358b76a78cde483ba5cef6a9dc9671440b27d59"
|
||||
integrity sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.0.tgz#cb5bdc74ff3f51892236eaf79d68bc44564ab81c"
|
||||
integrity sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==
|
||||
|
||||
postcss@8.4.31:
|
||||
version "8.4.31"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.31.tgz#92b451050a9f914da6755af352bdc0192508656d"
|
||||
integrity sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==
|
||||
postcss@8.4.14:
|
||||
version "8.4.14"
|
||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.14.tgz#ee9274d5622b4858c1007a74d76e42e56fd21caf"
|
||||
integrity sha512-E398TUmfAYFPBSdzgeieK2Y1+1cpdxJx8yXbK/m57nRhKSmk1GB2tO4lbLBtlkfPQTDKfe4Xqv1ASWPpayPEig==
|
||||
dependencies:
|
||||
nanoid "^3.3.6"
|
||||
nanoid "^3.3.4"
|
||||
picocolors "^1.0.0"
|
||||
source-map-js "^1.0.2"
|
||||
|
||||
|
@ -259,47 +274,47 @@ prop-types@^15.8.1:
|
|||
object-assign "^4.1.1"
|
||||
react-is "^16.13.1"
|
||||
|
||||
react-dom@*:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
|
||||
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
|
||||
react-dom@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.2.0.tgz#22aaf38708db2674ed9ada224ca4aa708d821e3d"
|
||||
integrity sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
scheduler "^0.23.2"
|
||||
scheduler "^0.23.0"
|
||||
|
||||
react-is@^16.13.1:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
|
||||
|
||||
react@*:
|
||||
version "18.3.1"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
|
||||
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
|
||||
react@18.2.0:
|
||||
version "18.2.0"
|
||||
resolved "https://registry.yarnpkg.com/react/-/react-18.2.0.tgz#555bd98592883255fa00de14f1151a917b5d77d5"
|
||||
integrity sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
regenerator-runtime@^0.14.0:
|
||||
version "0.14.1"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz#356ade10263f685dda125100cd862c1db895327f"
|
||||
integrity sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==
|
||||
version "0.14.0"
|
||||
resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz#5e19d68eb12d486f797e15a3c6a918f7cec5eb45"
|
||||
integrity sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA==
|
||||
|
||||
scheduler@^0.23.2:
|
||||
version "0.23.2"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
|
||||
integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
|
||||
scheduler@^0.23.0:
|
||||
version "0.23.0"
|
||||
resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.0.tgz#ba8041afc3d30eb206a487b6b384002e4e61fdfe"
|
||||
integrity sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==
|
||||
dependencies:
|
||||
loose-envify "^1.1.0"
|
||||
|
||||
server-only@*:
|
||||
server-only@^0.0.1:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.yarnpkg.com/server-only/-/server-only-0.0.1.tgz#0f366bb6afb618c37c9255a314535dc412cd1c9e"
|
||||
integrity sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA==
|
||||
|
||||
source-map-js@^1.0.2:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46"
|
||||
integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.0.2.tgz#adbc361d9c62df380125e7f161f71c826f1e490c"
|
||||
integrity sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==
|
||||
|
||||
streamsearch@^1.1.0:
|
||||
version "1.1.0"
|
||||
|
@ -314,21 +329,29 @@ styled-jsx@5.1.1:
|
|||
client-only "0.0.1"
|
||||
|
||||
tslib@^2.4.0:
|
||||
version "2.7.0"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.7.0.tgz#d9b40c5c40ab59e8738f297df3087bf1a2690c01"
|
||||
integrity sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==
|
||||
version "2.6.1"
|
||||
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.1.tgz#fd8c9a0ff42590b25703c0acb3de3d3f4ede0410"
|
||||
integrity sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==
|
||||
|
||||
typescript@*:
|
||||
version "5.6.2"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.6.2.tgz#d1de67b6bef77c41823f822df8f0b3bcff60a5a0"
|
||||
integrity sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==
|
||||
typescript@5.1.6:
|
||||
version "5.1.6"
|
||||
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.1.6.tgz#02f8ac202b6dad2c0dd5e0913745b47a37998274"
|
||||
integrity sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==
|
||||
|
||||
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@*:
|
||||
use-local-storage@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/use-local-storage/-/use-local-storage-3.0.0.tgz#ecf90952374150f0c65baf027eaaf2062a4c20c6"
|
||||
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==
|
||||
|
|
|
@ -1,8 +0,0 @@
|
|||
<?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>
|
|
@ -8,7 +8,6 @@ services:
|
|||
--loglevel notice
|
||||
volumes:
|
||||
- "./data/redis/rdata:/data"
|
||||
|
||||
red:
|
||||
image: "ghcr.io/steffo99/todocolors-red"
|
||||
restart: unless-stopped
|
||||
|
@ -17,11 +16,9 @@ services:
|
|||
AXUM_XFORWARDED: "TODO-YOUR-PUBLIC-URL-GOES-HERE"
|
||||
TODORED_RATE_LIMIT_CONNECTIONS_PER_MINUTE: 5
|
||||
TODORED_RATE_LIMIT_MESSAGES_PER_MINUTE: 100
|
||||
|
||||
blue:
|
||||
image: "ghcr.io/steffo99/todocolors-blue"
|
||||
restart: unless-stopped
|
||||
|
||||
caddy:
|
||||
image: "caddy"
|
||||
restart: unless-stopped
|
||||
|
|
2
todored/Cargo.lock
generated
|
@ -1151,7 +1151,7 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
|
|||
|
||||
[[package]]
|
||||
name = "todored"
|
||||
version = "0.4.0"
|
||||
version = "0.3.0"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"chrono",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "todored"
|
||||
version = "0.4.0"
|
||||
version = "0.3.0"
|
||||
license = "EUPL-1.2"
|
||||
edition = "2021"
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
FROM --platform=${BUILDPLATFORM} rust:bullseye AS builder
|
||||
FROM --platform=${BUILDPLATFORM} rust:1.71-bullseye AS builder
|
||||
ARG BUILDPLATFORM
|
||||
ARG TARGETPLATFORM
|
||||
|
||||
|
@ -60,7 +60,7 @@ RUN \
|
|||
|
||||
#############################################################################
|
||||
|
||||
FROM --platform=${TARGETPLATFORM} rust:slim-bullseye AS final
|
||||
FROM --platform=${TARGETPLATFORM} rust:1.71-slim-bullseye AS final
|
||||
|
||||
WORKDIR /usr/src/todored/
|
||||
COPY --from=builder \
|
||||
|
|
|
@ -37,9 +37,8 @@ async fn main() {
|
|||
)
|
||||
);
|
||||
|
||||
let host = 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)
|
||||
log::info!("Starting web server!");
|
||||
axum::Server::bind(&std::net::SocketAddr::from_str(&config::AXUM_HOST).expect("AXUM_HOST to be a valid SocketAddr"))
|
||||
.serve(router.into_make_service())
|
||||
.await
|
||||
.expect("to be able to run the Axum server");
|
||||
|
|
|
@ -7,7 +7,6 @@ pub type ResponseError = StatusCode;
|
|||
pub type Response<T> = Result<T, ResponseError>;
|
||||
|
||||
/// Trait to easily [`log`] function outcomes.
|
||||
#[allow(dead_code)]
|
||||
pub(crate) trait LoggableOutcome {
|
||||
fn log_err_to_trace(self, msg: &str) -> Self;
|
||||
fn log_err_to_debug(self, msg: &str) -> Self;
|
||||
|
|
|
@ -45,7 +45,7 @@ pub(crate) async fn handler(
|
|||
log::trace!("Connection rate limit is: {count} / 60 s");
|
||||
if count > 0 {
|
||||
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() {
|
||||
return Err::<(), StatusCode>(StatusCode::BAD_REQUEST).into_response()
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ pub async fn rate_limit_by_key(
|
|||
) -> Result<usize, CloseCode> {
|
||||
log::trace!("Incrementing rate limit counter for {key:?}...");
|
||||
let response: usize = redis::cmd("INCRBY")
|
||||
.arg(key)
|
||||
.arg(&key)
|
||||
.arg(increment)
|
||||
.query_async::<redis::aio::Connection, usize>(rconn).await
|
||||
.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:?}...");
|
||||
let _ = redis::cmd("EXPIRE")
|
||||
.arg(key)
|
||||
.arg(&key)
|
||||
.arg(expiration_s)
|
||||
.query_async::<redis::aio::Connection, ()>(rconn).await
|
||||
.log_err_to_warn("Could not set expiration for rate limit counter");
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
pub mod structs;
|
||||
pub mod stream;
|
||||
|
||||
mod axum;
|
||||
mod ws;
|
||||
mod ws_receive;
|
||||
mod redis_xadd;
|
||||
mod redis_xread;
|
||||
mod ws_send;
|
||||
mod limit;
|
||||
pub(self) mod axum;
|
||||
pub(self) mod ws;
|
||||
pub(self) mod ws_receive;
|
||||
pub(self) mod redis_xadd;
|
||||
pub(self) mod redis_xread;
|
||||
pub(self) mod ws_send;
|
||||
pub(self) mod limit;
|
||||
|
||||
pub(crate) use self::axum::handler as board_websocket;
|
||||
|
|
|
@ -4,7 +4,7 @@ use crate::task::{v1, VersionedBoardChange};
|
|||
|
||||
pub async fn xread_to_vbc(r: StreamReadReply) -> (Vec<VersionedBoardChange>, String) {
|
||||
log::trace!("Making sure that the Redis Stream existed...");
|
||||
let r = r.keys.first();
|
||||
let r = r.keys.get(0);
|
||||
|
||||
let mut current_id: String = "0".to_string();
|
||||
|
||||
|
|
|
@ -5,8 +5,8 @@ use serde::{Deserialize, Serialize};
|
|||
use uuid::Uuid;
|
||||
use crate::outcome::LoggableOutcome;
|
||||
use crate::routes::board::stream::xread_to_vbc;
|
||||
use crate::task::VersionedBoardChange;
|
||||
use crate::task::latest::{BoardChange, BoardState, Task};
|
||||
use crate::task::VersionedBoardChange::V2;
|
||||
use crate::task::v2::{BoardChange, BoardState, Task};
|
||||
|
||||
/// A request sent from a client to the server to perform a [`BoardAction`] on a board.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
|
@ -24,7 +24,7 @@ impl BoardRequest {
|
|||
match self.action {
|
||||
BoardAction::Title(title) => {
|
||||
log::debug!("Setting board Title: {title:?}");
|
||||
let operation = VersionedBoardChange::new_latest(BoardChange::Title(title));
|
||||
let operation = V2(BoardChange::Title(title));
|
||||
let _id = operation.store_in_redis_stream(rconn, &self.key).await;
|
||||
Ok(())
|
||||
},
|
||||
|
@ -32,25 +32,25 @@ impl BoardRequest {
|
|||
log::debug!("Creating Task: {task:?}");
|
||||
let id = Uuid::new_v4();
|
||||
log::trace!("Assigned id {id:?} to Task: {task:?}");
|
||||
let operation = VersionedBoardChange::new_latest(BoardChange::Task(id, Some(task)));
|
||||
let operation = V2(BoardChange::Task(id, Some(task)));
|
||||
let _id = operation.store_in_redis_stream(rconn, &self.key).await;
|
||||
Ok(())
|
||||
},
|
||||
BoardAction::Task(Some(id), Some(task)) => {
|
||||
log::debug!("Editing Task {id:?}: {task:?}");
|
||||
let operation = VersionedBoardChange::new_latest(BoardChange::Task(id, Some(task)));
|
||||
let operation = V2(BoardChange::Task(id, Some(task)));
|
||||
let _id = operation.store_in_redis_stream(rconn, &self.key).await;
|
||||
Ok(())
|
||||
},
|
||||
BoardAction::Task(Some(id), None) => {
|
||||
log::debug!("Deleting Task {id:?}...");
|
||||
let operation = VersionedBoardChange::new_latest(BoardChange::Task(id, None));
|
||||
let operation = V2(BoardChange::Task(id, None));
|
||||
let _id = operation.store_in_redis_stream(rconn, &self.key).await;
|
||||
Ok(())
|
||||
},
|
||||
BoardAction::Lock(lock) => {
|
||||
log::debug!("Setting board lock to: {lock:?}");
|
||||
let operation = VersionedBoardChange::new_latest(BoardChange::Lock(lock));
|
||||
let operation = V2(BoardChange::Lock(lock));
|
||||
let _id = operation.store_in_redis_stream(rconn, &self.key).await;
|
||||
Ok(())
|
||||
},
|
||||
|
@ -81,7 +81,7 @@ impl BoardRequest {
|
|||
});
|
||||
|
||||
log::trace!("Storing new full board state in the Redis Stream...");
|
||||
let id = VersionedBoardChange::new_latest(BoardChange::State(board))
|
||||
let id = V2(BoardChange::State(board))
|
||||
.store_in_redis_stream(rconn, &self.key).await
|
||||
.log_err_to_error("Failed to store trimmed board")
|
||||
.map_err(|_| 1011u16)?;
|
||||
|
|
|
@ -3,8 +3,8 @@ use std::sync::Arc;
|
|||
use deadqueue::unlimited::Queue;
|
||||
use futures_util::StreamExt;
|
||||
use uuid::Uuid;
|
||||
use crate::task::latest::BoardChange;
|
||||
use crate::task::VersionedBoardChange;
|
||||
use crate::task::VersionedBoardChange::V2;
|
||||
use crate::task::v2::BoardChange;
|
||||
use super::{redis_xread, redis_xadd, ws_receive, ws_send};
|
||||
|
||||
pub async fn handler(
|
||||
|
@ -65,7 +65,7 @@ pub async fn handler(
|
|||
log::trace!("Client UUID is: {client_uuid:?}");
|
||||
|
||||
log::trace!("Notifying clients of the new connection...");
|
||||
let _connect_id = VersionedBoardChange::new_latest(BoardChange::Connect(client_uuid)).store_in_redis_stream(&mut main_redis, &redis_key).await;
|
||||
let _connect_id = V2(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!("Creating synchronization structures...");
|
||||
|
@ -121,7 +121,7 @@ pub async fn handler(
|
|||
redis_xread_abort.abort();
|
||||
|
||||
log::trace!("Notifying clients of the disconnection...");
|
||||
let _connect_id = VersionedBoardChange::new_latest(BoardChange::Disconnect(client_uuid)).store_in_redis_stream(&mut main_redis, &redis_key).await;
|
||||
let _connect_id = V2(BoardChange::Disconnect(client_uuid)).store_in_redis_stream(&mut main_redis, &redis_key).await;
|
||||
log::trace!("Notified clients of the disconnection successfully!");
|
||||
|
||||
log::trace!("Waiting for the last messages to be sent...");
|
||||
|
|
|
@ -24,7 +24,7 @@ pub async fn handler(
|
|||
log::trace!("Connection rate limit is: {count} / 60 s");
|
||||
if count > 0 {
|
||||
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() {
|
||||
log::warn!("Hit rate limit, closing connection.");
|
||||
return Err(1008u16);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
pub mod board;
|
||||
pub mod structs;
|
||||
|
||||
mod root;
|
||||
pub(self) mod root;
|
||||
|
||||
pub(crate) use root::version as version_route;
|
||||
pub(crate) use root::healthcheck as healthcheck_route;
|
||||
|
|
|
@ -5,32 +5,23 @@ use crate::outcome::LoggableOutcome;
|
|||
|
||||
pub mod v1;
|
||||
pub mod v2;
|
||||
pub mod v3;
|
||||
|
||||
pub use v3 as latest;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum VersionedBoardChange {
|
||||
V1(v1::BoardChange),
|
||||
V2(v2::BoardChange),
|
||||
V3(v3::BoardChange),
|
||||
}
|
||||
|
||||
impl VersionedBoardChange {
|
||||
pub fn to_latest_bc(self) -> latest::BoardChange {
|
||||
pub fn to_latest_bc(self) -> v2::BoardChange {
|
||||
match self {
|
||||
Self::V1(bc) => Self::V2(v2::BoardChange::from(bc)).to_latest_bc(),
|
||||
Self::V2(bc) => Self::V3(v3::BoardChange::from(bc)).to_latest_bc(),
|
||||
Self::V3(bc) => bc,
|
||||
VersionedBoardChange::V1(bc) => bc.into(),
|
||||
VersionedBoardChange::V2(bc) => bc,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn to_latest(self) -> VersionedBoardChange {
|
||||
Self::V3(self.to_latest_bc())
|
||||
}
|
||||
|
||||
pub fn new_latest(bc: latest::BoardChange) -> VersionedBoardChange {
|
||||
Self::V3(bc)
|
||||
pub fn to_latest_vbc(self) -> VersionedBoardChange {
|
||||
Self::V2(self.to_latest_bc())
|
||||
}
|
||||
|
||||
pub(crate) async fn store_in_redis_stream(&self, rconn: &mut redis::aio::Connection, key: &str) -> Result<String, ()> {
|
||||
|
|
|
@ -8,10 +8,6 @@ use chrono::serde::{ts_milliseconds, ts_milliseconds_option};
|
|||
use uuid::Uuid;
|
||||
use super::v1;
|
||||
|
||||
pub use v1::TaskIcon;
|
||||
pub use v1::TaskImportance;
|
||||
pub use v1::TaskPriority;
|
||||
|
||||
/// A change to a board's contents.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
#[non_exhaustive]
|
||||
|
@ -39,13 +35,13 @@ pub struct Task {
|
|||
pub text: String,
|
||||
|
||||
#[serde(default)]
|
||||
pub icon: TaskIcon,
|
||||
pub icon: v1::TaskIcon,
|
||||
|
||||
#[serde(default)]
|
||||
pub importance: TaskImportance,
|
||||
pub importance: v1::TaskImportance,
|
||||
|
||||
#[serde(default)]
|
||||
pub priority: TaskPriority,
|
||||
pub priority: v1::TaskPriority,
|
||||
|
||||
/// When the task was created.
|
||||
#[serde(default, with = "ts_milliseconds")]
|
||||
|
|
|
@ -1,165 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|