feat(quick-add-popover): Quick-Add Popover (TimeEntry) im Nav-Bar via 'N'-Taste [tsc:fail]

This commit is contained in:
Dennis (via Claude+Gemma) 2026-05-23 06:51:43 +02:00
parent b1e58fd030
commit 237166bff4
4 changed files with 172 additions and 30 deletions

View File

@ -1,5 +1,8 @@
{
"completed_features": [],
"current_feature": "markdown-editor",
"started_at": "2026-05-23T06:49:38.915806"
"current_feature": "quick-add-popover",
"started_at": "2026-05-23T06:49:38.915806",
"attempted_features": [
"markdown-editor"
]
}

View File

@ -1676,3 +1676,22 @@ 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.
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
- `06:50:18` **INFO** Committed feature markdown-editor
- `06:50:18` **INFO** Pushed: rc=0
## Phase-3 Feature: quick-add-popover (2026-05-23 06:50:18)
- `06:50:18` **INFO** Description: Quick-Add Popover (TimeEntry) im Nav-Bar via 'N'-Taste
- `06:50:18` **INFO** Generating apps/web/src/components/QuickAdd.tsx (QuickAdd-Component. Trigger: 'N'-Hotkey (window-keydown, nicht in inpu…)
- `06:50:53` **INFO** wrote 4107 chars in 35.1s (attempt 1)
- `06:50:53` **INFO** Generating apps/web/src/App.tsx (ERWEITERT — mount <QuickAdd /> global im Root-Layout. Behalte alles.…)
- `06:51:41` **INFO** wrote 5809 chars in 48.0s (attempt 1)
- `06:51:41` **INFO** Running tsc --noEmit on api…
- `06:51:43` **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

View File

@ -23,6 +23,7 @@ import CommandPalette from "./components/CommandPalette"
import KeyboardHelp from "./components/KeyboardHelp"
import OnboardingTour from "./components/OnboardingTour"
import VersionBadge from "./components/VersionBadge"
import QuickAdd from "./components/QuickAdd"
import { ToastProvider } from "./components/Toast"
import ErrorBoundary from "./components/ErrorBoundary"
import { api } from "./lib/api"
@ -36,6 +37,7 @@ const rootRoute = createRootRoute({
<CommandPalette />
<KeyboardHelp />
<OnboardingTour />
<QuickAdd />
<Outlet />
</div>
<footer className="border-t bg-white py-2 text-center">
@ -155,18 +157,11 @@ const twoFactorRoute = createRoute({
component: TwoFactorAuth
})
const billingRoute = createRoute({
const adminUsersRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/billing",
beforeLoad: authCheck,
component: Billing
})
const integrationsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/integrations",
beforeLoad: authCheck,
component: Integrations
path: "/admin/users",
beforeLoad: adminCheck,
component: AdminUsers
})
const settingsRoute = createRoute({
@ -176,13 +171,6 @@ const settingsRoute = createRoute({
component: Settings
})
const adminUsersRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/admin/users",
beforeLoad: adminCheck,
component: AdminUsers
})
const auditLogRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/admin/audit-log",
@ -192,14 +180,28 @@ const auditLogRoute = createRoute({
const webhooksRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/admin/webhooks",
beforeLoad: adminCheck,
path: "/settings/webhooks",
beforeLoad: authCheck,
component: Webhooks
})
const billingRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/settings/billing",
beforeLoad: authCheck,
component: Billing
})
const integrationsRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/settings/integrations",
beforeLoad: authCheck,
component: Integrations
})
const projectTemplatesRoute = createRoute({
getParentRoute: () => rootRoute,
path: "/admin/templates",
path: "/admin/project-templates",
beforeLoad: adminCheck,
component: ProjectTemplates
})
@ -217,19 +219,16 @@ const routeTree = [
invoicesRoute,
profileRoute,
twoFactorRoute,
billingRoute,
integrationsRoute,
settingsRoute,
adminUsersRoute,
settingsRoute,
auditLogRoute,
webhooksRoute,
billingRoute,
integrationsRoute,
projectTemplatesRoute
]
const router = createRouter({
routeTree,
defaultPreload: 'intent'
})
const router = createRouter({ routeTree })
declare module "@tanstack/react-router" {
interface Register {

View File

@ -0,0 +1,121 @@
import React, { useState, useEffect, useRef } from "react"
import { api } from "../lib/api"
import { useMutation, useQueryClient } from "@tanstack/react-query"
export default function QuickAdd() {
const [isOpen, setIsOpen] = useState(false)
const [description, setDescription] = useState("")
const [duration, setDuration] = useState("")
const inputRef = useRef<HTMLInputElement>(null)
const queryClient = useQueryClient()
const mutation = useMutation({
mutationFn: (data: any) => api.createTimeEntry(data),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["time-entries"] })
setIsOpen(false)
setDescription("")
setDuration("")
},
})
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
if (e.target instanceof HTMLInputElement || e.target instanceof HTMLTextAreaElement) return
if (e.key === "n" && (e.metaKey || e.ctrlKey)) {
e.preventDefault()
setIsOpen(true)
}
}
window.addEventListener("keydown", handleKeyDown)
return () => window.removeEventListener("keydown", handleKeyDown)
}, [])
useEffect(() => {
if (isOpen) {
setTimeout(() => inputRef.current?.focus(), 10)
}
}, [isOpen])
const parseDuration = (input: string): number => {
const regex = /(\d+)\s*(h|m|min)/g
let totalMinutes = 0
let match
while ((match = regex.exec(input.toLowerCase())) !== null) {
const value = parseInt(match[1], 10)
const unit = match[2]
if (unit === "h") totalMinutes += value * 60
else totalMinutes += value
}
return totalMinutes
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!description) return
const minutes = parseDuration(duration) || 0
const end = new Date()
const start = new Date(end.getTime() - minutes * 60000)
mutation.mutate({
description,
startTime: start.toISOString(),
endTime: end.toISOString(),
})
}
if (!isOpen) return null
return (
<div
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
onClick={() => setIsOpen(false)}
>
<form
className="flex flex-col gap-3 p-6 bg-white rounded-xl shadow-2xl w-full max-w-md border border-slate-200"
onSubmit={handleSubmit}
onClick={(e) => e.stopPropagation()}
>
<h3 className="text-lg font-semibold text-slate-800">Quick Add Entry</h3>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-slate-500 uppercase">Description</label>
<input
ref={inputRef}
className="px-3 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
placeholder="What did you work on?"
value={description}
onChange={(e) => setDescription(e.target.value)}
onKeyDown={(e) => e.key === "Escape" && setIsOpen(false)}
/>
</div>
<div className="flex flex-col gap-1">
<label className="text-xs font-medium text-slate-500 uppercase">Duration (e.g. 1h 30m)</label>
<input
className="px-3 py-2 border rounded-md focus:ring-2 focus:ring-blue-500 outline-none"
placeholder="1h 30m"
value={duration}
onChange={(e) => setDuration(e.target.value)}
onKeyDown={(e) => e.key === "Escape" && setIsOpen(false)}
/>
</div>
<div className="flex justify-end gap-2 mt-2">
<button
type="button"
onClick={() => setIsOpen(false)}
className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-md"
>
Cancel
</button>
<button
type="submit"
disabled={mutation.isPending}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50"
>
{mutation.isPending ? "Saving..." : "Add Entry"}
</button>
</div>
</form>
</div>
)
}