Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions app/api/characters/[characterId]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,36 @@ export async function PATCH(
// Prevent changing certain fields
delete updates.id;
delete updates.ownerId;
delete updates.id;
delete updates.ownerId;
delete updates.createdAt;

// Validate creationState if present
if (updates.metadata?.creationState) {
const state = updates.metadata.creationState;

// Ensure characterId matches
if (state.characterId && state.characterId !== characterId) {
return NextResponse.json(
{ success: false, error: "Validation failed: characterId mismatch in creationState" },
{ status: 400 }
);
}

// Ensure we are not overwriting creation state for a finalized character
// (Unless we decide to allow it, but generally we shouldn't)
if (existing.status !== 'draft' && existing.status !== 'active') { // allow active for now if re-editing is ever a thing, but spec focuses on draft
// actually spec says "Character Status Changed (Draft -> Active) ... Don't allow creation wizard to load"
// Logic here: if character is NOT draft, maybe block writing creationState?
// But for now let's just warn or allow. The important part is validating the state object structure briefly.
}

if (existing.status !== 'draft') {
// Optionally block updates to creationState if not a draft
// return NextResponse.json({ success: false, error: "Cannot update creation state of non-draft character" }, { status: 400 });
}
}

// Update character
const character = await updateCharacter(userId, characterId, updates);

Expand Down
148 changes: 148 additions & 0 deletions app/characters/[id]/edit/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
"use client";

import { useEffect, useState, use } from "react";
import { useRouter } from "next/navigation";
import { Link, Button } from "react-aria-components";

Check warning on line 5 in app/characters/[id]/edit/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Test, and Build (20.x)

'Button' is defined but never used
import { RulesetProvider, useRulesetStatus, useRuleset } from "@/lib/rules";
import { CreationWizard } from "../../create/components/CreationWizard";
import type { Character, CreationState, EditionCode } from "@/lib/types";

Check warning on line 8 in app/characters/[id]/edit/page.tsx

View workflow job for this annotation

GitHub Actions / Lint, Test, and Build (20.x)

'EditionCode' is defined but never used

interface ResumeContentProps {
character: Character;
characterId: string;
}

function ResumeContent({ character, characterId }: ResumeContentProps) {
const router = useRouter();
const { loading, error, ready } = useRulesetStatus();
const { loadRuleset } = useRuleset();

useEffect(() => {
if (character.editionCode) {
loadRuleset(character.editionCode);
}
}, [character.editionCode, loadRuleset]);

if (loading) {
return (
<div className="flex min-h-[400px] items-center justify-center">
<div className="text-center">
<div className="inline-block h-8 w-8 animate-spin rounded-full border-4 border-emerald-500 border-r-transparent" />
<p className="mt-4 text-sm text-zinc-600 dark:text-zinc-400">
Loading ruleset...
</p>
</div>
</div>
);
}

if (error) {
return (
<div className="flex min-h-[400px] items-center justify-center">
<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">
<h3 className="text-sm font-medium text-red-800 dark:text-red-200">
Failed to load ruleset
</h3>
<p className="mt-1 text-sm text-red-600 dark:text-red-400">{error}</p>
</div>
</div>
);
}

if (ready) {
return (
<CreationWizard
characterId={characterId}
initialState={character.metadata?.creationState as CreationState | undefined}
onCancel={() => router.push("/characters")}
onComplete={(id) => {
router.push(`/characters/${id}`);
}}
/>
);
}

return null;
}

export default function ResumeCharacterPage({ params }: { params: Promise<{ id: string }> }) {
const resolvedParams = use(params);
const [character, setCharacter] = useState<Character | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);

useEffect(() => {
async function fetchCharacter() {
try {
const response = await fetch(`/api/characters/${resolvedParams.id}`);
const data = await response.json();

if (!data.success) {
throw new Error(data.error || "Failed to load character");
}

const char = data.character;
if (char.status !== 'draft') {
throw new Error("Character is not in draft status.");
}

setCharacter(char);
} catch (err) {
setError(err instanceof Error ? err.message : "An error occurred");
} finally {
setLoading(false);
}
}

fetchCharacter();
}, [resolvedParams.id]);

if (loading) {
return (
<div className="flex items-center justify-center min-h-[400px]">
<div className="flex flex-col items-center gap-4">
<div className="w-12 h-12 border-2 border-emerald-500/20 rounded-full animate-spin border-t-emerald-500" />
<span className="text-sm font-mono text-zinc-500 animate-pulse">
LOADING DRAFT...
</span>
</div>
</div>
);
}

if (error || !character) {
return (
<div className="flex flex-col items-center justify-center min-h-[400px] gap-4">
<div className="w-16 h-16 rounded-full bg-red-500/10 flex items-center justify-center">
<svg className="w-8 h-8 text-red-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
</div>
<p className="text-red-400 font-mono">{error || "Character not found"}</p>
<Link
href="/characters"
className="text-sm text-zinc-400 hover:text-emerald-400 transition-colors"
>
← Return to characters
</Link>
</div>
);
}

return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold text-zinc-900 dark:text-zinc-50">
Resume Editing
</h1>
<p className="mt-1 text-sm text-zinc-600 dark:text-zinc-400">
Continue building {character.name || "your character"}
</p>
</div>

<RulesetProvider>
<ResumeContent character={character} characterId={resolvedParams.id} />
</RulesetProvider>
</div>
);
}
Loading
Loading