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;
|
||||
onUpdate: (characterId: number, data: Partial<Character>) => 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 (
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ interface CharacterDetailProps {
|
|||
onRemoveTalent: (characterId: number, talentId: number) => 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({
|
|||
<div className={styles.overlay} onClick={onClose}>
|
||||
<div className={styles.modal} onClick={(e) => e.stopPropagation()}>
|
||||
<div className={styles.topBar}>
|
||||
<button
|
||||
className={`${styles.editBtn} ${mode === "edit" ? styles.active : ""}`}
|
||||
onClick={() => setMode(mode === "view" ? "edit" : "view")}
|
||||
>
|
||||
{mode === "view" ? "Edit" : "Done"}
|
||||
</button>
|
||||
{canEdit && (
|
||||
<button
|
||||
className={`${styles.editBtn} ${mode === "edit" ? styles.active : ""}`}
|
||||
onClick={() => setMode(mode === "view" ? "edit" : "view")}
|
||||
>
|
||||
{mode === "view" ? "Edit" : "Done"}
|
||||
</button>
|
||||
)}
|
||||
<button className={styles.closeBtn} onClick={onClose}>
|
||||
✕
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -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<Character[]>([]);
|
||||
const [selectedId, setSelectedId] = useState<number | null>(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"}
|
||||
</span>
|
||||
<div className={styles.headerBtns}>
|
||||
<AtmospherePanel
|
||||
atmosphere={atmosphere}
|
||||
onAtmosphereChange={handleAtmosphereChange}
|
||||
/>
|
||||
{role === "dm" && (
|
||||
<AtmospherePanel
|
||||
atmosphere={atmosphere}
|
||||
onAtmosphereChange={handleAtmosphereChange}
|
||||
/>
|
||||
)}
|
||||
{role === "dm" && (
|
||||
<button className={styles.addBtn} onClick={handleInvite}>
|
||||
Invite Player
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className={styles.addBtn}
|
||||
onClick={() => setShowCreate(true)}
|
||||
|
|
@ -391,6 +414,7 @@ export default function CampaignView() {
|
|||
onHpChange={handleHpChange}
|
||||
onUpdate={handleUpdate}
|
||||
onClick={setSelectedId}
|
||||
canEdit={role === "dm" || char.user_id === user?.userId}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
@ -409,6 +433,7 @@ export default function CampaignView() {
|
|||
onRemoveTalent={handleRemoveTalent}
|
||||
onDelete={handleDelete}
|
||||
onClose={() => setSelectedId(null)}
|
||||
canEdit={role === "dm" || selectedCharacter.user_id === user?.userId}
|
||||
/>
|
||||
)}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue