Skip to content

Commit 4c8d10c

Browse files
committed
merge
2 parents ace279a + c40ecd6 commit 4c8d10c

21 files changed

Lines changed: 2000 additions & 111 deletions

client/src/components/task_item.tsx

Lines changed: 180 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
import { useState } from "react";
2+
3+
import { TimeInput } from "@/components/time_input";
4+
import { TopicInput } from "@/components/topic_input";
5+
16
interface Time {
27
id: number;
38
day: number;
@@ -24,58 +29,200 @@ interface Item {
2429
type ItemProps = {
2530
item: Item;
2631
onToggle?: (id: number) => void;
32+
onUpdate: (item: Item) => void;
33+
availableTopics: Topic[];
2734
};
2835

29-
export function TaskItem({ item, onToggle }: ItemProps) {
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+
}
30122
return (
31-
<label className="m-1 flex w-96 flex-col rounded-md border-2 border-zinc-200 bg-zinc-700 p-2">
123+
<div className="m-1 flex w-96 flex-col rounded-md border-2 border-zinc-200 bg-zinc-700 p-2">
32124
<div className="flex items-center justify-between">
125+
{/* Task Name and Completion */}
33126
<div className="flex items-center space-x-2">
34127
<input
35128
type="checkbox"
36129
checked={item.completed}
37130
onChange={() => onToggle?.(item.id)}
38131
className="w-xl h-xl accent-zinc-600"
39132
/>
40-
<span
41-
className={
42-
item.completed ? "text-zinc-400 line-through" : "text-zinc-300"
43-
}
44-
>
45-
{item.name}
46-
</span>
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+
)}
47148
</div>
149+
150+
{/* Task Times */}
48151
<div className="flex flex-col items-end text-sm text-zinc-300">
49-
{item.times?.length > 0
50-
? item.times.map((t) => (
51-
<span key={t.id}>
52-
{formatDay(t.day)} {t.start_time.slice(0, 5)} -{" "}
53-
{t.end_time.slice(0, 5)}
54-
</span>
55-
))
56-
: "No time set"}
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+
)}
57164
</div>
58165
</div>
59166

167+
{/* Task Topics */}
60168
<div className="flex flex-wrap gap-1 text-sm text-zinc-300">
61-
{item.topics?.length > 0
62-
? item.topics.map((topic) => (
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+
>
63181
<span
64-
key={topic.id}
65-
className="flex items-center gap-1 rounded-lg border-2 border-zinc-500 px-2 py-0.5 text-zinc-100"
66-
>
67-
<span
68-
className="h-2 w-2 rounded-full"
69-
style={{
70-
backgroundColor: `#${topic.color_hex.toString(16).padStart(6, "0")}`,
71-
}}
72-
/>
73-
{topic.name}
74-
</span>
75-
))
76-
: "No topics"}
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+
)}
77224
</div>
78-
</label>
225+
</div>
79226
);
80227
}
81228

client/src/components/task_list.tsx

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,9 +26,16 @@ interface Item {
2626
type ListProps = {
2727
items: Item[];
2828
onToggleTask?: (id: number) => void;
29+
onUpdate: (item: Item) => void;
30+
availableTopics: Topic[];
2931
};
3032

31-
export function TaskList({ items, onToggleTask }: ListProps) {
33+
export function TaskList({
34+
items,
35+
onToggleTask,
36+
onUpdate,
37+
availableTopics,
38+
}: ListProps) {
3239
if (items.length === 0) {
3340
return (
3441
<div>
@@ -41,7 +48,12 @@ export function TaskList({ items, onToggleTask }: ListProps) {
4148
<ul className="space-y-2">
4249
{items.map((item) => (
4350
<li key={item.id}>
44-
<TaskItem item={item} onToggle={onToggleTask} />
51+
<TaskItem
52+
item={item}
53+
onToggle={onToggleTask}
54+
onUpdate={onUpdate}
55+
availableTopics={availableTopics}
56+
/>
4557
</li>
4658
))}
4759
</ul>
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import {
2+
getTimeTopPosition,
3+
getVisibleTimetableRect,
4+
} from "@/components/timetable";
5+
6+
/*
7+
Resizes the time indicator to the correct width and positions it at the
8+
y-position corresponding to the current time.
9+
*/
10+
export function resizeAndPositionTimeIndicator() {
11+
const time_indicator = document.getElementById("time-indicator");
12+
if (time_indicator == null) return;
13+
14+
const now_label = document.getElementById("time-indicator-label");
15+
if (now_label == null) return;
16+
17+
const separator_rect = document
18+
.getElementById("timetable-separator")
19+
?.getBoundingClientRect();
20+
if (separator_rect == undefined) return;
21+
22+
const content_rect = document
23+
.getElementById("timetable-content")
24+
?.getBoundingClientRect();
25+
if (content_rect == undefined) return;
26+
27+
const visible = getVisibleTimetableRect();
28+
if (visible == undefined) return;
29+
30+
const now = new Date(Date.now());
31+
const hour = now.getHours();
32+
const mins = now.getMinutes();
33+
34+
const time_top = getTimeTopPosition(hour, mins);
35+
if (time_top == undefined) return;
36+
37+
time_indicator.style.left =
38+
visible.left - separator_rect.width - content_rect.left + "px";
39+
time_indicator.style.top = time_top - content_rect.top + "px";
40+
time_indicator.style.width = visible.width + separator_rect.width + "px";
41+
42+
const now_label_rect = now_label.getBoundingClientRect();
43+
if (now_label_rect == undefined) return;
44+
45+
const now_label_left =
46+
visible.left - now_label_rect.width - separator_rect.width;
47+
const now_label_top = time_top - now_label_rect.height / 2;
48+
49+
const parent_rect = time_indicator.parentElement?.getBoundingClientRect();
50+
if (parent_rect == undefined) return;
51+
52+
now_label.style.left = now_label_left - parent_rect.left + "px";
53+
now_label.style.top = now_label_top - parent_rect.top + "px";
54+
}
55+
56+
/*
57+
Component consisting of a rounded label containing the text "Now" and a 2px
58+
thick line stretching across the visible area of the timetable.
59+
60+
There should only be one TimeIndicator per page.
61+
*/
62+
function TimeIndicator() {
63+
return (
64+
<>
65+
<div
66+
id="time-indicator-label"
67+
className="absolute z-[100] h-fit w-fit rounded-full bg-slate-400 p-1 pl-2 pr-2"
68+
>
69+
<p className="text-slate-100">Now</p>
70+
</div>
71+
<div
72+
id="time-indicator"
73+
className="absolute z-[100] min-h-[2px] bg-slate-200 opacity-75"
74+
></div>
75+
</>
76+
);
77+
}
78+
79+
export default TimeIndicator;

0 commit comments

Comments
 (0)