diff --git a/todoblue/src/app/[lang]/board/[board]/(api)/(signal)/BoardSignal.ts b/todoblue/src/app/[lang]/board/[board]/(api)/(signal)/BoardSignal.ts
index 5252c37..591aa2a 100644
--- a/todoblue/src/app/[lang]/board/[board]/(api)/(signal)/BoardSignal.ts
+++ b/todoblue/src/app/[lang]/board/[board]/(api)/(signal)/BoardSignal.ts
@@ -1,5 +1,7 @@
+import {ConnectBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/ConnectBoardSignal"
+import {DisconnectBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/DisconnectBoardSignal"
import {TaskBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/TaskBoardSignal"
import {TitleBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/TitleBoardSignal"
-export type BoardSignal = TaskBoardSignal | TitleBoardSignal;
+export type BoardSignal = TaskBoardSignal | TitleBoardSignal | ConnectBoardSignal | DisconnectBoardSignal;
diff --git a/todoblue/src/app/[lang]/board/[board]/(api)/(signal)/ConnectBoardSignal.ts b/todoblue/src/app/[lang]/board/[board]/(api)/(signal)/ConnectBoardSignal.ts
new file mode 100644
index 0000000..df81e8f
--- /dev/null
+++ b/todoblue/src/app/[lang]/board/[board]/(api)/(signal)/ConnectBoardSignal.ts
@@ -0,0 +1,6 @@
+/**
+ * **Object** signaling the connection of a new client to the board.
+ */
+export type ConnectBoardSignal = {
+ "Connect": string,
+}
diff --git a/todoblue/src/app/[lang]/board/[board]/(api)/(signal)/DisconnectBoardSignal.ts b/todoblue/src/app/[lang]/board/[board]/(api)/(signal)/DisconnectBoardSignal.ts
new file mode 100644
index 0000000..1cd2227
--- /dev/null
+++ b/todoblue/src/app/[lang]/board/[board]/(api)/(signal)/DisconnectBoardSignal.ts
@@ -0,0 +1,6 @@
+/**
+ * **Object** signaling the disconnection of a client from the board.
+ */
+export type DisconnectBoardSignal = {
+ "Disconnect": string,
+}
diff --git a/todoblue/src/app/[lang]/board/[board]/(api)/(state)/BoardState.ts b/todoblue/src/app/[lang]/board/[board]/(api)/(state)/BoardState.ts
index 65c67c0..6a53c14 100644
--- a/todoblue/src/app/[lang]/board/[board]/(api)/(state)/BoardState.ts
+++ b/todoblue/src/app/[lang]/board/[board]/(api)/(state)/BoardState.ts
@@ -7,4 +7,5 @@ import {Task} from "@/app/[lang]/board/[board]/(api)/(task)"
export type BoardState = {
title: string,
tasksById: {[key: string]: Task},
+ connectedClients: string[],
}
diff --git a/todoblue/src/app/[lang]/board/[board]/(api)/(state)/boardReducer.ts b/todoblue/src/app/[lang]/board/[board]/(api)/(state)/boardReducer.ts
index 9995059..4bef106 100644
--- a/todoblue/src/app/[lang]/board/[board]/(api)/(state)/boardReducer.ts
+++ b/todoblue/src/app/[lang]/board/[board]/(api)/(state)/boardReducer.ts
@@ -1,4 +1,6 @@
import {BoardSignal, DeleteTaskBoardSignal, TaskBoardSignal, TitleBoardSignal, UpdateTaskBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)"
+import {ConnectBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/ConnectBoardSignal"
+import {DisconnectBoardSignal} from "@/app/[lang]/board/[board]/(api)/(signal)/DisconnectBoardSignal"
import {BoardState} from "@/app/[lang]/board/[board]/(api)/(state)/BoardState"
import {DEFAULT_BOARD_STATE} from "@/app/[lang]/board/[board]/(api)/(state)/defaultBoardState"
@@ -41,6 +43,28 @@ export function boardReducer(state: BoardState, action: BoardSignal | null) {
}
return {...state, tasksById}
}
+ else if("Connect" in action) {
+ const connectAction = action as ConnectBoardSignal;
+ const id = connectAction["Connect"];
+ const connectedClients = [...state.connectedClients, id]
+ console.debug("[boardReducer] Adding new client:", id)
+ return {...state, connectedClients}
+ }
+ else if("Disconnect" in action) {
+ const disconnectAction = action as DisconnectBoardSignal;
+ const id = disconnectAction["Disconnect"];
+ const connectedClients = [...state.connectedClients]
+ const clientIndex = connectedClients.indexOf(id)
+ if(clientIndex !== -1) {
+ connectedClients.splice(clientIndex, 1);
+ console.debug("[boardReducer] Removing client:", id)
+ return {...state, connectedClients}
+ }
+ else {
+ console.warn("[boardReducer] Received DisconnectBoardSignal without the client being connected in first place.")
+ return state
+ }
+ }
else {
console.warn("[boardReducer] Received unknown signal, ignoring:", action)
return state
diff --git a/todoblue/src/app/[lang]/board/[board]/(api)/(state)/defaultBoardState.ts b/todoblue/src/app/[lang]/board/[board]/(api)/(state)/defaultBoardState.ts
index b09e46b..632fdc4 100644
--- a/todoblue/src/app/[lang]/board/[board]/(api)/(state)/defaultBoardState.ts
+++ b/todoblue/src/app/[lang]/board/[board]/(api)/(state)/defaultBoardState.ts
@@ -4,4 +4,4 @@ import {BoardState} from "@/app/[lang]/board/[board]/(api)/(state)/BoardState"
/**
* **Object** denoting the {@link BoardState} of a board where no {@link BoardAction}s have been performed.
*/
-export const DEFAULT_BOARD_STATE: BoardState = {title: "", tasksById: {}}
+export const DEFAULT_BOARD_STATE: BoardState = {title: "", tasksById: {}, connectedClients: []}
diff --git a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/BoardHeader.module.css b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/BoardHeader.module.css
index eba2815..f9315cf 100644
--- a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/BoardHeader.module.css
+++ b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/BoardHeader.module.css
@@ -58,9 +58,6 @@
var(--bhsl-background-lightness),
100%
);
-
- width: 36px;
- height: 36px;
}
.leftButtonsArea {
diff --git a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/BoardHeader.tsx b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/BoardHeader.tsx
index 87bccf2..4f533b3 100644
--- a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/BoardHeader.tsx
+++ b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/BoardHeader.tsx
@@ -3,6 +3,7 @@ import {CycleColumningButton} from "@/app/[lang]/board/[board]/(page)/(header)/C
import {CycleGroupingButton} from "@/app/[lang]/board/[board]/(page)/(header)/CycleGroupingButton"
import {CycleSortingButton} from "@/app/[lang]/board/[board]/(page)/(header)/CycleSortingButton"
import {NavigateHomeButton} from "@/app/[lang]/board/[board]/(page)/(header)/NavigateHomeButton"
+import {ConnectedClientsButton} from "@/app/[lang]/board/[board]/(page)/(header)/ConnectedClientsButton"
import {ToggleEditingButton} from "@/app/[lang]/board/[board]/(page)/(header)/ToggleEditingButton"
import {ToggleStarredButton} from "@/app/[lang]/board/[board]/(page)/(header)/ToggleStarredButton"
import {useBoardLayoutEditor} from "@/app/[lang]/board/[board]/(page)/useBoardLayoutEditor"
@@ -25,7 +26,7 @@ export function BoardHeader({lang, className, metadataHook, layoutHook: {columni
-
+
+
diff --git a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css
new file mode 100644
index 0000000..ac6946b
--- /dev/null
+++ b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css
@@ -0,0 +1,16 @@
+.block {
+ display: inline-flex;
+ justify-content: center;
+ align-items: center;
+
+ height: 36px;
+ padding: 0.125em 0.75ex;
+}
+
+.singleBlock {
+ width: 36px;
+}
+
+.doubleBlock {
+ width: 76px;
+}
diff --git a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/ConnectedClientsButton.tsx b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/ConnectedClientsButton.tsx
new file mode 100644
index 0000000..0ee3aeb
--- /dev/null
+++ b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/ConnectedClientsButton.tsx
@@ -0,0 +1,25 @@
+import {useClientTranslation} from "@/app/(i18n)/client"
+import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
+import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
+import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
+import {faUsers} from "@fortawesome/free-solid-svg-icons"
+import cn from "classnames"
+
+
+export function ConnectedClientsButton({lang}: {lang: string}) {
+ const {isReady, boardState: {connectedClients}} = useBoardConsumer()
+ const {t} = useClientTranslation(lang, "board")
+
+ if(!isReady) return null;
+
+ return (
+
+
+
+ {connectedClients.length}
+
+ )
+}
diff --git a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleColumningButton.tsx b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleColumningButton.tsx
index f6efe30..b333340 100644
--- a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleColumningButton.tsx
+++ b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleColumningButton.tsx
@@ -1,5 +1,7 @@
import {useClientTranslation} from "@/app/(i18n)/client"
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
+import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
+import cn from "classnames"
import {COLUMNING_MODE_TO_ICON, ColumningMode} from "../(view)/(columning)"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
@@ -14,6 +16,7 @@ export function CycleColumningButton({lang, value, next}: {lang: string, value:
diff --git a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleGroupingButton.tsx b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleGroupingButton.tsx
index 8090076..f3942d7 100644
--- a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleGroupingButton.tsx
+++ b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleGroupingButton.tsx
@@ -1,7 +1,9 @@
import {useClientTranslation} from "@/app/(i18n)/client"
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
+import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {faObjectGroup} from "@fortawesome/free-solid-svg-icons"
+import cn from "classnames"
export function CycleGroupingButton({lang, next}: {lang: string, next: () => void}) {
@@ -14,6 +16,7 @@ export function CycleGroupingButton({lang, next}: {lang: string, next: () => voi
diff --git a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleSortingButton.tsx b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleSortingButton.tsx
index 270213d..f3395cb 100644
--- a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleSortingButton.tsx
+++ b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/CycleSortingButton.tsx
@@ -1,7 +1,9 @@
import {useClientTranslation} from "@/app/(i18n)/client"
import {useBoardConsumer} from "@/app/[lang]/board/[board]/(layout)/(contextBoard)"
+import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
import {faArrowDownWideShort} from "@fortawesome/free-solid-svg-icons"
+import cn from "classnames"
export function CycleSortingButton({lang, next}: {lang: string, next: () => void}) {
@@ -14,6 +16,7 @@ export function CycleSortingButton({lang, next}: {lang: string, next: () => void
diff --git a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/NavigateHomeButton.tsx b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/NavigateHomeButton.tsx
index 43cea5d..75c30e0 100644
--- a/todoblue/src/app/[lang]/board/[board]/(page)/(header)/NavigateHomeButton.tsx
+++ b/todoblue/src/app/[lang]/board/[board]/(page)/(header)/NavigateHomeButton.tsx
@@ -1,6 +1,8 @@
import {useClientTranslation} from "@/app/(i18n)/client"
+import style from "@/app/[lang]/board/[board]/(page)/(header)/BoardHeaderButtons.module.css"
import {faHouse} from "@fortawesome/free-solid-svg-icons"
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"
+import cn from "classnames"
import {useRouter} from "next/navigation"
import {useCallback} from "react"
@@ -11,7 +13,11 @@ export function NavigateHomeButton({lang}: {lang: string}) {
const goHome = useCallback(() => router.push("/"), [router])
return (
-