Skip to content

Commit 698221c

Browse files
authored
Merge pull request #9 from codersforcauses/issue-7-Allow_CRUD_Operations_on_Tasks
Issue 7 allow crud operations on tasks
2 parents 240cf38 + e6fc224 commit 698221c

12 files changed

Lines changed: 977 additions & 27 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import { useEffect,useState } from "react";
2+
3+
import { TimeInput } from "@/components/time_input";
4+
import { TopicInput } from "@/components/topic_input";
5+
6+
interface Time {
7+
id: number;
8+
day: number;
9+
start_time: string;
10+
end_time: string;
11+
repeating: boolean;
12+
}
13+
14+
interface Topic {
15+
id: number;
16+
name: string;
17+
color_hex: number;
18+
}
19+
20+
type TopicSelection =
21+
| { type: "existing"; id: number }
22+
| { type: "new"; name: string; color_hex: number };
23+
24+
interface Item {
25+
id: number;
26+
name: string;
27+
completed: boolean;
28+
description: string;
29+
times: Time[];
30+
topics: Topic[];
31+
}
32+
33+
interface TaskFormProps {
34+
userId: number;
35+
onTaskCreated: (task: Item) => void;
36+
}
37+
38+
export function TaskForm({ userId, onTaskCreated }: TaskFormProps) {
39+
const [taskName, setTaskName] = useState("");
40+
const [description, setDescription] = useState("");
41+
const [times, setTimes] = useState<Time[]>([]);
42+
const [topics, setTopics] = useState<TopicSelection[]>([]);
43+
const [availableTopics, setAvailableTopics] = useState<Topic[]>([]);
44+
45+
useEffect(() => {
46+
fetch("http://localhost:8000/api/planner/topic/")
47+
.then((res) => res.json())
48+
.then((data) => setAvailableTopics(data))
49+
.catch((err) => console.error("Failed to load topics:", err));
50+
}, []);
51+
52+
const handleSubmit = async (e: React.FormEvent) => {
53+
e.preventDefault();
54+
try {
55+
const existing_topic_ids = topics
56+
.filter((t) => t.type === "existing")
57+
.map((t) => t.id);
58+
59+
const new_topics = topics
60+
.filter((t) => t.type === "new")
61+
.map((t) => ({ name: t.name, color_hex: t.color_hex }));
62+
const response = await fetch("http://localhost:8000/api/planner/tasks/", {
63+
method: "POST",
64+
headers: {
65+
"Content-Type": "application/json",
66+
},
67+
body: JSON.stringify({
68+
name: taskName,
69+
description: description,
70+
completed: false,
71+
user_id: userId,
72+
times: times,
73+
existing_topic_ids: existing_topic_ids,
74+
new_topics: new_topics,
75+
}),
76+
});
77+
78+
if (!response.ok) {
79+
throw new Error("Failed to create task");
80+
}
81+
82+
const newTask = await response.json();
83+
onTaskCreated(newTask);
84+
setTaskName("");
85+
setDescription("");
86+
setTimes([]);
87+
setTopics([]);
88+
} catch (error) {
89+
console.error("Error creating task:", error);
90+
}
91+
};
92+
93+
return (
94+
<form
95+
onSubmit={handleSubmit}
96+
className="mx-auto h-[79vh] w-full max-w-md space-y-4 overflow-y-auto rounded-xl bg-zinc-700 p-4"
97+
>
98+
<input
99+
type="text"
100+
value={taskName}
101+
onChange={(e) => setTaskName(e.target.value)}
102+
placeholder="Task Name"
103+
className="w-full rounded border-2 bg-zinc-700 p-2 text-zinc-200"
104+
required
105+
/>
106+
<textarea
107+
value={description}
108+
onChange={(e) => setDescription(e.target.value)}
109+
placeholder="Description"
110+
className="h-32 w-full resize-none rounded border-2 bg-zinc-700 p-2 text-zinc-200"
111+
/>
112+
<TimeInput times={times} setTimes={setTimes} />
113+
<TopicInput
114+
availableTopics={availableTopics}
115+
topics={topics}
116+
setTopics={setTopics}
117+
/>
118+
119+
<button
120+
type="submit"
121+
className="rounded border-2 border-zinc-200 bg-blue-600 px-4 py-2 text-zinc-200 hover:bg-blue-700"
122+
>
123+
Add Task
124+
</button>
125+
</form>
126+
);
127+
}
Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,240 @@
1+
import { useState } from "react";
2+
3+
import { TimeInput } from "@/components/time_input";
4+
import { TopicInput } from "@/components/topic_input";
5+
6+
interface Time {
7+
id: number;
8+
day: number;
9+
start_time: string;
10+
end_time: string;
11+
repeating: boolean;
12+
}
13+
14+
interface Topic {
15+
id: number;
16+
name: string;
17+
color_hex: number;
18+
}
19+
20+
interface Item {
21+
id: number;
22+
name: string;
23+
completed: boolean;
24+
description: string;
25+
times: Time[];
26+
topics: Topic[];
27+
}
28+
29+
type ItemProps = {
30+
item: Item;
31+
onToggle?: (id: number) => void;
32+
onUpdate: (item: Item) => void;
33+
availableTopics: Topic[];
34+
};
35+
36+
type TopicSelection =
37+
| { type: "existing"; id: number }
38+
| { type: "new"; name: string; color_hex: number };
39+
40+
export function TaskItem({
41+
item,
42+
onToggle,
43+
onUpdate,
44+
availableTopics,
45+
}: ItemProps) {
46+
{
47+
/* Editing Functionality */
48+
}
49+
const [isEditing, setIsEditing] = useState(false);
50+
const [draftName, setDraftName] = useState(item.name);
51+
const [draftDescription, setDraftDescription] = useState(item.description);
52+
const [draftTopics, setDraftTopics] = useState<TopicSelection[]>([]);
53+
const [draftTimes, setDraftTimes] = useState<Time[]>([]);
54+
const [saving, setSaving] = useState(false);
55+
56+
function startEdit() {
57+
setDraftName(item.name);
58+
setDraftDescription(item.description);
59+
setDraftTopics(
60+
item.topics.map((t) => ({
61+
type: "existing",
62+
id: t.id,
63+
})),
64+
);
65+
setDraftTimes(item.times.map((t) => ({ ...t })));
66+
setIsEditing(true);
67+
}
68+
69+
function cancelEdit() {
70+
setDraftName(item.name);
71+
setDraftDescription(item.description);
72+
setDraftTopics(item.topics.map((t) => ({ type: "existing", id: t.id })));
73+
setDraftTimes(item.times.map((t) => ({ ...t })));
74+
setIsEditing(false);
75+
}
76+
77+
async function saveEdit() {
78+
setSaving(true);
79+
try {
80+
const existing_topic_ids = draftTopics
81+
.filter((t) => t.type === "existing")
82+
.map((t) => t.id);
83+
84+
const new_topics = draftTopics
85+
.filter((t) => t.type === "new")
86+
.map((t) => ({
87+
name: t.name,
88+
color_hex: t.color_hex,
89+
}));
90+
91+
const response = await fetch(
92+
`http://localhost:8000/api/planner/tasks/${item.id}/`,
93+
{
94+
method: "PUT",
95+
headers: { "Content-Type": "application/json" },
96+
body: JSON.stringify({
97+
name: draftName,
98+
description: draftDescription,
99+
completed: item.completed,
100+
existing_topic_ids: existing_topic_ids,
101+
new_topics: new_topics,
102+
times: draftTimes,
103+
}),
104+
},
105+
);
106+
if (!response.ok) {
107+
throw new Error("Failed to update task");
108+
}
109+
const updatedItem = await response.json();
110+
onUpdate(updatedItem);
111+
setIsEditing(false);
112+
} catch (error) {
113+
console.error("Error saving task:", error);
114+
} finally {
115+
setSaving(false);
116+
}
117+
}
118+
119+
{
120+
/* Item Display */
121+
}
122+
return (
123+
<div className="m-1 flex w-96 flex-col rounded-md border-2 border-zinc-200 bg-zinc-700 p-2">
124+
<div className="flex items-center justify-between">
125+
{/* Task Name and Completion */}
126+
<div className="flex items-center space-x-2">
127+
<input
128+
type="checkbox"
129+
checked={item.completed}
130+
onChange={() => onToggle?.(item.id)}
131+
className="w-xl h-xl accent-zinc-600"
132+
/>
133+
{isEditing ? (
134+
<input
135+
value={draftName}
136+
onChange={(e) => setDraftName(e.target.value)}
137+
className="rounded border-2 bg-zinc-600 p-1 text-zinc-200"
138+
/>
139+
) : (
140+
<span
141+
className={
142+
item.completed ? "text-zinc-400 line-through" : "text-zinc-300"
143+
}
144+
>
145+
{item.name}
146+
</span>
147+
)}
148+
</div>
149+
150+
{/* Task Times */}
151+
<div className="flex flex-col items-end text-sm text-zinc-300">
152+
{isEditing ? (
153+
<TimeInput times={draftTimes} setTimes={setDraftTimes} />
154+
) : item.times?.length > 0 ? (
155+
item.times.map((t) => (
156+
<span key={t.id}>
157+
{formatDay(t.day)} {t.start_time.slice(0, 5)} -{" "}
158+
{t.end_time.slice(0, 5)}
159+
</span>
160+
))
161+
) : (
162+
"No time set"
163+
)}
164+
</div>
165+
</div>
166+
167+
{/* Task Topics */}
168+
<div className="flex flex-wrap gap-1 text-sm text-zinc-300">
169+
{isEditing ? (
170+
<TopicInput
171+
availableTopics={availableTopics}
172+
topics={draftTopics}
173+
setTopics={setDraftTopics}
174+
/>
175+
) : item.topics.length > 0 ? (
176+
item.topics.map((topic) => (
177+
<span
178+
key={topic.id}
179+
className="flex items-center gap-1 rounded-lg border-2 border-zinc-500 px-2 py-0.5 text-zinc-100"
180+
>
181+
<span
182+
className="h-2 w-2 rounded-full"
183+
style={{
184+
backgroundColor: `#${topic.color_hex
185+
.toString(16)
186+
.padStart(6, "0")}`,
187+
}}
188+
/>
189+
{topic.name}
190+
</span>
191+
))
192+
) : (
193+
"No topics"
194+
)}
195+
</div>
196+
197+
{/* Task Description */}
198+
<div>
199+
{isEditing ? (
200+
<textarea
201+
value={draftDescription}
202+
onChange={(e) => setDraftDescription(e.target.value)}
203+
className="mt-2 w-full rounded border-2 bg-zinc-600 p-1 text-zinc-200"
204+
/>
205+
) : item.description ? (
206+
<span className="mt-2 block text-zinc-300">{item.description}</span>
207+
) : (
208+
<span className="mt-2 block italic text-zinc-500">
209+
No description.
210+
</span>
211+
)}
212+
</div>
213+
<div className="mt-2 flex gap-2">
214+
{isEditing ? (
215+
<>
216+
<button onClick={saveEdit} disabled={saving}>
217+
Save
218+
</button>
219+
<button onClick={cancelEdit}>Cancel</button>
220+
</>
221+
) : (
222+
<button onClick={startEdit}>Edit</button>
223+
)}
224+
</div>
225+
</div>
226+
);
227+
}
228+
229+
function formatDay(day: number): string {
230+
const days = [
231+
"Monday",
232+
"Tuesday",
233+
"Wednesday",
234+
"Thursday",
235+
"Friday",
236+
"Saturday",
237+
"Sunday",
238+
];
239+
return days[day - 1] || "Unknown";
240+
}

0 commit comments

Comments
 (0)