mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge pull request #67 from learnhouse/feat/new-modal-design
New Modal Component + Components refactor
This commit is contained in:
commit
49b6d1dfe7
42 changed files with 861 additions and 399 deletions
|
|
@ -2,13 +2,12 @@
|
|||
import { useRouter } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import React, { useMemo } from "react";
|
||||
import Layout from "@components/UI/Layout";
|
||||
import { getActivity } from "@services/courses/activities";
|
||||
import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config/config";
|
||||
import Canva from "@components/ActivityViews/DynamicCanva/DynamicCanva";
|
||||
import Canva from "@components/Pages/Activities/DynamicCanva/DynamicCanva";
|
||||
import styled from "styled-components";
|
||||
import { getCourse } from "@services/courses/courses";
|
||||
import VideoActivity from "@components/ActivityViews/Video/Video";
|
||||
import VideoActivity from "@components/Pages/Activities/Video/Video";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { Check } from "lucide-react";
|
||||
import { swrFetcher } from "@services/utils/requests";
|
||||
|
|
|
|||
|
|
@ -1,23 +1,24 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { Header } from "@components/UI/Header";
|
||||
import Layout from "@components/UI/Layout";
|
||||
import { Title } from "@components/UI/Elements/Styles/Title";
|
||||
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
||||
import { initialData, initialData2 } from "@components/Drags/data";
|
||||
import Chapter from "@components/Drags/Chapter";
|
||||
import { initialData, initialData2 } from "@components/Pages/CourseEdit/Draggables/data";
|
||||
import Chapter from "@components/Pages/CourseEdit/Draggables/Chapter";
|
||||
import { createChapter, deleteChapter, getCourseChaptersMetadata, updateChaptersMetadata } from "@services/courses/chapters";
|
||||
import { useRouter } from "next/navigation";
|
||||
import NewChapterModal from "@components/Modals/CourseEdit/NewChapter";
|
||||
import NewActivityModal from "@components/Modals/CourseEdit/NewActivity";
|
||||
import NewChapterModal from "@components/Pages/CourseEdit/NewChapter";
|
||||
import NewActivityModal from "@components/Pages/CourseEdit/NewActivity";
|
||||
import { createActivity, createFileActivity } from "@services/courses/activities";
|
||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||
import Modal from "@components/UI/Modal/Modal";
|
||||
import AuthProvider from "@components/Security/AuthProvider";
|
||||
|
||||
function CourseEdit(params: any) {
|
||||
const router = useRouter();
|
||||
|
||||
const router = useRouter();
|
||||
// Initial Course State
|
||||
const [data, setData] = useState(initialData2) as any;
|
||||
|
||||
|
|
@ -32,12 +33,15 @@ function CourseEdit(params: any) {
|
|||
const courseid = params.params.courseid;
|
||||
const orgslug = params.params.orgslug;
|
||||
|
||||
|
||||
|
||||
async function getCourseChapters() {
|
||||
const courseChapters = await getCourseChaptersMetadata(courseid);
|
||||
setData(courseChapters);
|
||||
console.log("courseChapters", courseChapters);
|
||||
try {
|
||||
const courseChapters = await getCourseChaptersMetadata(courseid);
|
||||
setData(courseChapters);
|
||||
} catch (error: any) {
|
||||
if (error.status === 401) {
|
||||
router.push("/login");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
|
|
@ -106,9 +110,7 @@ function CourseEdit(params: any) {
|
|||
};
|
||||
|
||||
/*
|
||||
|
||||
Modals
|
||||
|
||||
*/
|
||||
|
||||
const openNewActivityModal = async (chapterId: any) => {
|
||||
|
|
@ -123,6 +125,8 @@ function CourseEdit(params: any) {
|
|||
};
|
||||
|
||||
const closeNewActivityModal = () => {
|
||||
console.log("closeNewActivityModal");
|
||||
|
||||
setNewActivityModal(false);
|
||||
};
|
||||
|
||||
|
|
@ -233,13 +237,22 @@ function CourseEdit(params: any) {
|
|||
<Page>
|
||||
<Title>
|
||||
Edit Course {" "}
|
||||
<button
|
||||
onClick={() => {
|
||||
setNewChapterModal(true);
|
||||
}}
|
||||
>
|
||||
Add chapter +
|
||||
</button>
|
||||
<Modal
|
||||
isDialogOpen={newChapterModal}
|
||||
onOpenChange={setNewChapterModal}
|
||||
minHeight="sm"
|
||||
dialogContent={<NewChapterModal
|
||||
closeModal={closeNewChapterModal}
|
||||
submitChapter={submitChapter}
|
||||
></NewChapterModal>}
|
||||
dialogTitle="Create chapter"
|
||||
dialogDescription="Add a new chapter to the course"
|
||||
dialogTrigger={
|
||||
<button> Add chapter +
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
updateChapters();
|
||||
|
|
@ -247,16 +260,23 @@ function CourseEdit(params: any) {
|
|||
>
|
||||
Save
|
||||
</button>
|
||||
</Title>
|
||||
{newChapterModal && <NewChapterModal closeModal={closeNewChapterModal} submitChapter={submitChapter}></NewChapterModal>}
|
||||
{newActivityModal && (
|
||||
<NewActivityModal
|
||||
</Title>-
|
||||
|
||||
<Modal
|
||||
isDialogOpen={newActivityModal}
|
||||
onOpenChange={setNewActivityModal}
|
||||
minHeight="no-min"
|
||||
addDefCloseButton={false}
|
||||
dialogContent={<NewActivityModal
|
||||
closeModal={closeNewActivityModal}
|
||||
submitFileActivity={submitFileActivity}
|
||||
submitActivity={submitActivity}
|
||||
chapterId={newActivityModalData}
|
||||
></NewActivityModal>
|
||||
)}
|
||||
></NewActivityModal>}
|
||||
dialogTitle="Create Activity"
|
||||
dialogDescription="Choose between types of activities to add to the course"
|
||||
|
||||
/>
|
||||
|
||||
<br />
|
||||
{winReady && (
|
||||
|
|
@ -287,7 +307,7 @@ function CourseEdit(params: any) {
|
|||
</DragDropContext>
|
||||
</ChapterlistWrapper>
|
||||
)}
|
||||
</Page>
|
||||
</Page >
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
"use client";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React from "react";
|
||||
import { Header } from "@components/UI/Header";
|
||||
import Layout from "@components/UI/Layout";
|
||||
import { Title } from "@components/UI/Elements/Styles/Title";
|
||||
import { createNewCourse } from "@services/courses/courses";
|
||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
||||
|
|
|
|||
|
|
@ -3,18 +3,16 @@ import React, { createContext, useState } from 'react'
|
|||
import { styled } from '@stitches/react';
|
||||
import Link from 'next/link';
|
||||
import LearnHouseWhiteLogo from '@public/learnhouse_text_white.png';
|
||||
import { AuthContext } from '@components/Security/AuthProvider';
|
||||
import AuthProvider, { AuthContext } from '@components/Security/AuthProvider';
|
||||
import Avvvatars from 'avvvatars-react';
|
||||
import Image from 'next/image';
|
||||
|
||||
|
||||
|
||||
|
||||
function SettingsLayout({ children, params }: { children: React.ReactNode, params: any }) {
|
||||
const auth: any = React.useContext(AuthContext);
|
||||
|
||||
return (
|
||||
<>
|
||||
<>
|
||||
<AuthProvider/>
|
||||
<Main>
|
||||
<LeftWrapper>
|
||||
<LeftTopArea>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
import "../styles/globals.css";
|
||||
import StyledComponentsRegistry from "../components/lib/styled-registry";
|
||||
import StyledComponentsRegistry from "../components/UI/libs/styled-registry";
|
||||
import { motion } from "framer-motion";
|
||||
|
||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,6 @@
|
|||
"use client";
|
||||
import { useRouter } from 'next/navigation';
|
||||
import React from "react";
|
||||
import { Header } from "../../components/UI/Header";
|
||||
import Layout from "../../components/UI/Layout";
|
||||
import { Title } from "../../components/UI/Elements/Styles/Title";
|
||||
import { loginAndGetToken } from "../../services/auth/auth";
|
||||
|
||||
|
|
@ -35,7 +33,6 @@ const Login = () => {
|
|||
return (
|
||||
<div>
|
||||
< >
|
||||
<Header></Header>
|
||||
<Title>Login</Title>
|
||||
|
||||
<form>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import Layout from "../../../components/UI/Layout";
|
||||
import { Title } from "../../../components/UI/Elements/Styles/Title";
|
||||
import { createNewOrganization } from "../../../services/organizations/orgs";
|
||||
|
||||
|
|
@ -34,7 +33,7 @@ const Organizations = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Layout>
|
||||
<div>
|
||||
<Title>New Organization</Title>
|
||||
Name: <input onChange={handleNameChange} type="text" />
|
||||
<br />
|
||||
|
|
@ -45,7 +44,7 @@ const Organizations = () => {
|
|||
Email Address: <input onChange={handleEmailChange} type="text" />
|
||||
<br />
|
||||
<button onClick={handleSubmit}>Create</button>
|
||||
</Layout>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,12 +1,12 @@
|
|||
"use client"; //todo: use server components
|
||||
import Link from "next/link";
|
||||
import React from "react";
|
||||
import Layout from "../../components/UI/Layout";
|
||||
import { Title } from "../../components/UI/Elements/Styles/Title";
|
||||
import { deleteOrganizationFromBackend } from "@services/organizations/orgs";
|
||||
import useSWR, { mutate } from "swr";
|
||||
import { swrFetcher } from "@services/utils/requests";
|
||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
||||
import AuthProvider from "@components/Security/AuthProvider";
|
||||
|
||||
const Organizations = () => {
|
||||
const { data : organizations , error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher)
|
||||
|
|
@ -18,6 +18,7 @@ const Organizations = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<AuthProvider/>
|
||||
<Title>
|
||||
Your Organizations{" "}
|
||||
<Link href={"/organizations/new"}>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,5 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import { Header } from "../../components/UI/Header";
|
||||
import Layout from "../../components/UI/Layout";
|
||||
import { Title } from "../../components/UI/Elements/Styles/Title";
|
||||
import { signup } from "../../services/auth/auth";
|
||||
|
||||
|
|
@ -31,8 +29,7 @@ const SignUp = () => {
|
|||
|
||||
return (
|
||||
<div>
|
||||
<Layout title="Sign up">
|
||||
<Header></Header>
|
||||
<div title="Sign up">
|
||||
<Title>Sign up </Title>
|
||||
|
||||
<form>
|
||||
|
|
@ -43,7 +40,7 @@ const SignUp = () => {
|
|||
Sign up
|
||||
</button>
|
||||
</form>
|
||||
</Layout>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import { ArrowLeftIcon, Cross1Icon } from "@radix-ui/react-icons";
|
||||
import Modal from "../Modal";
|
||||
import styled from "styled-components";
|
||||
import DynamicCanvaModal from "./NewActivityModal/DynamicCanva";
|
||||
import VideoModal from "./NewActivityModal/Video";
|
||||
|
||||
function NewActivityModal({ closeModal, submitActivity, submitFileActivity, chapterId }: any) {
|
||||
const [selectedView, setSelectedView] = useState("home");
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<button onClick={ () => {setSelectedView("home")}}>
|
||||
<ArrowLeftIcon />
|
||||
</button>
|
||||
<button onClick={closeModal}>
|
||||
<Cross1Icon />
|
||||
</button>
|
||||
<h1>Add New Activity</h1>
|
||||
<br />
|
||||
|
||||
{selectedView === "home" && (
|
||||
<ActivityChooserWrapper>
|
||||
<ActivityButton onClick={() => {setSelectedView("dynamic")}}>✨📄</ActivityButton>
|
||||
<ActivityButton onClick={() => {setSelectedView("video")}}>📹</ActivityButton>
|
||||
</ActivityChooserWrapper>
|
||||
)}
|
||||
|
||||
{selectedView === "dynamic" && (
|
||||
<DynamicCanvaModal submitActivity={submitActivity} chapterId={chapterId} />
|
||||
)}
|
||||
|
||||
{selectedView === "video" && (
|
||||
<VideoModal submitFileActivity={submitFileActivity} chapterId={chapterId} />
|
||||
)}
|
||||
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
const ActivityChooserWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 20px;
|
||||
`;
|
||||
|
||||
const ActivityButton = styled.button`
|
||||
padding: 40px;
|
||||
border-radius: 10px !important;
|
||||
border: none;
|
||||
font-size: 80px !important;
|
||||
margin: 40px;
|
||||
background-color: #8c949c33 !important;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
background-color: #8c949c7b;
|
||||
}
|
||||
`;
|
||||
|
||||
export default NewActivityModal;
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
|
||||
function DynamicCanvaModal({ submitActivity, chapterId }: any) {
|
||||
const [activityName, setActivityName] = useState("");
|
||||
const [activityDescription, setActivityDescription] = useState("");
|
||||
|
||||
const handleActivityNameChange = (e: any) => {
|
||||
setActivityName(e.target.value);
|
||||
};
|
||||
|
||||
const handleActivityDescriptionChange = (e: any) => {
|
||||
setActivityDescription(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
console.log({ activityName, activityDescription, chapterId });
|
||||
submitActivity({
|
||||
name: activityName,
|
||||
chapterId: chapterId,
|
||||
type: "dynamic",
|
||||
});
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
<div>
|
||||
<input type="text" onChange={handleActivityNameChange} placeholder="Activity Name" /> <br />
|
||||
<input type="text" onChange={handleActivityDescriptionChange} placeholder="Activity Description" />
|
||||
<br />
|
||||
<button onClick={handleSubmit}>Add Activity</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicCanvaModal;
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
import React from "react";
|
||||
|
||||
function VideoModal({ submitFileActivity, chapterId }: any) {
|
||||
const [video, setVideo] = React.useState(null) as any;
|
||||
const [name, setName] = React.useState("");
|
||||
|
||||
const handleVideoChange = (event: React.ChangeEvent<any>) => {
|
||||
setVideo(event.target.files[0]);
|
||||
};
|
||||
|
||||
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(event.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
let status = await submitFileActivity(video, "video", { name, type: "video" }, chapterId);
|
||||
};
|
||||
|
||||
/* TODO : implement some sort of progress bar for file uploads, it is not possible yet because i'm not using axios.
|
||||
and the actual upload isn't happening here anyway, it's in the submitFileActivity function */
|
||||
|
||||
return (
|
||||
<div>
|
||||
<input type="text" placeholder="video title" onChange={handleNameChange} />
|
||||
<br />
|
||||
<br />
|
||||
<input type="file" onChange={handleVideoChange} name="video" id="" />
|
||||
<br />
|
||||
|
||||
<br />
|
||||
<button onClick={handleSubmit}>Send</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default VideoModal;
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import React, { useState } from "react";
|
||||
import Modal from "../Modal";
|
||||
|
||||
function NewChapterModal({ submitChapter , closeModal }: any) {
|
||||
const [chapterName, setChapterName] = useState("");
|
||||
const [chapterDescription, setChapterDescription] = useState("");
|
||||
|
||||
const handleChapterNameChange = (e: any) => {
|
||||
setChapterName(e.target.value);
|
||||
};
|
||||
|
||||
const handleChapterDescriptionChange = (e: any) => {
|
||||
setChapterDescription(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
console.log({ chapterName, chapterDescription });
|
||||
submitChapter({ name : chapterName, description : chapterDescription , activities : [] });
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal>
|
||||
<h1>Add New Chapter <button onClick={closeModal}>X</button></h1>
|
||||
<input type="text" onChange={handleChapterNameChange} placeholder="Chapter Name" /> <br />
|
||||
<input type="text" onChange={handleChapterDescriptionChange} placeholder="Chapter Description" />
|
||||
<br />
|
||||
<button onClick={handleSubmit}>Add Chapter</button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewChapterModal;
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
|
||||
function Modal(props: any) {
|
||||
return (
|
||||
<div>
|
||||
<Overlay>
|
||||
<AnimatePresence>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, left: "50%", top: "50%", scale: 0.9, backdropFilter: "blur(10px)", y: -1, position: "absolute" }}
|
||||
animate={{ opacity: 1, left: "50%", top: "50%", scale: 1, backdropFilter: "blur(10px)", y: 0, position: "absolute" }}
|
||||
key="modal"
|
||||
transition={{
|
||||
type: "spring",
|
||||
stiffness: 360,
|
||||
damping: 70,
|
||||
delay: 0.02,
|
||||
}}
|
||||
exit={{ opacity: 0, left: "50%", top: "46%", backdropFilter: "blur(10px)", y: -1, position: "absolute" }}
|
||||
>
|
||||
<Content>{props.children}</Content>
|
||||
</motion.div>
|
||||
</AnimatePresence>
|
||||
</Overlay>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const Overlay = styled.div`
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: 100;
|
||||
background-color: #00000029;
|
||||
backdrop-filter: blur(1px);
|
||||
|
||||
`;
|
||||
|
||||
const Content = styled.div`
|
||||
background-color: white;
|
||||
border-radius: 5px;
|
||||
padding: 20px;
|
||||
width: 500px;
|
||||
height: 500px;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
box-shadow: 0px 64px 84px 15px rgb(0 0 0 / 10%);
|
||||
`;
|
||||
export default Modal;
|
||||
|
|
@ -21,8 +21,11 @@ function Chapter(props: any) {
|
|||
props.openNewActivityModal(props.info.list.chapter.id);
|
||||
}}
|
||||
>
|
||||
|
||||
Create Activity
|
||||
</button>
|
||||
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
props.deleteChapter(props.info.list.chapter.id);
|
||||
106
front/components/Pages/CourseEdit/NewActivity.tsx
Normal file
106
front/components/Pages/CourseEdit/NewActivity.tsx
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
import React, { useState } from "react";
|
||||
import { ArrowLeftIcon, Cross1Icon } from "@radix-ui/react-icons";
|
||||
import DynamicPageActivityImage from "public/activities_types/dynamic-page-activity.png";
|
||||
import VideoPageActivityImage from "public//activities_types/video-page-activity.png";
|
||||
import { styled, keyframes } from '@stitches/react';
|
||||
import DynamicCanvaModal from "./NewActivityModal/DynamicCanva";
|
||||
import VideoModal from "./NewActivityModal/Video";
|
||||
import Image from "next/image";
|
||||
|
||||
function NewActivityModal({ closeModal, submitActivity, submitFileActivity, chapterId }: any) {
|
||||
const [selectedView, setSelectedView] = useState("home");
|
||||
|
||||
|
||||
return (
|
||||
<div>
|
||||
{selectedView === "home" && (
|
||||
<ActivityChooserWrapper>
|
||||
<ActivityOption onClick={() => { setSelectedView("dynamic") }}>
|
||||
<ActivityTypeImage>
|
||||
<Image alt="Dynamic Page" src={DynamicPageActivityImage}></Image>
|
||||
</ActivityTypeImage>
|
||||
<ActivityTypeTitle>Dynamic Page</ActivityTypeTitle>
|
||||
</ActivityOption>
|
||||
<ActivityOption onClick={() => { setSelectedView("video") }}>
|
||||
<ActivityTypeImage>
|
||||
<Image alt="Video Page" src={VideoPageActivityImage}></Image>
|
||||
</ActivityTypeImage>
|
||||
<ActivityTypeTitle>Video Page</ActivityTypeTitle>
|
||||
</ActivityOption>
|
||||
<ActivityOption onClick={() => { setSelectedView("video") }}>
|
||||
<ActivityTypeImage>
|
||||
<Image alt="Video Page" src={VideoPageActivityImage}></Image>
|
||||
</ActivityTypeImage>
|
||||
<ActivityTypeTitle>Video Page</ActivityTypeTitle>
|
||||
</ActivityOption>
|
||||
</ActivityChooserWrapper>
|
||||
)}
|
||||
|
||||
{selectedView === "dynamic" && (
|
||||
<DynamicCanvaModal submitActivity={submitActivity} chapterId={chapterId} />
|
||||
)}
|
||||
|
||||
{selectedView === "video" && (
|
||||
<VideoModal submitFileActivity={submitFileActivity} chapterId={chapterId} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const ActivityChooserWrapper = styled("div", {
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
justifyContent: "start",
|
||||
marginTop: 10,
|
||||
});
|
||||
|
||||
const ActivityOption = styled("div", {
|
||||
width: "180px",
|
||||
textAlign: "center",
|
||||
borderRadius: 10,
|
||||
background: "#F6F6F6",
|
||||
border: "4px solid #F5F5F5",
|
||||
margin: "auto",
|
||||
|
||||
// hover
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
background: "#ededed",
|
||||
border: "4px solid #ededed",
|
||||
|
||||
transition: "background 0.2s ease-in-out, border 0.2s ease-in-out",
|
||||
},
|
||||
});
|
||||
|
||||
const ActivityTypeImage = styled("div", {
|
||||
height: 80,
|
||||
borderRadius: 8,
|
||||
margin: 2,
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
justifyContent: "end",
|
||||
textAlign: "center",
|
||||
background: "#ffffff",
|
||||
|
||||
// hover
|
||||
"&:hover": {
|
||||
cursor: "pointer",
|
||||
},
|
||||
});
|
||||
|
||||
const ActivityTypeTitle = styled("div", {
|
||||
display: "flex",
|
||||
fontSize: 12,
|
||||
height: "20px",
|
||||
fontWeight: 500,
|
||||
color: "rgba(0, 0, 0, 0.38);",
|
||||
|
||||
// center text vertically
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
textAlign: "center",
|
||||
|
||||
});
|
||||
|
||||
export default NewActivityModal;
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, FormMessage, Input, Textarea } from "@components/UI/Form/Form";
|
||||
import React, { useState } from "react";
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
|
||||
function DynamicCanvaModal({ submitActivity, chapterId }: any) {
|
||||
const [activityName, setActivityName] = useState("");
|
||||
const [activityDescription, setActivityDescription] = useState("");
|
||||
|
||||
const handleActivityNameChange = (e: any) => {
|
||||
setActivityName(e.target.value);
|
||||
};
|
||||
|
||||
const handleActivityDescriptionChange = (e: any) => {
|
||||
setActivityDescription(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
console.log({ activityName, activityDescription, chapterId });
|
||||
submitActivity({
|
||||
name: activityName,
|
||||
chapterId: chapterId,
|
||||
type: "dynamic",
|
||||
});
|
||||
};
|
||||
return (
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="dynamic-activity-name">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Activity name</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a name for your activity</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input onChange={handleActivityNameChange} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<FormField name="dynamic-activity-desc">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Activity description</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a description for your activity</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Textarea onChange={handleActivityDescriptionChange} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>Create Activity</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default DynamicCanvaModal;
|
||||
55
front/components/Pages/CourseEdit/NewActivityModal/Video.tsx
Normal file
55
front/components/Pages/CourseEdit/NewActivityModal/Video.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import FormLayout, { ButtonBlack, Flex, FormField, FormLabel, FormMessage, Input, Textarea } from "@components/UI/Form/Form";
|
||||
import React, { useState } from "react";
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
|
||||
function VideoModal({ submitFileActivity, chapterId }: any) {
|
||||
const [video, setVideo] = React.useState(null) as any;
|
||||
const [name, setName] = React.useState("");
|
||||
|
||||
const handleVideoChange = (event: React.ChangeEvent<any>) => {
|
||||
setVideo(event.target.files[0]);
|
||||
};
|
||||
|
||||
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setName(event.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
let status = await submitFileActivity(video, "video", { name, type: "video" }, chapterId);
|
||||
};
|
||||
|
||||
/* TODO : implement some sort of progress bar for file uploads, it is not possible yet because i'm not using axios.
|
||||
and the actual upload isn't happening here anyway, it's in the submitFileActivity function */
|
||||
|
||||
return (
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="video-activity-name">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Video name</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a name for your video activity</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input onChange={handleNameChange} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<FormField name="video-activity-file">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Video file</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a video for your activity</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<input type="file" onChange={handleVideoChange} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>Create Activity</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default VideoModal;
|
||||
55
front/components/Pages/CourseEdit/NewChapter.tsx
Normal file
55
front/components/Pages/CourseEdit/NewChapter.tsx
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
import FormLayout, { Flex, FormField, Input, Textarea, FormLabel, ButtonBlack } from "@components/UI/Form/Form";
|
||||
import { FormMessage } from "@radix-ui/react-form";
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import React, { useState } from "react";
|
||||
|
||||
function NewChapterModal({ submitChapter, closeModal }: any) {
|
||||
const [chapterName, setChapterName] = useState("");
|
||||
const [chapterDescription, setChapterDescription] = useState("");
|
||||
|
||||
const handleChapterNameChange = (e: any) => {
|
||||
setChapterName(e.target.value);
|
||||
};
|
||||
|
||||
const handleChapterDescriptionChange = (e: any) => {
|
||||
setChapterDescription(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = async (e: any) => {
|
||||
e.preventDefault();
|
||||
console.log({ chapterName, chapterDescription });
|
||||
submitChapter({ name: chapterName, description: chapterDescription, activities: [] });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
<FormLayout onSubmit={handleSubmit}>
|
||||
<FormField name="chapter-name">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Chapter name</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a chapter name</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Input onChange={handleChapterNameChange} type="text" required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
<FormField name="chapter-desc">
|
||||
<Flex css={{ alignItems: 'baseline', justifyContent: 'space-between' }}>
|
||||
<FormLabel>Chapter description</FormLabel>
|
||||
<FormMessage match="valueMissing">Please provide a chapter description</FormMessage>
|
||||
</Flex>
|
||||
<Form.Control asChild>
|
||||
<Textarea onChange={handleChapterDescriptionChange} required />
|
||||
</Form.Control>
|
||||
</FormField>
|
||||
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Form.Submit asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>Create chapter</ButtonBlack>
|
||||
</Form.Submit>
|
||||
</Flex>
|
||||
</FormLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export default NewChapterModal;
|
||||
|
|
@ -5,7 +5,9 @@ import { useRouter, usePathname } from "next/navigation";
|
|||
|
||||
export const AuthContext: any = React.createContext({});
|
||||
|
||||
const NON_AUTHENTICATED_ROUTES = ["/login", "/signup"];
|
||||
const PRIVATE_ROUTES = ["/course/*/edit", "/settings*", "/trail"];
|
||||
const NON_AUTHENTICATED_ROUTES = ["/login", "/register"];
|
||||
|
||||
export interface Auth {
|
||||
access_token: string;
|
||||
isAuthenticated: boolean;
|
||||
|
|
@ -15,6 +17,8 @@ export interface Auth {
|
|||
|
||||
const AuthProvider = ({ children }: any) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
|
||||
const [auth, setAuth] = React.useState<Auth>({ access_token: "", isAuthenticated: false, userInfo: {}, isLoading: true });
|
||||
|
||||
async function checkRefreshToken() {
|
||||
|
|
@ -24,6 +28,7 @@ const AuthProvider = ({ children }: any) => {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
async function checkAuth() {
|
||||
try {
|
||||
let access_token = await checkRefreshToken();
|
||||
|
|
@ -34,13 +39,24 @@ const AuthProvider = ({ children }: any) => {
|
|||
userInfo = await getUserInfo(access_token);
|
||||
setAuth({ access_token, isAuthenticated: true, userInfo, isLoading });
|
||||
|
||||
|
||||
// Redirect to home if user is trying to access a NON_AUTHENTICATED_ROUTES route
|
||||
|
||||
if (NON_AUTHENTICATED_ROUTES.some((route) => new RegExp(`^${route.replace("*", ".*")}$`).test(pathname))) {
|
||||
router.push("/");
|
||||
}
|
||||
|
||||
|
||||
} else {
|
||||
setAuth({ access_token, isAuthenticated: false, userInfo, isLoading });
|
||||
//router.push("/login");
|
||||
|
||||
// Redirect to login if user is trying to access a private route
|
||||
if (PRIVATE_ROUTES.some((route) => new RegExp(`^${route.replace("*", ".*")}$`).test(pathname))) {
|
||||
router.push("/login");
|
||||
}
|
||||
|
||||
}
|
||||
} catch (error) {
|
||||
router.push("/");
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,13 +6,9 @@ import learnhouseIcon from "public/learnhouse_icon.png";
|
|||
import learnhouseLogo from "public/learnhouse_logo.png";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { useRouter, useSearchParams, usePathname } from "next/navigation";
|
||||
import { headers } from "next/headers";
|
||||
import { getOrgFromUri, getUriWithOrg } from "@services/config/config";
|
||||
import { getUriWithOrg } from "@services/config/config";
|
||||
|
||||
export const Menu = (props : any ) => {
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
const orgslug = props.orgslug;
|
||||
|
||||
|
||||
|
|
|
|||
86
front/components/UI/Form/Form.tsx
Normal file
86
front/components/UI/Form/Form.tsx
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
import React from 'react';
|
||||
import * as Form from '@radix-ui/react-form';
|
||||
import { styled, keyframes } from '@stitches/react';
|
||||
import { blackA, violet, mauve } from '@radix-ui/colors';
|
||||
|
||||
const FormLayout = (props: any, onSubmit : any ) => (
|
||||
<FormRoot onSubmit={props.onSubmit}>
|
||||
{props.children}
|
||||
</FormRoot>
|
||||
);
|
||||
|
||||
export const FormRoot = styled(Form.Root, {
|
||||
margin: 7
|
||||
});
|
||||
|
||||
export const FormField = styled(Form.Field, {
|
||||
display: 'grid',
|
||||
marginBottom: 10,
|
||||
});
|
||||
|
||||
export const FormLabel = styled(Form.Label, {
|
||||
fontSize: 15,
|
||||
fontWeight: 500,
|
||||
lineHeight: '35px',
|
||||
color: 'black',
|
||||
});
|
||||
|
||||
export const FormMessage = styled(Form.Message, {
|
||||
fontSize: 13,
|
||||
color: 'white',
|
||||
opacity: 0.8,
|
||||
});
|
||||
|
||||
export const Flex = styled('div', { display: 'flex' });
|
||||
|
||||
export const inputStyles = {
|
||||
all: 'unset',
|
||||
boxSizing: 'border-box',
|
||||
width: '100%',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 4,
|
||||
fontSize: 15,
|
||||
color: '#7c7c7c',
|
||||
background: "#F9FAFB",
|
||||
boxShadow: `0 0 0 1px #edeeef`,
|
||||
'&:hover': { boxShadow: `0 0 0 1px #edeeef` },
|
||||
'&:focus': { boxShadow: `0 0 0 2px #edeeef` },
|
||||
'&::selection': { backgroundColor: blackA.blackA9, color: 'white' },
|
||||
|
||||
};
|
||||
|
||||
export const Input = styled('input', {
|
||||
...inputStyles,
|
||||
height: 35,
|
||||
lineHeight: 1,
|
||||
padding: '0 10px',
|
||||
border: 'none',
|
||||
});
|
||||
|
||||
export const Textarea = styled('textarea', {
|
||||
...inputStyles,
|
||||
resize: 'none',
|
||||
padding: 10,
|
||||
});
|
||||
|
||||
export const ButtonBlack = styled('button', {
|
||||
all: 'unset',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 8,
|
||||
padding: '0 15px',
|
||||
fontSize: 15,
|
||||
lineHeight: 1,
|
||||
fontWeight: 500,
|
||||
height: 35,
|
||||
|
||||
background: "#000000",
|
||||
color: "#FFFFFF",
|
||||
'&:hover': { backgroundColor: "#181818" , cursor: "pointer"},
|
||||
'&:focus': { boxShadow: `0 0 0 2px black` },
|
||||
});
|
||||
|
||||
export default FormLayout;
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
import React from "react";
|
||||
import Link from 'next/link'
|
||||
import styled from "styled-components";
|
||||
|
||||
|
||||
export const Header = () => {
|
||||
return (
|
||||
<div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
|
|
@ -1,66 +0,0 @@
|
|||
"use client";
|
||||
import React from "react";
|
||||
import Head from "next/head";
|
||||
import styled from "styled-components";
|
||||
import AuthProvider from "../Security/AuthProvider";
|
||||
import { motion } from "framer-motion";
|
||||
import { Menu } from "./Elements/Menu";
|
||||
|
||||
const Layout = (props: any) => {
|
||||
const variants = {
|
||||
hidden: { opacity: 0, x: 0, y: 0 },
|
||||
enter: { opacity: 1, x: 0, y: 0 },
|
||||
exit: { opacity: 0, x: 0, y: 0 },
|
||||
};
|
||||
|
||||
return (
|
||||
<AuthProvider>
|
||||
<ProjectPhaseLabel>🚧 Dev Phase</ProjectPhaseLabel>
|
||||
<Menu orgslug={props.orgslug}></Menu>
|
||||
<motion.main
|
||||
variants={variants} // Pass the variant object into Framer Motion
|
||||
initial="hidden" // Set the initial state to variants.hidden
|
||||
animate="enter" // Animated state to variants.enter
|
||||
exit="exit" // Exit state (used later) to variants.exit
|
||||
transition={{ type: "linear" }} // Set the transition to linear
|
||||
className=""
|
||||
>
|
||||
<Main className="min-h-screen">{props.children}</Main>
|
||||
</motion.main>
|
||||
<Footer>
|
||||
<p>LearnHouse © 2021 - {new Date().getFullYear()} - All rights reserved</p>
|
||||
</Footer>
|
||||
</AuthProvider>
|
||||
);
|
||||
};
|
||||
|
||||
const Main = styled.main`
|
||||
min-height: 100vh;
|
||||
`;
|
||||
|
||||
const Footer = styled.footer`
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
margin: 20px;
|
||||
font-size: 16px;
|
||||
|
||||
img {
|
||||
width: 20px;
|
||||
opacity: 0.4;
|
||||
display: inline;
|
||||
}
|
||||
`;
|
||||
|
||||
export const ProjectPhaseLabel = styled.div`
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
padding: 9px;
|
||||
background-color: #080501;
|
||||
color: white;
|
||||
font-size: 19px;
|
||||
font-weight: bold;
|
||||
border-radius: 5px 0 0 0px;
|
||||
`;
|
||||
|
||||
export default Layout;
|
||||
243
front/components/UI/Modal/Modal.tsx
Normal file
243
front/components/UI/Modal/Modal.tsx
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import React from 'react';
|
||||
import * as Dialog from '@radix-ui/react-dialog';
|
||||
import { styled, keyframes } from '@stitches/react';
|
||||
import { violet, blackA, mauve, green } from '@radix-ui/colors';
|
||||
import { ButtonBlack } from '../Form/Form';
|
||||
|
||||
type ModalParams = {
|
||||
dialogTitle: string;
|
||||
dialogDescription: string;
|
||||
dialogContent: React.ReactNode;
|
||||
dialogClose?: React.ReactNode | null;
|
||||
dialogTrigger?: React.ReactNode;
|
||||
addDefCloseButton?: boolean;
|
||||
onOpenChange: any;
|
||||
isDialogOpen?: boolean;
|
||||
minHeight?: "sm" | "md" | "lg" | "xl" | "no-min";
|
||||
};
|
||||
|
||||
const Modal = (params: ModalParams) => (
|
||||
<Dialog.Root open={params.isDialogOpen} onOpenChange={params.onOpenChange}>
|
||||
{params.dialogTrigger ? (
|
||||
<Dialog.Trigger asChild>
|
||||
{params.dialogTrigger}
|
||||
</Dialog.Trigger>
|
||||
) : null}
|
||||
|
||||
<Dialog.Portal>
|
||||
<DialogOverlay />
|
||||
<DialogContent minHeight={params.minHeight}>
|
||||
<DialogTopBar>
|
||||
<DialogTitle>{params.dialogTitle}</DialogTitle>
|
||||
<DialogDescription>
|
||||
{params.dialogDescription}
|
||||
</DialogDescription>
|
||||
</DialogTopBar>
|
||||
{params.dialogContent}
|
||||
{params.dialogClose ? (
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Dialog.Close asChild>
|
||||
{params.dialogClose}
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
) : null}
|
||||
{params.addDefCloseButton ? (
|
||||
<Flex css={{ marginTop: 25, justifyContent: 'flex-end' }}>
|
||||
<Dialog.Close asChild>
|
||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>Close</ButtonBlack>
|
||||
</Dialog.Close>
|
||||
</Flex>
|
||||
) : null}
|
||||
|
||||
</DialogContent>
|
||||
|
||||
</Dialog.Portal>
|
||||
</Dialog.Root>
|
||||
);
|
||||
|
||||
const overlayShow = keyframes({
|
||||
'0%': { opacity: 0 },
|
||||
'100%': { opacity: 1 },
|
||||
});
|
||||
|
||||
const overlayClose = keyframes({
|
||||
'0%': { opacity: 1 },
|
||||
'100%': { opacity: 0 },
|
||||
});
|
||||
|
||||
const contentShow = keyframes({
|
||||
'0%': { opacity: 0, transform: 'translate(-50%, -50%) scale(.96)' },
|
||||
'100%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
});
|
||||
|
||||
const contentClose = keyframes({
|
||||
'0%': { opacity: 1, transform: 'translate(-50%, -50%) scale(1)' },
|
||||
'100%': { opacity: 0, transform: 'translate(-50%, -52%) scale(.96)' },
|
||||
});
|
||||
|
||||
const DialogOverlay = styled(Dialog.Overlay, {
|
||||
backgroundColor: blackA.blackA9,
|
||||
position: 'fixed',
|
||||
|
||||
inset: 0,
|
||||
animation: `${overlayShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${overlayClose} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
});
|
||||
|
||||
const DialogContent = styled(Dialog.Content, {
|
||||
|
||||
variants: {
|
||||
minHeight: {
|
||||
'no-min': {
|
||||
minHeight: '0px',
|
||||
},
|
||||
'sm': {
|
||||
minHeight: '300px',
|
||||
},
|
||||
'md': {
|
||||
minHeight: '500px',
|
||||
},
|
||||
'lg': {
|
||||
minHeight: '700px',
|
||||
},
|
||||
'xl': {
|
||||
minHeight: '900px',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
backgroundColor: 'white',
|
||||
borderRadius: 18,
|
||||
boxShadow: 'hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px',
|
||||
position: 'fixed',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate(-50%, -50%)',
|
||||
width: '90vw',
|
||||
overflow: 'hidden',
|
||||
maxHeight: '85vh',
|
||||
minHeight: '300px',
|
||||
maxWidth: '600px',
|
||||
padding: 11,
|
||||
animation: `${contentShow} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
'&:focus': { outline: 'none' },
|
||||
|
||||
'&[data-state="closed"]': {
|
||||
animation: `${contentClose} 150ms cubic-bezier(0.16, 1, 0.3, 1)`,
|
||||
},
|
||||
transition: "max-height 0.3s ease-out",
|
||||
});
|
||||
|
||||
const DialogTopBar = styled('div', {
|
||||
background: "#F7F7F7",
|
||||
padding: "8px 14px ",
|
||||
borderRadius: 14,
|
||||
});
|
||||
const DialogTitle = styled(Dialog.Title, {
|
||||
margin: 0,
|
||||
fontWeight: 700,
|
||||
letterSpacing: "-0.05em",
|
||||
padding: 0,
|
||||
color: mauve.mauve12,
|
||||
fontSize: 21,
|
||||
});
|
||||
|
||||
const DialogDescription = styled(Dialog.Description, {
|
||||
color: mauve.mauve11,
|
||||
letterSpacing: "-0.03em",
|
||||
fontSize: 15,
|
||||
padding: 0,
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
const Flex = styled('div', { display: 'flex' });
|
||||
|
||||
const Button = styled('button', {
|
||||
all: 'unset',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 4,
|
||||
padding: '0 15px',
|
||||
fontSize: 15,
|
||||
lineHeight: 1,
|
||||
fontWeight: 500,
|
||||
height: 35,
|
||||
|
||||
variants: {
|
||||
variant: {
|
||||
violet: {
|
||||
backgroundColor: 'white',
|
||||
color: violet.violet11,
|
||||
boxShadow: `0 2px 10px ${blackA.blackA7}`,
|
||||
'&:hover': { backgroundColor: mauve.mauve3 },
|
||||
'&:focus': { boxShadow: `0 0 0 2px black` },
|
||||
},
|
||||
green: {
|
||||
backgroundColor: green.green4,
|
||||
color: green.green11,
|
||||
'&:hover': { backgroundColor: green.green5 },
|
||||
'&:focus': { boxShadow: `0 0 0 2px ${green.green7}` },
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
defaultVariants: {
|
||||
variant: 'violet',
|
||||
},
|
||||
});
|
||||
|
||||
const IconButton = styled('button', {
|
||||
all: 'unset',
|
||||
fontFamily: 'inherit',
|
||||
borderRadius: '100%',
|
||||
height: 25,
|
||||
width: 25,
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
color: violet.violet11,
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
right: 10,
|
||||
|
||||
'&:hover': { backgroundColor: violet.violet4 },
|
||||
'&:focus': { boxShadow: `0 0 0 2px ${violet.violet7}` },
|
||||
});
|
||||
|
||||
const Fieldset = styled('fieldset', {
|
||||
all: 'unset',
|
||||
display: 'flex',
|
||||
gap: 20,
|
||||
alignItems: 'center',
|
||||
marginBottom: 15,
|
||||
});
|
||||
|
||||
const Label = styled('label', {
|
||||
fontSize: 15,
|
||||
color: violet.violet11,
|
||||
width: 90,
|
||||
textAlign: 'right',
|
||||
});
|
||||
|
||||
const Input = styled('input', {
|
||||
all: 'unset',
|
||||
width: '100%',
|
||||
flex: '1',
|
||||
display: 'inline-flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: 4,
|
||||
padding: '0 10px',
|
||||
fontSize: 15,
|
||||
lineHeight: 1,
|
||||
color: violet.violet11,
|
||||
boxShadow: `0 0 0 1px ${violet.violet7}`,
|
||||
height: 35,
|
||||
|
||||
'&:focus': { boxShadow: `0 0 0 2px ${violet.violet8}` },
|
||||
});
|
||||
|
||||
export default Modal;
|
||||
114
front/package-lock.json
generated
114
front/package-lock.json
generated
|
|
@ -8,7 +8,9 @@
|
|||
"name": "learnhouse",
|
||||
"version": "0.1.0",
|
||||
"dependencies": {
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-form": "^0.0.2",
|
||||
"@radix-ui/react-icons": "^1.1.1",
|
||||
"@stitches/react": "^1.2.8",
|
||||
"@tiptap/extension-collaboration": "^2.0.0-beta.199",
|
||||
|
|
@ -2352,6 +2354,11 @@
|
|||
"url": "https://opencollective.com/popperjs"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/colors": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz",
|
||||
"integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw=="
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
|
||||
|
|
@ -2451,6 +2458,37 @@
|
|||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-form": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.0.2.tgz",
|
||||
"integrity": "sha512-+WQU4Gs4MqjYsHwh5d19Ka4CMcWeXd7WPuWYCYGtNbDRMHFG2TtgM9PlEK4Yrk7wG1f5/da6Bgtteky2ggDXUg==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-id": "1.0.0",
|
||||
"@radix-ui/react-label": "2.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
|
||||
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-icons": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.1.1.tgz",
|
||||
|
|
@ -2471,6 +2509,32 @@
|
|||
"react": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.1.tgz",
|
||||
"integrity": "sha512-qcfbS3B8hTYmEO44RNcXB6pegkxRsJIbdxTMu0PEX0Luv5O2DvTIwwVYxQfUwLpM88EL84QRPLOLgwUSApMsLQ==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-primitive": "1.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
|
||||
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8 || ^17.0 || ^18.0",
|
||||
"react-dom": "^16.8 || ^17.0 || ^18.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@radix-ui/react-portal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz",
|
||||
|
|
@ -8906,6 +8970,11 @@
|
|||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
|
||||
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw=="
|
||||
},
|
||||
"@radix-ui/colors": {
|
||||
"version": "0.1.8",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-0.1.8.tgz",
|
||||
"integrity": "sha512-jwRMXYwC0hUo0mv6wGpuw254Pd9p/R6Td5xsRpOmaWkUHlooNWqVcadgyzlRumMq3xfOTXwJReU0Jv+EIy4Jbw=="
|
||||
},
|
||||
"@radix-ui/primitive": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz",
|
||||
|
|
@ -8984,6 +9053,31 @@
|
|||
"@radix-ui/react-use-callback-ref": "1.0.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-form": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.0.2.tgz",
|
||||
"integrity": "sha512-+WQU4Gs4MqjYsHwh5d19Ka4CMcWeXd7WPuWYCYGtNbDRMHFG2TtgM9PlEK4Yrk7wG1f5/da6Bgtteky2ggDXUg==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/primitive": "1.0.0",
|
||||
"@radix-ui/react-compose-refs": "1.0.0",
|
||||
"@radix-ui/react-context": "1.0.0",
|
||||
"@radix-ui/react-id": "1.0.0",
|
||||
"@radix-ui/react-label": "2.0.1",
|
||||
"@radix-ui/react-primitive": "1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
|
||||
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-icons": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.1.1.tgz",
|
||||
|
|
@ -8999,6 +9093,26 @@
|
|||
"@radix-ui/react-use-layout-effect": "1.0.0"
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-label": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.1.tgz",
|
||||
"integrity": "sha512-qcfbS3B8hTYmEO44RNcXB6pegkxRsJIbdxTMu0PEX0Luv5O2DvTIwwVYxQfUwLpM88EL84QRPLOLgwUSApMsLQ==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-primitive": "1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/react-primitive": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.2.tgz",
|
||||
"integrity": "sha512-zY6G5Qq4R8diFPNwtyoLRZBxzu1Z+SXMlfYpChN7Dv8gvmx9X3qhDqiLWvKseKVJMuedFeU/Sa0Sy/Ia+t06Dw==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.13.10",
|
||||
"@radix-ui/react-slot": "1.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"@radix-ui/react-portal": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz",
|
||||
|
|
|
|||
|
|
@ -9,7 +9,9 @@
|
|||
"lint": "next lint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@radix-ui/colors": "^0.1.8",
|
||||
"@radix-ui/react-dialog": "^1.0.2",
|
||||
"@radix-ui/react-form": "^0.0.2",
|
||||
"@radix-ui/react-icons": "^1.1.1",
|
||||
"@stitches/react": "^1.2.8",
|
||||
"@tiptap/extension-collaboration": "^2.0.0-beta.199",
|
||||
|
|
|
|||
BIN
front/public/activities_types/dynamic-page-activity.png
Normal file
BIN
front/public/activities_types/dynamic-page-activity.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 512 B |
BIN
front/public/activities_types/video-page-activity.png
Normal file
BIN
front/public/activities_types/video-page-activity.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 586 B |
|
|
@ -1,4 +1,4 @@
|
|||
import { initialData } from "../../components/Drags/data";
|
||||
import { initialData } from "../../components/Pages/CourseEdit/Draggables/data";
|
||||
import { getAPIUrl } from "@services/config/config";
|
||||
import { RequestBody } from "@services/utils/requests";
|
||||
|
||||
|
|
@ -9,10 +9,15 @@ import { RequestBody } from "@services/utils/requests";
|
|||
|
||||
//TODO : depreciate this function
|
||||
export async function getCourseChaptersMetadata(course_id: any) {
|
||||
const data: any = await fetch(`${getAPIUrl()}chapters/meta/course_${course_id}`, RequestBody("GET", null))
|
||||
.then((result) => result.json())
|
||||
.catch((error) => console.log("error", error));
|
||||
const response = await fetch(`${getAPIUrl()}chapters/meta/course_${course_id}`, RequestBody("GET", null));
|
||||
|
||||
if (!response.ok) {
|
||||
const error: any = new Error(`Error ${response.status}: ${response.statusText}`, {});
|
||||
error.status = response.status;
|
||||
throw error;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
|
|
@ -20,7 +25,7 @@ export async function updateChaptersMetadata(course_id: any, data: any) {
|
|||
const result: any = await fetch(`${getAPIUrl()}chapters/meta/course_${course_id}`, RequestBody("PUT", data))
|
||||
.then((result) => result.json())
|
||||
.catch((error) => console.log("error", error));
|
||||
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,29 +9,6 @@ from fastapi.responses import JSONResponse
|
|||
|
||||
router = APIRouter()
|
||||
|
||||
# DEPRECATED
|
||||
@router.post("/token", response_model=Token)
|
||||
async def login_for_access_token(request: Request, form_data: OAuth2PasswordRequestForm = Depends()):
|
||||
"""
|
||||
OAuth2 compatible token login, get access token for future requests
|
||||
"""
|
||||
user = await authenticate_user(request, form_data.username, form_data.password)
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Incorrect Email or password",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
access_token = create_access_token(
|
||||
data={"sub": user.username}, expires_delta=access_token_expires
|
||||
)
|
||||
|
||||
response = JSONResponse(content={"access_token" : access_token ,"token_type": "bearer"})
|
||||
response.set_cookie(key="user_token", value=access_token, httponly=True, expires=3600,secure=True)
|
||||
|
||||
return response
|
||||
|
||||
@router.post('/refresh')
|
||||
def refresh(Authorize: AuthJWT = Depends()):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ async def api_create_course(request: Request, org_id: str, name: str = Form(), m
|
|||
Create new Course
|
||||
"""
|
||||
course = Course(name=name, mini_description=mini_description, description=description,
|
||||
org_id=org_id, public=public, thumbnail="", chapters=[], learnings=[])
|
||||
org_id=org_id, public=public, thumbnail="", chapters=[], chapters_content=[], learnings=[])
|
||||
return await create_course(request, course, org_id, current_user, thumbnail)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
|||
from passlib.context import CryptContext
|
||||
from jose import JWTError, jwt
|
||||
from datetime import datetime, timedelta
|
||||
from src.services.users.schemas.users import AnonymousUser
|
||||
from src.services.users.users import *
|
||||
from fastapi import Cookie, FastAPI
|
||||
from src.security.security import *
|
||||
|
|
@ -76,14 +77,19 @@ async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
|
|||
)
|
||||
|
||||
try:
|
||||
Authorize.jwt_required()
|
||||
username = Authorize.get_jwt_subject()
|
||||
Authorize.jwt_optional()
|
||||
username = Authorize.get_jwt_subject() or None
|
||||
token_data = TokenData(username=username) # type: ignore
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return PublicUser(**user.dict())
|
||||
if username:
|
||||
user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return PublicUser(**user.dict())
|
||||
else:
|
||||
return AnonymousUser()
|
||||
|
||||
|
||||
async def non_public_endpoint(current_user: PublicUser ):
|
||||
if isinstance(current_user, AnonymousUser):
|
||||
raise HTTPException(status_code=401, detail="Not authenticated")
|
||||
|
|
@ -38,6 +38,18 @@ async def verify_user_rights_with_roles(request: Request, action: str, user_id:
|
|||
roles = request.app.db["roles"]
|
||||
users = request.app.db["users"]
|
||||
|
||||
user = await users.find_one({"user_id": user_id})
|
||||
|
||||
# Check if user is available
|
||||
if not user and user_id != "anonymous":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND, detail="User not found")
|
||||
|
||||
# Check if user is anonymous
|
||||
if user_id == "anonymous":
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN, detail="The ressource you are trying to access is not publicly available")
|
||||
|
||||
# Check if the user is an admin
|
||||
user: UserInDB = UserInDB(**await users.find_one({"user_id": user_id}))
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import pprint
|
|||
from typing import List
|
||||
from uuid import uuid4
|
||||
from pydantic import BaseModel
|
||||
from src.security.auth import non_public_endpoint
|
||||
from src.services.courses.courses import Course, CourseInDB
|
||||
from src.services.courses.activities.activities import Activity, ActivityInDB
|
||||
from src.security.security import verify_user_rights_with_roles
|
||||
|
|
@ -113,8 +114,14 @@ async def delete_coursechapter(request: Request, coursechapter_id: str, current
|
|||
# verify course rights
|
||||
await verify_rights(request, course["course_id"], current_user, "delete")
|
||||
|
||||
courses.update_one({"chapters_content.coursechapter_id": coursechapter_id}, {
|
||||
# Remove coursechapter from course
|
||||
res = await courses.update_one({"course_id": course["course_id"]}, {
|
||||
"$pull": {"chapters": coursechapter_id}})
|
||||
|
||||
await courses.update_one({"chapters_content.coursechapter_id": coursechapter_id}, {
|
||||
"$pull": {"chapters_content": {"coursechapter_id": coursechapter_id}}})
|
||||
|
||||
|
||||
|
||||
return {"message": "Coursechapter deleted"}
|
||||
|
||||
|
|
@ -143,10 +150,16 @@ async def get_coursechapters_meta(request: Request, course_id: str, current_user
|
|||
courses = request.app.db["courses"]
|
||||
activities = request.app.db["activities"]
|
||||
|
||||
await non_public_endpoint(current_user)
|
||||
|
||||
coursechapters = await courses.find_one({"course_id": course_id}, {"chapters": 1, "chapters_content": 1, "_id": 0})
|
||||
|
||||
coursechapters = coursechapters
|
||||
|
||||
if not coursechapters:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist")
|
||||
|
||||
# activities
|
||||
coursechapter_activityIds_global = []
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ from uuid import uuid4
|
|||
from pydantic import BaseModel
|
||||
from src.services.courses.activities.activities import ActivityInDB
|
||||
from src.services.courses.thumbnails import upload_thumbnail
|
||||
from src.services.users.schemas.users import AnonymousUser
|
||||
from src.services.users.users import PublicUser
|
||||
from src.security.security import *
|
||||
from fastapi import HTTPException, status, UploadFile
|
||||
|
|
@ -282,11 +283,14 @@ async def get_courses_orgslug(request: Request, page: int = 1, limit: int = 10,
|
|||
#### Security ####################################################
|
||||
|
||||
|
||||
async def verify_rights(request: Request, course_id: str, current_user: PublicUser, action: str):
|
||||
async def verify_rights(request: Request, course_id: str, current_user: PublicUser | AnonymousUser, action: str):
|
||||
courses = request.app.db["courses"]
|
||||
|
||||
course = await courses.find_one({"course_id": course_id})
|
||||
|
||||
if current_user.user_id == "anonymous" and course["public"] == True:
|
||||
return True
|
||||
|
||||
if not course:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_409_CONFLICT, detail=f"Course/CourseChapter does not exist")
|
||||
|
|
|
|||
|
|
@ -43,6 +43,11 @@ class PublicUser(User):
|
|||
creation_date: str
|
||||
update_date: str
|
||||
|
||||
class AnonymousUser(BaseModel):
|
||||
user_id: str = "anonymous"
|
||||
username: str = "anonymous"
|
||||
|
||||
|
||||
|
||||
# Forms ####################################################
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue