All checks were successful
Deploy to Production / Build and Deploy (push) Successful in 12s
- Sidebar: dynamic brand-accent colors, brainstorm section restyled - AI chat general: popup panel with expand/collapse, hides when contextual AI open - AI chat contextual: tabs reordered (Actions first), X close button, height fix - Settings: all tabs restyled, 6 new color presets (sage, terracotta, iron, etc.) - Global color cleanup: emerald/orange hardcoded → brand-accent dynamic - Brainstorm page: orange → brand-accent throughout - PageEntry animation component added to key pages - Floating AI button: bg-brand-accent instead of hardcoded black - i18n: all 15 locales updated with new AI/billing keys - Billing: freemium quota tracking, BYOK, stripe subscription scaffolding - Admin: integrated into new design - AGENTS.md + CLAUDE.md project rules added
338 lines
10 KiB
TypeScript
338 lines
10 KiB
TypeScript
'use client'
|
|
|
|
import { useQueryClient, useMutation, useQuery } from '@tanstack/react-query'
|
|
import { queryKeys } from '@/lib/query-keys'
|
|
import {
|
|
BrainstormQuotaError,
|
|
type BrainstormQuotaPayload,
|
|
} from '@/lib/brainstorm-quota-client'
|
|
import type { BrainstormSession, BrainstormSessionListItem } from '@/types/brainstorm'
|
|
|
|
function parseBrainstormQuota(res: Response, data: BrainstormQuotaPayload & Record<string, unknown>) {
|
|
if (res.status === 402 && data?.error === 'QUOTA_EXCEEDED') {
|
|
throw new BrainstormQuotaError(data, 'QUOTA_EXCEEDED')
|
|
}
|
|
}
|
|
|
|
/** i18n key for host-pays quota toasts (Story 3.4). */
|
|
export function brainstormQuotaMessageKey(err: unknown): string | null {
|
|
if (err instanceof BrainstormQuotaError) {
|
|
return err.isGuestActor ? 'brainstorm.quotaGuest' : 'brainstorm.quotaHost'
|
|
}
|
|
return null
|
|
}
|
|
|
|
export interface CreateBrainstormResult {
|
|
session: BrainstormSession
|
|
contextSummary?: {
|
|
support: number
|
|
tension: number
|
|
extension: number
|
|
}
|
|
}
|
|
|
|
export function useBrainstormSessions() {
|
|
return useQuery({
|
|
queryKey: queryKeys.brainstormSessions(),
|
|
queryFn: async (): Promise<BrainstormSessionListItem[]> => {
|
|
const res = await fetch('/api/brainstorm', { credentials: 'include' })
|
|
const data = await res.json()
|
|
return data.data || []
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useSharedBrainstormSessions() {
|
|
return useQuery({
|
|
queryKey: queryKeys.brainstormSharedSessions(),
|
|
queryFn: async (): Promise<BrainstormSessionListItem[]> => {
|
|
const res = await fetch('/api/brainstorm/shared', { credentials: 'include' })
|
|
const data = await res.json()
|
|
return data.data || []
|
|
},
|
|
})
|
|
}
|
|
|
|
export interface BrainstormSessionMeta {
|
|
role: 'owner' | 'editor' | 'viewer' | 'guest' | 'none'
|
|
canEdit: boolean
|
|
}
|
|
|
|
export interface BrainstormSessionResult {
|
|
session: BrainstormSession
|
|
meta?: BrainstormSessionMeta
|
|
}
|
|
|
|
export function useBrainstormSession(sessionId: string | null) {
|
|
return useQuery({
|
|
queryKey: queryKeys.brainstormSession(sessionId || ''),
|
|
queryFn: async (): Promise<BrainstormSessionResult | null> => {
|
|
if (!sessionId) return null
|
|
const res = await fetch(`/api/brainstorm/${sessionId}`, { credentials: 'include' })
|
|
const data = await res.json()
|
|
if (!data.data) return null
|
|
return {
|
|
session: data.data,
|
|
meta: data._meta || undefined,
|
|
}
|
|
},
|
|
enabled: !!sessionId,
|
|
})
|
|
}
|
|
|
|
export function useCreateBrainstorm() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (input: {
|
|
seedIdea: string
|
|
sourceNoteId?: string
|
|
contextNoteIds?: string[]
|
|
locale?: string
|
|
}): Promise<CreateBrainstormResult> => {
|
|
const res = await fetch('/api/brainstorm', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(input),
|
|
})
|
|
const data = await res.json()
|
|
parseBrainstormQuota(res, data)
|
|
if (!data.success) throw new Error(data.error || 'Failed to create brainstorm')
|
|
return { session: data.data, contextSummary: data.contextSummary }
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useExpandIdea(sessionId: string) {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (input: { ideaId: string; locale?: string }): Promise<BrainstormSession> => {
|
|
const res = await fetch(`/api/brainstorm/${sessionId}/expand`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(input),
|
|
})
|
|
const data = await res.json()
|
|
parseBrainstormQuota(res, data)
|
|
if (!data.success) throw new Error(data.error || 'Failed to expand idea')
|
|
return data.data
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useDismissIdea(sessionId: string) {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (ideaId: string) => {
|
|
const res = await fetch(`/api/brainstorm/${sessionId}/dismiss`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ ideaId }),
|
|
})
|
|
const data = await res.json()
|
|
if (!data.success) throw new Error(data.error || 'Failed to dismiss idea')
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useConvertIdea(sessionId: string) {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (ideaId: string) => {
|
|
const res = await fetch(`/api/brainstorm/${sessionId}/convert`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ ideaId }),
|
|
})
|
|
const data = await res.json()
|
|
if (!data.success) throw new Error(data.error || 'Failed to convert idea')
|
|
return data.data
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useExportBrainstorm(sessionId: string) {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async () => {
|
|
const res = await fetch(`/api/brainstorm/${sessionId}/export`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
})
|
|
const data = await res.json()
|
|
if (!data.success) throw new Error(data.error || 'Failed to export brainstorm')
|
|
return data.data
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useFinalizeBrainstorm(sessionId: string) {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async () => {
|
|
const res = await fetch(`/api/brainstorm/${sessionId}/finalize`, {
|
|
method: 'POST',
|
|
credentials: 'include',
|
|
})
|
|
const data = await res.json()
|
|
if (!data.success) throw new Error(data.error || 'Failed to finalize brainstorm')
|
|
return data.impact
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useDeleteBrainstorm() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (sessionId: string) => {
|
|
const res = await fetch(`/api/brainstorm/${sessionId}`, {
|
|
method: 'DELETE',
|
|
credentials: 'include',
|
|
})
|
|
const data = await res.json()
|
|
if (!data.success) throw new Error(data.error || 'Failed to delete brainstorm')
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useInviteParticipant(sessionId: string) {
|
|
return useMutation({
|
|
mutationFn: async (input: { role: 'editor' | 'viewer'; expiresInHours?: number; email?: string }) => {
|
|
const res = await fetch(`/api/brainstorm/${sessionId}/invite`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(input),
|
|
})
|
|
const data = await res.json()
|
|
if (!data.success) throw new Error(data.error || 'Failed to create invite')
|
|
return data
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useJoinBrainstorm() {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (token: string) => {
|
|
const res = await fetch(`/api/brainstorm/join`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify({ token }),
|
|
})
|
|
const data = await res.json()
|
|
if (!data.success) throw new Error(data.error || 'Failed to join')
|
|
return data
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSessions() })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useAddManualIdea(sessionId: string) {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (input: { title: string; description?: string; parentIdeaId?: string; locale?: string }) => {
|
|
const res = await fetch(`/api/brainstorm/${sessionId}/manual-idea`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(input),
|
|
})
|
|
const data = await res.json()
|
|
parseBrainstormQuota(res, data)
|
|
if (!data.success) throw new Error(data.error || 'Failed to add idea')
|
|
return data.data
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useBrainstormActivity(sessionId: string | null) {
|
|
return useQuery({
|
|
queryKey: ['brainstorm', 'activity', sessionId],
|
|
queryFn: async () => {
|
|
if (!sessionId) return []
|
|
const res = await fetch(`/api/brainstorm/${sessionId}/activity`, { credentials: 'include' })
|
|
const data = await res.json()
|
|
return data.data || []
|
|
},
|
|
enabled: !!sessionId,
|
|
refetchInterval: 10000,
|
|
})
|
|
}
|
|
|
|
export function useUpdateBrainstormSettings(sessionId: string) {
|
|
const queryClient = useQueryClient()
|
|
|
|
return useMutation({
|
|
mutationFn: async (input: { isPublic?: boolean; guestCanEdit?: boolean }) => {
|
|
const res = await fetch(`/api/brainstorm/${sessionId}`, {
|
|
method: 'PATCH',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
credentials: 'include',
|
|
body: JSON.stringify(input),
|
|
})
|
|
const data = await res.json()
|
|
if (!data.success) throw new Error(data.error || 'Failed to update settings')
|
|
return data.data
|
|
},
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({ queryKey: queryKeys.brainstormSession(sessionId) })
|
|
},
|
|
})
|
|
}
|
|
|
|
export function useBrainstormSnapshots(sessionId: string | null) {
|
|
return useQuery({
|
|
queryKey: ['brainstorm', 'snapshots', sessionId],
|
|
queryFn: async () => {
|
|
if (!sessionId) return []
|
|
const res = await fetch(`/api/brainstorm/${sessionId}/snapshots`, { credentials: 'include' })
|
|
const data = await res.json()
|
|
return data.data || []
|
|
},
|
|
enabled: !!sessionId,
|
|
})
|
|
}
|