feat(quick-add-popover): Quick-Add Popover (TimeEntry) im Nav-Bar via 'N'-Taste [tsc:fail]
This commit is contained in:
parent
b1e58fd030
commit
237166bff4
@ -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"
|
||||
]
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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 {
|
||||
|
||||
121
apps/web/src/components/QuickAdd.tsx
Normal file
121
apps/web/src/components/QuickAdd.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user