Initial commit: Todo app with Jira-style board

This commit is contained in:
PhongMacbook
2025-12-16 23:13:06 +07:00
commit fecae546e9
44 changed files with 6671 additions and 0 deletions

View File

@@ -0,0 +1,135 @@
'use client'
import { useState, useEffect } from 'react'
import { Task } from '@prisma/client'
import { Button, Input, TextArea, Select } from '@/components/ui'
import { formatDateInput } from '@/lib/utils'
import toast from 'react-hot-toast'
interface TaskFormProps {
task?: Task | null
defaultStatus?: string
onSubmit: (data: TaskFormData) => Promise<void>
onCancel: () => void
}
export interface TaskFormData {
title: string
description: string
status: string
deadline: string
}
const statusOptions = [
{ value: 'TODO', label: 'Chưa làm' },
{ value: 'IN_PROGRESS', label: 'Đang làm' },
{ value: 'DONE', label: 'Hoàn thành' },
]
export function TaskForm({ task, defaultStatus = 'TODO', onSubmit, onCancel }: TaskFormProps) {
const [formData, setFormData] = useState<TaskFormData>({
title: '',
description: '',
status: defaultStatus,
deadline: '',
})
const [loading, setLoading] = useState(false)
const [errors, setErrors] = useState<Record<string, string>>({})
useEffect(() => {
if (task) {
setFormData({
title: task.title,
description: task.description || '',
status: task.status,
deadline: formatDateInput(task.deadline),
})
} else {
setFormData(prev => ({ ...prev, status: defaultStatus }))
}
}, [task, defaultStatus])
const validate = (): boolean => {
const newErrors: Record<string, string> = {}
if (!formData.title.trim()) {
newErrors.title = 'Tiêu đề không được để trống'
} else if (formData.title.length > 200) {
newErrors.title = 'Tiêu đề tối đa 200 ký tự'
}
if (formData.description.length > 1000) {
newErrors.description = 'Mô tả tối đa 1000 ký tự'
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
if (!validate()) return
setLoading(true)
try {
await onSubmit(formData)
} catch (error) {
toast.error('Đã có lỗi xảy ra')
} finally {
setLoading(false)
}
}
return (
<form onSubmit={handleSubmit} className="space-y-4">
<Input
label="Tiêu đề *"
placeholder="Nhập tiêu đề công việc"
value={formData.title}
onChange={(e) => setFormData({ ...formData, title: e.target.value })}
error={errors.title}
disabled={loading}
/>
<TextArea
label="Mô tả"
placeholder="Nhập mô tả công việc (không bắt buộc)"
value={formData.description}
onChange={(e) => setFormData({ ...formData, description: e.target.value })}
error={errors.description}
disabled={loading}
/>
<Select
label="Trạng thái"
options={statusOptions}
value={formData.status}
onChange={(e) => setFormData({ ...formData, status: e.target.value })}
disabled={loading}
/>
<Input
label="Deadline"
type="datetime-local"
value={formData.deadline}
onChange={(e) => setFormData({ ...formData, deadline: e.target.value })}
disabled={loading}
/>
<div className="flex justify-end space-x-3 pt-4">
<Button
type="button"
variant="secondary"
onClick={onCancel}
disabled={loading}
>
Hủy
</Button>
<Button type="submit" loading={loading}>
{task ? 'Cập nhật' : 'Tạo mới'}
</Button>
</div>
</form>
)
}