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
73 changes: 45 additions & 28 deletions app/(course)/courses/[slug]/introduction/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { StageHeader } from "@/components/stage/stage-header";
import { StageTabs } from "@/components/stage/stage-tabs";
import { getIntroductionStatus, StageStatus } from "@/types/stage-status";

import { useCourseContext } from "@/app/(course)/layout";
import { NotFound } from "@/components/common/not-found";
import {
Callbacks,
CourseAssessment,
Expand All @@ -21,88 +21,105 @@ import {
useCreateUserCourse,
useUpdateUserCourse,
} from "@/hooks/use-user-course";
import { useCourseStore, useIsNewUserCourse } from "@/stores/course-store";
import { useRouter } from "next/navigation";
import { useMemo, useState } from "react";

export default function CourseIntroductionPage() {
const router = useRouter();

const { session } = useSession();

const {
course,
userCourse: contextUserCourse,
isNew,
updateUserCourse: updateContextUserCourse,
attempts,
} = useCourseContext();
const [userCourse, setUserCourse] = useState(contextUserCourse);
const status = useMemo(() => getIntroductionStatus(userCourse), [userCourse]);
const { course, userCourse, attempts, setUserCourse } = useCourseStore();
const isNew = useIsNewUserCourse();

const [localUserCourse, setLocalUserCourse] = useState(
userCourse || {
course_slug: course?.slug || "",
proficiency: null,
cadence: null,
accountability: null,
started_at: new Date().toISOString(),
completed_stage_count: 0,
activated: false,
repository: "",
},
);

const status = useMemo(
() => getIntroductionStatus(localUserCourse),
[localUserCourse],
);

const callbacks: Callbacks = {
onProficiencyChange: (proficiency) => {
setUserCourse((prev) => ({ ...prev, proficiency }));
setLocalUserCourse((prev) => ({ ...prev, proficiency }));
},
onCadenceChange: (cadence) => {
setUserCourse((prev) => ({ ...prev, cadence }));
setLocalUserCourse((prev) => ({ ...prev, cadence }));
},
onAccountabilityChange: (accountability) => {
setUserCourse((prev) => ({ ...prev, accountability }));
setLocalUserCourse((prev) => ({ ...prev, accountability }));
},
};

const navigateToNextStage = (currentStageSlug?: string) => {
if (!course) return;
const targetPath = currentStageSlug
? `/courses/${course.slug}/stages/${currentStageSlug}`
: `/courses/${course.slug}/setup`;
router.push(targetPath);
};

const hasChanges =
userCourse.proficiency !== contextUserCourse.proficiency ||
userCourse.cadence !== contextUserCourse.cadence ||
userCourse.accountability !== contextUserCourse.accountability;
localUserCourse.proficiency !== userCourse?.proficiency ||
localUserCourse.cadence !== userCourse?.cadence ||
localUserCourse.accountability !== userCourse?.accountability;

const { mutate: createUserCourse } = useCreateUserCourse({
onSuccess: (data) => {
console.log("User course created successfully:", data);
updateContextUserCourse(data);
setUserCourse(data);
navigateToNextStage(data.current_stage_slug);
},
onError: (error) => {
console.error("Failed to create user course:", error);
},
});

const { mutate: updateUserCourse } = useUpdateUserCourse(course.slug, {
const { mutate: updateUserCourse } = useUpdateUserCourse(course?.slug || "", {
onSuccess: () => {
console.log("User course updated successfully");
navigateToNextStage(userCourse.current_stage_slug);
setUserCourse(localUserCourse);
navigateToNextStage(localUserCourse.current_stage_slug);
},
onError: (error) => {
console.error("Failed to update user course:", error);
},
});

const handleContinue = () => {
if (!course) return;

if (isNew) {
createUserCourse({
course_slug: course.slug,
proficiency: userCourse.proficiency || "beginner",
cadence: userCourse.cadence || "once_week",
accountability: userCourse.accountability || false,
proficiency: localUserCourse.proficiency || "beginner",
cadence: localUserCourse.cadence || "once_week",
accountability: localUserCourse.accountability || false,
});
} else if (hasChanges) {
updateUserCourse({
proficiency: userCourse.proficiency || "beginner",
cadence: userCourse.cadence || "once_week",
accountability: userCourse.accountability || false,
proficiency: localUserCourse.proficiency || "beginner",
cadence: localUserCourse.cadence || "once_week",
accountability: localUserCourse.accountability || false,
});
} else {
navigateToNextStage(userCourse.current_stage_slug);
navigateToNextStage(localUserCourse.current_stage_slug);
}
};

if (!course) return <NotFound message="Course not found." />;

return (
<>
<StageHeader
Expand Down Expand Up @@ -132,7 +149,7 @@ export default function CourseIntroductionPage() {

<CourseAssessment
status={status}
userCourse={userCourse}
userCourse={localUserCourse}
callbacks={callbacks}
/>

Expand Down
11 changes: 7 additions & 4 deletions app/(course)/courses/[slug]/setup/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,24 +8,25 @@ import { GenericCard } from "@/components/stage/generic-card";
import { StageHeader } from "@/components/stage/stage-header";
import { StageTabs } from "@/components/stage/stage-tabs";

import { useCourseContext } from "@/app/(course)/layout";
import { NotFound } from "@/components/common/not-found";
import { Attempts } from "@/components/course/course-attempts";
import { useSession } from "@/components/provider/auth-provider";
import Overlay from "@/components/stage/overlay";
import { StageCompleted } from "@/components/stage/stage-completed";
import { useUserCourseStatus } from "@/hooks/use-user-course-status";
import { useCourseStore } from "@/stores/course-store";
import { getSetupStatus, StageStatus } from "@/types/stage-status";
import Link from "next/link";
import { useMemo } from "react";

export default function CourseSetupPage() {
const { course, userCourse, attempts } = useCourseContext();
const { course, userCourse, attempts } = useCourseStore();
const { session } = useSession();

const projectName = `stackclass-${course.slug}`;
const projectName = `stackclass-${course?.slug}`;

const { status: userCourseStatus } = useUserCourseStatus(
course.slug,
course?.slug || "",
userCourse,
);

Expand All @@ -35,6 +36,8 @@ export default function CourseSetupPage() {
[userCourseStatus],
);

if (!course || !userCourse) return <NotFound message="Course not found." />;

return (
<>
<StageHeader
Expand Down
7 changes: 4 additions & 3 deletions app/(course)/courses/[slug]/stages/[stage_slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"use client";

import { useMemo } from "react";
import { useParams } from "next/navigation";

import { ErrorMessage } from "@/components/common/error-message";
Expand All @@ -9,7 +10,6 @@ import { InstructionCard } from "@/components/stage/instruction-card";
import { StageHeader } from "@/components/stage/stage-header";
import { StageTabs } from "@/components/stage/stage-tabs";

import { useCourseContext } from "@/app/(course)/layout";
import { Attempts } from "@/components/course/course-attempts";
import { useSession } from "@/components/provider/auth-provider";
import { GenericCard } from "@/components/stage/generic-card";
Expand All @@ -19,9 +19,10 @@ import TestRunner from "@/components/stage/test-runner";
import { useStage } from "@/hooks/use-stage";
import { useUserStage } from "@/hooks/use-user-stage";
import { useUserStageStatus } from "@/hooks/use-user-stage-status";
import { useCourseStore } from "@/stores/course-store";
import { UserStage } from "@/types/stage";
import { getStageStatus, StageStatus } from "@/types/stage-status";
import { useMemo } from "react";

import ReactMarkdown from "react-markdown";

export default function StagePage() {
Expand All @@ -30,7 +31,7 @@ export default function StagePage() {
stage_slug: string;
}>();

const { attempts } = useCourseContext();
const { attempts } = useCourseStore();
const { session } = useSession();

const {
Expand Down
159 changes: 3 additions & 156 deletions app/(course)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,158 +1,5 @@
"use client";
import CourseLayout from "@/components/layout/course-layout";

import { redirect, useParams } from "next/navigation";
import { createContext, useContext, useEffect, useMemo, useState } from "react";

import CourseHeader from "@/components/course/course-header";
import { CourseSidebar } from "@/components/course/course-sidebar";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { toast } from "sonner";

import { ErrorMessage } from "@/components/common/error-message";
import { Loading } from "@/components/common/loading";
import { NotFound } from "@/components/common/not-found";
import { useSession } from "@/components/provider/auth-provider";
import { useAttempts, useGetCourse } from "@/hooks/use-course";
import { useStages } from "@/hooks/use-stage";
import { useUserCourse } from "@/hooks/use-user-course";
import { useUserStages } from "@/hooks/use-user-stage";
import type { CourseDetail, UserCourse } from "@/types/course";
import type { StageWithState } from "@/types/stage";
import { Attempt } from "@/types/attempt";

interface CourseContextValue {
course: CourseDetail;
userCourse: UserCourse;
stages: StageWithState[];
isNew: boolean;
updateUserCourse: (newUserCourse: UserCourse) => void;
attempts: Attempt[];
}

const CourseContext = createContext<CourseContextValue | null>(null);

export function useCourseContext() {
const context = useContext(CourseContext);
if (!context) {
throw new Error("useCourse must be used within a CourseLayout");
}
return context;
}

export default function CourseLayout({
children,
}: {
children: React.ReactNode;
}) {
const { slug } = useParams<{ slug: string }>();

// Checking if the session is valid. If it's not,
// we are redirecting to the course overview page.
const { session, isLoading: sessionLoading } = useSession();
if (!sessionLoading && !session) {
toast.error("Please sign in to access this course.");
redirect(`/courses/${slug}/overview`);
}

// Fetch course details
const {
data: course,
isLoading: courseLoading,
error: courseError,
} = useGetCourse(slug);

// Fetch user course details
const { data: rawUserCourse, isLoading: userCourseLoading } = useUserCourse(
slug,
{ retry: false },
);

// Fetch all stages
const {
data: stages,
isLoading: stagesLoading,
error: stagesError,
} = useStages(slug);

// Fetching all user stages for a course
const { data: userStages } = useUserStages(slug, { retry: false });

const [userCourseData, setUserCourseData] = useState<UserCourse | null>(null);

useEffect(() => {
if (rawUserCourse) {
setUserCourseData(rawUserCourse);
}
}, [rawUserCourse]);

const updateUserCourse = (newUserCourse: UserCourse) => {
setUserCourseData(newUserCourse);
};

const { userCourse, isNew } = useMemo(() => {
if (userCourseData) return { userCourse: userCourseData, isNew: false };

return {
userCourse: {
course_slug: slug,
proficiency: null,
cadence: null,
accountability: null,
started_at: new Date().toISOString(),
completed_stage_count: 0,
activated: false,
repository: "",
},
isNew: true,
};
}, [userCourseData, slug]);

const stagesWithState = useMemo(() => {
if (!stages) return [];

return stages.map((stage) => {
const userStage =
userStages?.find((us) => us.stage_slug === stage.slug) || null;
return { stage, userStage };
});
}, [stages, userStages]);

const { data: attempts = [], isLoading: attemptsLoading } = useAttempts(slug);

const isLoading =
sessionLoading ||
courseLoading ||
stagesLoading ||
userCourseLoading ||
attemptsLoading;

if (isLoading) return <Loading message="Loading course details..." />;

if (courseError)
return <ErrorMessage message="Failed to load course details." />;
if (stagesError) return <ErrorMessage message="Failed to load stages." />;

if (!course) return <NotFound message="Course not found." />;
if (!stages) return <NotFound message="Stages not found." />;

const contextValue: CourseContextValue = {
course,
userCourse,
stages: stagesWithState,
isNew,
updateUserCourse,
attempts,
};

return (
<CourseContext.Provider value={contextValue}>
<SidebarProvider>
<CourseSidebar />
<SidebarInset>
<CourseHeader />
{children}
</SidebarInset>
</SidebarProvider>
</CourseContext.Provider>
);
export default function Layout({ children }: { children: React.ReactNode }) {
return <CourseLayout>{children}</CourseLayout>;
}
4 changes: 2 additions & 2 deletions app/(main)/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import Footer from "@/components/layout/footer";
import Header from "@/components/layout/header";
import Footer from "@/components/common/footer";
import Header from "@/components/common/header";

export default function MainLayout({
children,
Expand Down
File renamed without changes.
File renamed without changes.
Loading