diff --git a/next.config.js b/next.config.js
index bb15531..9015b00 100644
--- a/next.config.js
+++ b/next.config.js
@@ -1,6 +1,7 @@
/** @type {import('next').NextConfig} */
const nextConfig = {
output: "export",
+ images: { unoptimized: true }
};
module.exports = nextConfig;
diff --git a/src/@types/activities.ts b/src/@types/activities.ts
new file mode 100644
index 0000000..60509e8
--- /dev/null
+++ b/src/@types/activities.ts
@@ -0,0 +1,211 @@
+export interface ActivityResult {
+ permissions: unknown;
+ paging: {
+ count: number;
+ limit: number;
+ nextPage: string;
+ offset: number;
+ previousPage: string;
+ };
+ results: Activity[];
+}
+
+export interface Activity {
+ iconUrl: string
+ launchInNewWindow: boolean
+ contentDetail: ContentDetail
+ modifiedDate: number
+ courseId: string
+ title: string
+ isReviewable: boolean
+ isGroupContent: boolean
+ inSequence: boolean
+ dueDateExceptionType: string
+ renderType: string
+ thumbnailAltText: string
+ aiState: string
+ inLesson: boolean
+ titleColor: string
+ position: number
+ contentHandler: ContentHandler
+ description: string
+ body: Body
+ permissions: Permissions
+ id: string
+ state: string
+ visibility?: string
+ parentId?: string
+ adaptiveReleaseRules?: AdaptiveReleaseRules
+}
+
+type ContentHandler = "resource/x-bb-folder"
+ | "resource/x-bb-file"
+ | "resource/x-bb-assignment"
+ | "resource/x-bb-externallink";
+
+export interface ContentDetail {
+ "resource/x-bb-folder"?: ResourceXBbFolder
+ "resource/x-bb-file"?: ResourceXBbFile
+ "resource/x-bb-assignment"?: ResourceXBbAssignment
+ "resource/x-bb-externallink"?: ResourceXBbExternallink
+}
+
+export interface ResourceXBbFolder {
+ isBbPage: boolean
+ isFolder: boolean
+}
+
+export interface ResourceXBbFile {
+ file: File
+ fileAssociationMode: string
+}
+
+export interface File {
+ existingFileReference: string
+ permanentUrl: string
+ mimeType: string
+ viewerUrl: string
+ forceDownload: boolean
+ isMedia: boolean
+ fileName: string
+ xid: string
+}
+
+export interface ResourceXBbAssignment {
+ gradingColumn: GradingColumn
+ groupContent: boolean
+ safeAssignOptions: SafeAssignOptions
+}
+
+export interface GradingColumn {
+ gradebookCategory: GradebookCategory
+ localizedColumnName: LocalizedColumnName
+ effectiveColumnName: string
+ isAttemptBased: boolean
+ gradeScoreDesignation: string
+ formativeIndicator: string
+ isFormative: boolean
+ courseId: string
+ scoreProviderHandle: string
+ visible: boolean
+ peerGrading: boolean
+ delegatedGrading: boolean
+ contentId: string
+ anonymousGrading: boolean
+ dateCreated: string
+ deleted: boolean
+ gradingSchemaId: string
+ columnName: string
+ aggregationModel: string
+ hideAttempt: boolean
+ scorable: boolean
+ visibleInAllTerms: boolean
+ visibleInBook: boolean
+ showStatsToStudent: boolean
+ limitedAttendance: boolean
+ userCreatedColumn: boolean
+ autoPostGrades: boolean
+ ltiDomainId: any
+ gradesReleased: boolean
+ enforceDueDate: boolean
+ position: number
+ possible: number
+ multipleAttempts: number
+ calculationType: string
+ gradebookCategoryId: string
+ id: string
+ "@id": string
+}
+
+export interface GradebookCategory {
+ isUserDefined: boolean
+ courseId: string
+ title: string
+ localizableTitle: LocalizableTitle
+ description: any
+ id: string
+}
+
+export interface LocalizableTitle {
+ languageKey: string
+ bundle: string
+}
+
+export interface LocalizedColumnName {
+ languageKey: string
+ bundle: string
+}
+
+export interface SafeAssignOptions {
+ checkAttempts: boolean
+ canStudentViewReports: boolean
+ excludeSubmissions: boolean
+ globalSearch: boolean
+}
+
+export interface ResourceXBbExternallink {
+ url: string
+}
+
+export interface Body {
+ rawText: string
+ displayText: string
+ webLocation: string
+ fileLocation: string
+}
+
+export interface Permissions {
+ edit: boolean
+ contentHandlerPermissionMap: ContentHandlerPermissionMap
+ modifyAvailability: boolean
+ adaptiveRelease: AdaptiveRelease
+ canHaveReviewState: boolean
+ canViewReviewState: boolean
+ canUpdateReviewState: boolean
+ dashboardView: boolean
+ createLearningStandardsAlignment: boolean
+ viewDesigner: boolean
+ delete: boolean
+ copy: boolean
+}
+
+export interface ContentHandlerPermissionMap {
+ createDiscussion: boolean
+}
+
+export interface AdaptiveRelease {
+ edit: boolean
+ criteria: Criteria
+ delete: boolean
+ create: boolean
+ view: boolean
+}
+
+export interface Criteria {
+ performance: Performance
+ acl: Acl
+ dates: Dates
+}
+
+export interface Performance {
+ edit: boolean
+ delete: boolean
+ create: boolean
+}
+
+export interface Acl {
+ edit: boolean
+ delete: boolean
+ create: boolean
+}
+
+export interface Dates {
+ edit: boolean
+ delete: boolean
+ create: boolean
+}
+
+export interface AdaptiveReleaseRules {
+ endDate: string
+ startDate: any
+}
diff --git a/src/@types/courses.ts b/src/@types/courses.ts
new file mode 100644
index 0000000..1d24fa8
--- /dev/null
+++ b/src/@types/courses.ts
@@ -0,0 +1,144 @@
+export interface CourseResponse {
+ permissions: unknown;
+ paging: {
+ count: number
+ limit: number
+ nextPage: string
+ offset: number
+ previousPage: string;
+ };
+ results: CourseResult[];
+}
+export interface CourseResult {
+ role: string
+ course: Course
+ courseId: string
+ isAvailable: boolean
+ modifiedDate: string
+ userId: string
+ lastAccessDate: string
+ receiveEmail: boolean
+ includedInRoster: boolean
+ dueDateExceptionType: string
+ timeLimitExceptionType: string
+ enrollmentDate: string
+ dataSourceId: string
+ courseRole: CourseRole
+ userHasHidden: boolean
+ courseCardColorIndex: number
+ id: string
+}
+
+export interface Course {
+ courseId: string
+ isAvailable: boolean
+ createdDate: string
+ modifiedDate: string
+ batchUid: string
+ uuid: string
+ navStyle: string
+ durationType: string
+ classificationId: string
+ dataSourceId: string
+ enrollmentType: string
+ paceType: string
+ bannerImageFile?: string
+ bannerAltText: string
+ isLocaleEnforced: boolean
+ defaultViewContent: string
+ termId?: string
+ isClosed: boolean
+ description: string
+ term?: Term
+ banner?: Banner
+ displayId: string
+ effectiveAvailability: boolean
+ isBannerVisible: boolean
+ isAllowObservers: boolean
+ isHonorTermAvailability: boolean
+ bannerImageThumbnail?: BannerImageThumbnail
+ isAllowGuests: boolean
+ displayName: string
+ permissions: Permissions2
+ ultraStatus: string
+ courseViewOption: string
+ isOrganization: boolean
+ externalAccessUrl: string
+ homePageUrl: string
+ name: string
+ id: string
+ guestAccessUrl?: string
+}
+
+export interface Term {
+ isAvailable: boolean
+ endDate: string
+ startDate: string
+ durationType: string
+ daysOfUse: number
+ description: Description
+ dataSrcId: string
+ permissions: Permissions
+ coursesPerTerm: number
+ name: string
+ id: string
+}
+
+export interface Description {
+ rawText: string
+ displayText: string
+ webLocation: any
+ fileLocation: any
+}
+
+export interface Permissions {
+ edit: boolean
+ delete: boolean
+}
+
+export interface Banner {
+ permanentUrl: string
+ forceDownload: boolean
+}
+
+export interface BannerImageThumbnail {
+ permanentUrl: string
+ forceDownload: boolean
+}
+
+export interface Permissions2 {
+ edit: boolean
+ archive: boolean
+ delete: boolean
+ export: boolean
+ copy: boolean
+ editDuration: boolean
+ editTerm: boolean
+ chooseUltraStatus: boolean
+ editBanner: boolean
+ editBannerVisibility: boolean
+ editAvailability: boolean
+ editName: boolean
+ editLocale: boolean
+ import: boolean
+ viewAutoArchive: boolean
+ messagesEnabled: boolean
+ convert: boolean
+ editClosed: boolean
+ viewEnrollments: boolean
+}
+
+export interface CourseRole {
+ courseName: CourseName
+ roleBucket: string
+ sortOrder: number
+ identifier: string
+ roleAvailability: string
+ isActAsInstructor: boolean
+ id: string
+}
+
+export interface CourseName {
+ bundle: string
+ languageKey: string
+}
diff --git a/src/app/[locale]/(dashboard)/courses/[id]/page.tsx b/src/app/[locale]/(dashboard)/courses/[id]/page.tsx
new file mode 100644
index 0000000..851e482
--- /dev/null
+++ b/src/app/[locale]/(dashboard)/courses/[id]/page.tsx
@@ -0,0 +1,28 @@
+"use client";
+import {List, Stack, Typography} from "@mui/material";
+import {useGetCourseContentsQuery} from "@/redux/services/course-api";
+import ActivityCard from "@/components/activity-card";
+
+export default function CourseActivities({ params }: { params: { id: string }} ) {
+ const { data } = useGetCourseContentsQuery(params.id);
+
+ console.log(data, params.id)
+
+ return (
+
+
+ Activities from course - {params.id}
+
+
+
+ {data?.results?.map((activity) => {
+ if (activity.contentHandler !== "resource/x-bb-assignment") {
+ return;
+ }
+
+ return
+ })}
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/app/[locale]/(dashboard)/dashboard/page.tsx b/src/app/[locale]/(dashboard)/dashboard/page.tsx
index 72b3e5e..0ab7e6c 100644
--- a/src/app/[locale]/(dashboard)/dashboard/page.tsx
+++ b/src/app/[locale]/(dashboard)/dashboard/page.tsx
@@ -1,9 +1,14 @@
"use client";
-import { Stack, Typography } from "@mui/material";
-import { useGetMeQuery } from "@/redux/services/user-api";
+import {CircularProgress, Grid, Stack, Typography} from "@mui/material";
+import {useGetMeQuery} from "@/redux/services/user-api";
+import {useGetCoursesQuery} from "@/redux/services/course-api";
+import CourseCard from "@/components/course-card";
export default function Activities() {
const { isLoading, data } = useGetMeQuery();
+ const { data: courses } = useGetCoursesQuery(data?.id ?? "", { skip: data === undefined });
+
+ console.log(courses)
return (
- Activities page
+
+ Activities - { courses?.paging?.count ?? 0 }
+ {isLoading ? : undefined}
+
+
+
+ {courses?.results?.map((courseRoot) => (
+
+ ))}
+
);
}
diff --git a/src/components/activity-card.tsx b/src/components/activity-card.tsx
new file mode 100644
index 0000000..ed010d0
--- /dev/null
+++ b/src/components/activity-card.tsx
@@ -0,0 +1,40 @@
+import { ListItem, ListItemText, Tooltip } from "@mui/material";
+import { Done, QueryBuilder } from "@mui/icons-material";
+import { Activity } from "@/@types/activities";
+import { useGetAttemptsQuery } from "@/redux/services/activity-api";
+
+export default function ActivityCard({ activity }: { activity: Activity }) {
+ const { data } = useGetAttemptsQuery({
+ activityId: activity.contentDetail[activity.contentHandler as "resource/x-bb-assignment"]!.gradingColumn.id,
+ courseId: activity.courseId
+ });
+
+ return (
+
+
+
+ {
+ data && Object.keys(data.lookup).length !== 0
+ ? (
+
+
+
+ )
+ : undefined
+ }
+
+ {
+ activity.adaptiveReleaseRules
+ ? (
+
+
+
+ )
+ : undefined
+ }
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/course-card.tsx b/src/components/course-card.tsx
new file mode 100644
index 0000000..2989b43
--- /dev/null
+++ b/src/components/course-card.tsx
@@ -0,0 +1,71 @@
+import {ButtonBase, Card, CardContent, CardMedia, Grid, Typography} from "@mui/material";
+import {CourseResult} from "@/@types/courses";
+import Image from "next/image";
+import {useRouter} from "next/navigation";
+import {useEffect, useState} from "react";
+import {useGetCourseBannerQuery, useGetCourseStaticBannerQuery} from "@/redux/services/banner-api";
+import {useWithLocale} from "@/hooks/useWithLocale";
+
+export default function CourseCard({ courseRoot }: { courseRoot: CourseResult }) {
+ const router = useRouter();
+ const withLocale = useWithLocale();
+ const [expired, setExpired] = useState(false);
+ const { data: bannerUrl } = courseRoot.course.banner
+ ? useGetCourseBannerQuery(courseRoot.courseId)
+ : useGetCourseStaticBannerQuery(`nature${courseRoot.courseCardColorIndex % 20 + 1}_thumb`);
+
+ function handleClick() {
+ router.push(withLocale(`/courses/${courseRoot.courseId}`));
+ }
+
+ useEffect(() => {
+ const endDate = courseRoot.course.term?.endDate;
+
+ if (!endDate) {
+ return;
+ }
+
+ const endDateParsed = Date.parse(endDate);
+ const now = Date.now();
+
+ setExpired(endDateParsed < now);
+ }, [courseRoot.course.term?.endDate]);
+
+ return (
+
+ handleClick()} sx={{ width: "100%", height: "100%" }}>
+
+
+
+
+
+
+
+
+
+ {courseRoot.courseId}
+
+
+
+ {courseRoot.course.name}
+
+
+
+ {courseRoot.course.description}
+
+
+
+
+
+ )
+}
\ No newline at end of file
diff --git a/src/components/navbar.tsx b/src/components/navbar.tsx
index 2236b14..2e3aeb1 100644
--- a/src/components/navbar.tsx
+++ b/src/components/navbar.tsx
@@ -17,17 +17,12 @@ import {
Skeleton,
Stack,
Toolbar,
- Typography,
+ Typography
} from "@mui/material";
import { stringAvatar } from "@/helpers/text-to-avatar";
import { useGetMeQuery } from "@/redux/services/user-api";
import React, { FC, PropsWithChildren, useState } from "react";
-import {
- AccountCircle,
- Logout,
- Menu as MenuIcon,
- Settings,
-} from "@mui/icons-material";
+import { AccountCircle, Logout, Menu as MenuIcon, Settings } from "@mui/icons-material";
import { signOut } from "@/redux/actions/auth-action";
import { useAppDispatch } from "@/redux/hooks";
import Link from "next/link";
diff --git a/src/redux/services/activity-api.ts b/src/redux/services/activity-api.ts
new file mode 100644
index 0000000..d0cc577
--- /dev/null
+++ b/src/redux/services/activity-api.ts
@@ -0,0 +1,18 @@
+import { createApi } from "@reduxjs/toolkit/query/react";
+import { authFetchBaseQuery } from "@/utils/auth-fetch-base-query";
+import { getApi } from "@/utils/get-api";
+
+
+export const activityApi = createApi({
+ reducerPath: "activityApi",
+ baseQuery: authFetchBaseQuery({ baseUrl: getApi() }),
+ endpoints: (builder) => ({
+ getAttempts: builder.query<{ lookup: { [key: string]: any } }, { courseId: string, activityId: string }>({
+ query: (data) => ({
+ url: `/learn/api/v1/courses/${data.courseId}/gradebook/columns/${data.activityId}/attempts`
+ })
+ })
+ })
+});
+
+export const { useGetAttemptsQuery } = activityApi;
\ No newline at end of file
diff --git a/src/redux/services/banner-api.ts b/src/redux/services/banner-api.ts
new file mode 100644
index 0000000..f677038
--- /dev/null
+++ b/src/redux/services/banner-api.ts
@@ -0,0 +1,44 @@
+import {createApi} from "@reduxjs/toolkit/query/react";
+import {authFetchBaseQuery} from "@/utils/auth-fetch-base-query";
+import {getApi} from "@/utils/get-api";
+import {ResponseType} from "@tauri-apps/api/http";
+
+export const bannerApi = createApi({
+ reducerPath: "bannerApi",
+ baseQuery: authFetchBaseQuery({ baseUrl: getApi() }),
+ endpoints: (builder) => ({
+ getCourseBanner: builder.query({
+ query: (courseId) => ({
+ url: `/bannerthumbnail/${courseId}`,
+ options: {
+ responseType: ResponseType.Text,
+ method: "GET",
+ },
+ }),
+ transformResponse: (response: string, meta) => {
+ if (meta?.response.status !== 200) return null;
+
+ return meta?.response.url;
+ }
+ }),
+ getCourseStaticBanner: builder.query({
+ query: (bannerName) => ({
+ url: `/ultra/static/images/default-banners/${bannerName}.jpg`,
+ options: {
+ responseType: ResponseType.Binary,
+ method: "GET",
+ },
+ }),
+ transformResponse: (response: string, meta) => {
+ if (meta?.response.status !== 200) return null;
+ if (meta?.response.data === undefined) return null;
+
+ const buffer = Buffer.from(meta.response.data as string, 'binary');
+
+ return `data:image/png;base64,${buffer.toString("base64")}`;
+ }
+ })
+ }),
+});
+
+export const { useGetCourseBannerQuery, useGetCourseStaticBannerQuery } = bannerApi;
diff --git a/src/redux/services/course-api.ts b/src/redux/services/course-api.ts
new file mode 100644
index 0000000..1ec549e
--- /dev/null
+++ b/src/redux/services/course-api.ts
@@ -0,0 +1,38 @@
+import {createApi} from "@reduxjs/toolkit/query/react";
+import {authFetchBaseQuery} from "@/utils/auth-fetch-base-query";
+import {getApi} from "@/utils/get-api";
+import {CourseResponse} from "@/@types/courses";
+import {ActivityResult} from "@/@types/activities";
+
+export const courseApi = createApi({
+ reducerPath: "courseApi",
+ baseQuery: authFetchBaseQuery({ baseUrl: getApi() }),
+ endpoints: (builder) => ({
+ getCourses: builder.query({
+ query: (userid) => ({
+ url: `/learn/api/v1/users/${userid}/memberships`,
+ options: {
+ query: {
+ expand: "course.effectiveAvailability,course.permissions,courseRole",
+ includeCount: "true",
+ limit: "10000",
+ },
+ method: "GET",
+ },
+ })
+ }),
+ getCourseContents: builder.query({
+ query: (courseId) => ({
+ url: `/learn/api/v1/courses/${courseId}/contents`,
+ options: {
+ query: {
+ recursive: "true"
+ },
+ method: "GET"
+ }
+ })
+ })
+ }),
+});
+
+export const { useGetCoursesQuery, useGetCourseContentsQuery } = courseApi;
diff --git a/src/redux/services/user-api.ts b/src/redux/services/user-api.ts
index 18d89e1..e1946c1 100644
--- a/src/redux/services/user-api.ts
+++ b/src/redux/services/user-api.ts
@@ -27,7 +27,7 @@ export const userApi = createApi({
method: "GET",
responseType: ResponseType.JSON,
},
- }),
+ })
}),
}),
});
diff --git a/src/redux/store.ts b/src/redux/store.ts
index 305d6e8..a42efbb 100644
--- a/src/redux/store.ts
+++ b/src/redux/store.ts
@@ -1,15 +1,21 @@
-import { combineReducers, configureStore } from "@reduxjs/toolkit";
-import { persistReducer, persistStore } from "redux-persist";
-import { setupListeners } from "@reduxjs/toolkit/query";
-import { authReducer } from "@/redux/features/auth-slice";
-import { userApi } from "@/redux/services/user-api";
+import {combineReducers, configureStore} from "@reduxjs/toolkit";
+import {persistReducer, persistStore} from "redux-persist";
+import {setupListeners} from "@reduxjs/toolkit/query";
+import {authReducer} from "@/redux/features/auth-slice";
+import {userApi} from "@/redux/services/user-api";
import storage from "@/redux/custom-storage";
import { settingsReducer } from "@/redux/features/settings-slice";
+import {courseApi} from "@/redux/services/course-api";
+import {activityApi} from "@/redux/services/activity-api";
+import {bannerApi} from "@/redux/services/banner-api";
export const rootReducers = combineReducers({
auth: authReducer,
settings: settingsReducer,
[userApi.reducerPath]: userApi.reducer,
+ [courseApi.reducerPath]: courseApi.reducer,
+ [activityApi.reducerPath]: activityApi.reducer,
+ [bannerApi.reducerPath]: bannerApi.reducer
});
const persistConfig = {
@@ -26,6 +32,9 @@ export const store = configureStore({
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({ serializableCheck: false }).concat([
userApi.middleware,
+ courseApi.middleware,
+ activityApi.middleware,
+ bannerApi.middleware
]),
});