From fe64c683fd0ee27737ea5f92df2e4ac63c4c4e6e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Johannes=20Schie=C3=9Fl?=
<150372753+johannesschiessl@users.noreply.github.com>
Date: Wed, 15 Jul 2026 07:52:21 +0200
Subject: [PATCH 1/2] Improve React code quality
---
apps/web/src/chats/ChatNavigation.ts | 33 +-
.../src/components/chats/ChatMessageBody.tsx | 9 +-
.../src/components/chats/ChatPresetDialog.tsx | 18 +-
.../src/components/chats/ChatWorkspace.tsx | 20 +-
.../connections/ConnectionDialog.tsx | 124 +++--
.../components/live/LiveSongNavigation.tsx | 9 +-
.../components/microphones/MicrophoneCard.tsx | 182 +++++++
.../microphones/MicrophoneDeleteDialog.tsx | 81 +++
apps/web/src/components/mixes/MixCard.tsx | 189 +++++++
.../src/components/mixes/MixDeleteDialog.tsx | 77 +++
.../components/profiles/ProfileSwitcher.tsx | 20 +-
.../src/components/shows/ShowComingSoon.tsx | 17 -
.../src/components/shows/ShowDeleteDialog.tsx | 4 +-
apps/web/src/components/shows/ShowList.tsx | 4 +-
.../src/components/songs/MicrophoneName.tsx | 65 +++
.../src/components/songs/SongDeleteDialog.tsx | 75 +++
apps/web/src/components/songs/SongDetail.tsx | 272 ++++++++++
.../components/songs/SongMixAssignments.tsx | 154 ++++++
apps/web/src/components/ui/badge.tsx | 2 +-
apps/web/src/components/ui/button.tsx | 2 +-
apps/web/src/components/ui/item.tsx | 11 +-
apps/web/src/components/ui/tabs.tsx | 2 +-
apps/web/src/connection.ts | 2 -
apps/web/src/main.tsx | 12 +
.../src/notifications/NotificationCenter.ts | 9 -
apps/web/src/profiles/currentProfilesState.ts | 12 +
.../src/routes/shows/$showId/microphones.tsx | 258 +--------
apps/web/src/routes/shows/$showId/mixes.tsx | 261 +--------
.../routes/shows/$showId/setlist/$songId.tsx | 512 +-----------------
.../routes/shows/$showId/setlist/index.tsx | 4 +-
30 files changed, 1292 insertions(+), 1148 deletions(-)
create mode 100644 apps/web/src/components/microphones/MicrophoneCard.tsx
create mode 100644 apps/web/src/components/microphones/MicrophoneDeleteDialog.tsx
create mode 100644 apps/web/src/components/mixes/MixCard.tsx
create mode 100644 apps/web/src/components/mixes/MixDeleteDialog.tsx
delete mode 100644 apps/web/src/components/shows/ShowComingSoon.tsx
create mode 100644 apps/web/src/components/songs/MicrophoneName.tsx
create mode 100644 apps/web/src/components/songs/SongDeleteDialog.tsx
create mode 100644 apps/web/src/components/songs/SongDetail.tsx
create mode 100644 apps/web/src/components/songs/SongMixAssignments.tsx
create mode 100644 apps/web/src/profiles/currentProfilesState.ts
diff --git a/apps/web/src/chats/ChatNavigation.ts b/apps/web/src/chats/ChatNavigation.ts
index f7d6488..d5ecc89 100644
--- a/apps/web/src/chats/ChatNavigation.ts
+++ b/apps/web/src/chats/ChatNavigation.ts
@@ -1,27 +1,28 @@
-import { router } from "@/router";
-import { makeChatNavigation } from "./ChatNavigationState";
+import type { ShowId } from "@showtime/contracts";
+import { makeChatNavigation, type ChatOpenRequest } from "./ChatNavigationState";
export type { ChatOpenRequest } from "./ChatNavigationState";
const eventName = "showtime-chat-open";
-const activeShowId = () => {
- for (const match of router.state.matches) {
- const showId = (match.params as { readonly showId?: unknown }).showId;
- if (typeof showId === "string") return showId;
- }
- return undefined;
-};
+let chatNavigation: ReturnType | undefined;
-const chatNavigation = makeChatNavigation({
- getActiveShowId: activeShowId,
- navigateToShow: (showId) => router.navigate({ to: "/shows/$showId", params: { showId } }),
- publishOpenRequest: () => window.dispatchEvent(new Event(eventName)),
-});
+export const configureChatNavigation = (options: {
+ readonly getActiveShowId: () => string | undefined;
+ readonly navigateToShow: (showId: ShowId) => Promise;
+}) => {
+ chatNavigation = makeChatNavigation({
+ ...options,
+ publishOpenRequest: () => window.dispatchEvent(new Event(eventName)),
+ });
+};
-export const openChat = chatNavigation.open;
+export const openChat = (request: ChatOpenRequest) => {
+ if (!chatNavigation) throw new Error("Chat navigation has not been configured");
+ return chatNavigation.open(request);
+};
-export const consumeChatOpenRequest = chatNavigation.consume;
+export const consumeChatOpenRequest = (showId: ShowId) => chatNavigation?.consume(showId);
export const subscribeChatOpenRequests = (listener: () => void) => {
window.addEventListener(eventName, listener);
diff --git a/apps/web/src/components/chats/ChatMessageBody.tsx b/apps/web/src/components/chats/ChatMessageBody.tsx
index 8005236..3bd7f60 100644
--- a/apps/web/src/components/chats/ChatMessageBody.tsx
+++ b/apps/web/src/components/chats/ChatMessageBody.tsx
@@ -11,14 +11,17 @@ export function ChatMessageBody({
readonly parts?: ReadonlyArray;
}) {
if (!parts?.length) return body;
- return parts.map((part, index) => {
- if (part.type === "text") return {part.text};
+ let characterOffset = 0;
+ return parts.map((part) => {
+ const key = `${part.type}:${characterOffset}:${part.text}`;
+ characterOffset += part.text.length;
+ if (part.type === "text") return {part.text};
const colors = microphoneColorClassNames[part.color];
const label = part.type === "microphone" ? "Microphone" : "Mix";
const Icon = part.type === "microphone" ? Mic2Icon : SpeakerIcon;
return (
({ type: "list" });
- React.useEffect(() => {
- if (!open) setMode({ type: "list" });
- }, [open]);
-
const back = () => setMode({ type: "list" });
+ const changeOpen = (nextOpen: boolean) => {
+ if (!nextOpen) back();
+ onOpenChange(nextOpen);
+ };
const title =
mode.type === "list"
? "Message presets"
@@ -142,7 +142,7 @@ export function ChatPresetDialog({
: "New preset";
return (
-
-
+
);
}
diff --git a/apps/web/src/components/connections/ConnectionDialog.tsx b/apps/web/src/components/connections/ConnectionDialog.tsx
index 58cc525..a2b5b21 100644
--- a/apps/web/src/components/connections/ConnectionDialog.tsx
+++ b/apps/web/src/components/connections/ConnectionDialog.tsx
@@ -371,6 +371,7 @@ export function ConnectionDialog({
profilesResult={profilesResult}
/>
();
- React.useEffect(() => {
- if (!open) return;
- setDraft(currentState.hostName);
- setConfirming(false);
- setSaving(false);
- setError(undefined);
- }, [open, currentState.hostName]);
-
const candidate = draft.trim() ? normalizeShowtimeHostName(draft) : undefined;
const hostname = candidate ? `showtime-${candidate}.local` : undefined;
const changed = candidate !== undefined && candidate !== currentState.hostName;
diff --git a/apps/web/src/components/connections/ConnectionLinkDialog.tsx b/apps/web/src/components/connections/ConnectionLinkDialog.tsx
index be5bb7d..f538a2e 100644
--- a/apps/web/src/components/connections/ConnectionLinkDialog.tsx
+++ b/apps/web/src/components/connections/ConnectionLinkDialog.tsx
@@ -26,16 +26,6 @@ export function ConnectionLinkDialog({
const [message, setMessage] = React.useState();
const [connectionUrl, setConnectionUrl] = React.useState("");
- React.useEffect(() => {
- if (!open) {
- setConnectionUrl("");
- return;
- }
- handledRef.current = false;
- setState("idle");
- setMessage(undefined);
- }, [open]);
-
const connect = React.useCallback(async (value: string) => {
const standalone = isStandalonePwa();
const pairingUrl = showtimePairingNavigationUrl(value, window.location.href, standalone);
diff --git a/apps/web/src/components/microphones/MicrophoneCard.tsx b/apps/web/src/components/microphones/MicrophoneCard.tsx
index a154da8..957317d 100644
--- a/apps/web/src/components/microphones/MicrophoneCard.tsx
+++ b/apps/web/src/components/microphones/MicrophoneCard.tsx
@@ -38,13 +38,6 @@ export function MicrophoneCard({
const [name, setName] = React.useState(microphone.name ?? "");
const [color, setColor] = React.useState(microphone.color);
const [saveError, setSaveError] = React.useState();
- React.useEffect(() => {
- setNumber(String(microphone.number));
- setName(microphone.name ?? "");
- setColor(microphone.color);
- setSaveError(undefined);
- }, [microphone.color, microphone.name, microphone.number]);
-
const save = async (next: { number?: string; name?: string; color?: Color }) => {
setSaveError(undefined);
const result = await edit({
diff --git a/apps/web/src/components/microphones/MicrophoneDeleteDialog.tsx b/apps/web/src/components/microphones/MicrophoneDeleteDialog.tsx
index 460b235..64b6b0c 100644
--- a/apps/web/src/components/microphones/MicrophoneDeleteDialog.tsx
+++ b/apps/web/src/components/microphones/MicrophoneDeleteDialog.tsx
@@ -37,14 +37,17 @@ export function MicrophoneDeleteDialog({
if (!microphone) return;
setIsDeleting(true);
setDeleteError(undefined);
- const result = await deleteMicrophone({
- payload: { showId, id: microphone.id },
- reactivityKeys: microphonesRpcReactivityKey(showId),
- });
- if (Exit.isSuccess(result)) {
- onClose();
- } else {
- setDeleteError(rpcErrorMessageFromCause(result.cause));
+ try {
+ const result = await deleteMicrophone({
+ payload: { showId, id: microphone.id },
+ reactivityKeys: microphonesRpcReactivityKey(showId),
+ });
+ if (Exit.isSuccess(result)) {
+ onClose();
+ } else {
+ setDeleteError(rpcErrorMessageFromCause(result.cause));
+ }
+ } finally {
setIsDeleting(false);
}
};
diff --git a/apps/web/src/components/mixes/MixCard.tsx b/apps/web/src/components/mixes/MixCard.tsx
index 73a65b7..045d541 100644
--- a/apps/web/src/components/mixes/MixCard.tsx
+++ b/apps/web/src/components/mixes/MixCard.tsx
@@ -40,13 +40,6 @@ export function MixCard({
const [name, setName] = React.useState(mix.name ?? "");
const [color, setColor] = React.useState(mix.color);
const [saveError, setSaveError] = React.useState();
- React.useEffect(() => {
- setNumber(String(mix.number));
- setName(mix.name ?? "");
- setColor(mix.color);
- setSaveError(undefined);
- }, [mix.color, mix.name, mix.number]);
-
const save = async (next: { number?: string; name?: string; color?: Color }) => {
setSaveError(undefined);
const result = await edit({
diff --git a/apps/web/src/components/mixes/MixDeleteDialog.tsx b/apps/web/src/components/mixes/MixDeleteDialog.tsx
index cf29c6b..5a2de9c 100644
--- a/apps/web/src/components/mixes/MixDeleteDialog.tsx
+++ b/apps/web/src/components/mixes/MixDeleteDialog.tsx
@@ -37,14 +37,17 @@ export function MixDeleteDialog({
if (!mix) return;
setIsDeleting(true);
setDeleteError(undefined);
- const result = await deleteMix({
- payload: { showId, id: mix.id },
- reactivityKeys: mixesRpcReactivityKey(showId),
- });
- if (Exit.isSuccess(result)) {
- onClose();
- } else {
- setDeleteError(rpcErrorMessageFromCause(result.cause));
+ try {
+ const result = await deleteMix({
+ payload: { showId, id: mix.id },
+ reactivityKeys: mixesRpcReactivityKey(showId),
+ });
+ if (Exit.isSuccess(result)) {
+ onClose();
+ } else {
+ setDeleteError(rpcErrorMessageFromCause(result.cause));
+ }
+ } finally {
setIsDeleting(false);
}
};
diff --git a/apps/web/src/components/shows/ShowDeleteDialog.tsx b/apps/web/src/components/shows/ShowDeleteDialog.tsx
index eb5b552..07ed3cb 100644
--- a/apps/web/src/components/shows/ShowDeleteDialog.tsx
+++ b/apps/web/src/components/shows/ShowDeleteDialog.tsx
@@ -64,24 +64,32 @@ export function ShowDeleteDialog({ onDeleted }: ShowDeleteDialogProps) {
setIsDeleting(true);
setDeleteError(undefined);
- const result = await deleteShow({
- payload: {
- id: deletingShowId,
- },
- ...showMutationOptions,
- });
+ try {
+ const result = await deleteShow({
+ payload: {
+ id: deletingShowId,
+ },
+ ...showMutationOptions,
+ });
- const currentDialog = dialogRef.current;
- if (currentDialog.type !== "delete" || currentDialog.show.id !== deletingShowId) {
- return;
- }
+ const currentDialog = dialogRef.current;
+ if (currentDialog.type !== "delete" || currentDialog.show.id !== deletingShowId) {
+ return;
+ }
- if (Exit.isSuccess(result)) {
- close();
- notifyDeleted();
- } else {
- setDeleteError(rpcErrorMessageFromCause(result.cause));
- setIsDeleting(false);
+ if (Exit.isSuccess(result)) {
+ close();
+ notifyDeleted();
+ } else {
+ setDeleteError(rpcErrorMessageFromCause(result.cause));
+ }
+ } finally {
+ setIsDeleting((current) => {
+ const currentDialog = dialogRef.current;
+ return currentDialog.type === "delete" && currentDialog.show.id === deletingShowId
+ ? false
+ : current;
+ });
}
};
diff --git a/apps/web/src/components/shows/ShowFormDialog.tsx b/apps/web/src/components/shows/ShowFormDialog.tsx
index 2a21338..4d93d3a 100644
--- a/apps/web/src/components/shows/ShowFormDialog.tsx
+++ b/apps/web/src/components/shows/ShowFormDialog.tsx
@@ -57,39 +57,38 @@ export function ShowFormDialog() {
if (trimmed.length === 0) {
return;
}
+ if (dialog.type !== "create" && dialog.type !== "edit") {
+ return;
+ }
setIsSubmitting(true);
setSubmitError(undefined);
- const result =
- dialog.type === "edit"
- ? await editShow({
- payload: {
- id: dialog.show.id,
- name: trimmed as ShowName,
- color,
- },
- ...showMutationOptions,
- })
- : dialog.type === "create"
- ? await createShow({
+ try {
+ const result =
+ dialog.type === "edit"
+ ? await editShow({
payload: {
+ id: dialog.show.id,
name: trimmed as ShowName,
color,
},
...showMutationOptions,
})
- : undefined;
-
- if (result === undefined) {
- setIsSubmitting(false);
- return;
- }
+ : await createShow({
+ payload: {
+ name: trimmed as ShowName,
+ color,
+ },
+ ...showMutationOptions,
+ });
- if (Exit.isSuccess(result)) {
- close();
- } else {
- setSubmitError(rpcErrorMessageFromCause(result.cause));
+ if (Exit.isSuccess(result)) {
+ close();
+ } else {
+ setSubmitError(rpcErrorMessageFromCause(result.cause));
+ }
+ } finally {
setIsSubmitting(false);
}
},
diff --git a/apps/web/src/components/songs/SongDeleteDialog.tsx b/apps/web/src/components/songs/SongDeleteDialog.tsx
index cc6e12e..e7057d2 100644
--- a/apps/web/src/components/songs/SongDeleteDialog.tsx
+++ b/apps/web/src/components/songs/SongDeleteDialog.tsx
@@ -35,17 +35,21 @@ export function SongDeleteDialog({
const [error, setError] = React.useState();
const confirm = async () => {
setIsDeleting(true);
- const result = await deleteSong({
- payload: { showId, id: songId },
- reactivityKeys: songsRpcReactivityKey(showId),
- });
- if (Exit.isFailure(result)) {
- setError(rpcErrorMessageFromCause(result.cause));
+ setError(undefined);
+ try {
+ const result = await deleteSong({
+ payload: { showId, id: songId },
+ reactivityKeys: songsRpcReactivityKey(showId),
+ });
+ if (Exit.isFailure(result)) {
+ setError(rpcErrorMessageFromCause(result.cause));
+ return;
+ }
+ onOpenChange(false);
+ void navigate({ to: "/shows/$showId/setlist", params: { showId } });
+ } finally {
setIsDeleting(false);
- return;
}
- onOpenChange(false);
- void navigate({ to: "/shows/$showId/setlist", params: { showId } });
};
return (
diff --git a/apps/web/src/routes/shows/$showId/microphones.tsx b/apps/web/src/routes/shows/$showId/microphones.tsx
index 8f0d580..6e4b2fb 100644
--- a/apps/web/src/routes/shows/$showId/microphones.tsx
+++ b/apps/web/src/routes/shows/$showId/microphones.tsx
@@ -1,5 +1,6 @@
import * as React from "react";
import { createFileRoute } from "@tanstack/react-router";
+import { DateTime } from "effect";
import { AsyncResult } from "effect/unstable/reactivity";
import { AlertCircleIcon, Mic2Icon } from "lucide-react";
import type { ShowId } from "@showtime/contracts";
@@ -63,7 +64,7 @@ function RouteComponent() {
{microphones.map((microphone) => (
{mixes.map((mix) => (