FRE-592: Implement character database and relationship mapping
Add full character management system with enriched profiles (bio, traits, arcs, motivation, conflict, secrets), relationship mapping between characters with types and strength, character search/filter by role and arc type, and character statistics (scene count, dialogue, screen time). Includes database schema, tRPC router procedures, SolidJS components, API hooks, and unit tests. Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
31
src/lib/api/trpc-client.ts
Normal file
31
src/lib/api/trpc-client.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createTRPCClient, httpBatchLink } from '@trpc/client';
|
||||
import type { AppRouter } from '../../../server/trpc';
|
||||
|
||||
// Create tRPC client
|
||||
export function createTRPCClientInstance(baseUrl: string = 'http://localhost:8080') {
|
||||
return createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: `${baseUrl}/trpc`,
|
||||
headers: () => {
|
||||
// Add auth headers if available
|
||||
const token = localStorage.getItem('auth_token');
|
||||
return {
|
||||
authorization: token ? `Bearer ${token}` : '',
|
||||
};
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
// Helper for SSR
|
||||
export function createServerTRPCClient(baseUrl: string = 'http://localhost:8080') {
|
||||
return createTRPCClient<AppRouter>({
|
||||
links: [
|
||||
httpBatchLink({
|
||||
url: `${baseUrl}/trpc`,
|
||||
}),
|
||||
],
|
||||
});
|
||||
}
|
||||
266
src/lib/api/trpc-hooks.ts
Normal file
266
src/lib/api/trpc-hooks.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { createQuery, createMutation, useQueryClient } from '@tanstack/solid-query';
|
||||
import { createTRPCClientInstance } from './trpc-client';
|
||||
import type { Project, Character, CharacterRelationship, CharacterStats, Scene } from '../../../server/types/project';
|
||||
|
||||
const trpcClient = createTRPCClientInstance();
|
||||
|
||||
// Project hooks
|
||||
export function useProjects() {
|
||||
return createQuery({
|
||||
queryKey: ['projects'],
|
||||
queryFn: async () => {
|
||||
const result = await trpcClient.project.listProjects.query();
|
||||
return result as Project[];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useProject(projectId: string) {
|
||||
return createQuery({
|
||||
queryKey: ['project', projectId],
|
||||
queryFn: async () => {
|
||||
const result = await trpcClient.project.getProject.query({ id: projectId });
|
||||
return result as Project;
|
||||
},
|
||||
enabled: !!projectId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateProject() {
|
||||
return createMutation({
|
||||
mutationFn: async (input: { name: string; description?: string }) => {
|
||||
return trpcClient.project.createProject.mutate(input) as Promise<Project>;
|
||||
},
|
||||
onSuccess: () => {
|
||||
const qc = useQueryClient();
|
||||
qc.invalidateQueries({ queryKey: ['projects'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Character hooks
|
||||
export function useCharacters(projectId: string) {
|
||||
return createQuery({
|
||||
queryKey: ['characters', projectId],
|
||||
queryFn: async () => {
|
||||
const result = await trpcClient.project.listCharacters.query({ projectId });
|
||||
return result as Character[];
|
||||
},
|
||||
enabled: !!projectId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCharacter(characterId: string) {
|
||||
return createQuery({
|
||||
queryKey: ['character', characterId],
|
||||
queryFn: async () => {
|
||||
const result = await trpcClient.project.getCharacter.query({ id: characterId });
|
||||
return result as Character;
|
||||
},
|
||||
enabled: !!characterId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useSearchCharacters(projectId: string, query?: string, role?: string, arcType?: string) {
|
||||
return createQuery({
|
||||
queryKey: ['searchCharacters', projectId, query, role, arcType],
|
||||
queryFn: async () => {
|
||||
const result = await trpcClient.project.searchCharacters.query({
|
||||
projectId,
|
||||
query,
|
||||
role,
|
||||
arcType,
|
||||
});
|
||||
return result as Character[];
|
||||
},
|
||||
enabled: !!projectId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCharacterStats(characterId: string) {
|
||||
return createQuery({
|
||||
queryKey: ['characterStats', characterId],
|
||||
queryFn: async () => {
|
||||
const result = await trpcClient.project.getCharacterStats.query({ characterId });
|
||||
return result as CharacterStats;
|
||||
},
|
||||
enabled: !!characterId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useProjectCharacterStats(projectId: string) {
|
||||
return createQuery({
|
||||
queryKey: ['projectCharacterStats', projectId],
|
||||
queryFn: async () => {
|
||||
const result = await trpcClient.project.getProjectCharacterStats.query({ projectId });
|
||||
return result as CharacterStats[];
|
||||
},
|
||||
enabled: !!projectId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateCharacter() {
|
||||
const qc = useQueryClient();
|
||||
return createMutation({
|
||||
mutationFn: async (input: {
|
||||
name: string;
|
||||
description?: string;
|
||||
bio?: string;
|
||||
role?: 'protagonist' | 'antagonist' | 'supporting' | 'background' | 'ensemble';
|
||||
arc?: string;
|
||||
arcType?: 'positive' | 'negative' | 'flat' | 'complex';
|
||||
age?: number;
|
||||
gender?: string;
|
||||
voice?: string;
|
||||
traits?: string;
|
||||
motivation?: string;
|
||||
conflict?: string;
|
||||
secret?: string;
|
||||
imageUrl?: string;
|
||||
projectId: string;
|
||||
}) => {
|
||||
return trpcClient.project.createCharacter.mutate(input) as Promise<Character>;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
qc.invalidateQueries({ queryKey: ['characters', variables.projectId] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateCharacter() {
|
||||
const qc = useQueryClient();
|
||||
return createMutation({
|
||||
mutationFn: async (input: {
|
||||
id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
bio?: string;
|
||||
role?: 'protagonist' | 'antagonist' | 'supporting' | 'background' | 'ensemble';
|
||||
arc?: string;
|
||||
arcType?: 'positive' | 'negative' | 'flat' | 'complex';
|
||||
age?: number;
|
||||
gender?: string;
|
||||
voice?: string;
|
||||
traits?: string;
|
||||
motivation?: string;
|
||||
conflict?: string;
|
||||
secret?: string;
|
||||
imageUrl?: string;
|
||||
projectId?: string;
|
||||
}) => {
|
||||
return trpcClient.project.updateCharacter.mutate(input) as Promise<Character>;
|
||||
},
|
||||
onSuccess: (_, variables) => {
|
||||
qc.invalidateQueries({ queryKey: ['character', variables.id] });
|
||||
qc.invalidateQueries({ queryKey: ['characters'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteCharacter() {
|
||||
const qc = useQueryClient();
|
||||
return createMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
return trpcClient.project.deleteCharacter.mutate({ id }) as Promise<{ success: boolean }>;
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['characters'] });
|
||||
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Relationship hooks
|
||||
export function useRelationships(projectId: string) {
|
||||
return createQuery({
|
||||
queryKey: ['characterRelationships', projectId],
|
||||
queryFn: async () => {
|
||||
const result = await trpcClient.project.listRelationships.query({ projectId });
|
||||
return result as CharacterRelationship[];
|
||||
},
|
||||
enabled: !!projectId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCharacterRelationships(characterId: string) {
|
||||
return createQuery({
|
||||
queryKey: ['characterRelationships', characterId],
|
||||
queryFn: async () => {
|
||||
const result = await trpcClient.project.getRelationshipsForCharacter.query({ characterId });
|
||||
return result as CharacterRelationship[];
|
||||
},
|
||||
enabled: !!characterId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useCreateRelationship() {
|
||||
const qc = useQueryClient();
|
||||
return createMutation({
|
||||
mutationFn: async (input: {
|
||||
characterIdA: string;
|
||||
characterIdB: string;
|
||||
relationshipType: 'family' | 'romantic' | 'friendship' | 'rivalry' | 'mentor' | 'alliance' | 'conflict' | 'professional' | 'other';
|
||||
description?: string;
|
||||
strength?: number;
|
||||
isAntagonistic?: boolean;
|
||||
}) => {
|
||||
return trpcClient.project.createRelationship.mutate(input) as Promise<CharacterRelationship>;
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useUpdateRelationship() {
|
||||
const qc = useQueryClient();
|
||||
return createMutation({
|
||||
mutationFn: async (input: {
|
||||
id: string;
|
||||
relationshipType?: 'family' | 'romantic' | 'friendship' | 'rivalry' | 'mentor' | 'alliance' | 'conflict' | 'professional' | 'other';
|
||||
description?: string;
|
||||
strength?: number;
|
||||
isAntagonistic?: boolean;
|
||||
}) => {
|
||||
return trpcClient.project.updateRelationship.mutate(input) as Promise<CharacterRelationship>;
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteRelationship() {
|
||||
const qc = useQueryClient();
|
||||
return createMutation({
|
||||
mutationFn: async (id: string) => {
|
||||
return trpcClient.project.deleteRelationship.mutate({ id }) as Promise<{ success: boolean }>;
|
||||
},
|
||||
onSuccess: () => {
|
||||
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// Scene hooks
|
||||
export function useScenes(projectId: string) {
|
||||
return createQuery({
|
||||
queryKey: ['scenes', projectId],
|
||||
queryFn: async () => {
|
||||
const result = await trpcClient.project.listScenes.query({ projectId });
|
||||
return result as Scene[];
|
||||
},
|
||||
enabled: !!projectId,
|
||||
});
|
||||
}
|
||||
|
||||
export function useScene(sceneId: string) {
|
||||
return createQuery({
|
||||
queryKey: ['scene', sceneId],
|
||||
queryFn: async () => {
|
||||
const result = await trpcClient.project.getScene.query({ id: sceneId });
|
||||
return result as Scene;
|
||||
},
|
||||
enabled: !!sceneId,
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user