Skip to content

Commit 076bd0c

Browse files
committed
completed task addition
1 parent eea531e commit 076bd0c

7 files changed

Lines changed: 154 additions & 22 deletions

File tree

client/src/components/task_form.tsx

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { useState } from "react";
1+
import { useEffect,useState } from "react";
22

33
import { TimeInput } from "@/components/time_input";
4+
import { TopicInput } from "@/components/topic_input";
45

56
interface Time {
67
id: number;
@@ -16,6 +17,10 @@ interface Topic {
1617
color_hex: number;
1718
}
1819

20+
type TopicSelection =
21+
| { type: "existing"; id: number }
22+
| { type: "new"; name: string; color_hex: number };
23+
1924
interface Item {
2025
id: number;
2126
name: string;
@@ -34,12 +39,26 @@ export function TaskForm({ userId, onTaskCreated }: TaskFormProps) {
3439
const [taskName, setTaskName] = useState("");
3540
const [description, setDescription] = useState("");
3641
const [times, setTimes] = useState<Time[]>([]);
37-
//const [existingTopicIds, setExistingTopicIds] = useState<number[]>([]);
38-
//const [newTopics, setNewTopics] = useState<{ name: string; color_hex: string }[]>([]);
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+
}, []);
3951

4052
const handleSubmit = async (e: React.FormEvent) => {
4153
e.preventDefault();
4254
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 }));
4362
const response = await fetch("http://localhost:8000/api/planner/tasks/", {
4463
method: "POST",
4564
headers: {
@@ -51,8 +70,8 @@ export function TaskForm({ userId, onTaskCreated }: TaskFormProps) {
5170
completed: false,
5271
user_id: userId,
5372
times: times,
54-
//existing_topic_ids: existingTopicIds,
55-
//new_topics: newTopics,
73+
existing_topic_ids: existing_topic_ids,
74+
new_topics: new_topics,
5675
}),
5776
});
5877

@@ -64,6 +83,8 @@ export function TaskForm({ userId, onTaskCreated }: TaskFormProps) {
6483
onTaskCreated(newTask);
6584
setTaskName("");
6685
setDescription("");
86+
setTimes([]);
87+
setTopics([]);
6788
} catch (error) {
6889
console.error("Error creating task:", error);
6990
}
@@ -89,6 +110,12 @@ export function TaskForm({ userId, onTaskCreated }: TaskFormProps) {
89110
className="h-32 w-full resize-none rounded border-2 bg-zinc-700 p-2 text-zinc-200"
90111
/>
91112
<TimeInput times={times} setTimes={setTimes} />
113+
<TopicInput
114+
availableTopics={availableTopics}
115+
topics={topics}
116+
setTopics={setTopics}
117+
/>
118+
92119
<button
93120
type="submit"
94121
className="rounded border-2 border-zinc-200 bg-blue-600 px-4 py-2 text-zinc-200 hover:bg-blue-700"

client/src/components/task_item.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ export function TaskItem({ item, onToggle }: ItemProps) {
4646
</span>
4747
</div>
4848
<div className="flex flex-col items-end text-sm text-zinc-300">
49-
{item.times.length > 0
49+
{item.times?.length > 0
5050
? item.times.map((t) => (
5151
<span key={t.id}>
5252
{formatDay(t.day)} {t.start_time.slice(0, 5)} -{" "}
@@ -58,7 +58,7 @@ export function TaskItem({ item, onToggle }: ItemProps) {
5858
</div>
5959

6060
<div className="flex flex-wrap gap-1 text-sm text-zinc-300">
61-
{item.topics.length > 0
61+
{item.topics?.length > 0
6262
? item.topics.map((topic) => (
6363
<span
6464
key={topic.id}

client/src/components/topic_input.tsx

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export function TopicInput({
2121
}: TopicInputProps) {
2222
return (
2323
<div className="mb-4">
24+
{/* Existing Topic Selection */}
2425
<select
2526
className="w-full rounded border-2 bg-zinc-700 p-2 text-zinc-200"
2627
onChange={(e) => {
@@ -39,6 +40,71 @@ export function TopicInput({
3940
</option>
4041
))}
4142
</select>
43+
44+
{/* Selected Topics Display */}
45+
{topics.map((topic, index) => {
46+
if (topic.type === "existing") {
47+
const topicData = availableTopics.find((t) => t.id === topic.id);
48+
if (!topicData) return null;
49+
return (
50+
<div
51+
key={index}
52+
className="mt-2 flex items-center justify-between rounded bg-zinc-600 p-2 text-zinc-200"
53+
>
54+
<span>{topicData.name}</span>
55+
<button
56+
type="button"
57+
onClick={() => setTopics(topics.filter((_, i) => i !== index))}
58+
className="text-red-500 hover:text-red-700"
59+
>
60+
Remove
61+
</button>
62+
</div>
63+
);
64+
}
65+
66+
return (
67+
<div key={index} className="mt-2 flex items-center gap-2">
68+
<input
69+
type="text"
70+
placeholder="New Topic Name"
71+
value={topic.name}
72+
onChange={(e) => {
73+
const newTopics = [...topics];
74+
newTopics[index] = { ...topic, name: e.target.value };
75+
setTopics(newTopics);
76+
}}
77+
/>
78+
<input
79+
type="color"
80+
value={`#${topic.color_hex.toString(16).padStart(6, "0")}`}
81+
onChange={(e) => {
82+
const hex = parseInt(e.target.value.replace("#", ""), 16);
83+
const copy = [...topics];
84+
copy[index] = { ...topic, color_hex: hex };
85+
setTopics(copy);
86+
}}
87+
/>
88+
<button
89+
type="button"
90+
onClick={() => setTopics(topics.filter((_, i) => i !== index))}
91+
className="text-red-500 hover:text-red-700"
92+
>
93+
Remove
94+
</button>
95+
</div>
96+
);
97+
})}
98+
99+
<button
100+
type="button"
101+
className="mt-3 rounded bg-green-700 px-2 py-1 text-zinc-200"
102+
onClick={() =>
103+
setTopics([...topics, { type: "new", name: "", color_hex: 0xffffff }])
104+
}
105+
>
106+
+ Create New Topic
107+
</button>
42108
</div>
43109
);
44110
}

client/src/pages/[id]/tasks.tsx

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,34 @@ export default function TasksPage() {
6161
setItems((prev) => [...prev, task]);
6262
};
6363

64-
function handleToggleTask(id: number) {
65-
setItems((prevItems) =>
66-
prevItems.map((item) =>
67-
item.id === id ? { ...item, completed: !item.completed } : item,
68-
),
69-
);
64+
async function handleToggleTask(id: number) {
65+
const task = items.find((t) => t.id === id);
66+
if (!task) return;
67+
68+
try {
69+
const response = await fetch(
70+
`http://localhost:8000/api/planner/tasks/${id}/`,
71+
{
72+
method: "PATCH",
73+
headers: {
74+
"Content-Type": "application/json",
75+
},
76+
body: JSON.stringify({ completed: !task.completed }),
77+
},
78+
);
79+
80+
if (!response.ok) {
81+
throw new Error("Failed to update task");
82+
}
83+
84+
const updatedTask = await response.json();
85+
86+
setItems((prevItems) =>
87+
prevItems.map((item) => (item.id === id ? updatedTask : item)),
88+
);
89+
} catch (error) {
90+
console.error("Error updating task:", error);
91+
}
7092
}
7193

7294
return (

server/task_planner/serializers.py

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -46,26 +46,25 @@ class Meta:
4646

4747
@transaction.atomic
4848
def create(self, validated_data):
49+
user_id = validated_data.pop("user_id", None)
4950
existing_topics = validated_data.pop("existing_topic_ids", [])
5051
new_topics_data = validated_data.pop("new_topics", [])
5152
times_data = validated_data.pop("times", [])
5253

53-
task = Task.objects.create(**validated_data)
54-
user = task.user
55-
56-
if not user:
57-
raise serializers.ValidationError("Task user is required to create topics.")
54+
task = Task.objects.create(**validated_data, user_id=user_id)
5855

5956
for topic in existing_topics:
6057
task.topics.add(topic)
6158

6259
for topic_data in new_topics_data:
63-
topic = Topic.objects.create(**topic_data, user=task.user)
60+
topic = Topic.objects.create(**topic_data, user_id=user_id)
6461
task.topics.add(topic)
6562

6663
Time.objects.bulk_create([
6764
Time(task=task, **time_data)
6865
for time_data in times_data
6966
])
7067

71-
return task
68+
task.save()
69+
task.refresh_from_db()
70+
return Task.objects.get(id=task.id)

server/task_planner/urls.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@
77
path("topic/", views.TopicList.as_view(), name="topic-list"),
88
path("time/", views.TimeList.as_view(), name="time-list"),
99
path("tasks/", views.TaskViewSet.as_view({'get': 'list', 'post': 'create'}), name="task-viewset"),
10-
path("tasks/<int:pk>/", views.TaskViewSet.as_view({'get': 'retrieve', 'put': 'update', 'delete': 'destroy'}), name="task-detail"),
10+
path("tasks/<int:pk>/", views.TaskViewSet.as_view({'get': 'retrieve', 'put': 'update', 'patch': 'partial_update', 'delete': 'destroy'}), name="task-detail"),
1111
]

server/task_planner/views.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from rest_framework.response import Response
66
from rest_framework.views import APIView
77
from rest_framework.viewsets import ModelViewSet
8+
from rest_framework import status
89
#from rest_framework.permissions import IsAuthenticated
910

1011
from .models import Task, Topic, Time
@@ -54,4 +55,21 @@ def perform_create(self, serializer):
5455
if self.request.user and self.request.user.is_authenticated:
5556
serializer.save(user=self.request.user)
5657
return
57-
raise ValidationError({"user_id": "Provide user_id or authenticate."})
58+
raise ValidationError({"user_id": "Provide user_id or authenticate."})
59+
60+
def create(self, request, *args, **kwargs):
61+
write_serializer = TaskWriteSerializer(data=request.data)
62+
write_serializer.is_valid(raise_exception=True)
63+
64+
# 👇 THIS is what perform_create used to do
65+
user_id = request.data.get("user_id")
66+
if user_id:
67+
task = write_serializer.save(user_id=user_id)
68+
elif request.user and request.user.is_authenticated:
69+
task = write_serializer.save(user=request.user)
70+
else:
71+
raise ValidationError({"user_id": "Provide user_id or authenticate."})
72+
73+
# Return READ shape
74+
read_serializer = TaskReadSerializer(task)
75+
return Response(read_serializer.data, status=status.HTTP_201_CREATED)

0 commit comments

Comments
 (0)