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 <noreply@anthropic.com>
This commit is contained in:
@@ -1,9 +1,20 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
|
||||||
import { Game } from "../api/games";
|
import { Game } from "../api/games";
|
||||||
|
|
||||||
export default function GameCard({ game }: { game: Game }) {
|
export default function GameCard({ game }: { game: Game }) {
|
||||||
const created = new Date(game.created_at).toLocaleString("de-DE");
|
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 (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -18,9 +29,25 @@ export default function GameCard({ game }: { game: Game }) {
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{game.participation_code && (
|
{game.participation_code && (
|
||||||
<span className="shrink-0 rounded bg-amber-500/20 px-2 py-1 font-mono text-xs text-amber-300">
|
<button
|
||||||
|
onClick={handleCopyLink}
|
||||||
|
title="Einladungslink kopieren"
|
||||||
|
className="flex shrink-0 items-center gap-1.5 rounded bg-amber-500/20 px-2 py-1 font-mono text-xs text-amber-300 hover:bg-amber-500/30"
|
||||||
|
>
|
||||||
Code: {game.participation_code}
|
Code: {game.participation_code}
|
||||||
</span>
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
strokeWidth={2}
|
||||||
|
className="h-3.5 w-3.5"
|
||||||
|
>
|
||||||
|
<rect x="9" y="9" width="11" height="11" rx="1.5" />
|
||||||
|
<path d="M5 15V5a2 2 0 0 1 2-2h10" />
|
||||||
|
</svg>
|
||||||
|
{copied && <span className="text-emerald-400">Kopiert!</span>}
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<p className="mt-1 text-sm text-slate-400">{game.description}</p>
|
<p className="mt-1 text-sm text-slate-400">{game.description}</p>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { FormEvent, useEffect, useState } from "react";
|
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 { listCharacters, Character } from "../api/characters";
|
||||||
import { Game, getFullMessages, getGame, getRecentMessages, joinGame, Message } from "../api/games";
|
import { Game, getFullMessages, getGame, getRecentMessages, joinGame, Message } from "../api/games";
|
||||||
@@ -9,6 +9,7 @@ import { useGameSocket } from "../hooks/useGameSocket";
|
|||||||
|
|
||||||
export default function GameSession() {
|
export default function GameSession() {
|
||||||
const { gameId } = useParams<{ gameId: string }>();
|
const { gameId } = useParams<{ gameId: string }>();
|
||||||
|
const [searchParams] = useSearchParams();
|
||||||
|
|
||||||
const [game, setGame] = useState<Game | null>(null);
|
const [game, setGame] = useState<Game | null>(null);
|
||||||
const [messages, setMessages] = useState<Message[]>([]);
|
const [messages, setMessages] = useState<Message[]>([]);
|
||||||
@@ -16,7 +17,7 @@ export default function GameSession() {
|
|||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
const [characters, setCharacters] = useState<Character[]>([]);
|
const [characters, setCharacters] = useState<Character[]>([]);
|
||||||
const [joinCode, setJoinCode] = useState("");
|
const [joinCode, setJoinCode] = useState(() => (searchParams.get("code") ?? "").toUpperCase());
|
||||||
const [selectedCharacterId, setSelectedCharacterId] = useState("");
|
const [selectedCharacterId, setSelectedCharacterId] = useState("");
|
||||||
const [joinError, setJoinError] = useState<string | null>(null);
|
const [joinError, setJoinError] = useState<string | null>(null);
|
||||||
const [isTyping, setIsTyping] = useState(false);
|
const [isTyping, setIsTyping] = useState(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user