feat(popout-tracker): Active-Timer als Popup-Window (window.open) [tsc:fail]
This commit is contained in:
parent
61d337844c
commit
1150dd635f
@ -1,5 +1,8 @@
|
|||||||
{
|
{
|
||||||
"completed_features": [],
|
"completed_features": [],
|
||||||
"current_feature": "voice-input-stub",
|
"current_feature": "popout-tracker",
|
||||||
"started_at": "2026-05-23T08:17:40.778724"
|
"started_at": "2026-05-23T08:17:40.778724",
|
||||||
|
"attempted_features": [
|
||||||
|
"voice-input-stub"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
@ -2610,3 +2610,20 @@ src/index.ts(27,25): error TS2769: No overload matches this call.
|
|||||||
Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error.
|
Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error.
|
||||||
Argument of type 'Promise<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
Argument of type 'Promise<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||||
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTy
|
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTy
|
||||||
|
- `08:18:05` **INFO** Committed feature voice-input-stub
|
||||||
|
- `08:18:05` **INFO** Pushed: rc=0
|
||||||
|
|
||||||
|
## Phase-3 Feature: popout-tracker (2026-05-23 08:18:05)
|
||||||
|
|
||||||
|
- `08:18:05` **INFO** Description: Active-Timer als Popup-Window (window.open)
|
||||||
|
- `08:18:05` **INFO** Generating apps/web/src/components/ActiveTimer.tsx (ERWEITERT — behalte bestehende ActiveTimer. Füge 'Pop out'-Button: win…)
|
||||||
|
- `08:19:23` **INFO** wrote 8854 chars in 78.1s (attempt 1)
|
||||||
|
- `08:19:23` **INFO** Running tsc --noEmit on api…
|
||||||
|
- `08:19:25` **WARN** tsc errors:
|
||||||
|
src/index.ts(27,25): error TS2769: No overload matches this call.
|
||||||
|
Overload 1 of 3, '(plugin: FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error.
|
||||||
|
Argument of type 'Promise<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginCallback<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||||
|
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTypeProvider>, opts: { ...; }, done: (err?: Error | undefined) => void): void'.
|
||||||
|
Overload 2 of 3, '(plugin: FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>, opts?: FastifyRegisterOptions<...> | undefined): FastifyInstance<...> & PromiseLike<...>', gave the following error.
|
||||||
|
Argument of type 'Promise<FastifyMultipartPlugin>' is not assignable to parameter of type 'FastifyPluginAsync<{ limits: { fileSize: number; }; }, RawServerDefault, FastifyTypeProvider, FastifyBaseLogger>'.
|
||||||
|
Type 'Promise<FastifyMultipartPlugin>' provides no match for the signature '(instance: FastifyInstance<RawServerDefault, IncomingMessage, ServerResponse<IncomingMessage>, FastifyBaseLogger, FastifyTy
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from 'react'
|
||||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||||
import { api } from '../lib/api'
|
import { api } from '../lib/api'
|
||||||
import { Loader2, Play, Square } from 'lucide-react'
|
import { Loader2, Play, Square, ExternalLink } from 'lucide-react'
|
||||||
|
|
||||||
export const ActiveTimer = () => {
|
export const ActiveTimer = () => {
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@ -56,6 +56,10 @@ export const ActiveTimer = () => {
|
|||||||
: `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
|
: `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handlePopout = () => {
|
||||||
|
window.open('/tracker-popout', 'tracker', 'width=320,height=220')
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) return <div className="h-10 w-10 flex items-center justify-center"><Loader2 className="w-4 h-4 animate-spin" /></div>
|
if (isLoading) return <div className="h-10 w-10 flex items-center justify-center"><Loader2 className="w-4 h-4 animate-spin" /></div>
|
||||||
|
|
||||||
if (runningEntry) {
|
if (runningEntry) {
|
||||||
@ -67,6 +71,14 @@ export const ActiveTimer = () => {
|
|||||||
<span className="tabular-nums font-mono text-blue-600 dark:text-blue-400">
|
<span className="tabular-nums font-mono text-blue-600 dark:text-blue-400">
|
||||||
{formatTime(elapsed)}
|
{formatTime(elapsed)}
|
||||||
</span>
|
</span>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button
|
||||||
|
onClick={handlePopout}
|
||||||
|
title="Pop out timer"
|
||||||
|
className="p-1 hover:bg-slate-200 dark:hover:bg-slate-700 text-slate-500 dark:text-slate-400 rounded-full transition-colors"
|
||||||
|
>
|
||||||
|
<ExternalLink className="w-4 h-4" />
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={() => stopMutation.mutate(runningEntry.id)}
|
onClick={() => stopMutation.mutate(runningEntry.id)}
|
||||||
disabled={stopMutation.isPending}
|
disabled={stopMutation.isPending}
|
||||||
@ -75,6 +87,7 @@ export const ActiveTimer = () => {
|
|||||||
<Square className="w-4 h-4 fill-current" />
|
<Square className="w-4 h-4 fill-current" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -101,3 +114,121 @@ export const ActiveTimer = () => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PopoutTrackerView = () => {
|
||||||
|
const queryClient = useQueryClient()
|
||||||
|
const [description, setDescription] = useState('')
|
||||||
|
const [isInputVisible, setIsInputVisible] = useState(false)
|
||||||
|
const [elapsed, setElapsed] = useState(0)
|
||||||
|
|
||||||
|
const { data: runningEntry, isLoading } = useQuery({
|
||||||
|
queryKey: ['running-entry'],
|
||||||
|
queryFn: () => api.getRunningTimeEntry(),
|
||||||
|
refetchInterval: 10000,
|
||||||
|
})
|
||||||
|
|
||||||
|
const startMutation = useMutation({
|
||||||
|
mutationFn: (desc: string) => api.createTimeEntry({ description, startTime: new Date().toISOString() }),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['running-entry'] })
|
||||||
|
setIsInputVisible(false)
|
||||||
|
setDescription('')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const stopMutation = useMutation({
|
||||||
|
mutationFn: (id: string) => api.stopTimeEntry(id),
|
||||||
|
onSuccess: () => {
|
||||||
|
queryClient.invalidateQueries({ queryKey: ['running-entry'] })
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let interval: NodeJS.Timeout
|
||||||
|
if (runningEntry?.startTime) {
|
||||||
|
const update = () => {
|
||||||
|
const start = new Date(runningEntry.startTime).getTime()
|
||||||
|
const now = Date.now()
|
||||||
|
setElapsed(Math.floor((now - start) / 1000))
|
||||||
|
}
|
||||||
|
update()
|
||||||
|
interval = setInterval(update, 1000)
|
||||||
|
} else {
|
||||||
|
setElapsed(0)
|
||||||
|
}
|
||||||
|
return () => clearInterval(interval)
|
||||||
|
}, [runningEntry])
|
||||||
|
|
||||||
|
const formatTime = (seconds: number) => {
|
||||||
|
const h = Math.floor(seconds / 3600)
|
||||||
|
const m = Math.floor((seconds % 3600) / 60)
|
||||||
|
const s = seconds % 60
|
||||||
|
return h > 0
|
||||||
|
? `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
|
||||||
|
: `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLoading) return <div className="flex items-center justify-center h-full"><Loader2 className="w-6 h-6 animate-spin" /></div>
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="p-4 flex flex-col items-center justify-center h-full gap-4 text-center">
|
||||||
|
{runningEntry ? (
|
||||||
|
<>
|
||||||
|
<div className="text-slate-500 dark:text-slate-400 text-sm truncate w-full max-w-[200px]">
|
||||||
|
{runningEntry.description || 'No description'}
|
||||||
|
</div>
|
||||||
|
<div className="text-4xl font-mono font-bold tabular-nums text-blue-600 dark:text-blue-400">
|
||||||
|
{formatTime(elapsed)}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
onClick={() => stopMutation.mutate(runningEntry.id)}
|
||||||
|
disabled={stopMutation.isPending}
|
||||||
|
className="flex items-center gap-2 px-4 py-2 bg-red-600 hover:bg-red-700 text-white rounded-md transition-colors font-medium"
|
||||||
|
>
|
||||||
|
<Square className="w-4 h-4 fill-current" />
|
||||||
|
Stop Timer
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<div className="text-slate-500 dark:text-slate-400 text-sm">No active timer</div>
|
||||||
|
{isInputVisible ? (
|
||||||
|
<div className="flex flex-col gap-2 w-full max-w-[200px]">
|
||||||
|
<input
|
||||||
|
autoFocus
|
||||||
|
className="px-2 py-1 text-sm border rounded-md bg-white dark:bg-slate-900 dark:border-slate-700 outline-none focus:ring-1 ring-blue-500"
|
||||||
|
placeholder="What are you working on?"
|
||||||
|
value={description}
|
||||||
|
onChange={(e) => setDescription(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && startMutation.mutate(description)}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => startMutation.mutate(description)}
|
||||||
|
disabled={startMutation.isPending}
|
||||||
|
className="flex-1 px-3 py-1 bg-blue-600 text-white text-xs rounded-md"
|
||||||
|
>
|
||||||
|
Start
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setIsInputVisible(false)}
|
||||||
|
className="flex-1 px-3 py-1 bg-slate-200 dark:bg-slate-700 text-xs rounded-md"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={() => setIsInputVisible(true)}
|
||||||
|
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 text-white rounded-md transition-colors font-medium flex items-center gap-2"
|
||||||
|
>
|
||||||
|
<Play className="w-4 h-4 fill-current" />
|
||||||
|
Start Timer
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user