From 47215b48f1627dd3ad30bcdd194c9a940b05f415 Mon Sep 17 00:00:00 2001 From: Aaron Wood Date: Sat, 11 Apr 2026 00:44:27 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20add=20DM/player=20role=20separation=20?= =?UTF-8?q?=E2=80=94=20atmosphere,=20invite,=20and=20edit=20controls=20gat?= =?UTF-8?q?ed=20by=20role?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- client/src/components/CharacterCard.tsx | 2 ++ client/src/components/CharacterDetail.tsx | 16 ++++++----- client/src/pages/CampaignView.tsx | 33 ++++++++++++++++++++--- client/src/types.ts | 3 ++- 4 files changed, 43 insertions(+), 11 deletions(-) diff --git a/client/src/components/CharacterCard.tsx b/client/src/components/CharacterCard.tsx index 51dad84..5333c8f 100644 --- a/client/src/components/CharacterCard.tsx +++ b/client/src/components/CharacterCard.tsx @@ -20,6 +20,7 @@ interface CharacterCardProps { onHpChange: (characterId: number, hp: number) => void; onUpdate: (characterId: number, data: Partial) => void; onClick: (characterId: number) => void; + canEdit?: boolean; } export default function CharacterCard({ @@ -27,6 +28,7 @@ export default function CharacterCard({ onHpChange, onUpdate, onClick, + canEdit = true, }: CharacterCardProps) { return (
void; onDelete: (id: number) => void; onClose: () => void; + canEdit?: boolean; } export default function CharacterDetail({ @@ -42,6 +43,7 @@ export default function CharacterDetail({ onRemoveTalent, onDelete, onClose, + canEdit = true, }: CharacterDetailProps) { const [mode, setMode] = useState<"view" | "edit">("view"); @@ -49,12 +51,14 @@ export default function CharacterDetail({
e.stopPropagation()}>
- + {canEdit && ( + + )} diff --git a/client/src/pages/CampaignView.tsx b/client/src/pages/CampaignView.tsx index 535b8c0..2a8fb8a 100644 --- a/client/src/pages/CampaignView.tsx +++ b/client/src/pages/CampaignView.tsx @@ -13,7 +13,10 @@ import { addTalent, removeTalent, getRolls, + getMyCampaignRole, + generateInvite, } from "../api"; +import { useAuth } from "../context/AuthContext"; import type { Character, Gear, Talent, GameItem, RollResult } from "../types"; import CharacterCard from "../components/CharacterCard"; import CharacterDetail from "../components/CharacterDetail"; @@ -34,6 +37,8 @@ const ANCESTRIES = ["Human", "Elf", "Dwarf", "Halfling", "Goblin", "Half-Orc"]; export default function CampaignView() { const { id } = useParams<{ id: string }>(); const campaignId = Number(id); + const { user } = useAuth(); + const [role, setRole] = useState<"dm" | "player" | null>(null); const [characters, setCharacters] = useState([]); const [selectedId, setSelectedId] = useState(null); const [showCreate, setShowCreate] = useState(false); @@ -65,6 +70,7 @@ export default function CampaignView() { useEffect(() => { getCharacters(campaignId).then(setCharacters); getRolls(campaignId).then(setRolls); + getMyCampaignRole(campaignId).then((r) => setRole(r.role)).catch(() => {}); getCampaigns().then((camps) => { const c = camps.find((x) => x.id === campaignId); if (c) setCampaignName(c.name); @@ -352,6 +358,16 @@ export default function CampaignView() { await removeTalent(characterId, talentId); } + async function handleInvite() { + try { + const { url } = await generateInvite(campaignId); + await navigator.clipboard.writeText(url); + alert("Invite link copied to clipboard!"); + } catch { + alert("Failed to generate invite link"); + } + } + const selectedCharacter = characters.find((c) => c.id === selectedId) ?? null; return ( @@ -365,10 +381,17 @@ export default function CampaignView() { {campaignName || "Campaign"}
- + {role === "dm" && ( + + )} + {role === "dm" && ( + + )}
@@ -409,6 +433,7 @@ export default function CampaignView() { onRemoveTalent={handleRemoveTalent} onDelete={handleDelete} onClose={() => setSelectedId(null)} + canEdit={role === "dm" || selectedCharacter.user_id === user?.userId} /> )} diff --git a/client/src/types.ts b/client/src/types.ts index 7ba0950..c3c7b48 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -1,7 +1,7 @@ export interface Campaign { id: number; name: string; - created_by: string; + role?: string; created_at: string; } @@ -33,6 +33,7 @@ export interface Talent { export interface Character { id: number; campaign_id: number; + user_id?: number | null; created_by: string; name: string; class: string;