From ff4c6f07e3f2c71c06f0ac2edeec9f9d74ce28e3 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Tue, 1 Sep 2026 18:15:47 +0200 Subject: [PATCH] Make the participation code copyable as a shareable invite link The code was shown as plain text on the game card with no way to copy it. It's now a button that copies a full invite link (origin/games/{id}?code={code}) to the clipboard instead of just the bare code, since the recipient still needs the game id to get there. GameSession's join form reads that ?code= param on mount and pre-fills the input, so following the link lands straight on "click Beitreten" instead of needing the code typed in by hand. Co-Authored-By: Claude Sonnet 5 --- frontend/src/components/GameCard.tsx | 31 ++++++++++++++++++++++++++-- frontend/src/pages/GameSession.tsx | 5 +++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/GameCard.tsx b/frontend/src/components/GameCard.tsx index 7d88390..9ea32ba 100644 --- a/frontend/src/components/GameCard.tsx +++ b/frontend/src/components/GameCard.tsx @@ -1,9 +1,20 @@ +import { useState } from "react"; import { Link } from "react-router-dom"; import { Game } from "../api/games"; export default function GameCard({ game }: { game: Game }) { const created = new Date(game.created_at).toLocaleString("de-DE"); + const [copied, setCopied] = useState(false); + + async function handleCopyLink(e: React.MouseEvent) { + e.preventDefault(); + e.stopPropagation(); + const link = `${window.location.origin}/games/${game.id}?code=${game.participation_code}`; + await navigator.clipboard.writeText(link); + setCopied(true); + setTimeout(() => setCopied(false), 1500); + } return ( )} {game.participation_code && ( - + )}

{game.description}

diff --git a/frontend/src/pages/GameSession.tsx b/frontend/src/pages/GameSession.tsx index 35d69b0..47961ee 100644 --- a/frontend/src/pages/GameSession.tsx +++ b/frontend/src/pages/GameSession.tsx @@ -1,5 +1,5 @@ import { FormEvent, useEffect, useState } from "react"; -import { Link, useParams } from "react-router-dom"; +import { Link, useParams, useSearchParams } from "react-router-dom"; import { listCharacters, Character } from "../api/characters"; import { Game, getFullMessages, getGame, getRecentMessages, joinGame, Message } from "../api/games"; @@ -9,6 +9,7 @@ import { useGameSocket } from "../hooks/useGameSocket"; export default function GameSession() { const { gameId } = useParams<{ gameId: string }>(); + const [searchParams] = useSearchParams(); const [game, setGame] = useState(null); const [messages, setMessages] = useState([]); @@ -16,7 +17,7 @@ export default function GameSession() { const [loading, setLoading] = useState(true); const [characters, setCharacters] = useState([]); - const [joinCode, setJoinCode] = useState(""); + const [joinCode, setJoinCode] = useState(() => (searchParams.get("code") ?? "").toUpperCase()); const [selectedCharacterId, setSelectedCharacterId] = useState(""); const [joinError, setJoinError] = useState(null); const [isTyping, setIsTyping] = useState(false);