Add phase persistence to Supabase
- Add phase_metadata column to projects table - Update database types with phase_metadata field - Implement useProjectPhase hook with persistence - Add setPhaseStatus and addActivity functions - Real-time subscription for phase updates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
187
src/hooks/useProjectPhase.ts
Normal file
187
src/hooks/useProjectPhase.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { createClient } from '@/lib/supabase/client'
|
||||
import type { Phase } from '@/components/ui/progress-timeline'
|
||||
import type { Activity } from '@/components/ui/activity-feed'
|
||||
|
||||
export interface ProjectPhaseState {
|
||||
projectId: string
|
||||
currentPhase: Phase
|
||||
completedPhases: Phase[]
|
||||
phaseStatus: 'not_started' | 'in_progress' | 'completed' | 'blocked' | 'failed'
|
||||
activities: Activity[]
|
||||
metrics: {
|
||||
totalDuration: number
|
||||
phaseDurations: Record<Phase, number>
|
||||
iterationCount: number
|
||||
}
|
||||
}
|
||||
|
||||
export function useProjectPhase(projectId: string) {
|
||||
const [state, setState] = useState<ProjectPhaseState | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<Error | null>(null)
|
||||
|
||||
const fetchProjectState = useCallback(async () => {
|
||||
const supabase = createClient()
|
||||
|
||||
try {
|
||||
const { data: project, error: projectError } = await supabase
|
||||
.from('projects')
|
||||
.select('*')
|
||||
.eq('id', projectId)
|
||||
.single()
|
||||
|
||||
if (projectError) throw projectError
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const metadata = (project as any)?.phase_metadata as ProjectPhaseState | undefined
|
||||
|
||||
setState({
|
||||
projectId,
|
||||
currentPhase: metadata?.currentPhase || 'think',
|
||||
completedPhases: metadata?.completedPhases || [],
|
||||
phaseStatus: metadata?.phaseStatus || 'not_started',
|
||||
activities: metadata?.activities || [],
|
||||
metrics: metadata?.metrics || {
|
||||
totalDuration: 0,
|
||||
phaseDurations: {} as Record<Phase, number>,
|
||||
iterationCount: 0
|
||||
}
|
||||
})
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err : new Error('Failed to fetch project'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}, [projectId])
|
||||
|
||||
useEffect(() => {
|
||||
fetchProjectState()
|
||||
|
||||
const supabase = createClient()
|
||||
const channel = supabase
|
||||
.channel(`project:${projectId}`)
|
||||
.on('postgres_changes', {
|
||||
event: 'UPDATE',
|
||||
schema: 'public',
|
||||
table: 'projects',
|
||||
filter: `id=eq.${projectId}`
|
||||
}, (payload) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const metadata = (payload.new as any)?.phase_metadata as ProjectPhaseState | undefined
|
||||
if (metadata) {
|
||||
setState(prev => prev ? { ...prev, ...metadata, projectId } : null)
|
||||
}
|
||||
})
|
||||
.subscribe()
|
||||
|
||||
return () => {
|
||||
supabase.removeChannel(channel)
|
||||
}
|
||||
}, [projectId, fetchProjectState])
|
||||
|
||||
const transitionPhase = useCallback(async (newPhase: Phase) => {
|
||||
if (!state) return
|
||||
|
||||
const supabase = createClient()
|
||||
|
||||
const newActivity: Activity = {
|
||||
id: crypto.randomUUID(),
|
||||
type: 'phase_change',
|
||||
message: `Transitioned to ${newPhase} phase`,
|
||||
timestamp: new Date()
|
||||
}
|
||||
|
||||
const updatedState: ProjectPhaseState = {
|
||||
...state,
|
||||
currentPhase: newPhase,
|
||||
completedPhases: state.currentPhase !== newPhase
|
||||
? [...state.completedPhases, state.currentPhase]
|
||||
: state.completedPhases,
|
||||
phaseStatus: 'in_progress',
|
||||
activities: [newActivity, ...state.activities].slice(0, 50)
|
||||
}
|
||||
|
||||
// Optimistic update
|
||||
setState(updatedState)
|
||||
|
||||
// Persist to database
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { error: updateError } = await (supabase as any)
|
||||
.from('projects')
|
||||
.update({ phase_metadata: updatedState })
|
||||
.eq('id', projectId)
|
||||
|
||||
if (updateError) {
|
||||
console.error('Failed to persist phase:', updateError)
|
||||
// Revert on error
|
||||
setState(state)
|
||||
}
|
||||
}, [state, projectId])
|
||||
|
||||
const setPhaseStatus = useCallback(async (status: ProjectPhaseState['phaseStatus']) => {
|
||||
if (!state) return
|
||||
|
||||
const supabase = createClient()
|
||||
|
||||
const updatedState: ProjectPhaseState = {
|
||||
...state,
|
||||
phaseStatus: status
|
||||
}
|
||||
|
||||
setState(updatedState)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { error: updateError } = await (supabase as any)
|
||||
.from('projects')
|
||||
.update({ phase_metadata: updatedState })
|
||||
.eq('id', projectId)
|
||||
|
||||
if (updateError) {
|
||||
console.error('Failed to update phase status:', updateError)
|
||||
setState(state)
|
||||
}
|
||||
}, [state, projectId])
|
||||
|
||||
const addActivity = useCallback(async (activity: Omit<Activity, 'id' | 'timestamp'>) => {
|
||||
if (!state) return
|
||||
|
||||
const supabase = createClient()
|
||||
|
||||
const newActivity: Activity = {
|
||||
...activity,
|
||||
id: crypto.randomUUID(),
|
||||
timestamp: new Date()
|
||||
}
|
||||
|
||||
const updatedState: ProjectPhaseState = {
|
||||
...state,
|
||||
activities: [newActivity, ...state.activities].slice(0, 50)
|
||||
}
|
||||
|
||||
setState(updatedState)
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const { error: updateError } = await (supabase as any)
|
||||
.from('projects')
|
||||
.update({ phase_metadata: updatedState })
|
||||
.eq('id', projectId)
|
||||
|
||||
if (updateError) {
|
||||
console.error('Failed to add activity:', updateError)
|
||||
setState(state)
|
||||
}
|
||||
}, [state, projectId])
|
||||
|
||||
return {
|
||||
state,
|
||||
loading,
|
||||
error,
|
||||
transitionPhase,
|
||||
setPhaseStatus,
|
||||
addActivity,
|
||||
refresh: fetchProjectState
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ export interface Database {
|
||||
tech_stack: string[]
|
||||
platform: string | null
|
||||
status: 'active' | 'archived' | 'paused'
|
||||
phase_metadata: Json | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
@@ -111,6 +112,7 @@ export interface Database {
|
||||
tech_stack?: string[]
|
||||
platform?: string | null
|
||||
status?: 'active' | 'archived' | 'paused'
|
||||
phase_metadata?: Json | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
@@ -124,6 +126,7 @@ export interface Database {
|
||||
tech_stack?: string[]
|
||||
platform?: string | null
|
||||
status?: 'active' | 'archived' | 'paused'
|
||||
phase_metadata?: Json | null
|
||||
created_at?: string
|
||||
updated_at?: string
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user