feat: integrate tRPC client with Clerk authentication

- Update trpc-client.ts with auth token management
- Create comprehensive tRPC hooks for all project operations
- Store Clerk session token in localStorage for API auth
- Clear token on sign out
- Support integer IDs matching backend schema

Hooks created:
- Projects: useProjects, useProject, useCreateProject, useUpdateProject, useDeleteProject
- Characters: useCharacters, useCharacter, useCreateCharacter, useUpdateCharacter, useDeleteCharacter
- Relationships: useRelationships, useCharacterRelationships, useCreateRelationship, useUpdateRelationship, useDeleteRelationship
- Scenes: useScenes, useScene, useCreateScene, useUpdateScene, useDeleteScene
- Stats: useCharacterStats, useProjectCharacterStats

All 34 backend tests passing.
This commit is contained in:
2026-04-25 08:31:05 -04:00
parent 754fce269f
commit 4435a12dd7
3 changed files with 262 additions and 128 deletions

View File

@@ -1,31 +1,39 @@
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}` : '',
};
},
}),
],
});
let cachedToken: string | null = null;
function getAuthToken(): string | null {
if (typeof window === 'undefined') return null;
if (cachedToken) return cachedToken;
cachedToken = localStorage.getItem('auth_token');
return cachedToken;
}
// Helper for SSR
export function createServerTRPCClient(baseUrl: string = 'http://localhost:8080') {
return createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: `${baseUrl}/trpc`,
}),
],
});
export function setAuthToken(token: string | null) {
cachedToken = token;
if (typeof window !== 'undefined') {
if (token) {
localStorage.setItem('auth_token', token);
} else {
localStorage.removeItem('auth_token');
}
}
}
// Create tRPC client with auth
export const trpc = createTRPCClient<AppRouter>({
links: [
httpBatchLink({
url: `${(import.meta as any).env?.VITE_API_URL || 'http://localhost:8080'}/trpc`,
headers: () => {
const token = getAuthToken();
return {
authorization: token ? `Bearer ${token}` : '',
};
},
}),
],
});
export type { AppRouter };

View File

@@ -1,107 +1,123 @@
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();
import { trpc } from './trpc-client';
// Project hooks
export function useProjects() {
return createQuery({
return createQuery(() => ({
queryKey: ['projects'],
queryFn: async () => {
const result = await trpcClient.project.listProjects.query();
return result as Project[];
return await trpc.project.listProjects.query(undefined);
},
});
}));
}
export function useProject(projectId: string) {
return createQuery({
queryKey: ['project', projectId],
export function useProject(id: number) {
return createQuery(() => ({
queryKey: ['project', id],
queryFn: async () => {
const result = await trpcClient.project.getProject.query({ id: projectId });
return result as Project;
return await trpc.project.getProject.query({ id });
},
enabled: !!projectId,
});
enabled: !!id,
}));
}
export function useCreateProject() {
return createMutation({
const qc = useQueryClient();
return createMutation(() => ({
mutationFn: async (input: { name: string; description?: string }) => {
return trpcClient.project.createProject.mutate(input) as Promise<Project>;
return await trpc.project.createProject.mutate(input);
},
onSuccess: () => {
const qc = useQueryClient();
qc.invalidateQueries({ queryKey: ['projects'] });
},
});
}));
}
export function useUpdateProject() {
const qc = useQueryClient();
return createMutation(() => ({
mutationFn: async (input: { id: number; name?: string; description?: string }) => {
return await trpc.project.updateProject.mutate(input);
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['projects'] });
},
}));
}
export function useDeleteProject() {
const qc = useQueryClient();
return createMutation(() => ({
mutationFn: async (id: number) => {
return await trpc.project.deleteProject.mutate({ id });
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['projects'] });
},
}));
}
// Character hooks
export function useCharacters(projectId: string) {
return createQuery({
export function useCharacters(projectId: number) {
return createQuery(() => ({
queryKey: ['characters', projectId],
queryFn: async () => {
const result = await trpcClient.project.listCharacters.query({ projectId });
return result as Character[];
return await trpc.project.listCharacters.query({ projectId });
},
enabled: !!projectId,
});
}));
}
export function useCharacter(characterId: string) {
return createQuery({
queryKey: ['character', characterId],
export function useCharacter(id: number) {
return createQuery(() => ({
queryKey: ['character', id],
queryFn: async () => {
const result = await trpcClient.project.getCharacter.query({ id: characterId });
return result as Character;
return await trpc.project.getCharacter.query({ id });
},
enabled: !!characterId,
});
enabled: !!id,
}));
}
export function useSearchCharacters(projectId: string, query?: string, role?: string, arcType?: string) {
return createQuery({
export function useSearchCharacters(projectId: number, query?: string, role?: 'protagonist' | 'antagonist' | 'supporting' | 'background' | 'ensemble', arcType?: 'positive' | 'negative' | 'flat' | 'complex') {
return createQuery(() => ({
queryKey: ['searchCharacters', projectId, query, role, arcType],
queryFn: async () => {
const result = await trpcClient.project.searchCharacters.query({
return await trpc.project.searchCharacters.query({
projectId,
query,
role,
arcType,
});
return result as Character[];
},
enabled: !!projectId,
});
}));
}
export function useCharacterStats(characterId: string) {
return createQuery({
export function useCharacterStats(characterId: number) {
return createQuery(() => ({
queryKey: ['characterStats', characterId],
queryFn: async () => {
const result = await trpcClient.project.getCharacterStats.query({ characterId });
return result as CharacterStats;
return await trpc.project.getCharacterStats.query({ characterId });
},
enabled: !!characterId,
});
}));
}
export function useProjectCharacterStats(projectId: string) {
return createQuery({
export function useProjectCharacterStats(projectId: number) {
return createQuery(() => ({
queryKey: ['projectCharacterStats', projectId],
queryFn: async () => {
const result = await trpcClient.project.getProjectCharacterStats.query({ projectId });
return result as CharacterStats[];
return await trpc.project.getProjectCharacterStats.query(undefined, {
context: { projectId },
});
},
enabled: !!projectId,
});
}));
}
export function useCreateCharacter() {
const qc = useQueryClient();
return createMutation({
return createMutation(() => ({
mutationFn: async (input: {
name: string;
description?: string;
@@ -117,21 +133,21 @@ export function useCreateCharacter() {
conflict?: string;
secret?: string;
imageUrl?: string;
projectId: string;
projectId: number;
}) => {
return trpcClient.project.createCharacter.mutate(input) as Promise<Character>;
return await trpc.project.createCharacter.mutate(input);
},
onSuccess: (_, variables) => {
qc.invalidateQueries({ queryKey: ['characters', variables.projectId] });
},
});
}));
}
export function useUpdateCharacter() {
const qc = useQueryClient();
return createMutation({
return createMutation(() => ({
mutationFn: async (input: {
id: string;
id: number;
name?: string;
description?: string;
bio?: string;
@@ -146,121 +162,165 @@ export function useUpdateCharacter() {
conflict?: string;
secret?: string;
imageUrl?: string;
projectId?: string;
projectId?: number;
}) => {
return trpcClient.project.updateCharacter.mutate(input) as Promise<Character>;
return await trpc.project.updateCharacter.mutate(input);
},
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 }>;
return createMutation(() => ({
mutationFn: async (id: number) => {
return await trpc.project.deleteCharacter.mutate({ id });
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['characters'] });
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
},
});
}));
}
// Relationship hooks
export function useRelationships(projectId: string) {
return createQuery({
export function useRelationships(projectId: number) {
return createQuery(() => ({
queryKey: ['characterRelationships', projectId],
queryFn: async () => {
const result = await trpcClient.project.listRelationships.query({ projectId });
return result as CharacterRelationship[];
return await trpc.project.listRelationships.query(undefined, {
context: { projectId },
});
},
enabled: !!projectId,
});
}));
}
export function useCharacterRelationships(characterId: string) {
return createQuery({
export function useCharacterRelationships(characterId: number) {
return createQuery(() => ({
queryKey: ['characterRelationships', characterId],
queryFn: async () => {
const result = await trpcClient.project.getRelationshipsForCharacter.query({ characterId });
return result as CharacterRelationship[];
return await trpc.project.getRelationshipsForCharacter.query({ characterId });
},
enabled: !!characterId,
});
}));
}
export function useCreateRelationship() {
const qc = useQueryClient();
return createMutation({
return createMutation(() => ({
mutationFn: async (input: {
characterIdA: string;
characterIdB: string;
characterIdA: number;
characterIdB: number;
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>;
return await trpc.project.createRelationship.mutate(input);
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
},
});
}));
}
export function useUpdateRelationship() {
const qc = useQueryClient();
return createMutation({
return createMutation(() => ({
mutationFn: async (input: {
id: string;
id: number;
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>;
return await trpc.project.updateRelationship.mutate(input);
},
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 }>;
return createMutation(() => ({
mutationFn: async (id: number) => {
return await trpc.project.deleteRelationship.mutate({ id });
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['characterRelationships'] });
},
});
}));
}
// Scene hooks
export function useScenes(projectId: string) {
return createQuery({
export function useScenes(projectId: number) {
return createQuery(() => ({
queryKey: ['scenes', projectId],
queryFn: async () => {
const result = await trpcClient.project.listScenes.query({ projectId });
return result as Scene[];
return await trpc.project.listScenes.query({ projectId });
},
enabled: !!projectId,
});
}));
}
export function useScene(sceneId: string) {
return createQuery({
queryKey: ['scene', sceneId],
export function useScene(id: number) {
return createQuery(() => ({
queryKey: ['scene', id],
queryFn: async () => {
const result = await trpcClient.project.getScene.query({ id: sceneId });
return result as Scene;
return await trpc.project.getScene.query({ id });
},
enabled: !!sceneId,
});
enabled: !!id,
}));
}
export function useCreateScene() {
const qc = useQueryClient();
return createMutation(() => ({
mutationFn: async (input: {
title: string;
content?: string;
projectId: number;
order: number;
}) => {
return await trpc.project.createScene.mutate(input);
},
onSuccess: (_, variables) => {
qc.invalidateQueries({ queryKey: ['scenes', variables.projectId] });
},
}));
}
export function useUpdateScene() {
const qc = useQueryClient();
return createMutation(() => ({
mutationFn: async (input: {
id: number;
title?: string;
content?: string;
order?: number;
}) => {
return await trpc.project.updateScene.mutate(input);
},
onSuccess: (_, variables) => {
qc.invalidateQueries({ queryKey: ['scenes'] });
},
}));
}
export function useDeleteScene() {
const qc = useQueryClient();
return createMutation(() => ({
mutationFn: async (id: number) => {
return await trpc.project.deleteScene.mutate({ id });
},
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['scenes'] });
},
}));
}