From a9d797f552f032de6303b0c9076c3ca6e89f23cf Mon Sep 17 00:00:00 2001 From: hfcRed Date: Sat, 14 Mar 2026 16:31:04 +0100 Subject: [PATCH] Merge branch 'css-rework-sidenav' of https://github.com/sendou-ink/sendou.ink into css-rework-sidenav --- app/components/layout/ChatSidebar.tsx | 22 +++++++++- app/features/chat/chat-utils.test.ts | 44 ++++++++++++++++++++ app/features/chat/chat-utils.ts | 15 +++++++ app/features/scrims/actions/scrims.server.ts | 8 ++-- 4 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 app/features/chat/chat-utils.test.ts diff --git a/app/components/layout/ChatSidebar.tsx b/app/components/layout/ChatSidebar.tsx index 16f1ba711..4087b93fc 100644 --- a/app/components/layout/ChatSidebar.tsx +++ b/app/components/layout/ChatSidebar.tsx @@ -3,12 +3,15 @@ import { ArrowLeft, MessageSquare, X } from "lucide-react"; import { Button } from "react-aria-components"; import { useTranslation } from "react-i18next"; import { Link } from "react-router"; +import { resolveDatePlaceholders } from "~/features/chat/chat-utils"; import { Chat } from "~/features/chat/components/Chat"; import { useChatContext } from "~/features/chat/useChatContext"; import { useTimeFormat } from "~/hooks/useTimeFormat"; import sideNavStyles from "../SideNav.module.css"; import styles from "./ChatSidebar.module.css"; +// xxx: add navIcon/url per room + export function ChatSidebar({ onClose }: { onClose?: () => void }) { const chatContext = useChatContext(); @@ -94,7 +97,14 @@ function RoomList({ onClose }: { onClose?: () => void }) { styles.roomName, )} > - {room.header} + {resolveDatePlaceholders(room.header, (d) => + formatDateTime(d, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + }), + )} {room.subtitle} @@ -122,6 +132,7 @@ function RoomList({ onClose }: { onClose?: () => void }) { function ChatView({ onClose }: { onClose?: () => void }) { const chatContext = useChatContext()!; const activeRoom = chatContext.activeRoom!; + const { formatDateTime } = useTimeFormat(); const otherRoomsUnreadCount = Object.entries(chatContext.unreadCounts) .filter(([code]) => code !== activeRoom) @@ -147,7 +158,14 @@ function ChatView({ onClose }: { onClose?: () => void }) { const headerContent = ( <> - {room?.header ?? activeRoom} + {resolveDatePlaceholders(room?.header ?? activeRoom, (d) => + formatDateTime(d, { + month: "short", + day: "numeric", + hour: "numeric", + minute: "numeric", + }), + )} {room?.subtitle ? ( {room.subtitle} diff --git a/app/features/chat/chat-utils.test.ts b/app/features/chat/chat-utils.test.ts new file mode 100644 index 000000000..f6c8349c5 --- /dev/null +++ b/app/features/chat/chat-utils.test.ts @@ -0,0 +1,44 @@ +import { describe, expect, test } from "vitest"; +import { datePlaceholder, resolveDatePlaceholders } from "./chat-utils"; + +describe("datePlaceholder", () => { + test("returns correctly formatted placeholder string", () => { + const date = new Date(1700000000000); + + expect(datePlaceholder(date)).toBe("{{date:1700000000000}}"); + }); +}); + +describe("resolveDatePlaceholders", () => { + const mockFormat = (d: Date) => `FORMATTED:${d.getTime()}`; + + test("replaces a single placeholder with formatted date", () => { + const text = "Starts at {{date:1700000000000}}"; + + expect(resolveDatePlaceholders(text, mockFormat)).toBe( + "Starts at FORMATTED:1700000000000", + ); + }); + + test("replaces multiple placeholders in one string", () => { + const text = "From {{date:1700000000000}} to {{date:1700003600000}}"; + + expect(resolveDatePlaceholders(text, mockFormat)).toBe( + "From FORMATTED:1700000000000 to FORMATTED:1700003600000", + ); + }); + + test("returns text unchanged when no placeholders present", () => { + const text = "Just a normal string"; + + expect(resolveDatePlaceholders(text, mockFormat)).toBe(text); + }); + + test("handles text that is only a placeholder", () => { + const text = "{{date:1700000000000}}"; + + expect(resolveDatePlaceholders(text, mockFormat)).toBe( + "FORMATTED:1700000000000", + ); + }); +}); diff --git a/app/features/chat/chat-utils.ts b/app/features/chat/chat-utils.ts index d23594587..56de7d0e1 100644 --- a/app/features/chat/chat-utils.ts +++ b/app/features/chat/chat-utils.ts @@ -1,5 +1,20 @@ import type { ChatMessage } from "./chat-types"; +const DATE_PLACEHOLDER_PATTERN = /\{\{date:(\d+)\}\}/g; + +export function datePlaceholder(date: Date): string { + return `{{date:${date.getTime()}}}`; +} + +export function resolveDatePlaceholders( + text: string, + formatDateTime: (date: Date) => string, +): string { + return text.replace(DATE_PLACEHOLDER_PATTERN, (_match, ts) => + formatDateTime(new Date(Number(ts))), + ); +} + export function messageTypeToSound(type: ChatMessage["type"]) { if (type === "LIKE_RECEIVED") return "sq_like"; if (type === "MATCH_STARTED") return "sq_match"; diff --git a/app/features/scrims/actions/scrims.server.ts b/app/features/scrims/actions/scrims.server.ts index 96cd5cb5a..a894b975b 100644 --- a/app/features/scrims/actions/scrims.server.ts +++ b/app/features/scrims/actions/scrims.server.ts @@ -3,6 +3,7 @@ import type { ActionFunctionArgs } from "react-router"; import { redirect } from "react-router"; import { requireUser } from "~/features/auth/core/user.server"; import * as ChatSystemMessage from "~/features/chat/ChatSystemMessage.server"; +import { datePlaceholder } from "~/features/chat/chat-utils"; import { notify } from "~/features/notifications/core/notify.server"; import * as UserRepository from "~/features/user-page/UserRepository.server"; import { requirePermission } from "~/modules/permissions/guards.server"; @@ -113,9 +114,10 @@ export const action = async ({ request }: ActionFunctionArgs) => { if (fullPost?.chatCode) { ChatSystemMessage.setMetadata({ chatCode: fullPost.chatCode, - header: "Scrim", - // xxx: better subtitle? - subtitle: `Scrim #${post.id}`, + header: datePlaceholder( + databaseTimestampToDate(request.at ?? post.at), + ), + subtitle: "Scrim", url: scrimPage(post.id), participantUserIds: Scrim.participantIdsListFromAccepted(fullPost), expiresAt: add(databaseTimestampToDate(request.at ?? post.at), {