Merge pull request #91 from learnhouse/swve/eng-32-collections-various-improvements

Redesign Collections & Collection Pages
This commit is contained in:
Badr B 2023-05-27 18:21:14 +02:00 committed by GitHub
commit 2414f47dc1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 271 additions and 86 deletions

View file

@ -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 (
<div>
<ErrorUI></ErrorUI>
</div>
);
}

View file

@ -0,0 +1,8 @@
import PageLoading from "@components/Pages/PageLoading";
export default function Loading() {
return (
<PageLoading></PageLoading>
)
}

View file

@ -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<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'] });
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 <div className="max-w-7xl mx-auto px-4 py-10" >
<h2 className="text-sm font-bold text-gray-400">Collection</h2>
<h1 className="text-3xl font-bold">{col.name}</h1>
<br />
<div className="home_courses flex flex-wrap">
{col.courses.map((course: any) => (
<div className="pr-8" key={course.course_id}>
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_id))}>
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getBackendUrl()}content/uploads/img/${course.thumbnail})` }}>
</div>
</Link>
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>
</div>
))}
</div>
</div>;
};
export default CollectionPage;

View file

@ -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 (
<div className="flex space-x-2 py-2">
<button className="rounded-md text-sm px-3 font-bold text-red-800 bg-red-200 w-16 flex justify-center items-center" onClick={() => deleteCollectionUI(props.collection_id)}>
Delete <Trash size={10}></Trash>
</button>
</div>
)
} else {
return (
<div></div>
)
}
}
else {
return (
<div></div>
)
}
}
export default CollectionAdminEditsArea;

View file

@ -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"));
};

View file

@ -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) {
type MetadataProps = {
params: { orgslug: string, courseid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
export async function generateMetadata(
{ params }: MetadataProps,
): 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'] });
return {
title: `Collections — ${org.name}`,
description: `Collections of courses from ${org.name}`,
};
}
const removeCollectionPrefix = (collectionid: string) => {
return collectionid.replace("collection_", "")
}
const CollectionsPage = async (params: any) => {
const cookieStore = cookies();
const access_token_cookie: any = cookieStore.get('access_token_cookie');
const orgslug = params.params.orgslug;
const { data: collections, error: error } = useSWR(`${getAPIUrl()}collections/page/1/limit/10`, swrFetcher);
const collections = await getOrgCollectionsWithAuthHeader(access_token_cookie ? access_token_cookie.value : null);
async function deleteCollectionAndFetch(collectionId: number) {
await deleteCollection(collectionId);
mutate(`${getAPIUrl()}collections/page/1/limit/10`);
}
return (
<>
<Title>
{orgslug} Collections :{" "}
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
<button>+</button>
</Link>{" "}
</Title>
{error && <p>Failed to load</p>}
{!collections ? (
<div>Loading...</div>
) : (
<div>
<div className="max-w-7xl mx-auto px-4 py-10" >
<div className="flex justify-between" >
<Title title="Collections" type="col" />
<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) => (
<CollectionItem key={collection.collection_id}>
<Link href={"/org/" + orgslug + "/collections/" + collection.collection_id}>{collection.name}</Link>
<CourseMiniThumbnail>
{collection.courses.map((course: any) => (
<Link key={course.course_id} href={"/org/" + orgslug + "/course/" + course.course_id.substring(7)}>
<img key={course.course_id} src={`${getBackendUrl()}content/uploads/img/${course.thumbnail}`} alt={course.name} />
<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>
))}
</CourseMiniThumbnail>
<button onClick={() => deleteCollectionAndFetch(collection.collection_id)}>Delete</button>
</CollectionItem>
</div>
</div>
</Link>
</div>
))}
</div>
)}
</>
</div>
);
}
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 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;
export default CollectionsPage

View file

@ -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']);
}

View file

@ -16,6 +16,7 @@ export async function generateMetadata(
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 course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token_cookie ? access_token_cookie.value : null)

View file

@ -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} />

View file

@ -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">

View file

@ -7,7 +7,6 @@ const { withSentryConfig } = require('@sentry/nextjs');
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: false,
swcMinify: false,
compiler: {
styledComponents: true,
},

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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