121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
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>
|
|
)
|
|
} |