feat: add DM/player role separation — atmosphere, invite, and edit controls gated by role
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e2548ad660
commit
47215b48f1
4 changed files with 43 additions and 11 deletions
|
|
@ -20,6 +20,7 @@ interface CharacterCardProps {
|
||||||
onHpChange: (characterId: number, hp: number) => void;
|
onHpChange: (characterId: number, hp: number) => void;
|
||||||
onUpdate: (characterId: number, data: Partial<Character>) => void;
|
onUpdate: (characterId: number, data: Partial<Character>) => void;
|
||||||
onClick: (characterId: number) => void;
|
onClick: (characterId: number) => void;
|
||||||
|
canEdit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CharacterCard({
|
export default function CharacterCard({
|
||||||
|
|
@ -27,6 +28,7 @@ export default function CharacterCard({
|
||||||
onHpChange,
|
onHpChange,
|
||||||
onUpdate,
|
onUpdate,
|
||||||
onClick,
|
onClick,
|
||||||
|
canEdit = true,
|
||||||
}: CharacterCardProps) {
|
}: CharacterCardProps) {
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,7 @@ interface CharacterDetailProps {
|
||||||
onRemoveTalent: (characterId: number, talentId: number) => void;
|
onRemoveTalent: (characterId: number, talentId: number) => void;
|
||||||
onDelete: (id: number) => void;
|
onDelete: (id: number) => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
canEdit?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function CharacterDetail({
|
export default function CharacterDetail({
|
||||||
|
|
@ -42,6 +43,7 @@ export default function CharacterDetail({
|
||||||
onRemoveTalent,
|
onRemoveTalent,
|
||||||
onDelete,
|
onDelete,
|
||||||
onClose,
|
onClose,
|
||||||
|
canEdit = true,
|
||||||
}: CharacterDetailProps) {
|
}: CharacterDetailProps) {
|
||||||
const [mode, setMode] = useState<"view" | "edit">("view");
|
const [mode, setMode] = useState<"view" | "edit">("view");
|
||||||
|
|
||||||
|
|
@ -49,12 +51,14 @@ export default function CharacterDetail({
|
||||||
<div className={styles.overlay} onClick={onClose}>
|
<div className={styles.overlay} onClick={onClose}>
|
||||||
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
|
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
|
||||||
<div className={styles.topBar}>
|
<div className={styles.topBar}>
|
||||||
<button
|
{canEdit && (
|
||||||
className={`${styles.editBtn} ${mode === "edit" ? styles.active : ""}`}
|
<button
|
||||||
onClick={() => setMode(mode === "view" ? "edit" : "view")}
|
className={`${styles.editBtn} ${mode === "edit" ? styles.active : ""}`}
|
||||||
>
|
onClick={() => setMode(mode === "view" ? "edit" : "view")}
|
||||||
{mode === "view" ? "Edit" : "Done"}
|
>
|
||||||
</button>
|
{mode === "view" ? "Edit" : "Done"}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button className={styles.closeBtn} onClick={onClose}>
|
<button className={styles.closeBtn} onClick={onClose}>
|
||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,10 @@ import {
|
||||||
addTalent,
|
addTalent,
|
||||||
removeTalent,
|
removeTalent,
|
||||||
getRolls,
|
getRolls,
|
||||||
|
getMyCampaignRole,
|
||||||
|
generateInvite,
|
||||||
} from "../api";
|
} from "../api";
|
||||||
|
import { useAuth } from "../context/AuthContext";
|
||||||
import type { Character, Gear, Talent, GameItem, RollResult } from "../types";
|
import type { Character, Gear, Talent, GameItem, RollResult } from "../types";
|
||||||
import CharacterCard from "../components/CharacterCard";
|
import CharacterCard from "../components/CharacterCard";
|
||||||
import CharacterDetail from "../components/CharacterDetail";
|
import CharacterDetail from "../components/CharacterDetail";
|
||||||
|
|
@ -34,6 +37,8 @@ const ANCESTRIES = ["Human", "Elf", "Dwarf", "Halfling", "Goblin", "Half-Orc"];
|
||||||
export default function CampaignView() {
|
export default function CampaignView() {
|
||||||
const { id } = useParams<{ id: string }>();
|
const { id } = useParams<{ id: string }>();
|
||||||
const campaignId = Number(id);
|
const campaignId = Number(id);
|
||||||
|
const { user } = useAuth();
|
||||||
|
const [role, setRole] = useState<"dm" | "player" | null>(null);
|
||||||
const [characters, setCharacters] = useState<Character[]>([]);
|
const [characters, setCharacters] = useState<Character[]>([]);
|
||||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
const [showCreate, setShowCreate] = useState(false);
|
||||||
|
|
@ -65,6 +70,7 @@ export default function CampaignView() {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getCharacters(campaignId).then(setCharacters);
|
getCharacters(campaignId).then(setCharacters);
|
||||||
getRolls(campaignId).then(setRolls);
|
getRolls(campaignId).then(setRolls);
|
||||||
|
getMyCampaignRole(campaignId).then((r) => setRole(r.role)).catch(() => {});
|
||||||
getCampaigns().then((camps) => {
|
getCampaigns().then((camps) => {
|
||||||
const c = camps.find((x) => x.id === campaignId);
|
const c = camps.find((x) => x.id === campaignId);
|
||||||
if (c) setCampaignName(c.name);
|
if (c) setCampaignName(c.name);
|
||||||
|
|
@ -352,6 +358,16 @@ export default function CampaignView() {
|
||||||
await removeTalent(characterId, talentId);
|
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;
|
const selectedCharacter = characters.find((c) => c.id === selectedId) ?? null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -365,10 +381,17 @@ export default function CampaignView() {
|
||||||
{campaignName || "Campaign"}
|
{campaignName || "Campaign"}
|
||||||
</span>
|
</span>
|
||||||
<div className={styles.headerBtns}>
|
<div className={styles.headerBtns}>
|
||||||
<AtmospherePanel
|
{role === "dm" && (
|
||||||
atmosphere={atmosphere}
|
<AtmospherePanel
|
||||||
onAtmosphereChange={handleAtmosphereChange}
|
atmosphere={atmosphere}
|
||||||
/>
|
onAtmosphereChange={handleAtmosphereChange}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{role === "dm" && (
|
||||||
|
<button className={styles.addBtn} onClick={handleInvite}>
|
||||||
|
Invite Player
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
<button
|
<button
|
||||||
className={styles.addBtn}
|
className={styles.addBtn}
|
||||||
onClick={() => setShowCreate(true)}
|
onClick={() => setShowCreate(true)}
|
||||||
|
|
@ -391,6 +414,7 @@ export default function CampaignView() {
|
||||||
onHpChange={handleHpChange}
|
onHpChange={handleHpChange}
|
||||||
onUpdate={handleUpdate}
|
onUpdate={handleUpdate}
|
||||||
onClick={setSelectedId}
|
onClick={setSelectedId}
|
||||||
|
canEdit={role === "dm" || char.user_id === user?.userId}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -409,6 +433,7 @@ export default function CampaignView() {
|
||||||
onRemoveTalent={handleRemoveTalent}
|
onRemoveTalent={handleRemoveTalent}
|
||||||
onDelete={handleDelete}
|
onDelete={handleDelete}
|
||||||
onClose={() => setSelectedId(null)}
|
onClose={() => setSelectedId(null)}
|
||||||
|
canEdit={role === "dm" || selectedCharacter.user_id === user?.userId}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
export interface Campaign {
|
export interface Campaign {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
created_by: string;
|
role?: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,6 +33,7 @@ export interface Talent {
|
||||||
export interface Character {
|
export interface Character {
|
||||||
id: number;
|
id: number;
|
||||||
campaign_id: number;
|
campaign_id: number;
|
||||||
|
user_id?: number | null;
|
||||||
created_by: string;
|
created_by: string;
|
||||||
name: string;
|
name: string;
|
||||||
class: string;
|
class: string;
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue