Skip to content

Commit 42d4744

Browse files
authored
Merge pull request #37 from Jasrags/draft_character
Draft character
2 parents 3e65ea2 + 8cd31d7 commit 42d4744

File tree

9 files changed

+2238
-212
lines changed

9 files changed

+2238
-212
lines changed

app/api/characters/[characterId]/route.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,8 +83,36 @@ export async function PATCH(
8383
// Prevent changing certain fields
8484
delete updates.id;
8585
delete updates.ownerId;
86+
delete updates.id;
87+
delete updates.ownerId;
8688
delete updates.createdAt;
8789

90+
// Validate creationState if present
91+
if (updates.metadata?.creationState) {
92+
const state = updates.metadata.creationState;
93+
94+
// Ensure characterId matches
95+
if (state.characterId && state.characterId !== characterId) {
96+
return NextResponse.json(
97+
{ success: false, error: "Validation failed: characterId mismatch in creationState" },
98+
{ status: 400 }
99+
);
100+
}
101+
102+
// Ensure we are not overwriting creation state for a finalized character
103+
// (Unless we decide to allow it, but generally we shouldn't)
104+
if (existing.status !== 'draft' && existing.status !== 'active') { // allow active for now if re-editing is ever a thing, but spec focuses on draft
105+
// actually spec says "Character Status Changed (Draft -> Active) ... Don't allow creation wizard to load"
106+
// Logic here: if character is NOT draft, maybe block writing creationState?
107+
// But for now let's just warn or allow. The important part is validating the state object structure briefly.
108+
}
109+
110+
if (existing.status !== 'draft') {
111+
// Optionally block updates to creationState if not a draft
112+
// return NextResponse.json({ success: false, error: "Cannot update creation state of non-draft character" }, { status: 400 });
113+
}
114+
}
115+
88116
// Update character
89117
const character = await updateCharacter(userId, characterId, updates);
90118

app/characters/[id]/edit/page.tsx

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
"use client";
2+
3+
import { useEffect, useState, use } from "react";
4+
import { useRouter } from "next/navigation";
5+
import { Link, Button } from "react-aria-components";
6+
import { RulesetProvider, useRulesetStatus, useRuleset } from "@/lib/rules";
7+
import { CreationWizard } from "../../create/components/CreationWizard";
8+
import type { Character, CreationState, EditionCode } from "@/lib/types";
9+
10+
interface ResumeContentProps {
11+
character: Character;
12+
characterId: string;
13+
}
14+
15+
function ResumeContent({ character, characterId }: ResumeContentProps) {
16+
const router = useRouter();
17+
const { loading, error, ready } = useRulesetStatus();
18+
const { loadRuleset } = useRuleset();
19+
20+
useEffect(() => {
21+
if (character.editionCode) {
22+
loadRuleset(character.editionCode);
23+
}
24+
}, [character.editionCode, loadRuleset]);
25+
26+
if (loading) {
27+
return (
28+
<div className="flex min-h-[400px] items-center justify-center">
29+
<div className="text-center">
30+
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-emerald-500 border-r-transparent" />
31+
<p className="mt-4 text-sm text-zinc-600 dark:text-zinc-400">
32+
Loading ruleset...
33+
</p>
34+
</div>
35+
</div>
36+
);
37+
}
38+
39+
if (error) {
40+
return (
41+
<div className="flex min-h-[400px] items-center justify-center">
42+
<div className="max-w-md rounded-lg border border-red-200 bg-red-50 p-6 text-center dark:border-red-900 dark:bg-red-950">
43+
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
44+
Failed to load ruleset
45+
</h3>
46+
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{error}</p>
47+
</div>
48+
</div>
49+
);
50+
}
51+
52+
if (ready) {
53+
return (
54+
<CreationWizard
55+
characterId={characterId}
56+
initialState={character.metadata?.creationState as CreationState | undefined}
57+
onCancel={() => router.push("/characters")}
58+
onComplete={(id) => {
59+
router.push(`/characters/${id}`);
60+
}}
61+
/>
62+
);
63+
}
64+
65+
return null;
66+
}
67+
68+
export default function ResumeCharacterPage({ params }: { params: Promise<{ id: string }> }) {
69+
const resolvedParams = use(params);
70+
const [character, setCharacter] = useState<Character | null>(null);
71+
const [loading, setLoading] = useState(true);
72+
const [error, setError] = useState<string | null>(null);
73+
74+
useEffect(() => {
75+
async function fetchCharacter() {
76+
try {
77+
const response = await fetch(`/api/characters/${resolvedParams.id}`);
78+
const data = await response.json();
79+
80+
if (!data.success) {
81+
throw new Error(data.error || "Failed to load character");
82+
}
83+
84+
const char = data.character;
85+
if (char.status !== 'draft') {
86+
throw new Error("Character is not in draft status.");
87+
}
88+
89+
setCharacter(char);
90+
} catch (err) {
91+
setError(err instanceof Error ? err.message : "An error occurred");
92+
} finally {
93+
setLoading(false);
94+
}
95+
}
96+
97+
fetchCharacter();
98+
}, [resolvedParams.id]);
99+
100+
if (loading) {
101+
return (
102+
<div className="flex items-center justify-center min-h-[400px]">
103+
<div className="flex flex-col items-center gap-4">
104+
<div className="w-12 h-12 border-2 border-emerald-500/20 rounded-full animate-spin border-t-emerald-500" />
105+
<span className="text-sm font-mono text-zinc-500 animate-pulse">
106+
LOADING DRAFT...
107+
</span>
108+
</div>
109+
</div>
110+
);
111+
}
112+
113+
if (error || !character) {
114+
return (
115+
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
116+
<div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center">
117+
<svg className="w-8 h-8 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
118+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
119+
</svg>
120+
</div>
121+
<p className="text-red-400 font-mono">{error || "Character not found"}</p>
122+
<Link
123+
href="/characters"
124+
className="text-sm text-zinc-400 hover:text-emerald-400 transition-colors"
125+
>
126+
← Return to characters
127+
</Link>
128+
</div>
129+
);
130+
}
131+
132+
return (
133+
<div className="space-y-6">
134+
<div>
135+
<h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50">
136+
Resume Editing
137+
</h1>
138+
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
139+
Continue building {character.name || "your character"}
140+
</p>
141+
</div>
142+
143+
<RulesetProvider>
144+
<ResumeContent character={character} characterId={resolvedParams.id} />
145+
</RulesetProvider>
146+
</div>
147+
);
148+
}

0 commit comments

Comments
 (0)