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[]
|
tech_stack: string[]
|
||||||
platform: string | null
|
platform: string | null
|
||||||
status: 'active' | 'archived' | 'paused'
|
status: 'active' | 'archived' | 'paused'
|
||||||
|
phase_metadata: Json | null
|
||||||
created_at: string
|
created_at: string
|
||||||
updated_at: string
|
updated_at: string
|
||||||
}
|
}
|
||||||
@@ -111,6 +112,7 @@ export interface Database {
|
|||||||
tech_stack?: string[]
|
tech_stack?: string[]
|
||||||
platform?: string | null
|
platform?: string | null
|
||||||
status?: 'active' | 'archived' | 'paused'
|
status?: 'active' | 'archived' | 'paused'
|
||||||
|
phase_metadata?: Json | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
}
|
}
|
||||||
@@ -124,6 +126,7 @@ export interface Database {
|
|||||||
tech_stack?: string[]
|
tech_stack?: string[]
|
||||||
platform?: string | null
|
platform?: string | null
|
||||||
status?: 'active' | 'archived' | 'paused'
|
status?: 'active' | 'archived' | 'paused'
|
||||||
|
phase_metadata?: Json | null
|
||||||
created_at?: string
|
created_at?: string
|
||||||
updated_at?: string
|
updated_at?: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user