EmberClone/apps/web/src/components/QuickAdd.tsx

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>
)
}