mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
Merge pull request #77 from learnhouse/swve/eng-22-improve-editor-topbar
Improve editor elements
This commit is contained in:
commit
7e9ac3712b
7 changed files with 143 additions and 21 deletions
|
|
@ -8,7 +8,7 @@ import { getActivity } from "@services/courses/activities";
|
||||||
import AuthProvider from "@components/Security/AuthProvider";
|
import AuthProvider from "@components/Security/AuthProvider";
|
||||||
import EditorWrapper from "@components/Editor/EditorWrapper";
|
import EditorWrapper from "@components/Editor/EditorWrapper";
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from "swr";
|
||||||
import { getAPIUrl } from "@services/config/config";
|
import { getAPIUrl, getOrgFromUri } from "@services/config/config";
|
||||||
import { swrFetcher } from "@services/utils/ts/requests";
|
import { swrFetcher } from "@services/utils/ts/requests";
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -16,15 +16,18 @@ function EditActivity(params: any) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const activityid = params.params.activityid;
|
const activityid = params.params.activityid;
|
||||||
const courseid = params.params.courseid;
|
const courseid = params.params.courseid;
|
||||||
|
const orgslug = params.params.orgslug;
|
||||||
const { data: courseInfo, error: error_course } = useSWR(`${getAPIUrl()}courses/meta/course_${courseid}`, swrFetcher);
|
const { data: courseInfo, error: error_course } = useSWR(`${getAPIUrl()}courses/meta/course_${courseid}`, swrFetcher);
|
||||||
const { data: activity, error: error_activity } = useSWR(`${getAPIUrl()}activities/activity_${activityid}`, swrFetcher);
|
const { data: activity, error: error_activity } = useSWR(`${getAPIUrl()}activities/activity_${activityid}`, swrFetcher);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
{!courseInfo || !activity ? <div>Loading...</div> : <EditorWrapper course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>}
|
{!courseInfo || !activity ? <div>Loading...</div> : <EditorWrapper orgslug={orgslug} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>}
|
||||||
</AuthProvider>
|
</AuthProvider>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ import { ToolbarButtons } from "./Toolbar/ToolbarButtons";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import Image from "next/image";
|
import Image from "next/image";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { getBackendUrl } from "@services/config/config";
|
import { getBackendUrl, getUriWithOrg } from "@services/config/config";
|
||||||
import { SlashIcon } from "@radix-ui/react-icons";
|
import { DividerVerticalIcon, EyeOpenIcon, SlashIcon } from "@radix-ui/react-icons";
|
||||||
import Avvvatars from "avvvatars-react";
|
import Avvvatars from "avvvatars-react";
|
||||||
// extensions
|
// extensions
|
||||||
import InfoCallout from "./Extensions/Callout/Info/InfoCallout";
|
import InfoCallout from "./Extensions/Callout/Info/InfoCallout";
|
||||||
|
|
@ -18,22 +18,30 @@ import WarningCallout from "./Extensions/Callout/Warning/WarningCallout";
|
||||||
import ImageBlock from "./Extensions/Image/ImageBlock";
|
import ImageBlock from "./Extensions/Image/ImageBlock";
|
||||||
import Youtube from "@tiptap/extension-youtube";
|
import Youtube from "@tiptap/extension-youtube";
|
||||||
import VideoBlock from "./Extensions/Video/VideoBlock";
|
import VideoBlock from "./Extensions/Video/VideoBlock";
|
||||||
import { Save } from "lucide-react";
|
import { Eye, Save } from "lucide-react";
|
||||||
import MathEquationBlock from "./Extensions/MathEquation/MathEquationBlock";
|
import MathEquationBlock from "./Extensions/MathEquation/MathEquationBlock";
|
||||||
import PDFBlock from "./Extensions/PDF/PDFBlock";
|
import PDFBlock from "./Extensions/PDF/PDFBlock";
|
||||||
import QuizBlock from "./Extensions/Quiz/QuizBlock";
|
import QuizBlock from "./Extensions/Quiz/QuizBlock";
|
||||||
|
import ToolTip from "@components/UI/Tooltip/Tooltip";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
interface Editor {
|
interface Editor {
|
||||||
content: string;
|
content: string;
|
||||||
ydoc: any;
|
ydoc: any;
|
||||||
provider: any;
|
provider: any;
|
||||||
activity: any;
|
activity: any;
|
||||||
|
orgslug: string
|
||||||
course: any;
|
course: any;
|
||||||
setContent: (content: string) => void;
|
setContent: (content: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Editor(props: Editor) {
|
function Editor(props: Editor) {
|
||||||
const auth: any = React.useContext(AuthContext);
|
const auth: any = React.useContext(AuthContext);
|
||||||
|
// remove course_ from course_id
|
||||||
|
const course_id = props.course.course.course_id.substring(7);
|
||||||
|
|
||||||
|
// remove activity_ from activity_id
|
||||||
|
const activity_id = props.activity.activity_id.substring(9);
|
||||||
|
|
||||||
const editor: any = useEditor({
|
const editor: any = useEditor({
|
||||||
editable: true,
|
editable: true,
|
||||||
|
|
@ -107,18 +115,20 @@ function Editor(props: Editor) {
|
||||||
<EditorTop>
|
<EditorTop>
|
||||||
<EditorDocSection>
|
<EditorDocSection>
|
||||||
<EditorInfoWrapper>
|
<EditorInfoWrapper>
|
||||||
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
|
<Link href="/">
|
||||||
<EditorInfoThumbnail src={`${getBackendUrl()}content/uploads/img/${props.course.course.thumbnail}`} alt=""></EditorInfoThumbnail>
|
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
|
||||||
|
</Link>
|
||||||
|
<Link target="_blank" href={`/course/${course_id}`}>
|
||||||
|
<EditorInfoThumbnail src={`${getBackendUrl()}content/uploads/img/${props.course.course.thumbnail}`} alt=""></EditorInfoThumbnail>
|
||||||
|
</Link>
|
||||||
<EditorInfoDocName>
|
<EditorInfoDocName>
|
||||||
{" "}
|
{" "}
|
||||||
<b>{props.course.course.name}</b> <SlashIcon /> {props.activity.name}{" "}
|
<b>{props.course.course.name}</b> <SlashIcon /> {props.activity.name}{" "}
|
||||||
</EditorInfoDocName>
|
</EditorInfoDocName>
|
||||||
<EditorSaveButton onClick={() => props.setContent(editor.getJSON())}>
|
|
||||||
Save <Save size={11} />
|
|
||||||
</EditorSaveButton>
|
|
||||||
</EditorInfoWrapper>
|
</EditorInfoWrapper>
|
||||||
<EditorButtonsWrapper>
|
<EditorButtonsWrapper>
|
||||||
<ToolbarButtons editor={editor} />
|
<ToolbarButtons editor={editor} />
|
||||||
</EditorButtonsWrapper>
|
</EditorButtonsWrapper>
|
||||||
</EditorDocSection>
|
</EditorDocSection>
|
||||||
<EditorUsersSection>
|
<EditorUsersSection>
|
||||||
|
|
@ -126,6 +136,11 @@ function Editor(props: Editor) {
|
||||||
{!auth.isAuthenticated && <span>Loading</span>}
|
{!auth.isAuthenticated && <span>Loading</span>}
|
||||||
{auth.isAuthenticated && <Avvvatars value={auth.userInfo.user_object.user_id} style="shape" />}
|
{auth.isAuthenticated && <Avvvatars value={auth.userInfo.user_object.user_id} style="shape" />}
|
||||||
</EditorUserProfileWrapper>
|
</EditorUserProfileWrapper>
|
||||||
|
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey" }} />
|
||||||
|
<EditorLeftOptionsSection>
|
||||||
|
<EditorLeftOptionsSaveButton onClick={() => props.setContent(editor.getJSON())}> Save </EditorLeftOptionsSaveButton>
|
||||||
|
<ToolTip content="Preview"><Link target="_blank" href={`/course/${course_id}/activity/${activity_id}`}><EditorLeftOptionsPreviewButton> <Eye size={15} /> </EditorLeftOptionsPreviewButton></Link></ToolTip>
|
||||||
|
</EditorLeftOptionsSection>
|
||||||
</EditorUsersSection>
|
</EditorUsersSection>
|
||||||
</EditorTop>
|
</EditorTop>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -186,11 +201,63 @@ const EditorDocSection = styled.div`
|
||||||
`;
|
`;
|
||||||
const EditorUsersSection = styled.div`
|
const EditorUsersSection = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
const EditorLeftOptionsSection = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EditorLeftOptionsSaveButton = styled.button`
|
||||||
|
background-color: #8783f7;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
color: white;
|
||||||
|
padding: 8px;
|
||||||
|
margin-left: 10px;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #4a44f9;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const EditorLeftOptionsPreviewButton = styled.button`
|
||||||
|
background-color: #a4a4a449;
|
||||||
|
border-radius: 8px;
|
||||||
|
border: none;
|
||||||
|
color: #000000;
|
||||||
|
padding: 8px;
|
||||||
|
margin-right: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
// center icon
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #c0bfbf;
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
`;
|
||||||
|
|
||||||
|
|
||||||
// Inside EditorDocSection
|
// Inside EditorDocSection
|
||||||
const EditorInfoWrapper = styled.div`
|
const EditorInfoWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
|
||||||
|
|
@ -3,14 +3,17 @@ import * as Y from "yjs";
|
||||||
import { WebrtcProvider } from "y-webrtc";
|
import { WebrtcProvider } from "y-webrtc";
|
||||||
import Editor from "./Editor";
|
import Editor from "./Editor";
|
||||||
import { updateActivity } from "@services/courses/activities";
|
import { updateActivity } from "@services/courses/activities";
|
||||||
|
import { toast } from "react-hot-toast";
|
||||||
|
import Toast from "@components/UI/Toast/Toast";
|
||||||
|
|
||||||
interface EditorWrapperProps {
|
interface EditorWrapperProps {
|
||||||
content: string;
|
content: string;
|
||||||
activity: any;
|
activity: any;
|
||||||
course:any
|
course: any
|
||||||
|
orgslug: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorWrapper(props: EditorWrapperProps) : JSX.Element {
|
function EditorWrapper(props: EditorWrapperProps): JSX.Element {
|
||||||
// A new Y document
|
// A new Y document
|
||||||
const ydoc = new Y.Doc();
|
const ydoc = new Y.Doc();
|
||||||
const [providerState, setProviderState] = React.useState<any>({});
|
const [providerState, setProviderState] = React.useState<any>({});
|
||||||
|
|
@ -18,24 +21,37 @@ function EditorWrapper(props: EditorWrapperProps) : JSX.Element {
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
const [isLoading, setIsLoading] = React.useState(true);
|
||||||
|
|
||||||
function createRTCProvider() {
|
function createRTCProvider() {
|
||||||
// const provider = new WebrtcProvider(props.activity.activity_id, ydoc);
|
// const provider = new WebrtcProvider(props.activity.activity_id, ydoc);
|
||||||
// setYdocState(ydoc);
|
// setYdocState(ydoc);
|
||||||
// setProviderState(provider);
|
// setProviderState(provider);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
async function setContent(content: any) {
|
async function setContent(content: any) {
|
||||||
let activity = props.activity;
|
let activity = props.activity;
|
||||||
activity.content = content;
|
activity.content = content;
|
||||||
const res = await updateActivity(activity, activity.activity_id);
|
|
||||||
alert(JSON.stringify(res));
|
toast.promise(
|
||||||
|
updateActivity(activity, activity.activity_id),
|
||||||
|
{
|
||||||
|
loading: 'Saving...',
|
||||||
|
success: <b>Activity saved!</b>,
|
||||||
|
error: <b>Could not save.</b>,
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
createRTCProvider();
|
createRTCProvider();
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>;
|
||||||
} else {
|
} else {
|
||||||
return <Editor course={props.course} activity={props.activity} content={props.content} setContent={setContent} provider={providerState} ydoc={ydocState}></Editor>;
|
return <>
|
||||||
|
<Toast></Toast>
|
||||||
|
<Editor orgslug={props.orgslug} course={props.course} activity={props.activity} content={props.content} setContent={setContent} provider={providerState} ydoc={ydocState}></Editor>;
|
||||||
|
|
||||||
|
</>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
11
front/components/UI/Toast/Toast.tsx
Normal file
11
front/components/UI/Toast/Toast.tsx
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import React from 'react'
|
||||||
|
import { Toaster } from 'react-hot-toast';
|
||||||
|
|
||||||
|
|
||||||
|
function Toast() {
|
||||||
|
return (
|
||||||
|
<><Toaster /></>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Toast
|
||||||
24
front/package-lock.json
generated
24
front/package-lock.json
generated
|
|
@ -31,6 +31,7 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-katex": "^3.0.1",
|
"react-katex": "^3.0.1",
|
||||||
"react-spinners": "^0.13.8",
|
"react-spinners": "^0.13.8",
|
||||||
"styled-components": "^6.0.0-beta.9",
|
"styled-components": "^6.0.0-beta.9",
|
||||||
|
|
@ -6952,6 +6953,21 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||||
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
|
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
|
||||||
},
|
},
|
||||||
|
"node_modules/react-hot-toast": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"goober": "^2.1.10"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=10"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16",
|
||||||
|
"react-dom": ">=16"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
|
@ -13070,6 +13086,14 @@
|
||||||
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz",
|
||||||
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
|
"integrity": "sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw=="
|
||||||
},
|
},
|
||||||
|
"react-hot-toast": {
|
||||||
|
"version": "2.4.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-hot-toast/-/react-hot-toast-2.4.1.tgz",
|
||||||
|
"integrity": "sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==",
|
||||||
|
"requires": {
|
||||||
|
"goober": "^2.1.10"
|
||||||
|
}
|
||||||
|
},
|
||||||
"react-is": {
|
"react-is": {
|
||||||
"version": "16.13.1",
|
"version": "16.13.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||||
|
|
|
||||||
|
|
@ -32,6 +32,7 @@
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-beautiful-dnd": "^13.1.1",
|
"react-beautiful-dnd": "^13.1.1",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
|
"react-hot-toast": "^2.4.1",
|
||||||
"react-katex": "^3.0.1",
|
"react-katex": "^3.0.1",
|
||||||
"react-spinners": "^0.13.8",
|
"react-spinners": "^0.13.8",
|
||||||
"styled-components": "^6.0.0-beta.9",
|
"styled-components": "^6.0.0-beta.9",
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,7 @@ async def get_course_meta(request: Request, course_id: str, current_user: Public
|
||||||
for chapter in chapters:
|
for chapter in chapters:
|
||||||
chapters_list_with_activities.append(
|
chapters_list_with_activities.append(
|
||||||
{"id": chapters[chapter]["id"], "name": chapters[chapter]["name"], "activities": [activities_list[activity] for activity in chapters[chapter]["activityIds"]]})
|
{"id": chapters[chapter]["id"], "name": chapters[chapter]["name"], "activities": [activities_list[activity] for activity in chapters[chapter]["activityIds"]]})
|
||||||
course = Course(**course)
|
course = CourseInDB(**course)
|
||||||
|
|
||||||
# Get activity by user
|
# Get activity by user
|
||||||
trail = await trails.find_one(
|
trail = await trails.find_one(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue