Merge pull request #67 from learnhouse/feat/new-modal-design

New Modal Component + Components refactor
This commit is contained in:
Badr B 2023-04-10 19:00:47 +02:00 committed by GitHub
commit 49b6d1dfe7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 861 additions and 399 deletions

View file

@ -2,13 +2,12 @@
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import Link from "next/link"; import Link from "next/link";
import React, { useMemo } from "react"; import React, { useMemo } from "react";
import Layout from "@components/UI/Layout";
import { getActivity } from "@services/courses/activities"; import { getActivity } from "@services/courses/activities";
import { getAPIUrl, getBackendUrl, getUriWithOrg } from "@services/config/config"; 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 styled from "styled-components";
import { getCourse } from "@services/courses/courses"; 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 useSWR, { mutate } from "swr";
import { Check } from "lucide-react"; import { Check } from "lucide-react";
import { swrFetcher } from "@services/utils/requests"; import { swrFetcher } from "@services/utils/requests";

View file

@ -1,23 +1,24 @@
"use client"; "use client";
import React from "react"; import React from "react";
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import styled from "styled-components"; 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 { Title } from "@components/UI/Elements/Styles/Title";
import { DragDropContext, Droppable } from "react-beautiful-dnd"; import { DragDropContext, Droppable } from "react-beautiful-dnd";
import { initialData, initialData2 } from "@components/Drags/data"; import { initialData, initialData2 } from "@components/Pages/CourseEdit/Draggables/data";
import Chapter from "@components/Drags/Chapter"; import Chapter from "@components/Pages/CourseEdit/Draggables/Chapter";
import { createChapter, deleteChapter, getCourseChaptersMetadata, updateChaptersMetadata } from "@services/courses/chapters"; import { createChapter, deleteChapter, getCourseChaptersMetadata, updateChaptersMetadata } from "@services/courses/chapters";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import NewChapterModal from "@components/Modals/CourseEdit/NewChapter"; import NewChapterModal from "@components/Pages/CourseEdit/NewChapter";
import NewActivityModal from "@components/Modals/CourseEdit/NewActivity"; import NewActivityModal from "@components/Pages/CourseEdit/NewActivity";
import { createActivity, createFileActivity } from "@services/courses/activities"; import { createActivity, createFileActivity } from "@services/courses/activities";
import { getOrganizationContextInfo } from "@services/organizations/orgs"; import { getOrganizationContextInfo } from "@services/organizations/orgs";
import Modal from "@components/UI/Modal/Modal";
import AuthProvider from "@components/Security/AuthProvider";
function CourseEdit(params: any) { function CourseEdit(params: any) {
const router = useRouter();
const router = useRouter();
// Initial Course State // Initial Course State
const [data, setData] = useState(initialData2) as any; const [data, setData] = useState(initialData2) as any;
@ -32,12 +33,15 @@ function CourseEdit(params: any) {
const courseid = params.params.courseid; const courseid = params.params.courseid;
const orgslug = params.params.orgslug; const orgslug = params.params.orgslug;
async function getCourseChapters() { async function getCourseChapters() {
try {
const courseChapters = await getCourseChaptersMetadata(courseid); const courseChapters = await getCourseChaptersMetadata(courseid);
setData(courseChapters); setData(courseChapters);
console.log("courseChapters", courseChapters); } catch (error: any) {
if (error.status === 401) {
router.push("/login");
}
}
} }
useEffect(() => { useEffect(() => {
@ -106,9 +110,7 @@ function CourseEdit(params: any) {
}; };
/* /*
Modals Modals
*/ */
const openNewActivityModal = async (chapterId: any) => { const openNewActivityModal = async (chapterId: any) => {
@ -123,6 +125,8 @@ function CourseEdit(params: any) {
}; };
const closeNewActivityModal = () => { const closeNewActivityModal = () => {
console.log("closeNewActivityModal");
setNewActivityModal(false); setNewActivityModal(false);
}; };
@ -233,13 +237,22 @@ function CourseEdit(params: any) {
<Page> <Page>
<Title> <Title>
Edit Course {" "} Edit Course {" "}
<button <Modal
onClick={() => { isDialogOpen={newChapterModal}
setNewChapterModal(true); onOpenChange={setNewChapterModal}
}} minHeight="sm"
> dialogContent={<NewChapterModal
Add chapter + closeModal={closeNewChapterModal}
submitChapter={submitChapter}
></NewChapterModal>}
dialogTitle="Create chapter"
dialogDescription="Add a new chapter to the course"
dialogTrigger={
<button> Add chapter +
</button> </button>
}
/>
<button <button
onClick={() => { onClick={() => {
updateChapters(); updateChapters();
@ -247,16 +260,23 @@ function CourseEdit(params: any) {
> >
Save Save
</button> </button>
</Title> </Title>-
{newChapterModal && <NewChapterModal closeModal={closeNewChapterModal} submitChapter={submitChapter}></NewChapterModal>}
{newActivityModal && ( <Modal
<NewActivityModal isDialogOpen={newActivityModal}
onOpenChange={setNewActivityModal}
minHeight="no-min"
addDefCloseButton={false}
dialogContent={<NewActivityModal
closeModal={closeNewActivityModal} closeModal={closeNewActivityModal}
submitFileActivity={submitFileActivity} submitFileActivity={submitFileActivity}
submitActivity={submitActivity} submitActivity={submitActivity}
chapterId={newActivityModalData} chapterId={newActivityModalData}
></NewActivityModal> ></NewActivityModal>}
)} dialogTitle="Create Activity"
dialogDescription="Choose between types of activities to add to the course"
/>
<br /> <br />
{winReady && ( {winReady && (

View file

@ -1,8 +1,6 @@
"use client"; "use client";
import { useRouter } from "next/navigation"; import { useRouter } from "next/navigation";
import React from "react"; 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 { Title } from "@components/UI/Elements/Styles/Title";
import { createNewCourse } from "@services/courses/courses"; import { createNewCourse } from "@services/courses/courses";
import { getOrganizationContextInfo } from "@services/organizations/orgs"; import { getOrganizationContextInfo } from "@services/organizations/orgs";

View file

@ -3,18 +3,16 @@ import React, { createContext, useState } from 'react'
import { styled } from '@stitches/react'; import { styled } from '@stitches/react';
import Link from 'next/link'; import Link from 'next/link';
import LearnHouseWhiteLogo from '@public/learnhouse_text_white.png'; 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 Avvvatars from 'avvvatars-react';
import Image from 'next/image'; import Image from 'next/image';
function SettingsLayout({ children, params }: { children: React.ReactNode, params: any }) { function SettingsLayout({ children, params }: { children: React.ReactNode, params: any }) {
const auth: any = React.useContext(AuthContext); const auth: any = React.useContext(AuthContext);
return ( return (
<> <>
<AuthProvider/>
<Main> <Main>
<LeftWrapper> <LeftWrapper>
<LeftTopArea> <LeftTopArea>

View file

@ -1,6 +1,6 @@
"use client"; "use client";
import "../styles/globals.css"; import "../styles/globals.css";
import StyledComponentsRegistry from "../components/lib/styled-registry"; import StyledComponentsRegistry from "../components/UI/libs/styled-registry";
import { motion } from "framer-motion"; import { motion } from "framer-motion";
export default function RootLayout({ children }: { children: React.ReactNode }) { export default function RootLayout({ children }: { children: React.ReactNode }) {

View file

@ -1,8 +1,6 @@
"use client"; "use client";
import { useRouter } from 'next/navigation'; import { useRouter } from 'next/navigation';
import React from "react"; 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 { Title } from "../../components/UI/Elements/Styles/Title";
import { loginAndGetToken } from "../../services/auth/auth"; import { loginAndGetToken } from "../../services/auth/auth";
@ -35,7 +33,6 @@ const Login = () => {
return ( return (
<div> <div>
< > < >
<Header></Header>
<Title>Login</Title> <Title>Login</Title>
<form> <form>

View file

@ -1,6 +1,5 @@
"use client"; "use client";
import React from "react"; import React from "react";
import Layout from "../../../components/UI/Layout";
import { Title } from "../../../components/UI/Elements/Styles/Title"; import { Title } from "../../../components/UI/Elements/Styles/Title";
import { createNewOrganization } from "../../../services/organizations/orgs"; import { createNewOrganization } from "../../../services/organizations/orgs";
@ -34,7 +33,7 @@ const Organizations = () => {
}; };
return ( return (
<Layout> <div>
<Title>New Organization</Title> <Title>New Organization</Title>
Name: <input onChange={handleNameChange} type="text" /> Name: <input onChange={handleNameChange} type="text" />
<br /> <br />
@ -45,7 +44,7 @@ const Organizations = () => {
Email Address: <input onChange={handleEmailChange} type="text" /> Email Address: <input onChange={handleEmailChange} type="text" />
<br /> <br />
<button onClick={handleSubmit}>Create</button> <button onClick={handleSubmit}>Create</button>
</Layout> </div>
); );
}; };

View file

@ -1,12 +1,12 @@
"use client"; //todo: use server components "use client"; //todo: use server components
import Link from "next/link"; import Link from "next/link";
import React from "react"; import React from "react";
import Layout from "../../components/UI/Layout";
import { Title } from "../../components/UI/Elements/Styles/Title"; import { Title } from "../../components/UI/Elements/Styles/Title";
import { deleteOrganizationFromBackend } from "@services/organizations/orgs"; import { deleteOrganizationFromBackend } from "@services/organizations/orgs";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
import { swrFetcher } from "@services/utils/requests"; import { swrFetcher } from "@services/utils/requests";
import { getAPIUrl, getUriWithOrg } from "@services/config/config"; import { getAPIUrl, getUriWithOrg } from "@services/config/config";
import AuthProvider from "@components/Security/AuthProvider";
const Organizations = () => { const Organizations = () => {
const { data : organizations , error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher) const { data : organizations , error } = useSWR(`${getAPIUrl()}orgs/user/page/1/limit/10`, swrFetcher)
@ -18,6 +18,7 @@ const Organizations = () => {
return ( return (
<> <>
<AuthProvider/>
<Title> <Title>
Your Organizations{" "} Your Organizations{" "}
<Link href={"/organizations/new"}> <Link href={"/organizations/new"}>

View file

@ -1,7 +1,5 @@
"use client"; "use client";
import React from "react"; 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 { Title } from "../../components/UI/Elements/Styles/Title";
import { signup } from "../../services/auth/auth"; import { signup } from "../../services/auth/auth";
@ -31,8 +29,7 @@ const SignUp = () => {
return ( return (
<div> <div>
<Layout title="Sign up"> <div title="Sign up">
<Header></Header>
<Title>Sign up </Title> <Title>Sign up </Title>
<form> <form>
@ -43,7 +40,7 @@ const SignUp = () => {
Sign up Sign up
</button> </button>
</form> </form>
</Layout> </div>
</div> </div>
); );
}; };

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -21,8 +21,11 @@ function Chapter(props: any) {
props.openNewActivityModal(props.info.list.chapter.id); props.openNewActivityModal(props.info.list.chapter.id);
}} }}
> >
Create Activity Create Activity
</button> </button>
<button <button
onClick={() => { onClick={() => {
props.deleteChapter(props.info.list.chapter.id); props.deleteChapter(props.info.list.chapter.id);

View 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;

View file

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

View 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;

View 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;

View file

@ -5,7 +5,9 @@ import { useRouter, usePathname } from "next/navigation";
export const AuthContext: any = React.createContext({}); 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 { export interface Auth {
access_token: string; access_token: string;
isAuthenticated: boolean; isAuthenticated: boolean;
@ -15,6 +17,8 @@ export interface Auth {
const AuthProvider = ({ children }: any) => { const AuthProvider = ({ children }: any) => {
const router = useRouter(); const router = useRouter();
const pathname = usePathname();
const [auth, setAuth] = React.useState<Auth>({ access_token: "", isAuthenticated: false, userInfo: {}, isLoading: true }); const [auth, setAuth] = React.useState<Auth>({ access_token: "", isAuthenticated: false, userInfo: {}, isLoading: true });
async function checkRefreshToken() { async function checkRefreshToken() {
@ -24,6 +28,7 @@ const AuthProvider = ({ children }: any) => {
} }
} }
async function checkAuth() { async function checkAuth() {
try { try {
let access_token = await checkRefreshToken(); let access_token = await checkRefreshToken();
@ -34,13 +39,24 @@ const AuthProvider = ({ children }: any) => {
userInfo = await getUserInfo(access_token); userInfo = await getUserInfo(access_token);
setAuth({ access_token, isAuthenticated: true, userInfo, isLoading }); 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 { } else {
setAuth({ access_token, isAuthenticated: false, userInfo, isLoading }); 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) { } catch (error) {
router.push("/");
} }
} }

View file

@ -6,13 +6,9 @@ import learnhouseIcon from "public/learnhouse_icon.png";
import learnhouseLogo from "public/learnhouse_logo.png"; import learnhouseLogo from "public/learnhouse_logo.png";
import Link from "next/link"; import Link from "next/link";
import Image from "next/image"; import Image from "next/image";
import { useRouter, useSearchParams, usePathname } from "next/navigation"; import { getUriWithOrg } from "@services/config/config";
import { headers } from "next/headers";
import { getOrgFromUri, getUriWithOrg } from "@services/config/config";
export const Menu = (props : any ) => { export const Menu = (props : any ) => {
const router = useRouter();
const pathname = usePathname();
const orgslug = props.orgslug; const orgslug = props.orgslug;

View 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;

View file

@ -1,13 +0,0 @@
import React from "react";
import Link from 'next/link'
import styled from "styled-components";
export const Header = () => {
return (
<div>
</div>
);
};

View file

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

View 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
View file

@ -8,7 +8,9 @@
"name": "learnhouse", "name": "learnhouse",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-form": "^0.0.2",
"@radix-ui/react-icons": "^1.1.1", "@radix-ui/react-icons": "^1.1.1",
"@stitches/react": "^1.2.8", "@stitches/react": "^1.2.8",
"@tiptap/extension-collaboration": "^2.0.0-beta.199", "@tiptap/extension-collaboration": "^2.0.0-beta.199",
@ -2352,6 +2354,11 @@
"url": "https://opencollective.com/popperjs" "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": { "node_modules/@radix-ui/primitive": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", "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" "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": { "node_modules/@radix-ui/react-icons": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.1.1.tgz", "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" "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": { "node_modules/@radix-ui/react-portal": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz", "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", "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.6.tgz",
"integrity": "sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==" "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": { "@radix-ui/primitive": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.0.tgz", "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-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": { "@radix-ui/react-icons": {
"version": "1.1.1", "version": "1.1.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-icons/-/react-icons-1.1.1.tgz", "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-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": { "@radix-ui/react-portal": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.0.1.tgz",

View file

@ -9,7 +9,9 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@radix-ui/colors": "^0.1.8",
"@radix-ui/react-dialog": "^1.0.2", "@radix-ui/react-dialog": "^1.0.2",
"@radix-ui/react-form": "^0.0.2",
"@radix-ui/react-icons": "^1.1.1", "@radix-ui/react-icons": "^1.1.1",
"@stitches/react": "^1.2.8", "@stitches/react": "^1.2.8",
"@tiptap/extension-collaboration": "^2.0.0-beta.199", "@tiptap/extension-collaboration": "^2.0.0-beta.199",

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 586 B

View file

@ -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 { getAPIUrl } from "@services/config/config";
import { RequestBody } from "@services/utils/requests"; import { RequestBody } from "@services/utils/requests";
@ -9,10 +9,15 @@ import { RequestBody } from "@services/utils/requests";
//TODO : depreciate this function //TODO : depreciate this function
export async function getCourseChaptersMetadata(course_id: any) { export async function getCourseChaptersMetadata(course_id: any) {
const data: any = await fetch(`${getAPIUrl()}chapters/meta/course_${course_id}`, RequestBody("GET", null)) const response = await fetch(`${getAPIUrl()}chapters/meta/course_${course_id}`, RequestBody("GET", null));
.then((result) => result.json())
.catch((error) => console.log("error", error));
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; return data;
} }

View file

@ -9,29 +9,6 @@ from fastapi.responses import JSONResponse
router = APIRouter() 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') @router.post('/refresh')
def refresh(Authorize: AuthJWT = Depends()): def refresh(Authorize: AuthJWT = Depends()):
""" """

View file

@ -14,7 +14,7 @@ async def api_create_course(request: Request, org_id: str, name: str = Form(), m
Create new Course Create new Course
""" """
course = Course(name=name, mini_description=mini_description, description=description, 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) return await create_course(request, course, org_id, current_user, thumbnail)

View file

@ -4,6 +4,7 @@ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from passlib.context import CryptContext from passlib.context import CryptContext
from jose import JWTError, jwt from jose import JWTError, jwt
from datetime import datetime, timedelta from datetime import datetime, timedelta
from src.services.users.schemas.users import AnonymousUser
from src.services.users.users import * from src.services.users.users import *
from fastapi import Cookie, FastAPI from fastapi import Cookie, FastAPI
from src.security.security import * from src.security.security import *
@ -76,14 +77,19 @@ async def get_current_user(request: Request, Authorize: AuthJWT = Depends()):
) )
try: try:
Authorize.jwt_required() Authorize.jwt_optional()
username = Authorize.get_jwt_subject() username = Authorize.get_jwt_subject() or None
token_data = TokenData(username=username) # type: ignore token_data = TokenData(username=username) # type: ignore
except JWTError: except JWTError:
raise credentials_exception raise credentials_exception
if username:
user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email user = await security_get_user(request, email=token_data.username) # type: ignore # treated as an email
if user is None: if user is None:
raise credentials_exception raise credentials_exception
return PublicUser(**user.dict()) 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")

View file

@ -38,6 +38,18 @@ async def verify_user_rights_with_roles(request: Request, action: str, user_id:
roles = request.app.db["roles"] roles = request.app.db["roles"]
users = request.app.db["users"] 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 # Check if the user is an admin
user: UserInDB = UserInDB(**await users.find_one({"user_id": user_id})) user: UserInDB = UserInDB(**await users.find_one({"user_id": user_id}))

View file

@ -4,6 +4,7 @@ import pprint
from typing import List from typing import List
from uuid import uuid4 from uuid import uuid4
from pydantic import BaseModel from pydantic import BaseModel
from src.security.auth import non_public_endpoint
from src.services.courses.courses import Course, CourseInDB from src.services.courses.courses import Course, CourseInDB
from src.services.courses.activities.activities import Activity, ActivityInDB from src.services.courses.activities.activities import Activity, ActivityInDB
from src.security.security import verify_user_rights_with_roles from src.security.security import verify_user_rights_with_roles
@ -113,9 +114,15 @@ async def delete_coursechapter(request: Request, coursechapter_id: str, current
# verify course rights # verify course rights
await verify_rights(request, course["course_id"], current_user, "delete") 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}}}) "$pull": {"chapters_content": {"coursechapter_id": coursechapter_id}}})
return {"message": "Coursechapter deleted"} return {"message": "Coursechapter deleted"}
else: else:
@ -143,10 +150,16 @@ async def get_coursechapters_meta(request: Request, course_id: str, current_user
courses = request.app.db["courses"] courses = request.app.db["courses"]
activities = request.app.db["activities"] 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 = await courses.find_one({"course_id": course_id}, {"chapters": 1, "chapters_content": 1, "_id": 0})
coursechapters = coursechapters coursechapters = coursechapters
if not coursechapters:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail="Course does not exist")
# activities # activities
coursechapter_activityIds_global = [] coursechapter_activityIds_global = []

View file

@ -4,6 +4,7 @@ from uuid import uuid4
from pydantic import BaseModel from pydantic import BaseModel
from src.services.courses.activities.activities import ActivityInDB from src.services.courses.activities.activities import ActivityInDB
from src.services.courses.thumbnails import upload_thumbnail 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.services.users.users import PublicUser
from src.security.security import * from src.security.security import *
from fastapi import HTTPException, status, UploadFile from fastapi import HTTPException, status, UploadFile
@ -282,11 +283,14 @@ async def get_courses_orgslug(request: Request, page: int = 1, limit: int = 10,
#### Security #################################################### #### 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"] courses = request.app.db["courses"]
course = await courses.find_one({"course_id": course_id}) course = await courses.find_one({"course_id": course_id})
if current_user.user_id == "anonymous" and course["public"] == True:
return True
if not course: if not course:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_409_CONFLICT, detail=f"Course/CourseChapter does not exist") status_code=status.HTTP_409_CONFLICT, detail=f"Course/CourseChapter does not exist")

View file

@ -43,6 +43,11 @@ class PublicUser(User):
creation_date: str creation_date: str
update_date: str update_date: str
class AnonymousUser(BaseModel):
user_id: str = "anonymous"
username: str = "anonymous"
# Forms #################################################### # Forms ####################################################