Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 29 additions & 5 deletions client/src/components/task_form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,31 @@ export function TaskForm({ userId, onTaskCreated }: TaskFormProps) {
const [availableTopics, setAvailableTopics] = useState<Topic[]>([]);

useEffect(() => {
fetch(process.env.NEXT_PUBLIC_BACKEND_URL + "planner/topic/")
.then((res) => res.json())
.then((data) => setAvailableTopics(data))
.catch((err) => console.error("Failed to load topics:", err));
async function fetchTopics() {
try {
const token = localStorage.getItem("access");
if (!token) return;

const res = await fetch(
process.env.NEXT_PUBLIC_BACKEND_URL + "planner/topic/",
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);

if (!res.ok) {
throw new Error("Failed to load topics");
}

const data = await res.json();
setAvailableTopics(data);
} catch (err) {
console.error("Failed to load topics", err);
}
}
fetchTopics();
}, []);

const handleSubmit = async (e: React.FormEvent) => {
Expand All @@ -66,6 +87,9 @@ export function TaskForm({ userId, onTaskCreated }: TaskFormProps) {
const new_topics = topics
.filter((t) => t.type === "new")
.map((t) => ({ name: t.name, color_hex: t.color_hex }));

const validTimes = times.filter((t) => t.start_time && t.end_time);

const response = await fetch(
process.env.NEXT_PUBLIC_BACKEND_URL + "planner/tasks/",
{
Expand All @@ -79,7 +103,7 @@ export function TaskForm({ userId, onTaskCreated }: TaskFormProps) {
description: description,
completed: false,
user_id: userId,
times: times,
times: validTimes,
existing_topic_ids: existing_topic_ids,
new_topics: new_topics,
}),
Expand Down
4 changes: 3 additions & 1 deletion client/src/components/task_item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,8 @@ export function TaskItem({
color_hex: t.color_hex,
}));

const validTimes = draftTimes.filter((t) => t.start_time && t.end_time);

const response = await fetch(
process.env.NEXT_PUBLIC_BACKEND_URL + `planner/tasks/${item.id}/`,
{
Expand All @@ -111,7 +113,7 @@ export function TaskItem({
completed: item.completed,
existing_topic_ids: existing_topic_ids,
new_topics: new_topics,
times: draftTimes,
times: validTimes,
}),
},
);
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/task_list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export function TaskList({
);
}
return (
<ul className="task-list h-fit space-y-3 w-xl">
<ul className="task-list w-xl h-fit space-y-3">
{items.map((item) => (
<li key={item.id}>
<TaskItem
Expand Down
65 changes: 37 additions & 28 deletions client/src/components/time_input.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { FaRegCalendarAlt,FaRegClock } from "react-icons/fa";
import { FaRegCalendarAlt, FaRegClock } from "react-icons/fa";
import { FaRegTrashCan } from "react-icons/fa6";

interface Time {
Expand All @@ -25,6 +25,31 @@ const daysOfWeek = [
];

export function TimeInput({ times, setTimes }: TimeInputProps) {
const addTime = () => {
// Add a new empty draft time only if there isn't already an empty one
const hasEmpty = times.some((t) => !t.start_time || !t.end_time);
if (!hasEmpty) {
setTimes([
...times,
{ id: 0, day: 1, start_time: "", end_time: "", repeating: false },
]);
}
};

const updateTime = (
index: number,
field: keyof Time,
value: string | number | boolean,
) => {
const newTimes = [...times];
newTimes[index] = { ...newTimes[index], [field]: value };
setTimes(newTimes);
};

const removeTime = (index: number) => {
setTimes(times.filter((_, i) => i !== index));
};

return (
<div className="text-md flex w-full flex-col items-center justify-center gap-2">
<h1 className="time-input-title text-xl hover:brightness-110">Times</h1>
Expand All @@ -38,11 +63,9 @@ export function TimeInput({ times, setTimes }: TimeInputProps) {
<select
className="day-select h-full rounded-lg bg-indigo-400 p-1 text-center hover:brightness-110"
value={time.day}
onChange={(e) => {
const newTimes = [...times];
newTimes[index].day = parseInt(e.target.value);
setTimes(newTimes);
}}
onChange={(e) =>
updateTime(index, "day", parseInt(e.target.value))
}
>
{daysOfWeek.map((day) => (
<option key={day.value} value={day.value}>
Expand All @@ -58,21 +81,15 @@ export function TimeInput({ times, setTimes }: TimeInputProps) {
className="time-input h-full rounded-lg bg-indigo-400 hover:brightness-110"
type="time"
value={time.start_time}
onChange={(e) => {
const newTimes = [...times];
newTimes[index].start_time = e.target.value;
setTimes(newTimes);
}}
onChange={(e) =>
updateTime(index, "start_time", e.target.value)
}
/>
<input
className="time-input h-full rounded-lg bg-indigo-400 hover:brightness-110"
type="time"
value={time.end_time}
onChange={(e) => {
const newTimes = [...times];
newTimes[index].end_time = e.target.value;
setTimes(newTimes);
}}
onChange={(e) => updateTime(index, "end_time", e.target.value)}
/>
</div>
</div>
Expand All @@ -82,17 +99,14 @@ export function TimeInput({ times, setTimes }: TimeInputProps) {
className="repeat-input"
type="checkbox"
checked={time.repeating}
onChange={(e) => {
const newTimes = [...times];
newTimes[index].repeating = e.target.checked;
setTimes(newTimes);
}}
onChange={(e) => updateTime(index, "repeating", e.target.checked)}
/>
</div>

<button
className="time-delete"
type="button"
onClick={() => setTimes(times.filter((_, i) => i !== index))}
onClick={() => removeTime(index)}
>
<FaRegTrashCan className="hover:text-red-500" />
</button>
Expand All @@ -101,12 +115,7 @@ export function TimeInput({ times, setTimes }: TimeInputProps) {
<button
className="add-time-button w-12 rounded-full bg-indigo-400 text-xl hover:brightness-110"
type="button"
onClick={() =>
setTimes([
...times,
{ id: 0, day: 1, start_time: "", end_time: "", repeating: false },
])
}
onClick={addTime}
>
+
</button>
Expand Down
2 changes: 1 addition & 1 deletion client/src/components/timetable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -374,7 +374,7 @@ function Timetable({ timetable_tasks_props }: TimetableProps) {
>
<div
id="timetable-content"
className="h-full w-full overflow-auto overscroll-none scrollbar"
className="scrollbar h-full w-full overflow-auto overscroll-none"
onScroll={resizeAndPositionTimetableElements}
>
<div
Expand Down
4 changes: 2 additions & 2 deletions client/src/components/ui/focus/timer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ const Timer = (timeRemaining: number) => {
const formatTime = (time: number) => {
const seconds = Math.floor(time % 60);
const minutes = Math.floor((time / 60) % 60);
const hours = Math.floor((time / (60 * 60)));
const hours = Math.floor(time / (60 * 60));

return (
<div className="countdown-display">
Expand All @@ -24,7 +24,7 @@ const Timer = (timeRemaining: number) => {
};

return (
<div className="rounded-full bg-indigo-400 p-3 px-8 font-inter text-xl font-semibold w-full">
<div className="w-full rounded-full bg-indigo-400 p-3 px-8 font-inter text-xl font-semibold">
{formatTime(timeRemaining)}
</div>
);
Expand Down
30 changes: 26 additions & 4 deletions client/src/pages/tasks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,32 @@ export default function TasksPage() {
}, [router]);

useEffect(() => {
fetch(process.env.NEXT_PUBLIC_BACKEND_URL + "planner/topic/")
.then((res) => res.json())
.then((data) => setAvailableTopics(data))
.catch((err) => console.error("Failed to load topics", err));
async function fetchTopics() {
try {
const token = localStorage.getItem("access");
if (!token) return;

const res = await fetch(
process.env.NEXT_PUBLIC_BACKEND_URL + "planner/topic/",
{
headers: {
Authorization: `Bearer ${token}`,
},
},
);

if (!res.ok) {
throw new Error("Failed to load topics");
}

const data = await res.json();
setAvailableTopics(data);
} catch (err) {
console.error("Failed to load topics", err);
}
}

fetchTopics();
}, []);

//if (loading) {
Expand Down
5 changes: 4 additions & 1 deletion server/task_planner/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@
})

class TopicList(APIView):
authentication_classes = [JWTAuthentication]
permission_classes = [IsAuthenticated]

def get(self, request):
topics = Topic.objects.all()
topics = Topic.objects.filter(user=request.user)
serializer = TopicReadSerializer(topics, many=True)
return Response(serializer.data)

Expand All @@ -79,7 +82,7 @@
def get_queryset(self):
return Task.objects.filter(user=self.request.user) # Will restrict tasks to a given user when authentication is added.
return Task.objects.all()

Check warning on line 85 in server/task_planner/views.py

View workflow job for this annotation

GitHub Actions / Run Flake8

blank line contains whitespace
def perform_create(self, serializer):
# Use explicit user_id when provided (no auth yet); otherwise require auth
user_id = self.request.data.get("user_id")
Expand All @@ -90,7 +93,7 @@
serializer.save(user=self.request.user)
return
raise ValidationError({"user_id": "Provide user_id or authenticate."})

Check warning on line 96 in server/task_planner/views.py

View workflow job for this annotation

GitHub Actions / Run Flake8

blank line contains whitespace
def create(self, request, *args, **kwargs):
write_serializer = TaskWriteSerializer(data=request.data)
write_serializer.is_valid(raise_exception=True)
Expand All @@ -105,7 +108,7 @@

read_serializer = TaskReadSerializer(task)
return Response(read_serializer.data, status=status.HTTP_201_CREATED)

Check warning on line 111 in server/task_planner/views.py

View workflow job for this annotation

GitHub Actions / Run Flake8

blank line contains whitespace
@action(detail=True, methods=['patch'])
def toggle_complete(self, request, pk=None):
task = self.get_object()
Expand All @@ -114,7 +117,7 @@
serializer.save()
read_serializer = TaskReadSerializer(task)
return Response(read_serializer.data)

Check warning on line 120 in server/task_planner/views.py

View workflow job for this annotation

GitHub Actions / Run Flake8

blank line contains whitespace
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object()
Expand All @@ -124,3 +127,3 @@
task = write_serializer.save()

read_serializer = TaskReadSerializer(task)
Expand Down
Loading