feat: add TypeScript types, API client, and TanStack Query hooks
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,12 @@
|
|||||||
|
import axios from 'axios'
|
||||||
|
import type { ApiResponse } from '../types'
|
||||||
|
|
||||||
|
const api = axios.create({ baseURL: '/api' })
|
||||||
|
|
||||||
|
export async function request<T>(config: Parameters<typeof api.request>[0]): Promise<T> {
|
||||||
|
const { data } = await api.request<ApiResponse<T>>(config)
|
||||||
|
if (!data.success) throw new Error(data.error ?? 'API error')
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
|
export default api
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query'
|
||||||
|
import { request } from './client'
|
||||||
|
import type { ContextEvent, ContextSummaryItem } from '../types'
|
||||||
|
|
||||||
|
export function useRecentContext(minutes = 30) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['context', 'recent', minutes],
|
||||||
|
queryFn: () => request<ContextEvent[]>({ url: '/context/recent', params: { minutes } }),
|
||||||
|
refetchInterval: 60_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useContextSummary() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['context', 'summary'],
|
||||||
|
queryFn: () => request<ContextSummaryItem[]>({ url: '/context/summary' }),
|
||||||
|
refetchInterval: 60_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { request } from './client'
|
||||||
|
import type { AppMapping } from '../types'
|
||||||
|
|
||||||
|
export function useMappings() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['mappings'],
|
||||||
|
queryFn: () => request<AppMapping[]>({ url: '/mappings' }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateMapping() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (body: { pattern: string; matchType: string; category: string; friendlyName?: string }) =>
|
||||||
|
request<AppMapping>({ method: 'POST', url: '/mappings', data: body }),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['mappings'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateMapping() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, ...body }: { id: number; pattern: string; matchType: string; category: string; friendlyName?: string }) =>
|
||||||
|
request<AppMapping>({ method: 'PUT', url: `/mappings/${id}`, data: body }),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['mappings'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useDeleteMapping() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => request<void>({ method: 'DELETE', url: `/mappings/${id}` }),
|
||||||
|
onSuccess: () => qc.invalidateQueries({ queryKey: ['mappings'] }),
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
|
import { request } from './client'
|
||||||
|
import type { WorkTask } from '../types'
|
||||||
|
|
||||||
|
export function useTasks(includeSubTasks = true) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['tasks', { includeSubTasks }],
|
||||||
|
queryFn: () => request<WorkTask[]>({ url: '/tasks', params: { includeSubTasks } }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useActiveTask() {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['tasks', 'active'],
|
||||||
|
queryFn: () => request<WorkTask | null>({ url: '/tasks/active' }),
|
||||||
|
refetchInterval: 30_000,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useTask(id: number) {
|
||||||
|
return useQuery({
|
||||||
|
queryKey: ['tasks', id],
|
||||||
|
queryFn: () => request<WorkTask>({ url: `/tasks/${id}` }),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
function useInvalidateTasks() {
|
||||||
|
const qc = useQueryClient()
|
||||||
|
return () => {
|
||||||
|
qc.invalidateQueries({ queryKey: ['tasks'] })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCreateTask() {
|
||||||
|
const invalidate = useInvalidateTasks()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (body: { title: string; description?: string; category?: string; parentTaskId?: number; estimatedMinutes?: number }) =>
|
||||||
|
request<WorkTask>({ method: 'POST', url: '/tasks', data: body }),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUpdateTask() {
|
||||||
|
const invalidate = useInvalidateTasks()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, ...body }: { id: number; title?: string; description?: string; category?: string; estimatedMinutes?: number }) =>
|
||||||
|
request<WorkTask>({ method: 'PUT', url: `/tasks/${id}`, data: body }),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useStartTask() {
|
||||||
|
const invalidate = useInvalidateTasks()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => request<WorkTask>({ method: 'PUT', url: `/tasks/${id}/start` }),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePauseTask() {
|
||||||
|
const invalidate = useInvalidateTasks()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, note }: { id: number; note?: string }) =>
|
||||||
|
request<WorkTask>({ method: 'PUT', url: `/tasks/${id}/pause`, data: { note } }),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useResumeTask() {
|
||||||
|
const invalidate = useInvalidateTasks()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: ({ id, note }: { id: number; note?: string }) =>
|
||||||
|
request<WorkTask>({ method: 'PUT', url: `/tasks/${id}/resume`, data: { note } }),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useCompleteTask() {
|
||||||
|
const invalidate = useInvalidateTasks()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => request<WorkTask>({ method: 'PUT', url: `/tasks/${id}/complete` }),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useAbandonTask() {
|
||||||
|
const invalidate = useInvalidateTasks()
|
||||||
|
return useMutation({
|
||||||
|
mutationFn: (id: number) => request<void>({ method: 'DELETE', url: `/tasks/${id}` }),
|
||||||
|
onSuccess: invalidate,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -1,10 +1,17 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import { createRoot } from 'react-dom/client'
|
import { createRoot } from 'react-dom/client'
|
||||||
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
||||||
import './index.css'
|
import './index.css'
|
||||||
import App from './App'
|
import App from './App'
|
||||||
|
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions: { queries: { staleTime: 10_000, retry: 1 } },
|
||||||
|
})
|
||||||
|
|
||||||
createRoot(document.getElementById('root')!).render(
|
createRoot(document.getElementById('root')!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<App />
|
<QueryClientProvider client={queryClient}>
|
||||||
|
<App />
|
||||||
|
</QueryClientProvider>
|
||||||
</StrictMode>,
|
</StrictMode>,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
export const WorkTaskStatus = {
|
||||||
|
Pending: 0,
|
||||||
|
Active: 1,
|
||||||
|
Paused: 2,
|
||||||
|
Completed: 3,
|
||||||
|
Abandoned: 4,
|
||||||
|
} as const
|
||||||
|
export type WorkTaskStatus = (typeof WorkTaskStatus)[keyof typeof WorkTaskStatus]
|
||||||
|
|
||||||
|
export const NoteType = {
|
||||||
|
PauseNote: 0,
|
||||||
|
ResumeNote: 1,
|
||||||
|
General: 2,
|
||||||
|
} as const
|
||||||
|
export type NoteType = (typeof NoteType)[keyof typeof NoteType]
|
||||||
|
|
||||||
|
export interface WorkTask {
|
||||||
|
id: number
|
||||||
|
title: string
|
||||||
|
description: string | null
|
||||||
|
status: WorkTaskStatus
|
||||||
|
category: string | null
|
||||||
|
createdAt: string
|
||||||
|
startedAt: string | null
|
||||||
|
completedAt: string | null
|
||||||
|
estimatedMinutes: number | null
|
||||||
|
parentTaskId: number | null
|
||||||
|
subTasks: WorkTask[]
|
||||||
|
notes: TaskNote[]
|
||||||
|
contextEvents: ContextEvent[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskNote {
|
||||||
|
id: number
|
||||||
|
workTaskId: number
|
||||||
|
content: string
|
||||||
|
type: NoteType
|
||||||
|
createdAt: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextEvent {
|
||||||
|
id: number
|
||||||
|
workTaskId: number | null
|
||||||
|
source: string
|
||||||
|
appName: string
|
||||||
|
windowTitle: string
|
||||||
|
url: string | null
|
||||||
|
timestamp: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AppMapping {
|
||||||
|
id: number
|
||||||
|
pattern: string
|
||||||
|
matchType: string
|
||||||
|
category: string
|
||||||
|
friendlyName: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ContextSummaryItem {
|
||||||
|
appName: string
|
||||||
|
category: string
|
||||||
|
eventCount: number
|
||||||
|
firstSeen: string
|
||||||
|
lastSeen: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiResponse<T> {
|
||||||
|
success: boolean
|
||||||
|
data: T
|
||||||
|
error: string | null
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user