Refactor AdminLessonApi and AdminActivityApi.

This commit is contained in:
Hikmet 2025-01-28 12:05:22 +03:00
parent 2e6b9b6eb2
commit 01787f93a7
13 changed files with 164 additions and 210 deletions

View File

@ -0,0 +1,13 @@
import { useQuery } from "@tanstack/react-query";
import { ApiResponse } from "../api_response";
import { apiAdmin } from "@/lib/axios";
export const useAdminActivity = ({ themeId, lessonId, activityId }: { themeId: string; lessonId: string; activityId: string }) => {
return useQuery({
queryKey: ["admin-activity", themeId, lessonId, activityId],
queryFn: async () => {
const { data } = await apiAdmin.get(`/themes/${themeId}/lessons/${lessonId}/activities/${activityId}`);
return data as ApiResponse;
},
});
}

View File

@ -0,0 +1,13 @@
import { apiAdmin } from "@/lib/axios";
import { ApiResponse } from "../api_response";
import { useMutation } from "@tanstack/react-query";
import IActivity from "@/lib/activity/activity";
export const useAdminCreateActivity = () => {
return useMutation({
mutationFn: async ({ themeId, lessonId, activity }: { themeId: string; lessonId: string; activity: IActivity }) => {
const { data } = await apiAdmin.post(`/themes/${themeId}/lessons/${lessonId}/activities`, { activity });
return data as ApiResponse;
},
});
};

View File

@ -0,0 +1,12 @@
import { useMutation } from "@tanstack/react-query";
import { ApiResponse } from "../api_response";
import { apiAdmin } from "@/lib/axios";
export const useAdminDeleteActivity = () => {
return useMutation({
mutationFn: async ({ themeId, lessonId, activityId }: { themeId: string; lessonId: string; activityId: string }) => {
const { data } = await apiAdmin.delete(`/themes/${themeId}/lessons/${lessonId}/activities/${activityId}`);
return data as ApiResponse;
},
});
};

View File

@ -0,0 +1,13 @@
import { useMutation } from "@tanstack/react-query";
import { ApiResponse } from "../api_response";
import { apiAdmin } from "@/lib/axios";
import IActivity from "@/lib/activity/activity";
export const useAdminUpdateActivity = () => {
return useMutation({
mutationFn: async ({ themeId, lessonId, activity }: { themeId: string; lessonId: string; activity: IActivity }) => {
const { data } = await apiAdmin.put(`/themes/${themeId}/lessons/${lessonId}/activities/${activity.id}`, { activity });
return data as ApiResponse;
},
});
}

View File

@ -1,74 +0,0 @@
import IActivity from "../lib/activity/activity";
import { BackendActivityService } from "../backend/services/activity_service";
import { ApiResponse } from "./api_response";
export class AdminActivityApi {
async fetchActivity(
themeId: string,
lessonId: string,
activityId: string
): Promise<IActivity | undefined> {
const resObj = await fetch(
`/api/admin/themes/${themeId}/lessons/${lessonId}/activities/${activityId}`
);
const res = await (resObj.json() as ReturnType<
BackendActivityService["getActivity"]
>);
if (res.status === "success" && res.data) {
return res.data;
}
return undefined;
}
async saveActivity(themeId: string, lessonId: string, activity: IActivity) {
const resObj = await fetch(
`/api/admin/themes/${themeId}/lessons/${lessonId}/activities/${activity.id}`,
{
method: "PUT",
body: JSON.stringify({
activity,
}),
headers: {
"Content-Type": "application/json",
},
}
);
return await (resObj.json() as ReturnType<
BackendActivityService["saveActivity"]
>);
}
deleteActivity = async (
themeId: string,
lessonId: string,
activityId: string
) => {
const resObj = await fetch(
`/api/admin/themes/${themeId}/lessons/${lessonId}/activities/${activityId}`,
{
method: "DELETE",
}
);
return (await resObj.json()) as ApiResponse;
};
createActivity = async (
themeId: string,
lessonId: string,
activity: IActivity
) => {
const resObj = await fetch(
`/api/admin/themes/${themeId}/lessons/${lessonId}/activities`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
activity,
}),
}
);
return (await resObj.json()) as ApiResponse;
};
}

View File

@ -1,43 +0,0 @@
import ILesson from "@/lib/lesson/lesson";
import { ApiResponse } from "./api_response";
export class AdminLessonApi {
deleteLesson = async (themeId: string, lessonId: string) => {
const resObj = await fetch(
`/api/admin/themes/${themeId}/lessons/${lessonId}`,
{
method: "DELETE",
}
);
return (await resObj.json()) as ApiResponse;
};
createLesson = async (themeId: string, lesson: ILesson) => {
const resObj = await fetch(`/api/admin/themes/${themeId}/lessons`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
lesson,
}),
});
return (await resObj.json()) as ApiResponse;
};
saveLesson = async (themeId: string, lesson: Omit<ILesson, "activities">) => {
const resObj = await fetch(
`/api/admin/themes/${themeId}/lessons/${lesson.id}`,
{
method: "PUT",
body: JSON.stringify({
lesson,
}),
headers: {
"Content-Type": "application/json",
},
}
);
return (await resObj.json()) as ApiResponse;
};
}

View File

@ -0,0 +1,13 @@
import { apiAdmin } from "@/lib/axios";
import { ApiResponse } from "../api_response";
import { useMutation } from "@tanstack/react-query";
import ILesson from "@/lib/lesson/lesson";
export const useAdminCreateLesson = () => {
return useMutation({
mutationFn: async ({ themeId, lesson }: { themeId: string; lesson: ILesson }) => {
const { data } = await apiAdmin.post(`/themes/${themeId}/lessons`, { lesson });
return data as ApiResponse;
},
});
};

View File

@ -0,0 +1,12 @@
import { apiAdmin } from "@/lib/axios";
import { ApiResponse } from "../api_response";
import { useMutation } from "@tanstack/react-query";
export const useAdminDeleteLesson = () => {
return useMutation({
mutationFn: async ({ themeId, lessonId }: { themeId: string; lessonId: string }) => {
const { data } = await apiAdmin.delete(`/themes/${themeId}/lessons/${lessonId}`);
return data as ApiResponse;
},
});
};

View File

@ -0,0 +1,13 @@
import { apiAdmin } from "@/lib/axios";
import { ApiResponse } from "../api_response";
import { useMutation } from "@tanstack/react-query";
import ILesson from "@/lib/lesson/lesson";
export const useAdminUpdateLesson = () => {
return useMutation({
mutationFn: async ({ themeId, lesson }: { themeId: string; lesson: Omit<ILesson, "activities"> }) => {
const { data } = await apiAdmin.put(`/themes/${themeId}/lessons/${lesson.id}`, { lesson });
return data as ApiResponse;
},
});
};

View File

@ -1,15 +1,15 @@
import { useRef, useState } from "react";
import { useState } from "react";
import { IViewModel } from "../model/view_model";
import { AdminActivityApi } from "@/api/admin_activity_api";
import IActivity from "@/lib/activity/activity";
import IExercise from "@/lib/exercise/exercise";
import { useAdminUpdateActivity } from "@/api/activity/useAdminUpdateActivity";
export function useViewModel(
themeId: string,
lessonId: string,
activityData: IActivity
): IViewModel {
const adminService = useRef(new AdminActivityApi());
const { mutateAsync: adminUpdateActivity } = useAdminUpdateActivity();
const [type, setType] = useState<IViewModel["type"]>(activityData.type);
const [title, setTitle] = useState(activityData.title);
const [explanation, setExplanation] = useState(activityData.explanation);
@ -21,70 +21,74 @@ export function useViewModel(
>(
activityData.youtubeVideoUrl
? {
value: activityData.youtubeVideoUrl,
status: "success",
}
value: activityData.youtubeVideoUrl,
status: "success",
}
: {
value: "",
status: "idle",
}
value: "",
status: "idle",
}
);
const [image, setImage] = useState<IViewModel["image"]>(
activityData.image
? {
value: activityData.image,
status: "success",
}
value: activityData.image,
status: "success",
}
: {
value: "",
status: "idle",
}
value: "",
status: "idle",
}
);
const [audio, setAudio] = useState<IViewModel["audio"]>(
activityData.audio
? {
value: activityData.audio,
status: "success",
}
value: activityData.audio,
status: "success",
}
: {
value: "",
status: "idle",
}
value: "",
status: "idle",
}
);
const [exercise, setExercise] = useState<IExercise>(activityData.exercise);
const changeActivityType = (newActivityType: IViewModel["type"]) => {
const exerciseType: IExercise["type"] =
newActivityType === "drag-into-blanks" ||
newActivityType === "type-in-blanks"
newActivityType === "type-in-blanks"
? "fill-in-blanks-exercise"
: newActivityType === "pair-texts-with-images" ||
newActivityType === "true-false"
? "qa-exercise"
: "multiple-choice-exercise";
? "qa-exercise"
: "multiple-choice-exercise";
setType(newActivityType);
setExercise({ type: exerciseType, answers: [], template: [] });
};
const saveActivity = async () => {
try {
await adminService.current.saveActivity(themeId, lessonId, {
id: activityData.id,
title,
explanation,
textContent,
savedAt: Date.now(),
type,
audio: audio.status === "success" ? audio.value : activityData.audio,
exercise,
image: image.status === "success" ? image.value : activityData.image,
youtubeVideoUrl:
youtubeVideoUrl.status === "success"
? youtubeVideoUrl.value
: activityData.youtubeVideoUrl,
await adminUpdateActivity({
themeId,
lessonId,
activity: {
id: activityData.id,
title,
explanation,
textContent,
savedAt: Date.now(),
type,
audio: audio.status === "success" ? audio.value : activityData.audio,
exercise,
image: image.status === "success" ? image.value : activityData.image,
youtubeVideoUrl:
youtubeVideoUrl.status === "success"
? youtubeVideoUrl.value
: activityData.youtubeVideoUrl,
},
});
localStorage.removeItem(activityData.id);
} catch (error) {}
} catch (error) { }
};
return {

View File

@ -7,11 +7,14 @@ import { ApiResponse } from "@/api/api_response";
import { defaultActivity } from "@/lib/activity/default_activity";
import { slugifyLaz } from "@/utils/slugify_laz";
import ILesson from "@/lib/lesson/lesson";
import { AdminLessonApi } from "@/api/admin_lesson_api";
import { AdminActivityApi } from "@/api/admin_activity_api";
import { useAdminDeleteTheme } from "@/api/theme/useAdminDeleteTheme";
import { useAdminUpdateTheme } from "@/api/theme/useAdminUpdateTheme";
import { useAdminRelocateTheme } from "@/api/theme/useAdminRelocateTheme";
import { useAdminCreateLesson } from "@/api/lesson/useAdminCreateLesson";
import { useAdminUpdateLesson } from "@/api/lesson/useAdminUpdateLesson";
import { useAdminDeleteLesson } from "@/api/lesson/useAdminDeleteLesson";
import { useAdminCreateActivity } from "@/api/activity/useAdminCreateActivity";
import { useAdminDeleteActivity } from "@/api/activity/useAdminDeleteActivity";
export function useAdminViewModel(): IAdminViewModel {
const {
@ -32,8 +35,11 @@ export function useAdminViewModel(): IAdminViewModel {
const { mutateAsync: adminRelocateTheme } = useAdminRelocateTheme();
const { mutateAsync: adminUpdateTheme } = useAdminUpdateTheme();
const { mutateAsync: adminDeleteTheme } = useAdminDeleteTheme();
const adminLessonApi = useRef(new AdminLessonApi());
const adminActivityApi = useRef(new AdminActivityApi());
const { mutateAsync: adminDeleteLesson } = useAdminDeleteLesson();
const { mutateAsync: adminUpdateLesson } = useAdminUpdateLesson();
const { mutateAsync: adminCreateLesson } = useAdminCreateLesson();
const { mutateAsync: adminCreateActivity } = useAdminCreateActivity();
const { mutateAsync: adminDeleteActivity } = useAdminDeleteActivity();
const [stalling, setStalling] = useState(false);
const [snackbar, setSnackbar] = useState<{
severity: "error" | "success" | "warning" | "info";
@ -127,7 +133,7 @@ export function useAdminViewModel(): IAdminViewModel {
const saveLesson = async (lesson: Omit<ILesson, "activities">) => {
if (activeLesson === null) return;
await withFeedback(
() => adminLessonApi.current.saveLesson(id, lesson),
() => adminUpdateLesson({ themeId: id, lesson }),
() => {
setLessons((prev) => {
const newLessons = [...prev];
@ -141,7 +147,7 @@ export function useAdminViewModel(): IAdminViewModel {
const createLesson = async () => {
const lesson = defaultLesson();
await withFeedback(
() => adminLessonApi.current.createLesson(id, lesson),
() => adminCreateLesson({ themeId: id, lesson }),
(res) => {
setLessons((prev) => [...prev, lesson]);
if (activeLesson === null) {
@ -155,7 +161,7 @@ export function useAdminViewModel(): IAdminViewModel {
if (activeLesson === null) return;
const lessonId = lessons[activeLesson].id;
await withFeedback(
() => adminLessonApi.current.deleteLesson(id, lessonId),
() => adminDeleteLesson({ themeId: id, lessonId }),
(res) => {
const newLessons = lessons.filter((l) => l.id !== lessonId);
let newActiveLesson: number | null = activeLesson;
@ -177,7 +183,7 @@ export function useAdminViewModel(): IAdminViewModel {
const activity = defaultActivity();
const lessonId = lessons[activeLesson].id;
withFeedback(
() => adminActivityApi.current.createActivity(id, lessonId, activity),
() => adminCreateActivity({ themeId: id, lessonId, activity }),
() => {
setLessons((prev) => {
return prev.map((l) => {
@ -194,7 +200,7 @@ export function useAdminViewModel(): IAdminViewModel {
if (activeLesson === null) return;
const lessonId = lessons[activeLesson].id;
withFeedback(
() => adminActivityApi.current.deleteActivity(id, lessonId, activityId),
() => adminDeleteActivity({ themeId: id, lessonId, activityId }),
() => {
setLessons((prev) => {
const newLessons = [...prev] as any;

View File

@ -1,41 +1,21 @@
import { ActivityEditor } from "@/features/activity_editor";
import IActivity from "@/lib/activity/activity";
import { AdminActivityApi } from "@/api/admin_activity_api";
import { usePathname } from "next/navigation";
import { useEffect, useRef, useState } from "react";
import { useMemo } from "react";
import { useRouter } from "next/router";
import { useAdminActivity } from "@/api/activity/useAdminActivity";
export default function ActivityEditorPage() {
const pathname = usePathname();
const adminService = useRef(new AdminActivityApi());
const [pathnames, setPathnames] = useState<[string, string, string]>();
const [activityData, setActivityData] = useState<IActivity>();
const { query } = useRouter();
const [themeId, lessonId, activityId] = useMemo(() => {
return [query.theme as string, query.lesson as string, query.activity as string];
}, [query]);
const { data: activityData } = useAdminActivity({ themeId, lessonId, activityId });
const fetchActivity = async (
themeId: string,
lessonId: string,
activityId: string
) => {
setActivityData(
await adminService.current.fetchActivity(themeId, lessonId, activityId)
);
};
useEffect(() => {
if (!pathname) return;
const splitPathname = pathname.split("/");
const themeId = splitPathname[splitPathname.length - 3];
const lessonId = splitPathname[splitPathname.length - 2];
const activityId = splitPathname[splitPathname.length - 1];
setPathnames([themeId, lessonId, activityId]);
fetchActivity(themeId, lessonId, activityId);
}, [pathname]);
if (activityData && pathnames) {
if (activityData?.data && themeId && lessonId && activityId) {
return (
<ActivityEditor
themeId={pathnames[0]}
lessonId={pathnames[1]}
activityData={activityData}
themeId={themeId}
lessonId={lessonId}
activityData={activityData.data}
/>
);
}

View File

@ -1,8 +1,6 @@
import TP from "@/features/theme_page";
import { useEffect, useRef, useState } from "react";
import { usePathname } from "next/navigation";
import { useRouter } from "next/router";
import dynamic from "next/dynamic";
import ITheme from "@/lib/theme/theme";
import { useAdminTheme } from "@/api/theme/useAdminTheme";
const AT = dynamic(() => import("@/features/admin_tools"), {
@ -10,17 +8,11 @@ const AT = dynamic(() => import("@/features/admin_tools"), {
});
export default function ThemePage() {
const pathname = usePathname();
const { data: adminTheme } = useAdminTheme({ themeSlug: pathname ? pathname.split("/").pop() as string : "" });
const [themeData, setThemeData] = useState<ITheme>();
const { query } = useRouter();
const { data: adminTheme } = useAdminTheme({ themeSlug: query.theme as string });
useEffect(() => {
if (!pathname || !adminTheme) return;
setThemeData(adminTheme?.data as ITheme);
}, [pathname, adminTheme]);
if (themeData) {
return <TP home="/admin" theme={themeData} adminTools={<AT />} />;
if (adminTheme?.data) {
return <TP home="/admin" theme={adminTheme.data} adminTools={<AT />} />;
}
return (