Fix BASE_URL config loading, add tasks/projects; robust .env path resolution

- Config: try ENV_FILE, .env, ../.env for loading; trim trailing slash from BaseURL
- Log BASE_URL at server startup for verification
- .env.example: document BASE_URL
- Tasks, projects, tags, migrations and related API/handlers

Made-with: Cursor
This commit is contained in:
Michilis
2026-03-09 18:57:51 +00:00
parent 75105b8b46
commit bd24545b7b
61 changed files with 6595 additions and 90 deletions

View File

@@ -0,0 +1,271 @@
import { useState, useEffect } from "react";
import { format } from "date-fns";
import type { Task, Project, Tag } from "../types";
import { api } from "../api";
import Modal from "./Modal";
interface TaskModalProps {
open: boolean;
onClose: () => void;
onSaved: () => void;
task?: Task | null;
projects: Project[];
tags: Tag[];
}
const STATUS_OPTIONS = ["todo", "in_progress", "done"] as const;
const PRIORITY_OPTIONS = ["low", "medium", "high", "critical"] as const;
export default function TaskModal({
open,
onClose,
onSaved,
task,
projects,
tags,
}: TaskModalProps) {
const isEdit = !!task;
const [title, setTitle] = useState("");
const [description, setDescription] = useState("");
const [status, setStatus] = useState<Task["status"]>("todo");
const [priority, setPriority] = useState<Task["priority"]>("medium");
const [dueDate, setDueDate] = useState("");
const [projectId, setProjectId] = useState("");
const [saving, setSaving] = useState(false);
const [error, setError] = useState("");
const [showDelete, setShowDelete] = useState(false);
useEffect(() => {
if (!open) return;
setError("");
setShowDelete(false);
if (task) {
setTitle(task.title);
setDescription(task.description || "");
setStatus(task.status);
setPriority(task.priority);
setDueDate(task.due_date ? format(new Date(task.due_date), "yyyy-MM-dd") : "");
setProjectId(task.project_id || "");
} else {
setTitle("");
setDescription("");
setStatus("todo");
setPriority("medium");
setDueDate("");
setProjectId("");
}
}, [open, task]);
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
if (!title.trim()) return;
setSaving(true);
setError("");
try {
if (isEdit) {
await api.updateTask(task!.id, {
title: title.trim(),
description: description || undefined,
status,
priority,
due_date: dueDate || undefined,
project_id: projectId || undefined,
});
} else {
await api.createTask({
title: title.trim(),
description: description || undefined,
status,
priority,
due_date: dueDate || undefined,
project_id: projectId || undefined,
});
}
onSaved();
onClose();
} catch (err: unknown) {
setError((err as { error?: string })?.error || "Failed to save task");
} finally {
setSaving(false);
}
};
const handleDelete = async () => {
if (!task) return;
setSaving(true);
try {
await api.deleteTask(task.id);
onSaved();
onClose();
} catch {
setError("Failed to delete task");
} finally {
setSaving(false);
}
};
return (
<Modal
open={open}
onClose={onClose}
title={isEdit ? "Edit Task" : "New Task"}
wide
>
<form onSubmit={handleSubmit} className="space-y-4">
{error && (
<div className="p-3 rounded-lg bg-red-50 dark:bg-red-900/20 text-red-600 dark:text-red-400 text-sm">
{error}
</div>
)}
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Title
</label>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="Task title"
required
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Description
</label>
<textarea
value={description}
onChange={(e) => setDescription(e.target.value)}
rows={3}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
placeholder="Markdown supported"
/>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Status
</label>
<select
value={status}
onChange={(e) => setStatus(e.target.value as Task["status"])}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
{STATUS_OPTIONS.map((s) => (
<option key={s} value={s}>
{s.replace("_", " ")}
</option>
))}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Priority
</label>
<select
value={priority}
onChange={(e) => setPriority(e.target.value as Task["priority"])}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
{PRIORITY_OPTIONS.map((p) => (
<option key={p} value={p}>
{p}
</option>
))}
</select>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Due Date
</label>
<input
type="date"
value={dueDate}
onChange={(e) => setDueDate(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Project
</label>
<select
value={projectId}
onChange={(e) => setProjectId(e.target.value)}
className="w-full px-3 py-2 rounded-lg border border-gray-300 dark:border-gray-600 bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100"
>
<option value="">None</option>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
</div>
<div className="flex justify-between pt-4">
<div>
{isEdit && (
<button
type="button"
onClick={() => setShowDelete(true)}
className="text-red-600 dark:text-red-400 hover:underline text-sm"
>
Delete task
</button>
)}
</div>
<div className="flex gap-2">
<button
type="button"
onClick={onClose}
className="px-4 py-2 rounded-lg border border-gray-300 dark:border-gray-600 text-gray-700 dark:text-gray-300 hover:bg-gray-50 dark:hover:bg-gray-700"
>
Cancel
</button>
<button
type="submit"
disabled={saving || !title.trim()}
className="px-4 py-2 rounded-lg bg-blue-600 text-white hover:bg-blue-700 disabled:opacity-50"
>
{saving ? "Saving..." : isEdit ? "Save" : "Create"}
</button>
</div>
</div>
{showDelete && (
<div className="mt-4 p-4 rounded-lg bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800">
<p className="text-red-700 dark:text-red-300 text-sm mb-2">
Delete this task? This cannot be undone.
</p>
<div className="flex gap-2">
<button
type="button"
onClick={() => setShowDelete(false)}
className="px-3 py-1 rounded bg-gray-200 dark:bg-gray-600 text-sm"
>
Cancel
</button>
<button
type="button"
onClick={handleDelete}
disabled={saving}
className="px-3 py-1 rounded bg-red-600 text-white text-sm hover:bg-red-700 disabled:opacity-50"
>
Delete
</button>
</div>
</div>
)}
</form>
</Modal>
);
}