diff --git a/front/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/error.tsx b/front/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/error.tsx new file mode 100644 index 00000000..9e4c3efb --- /dev/null +++ b/front/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/error.tsx @@ -0,0 +1,23 @@ +'use client'; // Error components must be Client Components + +import ErrorUI from '@components/UI/Error/Error'; +import { useEffect } from 'react'; + +export default function Error({ + error, + reset, +}: { + error: Error; + reset: () => void; +}) { + useEffect(() => { + // Log the error to an error reporting service + console.error(error); + }, [error]); + + return ( +
+ +
+ ); +} \ No newline at end of file diff --git a/front/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/loading.tsx b/front/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/loading.tsx new file mode 100644 index 00000000..9a7cafe9 --- /dev/null +++ b/front/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/loading.tsx @@ -0,0 +1,8 @@ +import PageLoading from "@components/Pages/PageLoading"; + +export default function Loading() { + return ( + + ) + +} \ No newline at end of file diff --git a/front/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/page.tsx new file mode 100644 index 00000000..7a79fa23 --- /dev/null +++ b/front/app/orgs/[orgslug]/(withmenu)/collection/[collectionid]/page.tsx @@ -0,0 +1,62 @@ +import { getBackendUrl, getUriWithOrg } from "@services/config/config"; +import { getCollectionByIdWithAuthHeader } from "@services/courses/collections"; +import { getOrganizationContextInfo } from "@services/organizations/orgs"; +import { Metadata } from "next"; +import { cookies } from "next/headers"; +import Link from "next/link"; + +type MetadataProps = { + params: { orgslug: string, courseid: string, collectionid: string }; + searchParams: { [key: string]: string | string[] | undefined }; +}; + +export async function generateMetadata( + { params }: MetadataProps, +): Promise { + const cookieStore = cookies(); + const access_token_cookie: any = cookieStore.get('access_token_cookie'); + // Get Org context information + const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); + const col = await getCollectionByIdWithAuthHeader(params.collectionid, access_token_cookie ? access_token_cookie.value : null, { revalidate: 0, tags: ['collections'] }); + + console.log(col) + + return { + title: `Collection : ${col.name} — ${org.name}`, + description: `${col.description} `, + }; +} + +const CollectionPage = async (params : any) => { + const cookieStore = cookies(); + const access_token_cookie: any = cookieStore.get('access_token_cookie'); + const orgslug = params.params.orgslug; + const col = await getCollectionByIdWithAuthHeader(params.params.collectionid, access_token_cookie ? access_token_cookie.value : null, { revalidate: 0, tags: ['collections'] }); + + const removeCoursePrefix = (courseid: string) => { + return courseid.replace("course_", "") + } + + + return
+

Collection

+

{col.name}

+
+
+ {col.courses.map((course: any) => ( +
+ +
+
+ +

{course.name}

+
+ ))} +
+ + + +
; +}; + +export default CollectionPage; \ No newline at end of file diff --git a/front/app/orgs/[orgslug]/(withmenu)/collections/admin.tsx b/front/app/orgs/[orgslug]/(withmenu)/collections/admin.tsx new file mode 100644 index 00000000..bb5b9c8d --- /dev/null +++ b/front/app/orgs/[orgslug]/(withmenu)/collections/admin.tsx @@ -0,0 +1,73 @@ +'use client'; + +import { AuthContext } from '@components/Security/AuthProvider'; +import { deleteCollection } from '@services/courses/collections'; +import { revalidateTags } from '@services/utils/ts/requests'; +import { Link, Trash } from 'lucide-react'; +import React from 'react' + +const CollectionAdminEditsArea = (props: any) => { + const org_roles_values = ["admin", "owner"]; + const user_roles_values = ["role_admin"]; + console.log("props: ", props); + + const auth: any = React.useContext(AuthContext); + console.log("auth: ", auth); + + + // this is amazingly terrible code, but gotta release that MVP + // TODO: fix this + + function isAuthorized() { + const org_id = props.collection.org_id; + const org_roles = auth.userInfo.user_object.orgs; + const user_roles = auth.userInfo.user_object.roles; + const org_role = org_roles.find((org: any) => org.org_id == org_id); + const user_role = user_roles.find((role: any) => role.org_id == org_id); + + if (org_role && user_role) { + if (org_roles_values.includes(org_role.org_role) && user_roles_values.includes(user_role.role_id)) { + return true; + } + else { + return false; + } + } else { + return false; + } + } + + const deleteCollectionUI = async (collectionId: number) => { + await deleteCollection(collectionId); + revalidateTags(["collections"]); + // reload the page + window.location.reload(); + } + + // this is amazingly terrible code, but gotta release that MVP + // TODO: fix this + + if (auth.isAuthenticated) { + if (isAuthorized()) { + return ( +
+ + +
+ ) + } else { + return ( +
+ ) + } + } + else { + return ( +
+ ) + } +} + +export default CollectionAdminEditsArea; \ No newline at end of file diff --git a/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx index e1de0cb6..cf94e666 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/collections/new/page.tsx @@ -5,7 +5,7 @@ import { Title } from "@components/UI/Elements/Styles/Title"; import { createCollection } from "@services/courses/collections"; import useSWR from "swr"; import { getAPIUrl, getUriWithOrg } from "@services/config/config"; -import { swrFetcher } from "@services/utils/ts/requests"; +import { revalidateTags, swrFetcher } from "@services/utils/ts/requests"; import { getOrganizationContextInfo } from "@services/organizations/orgs"; function NewCollection(params: any) { @@ -44,6 +44,7 @@ function NewCollection(params: any) { org_id: org.org_id, }; await createCollection(collection); + revalidateTags(["collections"]); router.push(getUriWithOrg(orgslug, "/collections")); }; diff --git a/front/app/orgs/[orgslug]/(withmenu)/collections/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/collections/page.tsx index a6169647..930b9b1c 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/collections/page.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/collections/page.tsx @@ -1,85 +1,77 @@ -"use client"; +import { getBackendUrl, getUriWithOrg } from "@services/config/config"; +import { deleteCollection, getOrgCollectionsWithAuthHeader } from "@services/courses/collections"; +import { getCourseMetadataWithAuthHeader } from "@services/courses/courses"; +import { getOrganizationContextInfo } from "@services/organizations/orgs"; +import { revalidateTags } from "@services/utils/ts/requests"; +import { Metadata } from "next"; +import { revalidateTag } from "next/cache"; +import { cookies } from "next/headers"; import Link from "next/link"; -import React from "react"; -import styled from "styled-components"; -import { Title } from "@components/UI/Elements/Styles/Title"; -import { deleteCollection } from "@services/courses/collections"; -import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config/config"; -import { swrFetcher } from "@services/utils/ts/requests"; -import useSWR, { mutate } from "swr"; +import { Title } from "../courses/courses"; +import CollectionAdminEditsArea from "./admin"; -function Collections(params: any) { - const orgslug = params.params.orgslug; - const { data: collections, error: error } = useSWR(`${getAPIUrl()}collections/page/1/limit/10`, swrFetcher); +type MetadataProps = { + params: { orgslug: string, courseid: string }; + searchParams: { [key: string]: string | string[] | undefined }; +}; - async function deleteCollectionAndFetch(collectionId: number) { - await deleteCollection(collectionId); - mutate(`${getAPIUrl()}collections/page/1/limit/10`); - } +export async function generateMetadata( + { params }: MetadataProps, +): Promise { + const cookieStore = cookies(); + const access_token_cookie: any = cookieStore.get('access_token_cookie'); + // Get Org context information + const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); - return ( - <> - - {orgslug} Collections :{" "} - <Link href={getUriWithOrg(orgslug, "/collections/new")}> - <button>+</button> - </Link>{" "} - - {error &&

Failed to load

} - {!collections ? ( -
Loading...
- ) : ( -
- {collections.map((collection: any) => ( - - {collection.name} - - {collection.courses.map((course: any) => ( - - {course.name} - - ))} - - - - ))} -
- )} - - ); + return { + title: `Collections — ${org.name}`, + description: `Collections of courses from ${org.name}`, + }; } -const CollectionItem = styled.div` - display: flex; - flex-direction: row; - place-items: center; - width: 100%; - height: 100%; - padding: 10px; - border: 1px solid #e5e5e5; - border-radius: 5px; - box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.03); - background: #ffffff; - cursor: pointer; - transition: all 0.2s ease-in-out; - &:hover { - box-shadow: 0px 4px 16px rgba(0, 0, 0, 0.1); - } -`; +const removeCollectionPrefix = (collectionid: string) => { + return collectionid.replace("collection_", "") +} -const CourseMiniThumbnail = styled.div` - display: flex; - flex-direction: row; - img { - width: 20px; - height: 20px; - border-radius: 5px; - margin: 5px; - transition: all 0.2s ease-in-out; - } - &:hover { - opacity: 0.8; - } -`; -export default Collections; +const CollectionsPage = async (params: any) => { + const cookieStore = cookies(); + const access_token_cookie: any = cookieStore.get('access_token_cookie'); + const orgslug = params.params.orgslug; + const collections = await getOrgCollectionsWithAuthHeader(access_token_cookie ? access_token_cookie.value : null); + + + + + return ( +
+
+ + <Link className="flex justify-center" href={getUriWithOrg(orgslug, "/collections/new")}> + <button className="rounded-md bg-black antialiased ring-offset-purple-800 p-2 px-5 my-auto font text-sm font-bold text-white drop-shadow-lg">Add Collection + </button> + </Link> + </div> + <div className="home_collections flex flex-wrap"> + {collections.map((collection: any) => ( + <div className="pr-8 flex flex-col" key={collection.collection_id}> + <CollectionAdminEditsArea collection_id={collection.collection_id} collection={collection} /> + <Link href={getUriWithOrg(orgslug, "/collection/" + removeCollectionPrefix(collection.collection_id))}> + <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[180px] bg-cover flex flex-col items-center justify-center bg-indigo-600 font-bold text-zinc-50" > + <h1 className="font-bold text-lg py-2 justify-center mb-2">{collection.name}</h1> + <div className="flex -space-x-4"> + {collection.courses.slice(0, 3).map((course: any) => ( + <Link key={course.course_id} href={getUriWithOrg(orgslug, "/course/" + course.course_id.substring(7))}> + <img className="w-12 h-12 rounded-full flex items-center justify-center shadow-lg ring-2 ring-white z-50" key={course.course_id} src={`${getBackendUrl()}content/uploads/img/${course.thumbnail}`} alt={course.name} /> + </Link> + ))} + </div> + </div> + </Link> + </div> + ))} + </div> + </div> + ); +} + +export default CollectionsPage \ No newline at end of file diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx index aa1678d4..d88b6ce8 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/course.tsx @@ -18,7 +18,6 @@ const CourseClient = (props: any) => { async function startCourseUI() { // Create activity await startCourse("course_" + courseid, orgslug); - revalidateTags(['courses']); } @@ -26,8 +25,6 @@ const CourseClient = (props: any) => { // Close activity let activity = await removeCourse("course_" + courseid, orgslug); - console.log(activity); - // Mutate course revalidateTags(['courses']); } diff --git a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/page.tsx index e5494c84..2e07285e 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/page.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/course/[courseid]/page.tsx @@ -15,6 +15,7 @@ export async function generateMetadata( ): Promise<Metadata> { const cookieStore = cookies(); const access_token_cookie: any = cookieStore.get('access_token_cookie'); + // Get Org context information const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] }); diff --git a/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx b/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx index 079cc54a..018f731a 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/courses/courses.tsx @@ -63,7 +63,6 @@ function Courses(props: CourseProps) { <div className="flex space-x-5"> - {courses.map((course: any) => ( <div key={course.course_id}> <AdminEditsArea course={course} orgslug={orgslug} course_id={course.course_id} deleteCourses={deleteCourses} /> diff --git a/front/app/orgs/[orgslug]/(withmenu)/page.tsx b/front/app/orgs/[orgslug]/(withmenu)/page.tsx index 4c5def42..f4d48efc 100644 --- a/front/app/orgs/[orgslug]/(withmenu)/page.tsx +++ b/front/app/orgs/[orgslug]/(withmenu)/page.tsx @@ -43,6 +43,10 @@ const OrgHomePage = async (params: any) => { return course_id.replace("course_", ""); } + function removeCollectionPrefix(collection_id: string) { + return collection_id.replace("collection_", ""); + } + return ( <div> <div className="max-w-7xl mx-auto px-4 py-10"> @@ -51,7 +55,7 @@ const OrgHomePage = async (params: any) => { <div className="home_collections flex flex-wrap"> {collections.map((collection: any) => ( <div className="pr-8 flex flex-col" key={collection.collection_id}> - <Link href={getUriWithOrg(orgslug, "/collection/" + removeCoursePrefix(collection.collection_id))}> + <Link href={getUriWithOrg(orgslug, "/collection/" + removeCollectionPrefix(collection.collection_id))}> <div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[180px] bg-cover flex flex-col items-center justify-center bg-indigo-600 font-bold text-zinc-50" > <h1 className="font-bold text-lg py-2 justify-center mb-2">{collection.name}</h1> <div className="flex -space-x-4"> diff --git a/front/next.config.js b/front/next.config.js index b8c0a4e7..e1035183 100644 --- a/front/next.config.js +++ b/front/next.config.js @@ -7,7 +7,6 @@ const { withSentryConfig } = require('@sentry/nextjs'); /** @type {import('next').NextConfig} */ const nextConfig = { reactStrictMode: false, - swcMinify: false, compiler: { styledComponents: true, }, diff --git a/front/services/courses/collections.ts b/front/services/courses/collections.ts index 73805141..ad0bf20c 100644 --- a/front/services/courses/collections.ts +++ b/front/services/courses/collections.ts @@ -19,6 +19,20 @@ export async function createCollection(collection: any) { return res; } + +// Get a colletion by id +export async function getCollectionById(collection_id: any) { + const result: any = await fetch(`${getAPIUrl()}collections/${collection_id}`, { next: { revalidate: 10 } }); + const res = await errorHandling(result); + return res; +} + +export async function getCollectionByIdWithAuthHeader(collection_id: any, access_token: string, next: any) { + const result: any = await fetch(`${getAPIUrl()}collections/collection_${collection_id}`, RequestBodyWithAuthHeader("GET", null, next, access_token)); + const res = await errorHandling(result); + return res; +} + // Get collections // TODO : add per org filter export async function getOrgCollections() { @@ -28,7 +42,7 @@ export async function getOrgCollections() { } export async function getOrgCollectionsWithAuthHeader(access_token: string) { - const result: any = await fetch(`${getAPIUrl()}collections/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, { revalidate: 10 }, access_token)); + const result: any = await fetch(`${getAPIUrl()}collections/page/1/limit/10`, RequestBodyWithAuthHeader("GET", null, { revalidate: 3 }, access_token)); const res = await errorHandling(result); return res; } diff --git a/front/services/utils/ts/requests.ts b/front/services/utils/ts/requests.ts index 06c88816..e5b9bf5a 100644 --- a/front/services/utils/ts/requests.ts +++ b/front/services/utils/ts/requests.ts @@ -78,7 +78,7 @@ export const swrFetcher = async (url: string, body: any, router?: AppRouterInsta export const errorHandling = (res: any) => { if (!res.ok) { - const error: any = new Error(`Error ${res.status}: ${res.statusText}`, {}); + const error: any = new Error(`${res.status}: ${res.statusText}`, {}); error.status = res.status; throw error; } diff --git a/src/services/courses/collections.py b/src/services/courses/collections.py index 3b5ed279..8f482f1c 100644 --- a/src/services/courses/collections.py +++ b/src/services/courses/collections.py @@ -38,6 +38,18 @@ async def get_collection(request: Request,collection_id: str, current_user: Publ status_code=status.HTTP_409_CONFLICT, detail="Collection does not exist") collection = Collection(**collection) + + # add courses to collection + courses = request.app.db["courses"] + courseids = [course for course in collection.courses] + + collection.courses = [] + collection.courses = courses.find( + {"course_id": {"$in": courseids}}, {'_id': 0}) + + collection.courses = [course for course in await collection.courses.to_list(length=100)] + + return collection