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
33 changes: 33 additions & 0 deletions codewit/client/src/components/auth/LoginRequiredPrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Link } from "react-router-dom";

import { CenterPrompt } from "../placeholders";

interface LoginRequiredPromptProps {
message?: string,
}

export default function LoginRequiredPrompt({
message = "To visit this page, you need to be logged in.",
}: LoginRequiredPromptProps) {
return (
<CenterPrompt header="Log in to proceed">
<p className="mt-2 text-center text-white">
{message}
</p>
<div className="mt-6 flex w-full flex-col items-center justify-center gap-3 sm:flex-row">
<Link
to="/"
className="w-full rounded-md bg-zinc-700 px-4 py-2 text-center text-white transition hover:bg-zinc-600 sm:w-auto"
>
Return Home
</Link>
<a
href="/api/oauth2/google"
className="w-full rounded-md bg-accent-500 px-4 py-2 text-center text-white transition hover:bg-accent-600 sm:w-auto"
>
Log In
</a>
</div>
</CenterPrompt>
);
}
11 changes: 7 additions & 4 deletions codewit/client/src/components/guards/DashboardGate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,18 @@ import { Navigate, useParams } from 'react-router-dom';
import LoadingPage from '../loading/LoadingPage';
import { useAuth } from '../../hooks/useAuth';
import { useCourseRole } from '../../hooks/useCourseRole';
import LoginRequiredPrompt from '../auth/LoginRequiredPrompt';

export default function DashboardGate({ children }: { children: JSX.Element }) {
const { user, loading: authLoading } = useAuth();
const { courseId } = useParams();
const { role, loading: roleLoading } = useCourseRole(courseId);
const { role, loading: roleLoading } = useCourseRole(user ? courseId : undefined);

if (authLoading || roleLoading) return <LoadingPage />;
if (authLoading) return <LoadingPage />;
if (!user) return <LoginRequiredPrompt />;
if (roleLoading) return <LoadingPage />;

const allowed = user && (user.isAdmin || role === 'instructor');
const allowed = user.isAdmin || role === 'instructor';

return allowed ? children : (
<Navigate
Expand All @@ -21,4 +24,4 @@ export default function DashboardGate({ children }: { children: JSX.Element }) {
}}
/>
);
}
}
29 changes: 20 additions & 9 deletions codewit/client/src/hooks/fetching.ts
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should consider moving more over to tanstack/query as this is doing the same thing.

Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { useState, useEffect } from 'react';
import axios, { AxiosError } from 'axios';
import axios from 'axios';

interface AxiosFetch<T> {
data: T,
Expand All @@ -14,21 +14,21 @@ interface AxiosFetch<T> {
// this will assume that the first mount is the initial loading of the data
// and will indicate that it is loading before sending the actual request to
// the server.
export function useAxiosFetch<T>(initialUrl: string, initialData: T): AxiosFetch<T> {
export function useAxiosFetch<T>(initialUrl: string | null, initialData: T): AxiosFetch<T> {
const [data, setData] = useState(initialData);
const [active_requests, setActiveRequests] = useState(0);
const [error, setError] = useState(false);
const [initial_loading, setInitialLoading] = useState(true);
const [initial_loading, setInitialLoading] = useState(initialUrl != null);

const fetchData = async (controller: AbortController) => {
const fetchData = async (url: string, controller: AbortController) => {
setActiveRequests(v => (v + 1));

let canceled = false;

setError(false);

try {
const response = await axios.get(initialUrl, {
const response = await axios.get(url, {
signal: controller.signal
});

Expand All @@ -49,14 +49,25 @@ export function useAxiosFetch<T>(initialUrl: string, initialData: T): AxiosFetch
};

const refetch = () => {
if (initialUrl == null) {
return;
}

const controller = new AbortController();
fetchData(controller);
fetchData(initialUrl, controller);
};

useEffect(() => {
let controller = new AbortController();
if (initialUrl == null) {
setError(false);
setInitialLoading(false);
return;
}

fetchData(controller);
const controller = new AbortController();
setInitialLoading(true);

fetchData(initialUrl, controller);

return () => {
controller.abort();
Expand All @@ -75,4 +86,4 @@ export function useAxiosFetch<T>(initialUrl: string, initialData: T): AxiosFetch
}, [initialUrl]);

return { data, setData, loading: initial_loading || active_requests > 0, error, refetch };
};
}
11 changes: 8 additions & 3 deletions codewit/client/src/hooks/useCourseRole.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,18 @@ export type CourseRole = 'instructor' | 'student' | null;

export function useCourseRole(courseId: string | number | undefined) {
const [role, setRole] = useState<CourseRole>(null);
const [loading, setLoading] = useState(false);
const [loading, setLoading] = useState(Boolean(courseId));
const [error, setError] = useState<unknown>(null);

useEffect(() => {
if (!courseId) return;
if (!courseId) {
setRole(null);
setLoading(false);
return;
}

setLoading(true);
setError(null);
axios
.get<{ role: CourseRole }>(`/api/courses/${courseId}/role`)
.then(res => setRole(res.data.role))
Expand All @@ -23,4 +28,4 @@ export function useCourseRole(courseId: string | number | undefined) {
}, [courseId]);

return { role, loading, error };
}
}
31 changes: 17 additions & 14 deletions codewit/client/src/pages/CourseView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@ import { StudentCourse as StuCourse} from '@codewit/interfaces';
import { ErrorPage } from "../components/error/Error";
import Loading from "../components/loading/LoadingPage";
import { useAxiosFetch } from "../hooks/fetching";
import { useAuth } from "../hooks/useAuth";

import StudentView from "./course/StudentView";
import { CenterPrompt } from "../components/placeholders";
import LoginRequiredPrompt from "../components/auth/LoginRequiredPrompt";

interface StudentCourse extends StuCourse {
type: "StudentView",
Expand Down Expand Up @@ -59,24 +61,32 @@ interface CourseView {
export default function CourseView({onCourseChange}: CourseView) {
const { course_id } = useParams();
const navigate = useNavigate();
const { user, loading: authLoading } = useAuth();

if (course_id == null) {
throw new Error("course_id not provided");
}

const [refresh, set_refresh] = useState(0);
const { data: course, loading, error, setData } = useAxiosFetch<GetCourse | null>(`/api/courses/${course_id}?student_view=1&r=${refresh}`, null);
const { data: course, loading, error, setData } = useAxiosFetch<GetCourse | null>(
user ? `/api/courses/${course_id}?student_view=1&r=${refresh}` : null,
null,
);

useEffect(() => {
if (course?.type === "StudentView") {
onCourseChange(course.title);
}
}, [course, onCourseChange]);

if (loading) {
if (authLoading || loading) {
return <Loading />;
}

if (!user) {
return <LoginRequiredPrompt />;
}

if (error || course == null) {
return <ErrorPage message="Failed to fetch courses. Please try again later."/>;
}
Expand Down Expand Up @@ -127,14 +137,7 @@ export default function CourseView({onCourseChange}: CourseView) {
/>;
}
default:
return <CenterPrompt header={"Unknown Response"}>
<p className="text-center text-white">
The client does not know how to handle the response from the server. Sorry, try going back to the home page.
</p>
<Link to="/" className="text-white bg-accent-500 rounded-md mt-4 p-2">
Home page
</Link>
</CenterPrompt>;
return <ErrorPage message="Failed to load course information. Please try again later." />;
}
}

Expand All @@ -151,9 +154,9 @@ interface RegError {
}

function EnrollingView({course_id, course_title, auto_enroll, on_update}: EnrollingViewProps) {
let [sending, set_sending] = useState(false);
let [reg_state, set_reg_state] = useState<RegistrationResult | null>(null);
let [error, set_error] = useState<RegError | null>(null);
const [sending, set_sending] = useState(false);
const [reg_state, set_reg_state] = useState<RegistrationResult | null>(null);
const [error, set_error] = useState<RegError | null>(null);

async function request_enrollment() {
if (sending) {
Expand All @@ -163,7 +166,7 @@ function EnrollingView({course_id, course_title, auto_enroll, on_update}: Enroll
set_sending(true);

try {
let result = await axios.post<RegistrationResult>(`/api/courses/${course_id}/register`);
const result = await axios.post<RegistrationResult>(`/api/courses/${course_id}/register`);

set_reg_state(result.data);
} catch (err) {
Expand Down
Loading