mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: format with prettier
This commit is contained in:
parent
03fb09c3d6
commit
a147ad6610
164 changed files with 11257 additions and 8154 deletions
|
|
@ -1,19 +1,19 @@
|
||||||
import { NextRequest, NextResponse } from "next/server";
|
import { NextRequest, NextResponse } from 'next/server'
|
||||||
import { revalidateTag } from "next/cache";
|
import { revalidateTag } from 'next/cache'
|
||||||
|
|
||||||
export async function GET(request: NextRequest) {
|
export async function GET(request: NextRequest) {
|
||||||
const tag: any = request.nextUrl.searchParams.get("tag");
|
const tag: any = request.nextUrl.searchParams.get('tag')
|
||||||
revalidateTag(tag);
|
revalidateTag(tag)
|
||||||
|
|
||||||
return NextResponse.json(
|
return NextResponse.json(
|
||||||
{ revalidated: true, now: Date.now(), tag },
|
{ revalidated: true, now: Date.now(), tag },
|
||||||
{
|
{
|
||||||
status: 200,
|
status: 200,
|
||||||
headers: {
|
headers: {
|
||||||
"Access-Control-Allow-Origin": "*",
|
'Access-Control-Allow-Origin': '*',
|
||||||
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
|
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
|
||||||
"Access-Control-Allow-Headers": "Content-Type, Authorization",
|
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
// Or a custom loading skeleton component
|
// Or a custom loading skeleton component
|
||||||
return (
|
return <PageLoading></PageLoading>
|
||||||
<PageLoading></PageLoading>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,52 +1,72 @@
|
||||||
import { default as React, } from "react";
|
import { default as React } from 'react'
|
||||||
import EditorWrapper from "@components/Objects/Editor/EditorWrapper";
|
import EditorWrapper from '@components/Objects/Editor/EditorWrapper'
|
||||||
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
|
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses'
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from 'next/headers'
|
||||||
import { Metadata } from "next";
|
import { Metadata } from 'next'
|
||||||
import { getActivityWithAuthHeader } from "@services/courses/activities";
|
import { getActivityWithAuthHeader } from '@services/courses/activities'
|
||||||
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
|
||||||
import { getOrganizationContextInfoWithId } from "@services/organizations/orgs";
|
import { getOrganizationContextInfoWithId } from '@services/organizations/orgs'
|
||||||
import SessionProvider from "@components/Contexts/SessionContext";
|
import SessionProvider from '@components/Contexts/SessionContext'
|
||||||
import EditorOptionsProvider from "@components/Contexts/Editor/EditorContext";
|
import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext'
|
||||||
import AIEditorProvider from "@components/Contexts/AI/AIEditorContext";
|
import AIEditorProvider from '@components/Contexts/AI/AIEditorContext'
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseid: string, activityid: string };
|
params: { orgslug: string; courseid: string; activityid: string }
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata({
|
||||||
{ params }: MetadataProps,
|
params,
|
||||||
): Promise<Metadata> {
|
}: MetadataProps): Promise<Metadata> {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies()
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
const course_meta = await getCourseMetadataWithAuthHeader(
|
||||||
|
params.courseid,
|
||||||
|
{ revalidate: 0, tags: ['courses'] },
|
||||||
|
access_token ? access_token : null
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: `Edit - ${course_meta.name} Activity`,
|
title: `Edit - ${course_meta.name} Activity`,
|
||||||
description: course_meta.mini_description,
|
description: course_meta.mini_description,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const EditActivity = async (params: any) => {
|
const EditActivity = async (params: any) => {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies()
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const activityuuid = params.params.activityuuid;
|
const activityuuid = params.params.activityuuid
|
||||||
const courseid = params.params.courseid;
|
const courseid = params.params.courseid
|
||||||
const courseInfo = await getCourseMetadataWithAuthHeader(courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
const courseInfo = await getCourseMetadataWithAuthHeader(
|
||||||
const activity = await getActivityWithAuthHeader(activityuuid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
|
courseid,
|
||||||
const org = await getOrganizationContextInfoWithId(courseInfo.org_id, { revalidate: 1800, tags: ['organizations'] });
|
{ revalidate: 0, tags: ['courses'] },
|
||||||
|
access_token ? access_token : null
|
||||||
|
)
|
||||||
|
const activity = await getActivityWithAuthHeader(
|
||||||
|
activityuuid,
|
||||||
|
{ revalidate: 0, tags: ['activities'] },
|
||||||
|
access_token ? access_token : null
|
||||||
|
)
|
||||||
|
const org = await getOrganizationContextInfoWithId(courseInfo.org_id, {
|
||||||
|
revalidate: 1800,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorOptionsProvider options={{ isEditable: true }}>
|
<EditorOptionsProvider options={{ isEditable: true }}>
|
||||||
<AIEditorProvider>
|
<AIEditorProvider>
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<EditorWrapper org={org} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
|
<EditorWrapper
|
||||||
|
org={org}
|
||||||
|
course={courseInfo}
|
||||||
|
activity={activity}
|
||||||
|
content={activity.content}
|
||||||
|
></EditorWrapper>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</AIEditorProvider>
|
</AIEditorProvider>
|
||||||
</EditorOptionsProvider>
|
</EditorOptionsProvider>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditActivity;
|
export default EditActivity
|
||||||
|
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
export const EDITOR = "main";
|
export const EDITOR = 'main'
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,17 @@
|
||||||
"use client";
|
'use client'
|
||||||
|
|
||||||
import * as Sentry from "@sentry/nextjs";
|
import * as Sentry from '@sentry/nextjs'
|
||||||
import NextError from "next/error";
|
import NextError from 'next/error'
|
||||||
import { useEffect } from "react";
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function GlobalError({
|
export default function GlobalError({
|
||||||
error,
|
error,
|
||||||
}: {
|
}: {
|
||||||
error: Error & { digest?: string };
|
error: Error & { digest?: string }
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Sentry.captureException(error);
|
Sentry.captureException(error)
|
||||||
}, [error]);
|
}, [error])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html>
|
<html>
|
||||||
|
|
@ -20,5 +20,5 @@ export default function GlobalError({
|
||||||
<NextError statusCode={undefined as any} />
|
<NextError statusCode={undefined as any} />
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -5,7 +5,6 @@ import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWra
|
||||||
import { useRouter, useSearchParams } from 'next/navigation'
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
import { Suspense } from 'react'
|
import { Suspense } from 'react'
|
||||||
|
|
||||||
|
|
||||||
function InstallClient() {
|
function InstallClient() {
|
||||||
return (
|
return (
|
||||||
<GeneralWrapperStyled>
|
<GeneralWrapperStyled>
|
||||||
|
|
@ -21,7 +20,7 @@ function InstallClient() {
|
||||||
const Stepscomp = () => {
|
const Stepscomp = () => {
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const step: any = parseInt(searchParams.get('step') || '0');
|
const step: any = parseInt(searchParams.get('step') || '0')
|
||||||
const [stepNumber, setStepNumber] = React.useState(step)
|
const [stepNumber, setStepNumber] = React.useState(step)
|
||||||
const [stepsState, setStepsState] = React.useState(INSTALL_STEPS)
|
const [stepsState, setStepsState] = React.useState(INSTALL_STEPS)
|
||||||
|
|
||||||
|
|
@ -36,8 +35,8 @@ const Stepscomp = () => {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='flex justify-center '>
|
<div className="flex justify-center ">
|
||||||
<div className='grow'>
|
<div className="grow">
|
||||||
<LearnHouseLogo />
|
<LearnHouseLogo />
|
||||||
</div>
|
</div>
|
||||||
<div className="steps flex space-x-2 justify-center text-sm p-3 bg-slate-50 rounded-full w-fit m-auto px-10">
|
<div className="steps flex space-x-2 justify-center text-sm p-3 bg-slate-50 rounded-full w-fit m-auto px-10">
|
||||||
|
|
@ -48,22 +47,23 @@ const Stepscomp = () => {
|
||||||
className={`flex items-center cursor-pointer space-x-2`}
|
className={`flex items-center cursor-pointer space-x-2`}
|
||||||
onClick={() => handleStepChange(index)}
|
onClick={() => handleStepChange(index)}
|
||||||
>
|
>
|
||||||
<div className={`flex w-7 h-7 rounded-full text-slate-700 bg-slate-200 justify-center items-center m-auto align-middle hover:bg-slate-300 transition-all ${index === stepNumber ? 'bg-slate-300' : ''}`}>
|
<div
|
||||||
|
className={`flex w-7 h-7 rounded-full text-slate-700 bg-slate-200 justify-center items-center m-auto align-middle hover:bg-slate-300 transition-all ${
|
||||||
|
index === stepNumber ? 'bg-slate-300' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
{index}
|
{index}
|
||||||
</div>
|
</div>
|
||||||
<div>{step.name}</div>
|
<div>{step.name}</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex pt-8 flex-col" >
|
<div className="flex pt-8 flex-col">
|
||||||
<h1 className='font-bold text-3xl'>{stepsState[stepNumber].name}</h1>
|
<h1 className="font-bold text-3xl">{stepsState[stepNumber].name}</h1>
|
||||||
<div className="pt-8">
|
<div className="pt-8">{stepsState[stepNumber].component}</div>
|
||||||
{stepsState[stepNumber].component}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
@ -71,21 +71,52 @@ const Stepscomp = () => {
|
||||||
|
|
||||||
const LearnHouseLogo = () => {
|
const LearnHouseLogo = () => {
|
||||||
return (
|
return (
|
||||||
<svg width="133" height="80" viewBox="0 0 433 80" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
|
width="133"
|
||||||
|
height="80"
|
||||||
|
viewBox="0 0 433 80"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
<rect width="80" height="80" rx="24" fill="black" />
|
<rect width="80" height="80" rx="24" fill="black" />
|
||||||
<rect width="80" height="80" rx="24" fill="url(#paint0_angular_1555_220)" />
|
<rect
|
||||||
<rect x="0.5" y="0.5" width="79" height="79" rx="23.5" stroke="white" strokeOpacity="0.12" />
|
width="80"
|
||||||
<path d="M37.546 55.926V35.04L33.534 30.497L37.546 29.258V27.016L33.534 22.473L44.626 19.11V55.926L48.992 61H33.18L37.546 55.926Z" fill="white" />
|
height="80"
|
||||||
<path d="M113.98 54.98V30.2L109.22 24.81L113.98 23.34V20.68L109.22 15.29L122.38 11.3V54.98L127.56 61H108.8L113.98 54.98ZM157.704 41.19V41.26H135.234C136.004 50.29 140.834 54.07 146.294 54.07C151.054 54.07 155.254 51.69 156.304 48.75L157.354 49.17C154.834 55.54 149.864 61.98 141.534 61.98C132.364 61.98 127.184 53.79 127.184 45.39C127.184 36.36 132.784 26 144.194 26C152.524 26 157.634 31.6 157.704 41.05L157.774 41.19H157.704ZM148.674 39.16V38.53C148.674 31.04 145.664 28.1 142.584 28.1C137.264 28.1 135.094 34.47 135.094 38.67V39.16H148.674ZM178.717 61V55.12C176.057 57.71 171.157 61.7 166.537 61.7C161.707 61.7 158.137 59.32 158.137 53.65C158.137 46.51 166.607 42.87 178.717 38.6C178.717 33 178.577 28.66 172.837 28.66C167.237 28.66 163.877 32.58 160.307 37.9H159.817V26.7H188.657L187.117 32.72V56.45H187.187L192.367 61H178.717ZM178.717 53.23V40.56C167.727 44.97 167.377 47.98 167.377 51.34C167.377 54.7 169.687 56.17 172.627 56.17C174.797 56.17 176.967 55.05 178.717 53.23ZM221.429 39.09H220.869C217.789 31.74 213.659 29.29 210.439 29.29C205.609 29.29 205.609 32.79 205.609 39.93V54.98L212.119 61H192.029L197.209 54.98V32.09L192.449 26.7H221.429V39.09ZM261.467 61H242.707L247.747 54.98V39.44C247.747 34.05 246.977 30.62 241.587 30.62C238.997 30.62 236.337 31.74 234.097 34.75V54.98L239.137 61H220.377L225.697 54.98V36.08L220.937 30.69L234.097 26V32.37C236.897 28.03 241.447 25.86 245.647 25.86C252.787 25.86 256.147 30.48 256.147 37.06V54.98L261.467 61ZM274.343 11.3V32.23C277.143 27.89 281.693 25.72 285.893 25.72C293.033 25.72 296.393 30.34 296.393 36.92V54.98H296.463L301.643 61H282.883L287.993 55.05V39.3C287.993 33.91 287.223 30.48 281.833 30.48C279.243 30.48 276.583 31.6 274.343 34.61V54.98L279.523 61H260.763L265.943 54.98V21.38L261.183 15.99L274.343 11.3ZM335.945 42.31C335.945 51.34 329.855 61.84 316.835 61.84C306.895 61.84 301.645 53.79 301.645 45.39C301.645 36.36 307.735 25.86 320.755 25.86C330.695 25.86 335.945 33.91 335.945 42.31ZM316.975 28.52C311.165 28.52 310.535 34.82 310.535 39.02C310.535 49.94 314.525 59.18 320.685 59.18C325.865 59.18 327.195 52.32 327.195 48.68C327.195 37.76 323.135 28.52 316.975 28.52ZM349.01 26.63V48.12C349.01 53.51 349.78 56.94 355.17 56.94C357.55 56.94 360 55.75 361.82 53.65V32.72L356.64 26.63H370.22V55.26L374.98 61L361.82 61.42V55.82C359.3 59.32 356.08 61.7 351.11 61.7C343.97 61.7 340.61 57.08 340.61 50.5V32.72L335.36 26.63H349.01ZM374.617 47.77H375.177C376.997 53.79 382.527 59.04 388.267 59.04C391.137 59.04 393.517 57.64 393.517 54.49C393.517 46.23 374.967 50.29 374.967 36.43C374.967 31.25 379.517 26.7 386.657 26.7H394.357L396.947 25.23V36.85L396.527 36.78C394.007 32.23 389.807 28.87 385.327 28.94C382.387 29.01 380.707 30.83 380.707 33.56C380.707 40.77 399.887 37.62 399.887 50.43C399.887 58.55 391.697 61.7 386.167 61.7C382.667 61.7 377.907 61.21 375.247 60.09L374.617 47.77ZM430.416 41.19V41.26H407.946C408.716 50.29 413.546 54.07 419.006 54.07C423.766 54.07 427.966 51.69 429.016 48.75L430.066 49.17C427.546 55.54 422.576 61.98 414.246 61.98C405.076 61.98 399.896 53.79 399.896 45.39C399.896 36.36 405.496 26 416.906 26C425.236 26 430.346 31.6 430.416 41.05L430.486 41.19H430.416ZM421.386 39.16V38.53C421.386 31.04 418.376 28.1 415.296 28.1C409.976 28.1 407.806 34.47 407.806 38.67V39.16H421.386Z" fill="#121212" />
|
rx="24"
|
||||||
|
fill="url(#paint0_angular_1555_220)"
|
||||||
|
/>
|
||||||
|
<rect
|
||||||
|
x="0.5"
|
||||||
|
y="0.5"
|
||||||
|
width="79"
|
||||||
|
height="79"
|
||||||
|
rx="23.5"
|
||||||
|
stroke="white"
|
||||||
|
strokeOpacity="0.12"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M37.546 55.926V35.04L33.534 30.497L37.546 29.258V27.016L33.534 22.473L44.626 19.11V55.926L48.992 61H33.18L37.546 55.926Z"
|
||||||
|
fill="white"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M113.98 54.98V30.2L109.22 24.81L113.98 23.34V20.68L109.22 15.29L122.38 11.3V54.98L127.56 61H108.8L113.98 54.98ZM157.704 41.19V41.26H135.234C136.004 50.29 140.834 54.07 146.294 54.07C151.054 54.07 155.254 51.69 156.304 48.75L157.354 49.17C154.834 55.54 149.864 61.98 141.534 61.98C132.364 61.98 127.184 53.79 127.184 45.39C127.184 36.36 132.784 26 144.194 26C152.524 26 157.634 31.6 157.704 41.05L157.774 41.19H157.704ZM148.674 39.16V38.53C148.674 31.04 145.664 28.1 142.584 28.1C137.264 28.1 135.094 34.47 135.094 38.67V39.16H148.674ZM178.717 61V55.12C176.057 57.71 171.157 61.7 166.537 61.7C161.707 61.7 158.137 59.32 158.137 53.65C158.137 46.51 166.607 42.87 178.717 38.6C178.717 33 178.577 28.66 172.837 28.66C167.237 28.66 163.877 32.58 160.307 37.9H159.817V26.7H188.657L187.117 32.72V56.45H187.187L192.367 61H178.717ZM178.717 53.23V40.56C167.727 44.97 167.377 47.98 167.377 51.34C167.377 54.7 169.687 56.17 172.627 56.17C174.797 56.17 176.967 55.05 178.717 53.23ZM221.429 39.09H220.869C217.789 31.74 213.659 29.29 210.439 29.29C205.609 29.29 205.609 32.79 205.609 39.93V54.98L212.119 61H192.029L197.209 54.98V32.09L192.449 26.7H221.429V39.09ZM261.467 61H242.707L247.747 54.98V39.44C247.747 34.05 246.977 30.62 241.587 30.62C238.997 30.62 236.337 31.74 234.097 34.75V54.98L239.137 61H220.377L225.697 54.98V36.08L220.937 30.69L234.097 26V32.37C236.897 28.03 241.447 25.86 245.647 25.86C252.787 25.86 256.147 30.48 256.147 37.06V54.98L261.467 61ZM274.343 11.3V32.23C277.143 27.89 281.693 25.72 285.893 25.72C293.033 25.72 296.393 30.34 296.393 36.92V54.98H296.463L301.643 61H282.883L287.993 55.05V39.3C287.993 33.91 287.223 30.48 281.833 30.48C279.243 30.48 276.583 31.6 274.343 34.61V54.98L279.523 61H260.763L265.943 54.98V21.38L261.183 15.99L274.343 11.3ZM335.945 42.31C335.945 51.34 329.855 61.84 316.835 61.84C306.895 61.84 301.645 53.79 301.645 45.39C301.645 36.36 307.735 25.86 320.755 25.86C330.695 25.86 335.945 33.91 335.945 42.31ZM316.975 28.52C311.165 28.52 310.535 34.82 310.535 39.02C310.535 49.94 314.525 59.18 320.685 59.18C325.865 59.18 327.195 52.32 327.195 48.68C327.195 37.76 323.135 28.52 316.975 28.52ZM349.01 26.63V48.12C349.01 53.51 349.78 56.94 355.17 56.94C357.55 56.94 360 55.75 361.82 53.65V32.72L356.64 26.63H370.22V55.26L374.98 61L361.82 61.42V55.82C359.3 59.32 356.08 61.7 351.11 61.7C343.97 61.7 340.61 57.08 340.61 50.5V32.72L335.36 26.63H349.01ZM374.617 47.77H375.177C376.997 53.79 382.527 59.04 388.267 59.04C391.137 59.04 393.517 57.64 393.517 54.49C393.517 46.23 374.967 50.29 374.967 36.43C374.967 31.25 379.517 26.7 386.657 26.7H394.357L396.947 25.23V36.85L396.527 36.78C394.007 32.23 389.807 28.87 385.327 28.94C382.387 29.01 380.707 30.83 380.707 33.56C380.707 40.77 399.887 37.62 399.887 50.43C399.887 58.55 391.697 61.7 386.167 61.7C382.667 61.7 377.907 61.21 375.247 60.09L374.617 47.77ZM430.416 41.19V41.26H407.946C408.716 50.29 413.546 54.07 419.006 54.07C423.766 54.07 427.966 51.69 429.016 48.75L430.066 49.17C427.546 55.54 422.576 61.98 414.246 61.98C405.076 61.98 399.896 53.79 399.896 45.39C399.896 36.36 405.496 26 416.906 26C425.236 26 430.346 31.6 430.416 41.05L430.486 41.19H430.416ZM421.386 39.16V38.53C421.386 31.04 418.376 28.1 415.296 28.1C409.976 28.1 407.806 34.47 407.806 38.67V39.16H421.386Z"
|
||||||
|
fill="#121212"
|
||||||
|
/>
|
||||||
<defs>
|
<defs>
|
||||||
<radialGradient id="paint0_angular_1555_220" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(40 40) rotate(90) scale(40)">
|
<radialGradient
|
||||||
|
id="paint0_angular_1555_220"
|
||||||
|
cx="0"
|
||||||
|
cy="0"
|
||||||
|
r="1"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="translate(40 40) rotate(90) scale(40)"
|
||||||
|
>
|
||||||
<stop stopColor="#FBFBFB" stopOpacity="0.15" />
|
<stop stopColor="#FBFBFB" stopOpacity="0.15" />
|
||||||
<stop offset="0.442708" stopOpacity="0.1" />
|
<stop offset="0.442708" stopOpacity="0.1" />
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
</defs>
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default InstallClient
|
export default InstallClient
|
||||||
|
|
@ -1,15 +1,14 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import InstallClient from './install'
|
import InstallClient from './install'
|
||||||
|
|
||||||
|
|
||||||
export const metadata = {
|
export const metadata = {
|
||||||
title: "Install LearnHouse",
|
title: 'Install LearnHouse',
|
||||||
description: "Install Learnhouse on your server",
|
description: 'Install Learnhouse on your server',
|
||||||
}
|
}
|
||||||
|
|
||||||
function InstallPage() {
|
function InstallPage() {
|
||||||
return (
|
return (
|
||||||
<div className='bg-white h-screen'>
|
<div className="bg-white h-screen">
|
||||||
<InstallClient />
|
<InstallClient />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,58 @@
|
||||||
"use client";
|
'use client'
|
||||||
import FormLayout, { ButtonBlack, FormField, FormLabelAndMessage, Input } from '@components/StyledElements/Form/Form'
|
import FormLayout, {
|
||||||
import * as Form from '@radix-ui/react-form';
|
ButtonBlack,
|
||||||
import { getAPIUrl } from '@services/config/config';
|
FormField,
|
||||||
import { createNewUserInstall, updateInstall } from '@services/install/install';
|
FormLabelAndMessage,
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
Input,
|
||||||
import { useFormik } from 'formik';
|
} from '@components/StyledElements/Form/Form'
|
||||||
import { useRouter } from 'next/navigation';
|
import * as Form from '@radix-ui/react-form'
|
||||||
|
import { getAPIUrl } from '@services/config/config'
|
||||||
|
import { createNewUserInstall, updateInstall } from '@services/install/install'
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { BarLoader } from 'react-spinners';
|
import { BarLoader } from 'react-spinners'
|
||||||
import useSWR from "swr";
|
import useSWR from 'swr'
|
||||||
|
|
||||||
const validate = (values: any) => {
|
const validate = (values: any) => {
|
||||||
const errors: any = {};
|
const errors: any = {}
|
||||||
|
|
||||||
if (!values.email) {
|
if (!values.email) {
|
||||||
errors.email = 'Required';
|
errors.email = 'Required'
|
||||||
}
|
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
|
||||||
else if (
|
errors.email = 'Invalid email address'
|
||||||
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
|
|
||||||
) {
|
|
||||||
errors.email = 'Invalid email address';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.password) {
|
if (!values.password) {
|
||||||
errors.password = 'Required';
|
errors.password = 'Required'
|
||||||
}
|
} else if (values.password.length < 8) {
|
||||||
else if (values.password.length < 8) {
|
errors.password = 'Password must be at least 8 characters'
|
||||||
errors.password = 'Password must be at least 8 characters';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.confirmPassword) {
|
if (!values.confirmPassword) {
|
||||||
errors.confirmPassword = 'Required';
|
errors.confirmPassword = 'Required'
|
||||||
}
|
} else if (values.confirmPassword !== values.password) {
|
||||||
else if (values.confirmPassword !== values.password) {
|
errors.confirmPassword = 'Passwords must match'
|
||||||
errors.confirmPassword = 'Passwords must match';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.username) {
|
if (!values.username) {
|
||||||
errors.username = 'Required';
|
errors.username = 'Required'
|
||||||
}
|
} else if (values.username.length < 3) {
|
||||||
else if (values.username.length < 3) {
|
errors.username = 'Username must be at least 3 characters'
|
||||||
errors.username = 'Username must be at least 3 characters';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors
|
||||||
};
|
}
|
||||||
|
|
||||||
function AccountCreation() {
|
function AccountCreation() {
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
const {
|
||||||
const router = useRouter(
|
data: install,
|
||||||
|
error: error,
|
||||||
)
|
isLoading,
|
||||||
|
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
|
||||||
|
const router = useRouter()
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
org_slug: '',
|
org_slug: '',
|
||||||
|
|
@ -61,12 +62,26 @@ function AccountCreation() {
|
||||||
username: '',
|
username: '',
|
||||||
},
|
},
|
||||||
validate,
|
validate,
|
||||||
onSubmit: async values => {
|
onSubmit: async (values) => {
|
||||||
|
let finalvalueswithoutpasswords = {
|
||||||
let finalvalueswithoutpasswords = { ...values, password: '', confirmPassword: '', org_slug: install.data[1].slug }
|
...values,
|
||||||
let install_data_without_passwords = { ...install.data, 3: finalvalueswithoutpasswords }
|
password: '',
|
||||||
|
confirmPassword: '',
|
||||||
|
org_slug: install.data[1].slug,
|
||||||
|
}
|
||||||
|
let install_data_without_passwords = {
|
||||||
|
...install.data,
|
||||||
|
3: finalvalueswithoutpasswords,
|
||||||
|
}
|
||||||
await updateInstall({ ...install_data_without_passwords }, 4)
|
await updateInstall({ ...install_data_without_passwords }, 4)
|
||||||
await createNewUserInstall({email:values.email,username:values.username,password:values.password},install.data[1].slug)
|
await createNewUserInstall(
|
||||||
|
{
|
||||||
|
email: values.email,
|
||||||
|
username: values.username,
|
||||||
|
password: values.password,
|
||||||
|
},
|
||||||
|
install.data[1].slug
|
||||||
|
)
|
||||||
|
|
||||||
// await 2 seconds
|
// await 2 seconds
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
|
@ -74,55 +89,87 @@ function AccountCreation() {
|
||||||
}, 2000)
|
}, 2000)
|
||||||
|
|
||||||
router.push('/install?step=4')
|
router.push('/install?step=4')
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormLayout onSubmit={formik.handleSubmit}>
|
<FormLayout onSubmit={formik.handleSubmit}>
|
||||||
<FormField name="email">
|
<FormField name="email">
|
||||||
<FormLabelAndMessage label='Email' message={formik.errors.email} />
|
<FormLabelAndMessage label="Email" message={formik.errors.email} />
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.email}
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
{/* for password */}
|
{/* for password */}
|
||||||
<FormField name="password">
|
<FormField name="password">
|
||||||
<FormLabelAndMessage label='Password' message={formik.errors.password} />
|
<FormLabelAndMessage
|
||||||
|
label="Password"
|
||||||
|
message={formik.errors.password}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.password}
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
{/* for confirm password */}
|
{/* for confirm password */}
|
||||||
<FormField name="confirmPassword">
|
<FormField name="confirmPassword">
|
||||||
|
<FormLabelAndMessage
|
||||||
<FormLabelAndMessage label='Confirm Password' message={formik.errors.confirmPassword} />
|
label="Confirm Password"
|
||||||
|
message={formik.errors.confirmPassword}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.confirmPassword} type="password" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.confirmPassword}
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
{/* for username */}
|
{/* for username */}
|
||||||
<FormField name="username">
|
<FormField name="username">
|
||||||
|
<FormLabelAndMessage
|
||||||
<FormLabelAndMessage label='Username' message={formik.errors.username} />
|
label="Username"
|
||||||
|
message={formik.errors.username}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.username}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<div className="flex flex-row-reverse py-4">
|
<div className="flex flex-row-reverse py-4">
|
||||||
<Form.Submit asChild>
|
<Form.Submit asChild>
|
||||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||||
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
|
{isSubmitting ? (
|
||||||
: "Create Admin Account"}
|
<BarLoader
|
||||||
|
cssOverride={{ borderRadius: 60 }}
|
||||||
|
width={60}
|
||||||
|
color="#ffffff"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
'Create Admin Account'
|
||||||
|
)}
|
||||||
</ButtonBlack>
|
</ButtonBlack>
|
||||||
</Form.Submit>
|
</Form.Submit>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,18 @@
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { createDefaultElements, updateInstall } from '@services/install/install';
|
import { createDefaultElements, updateInstall } from '@services/install/install'
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import useSWR from "swr";
|
import useSWR from 'swr'
|
||||||
|
|
||||||
function DefaultElements() {
|
function DefaultElements() {
|
||||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
const {
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
data: install,
|
||||||
const [isSubmitted, setIsSubmitted] = React.useState(false);
|
error: error,
|
||||||
|
isLoading,
|
||||||
|
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
|
||||||
|
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||||
|
const [isSubmitted, setIsSubmitted] = React.useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
function createDefElementsAndUpdateInstall() {
|
function createDefElementsAndUpdateInstall() {
|
||||||
|
|
@ -26,16 +30,16 @@ function DefaultElements() {
|
||||||
|
|
||||||
router.push('/install?step=3')
|
router.push('/install?step=3')
|
||||||
setIsSubmitted(true)
|
setIsSubmitted(true)
|
||||||
}
|
} catch (e) {}
|
||||||
catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex py-10 justify-center items-center space-x-3'>
|
<div className="flex py-10 justify-center items-center space-x-3">
|
||||||
<h1>Install Default Elements </h1>
|
<h1>Install Default Elements </h1>
|
||||||
<div onClick={createDefElementsAndUpdateInstall} className='p-3 font-bold bg-gray-200 text-gray-900 rounded-lg hover:cursor-pointer' >
|
<div
|
||||||
|
onClick={createDefElementsAndUpdateInstall}
|
||||||
|
className="p-3 font-bold bg-gray-200 text-gray-900 rounded-lg hover:cursor-pointer"
|
||||||
|
>
|
||||||
Install
|
Install
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -3,15 +3,29 @@ import React from 'react'
|
||||||
|
|
||||||
function DisableInstallMode() {
|
function DisableInstallMode() {
|
||||||
return (
|
return (
|
||||||
<div className='p-4 bg-green-300 text-green-950 rounded-md flex space-x-4 items-center'>
|
<div className="p-4 bg-green-300 text-green-950 rounded-md flex space-x-4 items-center">
|
||||||
<div>
|
<div>
|
||||||
<Check size={32} />
|
<Check size={32} />
|
||||||
</div>
|
</div>
|
||||||
<div><p className='font-bold text-lg'>You have reached the end of the Installation process, <b><i>please don't forget to disable installation mode.</i></b> </p>
|
<div>
|
||||||
<div className='flex space-x-2 items-center'>
|
<p className="font-bold text-lg">
|
||||||
|
You have reached the end of the Installation process,{' '}
|
||||||
|
<b>
|
||||||
|
<i>please don't forget to disable installation mode.</i>
|
||||||
|
</b>{' '}
|
||||||
|
</p>
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
<Link size={20} />
|
<Link size={20} />
|
||||||
<a rel='noreferrer' target='_blank' className="text-blue-950 font-medium" href="http://docs.learnhouse.app">LearnHouse Docs</a>
|
<a
|
||||||
</div></div>
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
className="text-blue-950 font-medium"
|
||||||
|
href="http://docs.learnhouse.app"
|
||||||
|
>
|
||||||
|
LearnHouse Docs
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,38 +1,41 @@
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { updateInstall } from '@services/install/install';
|
import { updateInstall } from '@services/install/install'
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import { Check } from 'lucide-react'
|
import { Check } from 'lucide-react'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import useSWR from "swr";
|
import useSWR from 'swr'
|
||||||
|
|
||||||
const Finish = () => {
|
const Finish = () => {
|
||||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
const {
|
||||||
|
data: install,
|
||||||
|
error: error,
|
||||||
|
isLoading,
|
||||||
|
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
async function finishInstall() {
|
async function finishInstall() {
|
||||||
|
|
||||||
let install_data = { ...install.data, 5: { status: 'OK' } }
|
let install_data = { ...install.data, 5: { status: 'OK' } }
|
||||||
|
|
||||||
let data = await updateInstall(install_data, 6)
|
let data = await updateInstall(install_data, 6)
|
||||||
if (data) {
|
if (data) {
|
||||||
router.push('/install?step=6')
|
router.push('/install?step=6')
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex py-10 justify-center items-center space-x-3'>
|
<div className="flex py-10 justify-center items-center space-x-3">
|
||||||
<h1>Installation Complete</h1>
|
<h1>Installation Complete</h1>
|
||||||
<br />
|
<br />
|
||||||
<Check size={32} />
|
<Check size={32} />
|
||||||
<div onClick={finishInstall} className='p-3 font-bold bg-gray-200 text-gray-900 rounded-lg hover:cursor-pointer' >
|
<div
|
||||||
|
onClick={finishInstall}
|
||||||
|
className="p-3 font-bold bg-gray-200 text-gray-900 rounded-lg hover:cursor-pointer"
|
||||||
|
>
|
||||||
Next Step
|
Next Step
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,32 @@
|
||||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import useSWR, { mutate } from "swr";
|
import useSWR, { mutate } from 'swr'
|
||||||
|
|
||||||
function GetStarted() {
|
function GetStarted() {
|
||||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
const {
|
||||||
|
data: install,
|
||||||
|
error: error,
|
||||||
|
isLoading,
|
||||||
|
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
async function startInstallation() {
|
async function startInstallation() {
|
||||||
let res = await fetch(`${getAPIUrl()}install/start`, {
|
let res = await fetch(`${getAPIUrl()}install/start`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json'
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: JSON.stringify({})
|
body: JSON.stringify({}),
|
||||||
})
|
})
|
||||||
|
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
mutate(`${getAPIUrl()}install/latest`)
|
mutate(`${getAPIUrl()}install/latest`)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
router.push(`/install?step=1`)
|
router.push(`/install?step=1`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function redirectToStep() {
|
function redirectToStep() {
|
||||||
|
|
@ -32,32 +34,41 @@ function GetStarted() {
|
||||||
router.push(`/install?step=${step}`)
|
router.push(`/install?step=${step}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (install) {
|
if (install) {
|
||||||
redirectToStep()
|
redirectToStep()
|
||||||
}
|
}
|
||||||
}, [install])
|
}, [install])
|
||||||
|
|
||||||
|
if (error)
|
||||||
if (error) return <div className='flex py-10 justify-center items-center space-x-3'>
|
return (
|
||||||
|
<div className="flex py-10 justify-center items-center space-x-3">
|
||||||
<h1>Start a new installation</h1>
|
<h1>Start a new installation</h1>
|
||||||
<div onClick={startInstallation} className='p-3 font-bold bg-green-200 text-green-900 rounded-lg hover:cursor-pointer' >
|
<div
|
||||||
|
onClick={startInstallation}
|
||||||
|
className="p-3 font-bold bg-green-200 text-green-900 rounded-lg hover:cursor-pointer"
|
||||||
|
>
|
||||||
Start
|
Start
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
if (isLoading) return <PageLoading />
|
if (isLoading) return <PageLoading />
|
||||||
if (install) {
|
if (install) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='flex py-10 justify-center items-center space-x-3'>
|
<div className="flex py-10 justify-center items-center space-x-3">
|
||||||
<h1>You already started an installation</h1>
|
<h1>You already started an installation</h1>
|
||||||
<div onClick={redirectToStep} className='p-3 font-bold bg-orange-200 text-orange-900 rounded-lg hover:cursor-pointer' >
|
<div
|
||||||
|
onClick={redirectToStep}
|
||||||
|
className="p-3 font-bold bg-orange-200 text-orange-900 rounded-lg hover:cursor-pointer"
|
||||||
|
>
|
||||||
Continue
|
Continue
|
||||||
</div>
|
</div>
|
||||||
<div onClick={startInstallation} className='p-3 font-bold bg-green-200 text-green-900 rounded-lg hover:cursor-pointer' >
|
<div
|
||||||
|
onClick={startInstallation}
|
||||||
|
className="p-3 font-bold bg-green-200 text-green-900 rounded-lg hover:cursor-pointer"
|
||||||
|
>
|
||||||
Start
|
Start
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,59 @@
|
||||||
|
import FormLayout, {
|
||||||
import FormLayout, { ButtonBlack, FormField, FormLabelAndMessage, Input } from '@components/StyledElements/Form/Form'
|
ButtonBlack,
|
||||||
import * as Form from '@radix-ui/react-form';
|
FormField,
|
||||||
import { useFormik } from 'formik';
|
FormLabelAndMessage,
|
||||||
import { BarLoader } from 'react-spinners';
|
Input,
|
||||||
|
} from '@components/StyledElements/Form/Form'
|
||||||
|
import * as Form from '@radix-ui/react-form'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
|
import { BarLoader } from 'react-spinners'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import useSWR from "swr";
|
import useSWR from 'swr'
|
||||||
import { createNewOrgInstall, updateInstall } from '@services/install/install';
|
import { createNewOrgInstall, updateInstall } from '@services/install/install'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation'
|
||||||
import { Check } from 'lucide-react';
|
import { Check } from 'lucide-react'
|
||||||
|
|
||||||
const validate = (values: any) => {
|
const validate = (values: any) => {
|
||||||
const errors: any = {};
|
const errors: any = {}
|
||||||
|
|
||||||
if (!values.name) {
|
if (!values.name) {
|
||||||
errors.name = 'Required';
|
errors.name = 'Required'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.description) {
|
if (!values.description) {
|
||||||
errors.description = 'Required';
|
errors.description = 'Required'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.slug) {
|
if (!values.slug) {
|
||||||
errors.slug = 'Required';
|
errors.slug = 'Required'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.email) {
|
if (!values.email) {
|
||||||
errors.email = 'Required';
|
errors.email = 'Required'
|
||||||
}
|
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
|
||||||
else if (
|
errors.email = 'Invalid email address'
|
||||||
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
|
|
||||||
) {
|
|
||||||
errors.email = 'Invalid email address';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
|
}
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
function OrgCreation() {
|
function OrgCreation() {
|
||||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
const {
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
data: install,
|
||||||
const [isSubmitted, setIsSubmitted] = React.useState(false);
|
error: error,
|
||||||
|
isLoading,
|
||||||
|
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
|
||||||
|
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||||
|
const [isSubmitted, setIsSubmitted] = React.useState(false)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
|
||||||
function createOrgAndUpdateInstall(values: any) {
|
function createOrgAndUpdateInstall(values: any) {
|
||||||
try {
|
try {
|
||||||
createNewOrgInstall(values)
|
createNewOrgInstall(values)
|
||||||
install.data = {
|
install.data = {
|
||||||
1: values
|
1: values,
|
||||||
}
|
}
|
||||||
let install_data = { ...install.data, 1: values }
|
let install_data = { ...install.data, 1: values }
|
||||||
updateInstall(install_data, 2)
|
updateInstall(install_data, 2)
|
||||||
|
|
@ -62,11 +64,7 @@ function OrgCreation() {
|
||||||
|
|
||||||
router.push('/install?step=2')
|
router.push('/install?step=2')
|
||||||
setIsSubmitted(true)
|
setIsSubmitted(true)
|
||||||
}
|
} catch (e) {}
|
||||||
catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
|
|
@ -77,58 +75,89 @@ function OrgCreation() {
|
||||||
email: '',
|
email: '',
|
||||||
},
|
},
|
||||||
validate,
|
validate,
|
||||||
onSubmit: values => {
|
onSubmit: (values) => {
|
||||||
createOrgAndUpdateInstall(values)
|
createOrgAndUpdateInstall(values)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<FormLayout onSubmit={formik.handleSubmit}>
|
<FormLayout onSubmit={formik.handleSubmit}>
|
||||||
<FormField name="name">
|
<FormField name="name">
|
||||||
<FormLabelAndMessage label='Name' message={formik.errors.name} />
|
<FormLabelAndMessage label="Name" message={formik.errors.name} />
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.name} type="text" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.name}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField name="description">
|
<FormField name="description">
|
||||||
<FormLabelAndMessage label='Description' message={formik.errors.description} />
|
<FormLabelAndMessage
|
||||||
|
label="Description"
|
||||||
|
message={formik.errors.description}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.description} type="text" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.description}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField name="slug">
|
<FormField name="slug">
|
||||||
|
<FormLabelAndMessage label="Slug" message={formik.errors.slug} />
|
||||||
<FormLabelAndMessage label='Slug' message={formik.errors.slug} />
|
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.slug} type="text" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.slug}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
{/* for username */}
|
{/* for username */}
|
||||||
<FormField name="email">
|
<FormField name="email">
|
||||||
|
<FormLabelAndMessage label="Email" message={formik.errors.email} />
|
||||||
<FormLabelAndMessage label='Email' message={formik.errors.email} />
|
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.email}
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<div className="flex flex-row-reverse py-4">
|
<div className="flex flex-row-reverse py-4">
|
||||||
<Form.Submit asChild>
|
<Form.Submit asChild>
|
||||||
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
|
||||||
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
|
{isSubmitting ? (
|
||||||
: "Create Organization"}
|
<BarLoader
|
||||||
|
cssOverride={{ borderRadius: 60 }}
|
||||||
|
width={60}
|
||||||
|
color="#ffffff"
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
'Create Organization'
|
||||||
|
)}
|
||||||
</ButtonBlack>
|
</ButtonBlack>
|
||||||
</Form.Submit>
|
</Form.Submit>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isSubmitted && <div className='flex space-x-3'> <Check /> Organization Created Successfully</div>}
|
{isSubmitted && (
|
||||||
|
<div className="flex space-x-3">
|
||||||
|
{' '}
|
||||||
|
<Check /> Organization Created Successfully
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,22 @@
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { createSampleDataInstall, updateInstall } from '@services/install/install';
|
import {
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
createSampleDataInstall,
|
||||||
import { useRouter } from 'next/navigation';
|
updateInstall,
|
||||||
|
} from '@services/install/install'
|
||||||
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import useSWR from "swr";
|
import useSWR from 'swr'
|
||||||
|
|
||||||
function SampleData() {
|
function SampleData() {
|
||||||
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
|
const {
|
||||||
|
data: install,
|
||||||
|
error: error,
|
||||||
|
isLoading,
|
||||||
|
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
function createSampleData() {
|
function createSampleData() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let username = install.data[3].username
|
let username = install.data[3].username
|
||||||
let slug = install.data[1].slug
|
let slug = install.data[1].slug
|
||||||
|
|
@ -21,19 +27,16 @@ function SampleData() {
|
||||||
updateInstall(install_data, 5)
|
updateInstall(install_data, 5)
|
||||||
|
|
||||||
router.push('/install?step=5')
|
router.push('/install?step=5')
|
||||||
|
} catch (e) {}
|
||||||
}
|
}
|
||||||
catch (e) {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex py-10 justify-center items-center space-x-3'>
|
<div className="flex py-10 justify-center items-center space-x-3">
|
||||||
<h1>Install Sample data on your organization </h1>
|
<h1>Install Sample data on your organization </h1>
|
||||||
<div onClick={createSampleData} className='p-3 font-bold bg-purple-200 text-pruple-900 rounded-lg hover:cursor-pointer' >
|
<div
|
||||||
|
onClick={createSampleData}
|
||||||
|
className="p-3 font-bold bg-purple-200 text-pruple-900 rounded-lg hover:cursor-pointer"
|
||||||
|
>
|
||||||
Start
|
Start
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,53 +1,52 @@
|
||||||
import AccountCreation from "./account_creation";
|
import AccountCreation from './account_creation'
|
||||||
import DefaultElements from "./default_elements";
|
import DefaultElements from './default_elements'
|
||||||
import DisableInstallMode from "./disable_install_mode";
|
import DisableInstallMode from './disable_install_mode'
|
||||||
import Finish from "./finish";
|
import Finish from './finish'
|
||||||
import GetStarted from "./get_started";
|
import GetStarted from './get_started'
|
||||||
import OrgCreation from "./org_creation";
|
import OrgCreation from './org_creation'
|
||||||
import SampleData from "./sample_data";
|
import SampleData from './sample_data'
|
||||||
|
|
||||||
export const INSTALL_STEPS = [
|
export const INSTALL_STEPS = [
|
||||||
{
|
{
|
||||||
id: "INSTALL_STATUS",
|
id: 'INSTALL_STATUS',
|
||||||
name: "Get started",
|
name: 'Get started',
|
||||||
component: <GetStarted />,
|
component: <GetStarted />,
|
||||||
completed: false,
|
completed: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ORGANIZATION_CREATION",
|
id: 'ORGANIZATION_CREATION',
|
||||||
name: "Organization Creation",
|
name: 'Organization Creation',
|
||||||
component: <OrgCreation />,
|
component: <OrgCreation />,
|
||||||
completed: false,
|
completed: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "DEFAULT_ELEMENTS",
|
id: 'DEFAULT_ELEMENTS',
|
||||||
name: "Default Elements",
|
name: 'Default Elements',
|
||||||
component: <DefaultElements />,
|
component: <DefaultElements />,
|
||||||
completed: false,
|
completed: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ACCOUNT_CREATION",
|
id: 'ACCOUNT_CREATION',
|
||||||
name: "Account Creation",
|
name: 'Account Creation',
|
||||||
component: <AccountCreation />,
|
component: <AccountCreation />,
|
||||||
completed: false,
|
completed: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "SAMPLE_DATA",
|
id: 'SAMPLE_DATA',
|
||||||
name: "Sample Data",
|
name: 'Sample Data',
|
||||||
component: <SampleData />,
|
component: <SampleData />,
|
||||||
completed: false,
|
completed: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "FINISH",
|
id: 'FINISH',
|
||||||
name: "Finish",
|
name: 'Finish',
|
||||||
component: <Finish />,
|
component: <Finish />,
|
||||||
completed: false,
|
completed: false,
|
||||||
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "DISABLING_INSTALLATION_MODE",
|
id: 'DISABLING_INSTALLATION_MODE',
|
||||||
name: "Disabling Installation Mode",
|
name: 'Disabling Installation Mode',
|
||||||
component: <DisableInstallMode />,
|
component: <DisableInstallMode />,
|
||||||
completed: false,
|
completed: false,
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
"use client";
|
'use client'
|
||||||
import "../styles/globals.css";
|
import '../styles/globals.css'
|
||||||
import StyledComponentsRegistry from "../components/Utils/libs/styled-registry";
|
import StyledComponentsRegistry from '../components/Utils/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
|
||||||
|
}) {
|
||||||
const variants = {
|
const variants = {
|
||||||
hidden: { opacity: 0, x: 0, y: 0 },
|
hidden: { opacity: 0, x: 0, y: 0 },
|
||||||
enter: { opacity: 1, x: 0, y: 0 },
|
enter: { opacity: 1, x: 0, y: 0 },
|
||||||
exit: { opacity: 0, x: 0, y: 0 },
|
exit: { opacity: 0, x: 0, y: 0 },
|
||||||
};
|
}
|
||||||
return (
|
return (
|
||||||
<html className="" lang="en">
|
<html className="" lang="en">
|
||||||
<head />
|
<head />
|
||||||
|
|
@ -20,7 +24,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||||
initial="hidden" // Set the initial state to variants.hidden
|
initial="hidden" // Set the initial state to variants.hidden
|
||||||
animate="enter" // Animated state to variants.enter
|
animate="enter" // Animated state to variants.enter
|
||||||
exit="exit" // Exit state (used later) to variants.exit
|
exit="exit" // Exit state (used later) to variants.exit
|
||||||
transition={{ type: "linear" }} // Set the transition to linear
|
transition={{ type: 'linear' }} // Set the transition to linear
|
||||||
className=""
|
className=""
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -28,5 +32,5 @@ export default function RootLayout({ children }: { children: React.ReactNode })
|
||||||
</StyledComponentsRegistry>
|
</StyledComponentsRegistry>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,43 @@
|
||||||
"use client";
|
'use client'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import { createNewOrganization } from "../../../services/organizations/orgs";
|
import { createNewOrganization } from '../../../services/organizations/orgs'
|
||||||
|
|
||||||
const Organizations = () => {
|
const Organizations = () => {
|
||||||
const [name, setName] = React.useState("");
|
const [name, setName] = React.useState('')
|
||||||
const [description, setDescription] = React.useState("");
|
const [description, setDescription] = React.useState('')
|
||||||
const [email, setEmail] = React.useState("");
|
const [email, setEmail] = React.useState('')
|
||||||
const [slug, setSlug] = React.useState("");
|
const [slug, setSlug] = React.useState('')
|
||||||
|
|
||||||
const handleNameChange = (e: any) => {
|
const handleNameChange = (e: any) => {
|
||||||
setName(e.target.value);
|
setName(e.target.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDescriptionChange = (e: any) => {
|
const handleDescriptionChange = (e: any) => {
|
||||||
setDescription(e.target.value);
|
setDescription(e.target.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleEmailChange = (e: any) => {
|
const handleEmailChange = (e: any) => {
|
||||||
setEmail(e.target.value);
|
setEmail(e.target.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSlugChange = (e: any) => {
|
const handleSlugChange = (e: any) => {
|
||||||
setSlug(e.target.value);
|
setSlug(e.target.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: any) => {
|
const handleSubmit = async (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
let logo = ''
|
let logo = ''
|
||||||
const status = await createNewOrganization({ name, description, email, logo, slug, default: false });
|
const status = await createNewOrganization({
|
||||||
alert(JSON.stringify(status));
|
name,
|
||||||
};
|
description,
|
||||||
|
email,
|
||||||
|
logo,
|
||||||
|
slug,
|
||||||
|
default: false,
|
||||||
|
})
|
||||||
|
alert(JSON.stringify(status))
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -45,7 +52,7 @@ const Organizations = () => {
|
||||||
<br />
|
<br />
|
||||||
<button onClick={handleSubmit}>Create</button>
|
<button onClick={handleSubmit}>Create</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Organizations;
|
export default Organizations
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,30 @@
|
||||||
"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 { 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/ts/requests";
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
||||||
|
|
||||||
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
|
||||||
|
)
|
||||||
|
|
||||||
async function deleteOrganization(org_id: any) {
|
async function deleteOrganization(org_id: any) {
|
||||||
const response = await deleteOrganizationFromBackend(org_id);
|
const response = await deleteOrganizationFromBackend(org_id)
|
||||||
response && mutate(`${getAPIUrl()}orgs/user/page/1/limit/10`, organizations.filter((org: any) => org.org_id !== org_id));
|
response &&
|
||||||
|
mutate(
|
||||||
|
`${getAPIUrl()}orgs/user/page/1/limit/10`,
|
||||||
|
organizations.filter((org: any) => org.org_id !== org_id)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="font-bold text-lg">
|
<div className="font-bold text-lg">
|
||||||
Your Organizations{" "}
|
Your Organizations{' '}
|
||||||
<Link href="/organizations/new">
|
<Link href="/organizations/new">
|
||||||
<button className="bg-blue-500 text-white px-2 py-1 rounded-md hover:bg-blue-600 focus:outline-none">
|
<button className="bg-blue-500 text-white px-2 py-1 rounded-md hover:bg-blue-600 focus:outline-none">
|
||||||
+
|
+
|
||||||
|
|
@ -32,9 +39,14 @@ const Organizations = () => {
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{organizations.map((org: any) => (
|
{organizations.map((org: any) => (
|
||||||
<div key={org.org_id} className="flex items-center justify-between mb-4">
|
<div
|
||||||
<Link href={getUriWithOrg(org.slug, "/")}>
|
key={org.org_id}
|
||||||
<h3 className="text-blue-500 cursor-pointer hover:underline">{org.name}</h3>
|
className="flex items-center justify-between mb-4"
|
||||||
|
>
|
||||||
|
<Link href={getUriWithOrg(org.slug, '/')}>
|
||||||
|
<h3 className="text-blue-500 cursor-pointer hover:underline">
|
||||||
|
{org.name}
|
||||||
|
</h3>
|
||||||
</Link>
|
</Link>
|
||||||
<button
|
<button
|
||||||
onClick={() => deleteOrganization(org.org_id)}
|
onClick={() => deleteOrganization(org.org_id)}
|
||||||
|
|
@ -46,9 +58,8 @@ const Organizations = () => {
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default Organizations;
|
export default Organizations
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
'use client'; // Error components must be Client Components
|
'use client' // Error components must be Client Components
|
||||||
|
|
||||||
import ErrorUI from '@components/StyledElements/Error/Error';
|
import ErrorUI from '@components/StyledElements/Error/Error'
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
reset,
|
reset,
|
||||||
}: {
|
}: {
|
||||||
error: Error;
|
error: Error
|
||||||
reset: () => void;
|
reset: () => void
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Log the error to an error reporting service
|
// Log the error to an error reporting service
|
||||||
console.error(error);
|
console.error(error)
|
||||||
}, [error]);
|
}, [error])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ErrorUI></ErrorUI>
|
<ErrorUI></ErrorUI>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return <PageLoading></PageLoading>
|
||||||
<PageLoading></PageLoading>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,27 +1,34 @@
|
||||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
|
||||||
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
|
||||||
import { getUriWithOrg } from "@services/config/config";
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import { getCollectionByIdWithAuthHeader } from "@services/courses/collections";
|
import { getCollectionByIdWithAuthHeader } from '@services/courses/collections'
|
||||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { getOrganizationContextInfo } from '@services/organizations/orgs'
|
||||||
import { Metadata } from "next";
|
import { Metadata } from 'next'
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from 'next/headers'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseid: string, collectionid: string };
|
params: { orgslug: string; courseid: string; collectionid: string }
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata({
|
||||||
{ params }: MetadataProps,
|
params,
|
||||||
): Promise<Metadata> {
|
}: MetadataProps): Promise<Metadata> {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies()
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
|
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(params.orgslug, {
|
||||||
const col = await getCollectionByIdWithAuthHeader(params.collectionid, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
|
revalidate: 1800,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
const col = await getCollectionByIdWithAuthHeader(
|
||||||
|
params.collectionid,
|
||||||
|
access_token ? access_token : null,
|
||||||
|
{ revalidate: 0, tags: ['collections'] }
|
||||||
|
)
|
||||||
|
|
||||||
// SEO
|
// SEO
|
||||||
return {
|
return {
|
||||||
|
|
@ -34,48 +41,66 @@ export async function generateMetadata(
|
||||||
googleBot: {
|
googleBot: {
|
||||||
index: true,
|
index: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
"max-image-preview": "large",
|
'max-image-preview': 'large',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `Collection : ${col.name} — ${org.name}`,
|
title: `Collection : ${col.name} — ${org.name}`,
|
||||||
description: `${col.description} `,
|
description: `${col.description} `,
|
||||||
type: 'website',
|
type: 'website',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollectionPage = async (params: any) => {
|
const CollectionPage = async (params: any) => {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies()
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const org = await getOrganizationContextInfo(params.params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(params.params.orgslug, {
|
||||||
const orgslug = params.params.orgslug;
|
revalidate: 1800,
|
||||||
const col = await getCollectionByIdWithAuthHeader(params.params.collectionid, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
const orgslug = params.params.orgslug
|
||||||
|
const col = await getCollectionByIdWithAuthHeader(
|
||||||
|
params.params.collectionid,
|
||||||
|
access_token ? access_token : null,
|
||||||
|
{ revalidate: 0, tags: ['collections'] }
|
||||||
|
)
|
||||||
|
|
||||||
const removeCoursePrefix = (courseid: string) => {
|
const removeCoursePrefix = (courseid: string) => {
|
||||||
return courseid.replace("course_", "")
|
return courseid.replace('course_', '')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
return <GeneralWrapperStyled>
|
<GeneralWrapperStyled>
|
||||||
<h2 className="text-sm font-bold text-gray-400">Collection</h2>
|
<h2 className="text-sm font-bold text-gray-400">Collection</h2>
|
||||||
<h1 className="text-3xl font-bold">{col.name}</h1>
|
<h1 className="text-3xl font-bold">{col.name}</h1>
|
||||||
<br />
|
<br />
|
||||||
<div className="home_courses flex flex-wrap">
|
<div className="home_courses flex flex-wrap">
|
||||||
{col.courses.map((course: any) => (
|
{col.courses.map((course: any) => (
|
||||||
<div className="pr-8" key={course.course_uuid}>
|
<div className="pr-8" key={course.course_uuid}>
|
||||||
<Link href={getUriWithOrg(orgslug, "/course/" + removeCoursePrefix(course.course_uuid))}>
|
<Link
|
||||||
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org.org_uuid, course.course_uuid, course.thumbnail_image)})` }}>
|
href={getUriWithOrg(
|
||||||
</div>
|
orgslug,
|
||||||
|
'/course/' + removeCoursePrefix(course.course_uuid)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-[249px] h-[131px] bg-cover"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url(${getCourseThumbnailMediaDirectory(
|
||||||
|
org.org_uuid,
|
||||||
|
course.course_uuid,
|
||||||
|
course.thumbnail_image
|
||||||
|
)})`,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
</Link>
|
</Link>
|
||||||
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>
|
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</GeneralWrapperStyled>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CollectionPage
|
||||||
|
|
||||||
</GeneralWrapperStyled>;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CollectionPage;
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
// Or a custom loading skeleton component
|
// Or a custom loading skeleton component
|
||||||
return (
|
return <PageLoading></PageLoading>
|
||||||
<PageLoading></PageLoading>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,37 +1,41 @@
|
||||||
"use client";
|
'use client'
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from 'next/navigation'
|
||||||
import React, { useState } from "react";
|
import React, { useState } from 'react'
|
||||||
import { createCollection } from "@services/courses/collections";
|
import { createCollection } from '@services/courses/collections'
|
||||||
import useSWR from "swr";
|
import useSWR from 'swr'
|
||||||
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
|
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
||||||
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
|
import { revalidateTags, swrFetcher } from '@services/utils/ts/requests'
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
|
||||||
function NewCollection(params: any) {
|
function NewCollection(params: any) {
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug
|
||||||
const [name, setName] = React.useState("");
|
const [name, setName] = React.useState('')
|
||||||
const [description, setDescription] = React.useState("");
|
const [description, setDescription] = React.useState('')
|
||||||
const [selectedCourses, setSelectedCourses] = React.useState([]) as any;
|
const [selectedCourses, setSelectedCourses] = React.useState([]) as any
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const { data: courses, error: error } = useSWR(`${getAPIUrl()}courses/org_slug/${orgslug}/page/1/limit/10`, swrFetcher);
|
const { data: courses, error: error } = useSWR(
|
||||||
const [isPublic, setIsPublic] = useState('true');
|
`${getAPIUrl()}courses/org_slug/${orgslug}/page/1/limit/10`,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
|
const [isPublic, setIsPublic] = useState('true')
|
||||||
|
|
||||||
const handleVisibilityChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
const handleVisibilityChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
|
||||||
setIsPublic(e.target.value);
|
setIsPublic(e.target.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setName(event.target.value);
|
setName(event.target.value)
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleDescriptionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleDescriptionChange = (
|
||||||
setDescription(event.target.value);
|
event: React.ChangeEvent<HTMLInputElement>
|
||||||
};
|
) => {
|
||||||
|
setDescription(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: any) => {
|
const handleSubmit = async (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
|
|
||||||
const collection = {
|
const collection = {
|
||||||
name: name,
|
name: name,
|
||||||
|
|
@ -39,19 +43,17 @@ function NewCollection(params: any) {
|
||||||
courses: selectedCourses,
|
courses: selectedCourses,
|
||||||
public: isPublic,
|
public: isPublic,
|
||||||
org_id: org.id,
|
org_id: org.id,
|
||||||
};
|
}
|
||||||
await createCollection(collection);
|
await createCollection(collection)
|
||||||
await revalidateTags(["collections"], org.slug);
|
await revalidateTags(['collections'], org.slug)
|
||||||
// reload the page
|
// reload the page
|
||||||
router.refresh();
|
router.refresh()
|
||||||
|
|
||||||
// wait for 2s before reloading the page
|
// wait for 2s before reloading the page
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push(getUriWithOrg(orgslug, "/collections"));
|
router.push(getUriWithOrg(orgslug, '/collections'))
|
||||||
|
}, 1000)
|
||||||
}
|
}
|
||||||
, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -75,15 +77,16 @@ function NewCollection(params: any) {
|
||||||
<option value="true">Public Collection </option>
|
<option value="true">Public Collection </option>
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
|
||||||
{!courses ? (
|
{!courses ? (
|
||||||
<p className="text-gray-500">Loading...</p>
|
<p className="text-gray-500">Loading...</p>
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-4 p-3">
|
<div className="space-y-4 p-3">
|
||||||
<p>Courses</p>
|
<p>Courses</p>
|
||||||
{courses.map((course: any) => (
|
{courses.map((course: any) => (
|
||||||
<div key={course.course_uuid} className="flex items-center space-x-2">
|
<div
|
||||||
|
key={course.course_uuid}
|
||||||
|
className="flex items-center space-x-2"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
id={course.id}
|
id={course.id}
|
||||||
|
|
@ -91,20 +94,29 @@ function NewCollection(params: any) {
|
||||||
value={course.id}
|
value={course.id}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
if (e.target.checked) {
|
if (e.target.checked) {
|
||||||
setSelectedCourses([...selectedCourses, course.id]);
|
setSelectedCourses([...selectedCourses, course.id])
|
||||||
}
|
} else {
|
||||||
else {
|
setSelectedCourses(
|
||||||
setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid));
|
selectedCourses.filter(
|
||||||
|
(course_uuid: any) =>
|
||||||
|
course_uuid !== course.course_uuid
|
||||||
|
)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
className="text-blue-500 rounded focus:ring-2 focus:ring-blue-500"
|
className="text-blue-500 rounded focus:ring-2 focus:ring-blue-500"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<label htmlFor={course.course_uuid} className="text-sm text-gray-700">{course.name}</label>
|
<label
|
||||||
|
htmlFor={course.course_uuid}
|
||||||
|
className="text-sm text-gray-700"
|
||||||
|
>
|
||||||
|
{course.name}
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
|
|
@ -121,9 +133,8 @@ function NewCollection(params: any) {
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NewCollection;
|
export default NewCollection
|
||||||
|
|
|
||||||
|
|
@ -1,26 +1,29 @@
|
||||||
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
|
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
|
||||||
import TypeOfContentTitle from "@components/StyledElements/Titles/TypeOfContentTitle";
|
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'
|
||||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
|
||||||
import { getUriWithOrg } from "@services/config/config";
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import { getOrgCollectionsWithAuthHeader } from "@services/courses/collections";
|
import { getOrgCollectionsWithAuthHeader } from '@services/courses/collections'
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { getOrganizationContextInfo } from '@services/organizations/orgs'
|
||||||
import { Metadata } from "next";
|
import { Metadata } from 'next'
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from 'next/headers'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
|
||||||
import CollectionThumbnail from "@components/Objects/Thumbnails/CollectionThumbnail";
|
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail'
|
||||||
import NewCollectionButton from "@components/StyledElements/Buttons/NewCollectionButton";
|
import NewCollectionButton from '@components/StyledElements/Buttons/NewCollectionButton'
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseid: string };
|
params: { orgslug: string; courseid: string }
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata({
|
||||||
{ params }: MetadataProps,
|
params,
|
||||||
): Promise<Metadata> {
|
}: MetadataProps): Promise<Metadata> {
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(params.orgslug, {
|
||||||
|
revalidate: 1800,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
|
||||||
// SEO
|
// SEO
|
||||||
return {
|
return {
|
||||||
|
|
@ -33,71 +36,116 @@ export async function generateMetadata(
|
||||||
googleBot: {
|
googleBot: {
|
||||||
index: true,
|
index: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
"max-image-preview": "large",
|
'max-image-preview': 'large',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `Collections — ${org.name}`,
|
title: `Collections — ${org.name}`,
|
||||||
description: `Collections of courses from ${org.name}`,
|
description: `Collections of courses from ${org.name}`,
|
||||||
type: 'website',
|
type: 'website',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CollectionsPage = async (params: any) => {
|
const CollectionsPage = async (params: any) => {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies()
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, {
|
||||||
const org_id = org.id;
|
revalidate: 1800,
|
||||||
const collections = await getOrgCollectionsWithAuthHeader(org_id, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
const org_id = org.id
|
||||||
|
const collections = await getOrgCollectionsWithAuthHeader(
|
||||||
|
org_id,
|
||||||
|
access_token ? access_token : null,
|
||||||
|
{ revalidate: 0, tags: ['collections'] }
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GeneralWrapperStyled>
|
<GeneralWrapperStyled>
|
||||||
<div className="flex justify-between" >
|
<div className="flex justify-between">
|
||||||
<TypeOfContentTitle title="Collections" type="col" />
|
<TypeOfContentTitle title="Collections" type="col" />
|
||||||
<AuthenticatedClientElement
|
<AuthenticatedClientElement
|
||||||
ressourceType="collections"
|
ressourceType="collections"
|
||||||
action="create"
|
action="create"
|
||||||
checkMethod='roles' orgId={org_id}>
|
checkMethod="roles"
|
||||||
<Link className="flex justify-center" href={getUriWithOrg(orgslug, "/collections/new")}>
|
orgId={org_id}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="flex justify-center"
|
||||||
|
href={getUriWithOrg(orgslug, '/collections/new')}
|
||||||
|
>
|
||||||
<NewCollectionButton />
|
<NewCollectionButton />
|
||||||
</Link>
|
</Link>
|
||||||
</AuthenticatedClientElement>
|
</AuthenticatedClientElement>
|
||||||
</div>
|
</div>
|
||||||
<div className="home_collections flex flex-wrap">
|
<div className="home_collections flex flex-wrap">
|
||||||
{collections.map((collection: any) => (
|
{collections.map((collection: any) => (
|
||||||
<div className="flex flex-col py-1 px-3" key={collection.collection_uuid}>
|
<div
|
||||||
<CollectionThumbnail collection={collection} orgslug={orgslug} org_id={org_id} />
|
className="flex flex-col py-1 px-3"
|
||||||
|
key={collection.collection_uuid}
|
||||||
|
>
|
||||||
|
<CollectionThumbnail
|
||||||
|
collection={collection}
|
||||||
|
orgslug={orgslug}
|
||||||
|
org_id={org_id}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{collections.length == 0 &&
|
{collections.length == 0 && (
|
||||||
<div className="flex mx-auto h-[400px]">
|
<div className="flex mx-auto h-[400px]">
|
||||||
<div className="flex flex-col justify-center text-center items-center space-y-5">
|
<div className="flex flex-col justify-center text-center items-center space-y-5">
|
||||||
<div className='mx-auto'>
|
<div className="mx-auto">
|
||||||
<svg width="120" height="120" viewBox="0 0 295 295" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect opacity="0.51" x="10" y="10" width="275" height="275" rx="75" stroke="#4B5564" strokeOpacity="0.15" strokeWidth="20" />
|
width="120"
|
||||||
<path d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z" fill="#4B5564" fillOpacity="0.08" />
|
height="120"
|
||||||
|
viewBox="0 0 295 295"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
opacity="0.51"
|
||||||
|
x="10"
|
||||||
|
y="10"
|
||||||
|
width="275"
|
||||||
|
height="275"
|
||||||
|
rx="75"
|
||||||
|
stroke="#4B5564"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="20"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z"
|
||||||
|
fill="#4B5564"
|
||||||
|
fillOpacity="0.08"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
<h1 className="text-3xl font-bold text-gray-600">No collections yet</h1>
|
<h1 className="text-3xl font-bold text-gray-600">
|
||||||
<p className="text-lg text-gray-400">Create a collection to group courses together</p>
|
No collections yet
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-gray-400">
|
||||||
|
Create a collection to group courses together
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<AuthenticatedClientElement checkMethod='roles'
|
<AuthenticatedClientElement
|
||||||
|
checkMethod="roles"
|
||||||
ressourceType="collections"
|
ressourceType="collections"
|
||||||
action="create"
|
action="create"
|
||||||
orgId={org_id}>
|
orgId={org_id}
|
||||||
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
|
>
|
||||||
|
<Link href={getUriWithOrg(orgslug, '/collections/new')}>
|
||||||
<NewCollectionButton />
|
<NewCollectionButton />
|
||||||
</Link>
|
</Link>
|
||||||
</AuthenticatedClientElement>
|
</AuthenticatedClientElement>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</GeneralWrapperStyled>
|
</GeneralWrapperStyled>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CollectionsPage
|
export default CollectionsPage
|
||||||
|
|
@ -1,53 +1,50 @@
|
||||||
"use client";
|
'use client'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
import { getUriWithOrg } from "@services/config/config";
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import Canva from "@components/Objects/Activities/DynamicCanva/DynamicCanva";
|
import Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva'
|
||||||
import VideoActivity from "@components/Objects/Activities/Video/Video";
|
import VideoActivity from '@components/Objects/Activities/Video/Video'
|
||||||
import { Check, MoreVertical } from "lucide-react";
|
import { Check, MoreVertical } from 'lucide-react'
|
||||||
import { markActivityAsComplete } from "@services/courses/activity";
|
import { markActivityAsComplete } from '@services/courses/activity'
|
||||||
import DocumentPdfActivity from "@components/Objects/Activities/DocumentPdf/DocumentPdf";
|
import DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf'
|
||||||
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
|
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
|
||||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from 'next/navigation'
|
||||||
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
|
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
|
||||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import { CourseProvider } from "@components/Contexts/CourseContext";
|
import { CourseProvider } from '@components/Contexts/CourseContext'
|
||||||
import AIActivityAsk from "@components/Objects/Activities/AI/AIActivityAsk";
|
import AIActivityAsk from '@components/Objects/Activities/AI/AIActivityAsk'
|
||||||
import AIChatBotProvider from "@components/Contexts/AI/AIChatBotContext";
|
import AIChatBotProvider from '@components/Contexts/AI/AIChatBotContext'
|
||||||
|
|
||||||
interface ActivityClientProps {
|
interface ActivityClientProps {
|
||||||
activityid: string;
|
activityid: string
|
||||||
courseuuid: string;
|
courseuuid: string
|
||||||
orgslug: string;
|
orgslug: string
|
||||||
activity: any;
|
activity: any
|
||||||
course: any;
|
course: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function ActivityClient(props: ActivityClientProps) {
|
function ActivityClient(props: ActivityClientProps) {
|
||||||
const activityid = props.activityid;
|
const activityid = props.activityid
|
||||||
const courseuuid = props.courseuuid;
|
const courseuuid = props.courseuuid
|
||||||
const orgslug = props.orgslug;
|
const orgslug = props.orgslug
|
||||||
const activity = props.activity;
|
const activity = props.activity
|
||||||
const course = props.course;
|
const course = props.course
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
|
|
||||||
function getChapterNameByActivityId(course: any, activity_id: any) {
|
function getChapterNameByActivityId(course: any, activity_id: any) {
|
||||||
for (let i = 0; i < course.chapters.length; i++) {
|
for (let i = 0; i < course.chapters.length; i++) {
|
||||||
let chapter = course.chapters[i];
|
let chapter = course.chapters[i]
|
||||||
for (let j = 0; j < chapter.activities.length; j++) {
|
for (let j = 0; j < chapter.activities.length; j++) {
|
||||||
let activity = chapter.activities[j];
|
let activity = chapter.activities[j]
|
||||||
if (activity.id === activity_id) {
|
if (activity.id === activity_id) {
|
||||||
return chapter.name;
|
return chapter.name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null; // return null if no matching activity is found
|
return null // return null if no matching activity is found
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<CourseProvider courseuuid={course?.course_uuid}>
|
<CourseProvider courseuuid={course?.course_uuid}>
|
||||||
|
|
@ -56,90 +53,147 @@ function ActivityClient(props: ActivityClientProps) {
|
||||||
<div className="space-y-4 pt-4">
|
<div className="space-y-4 pt-4">
|
||||||
<div className="flex space-x-6">
|
<div className="flex space-x-6">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Link href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}`}>
|
<Link
|
||||||
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, course.course_uuid, course.thumbnail_image)}`} alt="" />
|
href={getUriWithOrg(orgslug, '') + `/course/${courseuuid}`}
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
className="w-[100px] h-[57px] rounded-md drop-shadow-md"
|
||||||
|
src={`${getCourseThumbnailMediaDirectory(
|
||||||
|
org?.org_uuid,
|
||||||
|
course.course_uuid,
|
||||||
|
course.thumbnail_image
|
||||||
|
)}`}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col -space-y-1">
|
<div className="flex flex-col -space-y-1">
|
||||||
<p className="font-bold text-gray-700 text-md">Course </p>
|
<p className="font-bold text-gray-700 text-md">Course </p>
|
||||||
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{course.name}</h1>
|
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase">
|
||||||
|
{course.name}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ActivityIndicators course_uuid={courseuuid} current_activity={activityid} orgslug={orgslug} course={course} />
|
<ActivityIndicators
|
||||||
|
course_uuid={courseuuid}
|
||||||
|
current_activity={activityid}
|
||||||
|
orgslug={orgslug}
|
||||||
|
course={course}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div className="flex flex-col -space-y-1">
|
<div className="flex flex-col -space-y-1">
|
||||||
<p className="font-bold text-gray-700 text-md">Chapter : {getChapterNameByActivityId(course, activity.id)}</p>
|
<p className="font-bold text-gray-700 text-md">
|
||||||
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{activity.name}</h1>
|
Chapter : {getChapterNameByActivityId(course, activity.id)}
|
||||||
|
</p>
|
||||||
|
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase">
|
||||||
|
{activity.name}
|
||||||
|
</h1>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-1 items-center">
|
<div className="flex space-x-1 items-center">
|
||||||
<AuthenticatedClientElement checkMethod="authentication">
|
<AuthenticatedClientElement checkMethod="authentication">
|
||||||
<AIActivityAsk activity={activity} />
|
<AIActivityAsk activity={activity} />
|
||||||
<MoreVertical size={17} className="text-gray-300 " />
|
<MoreVertical size={17} className="text-gray-300 " />
|
||||||
<MarkStatus activity={activity} activityid={activityid} course={course} orgslug={orgslug} />
|
<MarkStatus
|
||||||
|
activity={activity}
|
||||||
|
activityid={activityid}
|
||||||
|
course={course}
|
||||||
|
orgslug={orgslug}
|
||||||
|
/>
|
||||||
</AuthenticatedClientElement>
|
</AuthenticatedClientElement>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activity ? (
|
{activity ? (
|
||||||
<div className={`p-7 pt-4 drop-shadow-sm rounded-lg ${activity.activity_type == 'TYPE_DYNAMIC' ? 'bg-white' : 'bg-zinc-950'}`}>
|
<div
|
||||||
|
className={`p-7 pt-4 drop-shadow-sm rounded-lg ${
|
||||||
|
activity.activity_type == 'TYPE_DYNAMIC'
|
||||||
|
? 'bg-white'
|
||||||
|
: 'bg-zinc-950'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
{activity.activity_type == "TYPE_DYNAMIC" && <Canva content={activity.content} activity={activity} />}
|
{activity.activity_type == 'TYPE_DYNAMIC' && (
|
||||||
|
<Canva content={activity.content} activity={activity} />
|
||||||
|
)}
|
||||||
{/* todo : use apis & streams instead of this */}
|
{/* todo : use apis & streams instead of this */}
|
||||||
{activity.activity_type == "TYPE_VIDEO" && <VideoActivity course={course} activity={activity} />}
|
{activity.activity_type == 'TYPE_VIDEO' && (
|
||||||
{activity.activity_type == "TYPE_DOCUMENT" && <DocumentPdfActivity course={course} activity={activity} />}
|
<VideoActivity course={course} activity={activity} />
|
||||||
|
)}
|
||||||
|
{activity.activity_type == 'TYPE_DOCUMENT' && (
|
||||||
|
<DocumentPdfActivity
|
||||||
|
course={course}
|
||||||
|
activity={activity}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : (<div></div>)}
|
) : (
|
||||||
{<div style={{ height: "100px" }}></div>}
|
<div></div>
|
||||||
|
)}
|
||||||
|
{<div style={{ height: '100px' }}></div>}
|
||||||
</div>
|
</div>
|
||||||
</GeneralWrapperStyled>
|
</GeneralWrapperStyled>
|
||||||
</AIChatBotProvider>
|
</AIChatBotProvider>
|
||||||
</CourseProvider>
|
</CourseProvider>
|
||||||
</>
|
</>
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function MarkStatus(props: { activity: any, activityid: string, course: any, orgslug: string }) {
|
|
||||||
const router = useRouter();
|
|
||||||
console.log(props.course.trail)
|
|
||||||
|
|
||||||
async function markActivityAsCompleteFront() {
|
|
||||||
const trail = await markActivityAsComplete(props.orgslug, props.course.course_uuid, 'activity_' + props.activityid);
|
|
||||||
router.refresh();
|
|
||||||
}
|
|
||||||
|
|
||||||
const isActivityCompleted = () => {
|
|
||||||
let run = props.course.trail.runs.find((run: any) => run.course_id == props.course.id);
|
|
||||||
if (run) {
|
|
||||||
return run.steps.find((step: any) => step.activity_id == props.activity.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('isActivityCompleted', isActivityCompleted());
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>{isActivityCompleted() ? (
|
|
||||||
<div className="bg-teal-600 rounded-full px-5 drop-shadow-md flex items-center space-x-1 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" >
|
|
||||||
<i>
|
|
||||||
<Check size={17}></Check>
|
|
||||||
</i>{" "}
|
|
||||||
<i className="not-italic text-xs font-bold">Already completed</i>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div className="bg-gray-800 rounded-full px-5 drop-shadow-md flex items-center space-x-1 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out" onClick={markActivityAsCompleteFront}>
|
|
||||||
{" "}
|
|
||||||
<i>
|
|
||||||
<Check size={17}></Check>
|
|
||||||
</i>{" "}
|
|
||||||
<i className="not-italic text-xs font-bold">Mark as complete</i>
|
|
||||||
</div>
|
|
||||||
)}</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function MarkStatus(props: {
|
||||||
|
activity: any
|
||||||
|
activityid: string
|
||||||
|
course: any
|
||||||
|
orgslug: string
|
||||||
|
}) {
|
||||||
|
const router = useRouter()
|
||||||
|
console.log(props.course.trail)
|
||||||
|
|
||||||
|
async function markActivityAsCompleteFront() {
|
||||||
|
const trail = await markActivityAsComplete(
|
||||||
|
props.orgslug,
|
||||||
|
props.course.course_uuid,
|
||||||
|
'activity_' + props.activityid
|
||||||
|
)
|
||||||
|
router.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
export default ActivityClient;
|
const isActivityCompleted = () => {
|
||||||
|
let run = props.course.trail.runs.find(
|
||||||
|
(run: any) => run.course_id == props.course.id
|
||||||
|
)
|
||||||
|
if (run) {
|
||||||
|
return run.steps.find(
|
||||||
|
(step: any) => step.activity_id == props.activity.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('isActivityCompleted', isActivityCompleted())
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{isActivityCompleted() ? (
|
||||||
|
<div className="bg-teal-600 rounded-full px-5 drop-shadow-md flex items-center space-x-1 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out">
|
||||||
|
<i>
|
||||||
|
<Check size={17}></Check>
|
||||||
|
</i>{' '}
|
||||||
|
<i className="not-italic text-xs font-bold">Already completed</i>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="bg-gray-800 rounded-full px-5 drop-shadow-md flex items-center space-x-1 p-2.5 text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out"
|
||||||
|
onClick={markActivityAsCompleteFront}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
<i>
|
||||||
|
<Check size={17}></Check>
|
||||||
|
</i>{' '}
|
||||||
|
<i className="not-italic text-xs font-bold">Mark as complete</i>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ActivityClient
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
'use client'; // Error components must be Client Components
|
'use client' // Error components must be Client Components
|
||||||
|
|
||||||
import ErrorUI from '@components/StyledElements/Error/Error';
|
import ErrorUI from '@components/StyledElements/Error/Error'
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
reset,
|
reset,
|
||||||
}: {
|
}: {
|
||||||
error: Error;
|
error: Error
|
||||||
reset: () => void;
|
reset: () => void
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Log the error to an error reporting service
|
// Log the error to an error reporting service
|
||||||
console.error(error);
|
console.error(error)
|
||||||
}, [error]);
|
}, [error])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ErrorUI></ErrorUI>
|
<ErrorUI></ErrorUI>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
// Or a custom loading skeleton component
|
// Or a custom loading skeleton component
|
||||||
return (
|
return <PageLoading></PageLoading>
|
||||||
<PageLoading></PageLoading>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,27 +1,37 @@
|
||||||
import { getActivityWithAuthHeader } from "@services/courses/activities";
|
import { getActivityWithAuthHeader } from '@services/courses/activities'
|
||||||
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
|
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses'
|
||||||
import { cookies } from "next/headers";
|
import { cookies } from 'next/headers'
|
||||||
import ActivityClient from "./activity";
|
import ActivityClient from './activity'
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { getOrganizationContextInfo } from '@services/organizations/orgs'
|
||||||
import { Metadata } from "next";
|
import { Metadata } from 'next'
|
||||||
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
|
||||||
|
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseuuid: string, activityid: string };
|
params: { orgslug: string; courseuuid: string; activityid: string }
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata({
|
||||||
{ params }: MetadataProps,
|
params,
|
||||||
): Promise<Metadata> {
|
}: MetadataProps): Promise<Metadata> {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies()
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
|
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(params.orgslug, {
|
||||||
const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
revalidate: 1800,
|
||||||
const activity = await getActivityWithAuthHeader(params.activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
const course_meta = await getCourseMetadataWithAuthHeader(
|
||||||
|
params.courseuuid,
|
||||||
|
{ revalidate: 0, tags: ['courses'] },
|
||||||
|
access_token ? access_token : null
|
||||||
|
)
|
||||||
|
const activity = await getActivityWithAuthHeader(
|
||||||
|
params.activityid,
|
||||||
|
{ revalidate: 0, tags: ['activities'] },
|
||||||
|
access_token ? access_token : null
|
||||||
|
)
|
||||||
|
|
||||||
// SEO
|
// SEO
|
||||||
return {
|
return {
|
||||||
|
|
@ -35,8 +45,8 @@ export async function generateMetadata(
|
||||||
googleBot: {
|
googleBot: {
|
||||||
index: true,
|
index: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
"max-image-preview": "large",
|
'max-image-preview': 'large',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: activity.name + ` — ${course_meta.name} Course`,
|
title: activity.name + ` — ${course_meta.name} Course`,
|
||||||
|
|
@ -44,18 +54,26 @@ export async function generateMetadata(
|
||||||
publishedTime: course_meta.creation_date,
|
publishedTime: course_meta.creation_date,
|
||||||
tags: course_meta.learnings,
|
tags: course_meta.learnings,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const ActivityPage = async (params: any) => {
|
const ActivityPage = async (params: any) => {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies()
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const activityid = params.params.activityid;
|
const activityid = params.params.activityid
|
||||||
const courseuuid = params.params.courseuuid;
|
const courseuuid = params.params.courseuuid
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug
|
||||||
|
|
||||||
const course_meta = await getCourseMetadataWithAuthHeader(courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
const course_meta = await getCourseMetadataWithAuthHeader(
|
||||||
const activity = await getActivityWithAuthHeader(activityid, { revalidate: 0, tags: ['activities'] }, access_token ? access_token : null)
|
courseuuid,
|
||||||
|
{ revalidate: 0, tags: ['courses'] },
|
||||||
|
access_token ? access_token : null
|
||||||
|
)
|
||||||
|
const activity = await getActivityWithAuthHeader(
|
||||||
|
activityid,
|
||||||
|
{ revalidate: 0, tags: ['activities'] },
|
||||||
|
access_token ? access_token : null
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ActivityClient
|
<ActivityClient
|
||||||
|
|
@ -64,7 +82,8 @@ const ActivityPage = async (params: any) => {
|
||||||
orgslug={orgslug}
|
orgslug={orgslug}
|
||||||
activity={activity}
|
activity={activity}
|
||||||
course={course_meta}
|
course={course_meta}
|
||||||
/></>
|
/>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,63 +1,66 @@
|
||||||
"use client";
|
'use client'
|
||||||
import { removeCourse, startCourse } from "@services/courses/activity";
|
import { removeCourse, startCourse } from '@services/courses/activity'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from 'react'
|
||||||
import { getUriWithOrg } from "@services/config/config";
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import { revalidateTags } from "@services/utils/ts/requests";
|
import { revalidateTags } from '@services/utils/ts/requests'
|
||||||
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
|
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from 'next/navigation'
|
||||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
|
||||||
import { getCourseThumbnailMediaDirectory, getUserAvatarMediaDirectory } from "@services/media/media";
|
import {
|
||||||
import { ArrowRight, Check, File, Sparkles, Video } from "lucide-react";
|
getCourseThumbnailMediaDirectory,
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
getUserAvatarMediaDirectory,
|
||||||
import UserAvatar from "@components/Objects/UserAvatar";
|
} from '@services/media/media'
|
||||||
|
import { ArrowRight, Check, File, Sparkles, Video } from 'lucide-react'
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
import UserAvatar from '@components/Objects/UserAvatar'
|
||||||
|
|
||||||
const CourseClient = (props: any) => {
|
const CourseClient = (props: any) => {
|
||||||
const [user, setUser] = useState<any>({});
|
const [user, setUser] = useState<any>({})
|
||||||
const [learnings, setLearnings] = useState<any>([]);
|
const [learnings, setLearnings] = useState<any>([])
|
||||||
const courseuuid = props.courseuuid;
|
const courseuuid = props.courseuuid
|
||||||
const orgslug = props.orgslug;
|
const orgslug = props.orgslug
|
||||||
const course = props.course;
|
const course = props.course
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
function getLearningTags() {
|
function getLearningTags() {
|
||||||
// create array of learnings from a string object (comma separated)
|
// create array of learnings from a string object (comma separated)
|
||||||
let learnings = course?.learnings ? course?.learnings.split(",") : [];
|
let learnings = course?.learnings ? course?.learnings.split(',') : []
|
||||||
setLearnings(learnings);
|
setLearnings(learnings)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function startCourseUI() {
|
async function startCourseUI() {
|
||||||
// Create activity
|
// Create activity
|
||||||
await startCourse("course_" + courseuuid, orgslug);
|
await startCourse('course_' + courseuuid, orgslug)
|
||||||
await revalidateTags(['courses'], orgslug);
|
await revalidateTags(['courses'], orgslug)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
|
|
||||||
// refresh page (FIX for Next.js BUG)
|
// refresh page (FIX for Next.js BUG)
|
||||||
// window.location.reload();
|
// window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCourseStarted() {
|
function isCourseStarted() {
|
||||||
const runs = course.trail?.runs;
|
const runs = course.trail?.runs
|
||||||
if (!runs) return false;
|
if (!runs) return false
|
||||||
return runs.some((run: any) => run.status === "STATUS_IN_PROGRESS" && run.course_id === course.id);
|
return runs.some(
|
||||||
|
(run: any) =>
|
||||||
|
run.status === 'STATUS_IN_PROGRESS' && run.course_id === course.id
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function quitCourse() {
|
async function quitCourse() {
|
||||||
// Close activity
|
// Close activity
|
||||||
let activity = await removeCourse("course_" + courseuuid, orgslug);
|
let activity = await removeCourse('course_' + courseuuid, orgslug)
|
||||||
// Mutate course
|
// Mutate course
|
||||||
await revalidateTags(['courses'], orgslug);
|
await revalidateTags(['courses'], orgslug)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getLearningTags();
|
getLearningTags()
|
||||||
}
|
}, [org, course])
|
||||||
, [org, course]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -67,20 +70,35 @@ const CourseClient = (props: any) => {
|
||||||
<GeneralWrapperStyled>
|
<GeneralWrapperStyled>
|
||||||
<div className="pb-3">
|
<div className="pb-3">
|
||||||
<p className="text-md font-bold text-gray-400 pb-2">Course</p>
|
<p className="text-md font-bold text-gray-400 pb-2">Course</p>
|
||||||
<h1 className="text-3xl -mt-3 font-bold">
|
<h1 className="text-3xl -mt-3 font-bold">{course.name}</h1>
|
||||||
{course.name}
|
|
||||||
</h1>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{props.course?.thumbnail_image && org ?
|
{props.course?.thumbnail_image && org ? (
|
||||||
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[400px] bg-cover bg-center mb-4" style={{ backgroundImage: `url(${getCourseThumbnailMediaDirectory(org?.org_uuid, course?.course_uuid, course?.thumbnail_image)})` }}>
|
<div
|
||||||
</div>
|
className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[400px] bg-cover bg-center mb-4"
|
||||||
:
|
style={{
|
||||||
<div className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[400px] bg-cover bg-center mb-4" style={{ backgroundImage: `url('../empty_thumbnail.png')`, backgroundSize: 'auto' }}>
|
backgroundImage: `url(${getCourseThumbnailMediaDirectory(
|
||||||
</div>
|
org?.org_uuid,
|
||||||
}
|
course?.course_uuid,
|
||||||
|
course?.thumbnail_image
|
||||||
|
)})`,
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
) : (
|
||||||
|
<div
|
||||||
|
className="inset-0 ring-1 ring-inset ring-black/10 rounded-lg shadow-xl relative w-auto h-[400px] bg-cover bg-center mb-4"
|
||||||
|
style={{
|
||||||
|
backgroundImage: `url('../empty_thumbnail.png')`,
|
||||||
|
backgroundSize: 'auto',
|
||||||
|
}}
|
||||||
|
></div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ActivityIndicators course_uuid={props.course.course_uuid} orgslug={orgslug} course={course} />
|
<ActivityIndicators
|
||||||
|
course_uuid={props.course.course_uuid}
|
||||||
|
orgslug={orgslug}
|
||||||
|
course={course}
|
||||||
|
/>
|
||||||
|
|
||||||
<div className="flex flex-row pt-10">
|
<div className="flex flex-row pt-10">
|
||||||
<div className="course_metadata_left grow space-y-2">
|
<div className="course_metadata_left grow space-y-2">
|
||||||
|
|
@ -89,143 +107,223 @@ const CourseClient = (props: any) => {
|
||||||
<p className="py-5 px-5">{course.description}</p>
|
<p className="py-5 px-5">{course.description}</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{learnings.length > 0 && learnings[0] !== "null" &&
|
{learnings.length > 0 && learnings[0] !== 'null' && (
|
||||||
<div>
|
<div>
|
||||||
<h2 className="py-3 text-2xl font-bold">What you will learn</h2>
|
<h2 className="py-3 text-2xl font-bold">
|
||||||
|
What you will learn
|
||||||
|
</h2>
|
||||||
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden px-5 py-5 space-y-2">
|
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden px-5 py-5 space-y-2">
|
||||||
{learnings.map((learning: any) => {
|
{learnings.map((learning: any) => {
|
||||||
return (
|
return (
|
||||||
<div key={learning}
|
<div
|
||||||
className="flex space-x-2 items-center font-semibold text-gray-500">
|
key={learning}
|
||||||
|
className="flex space-x-2 items-center font-semibold text-gray-500"
|
||||||
|
>
|
||||||
<div className="px-2 py-2 rounded-full">
|
<div className="px-2 py-2 rounded-full">
|
||||||
<Check className="text-gray-400" size={15} />
|
<Check className="text-gray-400" size={15} />
|
||||||
</div>
|
</div>
|
||||||
<p>{learning}</p>
|
<p>{learning}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
<h2 className="py-3 text-2xl font-bold">Course Lessons</h2>
|
<h2 className="py-3 text-2xl font-bold">Course Lessons</h2>
|
||||||
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
|
<div className="bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
|
||||||
{course.chapters.map((chapter: any) => {
|
{course.chapters.map((chapter: any) => {
|
||||||
return (
|
return (
|
||||||
<div
|
<div key={chapter} className="">
|
||||||
key={chapter}
|
|
||||||
className=""
|
|
||||||
>
|
|
||||||
<div className="flex text-lg py-4 px-4 outline outline-1 outline-neutral-200/40 font-bold bg-neutral-50 text-neutral-600 items-center">
|
<div className="flex text-lg py-4 px-4 outline outline-1 outline-neutral-200/40 font-bold bg-neutral-50 text-neutral-600 items-center">
|
||||||
<h3 className="grow">{chapter.name}</h3>
|
<h3 className="grow">{chapter.name}</h3>
|
||||||
<p className="text-sm font-normal text-neutral-400 px-3 py-[2px] outline-1 outline outline-neutral-200 rounded-full ">
|
<p className="text-sm font-normal text-neutral-400 px-3 py-[2px] outline-1 outline outline-neutral-200 rounded-full ">
|
||||||
{chapter.activities.length} Activities
|
{chapter.activities.length} Activities
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className="py-3">
|
||||||
className="py-3"
|
{chapter.activities.map((activity: any) => {
|
||||||
>{chapter.activities.map((activity: any) => {
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className="flex text-md">
|
<p className="flex text-md"></p>
|
||||||
|
|
||||||
</p>
|
|
||||||
<div className="flex space-x-1 py-2 px-4 items-center">
|
<div className="flex space-x-1 py-2 px-4 items-center">
|
||||||
<div className="courseicon items-center flex space-x-2 text-neutral-400">
|
<div className="courseicon items-center flex space-x-2 text-neutral-400">
|
||||||
{activity.activity_type === "TYPE_DYNAMIC" &&
|
{activity.activity_type ===
|
||||||
|
'TYPE_DYNAMIC' && (
|
||||||
<div className="bg-gray-100 px-2 py-2 rounded-full">
|
<div className="bg-gray-100 px-2 py-2 rounded-full">
|
||||||
<Sparkles className="text-gray-400" size={13} />
|
<Sparkles
|
||||||
|
className="text-gray-400"
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{activity.activity_type === "TYPE_VIDEO" &&
|
{activity.activity_type === 'TYPE_VIDEO' && (
|
||||||
<div className="bg-gray-100 px-2 py-2 rounded-full">
|
<div className="bg-gray-100 px-2 py-2 rounded-full">
|
||||||
<Video className="text-gray-400" size={13} />
|
<Video
|
||||||
|
className="text-gray-400"
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
{activity.activity_type === "TYPE_DOCUMENT" &&
|
{activity.activity_type ===
|
||||||
|
'TYPE_DOCUMENT' && (
|
||||||
<div className="bg-gray-100 px-2 py-2 rounded-full">
|
<div className="bg-gray-100 px-2 py-2 rounded-full">
|
||||||
<File className="text-gray-400" size={13} />
|
<File
|
||||||
|
className="text-gray-400"
|
||||||
|
size={13}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<Link
|
||||||
|
className="flex font-semibold grow pl-2 text-neutral-500"
|
||||||
|
href={
|
||||||
|
getUriWithOrg(orgslug, '') +
|
||||||
|
`/course/${courseuuid}/activity/${activity.activity_uuid.replace(
|
||||||
|
'activity_',
|
||||||
|
''
|
||||||
|
)}`
|
||||||
}
|
}
|
||||||
|
rel="noopener noreferrer"
|
||||||
</div>
|
>
|
||||||
<Link className="flex font-semibold grow pl-2 text-neutral-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
|
|
||||||
<p>{activity.name}</p>
|
<p>{activity.name}</p>
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex ">
|
<div className="flex ">
|
||||||
{activity.activity_type === "TYPE_DYNAMIC" &&
|
{activity.activity_type ===
|
||||||
|
'TYPE_DYNAMIC' && (
|
||||||
<>
|
<>
|
||||||
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
|
<Link
|
||||||
|
className="flex grow pl-2 text-gray-500"
|
||||||
|
href={
|
||||||
|
getUriWithOrg(orgslug, '') +
|
||||||
|
`/course/${courseuuid}/activity/${activity.activity_uuid.replace(
|
||||||
|
'activity_',
|
||||||
|
''
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
|
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
|
||||||
<p>Page</p>
|
<p>Page</p>
|
||||||
<ArrowRight size={13} /></div>
|
<ArrowRight size={13} />
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{activity.activity_type === "TYPE_VIDEO" &&
|
{activity.activity_type === 'TYPE_VIDEO' && (
|
||||||
<>
|
<>
|
||||||
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
|
<Link
|
||||||
|
className="flex grow pl-2 text-gray-500"
|
||||||
|
href={
|
||||||
|
getUriWithOrg(orgslug, '') +
|
||||||
|
`/course/${courseuuid}/activity/${activity.activity_uuid.replace(
|
||||||
|
'activity_',
|
||||||
|
''
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
|
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
|
||||||
<p>Video</p>
|
<p>Video</p>
|
||||||
<ArrowRight size={13} /></div>
|
<ArrowRight size={13} />
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
{activity.activity_type === "TYPE_DOCUMENT" &&
|
{activity.activity_type ===
|
||||||
|
'TYPE_DOCUMENT' && (
|
||||||
<>
|
<>
|
||||||
<Link className="flex grow pl-2 text-gray-500" href={getUriWithOrg(orgslug, "") + `/course/${courseuuid}/activity/${activity.activity_uuid.replace("activity_", "")}`} rel="noopener noreferrer">
|
<Link
|
||||||
|
className="flex grow pl-2 text-gray-500"
|
||||||
|
href={
|
||||||
|
getUriWithOrg(orgslug, '') +
|
||||||
|
`/course/${courseuuid}/activity/${activity.activity_uuid.replace(
|
||||||
|
'activity_',
|
||||||
|
''
|
||||||
|
)}`
|
||||||
|
}
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
|
<div className="text-xs bg-gray-100 text-gray-400 font-bold px-2 py-1 rounded-full flex space-x-1 items-center">
|
||||||
<p>Document</p>
|
<p>Document</p>
|
||||||
<ArrowRight size={13} /></div>
|
<ArrowRight size={13} />
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
})}</div>
|
})}
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
|
)
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className="course_metadata_right space-y-3 w-72 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
|
<div className="course_metadata_right space-y-3 w-72 antialiased flex flex-col ml-10 h-fit p-3 py-5 bg-white shadow-md shadow-gray-300/25 outline outline-1 outline-neutral-200/40 rounded-lg overflow-hidden">
|
||||||
{user &&
|
{user && (
|
||||||
<div className="flex flex-col mx-auto space-y-3 px-2 py-2 items-center">
|
<div className="flex flex-col mx-auto space-y-3 px-2 py-2 items-center">
|
||||||
<UserAvatar border="border-8" avatar_url={getUserAvatarMediaDirectory(course.authors[0].user_uuid, course.authors[0].avatar_image)} width={100} />
|
<UserAvatar
|
||||||
|
border="border-8"
|
||||||
|
avatar_url={getUserAvatarMediaDirectory(
|
||||||
|
course.authors[0].user_uuid,
|
||||||
|
course.authors[0].avatar_image
|
||||||
|
)}
|
||||||
|
width={100}
|
||||||
|
/>
|
||||||
<div className="-space-y-2 ">
|
<div className="-space-y-2 ">
|
||||||
<div className="text-[12px] text-neutral-400 font-semibold">Author</div>
|
<div className="text-[12px] text-neutral-400 font-semibold">
|
||||||
|
Author
|
||||||
|
</div>
|
||||||
<div className="text-xl font-bold text-neutral-800">
|
<div className="text-xl font-bold text-neutral-800">
|
||||||
{course.authors[0].first_name && course.authors[0].last_name && (
|
{course.authors[0].first_name &&
|
||||||
|
course.authors[0].last_name && (
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
<p>{course.authors[0].first_name + ' ' + course.authors[0].last_name}</p><span className="text-xs bg-neutral-100 p-1 px-3 rounded-full text-neutral-400 font-semibold"> @{course.authors[0].username}</span>
|
<p>
|
||||||
</div>)}
|
{course.authors[0].first_name +
|
||||||
{!course.authors[0].first_name && !course.authors[0].last_name && (
|
' ' +
|
||||||
|
course.authors[0].last_name}
|
||||||
|
</p>
|
||||||
|
<span className="text-xs bg-neutral-100 p-1 px-3 rounded-full text-neutral-400 font-semibold">
|
||||||
|
{' '}
|
||||||
|
@{course.authors[0].username}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{!course.authors[0].first_name &&
|
||||||
|
!course.authors[0].last_name && (
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
<p>@{course.authors[0].username}</p>
|
<p>@{course.authors[0].username}</p>
|
||||||
</div>)}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
|
|
||||||
{isCourseStarted() ? (
|
{isCourseStarted() ? (
|
||||||
<button className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-red-600 hover:bg-red-700 hover:cursor-pointer" onClick={quitCourse}>
|
<button
|
||||||
|
className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-red-600 hover:bg-red-700 hover:cursor-pointer"
|
||||||
|
onClick={quitCourse}
|
||||||
|
>
|
||||||
Quit Course
|
Quit Course
|
||||||
</button>
|
</button>
|
||||||
) : (
|
) : (
|
||||||
<button className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-black hover:bg-gray-900 hover:cursor-pointer" onClick={startCourseUI}>Start Course</button>
|
<button
|
||||||
|
className="py-2 px-5 mx-auto rounded-xl text-white font-bold h-12 w-[200px] drop-shadow-md bg-black hover:bg-gray-900 hover:cursor-pointer"
|
||||||
|
onClick={startCourseUI}
|
||||||
|
>
|
||||||
|
Start Course
|
||||||
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</GeneralWrapperStyled>
|
</GeneralWrapperStyled>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export default CourseClient
|
||||||
export default CourseClient;
|
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
'use client'; // Error components must be Client Components
|
'use client' // Error components must be Client Components
|
||||||
|
|
||||||
import ErrorUI from '@components/StyledElements/Error/Error';
|
import ErrorUI from '@components/StyledElements/Error/Error'
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
reset,
|
reset,
|
||||||
}: {
|
}: {
|
||||||
error: Error;
|
error: Error
|
||||||
reset: () => void;
|
reset: () => void
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Log the error to an error reporting service
|
// Log the error to an error reporting service
|
||||||
console.error(error);
|
console.error(error)
|
||||||
}, [error]);
|
}, [error])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ErrorUI></ErrorUI>
|
<ErrorUI></ErrorUI>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
// Or a custom loading skeleton component
|
// Or a custom loading skeleton component
|
||||||
return (
|
return <PageLoading></PageLoading>
|
||||||
<PageLoading></PageLoading>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,26 +1,32 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CourseClient from './course'
|
import CourseClient from './course'
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers'
|
||||||
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses';
|
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses'
|
||||||
import { getOrganizationContextInfo } from '@services/organizations/orgs';
|
import { getOrganizationContextInfo } from '@services/organizations/orgs'
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next'
|
||||||
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
|
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseuuid: string };
|
params: { orgslug: string; courseuuid: string }
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata({
|
||||||
{ params }: MetadataProps,
|
params,
|
||||||
): Promise<Metadata> {
|
}: MetadataProps): Promise<Metadata> {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies()
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
|
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(params.orgslug, {
|
||||||
const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
revalidate: 1800,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
const course_meta = await getCourseMetadataWithAuthHeader(
|
||||||
|
params.courseuuid,
|
||||||
|
{ revalidate: 0, tags: ['courses'] },
|
||||||
|
access_token ? access_token : null
|
||||||
|
)
|
||||||
|
|
||||||
// SEO
|
// SEO
|
||||||
return {
|
return {
|
||||||
|
|
@ -34,8 +40,8 @@ export async function generateMetadata(
|
||||||
googleBot: {
|
googleBot: {
|
||||||
index: true,
|
index: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
"max-image-preview": "large",
|
'max-image-preview': 'large',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: course_meta.name + ` — ${org.name}`,
|
title: course_meta.name + ` — ${org.name}`,
|
||||||
|
|
@ -44,20 +50,27 @@ export async function generateMetadata(
|
||||||
publishedTime: course_meta.creation_date ? course_meta.creation_date : '',
|
publishedTime: course_meta.creation_date ? course_meta.creation_date : '',
|
||||||
tags: course_meta.learnings ? course_meta.learnings : [],
|
tags: course_meta.learnings ? course_meta.learnings : [],
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const CoursePage = async (params: any) => {
|
const CoursePage = async (params: any) => {
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies()
|
||||||
const courseuuid = params.params.courseuuid
|
const courseuuid = params.params.courseuuid
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const course_meta = await getCourseMetadataWithAuthHeader(courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
|
const course_meta = await getCourseMetadataWithAuthHeader(
|
||||||
|
courseuuid,
|
||||||
|
{ revalidate: 0, tags: ['courses'] },
|
||||||
|
access_token ? access_token : null
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<CourseClient courseuuid={courseuuid} orgslug={orgslug} course={course_meta} />
|
<CourseClient
|
||||||
|
courseuuid={courseuuid}
|
||||||
|
orgslug={orgslug}
|
||||||
|
course={course_meta}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,113 +1,138 @@
|
||||||
'use client';
|
'use client'
|
||||||
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse';
|
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse'
|
||||||
import Modal from '@components/StyledElements/Modal/Modal';
|
import Modal from '@components/StyledElements/Modal/Modal'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation'
|
||||||
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
|
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
|
||||||
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
|
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'
|
||||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
|
||||||
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
|
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'
|
||||||
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'
|
||||||
|
|
||||||
interface CourseProps {
|
interface CourseProps {
|
||||||
orgslug: string;
|
orgslug: string
|
||||||
courses: any;
|
courses: any
|
||||||
org_id: string;
|
org_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function Courses(props: CourseProps) {
|
function Courses(props: CourseProps) {
|
||||||
const orgslug = props.orgslug;
|
const orgslug = props.orgslug
|
||||||
const courses = props.courses;
|
const courses = props.courses
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams()
|
||||||
const isCreatingCourse = searchParams.get('new') ? true : false;
|
const isCreatingCourse = searchParams.get('new') ? true : false
|
||||||
const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse);
|
const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse)
|
||||||
|
|
||||||
async function closeNewCourseModal() {
|
async function closeNewCourseModal() {
|
||||||
setNewCourseModal(false);
|
setNewCourseModal(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<GeneralWrapperStyled>
|
<GeneralWrapperStyled>
|
||||||
|
<div className="flex flex-wrap justify-between">
|
||||||
<div className='flex flex-wrap justify-between'>
|
|
||||||
<TypeOfContentTitle title="Courses" type="cou" />
|
<TypeOfContentTitle title="Courses" type="cou" />
|
||||||
<AuthenticatedClientElement checkMethod='roles'
|
<AuthenticatedClientElement
|
||||||
action='create'
|
checkMethod="roles"
|
||||||
ressourceType='courses'
|
action="create"
|
||||||
orgId={props.org_id}>
|
ressourceType="courses"
|
||||||
|
orgId={props.org_id}
|
||||||
|
>
|
||||||
<Modal
|
<Modal
|
||||||
isDialogOpen={newCourseModal}
|
isDialogOpen={newCourseModal}
|
||||||
onOpenChange={setNewCourseModal}
|
onOpenChange={setNewCourseModal}
|
||||||
minHeight="md"
|
minHeight="md"
|
||||||
dialogContent={<CreateCourseModal
|
dialogContent={
|
||||||
|
<CreateCourseModal
|
||||||
closeModal={closeNewCourseModal}
|
closeModal={closeNewCourseModal}
|
||||||
orgslug={orgslug}
|
orgslug={orgslug}
|
||||||
></CreateCourseModal>}
|
></CreateCourseModal>
|
||||||
|
}
|
||||||
dialogTitle="Create Course"
|
dialogTitle="Create Course"
|
||||||
dialogDescription="Create a new course"
|
dialogDescription="Create a new course"
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
|
|
||||||
<button>
|
<button>
|
||||||
<NewCourseButton />
|
<NewCourseButton />
|
||||||
</button>}
|
</button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</AuthenticatedClientElement>
|
</AuthenticatedClientElement>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
<div className="flex flex-wrap">
|
<div className="flex flex-wrap">
|
||||||
{courses.map((course: any) => (
|
{courses.map((course: any) => (
|
||||||
<div className="px-3" key={course.course_uuid}>
|
<div className="px-3" key={course.course_uuid}>
|
||||||
<CourseThumbnail course={course} orgslug={orgslug} />
|
<CourseThumbnail course={course} orgslug={orgslug} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{courses.length == 0 &&
|
{courses.length == 0 && (
|
||||||
<div className="flex mx-auto h-[400px]">
|
<div className="flex mx-auto h-[400px]">
|
||||||
<div className="flex flex-col justify-center text-center items-center space-y-5">
|
<div className="flex flex-col justify-center text-center items-center space-y-5">
|
||||||
<div className='mx-auto'>
|
<div className="mx-auto">
|
||||||
<svg width="120" height="120" viewBox="0 0 295 295" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect opacity="0.51" x="10" y="10" width="275" height="275" rx="75" stroke="#4B5564" strokeOpacity="0.15" strokeWidth="20" />
|
width="120"
|
||||||
<path d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z" fill="#4B5564" fillOpacity="0.08" />
|
height="120"
|
||||||
|
viewBox="0 0 295 295"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
opacity="0.51"
|
||||||
|
x="10"
|
||||||
|
y="10"
|
||||||
|
width="275"
|
||||||
|
height="275"
|
||||||
|
rx="75"
|
||||||
|
stroke="#4B5564"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="20"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z"
|
||||||
|
fill="#4B5564"
|
||||||
|
fillOpacity="0.08"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
<h1 className="text-3xl font-bold text-gray-600">No courses yet</h1>
|
<h1 className="text-3xl font-bold text-gray-600">
|
||||||
<p className="text-lg text-gray-400">Create a course to add content</p>
|
No courses yet
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-gray-400">
|
||||||
|
Create a course to add content
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<AuthenticatedClientElement
|
<AuthenticatedClientElement
|
||||||
action='create'
|
action="create"
|
||||||
ressourceType='courses'
|
ressourceType="courses"
|
||||||
checkMethod='roles' orgId={props.org_id}>
|
checkMethod="roles"
|
||||||
|
orgId={props.org_id}
|
||||||
|
>
|
||||||
<Modal
|
<Modal
|
||||||
isDialogOpen={newCourseModal}
|
isDialogOpen={newCourseModal}
|
||||||
onOpenChange={setNewCourseModal}
|
onOpenChange={setNewCourseModal}
|
||||||
minHeight="md"
|
minHeight="md"
|
||||||
dialogContent={<CreateCourseModal
|
dialogContent={
|
||||||
|
<CreateCourseModal
|
||||||
closeModal={closeNewCourseModal}
|
closeModal={closeNewCourseModal}
|
||||||
orgslug={orgslug}
|
orgslug={orgslug}
|
||||||
></CreateCourseModal>}
|
></CreateCourseModal>
|
||||||
|
}
|
||||||
dialogTitle="Create Course"
|
dialogTitle="Create Course"
|
||||||
dialogDescription="Create a new course"
|
dialogDescription="Create a new course"
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
<button>
|
<button>
|
||||||
<NewCourseButton />
|
<NewCourseButton />
|
||||||
</button>}
|
</button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</AuthenticatedClientElement>
|
</AuthenticatedClientElement>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</GeneralWrapperStyled>
|
</GeneralWrapperStyled>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default Courses
|
export default Courses
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
'use client'; // Error components must be Client Components
|
'use client' // Error components must be Client Components
|
||||||
|
|
||||||
import ErrorUI from '@components/StyledElements/Error/Error';
|
import ErrorUI from '@components/StyledElements/Error/Error'
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
reset,
|
reset,
|
||||||
}: {
|
}: {
|
||||||
error: Error;
|
error: Error
|
||||||
reset: () => void;
|
reset: () => void
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Log the error to an error reporting service
|
// Log the error to an error reporting service
|
||||||
console.error(error);
|
console.error(error)
|
||||||
}, [error]);
|
}, [error])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ErrorUI></ErrorUI>
|
<ErrorUI></ErrorUI>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,9 +1,6 @@
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
// Or a custom loading skeleton component
|
// Or a custom loading skeleton component
|
||||||
return (
|
return <PageLoading></PageLoading>
|
||||||
<PageLoading></PageLoading>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,27 +1,28 @@
|
||||||
|
import React from 'react'
|
||||||
import React from "react";
|
import Courses from './courses'
|
||||||
import Courses from "./courses";
|
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses'
|
||||||
import { getOrgCoursesWithAuthHeader } from "@services/courses/courses";
|
import { Metadata } from 'next'
|
||||||
import { Metadata } from "next";
|
import { getOrganizationContextInfo } from '@services/organizations/orgs'
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { cookies } from 'next/headers'
|
||||||
import { cookies } from "next/headers";
|
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
|
||||||
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
|
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string };
|
params: { orgslug: string }
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
|
||||||
{ params }: MetadataProps,
|
|
||||||
): Promise<Metadata> {
|
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: MetadataProps): Promise<Metadata> {
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(params.orgslug, {
|
||||||
|
revalidate: 1800,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
|
||||||
// SEO
|
// SEO
|
||||||
return {
|
return {
|
||||||
title: "Courses — " + org.name,
|
title: 'Courses — ' + org.name,
|
||||||
description: org.description,
|
description: org.description,
|
||||||
keywords: `${org.name}, ${org.description}, courses, learning, education, online learning, edu, online courses, ${org.name} courses`,
|
keywords: `${org.name}, ${org.description}, courses, learning, education, online learning, edu, online courses, ${org.name} courses`,
|
||||||
robots: {
|
robots: {
|
||||||
|
|
@ -31,30 +32,36 @@ export async function generateMetadata(
|
||||||
googleBot: {
|
googleBot: {
|
||||||
index: true,
|
index: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
"max-image-preview": "large",
|
'max-image-preview': 'large',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "Courses — " + org.name,
|
title: 'Courses — ' + org.name,
|
||||||
description: org.description,
|
description: org.description,
|
||||||
type: 'website',
|
type: 'website',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const CoursesPage = async (params: any) => {
|
const CoursesPage = async (params: any) => {
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, {
|
||||||
const cookieStore = cookies();
|
revalidate: 1800,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
const cookieStore = cookies()
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null);
|
const courses = await getOrgCoursesWithAuthHeader(
|
||||||
|
orgslug,
|
||||||
|
{ revalidate: 0, tags: ['courses'] },
|
||||||
|
access_token ? access_token : null
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Courses org_id={org.org_id} orgslug={orgslug} courses={courses} />
|
<Courses org_id={org.org_id} orgslug={orgslug} courses={courses} />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default CoursesPage;
|
|
||||||
|
|
||||||
|
export default CoursesPage
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,23 @@
|
||||||
'use client'; // Error components must be Client Components
|
'use client' // Error components must be Client Components
|
||||||
|
|
||||||
import ErrorUI from '@components/StyledElements/Error/Error';
|
import ErrorUI from '@components/StyledElements/Error/Error'
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react'
|
||||||
|
|
||||||
export default function Error({
|
export default function Error({
|
||||||
error,
|
error,
|
||||||
reset,
|
reset,
|
||||||
}: {
|
}: {
|
||||||
error: Error;
|
error: Error
|
||||||
reset: () => void;
|
reset: () => void
|
||||||
}) {
|
}) {
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Log the error to an error reporting service
|
// Log the error to an error reporting service
|
||||||
console.error(error);
|
console.error(error)
|
||||||
}, [error]);
|
}, [error])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<ErrorUI ></ErrorUI>
|
<ErrorUI></ErrorUI>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,14 @@
|
||||||
import "@styles/globals.css";
|
import '@styles/globals.css'
|
||||||
import { Menu } from "@components/Objects/Menu/Menu";
|
import { Menu } from '@components/Objects/Menu/Menu'
|
||||||
import SessionProvider from "@components/Contexts/SessionContext";
|
import SessionProvider from '@components/Contexts/SessionContext'
|
||||||
|
|
||||||
export default function RootLayout({ children, params }: { children: React.ReactNode , params :any}) {
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
params: any
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
|
|
@ -10,5 +16,5 @@ export default function RootLayout({ children, params }: { children: React.React
|
||||||
{children}
|
{children}
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,5 @@
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
|
|
||||||
export default function Loading() {
|
export default function Loading() {
|
||||||
return (
|
return <PageLoading></PageLoading>
|
||||||
<PageLoading></PageLoading>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,30 +1,33 @@
|
||||||
export const dynamic = 'force-dynamic';
|
export const dynamic = 'force-dynamic'
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next'
|
||||||
import { getUriWithOrg } from "@services/config/config";
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import { getOrgCoursesWithAuthHeader } from "@services/courses/courses";
|
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
import { getOrgCollectionsWithAuthHeader } from "@services/courses/collections";
|
import { getOrgCollectionsWithAuthHeader } from '@services/courses/collections'
|
||||||
import { getOrganizationContextInfo } from '@services/organizations/orgs';
|
import { getOrganizationContextInfo } from '@services/organizations/orgs'
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers'
|
||||||
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
|
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
|
||||||
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
|
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'
|
||||||
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
|
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
|
||||||
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
|
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'
|
||||||
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail';
|
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail'
|
||||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
|
||||||
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'
|
||||||
import NewCollectionButton from '@components/StyledElements/Buttons/NewCollectionButton';
|
import NewCollectionButton from '@components/StyledElements/Buttons/NewCollectionButton'
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string };
|
params: { orgslug: string }
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata({
|
||||||
{ params }: MetadataProps,
|
params,
|
||||||
): Promise<Metadata> {
|
}: MetadataProps): Promise<Metadata> {
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(params.orgslug, {
|
||||||
|
revalidate: 1800,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
|
||||||
// SEO
|
// SEO
|
||||||
return {
|
return {
|
||||||
|
|
@ -37,83 +40,127 @@ export async function generateMetadata(
|
||||||
googleBot: {
|
googleBot: {
|
||||||
index: true,
|
index: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
"max-image-preview": "large",
|
'max-image-preview': 'large',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: `Home — ${org.name}`,
|
title: `Home — ${org.name}`,
|
||||||
description: org.description,
|
description: org.description,
|
||||||
type: 'website',
|
type: 'website',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const OrgHomePage = async (params: any) => {
|
const OrgHomePage = async (params: any) => {
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug
|
||||||
const cookieStore = cookies();
|
const cookieStore = cookies()
|
||||||
|
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null);
|
const courses = await getOrgCoursesWithAuthHeader(
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
orgslug,
|
||||||
const org_id = org.id;
|
{ revalidate: 0, tags: ['courses'] },
|
||||||
const collections = await getOrgCollectionsWithAuthHeader(org.id, access_token ? access_token : null, { revalidate: 0, tags: ['courses'] });
|
access_token ? access_token : null
|
||||||
|
)
|
||||||
|
const org = await getOrganizationContextInfo(orgslug, {
|
||||||
|
revalidate: 1800,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
const org_id = org.id
|
||||||
|
const collections = await getOrgCollectionsWithAuthHeader(
|
||||||
|
org.id,
|
||||||
|
access_token ? access_token : null,
|
||||||
|
{ revalidate: 0, tags: ['courses'] }
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<GeneralWrapperStyled>
|
<GeneralWrapperStyled>
|
||||||
{/* Collections */}
|
{/* Collections */}
|
||||||
<div className='flex items-center '>
|
<div className="flex items-center ">
|
||||||
<div className='flex grow'>
|
<div className="flex grow">
|
||||||
<TypeOfContentTitle title="Collections" type="col" />
|
<TypeOfContentTitle title="Collections" type="col" />
|
||||||
</div>
|
</div>
|
||||||
<AuthenticatedClientElement
|
<AuthenticatedClientElement
|
||||||
checkMethod='roles'
|
checkMethod="roles"
|
||||||
ressourceType='collections'
|
ressourceType="collections"
|
||||||
action='create'
|
action="create"
|
||||||
orgId={org_id}>
|
orgId={org_id}
|
||||||
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
|
>
|
||||||
|
<Link href={getUriWithOrg(orgslug, '/collections/new')}>
|
||||||
<NewCollectionButton />
|
<NewCollectionButton />
|
||||||
</Link>
|
</Link>
|
||||||
</AuthenticatedClientElement>
|
</AuthenticatedClientElement>
|
||||||
</div>
|
</div>
|
||||||
<div className="home_collections flex flex-wrap">
|
<div className="home_collections flex flex-wrap">
|
||||||
{collections.map((collection: any) => (
|
{collections.map((collection: any) => (
|
||||||
<div className="flex flex-col py-3 px-3" key={collection.collection_id}>
|
<div
|
||||||
<CollectionThumbnail collection={collection} orgslug={orgslug} org_id={org.org_id} />
|
className="flex flex-col py-3 px-3"
|
||||||
|
key={collection.collection_id}
|
||||||
|
>
|
||||||
|
<CollectionThumbnail
|
||||||
|
collection={collection}
|
||||||
|
orgslug={orgslug}
|
||||||
|
org_id={org.org_id}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{collections.length == 0 &&
|
{collections.length == 0 && (
|
||||||
<div className="flex mx-auto h-[100px]">
|
<div className="flex mx-auto h-[100px]">
|
||||||
<div className="flex flex-col justify-center text-center items-center space-y-3">
|
<div className="flex flex-col justify-center text-center items-center space-y-3">
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<div className='mx-auto'>
|
<div className="mx-auto">
|
||||||
<svg width="50" height="50" viewBox="0 0 295 295" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect opacity="0.51" x="10" y="10" width="275" height="275" rx="75" stroke="#4B5564" strokeOpacity="0.15" strokeWidth="20" />
|
width="50"
|
||||||
<path d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z" fill="#4B5564" fillOpacity="0.08" />
|
height="50"
|
||||||
|
viewBox="0 0 295 295"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
opacity="0.51"
|
||||||
|
x="10"
|
||||||
|
y="10"
|
||||||
|
width="275"
|
||||||
|
height="275"
|
||||||
|
rx="75"
|
||||||
|
stroke="#4B5564"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="20"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z"
|
||||||
|
fill="#4B5564"
|
||||||
|
fillOpacity="0.08"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
<h1 className="text-xl font-bold text-gray-600">No collections yet</h1>
|
<h1 className="text-xl font-bold text-gray-600">
|
||||||
<p className="text-md text-gray-400">Create a collection to group courses together</p>
|
No collections yet
|
||||||
|
</h1>
|
||||||
|
<p className="text-md text-gray-400">
|
||||||
|
Create a collection to group courses together
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Courses */}
|
{/* Courses */}
|
||||||
<div className='h-5'></div>
|
<div className="h-5"></div>
|
||||||
<div className='flex items-center '>
|
<div className="flex items-center ">
|
||||||
<div className='flex grow'>
|
<div className="flex grow">
|
||||||
<TypeOfContentTitle title="Courses" type="cou" />
|
<TypeOfContentTitle title="Courses" type="cou" />
|
||||||
</div>
|
</div>
|
||||||
<AuthenticatedClientElement
|
<AuthenticatedClientElement
|
||||||
ressourceType='courses'
|
ressourceType="courses"
|
||||||
action='create'
|
action="create"
|
||||||
checkMethod='roles'
|
checkMethod="roles"
|
||||||
orgId={org_id}>
|
orgId={org_id}
|
||||||
<Link href={getUriWithOrg(orgslug, "/courses?new=true")}>
|
>
|
||||||
|
<Link href={getUriWithOrg(orgslug, '/courses?new=true')}>
|
||||||
<NewCourseButton />
|
<NewCourseButton />
|
||||||
</Link>
|
</Link>
|
||||||
</AuthenticatedClientElement>
|
</AuthenticatedClientElement>
|
||||||
|
|
@ -124,30 +171,52 @@ const OrgHomePage = async (params: any) => {
|
||||||
<CourseThumbnail course={course} orgslug={orgslug} />
|
<CourseThumbnail course={course} orgslug={orgslug} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{courses.length == 0 &&
|
{courses.length == 0 && (
|
||||||
<div className="flex mx-auto h-[300px]">
|
<div className="flex mx-auto h-[300px]">
|
||||||
<div className="flex flex-col justify-center text-center items-center space-y-3">
|
<div className="flex flex-col justify-center text-center items-center space-y-3">
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
<div className='mx-auto'>
|
<div className="mx-auto">
|
||||||
<svg width="50" height="50" viewBox="0 0 295 295" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect opacity="0.51" x="10" y="10" width="275" height="275" rx="75" stroke="#4B5564" strokeOpacity="0.15" strokeWidth="20" />
|
width="50"
|
||||||
<path d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z" fill="#4B5564" fillOpacity="0.08" />
|
height="50"
|
||||||
|
viewBox="0 0 295 295"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
opacity="0.51"
|
||||||
|
x="10"
|
||||||
|
y="10"
|
||||||
|
width="275"
|
||||||
|
height="275"
|
||||||
|
rx="75"
|
||||||
|
stroke="#4B5564"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="20"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z"
|
||||||
|
fill="#4B5564"
|
||||||
|
fillOpacity="0.08"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
<h1 className="text-xl font-bold text-gray-600">No courses yet</h1>
|
<h1 className="text-xl font-bold text-gray-600">
|
||||||
<p className="text-md text-gray-400">Create a course to add content</p>
|
No courses yet
|
||||||
|
</h1>
|
||||||
|
<p className="text-md text-gray-400">
|
||||||
|
Create a course to add content
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</GeneralWrapperStyled>
|
</GeneralWrapperStyled>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
);
|
export default OrgHomePage
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
export default OrgHomePage;
|
|
||||||
|
|
|
||||||
|
|
@ -1,33 +1,36 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import { Metadata } from "next";
|
import { Metadata } from 'next'
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { getOrganizationContextInfo } from '@services/organizations/orgs'
|
||||||
import Trail from "./trail";
|
import Trail from './trail'
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string };
|
params: { orgslug: string }
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
|
||||||
{ params }: MetadataProps,
|
|
||||||
): Promise<Metadata> {
|
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: MetadataProps): Promise<Metadata> {
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(params.orgslug, {
|
||||||
|
revalidate: 1800,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
return {
|
return {
|
||||||
title: "Trail — " + org.name,
|
title: 'Trail — ' + org.name,
|
||||||
description: 'Check your progress using trail and easily navigate through your courses.',
|
description:
|
||||||
};
|
'Check your progress using trail and easily navigate through your courses.',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const TrailPage = async (params: any) => {
|
const TrailPage = async (params: any) => {
|
||||||
let orgslug = params.params.orgslug;
|
let orgslug = params.params.orgslug
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<Trail orgslug={orgslug} />
|
<Trail orgslug={orgslug} />
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default TrailPage;
|
export default TrailPage
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
"use client";
|
'use client'
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import TrailCourseElement from "@components/Pages/Trail/TrailCourseElement";
|
import TrailCourseElement from '@components/Pages/Trail/TrailCourseElement'
|
||||||
import TypeOfContentTitle from "@components/StyledElements/Titles/TypeOfContentTitle";
|
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'
|
||||||
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
|
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
|
||||||
import { getAPIUrl } from "@services/config/config";
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { swrFetcher } from "@services/utils/ts/requests";
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from 'react'
|
||||||
import useSWR from "swr";
|
import useSWR from 'swr'
|
||||||
|
|
||||||
function Trail(params: any) {
|
function Trail(params: any) {
|
||||||
let orgslug = params.orgslug;
|
let orgslug = params.orgslug
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const orgID = org?.id;
|
const orgID = org?.id
|
||||||
const { data: trail, error: error } = useSWR(`${getAPIUrl()}trail/org/${orgID}/trail`, swrFetcher);
|
const { data: trail, error: error } = useSWR(
|
||||||
|
`${getAPIUrl()}trail/org/${orgID}/trail`,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {}, [trail, org])
|
||||||
}
|
|
||||||
, [trail,org]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GeneralWrapperStyled>
|
<GeneralWrapperStyled>
|
||||||
|
|
@ -28,19 +29,17 @@ function Trail(params: any) {
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{trail.runs.map((run: any) => (
|
{trail.runs.map((run: any) => (
|
||||||
<>
|
<>
|
||||||
<TrailCourseElement run={run} course={run.course} orgslug={orgslug} />
|
<TrailCourseElement
|
||||||
|
run={run}
|
||||||
|
course={run.course}
|
||||||
|
orgslug={orgslug}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</GeneralWrapperStyled>
|
</GeneralWrapperStyled>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Trail;
|
export default Trail
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,58 +1,61 @@
|
||||||
'use client';
|
'use client'
|
||||||
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
||||||
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse';
|
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse'
|
||||||
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
|
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'
|
||||||
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
|
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
|
||||||
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
|
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'
|
||||||
import Modal from '@components/StyledElements/Modal/Modal';
|
import Modal from '@components/StyledElements/Modal/Modal'
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { useSearchParams } from 'next/navigation'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
type CourseProps = {
|
type CourseProps = {
|
||||||
orgslug: string;
|
orgslug: string
|
||||||
courses: any;
|
courses: any
|
||||||
org_id: string;
|
org_id: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function CoursesHome(params: CourseProps) {
|
function CoursesHome(params: CourseProps) {
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams()
|
||||||
const isCreatingCourse = searchParams.get('new') ? true : false;
|
const isCreatingCourse = searchParams.get('new') ? true : false
|
||||||
const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse);
|
const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse)
|
||||||
const orgslug = params.orgslug;
|
const orgslug = params.orgslug
|
||||||
const courses = params.courses;
|
const courses = params.courses
|
||||||
|
|
||||||
|
|
||||||
async function closeNewCourseModal() {
|
async function closeNewCourseModal() {
|
||||||
setNewCourseModal(false);
|
setNewCourseModal(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full w-full bg-[#f8f8f8]'>
|
<div className="h-full w-full bg-[#f8f8f8]">
|
||||||
<div >
|
<div>
|
||||||
<div className='pl-10 mr-10 tracking-tighter'>
|
<div className="pl-10 mr-10 tracking-tighter">
|
||||||
<BreadCrumbs type='courses' />
|
<BreadCrumbs type="courses" />
|
||||||
|
|
||||||
<div className='w-100 flex justify-between'>
|
<div className="w-100 flex justify-between">
|
||||||
<div className='pt-3 flex font-bold text-4xl'>Courses</div>
|
<div className="pt-3 flex font-bold text-4xl">Courses</div>
|
||||||
<AuthenticatedClientElement checkMethod='roles'
|
<AuthenticatedClientElement
|
||||||
action='create'
|
checkMethod="roles"
|
||||||
ressourceType='courses'
|
action="create"
|
||||||
orgId={params.org_id}>
|
ressourceType="courses"
|
||||||
|
orgId={params.org_id}
|
||||||
|
>
|
||||||
<Modal
|
<Modal
|
||||||
isDialogOpen={newCourseModal}
|
isDialogOpen={newCourseModal}
|
||||||
onOpenChange={setNewCourseModal}
|
onOpenChange={setNewCourseModal}
|
||||||
minHeight="md"
|
minHeight="md"
|
||||||
dialogContent={<CreateCourseModal
|
dialogContent={
|
||||||
|
<CreateCourseModal
|
||||||
closeModal={closeNewCourseModal}
|
closeModal={closeNewCourseModal}
|
||||||
orgslug={orgslug}
|
orgslug={orgslug}
|
||||||
></CreateCourseModal>}
|
></CreateCourseModal>
|
||||||
|
}
|
||||||
dialogTitle="Create Course"
|
dialogTitle="Create Course"
|
||||||
dialogDescription="Create a new course"
|
dialogDescription="Create a new course"
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
|
|
||||||
<button>
|
<button>
|
||||||
<NewCourseButton />
|
<NewCourseButton />
|
||||||
</button>}
|
</button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</AuthenticatedClientElement>
|
</AuthenticatedClientElement>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -64,42 +67,71 @@ function CoursesHome(params: CourseProps) {
|
||||||
<CourseThumbnail course={course} orgslug={orgslug} />
|
<CourseThumbnail course={course} orgslug={orgslug} />
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
{courses.length == 0 &&
|
{courses.length == 0 && (
|
||||||
<div className="flex mx-auto h-[400px]">
|
<div className="flex mx-auto h-[400px]">
|
||||||
<div className="flex flex-col justify-center text-center items-center space-y-5">
|
<div className="flex flex-col justify-center text-center items-center space-y-5">
|
||||||
<div className='mx-auto'>
|
<div className="mx-auto">
|
||||||
<svg width="120" height="120" viewBox="0 0 295 295" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg
|
||||||
<rect opacity="0.51" x="10" y="10" width="275" height="275" rx="75" stroke="#4B5564" strokeOpacity="0.15" strokeWidth="20" />
|
width="120"
|
||||||
<path d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z" fill="#4B5564" fillOpacity="0.08" />
|
height="120"
|
||||||
|
viewBox="0 0 295 295"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<rect
|
||||||
|
opacity="0.51"
|
||||||
|
x="10"
|
||||||
|
y="10"
|
||||||
|
width="275"
|
||||||
|
height="275"
|
||||||
|
rx="75"
|
||||||
|
stroke="#4B5564"
|
||||||
|
strokeOpacity="0.15"
|
||||||
|
strokeWidth="20"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M135.8 200.8V130L122.2 114.6L135.8 110.4V102.8L122.2 87.4L159.8 76V200.8L174.6 218H121L135.8 200.8Z"
|
||||||
|
fill="#4B5564"
|
||||||
|
fillOpacity="0.08"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-0">
|
<div className="space-y-0">
|
||||||
<h1 className="text-3xl font-bold text-gray-600">No courses yet</h1>
|
<h1 className="text-3xl font-bold text-gray-600">
|
||||||
<p className="text-lg text-gray-400">Create a course to add content</p>
|
No courses yet
|
||||||
|
</h1>
|
||||||
|
<p className="text-lg text-gray-400">
|
||||||
|
Create a course to add content
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<AuthenticatedClientElement
|
<AuthenticatedClientElement
|
||||||
action='create'
|
action="create"
|
||||||
ressourceType='courses'
|
ressourceType="courses"
|
||||||
checkMethod='roles' orgId={params.org_id}>
|
checkMethod="roles"
|
||||||
|
orgId={params.org_id}
|
||||||
|
>
|
||||||
<Modal
|
<Modal
|
||||||
isDialogOpen={newCourseModal}
|
isDialogOpen={newCourseModal}
|
||||||
onOpenChange={setNewCourseModal}
|
onOpenChange={setNewCourseModal}
|
||||||
minHeight="md"
|
minHeight="md"
|
||||||
dialogContent={<CreateCourseModal
|
dialogContent={
|
||||||
|
<CreateCourseModal
|
||||||
closeModal={closeNewCourseModal}
|
closeModal={closeNewCourseModal}
|
||||||
orgslug={orgslug}
|
orgslug={orgslug}
|
||||||
></CreateCourseModal>}
|
></CreateCourseModal>
|
||||||
|
}
|
||||||
dialogTitle="Create Course"
|
dialogTitle="Create Course"
|
||||||
dialogDescription="Create a new course"
|
dialogDescription="Create a new course"
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
<button>
|
<button>
|
||||||
<NewCourseButton />
|
<NewCourseButton />
|
||||||
</button>}
|
</button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</AuthenticatedClientElement>
|
</AuthenticatedClientElement>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,72 +1,93 @@
|
||||||
'use client';
|
'use client'
|
||||||
import EditCourseStructure from '../../../../../../../../components/Dashboard/Course/EditCourseStructure/EditCourseStructure'
|
import EditCourseStructure from '../../../../../../../../components/Dashboard/Course/EditCourseStructure/EditCourseStructure'
|
||||||
import { getUriWithOrg } from '@services/config/config';
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { CourseProvider } from '../../../../../../../../components/Contexts/CourseContext';
|
import { CourseProvider } from '../../../../../../../../components/Contexts/CourseContext'
|
||||||
import Link from 'next/link';
|
import Link from 'next/link'
|
||||||
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop';
|
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop'
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion'
|
||||||
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral';
|
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral'
|
||||||
import { GalleryVerticalEnd, Info } from 'lucide-react';
|
import { GalleryVerticalEnd, Info } from 'lucide-react'
|
||||||
|
|
||||||
export type CourseOverviewParams = {
|
export type CourseOverviewParams = {
|
||||||
orgslug: string,
|
orgslug: string
|
||||||
courseuuid: string,
|
courseuuid: string
|
||||||
subpage: string
|
subpage: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
|
||||||
|
|
||||||
function getEntireCourseUUID(courseuuid: string) {
|
function getEntireCourseUUID(courseuuid: string) {
|
||||||
// add course_ to uuid
|
// add course_ to uuid
|
||||||
return `course_${courseuuid}`
|
return `course_${courseuuid}`
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]'>
|
<div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]">
|
||||||
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}>
|
<CourseProvider courseuuid={getEntireCourseUUID(params.courseuuid)}>
|
||||||
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]'>
|
<div className="pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]">
|
||||||
<CourseOverviewTop params={params} />
|
<CourseOverviewTop params={params} />
|
||||||
<div className='flex space-x-5 font-black text-sm'>
|
<div className="flex space-x-5 font-black text-sm">
|
||||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/general`}>
|
<Link
|
||||||
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
href={
|
||||||
|
getUriWithOrg(params.orgslug, '') +
|
||||||
<div className='flex items-center space-x-2.5 mx-2'>
|
`/dash/courses/course/${params.courseuuid}/general`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
|
||||||
|
params.subpage.toString() === 'general'
|
||||||
|
? 'border-b-4'
|
||||||
|
: 'opacity-50'
|
||||||
|
} cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<Info size={16} />
|
<Info size={16} />
|
||||||
<div>General</div>
|
<div>General</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/courses/course/${params.courseuuid}/content`}>
|
<Link
|
||||||
<div className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'content' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
href={
|
||||||
<div className='flex items-center space-x-2.5 mx-2'>
|
getUriWithOrg(params.orgslug, '') +
|
||||||
|
`/dash/courses/course/${params.courseuuid}/content`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${
|
||||||
|
params.subpage.toString() === 'content'
|
||||||
|
? 'border-b-4'
|
||||||
|
: 'opacity-50'
|
||||||
|
} cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<GalleryVerticalEnd size={16} />
|
<GalleryVerticalEnd size={16} />
|
||||||
<div>Content</div>
|
<div>Content</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
|
||||||
className='h-full overflow-y-auto'
|
className="h-full overflow-y-auto"
|
||||||
>
|
>
|
||||||
{params.subpage == 'content' ? <EditCourseStructure orgslug={params.orgslug} /> : ''}
|
{params.subpage == 'content' ? (
|
||||||
{params.subpage == 'general' ? <EditCourseGeneral orgslug={params.orgslug} /> : ''}
|
<EditCourseStructure orgslug={params.orgslug} />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
{params.subpage == 'general' ? (
|
||||||
|
<EditCourseGeneral orgslug={params.orgslug} />
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</CourseProvider>
|
</CourseProvider>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default CourseOverviewPage
|
export default CourseOverviewPage
|
||||||
|
|
@ -1,26 +1,28 @@
|
||||||
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
|
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
|
||||||
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses';
|
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses'
|
||||||
import { getOrganizationContextInfo } from '@services/organizations/orgs';
|
import { getOrganizationContextInfo } from '@services/organizations/orgs'
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next'
|
||||||
import { cookies } from 'next/headers';
|
import { cookies } from 'next/headers'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import CoursesHome from './client';
|
import CoursesHome from './client'
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string };
|
params: { orgslug: string }
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
|
||||||
{ params }: MetadataProps,
|
|
||||||
): Promise<Metadata> {
|
|
||||||
|
|
||||||
|
export async function generateMetadata({
|
||||||
|
params,
|
||||||
|
}: MetadataProps): Promise<Metadata> {
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(params.orgslug, {
|
||||||
|
revalidate: 1800,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
|
||||||
// SEO
|
// SEO
|
||||||
return {
|
return {
|
||||||
title: "Courses — " + org.name,
|
title: 'Courses — ' + org.name,
|
||||||
description: org.description,
|
description: org.description,
|
||||||
keywords: `${org.name}, ${org.description}, courses, learning, education, online learning, edu, online courses, ${org.name} courses`,
|
keywords: `${org.name}, ${org.description}, courses, learning, education, online learning, edu, online courses, ${org.name} courses`,
|
||||||
robots: {
|
robots: {
|
||||||
|
|
@ -30,28 +32,32 @@ export async function generateMetadata(
|
||||||
googleBot: {
|
googleBot: {
|
||||||
index: true,
|
index: true,
|
||||||
follow: true,
|
follow: true,
|
||||||
"max-image-preview": "large",
|
'max-image-preview': 'large',
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
openGraph: {
|
openGraph: {
|
||||||
title: "Courses — " + org.name,
|
title: 'Courses — ' + org.name,
|
||||||
description: org.description,
|
description: org.description,
|
||||||
type: 'website',
|
type: 'website',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function CoursesPage(params: any) {
|
async function CoursesPage(params: any) {
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, {
|
||||||
const cookieStore = cookies();
|
revalidate: 1800,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
const cookieStore = cookies()
|
||||||
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
|
||||||
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null);
|
const courses = await getOrgCoursesWithAuthHeader(
|
||||||
|
orgslug,
|
||||||
|
{ revalidate: 0, tags: ['courses'] },
|
||||||
return (
|
access_token ? access_token : null
|
||||||
<CoursesHome org_id={org.org_id} orgslug={orgslug} courses={courses} />
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
return <CoursesHome org_id={org.org_id} orgslug={orgslug} courses={courses} />
|
||||||
}
|
}
|
||||||
|
|
||||||
export default CoursesPage
|
export default CoursesPage
|
||||||
|
|
@ -3,16 +3,20 @@ import LeftMenu from '@components/Dashboard/UI/LeftMenu'
|
||||||
import AdminAuthorization from '@components/Security/AdminAuthorization'
|
import AdminAuthorization from '@components/Security/AdminAuthorization'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) {
|
function DashboardLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
params: any
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<SessionProvider>
|
<SessionProvider>
|
||||||
<AdminAuthorization authorizationMode="page">
|
<AdminAuthorization authorizationMode="page">
|
||||||
<div className='flex'>
|
<div className="flex">
|
||||||
<LeftMenu />
|
<LeftMenu />
|
||||||
<div className='flex w-full'>
|
<div className="flex w-full">{children}</div>
|
||||||
{children}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</AdminAuthorization>
|
</AdminAuthorization>
|
||||||
</SessionProvider>
|
</SessionProvider>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
'use client';
|
'use client'
|
||||||
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
||||||
import { getUriWithOrg } from '@services/config/config'
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import { Info } from 'lucide-react'
|
import { Info } from 'lucide-react'
|
||||||
|
|
@ -14,33 +14,43 @@ export type OrgParams = {
|
||||||
|
|
||||||
function OrgPage({ params }: { params: OrgParams }) {
|
function OrgPage({ params }: { params: OrgParams }) {
|
||||||
return (
|
return (
|
||||||
<div className='h-full w-full bg-[#f8f8f8]'>
|
<div className="h-full w-full bg-[#f8f8f8]">
|
||||||
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]'>
|
<div className="pl-10 pr-10 tracking-tight bg-[#fcfbfc] shadow-[0px_4px_16px_rgba(0,0,0,0.02)]">
|
||||||
<BreadCrumbs type='org' ></BreadCrumbs>
|
<BreadCrumbs type="org"></BreadCrumbs>
|
||||||
<div className='my-2 tracking-tighter'>
|
<div className="my-2 tracking-tighter">
|
||||||
<div className='w-100 flex justify-between'>
|
<div className="w-100 flex justify-between">
|
||||||
<div className='pt-3 flex font-bold text-4xl'>Organization Settings</div>
|
<div className="pt-3 flex font-bold text-4xl">
|
||||||
|
Organization Settings
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex space-x-5 font-black text-sm'>
|
</div>
|
||||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/org/settings/general`}>
|
<div className="flex space-x-5 font-black text-sm">
|
||||||
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
<Link
|
||||||
|
href={
|
||||||
<div className='flex items-center space-x-2.5 mx-2'>
|
getUriWithOrg(params.orgslug, '') + `/dash/org/settings/general`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
|
||||||
|
params.subpage.toString() === 'general'
|
||||||
|
? 'border-b-4'
|
||||||
|
: 'opacity-50'
|
||||||
|
} cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<Info size={16} />
|
<Info size={16} />
|
||||||
<div>General</div>
|
<div>General</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='h-6'></div>
|
<div className="h-6"></div>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
|
||||||
>
|
>
|
||||||
{params.subpage == 'general' ? <OrgEditGeneral /> : ''}
|
{params.subpage == 'general' ? <OrgEditGeneral /> : ''}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
||||||
|
|
@ -8,52 +8,82 @@ import AdminAuthorization from '@components/Security/AdminAuthorization'
|
||||||
function DashboardHome() {
|
function DashboardHome() {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3">
|
<div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3">
|
||||||
<div className='mx-auto pb-10'>
|
<div className="mx-auto pb-10">
|
||||||
<Image alt='learnhouse logo' width={230} src={learnhousetextlogo}></Image>
|
<Image
|
||||||
|
alt="learnhouse logo"
|
||||||
|
width={230}
|
||||||
|
src={learnhousetextlogo}
|
||||||
|
></Image>
|
||||||
</div>
|
</div>
|
||||||
<AdminAuthorization authorizationMode="component">
|
<AdminAuthorization authorizationMode="component">
|
||||||
<div className='flex space-x-10'>
|
<div className="flex space-x-10">
|
||||||
<Link href={`/dash/courses`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
<Link
|
||||||
<div className='flex flex-col mx-auto space-y-2'>
|
href={`/dash/courses`}
|
||||||
<BookCopy className='mx-auto text-gray-500' size={50}></BookCopy>
|
className="flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer"
|
||||||
<div className='text-center font-bold text-gray-500'>Courses</div>
|
>
|
||||||
<p className='text-center text-sm text-gray-400'>Create and manage courses, chapters and ativities </p>
|
<div className="flex flex-col mx-auto space-y-2">
|
||||||
|
<BookCopy className="mx-auto text-gray-500" size={50}></BookCopy>
|
||||||
|
<div className="text-center font-bold text-gray-500">Courses</div>
|
||||||
|
<p className="text-center text-sm text-gray-400">
|
||||||
|
Create and manage courses, chapters and ativities{' '}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={`/dash/org/settings/general`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
<Link
|
||||||
<div className='flex flex-col mx-auto space-y-2'>
|
href={`/dash/org/settings/general`}
|
||||||
<School className='mx-auto text-gray-500' size={50}></School>
|
className="flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer"
|
||||||
<div className='text-center font-bold text-gray-500'>Organization</div>
|
>
|
||||||
<p className='text-center text-sm text-gray-400'>Configure your Organization general settings </p>
|
<div className="flex flex-col mx-auto space-y-2">
|
||||||
|
<School className="mx-auto text-gray-500" size={50}></School>
|
||||||
|
<div className="text-center font-bold text-gray-500">
|
||||||
|
Organization
|
||||||
|
</div>
|
||||||
|
<p className="text-center text-sm text-gray-400">
|
||||||
|
Configure your Organization general settings{' '}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={`/dash/users/settings/users`} className='flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
<Link
|
||||||
<div className='flex flex-col mx-auto space-y-2'>
|
href={`/dash/users/settings/users`}
|
||||||
<Users className='mx-auto text-gray-500' size={50}></Users>
|
className="flex bg-white shadow-lg p-[35px] w-[250px] rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer"
|
||||||
<div className='text-center font-bold text-gray-500'>Users</div>
|
>
|
||||||
<p className='text-center text-sm text-gray-400'>Manage your Organization's users, roles </p>
|
<div className="flex flex-col mx-auto space-y-2">
|
||||||
|
<Users className="mx-auto text-gray-500" size={50}></Users>
|
||||||
|
<div className="text-center font-bold text-gray-500">Users</div>
|
||||||
|
<p className="text-center text-sm text-gray-400">
|
||||||
|
Manage your Organization's users, roles{' '}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</AdminAuthorization>
|
</AdminAuthorization>
|
||||||
<div className='flex flex-col space-y-10 '>
|
<div className="flex flex-col space-y-10 ">
|
||||||
<AdminAuthorization authorizationMode="component">
|
<AdminAuthorization authorizationMode="component">
|
||||||
<div className='h-1 w-[100px] bg-neutral-200 rounded-full mx-auto'></div>
|
<div className="h-1 w-[100px] bg-neutral-200 rounded-full mx-auto"></div>
|
||||||
<div className="flex justify-center items-center">
|
<div className="flex justify-center items-center">
|
||||||
<Link href={'https://learn.learnhouse.io/'} className='flex mt-[40px] bg-black space-x-2 items-center py-3 px-7 rounded-lg shadow-lg hover:scale-105 transition-all ease-linear cursor-pointer'>
|
<Link
|
||||||
<BookCopy className=' text-gray-100' size={20}></BookCopy>
|
href={'https://learn.learnhouse.io/'}
|
||||||
<div className=' text-sm font-bold text-gray-100'>Learn LearnHouse</div>
|
className="flex mt-[40px] bg-black space-x-2 items-center py-3 px-7 rounded-lg shadow-lg hover:scale-105 transition-all ease-linear cursor-pointer"
|
||||||
|
>
|
||||||
|
<BookCopy className=" text-gray-100" size={20}></BookCopy>
|
||||||
|
<div className=" text-sm font-bold text-gray-100">
|
||||||
|
Learn LearnHouse
|
||||||
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className='mx-auto mt-[40px] w-28 h-1 bg-neutral-200 rounded-full'></div>
|
<div className="mx-auto mt-[40px] w-28 h-1 bg-neutral-200 rounded-full"></div>
|
||||||
</AdminAuthorization>
|
</AdminAuthorization>
|
||||||
|
|
||||||
<Link href={'/dash/user-account/settings/general'} className='flex bg-white shadow-lg p-[15px] items-center rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer'>
|
<Link
|
||||||
<div className='flex flex-row mx-auto space-x-3 items-center'>
|
href={'/dash/user-account/settings/general'}
|
||||||
<Settings className=' text-gray-500' size={20}></Settings>
|
className="flex bg-white shadow-lg p-[15px] items-center rounded-lg items-center mx-auto hover:scale-105 transition-all ease-linear cursor-pointer"
|
||||||
<div className=' font-bold text-gray-500'>Account Settings</div>
|
>
|
||||||
<p className=' text-sm text-gray-400'>Configure your personal settings, passwords, email</p>
|
<div className="flex flex-row mx-auto space-x-3 items-center">
|
||||||
|
<Settings className=" text-gray-500" size={20}></Settings>
|
||||||
|
<div className=" font-bold text-gray-500">Account Settings</div>
|
||||||
|
<p className=" text-sm text-gray-400">
|
||||||
|
Configure your personal settings, passwords, email
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
'use client';
|
'use client'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion'
|
||||||
import UserEditGeneral from '@components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral';
|
import UserEditGeneral from '@components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral'
|
||||||
import UserEditPassword from '@components/Dashboard/UserAccount/UserEditPassword/UserEditPassword';
|
import UserEditPassword from '@components/Dashboard/UserAccount/UserEditPassword/UserEditPassword'
|
||||||
import Link from 'next/link';
|
import Link from 'next/link'
|
||||||
import { getUriWithOrg } from '@services/config/config';
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import { Info, Lock } from 'lucide-react';
|
import { Info, Lock } from 'lucide-react'
|
||||||
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs';
|
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
||||||
import { useSession } from '@components/Contexts/SessionContext';
|
import { useSession } from '@components/Contexts/SessionContext'
|
||||||
|
|
||||||
export type SettingsParams = {
|
export type SettingsParams = {
|
||||||
subpage: string
|
subpage: string
|
||||||
|
|
@ -15,50 +15,70 @@ export type SettingsParams = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function SettingsPage({ params }: { params: SettingsParams }) {
|
function SettingsPage({ params }: { params: SettingsParams }) {
|
||||||
const session = useSession() as any;
|
const session = useSession() as any
|
||||||
|
|
||||||
|
useEffect(() => {}, [session])
|
||||||
useEffect(() => {
|
|
||||||
}
|
|
||||||
, [session])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-full w-full bg-[#f8f8f8]'>
|
<div className="h-full w-full bg-[#f8f8f8]">
|
||||||
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]'>
|
<div className="pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]">
|
||||||
<BreadCrumbs type='user' last_breadcrumb={session?.user?.username} ></BreadCrumbs>
|
<BreadCrumbs
|
||||||
<div className='my-2 tracking-tighter'>
|
type="user"
|
||||||
<div className='w-100 flex justify-between'>
|
last_breadcrumb={session?.user?.username}
|
||||||
<div className='pt-3 flex font-bold text-4xl'>Account Settings</div>
|
></BreadCrumbs>
|
||||||
|
<div className="my-2 tracking-tighter">
|
||||||
|
<div className="w-100 flex justify-between">
|
||||||
|
<div className="pt-3 flex font-bold text-4xl">Account Settings</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex space-x-5 font-black text-sm'>
|
<div className="flex space-x-5 font-black text-sm">
|
||||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user-account/settings/general`}>
|
<Link
|
||||||
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'general' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
href={
|
||||||
|
getUriWithOrg(params.orgslug, '') +
|
||||||
<div className='flex items-center space-x-2.5 mx-2'>
|
`/dash/user-account/settings/general`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
|
||||||
|
params.subpage.toString() === 'general'
|
||||||
|
? 'border-b-4'
|
||||||
|
: 'opacity-50'
|
||||||
|
} cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<Info size={16} />
|
<Info size={16} />
|
||||||
<div>General</div>
|
<div>General</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/user-account/settings/security`}>
|
<Link
|
||||||
<div className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'security' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
href={
|
||||||
<div className='flex items-center space-x-2.5 mx-2'>
|
getUriWithOrg(params.orgslug, '') +
|
||||||
|
`/dash/user-account/settings/security`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`flex space-x-4 py-2 w-fit text-center border-black transition-all ease-linear ${
|
||||||
|
params.subpage.toString() === 'security'
|
||||||
|
? 'border-b-4'
|
||||||
|
: 'opacity-50'
|
||||||
|
} cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<Lock size={16} />
|
<Lock size={16} />
|
||||||
<div>Password</div>
|
<div>Password</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='h-6'></div>
|
<div className="h-6"></div>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
|
||||||
className='h-full overflow-y-auto'
|
className="h-full overflow-y-auto"
|
||||||
>
|
>
|
||||||
{params.subpage == 'general' ? <UserEditGeneral /> : ''}
|
{params.subpage == 'general' ? <UserEditGeneral /> : ''}
|
||||||
{params.subpage == 'security' ? <UserEditPassword /> : ''}
|
{params.subpage == 'security' ? <UserEditPassword /> : ''}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
'use client';
|
'use client'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { motion } from 'framer-motion';
|
import { motion } from 'framer-motion'
|
||||||
import Link from 'next/link';
|
import Link from 'next/link'
|
||||||
import { getUriWithOrg } from '@services/config/config';
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import { ScanEye, UserPlus, Users } from 'lucide-react';
|
import { ScanEye, UserPlus, Users } from 'lucide-react'
|
||||||
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs';
|
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
|
||||||
import { useSession } from '@components/Contexts/SessionContext';
|
import { useSession } from '@components/Contexts/SessionContext'
|
||||||
import { useOrg } from '@components/Contexts/OrgContext';
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers';
|
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers'
|
||||||
import OrgAccess from '@components/Dashboard/Users/OrgAccess/OrgAccess';
|
import OrgAccess from '@components/Dashboard/Users/OrgAccess/OrgAccess'
|
||||||
|
|
||||||
export type SettingsParams = {
|
export type SettingsParams = {
|
||||||
subpage: string
|
subpage: string
|
||||||
|
|
@ -16,8 +16,8 @@ export type SettingsParams = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function UsersSettingsPage({ params }: { params: SettingsParams }) {
|
function UsersSettingsPage({ params }: { params: SettingsParams }) {
|
||||||
const session = useSession() as any;
|
const session = useSession() as any
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const [H1Label, setH1Label] = React.useState('')
|
const [H1Label, setH1Label] = React.useState('')
|
||||||
const [H2Label, setH2Label] = React.useState('')
|
const [H2Label, setH2Label] = React.useState('')
|
||||||
|
|
||||||
|
|
@ -36,60 +36,87 @@ function UsersSettingsPage({ params }: { params: SettingsParams }) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
handleLabels()
|
handleLabels()
|
||||||
}
|
}, [session, org, params.subpage, params])
|
||||||
, [session, org, params.subpage, params])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]'>
|
<div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]">
|
||||||
<div className='pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]'>
|
<div className="pl-10 pr-10 tracking-tight bg-[#fcfbfc] z-10 shadow-[0px_4px_16px_rgba(0,0,0,0.06)]">
|
||||||
<BreadCrumbs type='orgusers' ></BreadCrumbs>
|
<BreadCrumbs type="orgusers"></BreadCrumbs>
|
||||||
<div className='my-2 py-3'>
|
<div className="my-2 py-3">
|
||||||
<div className='w-100 flex flex-col space-y-1'>
|
<div className="w-100 flex flex-col space-y-1">
|
||||||
<div className='pt-3 flex font-bold text-4xl tracking-tighter'>{H1Label}</div>
|
<div className="pt-3 flex font-bold text-4xl tracking-tighter">
|
||||||
<div className='flex font-medium text-gray-400 text-md'>{H2Label} </div>
|
{H1Label}
|
||||||
|
</div>
|
||||||
|
<div className="flex font-medium text-gray-400 text-md">
|
||||||
|
{H2Label}{' '}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex space-x-5 font-black text-sm'>
|
</div>
|
||||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/users/settings/users`}>
|
<div className="flex space-x-5 font-black text-sm">
|
||||||
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'users' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
<Link
|
||||||
|
href={
|
||||||
<div className='flex items-center space-x-2.5 mx-2'>
|
getUriWithOrg(params.orgslug, '') + `/dash/users/settings/users`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
|
||||||
|
params.subpage.toString() === 'users'
|
||||||
|
? 'border-b-4'
|
||||||
|
: 'opacity-50'
|
||||||
|
} cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<Users size={16} />
|
<Users size={16} />
|
||||||
<div>Users</div>
|
<div>Users</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/users/settings/add`}>
|
<Link
|
||||||
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'add' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
href={
|
||||||
<div className='flex items-center space-x-2.5 mx-2'>
|
getUriWithOrg(params.orgslug, '') + `/dash/users/settings/add`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
|
||||||
|
params.subpage.toString() === 'add'
|
||||||
|
? 'border-b-4'
|
||||||
|
: 'opacity-50'
|
||||||
|
} cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<UserPlus size={16} />
|
<UserPlus size={16} />
|
||||||
<div>Invite users</div>
|
<div>Invite users</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
<Link href={getUriWithOrg(params.orgslug, "") + `/dash/users/settings/signups`}>
|
<Link
|
||||||
<div className={`py-2 w-fit text-center border-black transition-all ease-linear ${params.subpage.toString() === 'signups' ? 'border-b-4' : 'opacity-50'} cursor-pointer`}>
|
href={
|
||||||
<div className='flex items-center space-x-2.5 mx-2'>
|
getUriWithOrg(params.orgslug, '') + `/dash/users/settings/signups`
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={`py-2 w-fit text-center border-black transition-all ease-linear ${
|
||||||
|
params.subpage.toString() === 'signups'
|
||||||
|
? 'border-b-4'
|
||||||
|
: 'opacity-50'
|
||||||
|
} cursor-pointer`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center space-x-2.5 mx-2">
|
||||||
<ScanEye size={16} />
|
<ScanEye size={16} />
|
||||||
<div>Signup Access</div>
|
<div>Signup Access</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ opacity: 0, }}
|
initial={{ opacity: 0 }}
|
||||||
animate={{ opacity: 1 }}
|
animate={{ opacity: 1 }}
|
||||||
exit={{ opacity: 0 }}
|
exit={{ opacity: 0 }}
|
||||||
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
|
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
|
||||||
className='h-full overflow-y-auto'
|
className="h-full overflow-y-auto"
|
||||||
>
|
>
|
||||||
{params.subpage == 'users' ? <OrgUsers /> : ''}
|
{params.subpage == 'users' ? <OrgUsers /> : ''}
|
||||||
{params.subpage == 'signups' ? <OrgAccess /> : ''}
|
{params.subpage == 'signups' ? <OrgAccess /> : ''}
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,20 @@
|
||||||
'use client';
|
'use client'
|
||||||
import { OrgProvider } from "@components/Contexts/OrgContext";
|
import { OrgProvider } from '@components/Contexts/OrgContext'
|
||||||
import SessionProvider from "@components/Contexts/SessionContext";
|
import SessionProvider from '@components/Contexts/SessionContext'
|
||||||
import "@styles/globals.css";
|
import '@styles/globals.css'
|
||||||
|
|
||||||
export default function RootLayout({ children, params }: { children: React.ReactNode, params: any }) {
|
|
||||||
|
|
||||||
|
export default function RootLayout({
|
||||||
|
children,
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
params: any
|
||||||
|
}) {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<OrgProvider orgslug={params.orgslug}>
|
<OrgProvider orgslug={params.orgslug}>
|
||||||
<SessionProvider>
|
<SessionProvider>{children}</SessionProvider>
|
||||||
{children}
|
|
||||||
</SessionProvider>
|
|
||||||
</OrgProvider>
|
</OrgProvider>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,92 +1,116 @@
|
||||||
"use client";;
|
'use client'
|
||||||
import learnhouseIcon from "public/learnhouse_bigicon_1.png";
|
import learnhouseIcon from 'public/learnhouse_bigicon_1.png'
|
||||||
import FormLayout, { FormField, FormLabelAndMessage, Input } from '@components/StyledElements/Form/Form';
|
import FormLayout, {
|
||||||
import Image from 'next/image';
|
FormField,
|
||||||
import * as Form from '@radix-ui/react-form';
|
FormLabelAndMessage,
|
||||||
import { useFormik } from 'formik';
|
Input,
|
||||||
import { getOrgLogoMediaDirectory } from "@services/media/media";
|
} from '@components/StyledElements/Form/Form'
|
||||||
import React from "react";
|
import Image from 'next/image'
|
||||||
import { loginAndGetToken } from "@services/auth/auth";
|
import * as Form from '@radix-ui/react-form'
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { useFormik } from 'formik'
|
||||||
import { useRouter } from "next/navigation";
|
import { getOrgLogoMediaDirectory } from '@services/media/media'
|
||||||
import Link from "next/link";
|
import React from 'react'
|
||||||
import { getUriWithOrg } from "@services/config/config";
|
import { loginAndGetToken } from '@services/auth/auth'
|
||||||
|
import { AlertTriangle } from 'lucide-react'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
|
|
||||||
interface LoginClientProps {
|
interface LoginClientProps {
|
||||||
org: any;
|
org: any
|
||||||
}
|
}
|
||||||
|
|
||||||
const validate = (values: any) => {
|
const validate = (values: any) => {
|
||||||
const errors: any = {};
|
const errors: any = {}
|
||||||
|
|
||||||
if (!values.email) {
|
if (!values.email) {
|
||||||
errors.email = 'Required';
|
errors.email = 'Required'
|
||||||
}
|
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
|
||||||
else if (
|
errors.email = 'Invalid email address'
|
||||||
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
|
|
||||||
) {
|
|
||||||
errors.email = 'Invalid email address';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.password) {
|
if (!values.password) {
|
||||||
errors.password = 'Required';
|
errors.password = 'Required'
|
||||||
}
|
} else if (values.password.length < 8) {
|
||||||
else if (values.password.length < 8) {
|
errors.password = 'Password must be at least 8 characters'
|
||||||
errors.password = 'Password must be at least 8 characters';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors
|
||||||
};
|
}
|
||||||
|
|
||||||
const LoginClient = (props: LoginClientProps) => {
|
const LoginClient = (props: LoginClientProps) => {
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const [error, setError] = React.useState('');
|
const [error, setError] = React.useState('')
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
email: '',
|
email: '',
|
||||||
password: '',
|
password: '',
|
||||||
},
|
},
|
||||||
validate,
|
validate,
|
||||||
onSubmit: async values => {
|
onSubmit: async (values) => {
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true)
|
||||||
let res = await loginAndGetToken(values.email, values.password);
|
let res = await loginAndGetToken(values.email, values.password)
|
||||||
let message = await res.json();
|
let message = await res.json()
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
router.push(`/`);
|
router.push(`/`)
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false)
|
||||||
|
} else if (
|
||||||
|
res.status == 401 ||
|
||||||
|
res.status == 400 ||
|
||||||
|
res.status == 404 ||
|
||||||
|
res.status == 409
|
||||||
|
) {
|
||||||
|
setError(message.detail)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
} else {
|
||||||
|
setError('Something went wrong')
|
||||||
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) {
|
|
||||||
setError(message.detail);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setError("Something went wrong");
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
return (
|
return (
|
||||||
<div className='grid grid-flow-col justify-stretch h-screen'>
|
<div className="grid grid-flow-col justify-stretch h-screen">
|
||||||
<div className="right-login-part" style={{ background: "linear-gradient(041.61deg, #202020 7.15%, #000000 90.96%)" }} >
|
<div
|
||||||
<div className='login-topbar m-10'>
|
className="right-login-part"
|
||||||
<Link prefetch href={getUriWithOrg(props.org.slug, "/")}>
|
style={{
|
||||||
<Image quality={100} width={30} height={30} src={learnhouseIcon} alt="" />
|
background:
|
||||||
</Link></div>
|
'linear-gradient(041.61deg, #202020 7.15%, #000000 90.96%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="login-topbar m-10">
|
||||||
|
<Link prefetch href={getUriWithOrg(props.org.slug, '/')}>
|
||||||
|
<Image
|
||||||
|
quality={100}
|
||||||
|
width={30}
|
||||||
|
height={30}
|
||||||
|
src={learnhouseIcon}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
<div className="ml-10 h-4/6 flex flex-row text-white">
|
<div className="ml-10 h-4/6 flex flex-row text-white">
|
||||||
<div className="m-auto flex space-x-4 items-center flex-wrap">
|
<div className="m-auto flex space-x-4 items-center flex-wrap">
|
||||||
<div>Login to </div>
|
<div>Login to </div>
|
||||||
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" >
|
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]">
|
||||||
{props.org?.logo_image ? (
|
{props.org?.logo_image ? (
|
||||||
<img
|
<img
|
||||||
src={`${getOrgLogoMediaDirectory(props.org.org_uuid, props.org?.logo_image)}`}
|
src={`${getOrgLogoMediaDirectory(
|
||||||
|
props.org.org_uuid,
|
||||||
|
props.org?.logo_image
|
||||||
|
)}`}
|
||||||
alt="Learnhouse"
|
alt="Learnhouse"
|
||||||
style={{ width: "auto", height: 70 }}
|
style={{ width: 'auto', height: 70 }}
|
||||||
className="rounded-xl shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white"
|
className="rounded-xl shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Image quality={100} width={70} height={70} src={learnhouseIcon} alt="" />
|
<Image
|
||||||
|
quality={100}
|
||||||
|
width={70}
|
||||||
|
height={70}
|
||||||
|
src={learnhouseIcon}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-bold text-xl">{props.org?.name}</div>
|
<div className="font-bold text-xl">{props.org?.name}</div>
|
||||||
|
|
@ -103,36 +127,48 @@ const LoginClient = (props: LoginClientProps) => {
|
||||||
)}
|
)}
|
||||||
<FormLayout onSubmit={formik.handleSubmit}>
|
<FormLayout onSubmit={formik.handleSubmit}>
|
||||||
<FormField name="email">
|
<FormField name="email">
|
||||||
<FormLabelAndMessage label='Email' message={formik.errors.email} />
|
<FormLabelAndMessage
|
||||||
|
label="Email"
|
||||||
|
message={formik.errors.email}
|
||||||
|
/>
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.email}
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
{/* for password */}
|
{/* for password */}
|
||||||
<FormField name="password">
|
<FormField name="password">
|
||||||
<FormLabelAndMessage label='Password' message={formik.errors.password} />
|
<FormLabelAndMessage
|
||||||
|
label="Password"
|
||||||
|
message={formik.errors.password}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.password}
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
|
|
||||||
<div className="flex py-4">
|
<div className="flex py-4">
|
||||||
<Form.Submit asChild>
|
<Form.Submit asChild>
|
||||||
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
|
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer">
|
||||||
{isSubmitting ? "Loading..."
|
{isSubmitting ? 'Loading...' : 'Login'}
|
||||||
: "Login"}
|
|
||||||
</button>
|
</button>
|
||||||
</Form.Submit>
|
</Form.Submit>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
export default LoginClient
|
export default LoginClient
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,39 @@
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import { getOrganizationContextInfo } from '@services/organizations/orgs'
|
||||||
import LoginClient from "./login";
|
import LoginClient from './login'
|
||||||
import { Metadata } from 'next';
|
import { Metadata } from 'next'
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseid: string };
|
params: { orgslug: string; courseid: string }
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata({
|
||||||
{ params }: MetadataProps,
|
params,
|
||||||
): Promise<Metadata> {
|
}: MetadataProps): Promise<Metadata> {
|
||||||
const orgslug = params.orgslug;
|
const orgslug = params.orgslug
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, {
|
||||||
|
revalidate: 0,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: 'Login' + ` — ${org.name}`,
|
title: 'Login' + ` — ${org.name}`,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const Login = async (params: any) => {
|
const Login = async (params: any) => {
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, {
|
||||||
|
revalidate: 0,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<LoginClient org={org}></LoginClient>
|
<LoginClient org={org}></LoginClient>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
|
export default Login
|
||||||
|
|
||||||
export default Login;
|
|
||||||
|
|
|
||||||
|
|
@ -1,62 +1,59 @@
|
||||||
"use client";
|
'use client'
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form';
|
import FormLayout, {
|
||||||
import * as Form from '@radix-ui/react-form';
|
FormField,
|
||||||
import { AlertTriangle, Check, User } from 'lucide-react';
|
FormLabelAndMessage,
|
||||||
import Link from 'next/link';
|
Input,
|
||||||
import { signUpWithInviteCode } from '@services/auth/auth';
|
Textarea,
|
||||||
import { useOrg } from '@components/Contexts/OrgContext';
|
} from '@components/StyledElements/Form/Form'
|
||||||
|
import * as Form from '@radix-ui/react-form'
|
||||||
|
import { AlertTriangle, Check, User } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { signUpWithInviteCode } from '@services/auth/auth'
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
|
||||||
const validate = (values: any) => {
|
const validate = (values: any) => {
|
||||||
const errors: any = {};
|
const errors: any = {}
|
||||||
|
|
||||||
if (!values.email) {
|
if (!values.email) {
|
||||||
errors.email = 'Required';
|
errors.email = 'Required'
|
||||||
}
|
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
|
||||||
else if (
|
errors.email = 'Invalid email address'
|
||||||
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
|
|
||||||
) {
|
|
||||||
errors.email = 'Invalid email address';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.password) {
|
if (!values.password) {
|
||||||
errors.password = 'Required';
|
errors.password = 'Required'
|
||||||
}
|
} else if (values.password.length < 8) {
|
||||||
else if (values.password.length < 8) {
|
errors.password = 'Password must be at least 8 characters'
|
||||||
errors.password = 'Password must be at least 8 characters';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.username) {
|
if (!values.username) {
|
||||||
errors.username = 'Required';
|
errors.username = 'Required'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.username || values.username.length < 4) {
|
if (!values.username || values.username.length < 4) {
|
||||||
errors.username = 'Username must be at least 4 characters';
|
errors.username = 'Username must be at least 4 characters'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.bio) {
|
if (!values.bio) {
|
||||||
errors.bio = 'Required';
|
errors.bio = 'Required'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
return errors;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface InviteOnlySignUpProps {
|
|
||||||
inviteCode: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function InviteOnlySignUpComponent(props : InviteOnlySignUpProps) {
|
interface InviteOnlySignUpProps {
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
inviteCode: string
|
||||||
const org = useOrg() as any;
|
}
|
||||||
const router = useRouter();
|
|
||||||
const [error, setError] = React.useState('');
|
function InviteOnlySignUpComponent(props: InviteOnlySignUpProps) {
|
||||||
const [message, setMessage] = React.useState('');
|
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||||
|
const org = useOrg() as any
|
||||||
|
const router = useRouter()
|
||||||
|
const [error, setError] = React.useState('')
|
||||||
|
const [message, setMessage] = React.useState('')
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
org_slug: org?.slug,
|
org_slug: org?.slug,
|
||||||
|
|
@ -69,34 +66,32 @@ function InviteOnlySignUpComponent(props : InviteOnlySignUpProps) {
|
||||||
last_name: '',
|
last_name: '',
|
||||||
},
|
},
|
||||||
validate,
|
validate,
|
||||||
onSubmit: async values => {
|
onSubmit: async (values) => {
|
||||||
setError('')
|
setError('')
|
||||||
setMessage('')
|
setMessage('')
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true)
|
||||||
let res = await signUpWithInviteCode(values, props.inviteCode);
|
let res = await signUpWithInviteCode(values, props.inviteCode)
|
||||||
let message = await res.json();
|
let message = await res.json()
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
//router.push(`/login`);
|
//router.push(`/login`);
|
||||||
setMessage('Your account was successfully created')
|
setMessage('Your account was successfully created')
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false)
|
||||||
|
} else if (
|
||||||
|
res.status == 401 ||
|
||||||
|
res.status == 400 ||
|
||||||
|
res.status == 404 ||
|
||||||
|
res.status == 409
|
||||||
|
) {
|
||||||
|
setError(message.detail)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
} else {
|
||||||
|
setError('Something went wrong')
|
||||||
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) {
|
|
||||||
setError(message.detail);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setError("Something went wrong");
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {}, [org])
|
||||||
|
|
||||||
}
|
|
||||||
, [org]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-form m-auto w-72">
|
<div className="login-form m-auto w-72">
|
||||||
|
|
@ -108,56 +103,81 @@ function InviteOnlySignUpComponent(props : InviteOnlySignUpProps) {
|
||||||
)}
|
)}
|
||||||
{message && (
|
{message && (
|
||||||
<div className="flex flex-col space-y-4 justify-center bg-green-200 rounded-md text-green-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
<div className="flex flex-col space-y-4 justify-center bg-green-200 rounded-md text-green-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||||
<div className='flex space-x-2'>
|
<div className="flex space-x-2">
|
||||||
<Check size={18} />
|
<Check size={18} />
|
||||||
<div className="font-bold text-sm">{message}</div>
|
<div className="font-bold text-sm">{message}</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className='border-green-900/20 800 w-40 border' />
|
<hr className="border-green-900/20 800 w-40 border" />
|
||||||
<Link className='flex space-x-2 items-center' href={'/login'}><User size={14} /> <div>Login </div></Link>
|
<Link className="flex space-x-2 items-center" href={'/login'}>
|
||||||
|
<User size={14} /> <div>Login </div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<FormLayout onSubmit={formik.handleSubmit}>
|
<FormLayout onSubmit={formik.handleSubmit}>
|
||||||
<FormField name="email">
|
<FormField name="email">
|
||||||
<FormLabelAndMessage label='Email' message={formik.errors.email} />
|
<FormLabelAndMessage label="Email" message={formik.errors.email} />
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.email}
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
{/* for password */}
|
{/* for password */}
|
||||||
<FormField name="password">
|
<FormField name="password">
|
||||||
<FormLabelAndMessage label='Password' message={formik.errors.password} />
|
<FormLabelAndMessage
|
||||||
|
label="Password"
|
||||||
|
message={formik.errors.password}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.password}
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
{/* for username */}
|
{/* for username */}
|
||||||
<FormField name="username">
|
<FormField name="username">
|
||||||
<FormLabelAndMessage label='Username' message={formik.errors.username} />
|
<FormLabelAndMessage
|
||||||
|
label="Username"
|
||||||
|
message={formik.errors.username}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.username}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
{/* for bio */}
|
{/* for bio */}
|
||||||
<FormField name="bio">
|
<FormField name="bio">
|
||||||
<FormLabelAndMessage label='Bio' message={formik.errors.bio} />
|
<FormLabelAndMessage label="Bio" message={formik.errors.bio} />
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Textarea onChange={formik.handleChange} value={formik.values.bio} required />
|
<Textarea
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.bio}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<div className="flex py-4">
|
<div className="flex py-4">
|
||||||
<Form.Submit asChild>
|
<Form.Submit asChild>
|
||||||
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
|
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer">
|
||||||
{isSubmitting ? "Loading..."
|
{isSubmitting ? 'Loading...' : 'Create an account & Join'}
|
||||||
: "Create an account & Join"}
|
|
||||||
</button>
|
</button>
|
||||||
</Form.Submit>
|
</Form.Submit>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,59 +1,55 @@
|
||||||
"use client";
|
'use client'
|
||||||
import { useFormik } from 'formik';
|
import { useFormik } from 'formik'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form';
|
import FormLayout, {
|
||||||
import * as Form from '@radix-ui/react-form';
|
FormField,
|
||||||
import { AlertTriangle, Check, User } from 'lucide-react';
|
FormLabelAndMessage,
|
||||||
import Link from 'next/link';
|
Input,
|
||||||
import { signup } from '@services/auth/auth';
|
Textarea,
|
||||||
import { useOrg } from '@components/Contexts/OrgContext';
|
} from '@components/StyledElements/Form/Form'
|
||||||
|
import * as Form from '@radix-ui/react-form'
|
||||||
|
import { AlertTriangle, Check, User } from 'lucide-react'
|
||||||
|
import Link from 'next/link'
|
||||||
|
import { signup } from '@services/auth/auth'
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
|
||||||
const validate = (values: any) => {
|
const validate = (values: any) => {
|
||||||
const errors: any = {};
|
const errors: any = {}
|
||||||
|
|
||||||
if (!values.email) {
|
if (!values.email) {
|
||||||
errors.email = 'Required';
|
errors.email = 'Required'
|
||||||
}
|
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
|
||||||
else if (
|
errors.email = 'Invalid email address'
|
||||||
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
|
|
||||||
) {
|
|
||||||
errors.email = 'Invalid email address';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.password) {
|
if (!values.password) {
|
||||||
errors.password = 'Required';
|
errors.password = 'Required'
|
||||||
}
|
} else if (values.password.length < 8) {
|
||||||
else if (values.password.length < 8) {
|
errors.password = 'Password must be at least 8 characters'
|
||||||
errors.password = 'Password must be at least 8 characters';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.username) {
|
if (!values.username) {
|
||||||
errors.username = 'Required';
|
errors.username = 'Required'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.username || values.username.length < 4) {
|
if (!values.username || values.username.length < 4) {
|
||||||
errors.username = 'Username must be at least 4 characters';
|
errors.username = 'Username must be at least 4 characters'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!values.bio) {
|
if (!values.bio) {
|
||||||
errors.bio = 'Required';
|
errors.bio = 'Required'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return errors
|
||||||
return errors;
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
function OpenSignUpComponent() {
|
function OpenSignUpComponent() {
|
||||||
const [isSubmitting, setIsSubmitting] = React.useState(false);
|
const [isSubmitting, setIsSubmitting] = React.useState(false)
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const [error, setError] = React.useState('');
|
const [error, setError] = React.useState('')
|
||||||
const [message, setMessage] = React.useState('');
|
const [message, setMessage] = React.useState('')
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
org_slug: org?.slug,
|
org_slug: org?.slug,
|
||||||
|
|
@ -66,34 +62,32 @@ function OpenSignUpComponent() {
|
||||||
last_name: '',
|
last_name: '',
|
||||||
},
|
},
|
||||||
validate,
|
validate,
|
||||||
onSubmit: async values => {
|
onSubmit: async (values) => {
|
||||||
setError('')
|
setError('')
|
||||||
setMessage('')
|
setMessage('')
|
||||||
setIsSubmitting(true);
|
setIsSubmitting(true)
|
||||||
let res = await signup(values);
|
let res = await signup(values)
|
||||||
let message = await res.json();
|
let message = await res.json()
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
//router.push(`/login`);
|
//router.push(`/login`);
|
||||||
setMessage('Your account was successfully created')
|
setMessage('Your account was successfully created')
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false)
|
||||||
|
} else if (
|
||||||
|
res.status == 401 ||
|
||||||
|
res.status == 400 ||
|
||||||
|
res.status == 404 ||
|
||||||
|
res.status == 409
|
||||||
|
) {
|
||||||
|
setError(message.detail)
|
||||||
|
setIsSubmitting(false)
|
||||||
|
} else {
|
||||||
|
setError('Something went wrong')
|
||||||
|
setIsSubmitting(false)
|
||||||
}
|
}
|
||||||
else if (res.status == 401 || res.status == 400 || res.status == 404 || res.status == 409) {
|
|
||||||
setError(message.detail);
|
|
||||||
setIsSubmitting(false);
|
|
||||||
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setError("Something went wrong");
|
|
||||||
setIsSubmitting(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {}, [org])
|
||||||
|
|
||||||
}
|
|
||||||
, [org]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="login-form m-auto w-72">
|
<div className="login-form m-auto w-72">
|
||||||
|
|
@ -105,56 +99,81 @@ function OpenSignUpComponent() {
|
||||||
)}
|
)}
|
||||||
{message && (
|
{message && (
|
||||||
<div className="flex flex-col space-y-4 justify-center bg-green-200 rounded-md text-green-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
<div className="flex flex-col space-y-4 justify-center bg-green-200 rounded-md text-green-950 space-x-2 items-center p-4 transition-all shadow-sm">
|
||||||
<div className='flex space-x-2'>
|
<div className="flex space-x-2">
|
||||||
<Check size={18} />
|
<Check size={18} />
|
||||||
<div className="font-bold text-sm">{message}</div>
|
<div className="font-bold text-sm">{message}</div>
|
||||||
</div>
|
</div>
|
||||||
<hr className='border-green-900/20 800 w-40 border' />
|
<hr className="border-green-900/20 800 w-40 border" />
|
||||||
<Link className='flex space-x-2 items-center' href={'/login'}><User size={14} /> <div>Login </div></Link>
|
<Link className="flex space-x-2 items-center" href={'/login'}>
|
||||||
|
<User size={14} /> <div>Login </div>
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<FormLayout onSubmit={formik.handleSubmit}>
|
<FormLayout onSubmit={formik.handleSubmit}>
|
||||||
<FormField name="email">
|
<FormField name="email">
|
||||||
<FormLabelAndMessage label='Email' message={formik.errors.email} />
|
<FormLabelAndMessage label="Email" message={formik.errors.email} />
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.email}
|
||||||
|
type="email"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
{/* for password */}
|
{/* for password */}
|
||||||
<FormField name="password">
|
<FormField name="password">
|
||||||
<FormLabelAndMessage label='Password' message={formik.errors.password} />
|
<FormLabelAndMessage
|
||||||
|
label="Password"
|
||||||
|
message={formik.errors.password}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.password}
|
||||||
|
type="password"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
{/* for username */}
|
{/* for username */}
|
||||||
<FormField name="username">
|
<FormField name="username">
|
||||||
<FormLabelAndMessage label='Username' message={formik.errors.username} />
|
<FormLabelAndMessage
|
||||||
|
label="Username"
|
||||||
|
message={formik.errors.username}
|
||||||
|
/>
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
|
<Input
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.username}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
{/* for bio */}
|
{/* for bio */}
|
||||||
<FormField name="bio">
|
<FormField name="bio">
|
||||||
<FormLabelAndMessage label='Bio' message={formik.errors.bio} />
|
<FormLabelAndMessage label="Bio" message={formik.errors.bio} />
|
||||||
|
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Textarea onChange={formik.handleChange} value={formik.values.bio} required />
|
<Textarea
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.bio}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<div className="flex py-4">
|
<div className="flex py-4">
|
||||||
<Form.Submit asChild>
|
<Form.Submit asChild>
|
||||||
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
|
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer">
|
||||||
{isSubmitting ? "Loading..."
|
{isSubmitting ? 'Loading...' : 'Create an account'}
|
||||||
: "Create an account"}
|
|
||||||
</button>
|
</button>
|
||||||
</Form.Submit>
|
</Form.Submit>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,42 @@
|
||||||
|
import { Metadata } from 'next'
|
||||||
import { Metadata } from "next";
|
import { getOrganizationContextInfo } from '@services/organizations/orgs'
|
||||||
import { getOrganizationContextInfo } from "@services/organizations/orgs";
|
import SignUpClient from './signup'
|
||||||
import SignUpClient from "./signup";
|
import { Suspense } from 'react'
|
||||||
import { Suspense } from "react";
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
|
||||||
|
|
||||||
type MetadataProps = {
|
type MetadataProps = {
|
||||||
params: { orgslug: string, courseid: string };
|
params: { orgslug: string; courseid: string }
|
||||||
searchParams: { [key: string]: string | string[] | undefined };
|
searchParams: { [key: string]: string | string[] | undefined }
|
||||||
};
|
}
|
||||||
|
|
||||||
export async function generateMetadata(
|
export async function generateMetadata({
|
||||||
{ params }: MetadataProps,
|
params,
|
||||||
): Promise<Metadata> {
|
}: MetadataProps): Promise<Metadata> {
|
||||||
const orgslug = params.orgslug;
|
const orgslug = params.orgslug
|
||||||
// Get Org context information
|
// Get Org context information
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, {
|
||||||
|
revalidate: 0,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
title: 'Sign up' + ` — ${org.name}`,
|
title: 'Sign up' + ` — ${org.name}`,
|
||||||
};
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const SignUp = async (params: any) => {
|
const SignUp = async (params: any) => {
|
||||||
const orgslug = params.params.orgslug;
|
const orgslug = params.params.orgslug
|
||||||
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
|
const org = await getOrganizationContextInfo(orgslug, {
|
||||||
|
revalidate: 0,
|
||||||
|
tags: ['organizations'],
|
||||||
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Suspense fallback={<PageLoading/>}>
|
<Suspense fallback={<PageLoading />}>
|
||||||
<SignUpClient org={org} />
|
<SignUpClient org={org} />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</>
|
</>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
export default SignUp;
|
export default SignUp
|
||||||
|
|
|
||||||
|
|
@ -1,65 +1,89 @@
|
||||||
"use client";
|
'use client'
|
||||||
import learnhouseIcon from "public/learnhouse_bigicon_1.png";
|
import learnhouseIcon from 'public/learnhouse_bigicon_1.png'
|
||||||
import Image from 'next/image';
|
import Image from 'next/image'
|
||||||
import { getOrgLogoMediaDirectory } from '@services/media/media';
|
import { getOrgLogoMediaDirectory } from '@services/media/media'
|
||||||
import Link from 'next/link';
|
import Link from 'next/link'
|
||||||
import { getUriWithOrg } from '@services/config/config';
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import { useSession } from "@components/Contexts/SessionContext";
|
import { useSession } from '@components/Contexts/SessionContext'
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from 'react'
|
||||||
import { MailWarning, Shield, UserPlus } from "lucide-react";
|
import { MailWarning, Shield, UserPlus } from 'lucide-react'
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import UserAvatar from "@components/Objects/UserAvatar";
|
import UserAvatar from '@components/Objects/UserAvatar'
|
||||||
import OpenSignUpComponent from "./OpenSignup";
|
import OpenSignUpComponent from './OpenSignup'
|
||||||
import InviteOnlySignUpComponent from "./InviteOnlySignUp";
|
import InviteOnlySignUpComponent from './InviteOnlySignUp'
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from 'next/navigation'
|
||||||
import { validateInviteCode } from "@services/organizations/invites";
|
import { validateInviteCode } from '@services/organizations/invites'
|
||||||
import PageLoading from "@components/Objects/Loaders/PageLoading";
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import Toast from "@components/StyledElements/Toast/Toast";
|
import Toast from '@components/StyledElements/Toast/Toast'
|
||||||
import toast from "react-hot-toast";
|
import toast from 'react-hot-toast'
|
||||||
|
|
||||||
interface SignUpClientProps {
|
interface SignUpClientProps {
|
||||||
org: any;
|
org: any
|
||||||
}
|
}
|
||||||
|
|
||||||
function SignUpClient(props: SignUpClientProps) {
|
function SignUpClient(props: SignUpClientProps) {
|
||||||
const session = useSession() as any;
|
const session = useSession() as any
|
||||||
const [joinMethod, setJoinMethod] = React.useState('open');
|
const [joinMethod, setJoinMethod] = React.useState('open')
|
||||||
const [inviteCode, setInviteCode] = React.useState('');
|
const [inviteCode, setInviteCode] = React.useState('')
|
||||||
const searchParams = useSearchParams()
|
const searchParams = useSearchParams()
|
||||||
const inviteCodeParam = searchParams.get('inviteCode')
|
const inviteCodeParam = searchParams.get('inviteCode')
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (props.org.config) {
|
if (props.org.config) {
|
||||||
setJoinMethod(props.org?.config?.config?.GeneralConfig.users.signup_mechanism);
|
setJoinMethod(
|
||||||
console.log(props.org?.config?.config?.GeneralConfig.users.signup_mechanism)
|
props.org?.config?.config?.GeneralConfig.users.signup_mechanism
|
||||||
|
)
|
||||||
|
console.log(
|
||||||
|
props.org?.config?.config?.GeneralConfig.users.signup_mechanism
|
||||||
|
)
|
||||||
}
|
}
|
||||||
if (inviteCodeParam) {
|
if (inviteCodeParam) {
|
||||||
setInviteCode(inviteCodeParam);
|
setInviteCode(inviteCodeParam)
|
||||||
}
|
}
|
||||||
}
|
}, [props.org, inviteCodeParam])
|
||||||
, [props.org, inviteCodeParam]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='grid grid-flow-col justify-stretch h-screen'>
|
<div className="grid grid-flow-col justify-stretch h-screen">
|
||||||
<div className="right-login-part" style={{ background: "linear-gradient(041.61deg, #202020 7.15%, #000000 90.96%)" }} >
|
<div
|
||||||
<div className='login-topbar m-10'>
|
className="right-login-part"
|
||||||
<Link prefetch href={getUriWithOrg(props.org.slug, "/")}>
|
style={{
|
||||||
<Image quality={100} width={30} height={30} src={learnhouseIcon} alt="" />
|
background:
|
||||||
|
'linear-gradient(041.61deg, #202020 7.15%, #000000 90.96%)',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="login-topbar m-10">
|
||||||
|
<Link prefetch href={getUriWithOrg(props.org.slug, '/')}>
|
||||||
|
<Image
|
||||||
|
quality={100}
|
||||||
|
width={30}
|
||||||
|
height={30}
|
||||||
|
src={learnhouseIcon}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-10 h-3/4 flex flex-row text-white">
|
<div className="ml-10 h-3/4 flex flex-row text-white">
|
||||||
<div className="m-auto flex space-x-4 items-center flex-wrap">
|
<div className="m-auto flex space-x-4 items-center flex-wrap">
|
||||||
<div>You've been invited to join </div>
|
<div>You've been invited to join </div>
|
||||||
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" >
|
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]">
|
||||||
{props.org?.logo_image ? (
|
{props.org?.logo_image ? (
|
||||||
<img
|
<img
|
||||||
src={`${getOrgLogoMediaDirectory(props.org.org_uuid, props.org?.logo_image)}`}
|
src={`${getOrgLogoMediaDirectory(
|
||||||
|
props.org.org_uuid,
|
||||||
|
props.org?.logo_image
|
||||||
|
)}`}
|
||||||
alt="Learnhouse"
|
alt="Learnhouse"
|
||||||
style={{ width: "auto", height: 70 }}
|
style={{ width: 'auto', height: 70 }}
|
||||||
className="rounded-xl shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white"
|
className="rounded-xl shadow-xl inset-0 ring-1 ring-inset ring-black/10 bg-white"
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<Image quality={100} width={70} height={70} src={learnhouseIcon} alt="" />
|
<Image
|
||||||
|
quality={100}
|
||||||
|
width={70}
|
||||||
|
height={70}
|
||||||
|
src={learnhouseIcon}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="font-bold text-xl">{props.org?.name}</div>
|
<div className="font-bold text-xl">{props.org?.name}</div>
|
||||||
|
|
@ -67,44 +91,45 @@ function SignUpClient(props: SignUpClientProps) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="left-join-part bg-white flex flex-row">
|
<div className="left-join-part bg-white flex flex-row">
|
||||||
{joinMethod == 'open' && (
|
{joinMethod == 'open' &&
|
||||||
session.isAuthenticated ? <LoggedInJoinScreen inviteCode={inviteCode} /> : <OpenSignUpComponent />
|
(session.isAuthenticated ? (
|
||||||
)}
|
<LoggedInJoinScreen inviteCode={inviteCode} />
|
||||||
{joinMethod == 'inviteOnly' && (
|
) : (
|
||||||
inviteCode ? (
|
<OpenSignUpComponent />
|
||||||
session.isAuthenticated ? <LoggedInJoinScreen /> : <InviteOnlySignUpComponent inviteCode={inviteCode} />
|
))}
|
||||||
) : <NoTokenScreen />
|
{joinMethod == 'inviteOnly' &&
|
||||||
)}
|
(inviteCode ? (
|
||||||
|
session.isAuthenticated ? (
|
||||||
|
<LoggedInJoinScreen />
|
||||||
|
) : (
|
||||||
|
<InviteOnlySignUpComponent inviteCode={inviteCode} />
|
||||||
|
)
|
||||||
|
) : (
|
||||||
|
<NoTokenScreen />
|
||||||
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const LoggedInJoinScreen = (props: any) => {
|
const LoggedInJoinScreen = (props: any) => {
|
||||||
const session = useSession() as any;
|
const session = useSession() as any
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
const [isLoading, setIsLoading] = React.useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session && org) {
|
if (session && org) {
|
||||||
setIsLoading(false);
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
}, [org, session])
|
||||||
}
|
|
||||||
, [org, session]);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center mx-auto">
|
<div className="flex flex-row items-center mx-auto">
|
||||||
<div className="flex space-y-7 flex-col justify-center items-center">
|
<div className="flex space-y-7 flex-col justify-center items-center">
|
||||||
<p className='pt-3 text-2xl font-semibold text-black/70 flex justify-center space-x-2 items-center'>
|
<p className="pt-3 text-2xl font-semibold text-black/70 flex justify-center space-x-2 items-center">
|
||||||
<span className='items-center'>Hi</span>
|
<span className="items-center">Hi</span>
|
||||||
<span className='capitalize flex space-x-2 items-center'>
|
<span className="capitalize flex space-x-2 items-center">
|
||||||
<UserAvatar rounded='rounded-xl' border='border-4' width={35} />
|
<UserAvatar rounded="rounded-xl" border="border-4" width={35} />
|
||||||
<span>{session.user.username},</span>
|
<span>{session.user.username},</span>
|
||||||
</span>
|
</span>
|
||||||
<span>join {org?.name} ?</span>
|
<span>join {org?.name} ?</span>
|
||||||
|
|
@ -116,62 +141,71 @@ const LoggedInJoinScreen = (props: any) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const NoTokenScreen = (props: any) => {
|
const NoTokenScreen = (props: any) => {
|
||||||
const session = useSession() as any;
|
const session = useSession() as any
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
const [isLoading, setIsLoading] = React.useState(true)
|
||||||
const [inviteCode, setInviteCode] = React.useState('');
|
const [inviteCode, setInviteCode] = React.useState('')
|
||||||
const [messsage, setMessage] = React.useState('bruh');
|
const [messsage, setMessage] = React.useState('bruh')
|
||||||
|
|
||||||
const handleInviteCodeChange = (e: any) => {
|
const handleInviteCodeChange = (e: any) => {
|
||||||
setInviteCode(e.target.value);
|
setInviteCode(e.target.value)
|
||||||
}
|
}
|
||||||
|
|
||||||
const validateCode = async () => {
|
const validateCode = async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true)
|
||||||
let res = await validateInviteCode(org?.id, inviteCode);
|
let res = await validateInviteCode(org?.id, inviteCode)
|
||||||
//wait for 1s
|
//wait for 1s
|
||||||
if (res.success) {
|
if (res.success) {
|
||||||
toast.success("Invite code is valid, you'll be redirected to the signup page in a few seconds");
|
toast.success(
|
||||||
|
"Invite code is valid, you'll be redirected to the signup page in a few seconds"
|
||||||
|
)
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
router.push(`/signup?inviteCode=${inviteCode}`);
|
router.push(`/signup?inviteCode=${inviteCode}`)
|
||||||
}, 2000);
|
}, 2000)
|
||||||
|
} else {
|
||||||
|
toast.error('Invite code is invalid')
|
||||||
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
toast.error("Invite code is invalid");
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (session && org) {
|
if (session && org) {
|
||||||
setIsLoading(false);
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
|
}, [org, session])
|
||||||
}
|
|
||||||
, [org, session]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-row items-center mx-auto">
|
<div className="flex flex-row items-center mx-auto">
|
||||||
<Toast />
|
<Toast />
|
||||||
{isLoading ? <div className="flex space-y-7 flex-col w-[300px] justify-center items-center"><PageLoading /></div> : <div className="flex space-y-7 flex-col justify-center items-center">
|
{isLoading ? (
|
||||||
|
<div className="flex space-y-7 flex-col w-[300px] justify-center items-center">
|
||||||
|
<PageLoading />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="flex space-y-7 flex-col justify-center items-center">
|
||||||
<p className="flex space-x-2 text-lg font-medium text-red-800 items-center">
|
<p className="flex space-x-2 text-lg font-medium text-red-800 items-center">
|
||||||
<MailWarning size={18} />
|
<MailWarning size={18} />
|
||||||
<span>An invite code is required to join {org?.name}</span>
|
<span>An invite code is required to join {org?.name}</span>
|
||||||
</p>
|
</p>
|
||||||
<input onChange={handleInviteCodeChange} className="bg-white outline-2 outline outline-gray-200 rounded-lg px-5 w-[300px] h-[50px]" placeholder="Please enter an invite code" type="text" />
|
<input
|
||||||
<button onClick={validateCode} className="flex w-fit space-x-2 bg-black px-6 py-2 text-md rounded-lg font-semibold h-fit text-white items-center shadow-md">
|
onChange={handleInviteCodeChange}
|
||||||
|
className="bg-white outline-2 outline outline-gray-200 rounded-lg px-5 w-[300px] h-[50px]"
|
||||||
|
placeholder="Please enter an invite code"
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={validateCode}
|
||||||
|
className="flex w-fit space-x-2 bg-black px-6 py-2 text-md rounded-lg font-semibold h-fit text-white items-center shadow-md"
|
||||||
|
>
|
||||||
<Shield size={18} />
|
<Shield size={18} />
|
||||||
<p>Submit </p>
|
<p>Submit </p>
|
||||||
</button>
|
</button>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,9 @@
|
||||||
"use client";
|
'use client'
|
||||||
import { motion } from "framer-motion";
|
import { motion } from 'framer-motion'
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components'
|
||||||
import learnhouseBigIcon from "public/learnhouse_bigicon.png";
|
import learnhouseBigIcon from 'public/learnhouse_bigicon.png'
|
||||||
import Image from "next/legacy/image";
|
import Image from 'next/legacy/image'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
|
|
||||||
export default function Home() {
|
export default function Home() {
|
||||||
return (
|
return (
|
||||||
|
|
@ -12,14 +12,20 @@ export default function Home() {
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{
|
transition={{
|
||||||
type: "spring",
|
type: 'spring',
|
||||||
stiffness: 260,
|
stiffness: 260,
|
||||||
damping: 70,
|
damping: 70,
|
||||||
delay: 0.2,
|
delay: 0.2,
|
||||||
}}
|
}}
|
||||||
exit={{ opacity: 1 }}
|
exit={{ opacity: 1 }}
|
||||||
>
|
>
|
||||||
<Image alt="Learnhouse Icon" height={260} width={260} quality={100} src={learnhouseBigIcon}></Image>
|
<Image
|
||||||
|
alt="Learnhouse Icon"
|
||||||
|
height={260}
|
||||||
|
width={260}
|
||||||
|
quality={100}
|
||||||
|
src={learnhouseBigIcon}
|
||||||
|
></Image>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
|
|
@ -29,7 +35,7 @@ export default function Home() {
|
||||||
initial={{ opacity: 0, y: 20 }}
|
initial={{ opacity: 0, y: 20 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{
|
transition={{
|
||||||
type: "spring",
|
type: 'spring',
|
||||||
stiffness: 260,
|
stiffness: 260,
|
||||||
damping: 70,
|
damping: 70,
|
||||||
delay: 0.8,
|
delay: 0.8,
|
||||||
|
|
@ -37,19 +43,19 @@ export default function Home() {
|
||||||
exit={{ opacity: 1 }}
|
exit={{ opacity: 1 }}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<Link href={"/organizations"}>
|
<Link href={'/organizations'}>
|
||||||
<OrgsButton>See Organizations</OrgsButton>
|
<OrgsButton>See Organizations</OrgsButton>
|
||||||
</Link>
|
</Link>
|
||||||
<br />
|
<br />
|
||||||
<br />
|
<br />
|
||||||
<Link href={"/login"}>
|
<Link href={'/login'}>
|
||||||
<OrgsButton>Login</OrgsButton>
|
<OrgsButton>Login</OrgsButton>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</HomePage>
|
</HomePage>
|
||||||
);
|
)
|
||||||
};
|
}
|
||||||
|
|
||||||
const OrgsButton = styled.button`
|
const OrgsButton = styled.button`
|
||||||
background: #151515;
|
background: #151515;
|
||||||
|
|
@ -63,7 +69,7 @@ const OrgsButton = styled.button`
|
||||||
margin: 0 10px;
|
margin: 0 10px;
|
||||||
margin: auto;
|
margin: auto;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: "DM Sans";
|
font-family: 'DM Sans';
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
-webkit-transition: all 0.2s ease-in-out;
|
-webkit-transition: all 0.2s ease-in-out;
|
||||||
|
|
@ -71,7 +77,7 @@ const OrgsButton = styled.button`
|
||||||
&:hover {
|
&:hover {
|
||||||
background: #191919;
|
background: #191919;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
const HomePage = styled.div`
|
const HomePage = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -86,5 +92,4 @@ const HomePage = styled.div`
|
||||||
img {
|
img {
|
||||||
width: 60px;
|
width: 60px;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,44 +2,43 @@ import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
||||||
interface UseGetAIFeatures {
|
interface UseGetAIFeatures {
|
||||||
feature: 'editor' | 'activity_ask' | 'course_ask' | 'global_ai_ask',
|
feature: 'editor' | 'activity_ask' | 'course_ask' | 'global_ai_ask'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function useGetAIFeatures(props: UseGetAIFeatures) {
|
function useGetAIFeatures(props: UseGetAIFeatures) {
|
||||||
const org = useOrg() as any
|
const org = useOrg() as any
|
||||||
const [isEnabled, setisEnabled] = React.useState(false)
|
const [isEnabled, setisEnabled] = React.useState(false)
|
||||||
|
|
||||||
function checkAvailableAIFeaturesOnOrg(feature: string) {
|
function checkAvailableAIFeaturesOnOrg(feature: string) {
|
||||||
const config = org?.config?.config?.AIConfig;
|
const config = org?.config?.config?.AIConfig
|
||||||
|
|
||||||
if (!config) {
|
if (!config) {
|
||||||
console.log("AI or Organization config is not defined.");
|
console.log('AI or Organization config is not defined.')
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.enabled) {
|
if (!config.enabled) {
|
||||||
console.log("AI is not enabled for this Organization.");
|
console.log('AI is not enabled for this Organization.')
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!config.features[feature]) {
|
if (!config.features[feature]) {
|
||||||
console.log(`Feature ${feature} is not enabled for this Organization.`);
|
console.log(`Feature ${feature} is not enabled for this Organization.`)
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (org) { // Check if org is not null or undefined
|
if (org) {
|
||||||
|
// Check if org is not null or undefined
|
||||||
let isEnabledStatus = checkAvailableAIFeaturesOnOrg(props.feature)
|
let isEnabledStatus = checkAvailableAIFeaturesOnOrg(props.feature)
|
||||||
setisEnabled(isEnabledStatus)
|
setisEnabled(isEnabledStatus)
|
||||||
}
|
}
|
||||||
}, [org])
|
}, [org])
|
||||||
|
|
||||||
return isEnabled
|
return isEnabled
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default useGetAIFeatures
|
export default useGetAIFeatures
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
'use client';
|
'use client'
|
||||||
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk';
|
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk'
|
||||||
import React, { createContext, useContext, useReducer } from 'react'
|
import React, { createContext, useContext, useReducer } from 'react'
|
||||||
export const AIChatBotContext = createContext(null) as any;
|
export const AIChatBotContext = createContext(null) as any
|
||||||
export const AIChatBotDispatchContext = createContext(null) as any;
|
export const AIChatBotDispatchContext = createContext(null) as any
|
||||||
|
|
||||||
export type AIChatBotStateTypes = {
|
export type AIChatBotStateTypes = {
|
||||||
messages: AIMessage[],
|
messages: AIMessage[]
|
||||||
isModalOpen: boolean,
|
isModalOpen: boolean
|
||||||
aichat_uuid: string,
|
aichat_uuid: string
|
||||||
isWaitingForResponse: boolean,
|
isWaitingForResponse: boolean
|
||||||
chatInputValue: string
|
chatInputValue: string
|
||||||
error: AIError
|
error: AIError
|
||||||
}
|
}
|
||||||
|
|
@ -20,16 +20,14 @@ type AIError = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function AIChatBotProvider({ children }: { children: React.ReactNode }) {
|
function AIChatBotProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [aiChatBotState, dispatchAIChatBot] = useReducer(aiChatBotReducer,
|
const [aiChatBotState, dispatchAIChatBot] = useReducer(aiChatBotReducer, {
|
||||||
{
|
|
||||||
messages: [] as AIMessage[],
|
messages: [] as AIMessage[],
|
||||||
isModalOpen: false,
|
isModalOpen: false,
|
||||||
aichat_uuid: null,
|
aichat_uuid: null,
|
||||||
isWaitingForResponse: false,
|
isWaitingForResponse: false,
|
||||||
chatInputValue: '',
|
chatInputValue: '',
|
||||||
error: { isError: false, status: 0, error_message: ' ' } as AIError
|
error: { isError: false, status: 0, error_message: ' ' } as AIError,
|
||||||
}
|
})
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<AIChatBotContext.Provider value={aiChatBotState}>
|
<AIChatBotContext.Provider value={aiChatBotState}>
|
||||||
<AIChatBotDispatchContext.Provider value={dispatchAIChatBot}>
|
<AIChatBotDispatchContext.Provider value={dispatchAIChatBot}>
|
||||||
|
|
@ -42,33 +40,33 @@ function AIChatBotProvider({ children }: { children: React.ReactNode }) {
|
||||||
export default AIChatBotProvider
|
export default AIChatBotProvider
|
||||||
|
|
||||||
export function useAIChatBot() {
|
export function useAIChatBot() {
|
||||||
return useContext(AIChatBotContext);
|
return useContext(AIChatBotContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAIChatBotDispatch() {
|
export function useAIChatBotDispatch() {
|
||||||
return useContext(AIChatBotDispatchContext);
|
return useContext(AIChatBotDispatchContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
function aiChatBotReducer(state: any, action: any) {
|
function aiChatBotReducer(state: any, action: any) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'setMessages':
|
case 'setMessages':
|
||||||
return { ...state, messages: action.payload };
|
return { ...state, messages: action.payload }
|
||||||
case 'addMessage':
|
case 'addMessage':
|
||||||
return { ...state, messages: [...state.messages, action.payload] };
|
return { ...state, messages: [...state.messages, action.payload] }
|
||||||
case 'setIsModalOpen':
|
case 'setIsModalOpen':
|
||||||
return { ...state, isModalOpen: true };
|
return { ...state, isModalOpen: true }
|
||||||
case 'setIsModalClose':
|
case 'setIsModalClose':
|
||||||
return { ...state, isModalOpen: false };
|
return { ...state, isModalOpen: false }
|
||||||
case 'setAichat_uuid':
|
case 'setAichat_uuid':
|
||||||
return { ...state, aichat_uuid: action.payload };
|
return { ...state, aichat_uuid: action.payload }
|
||||||
case 'setIsWaitingForResponse':
|
case 'setIsWaitingForResponse':
|
||||||
return { ...state, isWaitingForResponse: true };
|
return { ...state, isWaitingForResponse: true }
|
||||||
case 'setIsNoLongerWaitingForResponse':
|
case 'setIsNoLongerWaitingForResponse':
|
||||||
return { ...state, isWaitingForResponse: false };
|
return { ...state, isWaitingForResponse: false }
|
||||||
case 'setChatInputValue':
|
case 'setChatInputValue':
|
||||||
return { ...state, chatInputValue: action.payload };
|
return { ...state, chatInputValue: action.payload }
|
||||||
case 'setError':
|
case 'setError':
|
||||||
return { ...state, error: action.payload };
|
return { ...state, error: action.payload }
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled action type: ${action.type}`)
|
throw new Error(`Unhandled action type: ${action.type}`)
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,22 @@
|
||||||
'use client';
|
'use client'
|
||||||
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk';
|
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk'
|
||||||
import React, { createContext, useContext, useReducer } from 'react'
|
import React, { createContext, useContext, useReducer } from 'react'
|
||||||
export const AIEditorContext = createContext(null) as any;
|
export const AIEditorContext = createContext(null) as any
|
||||||
export const AIEditorDispatchContext = createContext(null) as any;
|
export const AIEditorDispatchContext = createContext(null) as any
|
||||||
|
|
||||||
export type AIEditorStateTypes = {
|
export type AIEditorStateTypes = {
|
||||||
|
messages: AIMessage[]
|
||||||
messages: AIMessage[],
|
isModalOpen: boolean
|
||||||
isModalOpen: boolean,
|
isFeedbackModalOpen: boolean
|
||||||
isFeedbackModalOpen: boolean,
|
aichat_uuid: string
|
||||||
aichat_uuid: string,
|
isWaitingForResponse: boolean
|
||||||
isWaitingForResponse: boolean,
|
chatInputValue: string
|
||||||
chatInputValue: string,
|
selectedTool:
|
||||||
selectedTool: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate'
|
| 'Writer'
|
||||||
|
| 'ContinueWriting'
|
||||||
|
| 'MakeLonger'
|
||||||
|
| 'GenerateQuiz'
|
||||||
|
| 'Translate'
|
||||||
isUserInputEnabled: boolean
|
isUserInputEnabled: boolean
|
||||||
error: AIError
|
error: AIError
|
||||||
}
|
}
|
||||||
|
|
@ -24,8 +28,7 @@ type AIError = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function AIEditorProvider({ children }: { children: React.ReactNode }) {
|
function AIEditorProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [aIEditorState, dispatchAIEditor] = useReducer(aIEditorReducer,
|
const [aIEditorState, dispatchAIEditor] = useReducer(aIEditorReducer, {
|
||||||
{
|
|
||||||
messages: [] as AIMessage[],
|
messages: [] as AIMessage[],
|
||||||
isModalOpen: false,
|
isModalOpen: false,
|
||||||
isFeedbackModalOpen: false,
|
isFeedbackModalOpen: false,
|
||||||
|
|
@ -34,9 +37,8 @@ function AIEditorProvider({ children }: { children: React.ReactNode }) {
|
||||||
chatInputValue: '',
|
chatInputValue: '',
|
||||||
selectedTool: 'Writer',
|
selectedTool: 'Writer',
|
||||||
isUserInputEnabled: true,
|
isUserInputEnabled: true,
|
||||||
error: { isError: false, status: 0, error_message: ' ' } as AIError
|
error: { isError: false, status: 0, error_message: ' ' } as AIError,
|
||||||
}
|
})
|
||||||
);
|
|
||||||
return (
|
return (
|
||||||
<AIEditorContext.Provider value={aIEditorState}>
|
<AIEditorContext.Provider value={aIEditorState}>
|
||||||
<AIEditorDispatchContext.Provider value={dispatchAIEditor}>
|
<AIEditorDispatchContext.Provider value={dispatchAIEditor}>
|
||||||
|
|
@ -49,42 +51,41 @@ function AIEditorProvider({ children }: { children: React.ReactNode }) {
|
||||||
export default AIEditorProvider
|
export default AIEditorProvider
|
||||||
|
|
||||||
export function useAIEditor() {
|
export function useAIEditor() {
|
||||||
return useContext(AIEditorContext);
|
return useContext(AIEditorContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useAIEditorDispatch() {
|
export function useAIEditorDispatch() {
|
||||||
return useContext(AIEditorDispatchContext);
|
return useContext(AIEditorDispatchContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
function aIEditorReducer(state: any, action: any) {
|
function aIEditorReducer(state: any, action: any) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'setMessages':
|
case 'setMessages':
|
||||||
return { ...state, messages: action.payload };
|
return { ...state, messages: action.payload }
|
||||||
case 'addMessage':
|
case 'addMessage':
|
||||||
return { ...state, messages: [...state.messages, action.payload] };
|
return { ...state, messages: [...state.messages, action.payload] }
|
||||||
case 'setIsModalOpen':
|
case 'setIsModalOpen':
|
||||||
return { ...state, isModalOpen: true };
|
return { ...state, isModalOpen: true }
|
||||||
case 'setIsModalClose':
|
case 'setIsModalClose':
|
||||||
return { ...state, isModalOpen: false };
|
return { ...state, isModalOpen: false }
|
||||||
case 'setAichat_uuid':
|
case 'setAichat_uuid':
|
||||||
return { ...state, aichat_uuid: action.payload };
|
return { ...state, aichat_uuid: action.payload }
|
||||||
case 'setIsWaitingForResponse':
|
case 'setIsWaitingForResponse':
|
||||||
return { ...state, isWaitingForResponse: true };
|
return { ...state, isWaitingForResponse: true }
|
||||||
case 'setIsNoLongerWaitingForResponse':
|
case 'setIsNoLongerWaitingForResponse':
|
||||||
return { ...state, isWaitingForResponse: false };
|
return { ...state, isWaitingForResponse: false }
|
||||||
case 'setChatInputValue':
|
case 'setChatInputValue':
|
||||||
return { ...state, chatInputValue: action.payload };
|
return { ...state, chatInputValue: action.payload }
|
||||||
case 'setSelectedTool':
|
case 'setSelectedTool':
|
||||||
return { ...state, selectedTool: action.payload };
|
return { ...state, selectedTool: action.payload }
|
||||||
case 'setIsFeedbackModalOpen':
|
case 'setIsFeedbackModalOpen':
|
||||||
return { ...state, isFeedbackModalOpen: true };
|
return { ...state, isFeedbackModalOpen: true }
|
||||||
case 'setIsFeedbackModalClose':
|
case 'setIsFeedbackModalClose':
|
||||||
return { ...state, isFeedbackModalOpen: false };
|
return { ...state, isFeedbackModalOpen: false }
|
||||||
case 'setIsUserInputEnabled':
|
case 'setIsUserInputEnabled':
|
||||||
return { ...state, isUserInputEnabled: action.payload };
|
return { ...state, isUserInputEnabled: action.payload }
|
||||||
case 'setError':
|
case 'setError':
|
||||||
return { ...state, error: action.payload };
|
return { ...state, error: action.payload }
|
||||||
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled action type: ${action.type}`)
|
throw new Error(`Unhandled action type: ${action.type}`)
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,42 @@
|
||||||
'use client';
|
'use client'
|
||||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import React, { createContext, useContext, useEffect, useReducer } from 'react'
|
import React, { createContext, useContext, useEffect, useReducer } from 'react'
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr'
|
||||||
|
|
||||||
export const CourseContext = createContext(null) as any;
|
export const CourseContext = createContext(null) as any
|
||||||
export const CourseDispatchContext = createContext(null) as any;
|
export const CourseDispatchContext = createContext(null) as any
|
||||||
|
|
||||||
export function CourseProvider({ children, courseuuid }: { children: React.ReactNode, courseuuid: string }) {
|
export function CourseProvider({
|
||||||
const { data: courseStructureData } = useSWR(`${getAPIUrl()}courses/${courseuuid}/meta`, swrFetcher);
|
children,
|
||||||
const [courseStructure, dispatchCourseStructure] = useReducer(courseReducer,
|
courseuuid,
|
||||||
{
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
courseuuid: string
|
||||||
|
}) {
|
||||||
|
const { data: courseStructureData } = useSWR(
|
||||||
|
`${getAPIUrl()}courses/${courseuuid}/meta`,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
|
const [courseStructure, dispatchCourseStructure] = useReducer(courseReducer, {
|
||||||
courseStructure: courseStructureData ? courseStructureData : {},
|
courseStructure: courseStructureData ? courseStructureData : {},
|
||||||
courseOrder: {},
|
courseOrder: {},
|
||||||
isSaved: true
|
isSaved: true,
|
||||||
}
|
})
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// When courseStructureData is loaded, update the state
|
// When courseStructureData is loaded, update the state
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (courseStructureData) {
|
if (courseStructureData) {
|
||||||
dispatchCourseStructure({ type: 'setCourseStructure', payload: courseStructureData });
|
dispatchCourseStructure({
|
||||||
|
type: 'setCourseStructure',
|
||||||
|
payload: courseStructureData,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}, [courseStructureData]);
|
}, [courseStructureData])
|
||||||
|
|
||||||
|
|
||||||
if (!courseStructureData) return <PageLoading></PageLoading>
|
if (!courseStructureData) return <PageLoading></PageLoading>
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<CourseContext.Provider value={courseStructure}>
|
<CourseContext.Provider value={courseStructure}>
|
||||||
<CourseDispatchContext.Provider value={dispatchCourseStructure}>
|
<CourseDispatchContext.Provider value={dispatchCourseStructure}>
|
||||||
|
|
@ -40,24 +47,24 @@ export function CourseProvider({ children, courseuuid }: { children: React.React
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCourse() {
|
export function useCourse() {
|
||||||
return useContext(CourseContext);
|
return useContext(CourseContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCourseDispatch() {
|
export function useCourseDispatch() {
|
||||||
return useContext(CourseDispatchContext);
|
return useContext(CourseDispatchContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
function courseReducer(state: any, action: any) {
|
function courseReducer(state: any, action: any) {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case 'setCourseStructure':
|
case 'setCourseStructure':
|
||||||
return { ...state, courseStructure: action.payload };
|
return { ...state, courseStructure: action.payload }
|
||||||
case 'setCourseOrder':
|
case 'setCourseOrder':
|
||||||
return { ...state, courseOrder: action.payload };
|
return { ...state, courseOrder: action.payload }
|
||||||
case 'setIsSaved':
|
case 'setIsSaved':
|
||||||
return { ...state, isSaved: true };
|
return { ...state, isSaved: true }
|
||||||
case 'setIsNotSaved':
|
case 'setIsNotSaved':
|
||||||
return { ...state, isSaved: false };
|
return { ...state, isSaved: false }
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unhandled action type: ${action.type}`);
|
throw new Error(`Unhandled action type: ${action.type}`)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,8 +1,7 @@
|
||||||
'use client';
|
'use client'
|
||||||
import React, { useState } from 'react'
|
import React, { useState } from 'react'
|
||||||
|
|
||||||
|
export const EditorProviderContext = React.createContext(null) as any
|
||||||
export const EditorProviderContext = React.createContext(null) as any;
|
|
||||||
|
|
||||||
type EditorProviderProps = {
|
type EditorProviderProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
|
@ -14,7 +13,8 @@ type EditorProviderState = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function EditorOptionsProvider({ children, options }: EditorProviderProps) {
|
function EditorOptionsProvider({ children, options }: EditorProviderProps) {
|
||||||
const [editorOptions, setEditorOptions] = useState<EditorProviderState>(options);
|
const [editorOptions, setEditorOptions] =
|
||||||
|
useState<EditorProviderState>(options)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EditorProviderContext.Provider value={editorOptions}>
|
<EditorProviderContext.Provider value={editorOptions}>
|
||||||
|
|
@ -26,7 +26,5 @@ function EditorOptionsProvider({ children, options }: EditorProviderProps) {
|
||||||
export default EditorOptionsProvider
|
export default EditorOptionsProvider
|
||||||
|
|
||||||
export function useEditorProvider() {
|
export function useEditorProvider() {
|
||||||
return React.useContext(EditorProviderContext);
|
return React.useContext(EditorProviderContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,25 @@
|
||||||
'use client';
|
'use client'
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import React, { useContext, useEffect } from 'react'
|
import React, { useContext, useEffect } from 'react'
|
||||||
import useSWR from 'swr';
|
import useSWR from 'swr'
|
||||||
import { createContext } from 'react';
|
import { createContext } from 'react'
|
||||||
|
|
||||||
export const OrgContext = createContext({}) as any;
|
export const OrgContext = createContext({}) as any
|
||||||
|
|
||||||
export function OrgProvider({ children, orgslug }: { children: React.ReactNode, orgslug: string }) {
|
export function OrgProvider({
|
||||||
const { data: org } = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher);
|
children,
|
||||||
useEffect(() => {
|
orgslug,
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode
|
||||||
|
orgslug: string
|
||||||
|
}) {
|
||||||
|
const { data: org } = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher)
|
||||||
|
useEffect(() => {}, [org])
|
||||||
|
|
||||||
}, [org]);
|
return <OrgContext.Provider value={org}>{children}</OrgContext.Provider>
|
||||||
|
|
||||||
return (
|
|
||||||
<OrgContext.Provider value={org}>
|
|
||||||
{children}
|
|
||||||
</OrgContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useOrg() {
|
export function useOrg() {
|
||||||
return useContext(OrgContext);
|
return useContext(OrgContext)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,48 +1,66 @@
|
||||||
'use client';
|
'use client'
|
||||||
import { getNewAccessTokenUsingRefreshToken, getUserSession } from '@services/auth/auth';
|
import {
|
||||||
|
getNewAccessTokenUsingRefreshToken,
|
||||||
|
getUserSession,
|
||||||
|
} from '@services/auth/auth'
|
||||||
import React, { useContext, createContext, useEffect } from 'react'
|
import React, { useContext, createContext, useEffect } from 'react'
|
||||||
|
|
||||||
export const SessionContext = createContext({}) as any;
|
export const SessionContext = createContext({}) as any
|
||||||
|
|
||||||
type Session = {
|
type Session = {
|
||||||
access_token: string;
|
access_token: string
|
||||||
user: any;
|
user: any
|
||||||
roles: any;
|
roles: any
|
||||||
isLoading: boolean;
|
isLoading: boolean
|
||||||
isAuthenticated: boolean;
|
isAuthenticated: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function SessionProvider({ children }: { children: React.ReactNode }) {
|
function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||||
const [session, setSession] = React.useState<Session>({ access_token: "", user: {}, roles: {}, isLoading: true, isAuthenticated: false });
|
const [session, setSession] = React.useState<Session>({
|
||||||
|
access_token: '',
|
||||||
|
user: {},
|
||||||
|
roles: {},
|
||||||
|
isLoading: true,
|
||||||
|
isAuthenticated: false,
|
||||||
|
})
|
||||||
|
|
||||||
async function getNewAccessTokenUsingRefreshTokenUI() {
|
async function getNewAccessTokenUsingRefreshTokenUI() {
|
||||||
let data = await getNewAccessTokenUsingRefreshToken();
|
let data = await getNewAccessTokenUsingRefreshToken()
|
||||||
return data.access_token;
|
return data.access_token
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkSession() {
|
async function checkSession() {
|
||||||
// Get new access token using refresh token
|
// Get new access token using refresh token
|
||||||
const access_token = await getNewAccessTokenUsingRefreshTokenUI();
|
const access_token = await getNewAccessTokenUsingRefreshTokenUI()
|
||||||
|
|
||||||
if (access_token) {
|
if (access_token) {
|
||||||
// Get user session info
|
// Get user session info
|
||||||
const user_session = await getUserSession(access_token);
|
const user_session = await getUserSession(access_token)
|
||||||
|
|
||||||
// Set session
|
// Set session
|
||||||
setSession({ access_token: access_token, user: user_session.user, roles: user_session.roles, isLoading: false, isAuthenticated: true });
|
setSession({
|
||||||
|
access_token: access_token,
|
||||||
|
user: user_session.user,
|
||||||
|
roles: user_session.roles,
|
||||||
|
isLoading: false,
|
||||||
|
isAuthenticated: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!access_token) {
|
if (!access_token) {
|
||||||
setSession({ access_token: "", user: {}, roles: {}, isLoading: false, isAuthenticated: false });
|
setSession({
|
||||||
|
access_token: '',
|
||||||
|
user: {},
|
||||||
|
roles: {},
|
||||||
|
isLoading: false,
|
||||||
|
isAuthenticated: false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Check session
|
// Check session
|
||||||
checkSession();
|
checkSession()
|
||||||
|
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
@ -53,7 +71,7 @@ function SessionProvider({ children }: { children: React.ReactNode }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useSession() {
|
export function useSession() {
|
||||||
return useContext(SessionContext);
|
return useContext(SessionContext)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default SessionProvider
|
export default SessionProvider
|
||||||
|
|
@ -1,54 +1,54 @@
|
||||||
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form';
|
import FormLayout, {
|
||||||
import { useFormik } from 'formik';
|
FormField,
|
||||||
|
FormLabelAndMessage,
|
||||||
|
Input,
|
||||||
|
Textarea,
|
||||||
|
} from '@components/StyledElements/Form/Form'
|
||||||
|
import { useFormik } from 'formik'
|
||||||
import { AlertTriangle } from 'lucide-react'
|
import { AlertTriangle } from 'lucide-react'
|
||||||
import * as Switch from '@radix-ui/react-switch';
|
import * as Switch from '@radix-ui/react-switch'
|
||||||
import * as Form from '@radix-ui/react-form';
|
import * as Form from '@radix-ui/react-form'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext';
|
import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext'
|
||||||
import ThumbnailUpdate from './ThumbnailUpdate';
|
import ThumbnailUpdate from './ThumbnailUpdate'
|
||||||
|
|
||||||
|
|
||||||
type EditCourseStructureProps = {
|
type EditCourseStructureProps = {
|
||||||
orgslug: string,
|
orgslug: string
|
||||||
course_uuid?: string,
|
course_uuid?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
const validate = (values: any) => {
|
const validate = (values: any) => {
|
||||||
const errors: any = {};
|
const errors: any = {}
|
||||||
|
|
||||||
if (!values.name) {
|
if (!values.name) {
|
||||||
errors.name = 'Required';
|
errors.name = 'Required'
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.name.length > 100) {
|
if (values.name.length > 100) {
|
||||||
errors.name = 'Must be 100 characters or less';
|
errors.name = 'Must be 100 characters or less'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!values.description) {
|
if (!values.description) {
|
||||||
errors.description = 'Required';
|
errors.description = 'Required'
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (values.description.length > 1000) {
|
if (values.description.length > 1000) {
|
||||||
errors.description = 'Must be 1000 characters or less';
|
errors.description = 'Must be 1000 characters or less'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (!values.learnings) {
|
if (!values.learnings) {
|
||||||
errors.learnings = 'Required';
|
errors.learnings = 'Required'
|
||||||
}
|
}
|
||||||
|
|
||||||
return errors;
|
return errors
|
||||||
};
|
}
|
||||||
|
|
||||||
|
|
||||||
function EditCourseGeneral(props: EditCourseStructureProps) {
|
function EditCourseGeneral(props: EditCourseStructureProps) {
|
||||||
const [error, setError] = React.useState('');
|
const [error, setError] = React.useState('')
|
||||||
const course = useCourse() as any;
|
const course = useCourse() as any
|
||||||
const dispatchCourse = useCourseDispatch() as any;
|
const dispatchCourse = useCourseDispatch() as any
|
||||||
|
|
||||||
const courseStructure = course.courseStructure;
|
const courseStructure = course.courseStructure
|
||||||
const formik = useFormik({
|
const formik = useFormik({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
name: String(courseStructure.name),
|
name: String(courseStructure.name),
|
||||||
|
|
@ -59,17 +59,14 @@ function EditCourseGeneral(props: EditCourseStructureProps) {
|
||||||
public: String(courseStructure.public),
|
public: String(courseStructure.public),
|
||||||
},
|
},
|
||||||
validate,
|
validate,
|
||||||
onSubmit: async values => {
|
onSubmit: async (values) => {},
|
||||||
|
|
||||||
},
|
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
});
|
})
|
||||||
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// This code will run whenever form values are updated
|
// This code will run whenever form values are updated
|
||||||
if (formik.values !== formik.initialValues) {
|
if (formik.values !== formik.initialValues) {
|
||||||
dispatchCourse({ type: 'setIsNotSaved' });
|
dispatchCourse({ type: 'setIsNotSaved' })
|
||||||
const updatedCourse = {
|
const updatedCourse = {
|
||||||
...courseStructure,
|
...courseStructure,
|
||||||
name: formik.values.name,
|
name: formik.values.name,
|
||||||
|
|
@ -79,15 +76,15 @@ function EditCourseGeneral(props: EditCourseStructureProps) {
|
||||||
tags: formik.values.tags,
|
tags: formik.values.tags,
|
||||||
public: formik.values.public,
|
public: formik.values.public,
|
||||||
}
|
}
|
||||||
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse });
|
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse })
|
||||||
}
|
}
|
||||||
|
}, [course, formik.values, formik.initialValues])
|
||||||
}, [course, formik.values, formik.initialValues]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div> <div className="h-6"></div>
|
<div>
|
||||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
{' '}
|
||||||
|
<div className="h-6"></div>
|
||||||
|
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">
|
||||||
{course.courseStructure && (
|
{course.courseStructure && (
|
||||||
<div className="editcourse-form">
|
<div className="editcourse-form">
|
||||||
{error && (
|
{error && (
|
||||||
|
|
@ -98,63 +95,108 @@ function EditCourseGeneral(props: EditCourseStructureProps) {
|
||||||
)}
|
)}
|
||||||
<FormLayout onSubmit={formik.handleSubmit}>
|
<FormLayout onSubmit={formik.handleSubmit}>
|
||||||
<FormField name="name">
|
<FormField name="name">
|
||||||
<FormLabelAndMessage label='Name' message={formik.errors.name} />
|
<FormLabelAndMessage
|
||||||
|
label="Name"
|
||||||
|
message={formik.errors.name}
|
||||||
|
/>
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
|
<Input
|
||||||
|
style={{ backgroundColor: 'white' }}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.name}
|
||||||
|
type="text"
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField name="description">
|
<FormField name="description">
|
||||||
<FormLabelAndMessage label='Description' message={formik.errors.description} />
|
<FormLabelAndMessage
|
||||||
|
label="Description"
|
||||||
|
message={formik.errors.description}
|
||||||
|
/>
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
|
<Textarea
|
||||||
|
style={{ backgroundColor: 'white' }}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.description}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField name="about">
|
<FormField name="about">
|
||||||
<FormLabelAndMessage label='About' message={formik.errors.about} />
|
<FormLabelAndMessage
|
||||||
|
label="About"
|
||||||
|
message={formik.errors.about}
|
||||||
|
/>
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.about} required />
|
<Textarea
|
||||||
|
style={{ backgroundColor: 'white' }}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.about}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField name="learnings">
|
<FormField name="learnings">
|
||||||
<FormLabelAndMessage label='Learnings' message={formik.errors.learnings} />
|
<FormLabelAndMessage
|
||||||
|
label="Learnings"
|
||||||
|
message={formik.errors.learnings}
|
||||||
|
/>
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
|
<Textarea
|
||||||
|
style={{ backgroundColor: 'white' }}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.learnings}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField name="tags">
|
<FormField name="tags">
|
||||||
<FormLabelAndMessage label='Tags' message={formik.errors.tags} />
|
<FormLabelAndMessage
|
||||||
|
label="Tags"
|
||||||
|
message={formik.errors.tags}
|
||||||
|
/>
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.tags} required />
|
<Textarea
|
||||||
|
style={{ backgroundColor: 'white' }}
|
||||||
|
onChange={formik.handleChange}
|
||||||
|
value={formik.values.tags}
|
||||||
|
required
|
||||||
|
/>
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField name="thumbnail">
|
<FormField name="thumbnail">
|
||||||
<FormLabelAndMessage label='Thumbnail' />
|
<FormLabelAndMessage label="Thumbnail" />
|
||||||
<Form.Control asChild>
|
<Form.Control asChild>
|
||||||
<ThumbnailUpdate />
|
<ThumbnailUpdate />
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField className="flex items-center h-10" name="public">
|
<FormField className="flex items-center h-10" name="public">
|
||||||
<div className='flex my-auto items-center'>
|
<div className="flex my-auto items-center">
|
||||||
<label className="text-black text-[15px] leading-none pr-[15px]" htmlFor="public-course">
|
<label
|
||||||
|
className="text-black text-[15px] leading-none pr-[15px]"
|
||||||
|
htmlFor="public-course"
|
||||||
|
>
|
||||||
Public Course
|
Public Course
|
||||||
</label>
|
</label>
|
||||||
<Switch.Root
|
<Switch.Root
|
||||||
className="w-[42px] h-[25px] bg-neutral-200 rounded-full relative data-[state=checked]:bg-neutral-500 outline-none cursor-default"
|
className="w-[42px] h-[25px] bg-neutral-200 rounded-full relative data-[state=checked]:bg-neutral-500 outline-none cursor-default"
|
||||||
id="public-course"
|
id="public-course"
|
||||||
onCheckedChange={checked => formik.setFieldValue('public', checked)}
|
onCheckedChange={(checked) =>
|
||||||
|
formik.setFieldValue('public', checked)
|
||||||
|
}
|
||||||
checked={formik.values.public === 'true'}
|
checked={formik.values.public === 'true'}
|
||||||
>
|
>
|
||||||
<Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-[0_2px_2px] shadow-neutral-300 transition-transform duration-100 translate-x-0.5 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
<Switch.Thumb className="block w-[21px] h-[21px] bg-white rounded-full shadow-[0_2px_2px] shadow-neutral-300 transition-transform duration-100 translate-x-0.5 will-change-transform data-[state=checked]:translate-x-[19px]" />
|
||||||
</Switch.Root>
|
</Switch.Root>
|
||||||
</div>
|
</div>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
</FormLayout>
|
</FormLayout>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,92 @@
|
||||||
import { useCourse } from '@components/Contexts/CourseContext';
|
import { useCourse } from '@components/Contexts/CourseContext'
|
||||||
import { useOrg } from '@components/Contexts/OrgContext';
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { updateCourseThumbnail } from '@services/courses/courses';
|
import { updateCourseThumbnail } from '@services/courses/courses'
|
||||||
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
|
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||||
import { ArrowBigUpDash, UploadCloud } from 'lucide-react';
|
import { ArrowBigUpDash, UploadCloud } from 'lucide-react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { mutate } from 'swr';
|
import { mutate } from 'swr'
|
||||||
|
|
||||||
function ThumbnailUpdate() {
|
function ThumbnailUpdate() {
|
||||||
const course = useCourse() as any;
|
const course = useCourse() as any
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const [localThumbnail, setLocalThumbnail] = React.useState(null) as any;
|
const [localThumbnail, setLocalThumbnail] = React.useState(null) as any
|
||||||
const [isLoading, setIsLoading] = React.useState(false) as any;
|
const [isLoading, setIsLoading] = React.useState(false) as any
|
||||||
const [error, setError] = React.useState('') as any;
|
const [error, setError] = React.useState('') as any
|
||||||
|
|
||||||
|
|
||||||
const handleFileChange = async (event: any) => {
|
const handleFileChange = async (event: any) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0]
|
||||||
setLocalThumbnail(file);
|
setLocalThumbnail(file)
|
||||||
setIsLoading(true);
|
setIsLoading(true)
|
||||||
const res = await updateCourseThumbnail(course.courseStructure.course_uuid, file)
|
const res = await updateCourseThumbnail(
|
||||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
course.courseStructure.course_uuid,
|
||||||
|
file
|
||||||
|
)
|
||||||
|
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||||
// wait for 1 second to show loading animation
|
// wait for 1 second to show loading animation
|
||||||
await new Promise(r => setTimeout(r, 1500));
|
await new Promise((r) => setTimeout(r, 1500))
|
||||||
if (res.success === false) {
|
if (res.success === false) {
|
||||||
setError(res.HTTPmessage);
|
setError(res.HTTPmessage)
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(false);
|
setIsLoading(false)
|
||||||
setError('');
|
setError('')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow'>
|
<div className="w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow">
|
||||||
<div className='flex flex-col justify-center items-center h-full'>
|
<div className="flex flex-col justify-center items-center h-full">
|
||||||
<div className='flex flex-col justify-center items-center'>
|
<div className="flex flex-col justify-center items-center">
|
||||||
<div className='flex flex-col justify-center items-center'>
|
<div className="flex flex-col justify-center items-center">
|
||||||
{error && (
|
{error && (
|
||||||
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-2 transition-all shadow-sm">
|
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-2 transition-all shadow-sm">
|
||||||
<div className="text-sm font-semibold">{error}</div>
|
<div className="text-sm font-semibold">{error}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{localThumbnail ? (
|
{localThumbnail ? (
|
||||||
<img src={URL.createObjectURL(localThumbnail)} className={`${isLoading ? 'animate-pulse' : ''} shadow w-[200px] h-[100px] rounded-md`} />
|
<img
|
||||||
|
src={URL.createObjectURL(localThumbnail)}
|
||||||
|
className={`${
|
||||||
|
isLoading ? 'animate-pulse' : ''
|
||||||
|
} shadow w-[200px] h-[100px] rounded-md`}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<img src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, course.courseStructure.course_uuid, course.courseStructure.thumbnail_image)}`} className='shadow w-[200px] h-[100px] rounded-md' />
|
<img
|
||||||
|
src={`${getCourseThumbnailMediaDirectory(
|
||||||
|
org?.org_uuid,
|
||||||
|
course.courseStructure.course_uuid,
|
||||||
|
course.courseStructure.thumbnail_image
|
||||||
|
)}`}
|
||||||
|
className="shadow w-[200px] h-[100px] rounded-md"
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? (<div className='flex justify-center items-center'>
|
{isLoading ? (
|
||||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
<div className="flex justify-center items-center">
|
||||||
<div
|
<input
|
||||||
className='font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
|
type="file"
|
||||||
>
|
id="fileInput"
|
||||||
<ArrowBigUpDash size={16} className='mr-2' />
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
<div className="font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex">
|
||||||
|
<ArrowBigUpDash size={16} className="mr-2" />
|
||||||
<span>Uploading</span>
|
<span>Uploading</span>
|
||||||
</div>
|
</div>
|
||||||
</div>) : (
|
</div>
|
||||||
|
) : (
|
||||||
<div className='flex justify-center items-center'>
|
<div className="flex justify-center items-center">
|
||||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
<input
|
||||||
|
type="file"
|
||||||
|
id="fileInput"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
className='font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex'
|
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex"
|
||||||
onClick={() => document.getElementById('fileInput')?.click()}
|
onClick={() => document.getElementById('fileInput')?.click()}
|
||||||
>
|
>
|
||||||
<UploadCloud size={16} className='mr-2' />
|
<UploadCloud size={16} className="mr-2" />
|
||||||
<span>Change Thumbnail</span>
|
<span>Change Thumbnail</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,90 +1,113 @@
|
||||||
import { useCourse } from '@components/Contexts/CourseContext';
|
import { useCourse } from '@components/Contexts/CourseContext'
|
||||||
import NewActivityModal from '@components/Objects/Modals/Activities/Create/NewActivity';
|
import NewActivityModal from '@components/Objects/Modals/Activities/Create/NewActivity'
|
||||||
import Modal from '@components/StyledElements/Modal/Modal';
|
import Modal from '@components/StyledElements/Modal/Modal'
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { createActivity, createExternalVideoActivity, createFileActivity } from '@services/courses/activities';
|
import {
|
||||||
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs';
|
createActivity,
|
||||||
import { revalidateTags } from '@services/utils/ts/requests';
|
createExternalVideoActivity,
|
||||||
|
createFileActivity,
|
||||||
|
} from '@services/courses/activities'
|
||||||
|
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs'
|
||||||
|
import { revalidateTags } from '@services/utils/ts/requests'
|
||||||
import { Layers } from 'lucide-react'
|
import { Layers } from 'lucide-react'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { mutate } from 'swr';
|
import { mutate } from 'swr'
|
||||||
|
|
||||||
type NewActivityButtonProps = {
|
type NewActivityButtonProps = {
|
||||||
chapterId: string,
|
chapterId: string
|
||||||
orgslug: string
|
orgslug: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function NewActivityButton(props: NewActivityButtonProps) {
|
function NewActivityButton(props: NewActivityButtonProps) {
|
||||||
const [newActivityModal, setNewActivityModal] = React.useState(false);
|
const [newActivityModal, setNewActivityModal] = React.useState(false)
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const course = useCourse() as any;
|
const course = useCourse() as any
|
||||||
|
|
||||||
const openNewActivityModal = async (chapterId: any) => {
|
const openNewActivityModal = async (chapterId: any) => {
|
||||||
setNewActivityModal(true);
|
setNewActivityModal(true)
|
||||||
};
|
}
|
||||||
|
|
||||||
const closeNewActivityModal = async () => {
|
const closeNewActivityModal = async () => {
|
||||||
setNewActivityModal(false);
|
setNewActivityModal(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Submit new activity
|
// Submit new activity
|
||||||
const submitActivity = async (activity: any) => {
|
const submitActivity = async (activity: any) => {
|
||||||
let org = await getOrganizationContextInfoWithoutCredentials(props.orgslug, { revalidate: 1800 });
|
let org = await getOrganizationContextInfoWithoutCredentials(
|
||||||
await createActivity(activity, props.chapterId, org.org_id);
|
props.orgslug,
|
||||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
{ revalidate: 1800 }
|
||||||
setNewActivityModal(false);
|
)
|
||||||
await revalidateTags(['courses'], props.orgslug);
|
await createActivity(activity, props.chapterId, org.org_id)
|
||||||
router.refresh();
|
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||||
};
|
setNewActivityModal(false)
|
||||||
|
await revalidateTags(['courses'], props.orgslug)
|
||||||
|
router.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
// Submit File Upload
|
// Submit File Upload
|
||||||
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
|
const submitFileActivity = async (
|
||||||
await createFileActivity(file, type, activity, chapterId);
|
file: any,
|
||||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
type: any,
|
||||||
setNewActivityModal(false);
|
activity: any,
|
||||||
await revalidateTags(['courses'], props.orgslug);
|
chapterId: string
|
||||||
router.refresh();
|
) => {
|
||||||
};
|
await createFileActivity(file, type, activity, chapterId)
|
||||||
|
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||||
|
setNewActivityModal(false)
|
||||||
|
await revalidateTags(['courses'], props.orgslug)
|
||||||
|
router.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
// Submit YouTube Video Upload
|
// Submit YouTube Video Upload
|
||||||
const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => {
|
const submitExternalVideo = async (
|
||||||
await createExternalVideoActivity(external_video_data, activity, props.chapterId);
|
external_video_data: any,
|
||||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
activity: any,
|
||||||
setNewActivityModal(false);
|
chapterId: string
|
||||||
await revalidateTags(['courses'], props.orgslug);
|
) => {
|
||||||
router.refresh();
|
await createExternalVideoActivity(
|
||||||
};
|
external_video_data,
|
||||||
|
activity,
|
||||||
|
props.chapterId
|
||||||
|
)
|
||||||
|
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||||
|
setNewActivityModal(false)
|
||||||
|
await revalidateTags(['courses'], props.orgslug)
|
||||||
|
router.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => { }
|
useEffect(() => {}, [course])
|
||||||
, [course])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex justify-center'>
|
<div className="flex justify-center">
|
||||||
<Modal
|
<Modal
|
||||||
isDialogOpen={newActivityModal}
|
isDialogOpen={newActivityModal}
|
||||||
onOpenChange={setNewActivityModal}
|
onOpenChange={setNewActivityModal}
|
||||||
minHeight="no-min"
|
minHeight="no-min"
|
||||||
addDefCloseButton={false}
|
addDefCloseButton={false}
|
||||||
dialogContent={<NewActivityModal
|
dialogContent={
|
||||||
|
<NewActivityModal
|
||||||
closeModal={closeNewActivityModal}
|
closeModal={closeNewActivityModal}
|
||||||
submitFileActivity={submitFileActivity}
|
submitFileActivity={submitFileActivity}
|
||||||
submitExternalVideo={submitExternalVideo}
|
submitExternalVideo={submitExternalVideo}
|
||||||
submitActivity={submitActivity}
|
submitActivity={submitActivity}
|
||||||
chapterId={props.chapterId}
|
chapterId={props.chapterId}
|
||||||
course={course}
|
course={course}
|
||||||
></NewActivityModal>}
|
></NewActivityModal>
|
||||||
|
}
|
||||||
dialogTitle="Create Activity"
|
dialogTitle="Create Activity"
|
||||||
dialogDescription="Choose between types of activities to add to the course"
|
dialogDescription="Choose between types of activities to add to the course"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<div onClick={() => {
|
<div
|
||||||
|
onClick={() => {
|
||||||
openNewActivityModal(props.chapterId)
|
openNewActivityModal(props.chapterId)
|
||||||
}} className="flex w-44 h-10 space-x-2 items-center py-2 my-3 rounded-xl justify-center text-white bg-black hover:cursor-pointer">
|
}}
|
||||||
|
className="flex w-44 h-10 space-x-2 items-center py-2 my-3 rounded-xl justify-center text-white bg-black hover:cursor-pointer"
|
||||||
|
>
|
||||||
<Layers className="" size={17} />
|
<Layers className="" size={17} />
|
||||||
<div className="text-sm mx-auto my-auto items-center font-bold">Add Activity</div>
|
<div className="text-sm mx-auto my-auto items-center font-bold">
|
||||||
|
Add Activity
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,16 @@ import ConfirmationModal from '@components/StyledElements/ConfirmationModal/Conf
|
||||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
||||||
import { deleteActivity, updateActivity } from '@services/courses/activities'
|
import { deleteActivity, updateActivity } from '@services/courses/activities'
|
||||||
import { revalidateTags } from '@services/utils/ts/requests'
|
import { revalidateTags } from '@services/utils/ts/requests'
|
||||||
import { Eye, File, MoreVertical, Pencil, Save, Sparkles, Video, X } from 'lucide-react'
|
import {
|
||||||
|
Eye,
|
||||||
|
File,
|
||||||
|
MoreVertical,
|
||||||
|
Pencil,
|
||||||
|
Save,
|
||||||
|
Sparkles,
|
||||||
|
Video,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/navigation'
|
import { useRouter } from 'next/navigation'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
|
|
@ -10,33 +19,40 @@ import { Draggable } from 'react-beautiful-dnd'
|
||||||
import { mutate } from 'swr'
|
import { mutate } from 'swr'
|
||||||
|
|
||||||
type ActivitiyElementProps = {
|
type ActivitiyElementProps = {
|
||||||
orgslug: string,
|
orgslug: string
|
||||||
activity: any,
|
activity: any
|
||||||
activityIndex: any,
|
activityIndex: any
|
||||||
course_uuid: string
|
course_uuid: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModifiedActivityInterface {
|
interface ModifiedActivityInterface {
|
||||||
activityId: string;
|
activityId: string
|
||||||
activityName: string;
|
activityName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActivityElement(props: ActivitiyElementProps) {
|
function ActivityElement(props: ActivitiyElementProps) {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const [modifiedActivity, setModifiedActivity] = React.useState<ModifiedActivityInterface | undefined>(undefined);
|
const [modifiedActivity, setModifiedActivity] = React.useState<
|
||||||
const [selectedActivity, setSelectedActivity] = React.useState<string | undefined>(undefined);
|
ModifiedActivityInterface | undefined
|
||||||
const activityUUID = props.activity.activity_uuid;
|
>(undefined)
|
||||||
|
const [selectedActivity, setSelectedActivity] = React.useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined)
|
||||||
|
const activityUUID = props.activity.activity_uuid
|
||||||
|
|
||||||
async function deleteActivityUI() {
|
async function deleteActivityUI() {
|
||||||
await deleteActivity(props.activity.activity_uuid);
|
await deleteActivity(props.activity.activity_uuid)
|
||||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
|
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
|
||||||
await revalidateTags(['courses'], props.orgslug);
|
await revalidateTags(['courses'], props.orgslug)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateActivityName(activityId: string) {
|
async function updateActivityName(activityId: string) {
|
||||||
if ((modifiedActivity?.activityId === activityId) && selectedActivity !== undefined) {
|
if (
|
||||||
setSelectedActivity(undefined);
|
modifiedActivity?.activityId === activityId &&
|
||||||
|
selectedActivity !== undefined
|
||||||
|
) {
|
||||||
|
setSelectedActivity(undefined)
|
||||||
let modifiedActivityCopy = {
|
let modifiedActivityCopy = {
|
||||||
name: modifiedActivity.activityName,
|
name: modifiedActivity.activityName,
|
||||||
description: '',
|
description: '',
|
||||||
|
|
@ -45,14 +61,18 @@ function ActivityElement(props: ActivitiyElementProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateActivity(modifiedActivityCopy, activityUUID)
|
await updateActivity(modifiedActivityCopy, activityUUID)
|
||||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
|
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
|
||||||
await revalidateTags(['courses'], props.orgslug)
|
await revalidateTags(['courses'], props.orgslug)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable key={props.activity.activity_uuid} draggableId={props.activity.activity_uuid} index={props.activityIndex}>
|
<Draggable
|
||||||
|
key={props.activity.activity_uuid}
|
||||||
|
draggableId={props.activity.activity_uuid}
|
||||||
|
index={props.activityIndex}
|
||||||
|
>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<div
|
<div
|
||||||
className="flex flex-row py-2 my-2 w-full rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-1 items-center ring-1 ring-inset ring-gray-400/10 shadow-sm transition-all delay-100 duration-75 ease-linear"
|
className="flex flex-row py-2 my-2 w-full rounded-md bg-gray-50 text-gray-500 hover:bg-gray-100 hover:scale-102 hover:shadow space-x-1 items-center ring-1 ring-inset ring-gray-400/10 shadow-sm transition-all delay-100 duration-75 ease-linear"
|
||||||
|
|
@ -61,36 +81,84 @@ function ActivityElement(props: ActivitiyElementProps) {
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
|
|
||||||
{/* Activity Type Icon */}
|
{/* Activity Type Icon */}
|
||||||
<ActivityTypeIndicator activityType={props.activity.activity_type} />
|
<ActivityTypeIndicator activityType={props.activity.activity_type} />
|
||||||
|
|
||||||
{/* Centered Activity Name */}
|
{/* Centered Activity Name */}
|
||||||
<div className="grow items-center space-x-2 flex mx-auto justify-center">
|
<div className="grow items-center space-x-2 flex mx-auto justify-center">
|
||||||
{selectedActivity === props.activity.id ?
|
{selectedActivity === props.activity.id ? (
|
||||||
(<div className="chapter-modification-zone text-[7px] text-gray-600 shadow-inner bg-gray-200/60 py-1 px-4 rounded-lg space-x-3">
|
<div className="chapter-modification-zone text-[7px] text-gray-600 shadow-inner bg-gray-200/60 py-1 px-4 rounded-lg space-x-3">
|
||||||
<input type="text" className="bg-transparent outline-none text-xs text-gray-500" placeholder="Activity name" value={modifiedActivity ? modifiedActivity?.activityName : props.activity.name} onChange={(e) => setModifiedActivity({ activityId: props.activity.id, activityName: e.target.value })} />
|
<input
|
||||||
<button onClick={() => updateActivityName(props.activity.id)} className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900">
|
type="text"
|
||||||
<Save size={11} onClick={() => updateActivityName(props.activity.id)} />
|
className="bg-transparent outline-none text-xs text-gray-500"
|
||||||
|
placeholder="Activity name"
|
||||||
|
value={
|
||||||
|
modifiedActivity
|
||||||
|
? modifiedActivity?.activityName
|
||||||
|
: props.activity.name
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
setModifiedActivity({
|
||||||
|
activityId: props.activity.id,
|
||||||
|
activityName: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => updateActivityName(props.activity.id)}
|
||||||
|
className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900"
|
||||||
|
>
|
||||||
|
<Save
|
||||||
|
size={11}
|
||||||
|
onClick={() => updateActivityName(props.activity.id)}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>) : (<p className="first-letter:uppercase"> {props.activity.name} </p>)}
|
</div>
|
||||||
<Pencil onClick={() => setSelectedActivity(props.activity.id)}
|
) : (
|
||||||
size={12} className="text-neutral-400 hover:cursor-pointer" />
|
<p className="first-letter:uppercase"> {props.activity.name} </p>
|
||||||
|
)}
|
||||||
|
<Pencil
|
||||||
|
onClick={() => setSelectedActivity(props.activity.id)}
|
||||||
|
size={12}
|
||||||
|
className="text-neutral-400 hover:cursor-pointer"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/* Edit and View Button */}
|
{/* Edit and View Button */}
|
||||||
<div className="flex flex-row space-x-2">
|
<div className="flex flex-row space-x-2">
|
||||||
{props.activity.activity_type === "TYPE_DYNAMIC" && <>
|
{props.activity.activity_type === 'TYPE_DYNAMIC' && (
|
||||||
|
<>
|
||||||
<Link
|
<Link
|
||||||
href={getUriWithOrg(props.orgslug, "") + `/course/${props.course_uuid.replace("course_", "")}/activity/${props.activity.activity_uuid.replace("activity_", "")}/edit`}
|
href={
|
||||||
|
getUriWithOrg(props.orgslug, '') +
|
||||||
|
`/course/${props.course_uuid.replace(
|
||||||
|
'course_',
|
||||||
|
''
|
||||||
|
)}/activity/${props.activity.activity_uuid.replace(
|
||||||
|
'activity_',
|
||||||
|
''
|
||||||
|
)}/edit`
|
||||||
|
}
|
||||||
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
|
className=" hover:cursor-pointer p-1 px-3 bg-sky-700 rounded-md items-center"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer"
|
||||||
<div className="text-sky-100 font-bold text-xs" >Edit </div>
|
>
|
||||||
|
<div className="text-sky-100 font-bold text-xs">Edit </div>
|
||||||
</Link>
|
</Link>
|
||||||
</>}
|
</>
|
||||||
|
)}
|
||||||
<Link
|
<Link
|
||||||
href={getUriWithOrg(props.orgslug, "") + `/course/${props.course_uuid.replace("course_", "")}/activity/${props.activity.activity_uuid.replace("activity_", "")}`}
|
href={
|
||||||
|
getUriWithOrg(props.orgslug, '') +
|
||||||
|
`/course/${props.course_uuid.replace(
|
||||||
|
'course_',
|
||||||
|
''
|
||||||
|
)}/activity/${props.activity.activity_uuid.replace(
|
||||||
|
'activity_',
|
||||||
|
''
|
||||||
|
)}`
|
||||||
|
}
|
||||||
className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md"
|
className=" hover:cursor-pointer p-1 px-3 bg-gray-200 rounded-md"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<Eye strokeWidth={2} size={15} className="text-gray-600" />
|
<Eye strokeWidth={2} size={15} className="text-gray-600" />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -100,33 +168,60 @@ function ActivityElement(props: ActivitiyElementProps) {
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
confirmationMessage="Are you sure you want to delete this activity ?"
|
confirmationMessage="Are you sure you want to delete this activity ?"
|
||||||
confirmationButtonText="Delete Activity"
|
confirmationButtonText="Delete Activity"
|
||||||
dialogTitle={"Delete " + props.activity.name + " ?"}
|
dialogTitle={'Delete ' + props.activity.name + ' ?'}
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
<div
|
<div
|
||||||
className=" hover:cursor-pointer p-1 px-5 bg-red-600 rounded-md"
|
className=" hover:cursor-pointer p-1 px-5 bg-red-600 rounded-md"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<X size={15} className="text-rose-200 font-bold" />
|
<X size={15} className="text-rose-200 font-bold" />
|
||||||
</div>}
|
</div>
|
||||||
|
}
|
||||||
functionToExecute={() => deleteActivityUI()}
|
functionToExecute={() => deleteActivityUI()}
|
||||||
status='warning'
|
status="warning"
|
||||||
></ConfirmationModal></div>
|
></ConfirmationModal>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const ActivityTypeIndicator = (props: { activityType: string }) => {
|
const ActivityTypeIndicator = (props: { activityType: string }) => {
|
||||||
return (
|
return (
|
||||||
<div className="px-3 text-gray-300 space-x-1 w-28" >
|
<div className="px-3 text-gray-300 space-x-1 w-28">
|
||||||
|
{props.activityType === 'TYPE_VIDEO' && (
|
||||||
|
<>
|
||||||
{props.activityType === "TYPE_VIDEO" && <>
|
<div className="flex space-x-2 items-center">
|
||||||
<div className="flex space-x-2 items-center"><Video size={16} /> <div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full mx-auto justify-center align-middle">Video</div> </div></>}
|
<Video size={16} />{' '}
|
||||||
{props.activityType === "TYPE_DOCUMENT" && <><div className="flex space-x-2 items-center"><div className="w-[30px]"><File size={16} /> </div><div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">Document</div> </div></>}
|
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full mx-auto justify-center align-middle">
|
||||||
{props.activityType === "TYPE_DYNAMIC" && <><div className="flex space-x-2 items-center"><Sparkles size={16} /> <div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">Dynamic</div> </div></>}
|
Video
|
||||||
|
</div>{' '}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{props.activityType === 'TYPE_DOCUMENT' && (
|
||||||
|
<>
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
<div className="w-[30px]">
|
||||||
|
<File size={16} />{' '}
|
||||||
|
</div>
|
||||||
|
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">
|
||||||
|
Document
|
||||||
|
</div>{' '}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{props.activityType === 'TYPE_DYNAMIC' && (
|
||||||
|
<>
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
<Sparkles size={16} />{' '}
|
||||||
|
<div className="text-xs bg-gray-200 text-gray-400 font-bold px-2 py-1 rounded-full">
|
||||||
|
Dynamic
|
||||||
|
</div>{' '}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,51 +1,62 @@
|
||||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||||
import { Hexagon, MoreHorizontal, MoreVertical, Pencil, Save, X } from 'lucide-react';
|
import {
|
||||||
|
Hexagon,
|
||||||
|
MoreHorizontal,
|
||||||
|
MoreVertical,
|
||||||
|
Pencil,
|
||||||
|
Save,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react'
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Draggable, Droppable } from 'react-beautiful-dnd';
|
import { Draggable, Droppable } from 'react-beautiful-dnd'
|
||||||
import ActivityElement from './ActivityElement';
|
import ActivityElement from './ActivityElement'
|
||||||
import NewActivityButton from '../Buttons/NewActivityButton';
|
import NewActivityButton from '../Buttons/NewActivityButton'
|
||||||
import { deleteChapter, updateChapter } from '@services/courses/chapters';
|
import { deleteChapter, updateChapter } from '@services/courses/chapters'
|
||||||
import { revalidateTags } from '@services/utils/ts/requests';
|
import { revalidateTags } from '@services/utils/ts/requests'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation'
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { mutate } from 'swr';
|
import { mutate } from 'swr'
|
||||||
|
|
||||||
type ChapterElementProps = {
|
type ChapterElementProps = {
|
||||||
chapter: any,
|
chapter: any
|
||||||
chapterIndex: number,
|
chapterIndex: number
|
||||||
orgslug: string
|
orgslug: string
|
||||||
course_uuid: string
|
course_uuid: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ModifiedChapterInterface {
|
interface ModifiedChapterInterface {
|
||||||
chapterId: string;
|
chapterId: string
|
||||||
chapterName: string;
|
chapterName: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function ChapterElement(props: ChapterElementProps) {
|
function ChapterElement(props: ChapterElementProps) {
|
||||||
const activities = props.chapter.activities || [];
|
const activities = props.chapter.activities || []
|
||||||
const [modifiedChapter, setModifiedChapter] = React.useState<ModifiedChapterInterface | undefined>(undefined);
|
const [modifiedChapter, setModifiedChapter] = React.useState<
|
||||||
const [selectedChapter, setSelectedChapter] = React.useState<string | undefined>(undefined);
|
ModifiedChapterInterface | undefined
|
||||||
|
>(undefined)
|
||||||
|
const [selectedChapter, setSelectedChapter] = React.useState<
|
||||||
|
string | undefined
|
||||||
|
>(undefined)
|
||||||
|
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
|
|
||||||
const deleteChapterUI = async () => {
|
const deleteChapterUI = async () => {
|
||||||
await deleteChapter(props.chapter.id);
|
await deleteChapter(props.chapter.id)
|
||||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
|
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
|
||||||
await revalidateTags(['courses'], props.orgslug);
|
await revalidateTags(['courses'], props.orgslug)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
};
|
}
|
||||||
|
|
||||||
async function updateChapterName(chapterId: string) {
|
async function updateChapterName(chapterId: string) {
|
||||||
if (modifiedChapter?.chapterId === chapterId) {
|
if (modifiedChapter?.chapterId === chapterId) {
|
||||||
setSelectedChapter(undefined);
|
setSelectedChapter(undefined)
|
||||||
let modifiedChapterCopy = {
|
let modifiedChapterCopy = {
|
||||||
name: modifiedChapter.chapterName,
|
name: modifiedChapter.chapterName,
|
||||||
}
|
}
|
||||||
await updateChapter(chapterId, modifiedChapterCopy)
|
await updateChapter(chapterId, modifiedChapterCopy)
|
||||||
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
|
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
|
||||||
await revalidateTags(['courses'], props.orgslug)
|
await revalidateTags(['courses'], props.orgslug)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -63,39 +74,79 @@ function ChapterElement(props: ChapterElementProps) {
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
>
|
>
|
||||||
<div className="flex font-bold text-md items-center space-x-2 pb-3" >
|
<div className="flex font-bold text-md items-center space-x-2 pb-3">
|
||||||
<div className="flex grow text-lg space-x-3 items-center rounded-md ">
|
<div className="flex grow text-lg space-x-3 items-center rounded-md ">
|
||||||
<div className="bg-neutral-100 rounded-md p-2">
|
<div className="bg-neutral-100 rounded-md p-2">
|
||||||
<Hexagon strokeWidth={3} size={16} className="text-neutral-600 " />
|
<Hexagon
|
||||||
|
strokeWidth={3}
|
||||||
|
size={16}
|
||||||
|
className="text-neutral-600 "
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex space-x-2 items-center">
|
<div className="flex space-x-2 items-center">
|
||||||
{selectedChapter === props.chapter.id ?
|
{selectedChapter === props.chapter.id ? (
|
||||||
(<div className="chapter-modification-zone bg-neutral-100 py-1 px-4 rounded-lg space-x-3">
|
<div className="chapter-modification-zone bg-neutral-100 py-1 px-4 rounded-lg space-x-3">
|
||||||
<input type="text" className="bg-transparent outline-none text-sm text-neutral-700" placeholder="Chapter name" value={modifiedChapter ? modifiedChapter?.chapterName : props.chapter.name} onChange={(e) => setModifiedChapter({ chapterId: props.chapter.id, chapterName: e.target.value })} />
|
<input
|
||||||
<button onClick={() => updateChapterName(props.chapter.id)} className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900">
|
type="text"
|
||||||
<Save size={15} onClick={() => updateChapterName(props.chapter.id)} />
|
className="bg-transparent outline-none text-sm text-neutral-700"
|
||||||
|
placeholder="Chapter name"
|
||||||
|
value={
|
||||||
|
modifiedChapter
|
||||||
|
? modifiedChapter?.chapterName
|
||||||
|
: props.chapter.name
|
||||||
|
}
|
||||||
|
onChange={(e) =>
|
||||||
|
setModifiedChapter({
|
||||||
|
chapterId: props.chapter.id,
|
||||||
|
chapterName: e.target.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => updateChapterName(props.chapter.id)}
|
||||||
|
className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900"
|
||||||
|
>
|
||||||
|
<Save
|
||||||
|
size={15}
|
||||||
|
onClick={() => updateChapterName(props.chapter.id)}
|
||||||
|
/>
|
||||||
</button>
|
</button>
|
||||||
</div>) : (<p className="text-neutral-700 first-letter:uppercase">{props.chapter.name}</p>)}
|
</div>
|
||||||
<Pencil size={15} onClick={() => setSelectedChapter(props.chapter.id)} className="text-neutral-600 hover:cursor-pointer" />
|
) : (
|
||||||
|
<p className="text-neutral-700 first-letter:uppercase">
|
||||||
|
{props.chapter.name}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<Pencil
|
||||||
|
size={15}
|
||||||
|
onClick={() => setSelectedChapter(props.chapter.id)}
|
||||||
|
className="text-neutral-600 hover:cursor-pointer"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MoreVertical size={15} className="text-gray-300" />
|
<MoreVertical size={15} className="text-gray-300" />
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
confirmationButtonText="Delete Chapter"
|
confirmationButtonText="Delete Chapter"
|
||||||
confirmationMessage="Are you sure you want to delete this chapter?"
|
confirmationMessage="Are you sure you want to delete this chapter?"
|
||||||
dialogTitle={"Delete " + props.chapter.name + " ?"}
|
dialogTitle={'Delete ' + props.chapter.name + ' ?'}
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
<div
|
<div
|
||||||
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-md shadow flex space-x-1 items-center text-rose-100 text-sm"
|
className=" hover:cursor-pointer p-1 px-4 bg-red-600 rounded-md shadow flex space-x-1 items-center text-rose-100 text-sm"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
<X size={15} className="text-rose-200 font-bold" />
|
<X size={15} className="text-rose-200 font-bold" />
|
||||||
<p>Delete Chapter</p>
|
<p>Delete Chapter</p>
|
||||||
</div>}
|
</div>
|
||||||
|
}
|
||||||
functionToExecute={() => deleteChapterUI()}
|
functionToExecute={() => deleteChapterUI()}
|
||||||
status='warning'
|
status="warning"
|
||||||
></ConfirmationModal>
|
></ConfirmationModal>
|
||||||
</div>
|
</div>
|
||||||
<Droppable key={props.chapter.chapter_uuid} droppableId={props.chapter.chapter_uuid} type="activity">
|
<Droppable
|
||||||
|
key={props.chapter.chapter_uuid}
|
||||||
|
droppableId={props.chapter.chapter_uuid}
|
||||||
|
type="activity"
|
||||||
|
>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<div {...provided.droppableProps} ref={provided.innerRef}>
|
<div {...provided.droppableProps} ref={provided.innerRef}>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
|
|
@ -106,19 +157,24 @@ function ChapterElement(props: ChapterElementProps) {
|
||||||
orgslug={props.orgslug}
|
orgslug={props.orgslug}
|
||||||
course_uuid={props.course_uuid}
|
course_uuid={props.course_uuid}
|
||||||
activityIndex={index}
|
activityIndex={index}
|
||||||
activity={activity} />
|
activity={activity}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</Droppable>
|
</Droppable>
|
||||||
<NewActivityButton orgslug={props.orgslug} chapterId={props.chapter.id} />
|
<NewActivityButton
|
||||||
<div className='h-6'>
|
orgslug={props.orgslug}
|
||||||
<div className='flex items-center'><MoreHorizontal size={19} className="text-gray-300 mx-auto" /></div>
|
chapterId={props.chapter.id}
|
||||||
|
/>
|
||||||
|
<div className="h-6">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<MoreHorizontal size={19} className="text-gray-300 mx-auto" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,115 +1,144 @@
|
||||||
'use client';
|
'use client'
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { revalidateTags } from '@services/utils/ts/requests';
|
import { revalidateTags } from '@services/utils/ts/requests'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
|
import { DragDropContext, Droppable } from 'react-beautiful-dnd'
|
||||||
import { mutate } from 'swr';
|
import { mutate } from 'swr'
|
||||||
import ChapterElement from './DraggableElements/ChapterElement';
|
import ChapterElement from './DraggableElements/ChapterElement'
|
||||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import { createChapter } from '@services/courses/chapters';
|
import { createChapter } from '@services/courses/chapters'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation'
|
||||||
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext';
|
import {
|
||||||
import { Hexagon } from 'lucide-react';
|
useCourse,
|
||||||
import Modal from '@components/StyledElements/Modal/Modal';
|
useCourseDispatch,
|
||||||
import NewChapterModal from '@components/Objects/Modals/Chapters/NewChapter';
|
} from '@components/Contexts/CourseContext'
|
||||||
|
import { Hexagon } from 'lucide-react'
|
||||||
|
import Modal from '@components/StyledElements/Modal/Modal'
|
||||||
|
import NewChapterModal from '@components/Objects/Modals/Chapters/NewChapter'
|
||||||
|
|
||||||
type EditCourseStructureProps = {
|
type EditCourseStructureProps = {
|
||||||
orgslug: string,
|
orgslug: string
|
||||||
course_uuid?: string,
|
course_uuid?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export type OrderPayload = {
|
export type OrderPayload =
|
||||||
|
| {
|
||||||
chapter_order_by_ids: [
|
chapter_order_by_ids: [
|
||||||
{
|
{
|
||||||
chapter_id: string,
|
chapter_id: string
|
||||||
activities_order_by_ids: [
|
activities_order_by_ids: [
|
||||||
{
|
{
|
||||||
activity_id: string
|
activity_id: string
|
||||||
}
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
],
|
| undefined
|
||||||
} | undefined
|
|
||||||
|
|
||||||
const EditCourseStructure = (props: EditCourseStructureProps) => {
|
const EditCourseStructure = (props: EditCourseStructureProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
// Check window availability
|
// Check window availability
|
||||||
const [winReady, setwinReady] = useState(false);
|
const [winReady, setwinReady] = useState(false)
|
||||||
|
|
||||||
const dispatchCourse = useCourseDispatch() as any;
|
const dispatchCourse = useCourseDispatch() as any
|
||||||
|
|
||||||
const [order, setOrder] = useState<OrderPayload>();
|
const [order, setOrder] = useState<OrderPayload>()
|
||||||
const course = useCourse() as any;
|
const course = useCourse() as any
|
||||||
const course_structure = course ? course.courseStructure : {};
|
const course_structure = course ? course.courseStructure : {}
|
||||||
const course_uuid = course ? course.courseStructure.course_uuid : '';
|
const course_uuid = course ? course.courseStructure.course_uuid : ''
|
||||||
|
|
||||||
// New Chapter creation
|
// New Chapter creation
|
||||||
const [newChapterModal, setNewChapterModal] = useState(false);
|
const [newChapterModal, setNewChapterModal] = useState(false)
|
||||||
|
|
||||||
const closeNewChapterModal = async () => {
|
const closeNewChapterModal = async () => {
|
||||||
setNewChapterModal(false);
|
setNewChapterModal(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
// Submit new chapter
|
// Submit new chapter
|
||||||
const submitChapter = async (chapter: any) => {
|
const submitChapter = async (chapter: any) => {
|
||||||
await createChapter(chapter);
|
await createChapter(chapter)
|
||||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||||
await revalidateTags(['courses'], props.orgslug);
|
await revalidateTags(['courses'], props.orgslug)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
setNewChapterModal(false);
|
setNewChapterModal(false)
|
||||||
};
|
}
|
||||||
|
|
||||||
const updateStructure = (result: any) => {
|
const updateStructure = (result: any) => {
|
||||||
const { destination, source, draggableId, type } = result;
|
const { destination, source, draggableId, type } = result
|
||||||
if (!destination) return;
|
if (!destination) return
|
||||||
if (destination.droppableId === source.droppableId && destination.index === source.index) return;
|
if (
|
||||||
|
destination.droppableId === source.droppableId &&
|
||||||
|
destination.index === source.index
|
||||||
|
)
|
||||||
|
return
|
||||||
if (type === 'chapter') {
|
if (type === 'chapter') {
|
||||||
const newChapterOrder = Array.from(course_structure.chapters);
|
const newChapterOrder = Array.from(course_structure.chapters)
|
||||||
newChapterOrder.splice(source.index, 1);
|
newChapterOrder.splice(source.index, 1)
|
||||||
newChapterOrder.splice(destination.index, 0, course_structure.chapters[source.index]);
|
newChapterOrder.splice(
|
||||||
dispatchCourse({ type: 'setCourseStructure', payload: { ...course_structure, chapters: newChapterOrder } })
|
destination.index,
|
||||||
|
0,
|
||||||
|
course_structure.chapters[source.index]
|
||||||
|
)
|
||||||
|
dispatchCourse({
|
||||||
|
type: 'setCourseStructure',
|
||||||
|
payload: { ...course_structure, chapters: newChapterOrder },
|
||||||
|
})
|
||||||
dispatchCourse({ type: 'setIsNotSaved' })
|
dispatchCourse({ type: 'setIsNotSaved' })
|
||||||
}
|
}
|
||||||
if (type === 'activity') {
|
if (type === 'activity') {
|
||||||
const newChapterOrder = Array.from(course_structure.chapters);
|
const newChapterOrder = Array.from(course_structure.chapters)
|
||||||
const sourceChapter = newChapterOrder.find((chapter: any) => chapter.chapter_uuid === source.droppableId) as any;
|
const sourceChapter = newChapterOrder.find(
|
||||||
const destinationChapter = newChapterOrder.find((chapter: any) => chapter.chapter_uuid === destination.droppableId) ? newChapterOrder.find((chapter: any) => chapter.chapter_uuid === destination.droppableId) : sourceChapter;
|
(chapter: any) => chapter.chapter_uuid === source.droppableId
|
||||||
const activity = sourceChapter.activities.find((activity: any) => activity.activity_uuid === draggableId);
|
) as any
|
||||||
sourceChapter.activities.splice(source.index, 1);
|
const destinationChapter = newChapterOrder.find(
|
||||||
destinationChapter.activities.splice(destination.index, 0, activity);
|
(chapter: any) => chapter.chapter_uuid === destination.droppableId
|
||||||
dispatchCourse({ type: 'setCourseStructure', payload: { ...course_structure, chapters: newChapterOrder } })
|
)
|
||||||
|
? newChapterOrder.find(
|
||||||
|
(chapter: any) => chapter.chapter_uuid === destination.droppableId
|
||||||
|
)
|
||||||
|
: sourceChapter
|
||||||
|
const activity = sourceChapter.activities.find(
|
||||||
|
(activity: any) => activity.activity_uuid === draggableId
|
||||||
|
)
|
||||||
|
sourceChapter.activities.splice(source.index, 1)
|
||||||
|
destinationChapter.activities.splice(destination.index, 0, activity)
|
||||||
|
dispatchCourse({
|
||||||
|
type: 'setCourseStructure',
|
||||||
|
payload: { ...course_structure, chapters: newChapterOrder },
|
||||||
|
})
|
||||||
dispatchCourse({ type: 'setIsNotSaved' })
|
dispatchCourse({ type: 'setIsNotSaved' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setwinReady(true);
|
setwinReady(true)
|
||||||
|
}, [props.course_uuid, course_structure, course])
|
||||||
}, [props.course_uuid, course_structure, course]);
|
|
||||||
|
|
||||||
|
|
||||||
if (!course) return <PageLoading></PageLoading>
|
if (!course) return <PageLoading></PageLoading>
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex flex-col'>
|
<div className="flex flex-col">
|
||||||
<div className="h-6"></div>
|
<div className="h-6"></div>
|
||||||
{winReady ?
|
{winReady ? (
|
||||||
<DragDropContext onDragEnd={updateStructure}>
|
<DragDropContext onDragEnd={updateStructure}>
|
||||||
<Droppable type='chapter' droppableId='chapters'>
|
<Droppable type="chapter" droppableId="chapters">
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<div
|
<div
|
||||||
className='space-y-4'
|
className="space-y-4"
|
||||||
{...provided.droppableProps}
|
{...provided.droppableProps}
|
||||||
ref={provided.innerRef}>
|
ref={provided.innerRef}
|
||||||
{course_structure.chapters && course_structure.chapters.map((chapter: any, index: any) => {
|
>
|
||||||
|
{course_structure.chapters &&
|
||||||
|
course_structure.chapters.map((chapter: any, index: any) => {
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<ChapterElement
|
<ChapterElement
|
||||||
key={chapter.chapter_uuid}
|
key={chapter.chapter_uuid}
|
||||||
chapterIndex={index}
|
chapterIndex={index}
|
||||||
orgslug={props.orgslug}
|
orgslug={props.orgslug}
|
||||||
course_uuid={course_uuid}
|
course_uuid={course_uuid}
|
||||||
chapter={chapter} />
|
chapter={chapter}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
})}
|
})}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
|
|
@ -122,27 +151,33 @@ const EditCourseStructure = (props: EditCourseStructureProps) => {
|
||||||
isDialogOpen={newChapterModal}
|
isDialogOpen={newChapterModal}
|
||||||
onOpenChange={setNewChapterModal}
|
onOpenChange={setNewChapterModal}
|
||||||
minHeight="sm"
|
minHeight="sm"
|
||||||
dialogContent={<NewChapterModal
|
dialogContent={
|
||||||
|
<NewChapterModal
|
||||||
course={course ? course.courseStructure : null}
|
course={course ? course.courseStructure : null}
|
||||||
closeModal={closeNewChapterModal}
|
closeModal={closeNewChapterModal}
|
||||||
submitChapter={submitChapter}
|
submitChapter={submitChapter}
|
||||||
></NewChapterModal>}
|
></NewChapterModal>
|
||||||
|
}
|
||||||
dialogTitle="Create chapter"
|
dialogTitle="Create chapter"
|
||||||
dialogDescription="Add a new chapter to the course"
|
dialogDescription="Add a new chapter to the course"
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
<div className="w-44 my-16 py-5 max-w-screen-2xl mx-auto bg-cyan-800 text-white rounded-xl shadow-sm px-6 items-center flex flex-row h-10">
|
<div className="w-44 my-16 py-5 max-w-screen-2xl mx-auto bg-cyan-800 text-white rounded-xl shadow-sm px-6 items-center flex flex-row h-10">
|
||||||
<div className='mx-auto flex space-x-2 items-center hover:cursor-pointer'>
|
<div className="mx-auto flex space-x-2 items-center hover:cursor-pointer">
|
||||||
<Hexagon strokeWidth={3} size={16} className="text-white text-sm " />
|
<Hexagon
|
||||||
<div className='font-bold text-sm'>Add Chapter</div></div>
|
strokeWidth={3}
|
||||||
|
size={16}
|
||||||
|
className="text-white text-sm "
|
||||||
|
/>
|
||||||
|
<div className="font-bold text-sm">Add Chapter</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</DragDropContext>
|
</DragDropContext>
|
||||||
|
) : (
|
||||||
: <></>}
|
<></>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,77 +1,75 @@
|
||||||
"use client";
|
'use client'
|
||||||
import React, { useEffect, useState } from 'react'
|
import React, { useEffect, useState } from 'react'
|
||||||
import { Field, Form, Formik } from 'formik';
|
import { Field, Form, Formik } from 'formik'
|
||||||
import { updateOrganization, uploadOrganizationLogo } from '@services/settings/org';
|
import {
|
||||||
import { UploadCloud } from 'lucide-react';
|
updateOrganization,
|
||||||
import { revalidateTags } from '@services/utils/ts/requests';
|
uploadOrganizationLogo,
|
||||||
import { useRouter } from 'next/navigation';
|
} from '@services/settings/org'
|
||||||
import { useOrg } from '@components/Contexts/OrgContext';
|
import { UploadCloud } from 'lucide-react'
|
||||||
|
import { revalidateTags } from '@services/utils/ts/requests'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
|
||||||
interface OrganizationValues {
|
interface OrganizationValues {
|
||||||
name: string;
|
name: string
|
||||||
description: string;
|
description: string
|
||||||
slug: string;
|
slug: string
|
||||||
logo: string;
|
logo: string
|
||||||
email: string;
|
email: string
|
||||||
}
|
}
|
||||||
|
|
||||||
function OrgEditGeneral(props: any) {
|
function OrgEditGeneral(props: any) {
|
||||||
const [selectedFile, setSelectedFile] = useState<File | null>(null);
|
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
// ...
|
// ...
|
||||||
|
|
||||||
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
if (event.target.files && event.target.files.length > 0) {
|
if (event.target.files && event.target.files.length > 0) {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0]
|
||||||
setSelectedFile(file);
|
setSelectedFile(file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
const uploadLogo = async () => {
|
const uploadLogo = async () => {
|
||||||
if (selectedFile) {
|
if (selectedFile) {
|
||||||
let org_id = org.id;
|
let org_id = org.id
|
||||||
await uploadOrganizationLogo(org_id, selectedFile);
|
await uploadOrganizationLogo(org_id, selectedFile)
|
||||||
setSelectedFile(null); // Reset the selected file
|
setSelectedFile(null) // Reset the selected file
|
||||||
await revalidateTags(['organizations'], org.slug);
|
await revalidateTags(['organizations'], org.slug)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
let orgValues: OrganizationValues = {
|
let orgValues: OrganizationValues = {
|
||||||
name: org?.name,
|
name: org?.name,
|
||||||
description: org?.description,
|
description: org?.description,
|
||||||
slug: org?.slug,
|
slug: org?.slug,
|
||||||
logo: org?.logo,
|
logo: org?.logo,
|
||||||
email: org?.email
|
email: org?.email,
|
||||||
}
|
}
|
||||||
|
|
||||||
const updateOrg = async (values: OrganizationValues) => {
|
const updateOrg = async (values: OrganizationValues) => {
|
||||||
let org_id = org.id;
|
let org_id = org.id
|
||||||
await updateOrganization(org_id, values);
|
await updateOrganization(org_id, values)
|
||||||
|
|
||||||
// Mutate the org
|
// Mutate the org
|
||||||
await revalidateTags(['organizations'], org.slug);
|
await revalidateTags(['organizations'], org.slug)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {}, [org])
|
||||||
|
|
||||||
}
|
|
||||||
, [org])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">
|
||||||
<Formik
|
<Formik
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
initialValues={orgValues}
|
initialValues={orgValues}
|
||||||
onSubmit={(values, { setSubmitting }) => {
|
onSubmit={(values, { setSubmitting }) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false)
|
||||||
updateOrg(values)
|
updateOrg(values)
|
||||||
}, 400);
|
}, 400)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
|
|
@ -115,7 +113,6 @@ function OrgEditGeneral(props: any) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<label className="block mb-2 font-bold" htmlFor="slug">
|
<label className="block mb-2 font-bold" htmlFor="slug">
|
||||||
Slug
|
Slug
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -143,7 +140,6 @@ function OrgEditGeneral(props: any) {
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -9,23 +9,61 @@ type BreadCrumbsProps = {
|
||||||
}
|
}
|
||||||
|
|
||||||
function BreadCrumbs(props: BreadCrumbsProps) {
|
function BreadCrumbs(props: BreadCrumbsProps) {
|
||||||
const course = useCourse() as any;
|
const course = useCourse() as any
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className='h-7'></div>
|
<div className="h-7"></div>
|
||||||
<div className='text-gray-400 tracking-tight font-medium text-sm flex space-x-1'>
|
<div className="text-gray-400 tracking-tight font-medium text-sm flex space-x-1">
|
||||||
<div className='flex items-center space-x-1'>
|
<div className="flex items-center space-x-1">
|
||||||
{props.type == 'courses' ? <div className='flex space-x-2 items-center'> <Book className='text-gray' size={14}></Book><Link href='/dash/courses'>Courses</Link></div> : ''}
|
{props.type == 'courses' ? (
|
||||||
{props.type == 'user' ? <div className='flex space-x-2 items-center'> <User className='text-gray' size={14}></User><Link href='/dash/user-account/settings/general'>Account Settings</Link></div> : ''}
|
<div className="flex space-x-2 items-center">
|
||||||
{props.type == 'orgusers' ? <div className='flex space-x-2 items-center'> <Users className='text-gray' size={14}></Users><Link href='/dash/users/settings/users'>Organization users</Link></div> : ''}
|
{' '}
|
||||||
|
<Book className="text-gray" size={14}></Book>
|
||||||
|
<Link href="/dash/courses">Courses</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
{props.type == 'user' ? (
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
{' '}
|
||||||
|
<User className="text-gray" size={14}></User>
|
||||||
|
<Link href="/dash/user-account/settings/general">
|
||||||
|
Account Settings
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
{props.type == 'orgusers' ? (
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
{' '}
|
||||||
|
<Users className="text-gray" size={14}></Users>
|
||||||
|
<Link href="/dash/users/settings/users">Organization users</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
|
||||||
{props.type == 'org' ? <div className='flex space-x-2 items-center'> <School className='text-gray' size={14}></School><Link href='/dash/users'>Organization Settings</Link></div> : ''}
|
{props.type == 'org' ? (
|
||||||
<div className='flex items-center space-x-1 first-letter:uppercase'>
|
<div className="flex space-x-2 items-center">
|
||||||
|
{' '}
|
||||||
|
<School className="text-gray" size={14}></School>
|
||||||
|
<Link href="/dash/users">Organization Settings</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
''
|
||||||
|
)}
|
||||||
|
<div className="flex items-center space-x-1 first-letter:uppercase">
|
||||||
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
|
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
|
||||||
<div className='first-letter:uppercase'> {props.last_breadcrumb}</div>
|
<div className="first-letter:uppercase">
|
||||||
</div></div></div>
|
{' '}
|
||||||
|
{props.last_breadcrumb}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,43 +1,66 @@
|
||||||
import { useCourse } from "@components/Contexts/CourseContext";
|
import { useCourse } from '@components/Contexts/CourseContext'
|
||||||
import { useEffect } from "react";
|
import { useEffect } from 'react'
|
||||||
import BreadCrumbs from "./BreadCrumbs";
|
import BreadCrumbs from './BreadCrumbs'
|
||||||
import SaveState from "./SaveState";
|
import SaveState from './SaveState'
|
||||||
import { CourseOverviewParams } from "app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page";
|
import { CourseOverviewParams } from 'app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page'
|
||||||
import { getUriWithOrg } from "@services/config/config";
|
import { getUriWithOrg } from '@services/config/config'
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
import Image from "next/image";
|
import Image from 'next/image'
|
||||||
import EmptyThumbnailImage from '../../../public/empty_thumbnail.png';
|
import EmptyThumbnailImage from '../../../public/empty_thumbnail.png'
|
||||||
|
|
||||||
export function CourseOverviewTop({ params }: { params: CourseOverviewParams }) {
|
export function CourseOverviewTop({
|
||||||
const course = useCourse() as any;
|
params,
|
||||||
const org = useOrg() as any;
|
}: {
|
||||||
|
params: CourseOverviewParams
|
||||||
|
}) {
|
||||||
|
const course = useCourse() as any
|
||||||
|
const org = useOrg() as any
|
||||||
|
|
||||||
useEffect(() => { }
|
useEffect(() => {}, [course, org])
|
||||||
, [course, org])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<BreadCrumbs type='courses' last_breadcrumb={course.courseStructure.name} ></BreadCrumbs>
|
<BreadCrumbs
|
||||||
<div className='flex'>
|
type="courses"
|
||||||
<div className='flex py-5 grow items-center'>
|
last_breadcrumb={course.courseStructure.name}
|
||||||
<Link href={getUriWithOrg(org?.slug, "") + `/course/${params.courseuuid}`}>
|
></BreadCrumbs>
|
||||||
{course?.courseStructure?.thumbnail_image ?
|
<div className="flex">
|
||||||
<img className="w-[100px] h-[57px] rounded-md drop-shadow-md" src={`${getCourseThumbnailMediaDirectory(org?.org_uuid, "course_" + params.courseuuid, course.courseStructure.thumbnail_image)}`} alt="" />
|
<div className="flex py-5 grow items-center">
|
||||||
:
|
<Link
|
||||||
<Image width={100} className="h-[57px] rounded-md drop-shadow-md" src={EmptyThumbnailImage} alt="" />}
|
href={getUriWithOrg(org?.slug, '') + `/course/${params.courseuuid}`}
|
||||||
|
>
|
||||||
|
{course?.courseStructure?.thumbnail_image ? (
|
||||||
|
<img
|
||||||
|
className="w-[100px] h-[57px] rounded-md drop-shadow-md"
|
||||||
|
src={`${getCourseThumbnailMediaDirectory(
|
||||||
|
org?.org_uuid,
|
||||||
|
'course_' + params.courseuuid,
|
||||||
|
course.courseStructure.thumbnail_image
|
||||||
|
)}`}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Image
|
||||||
|
width={100}
|
||||||
|
className="h-[57px] rounded-md drop-shadow-md"
|
||||||
|
src={EmptyThumbnailImage}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
<div className="flex flex-col course_metadata justify-center pl-5">
|
<div className="flex flex-col course_metadata justify-center pl-5">
|
||||||
<div className='text-gray-400 font-semibold text-sm'>Course</div>
|
<div className="text-gray-400 font-semibold text-sm">Course</div>
|
||||||
<div className='text-black font-bold text-xl -mt-1 first-letter:uppercase'>{course.courseStructure.name}</div>
|
<div className="text-black font-bold text-xl -mt-1 first-letter:uppercase">
|
||||||
|
{course.courseStructure.name}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex items-center'>
|
</div>
|
||||||
|
<div className="flex items-center">
|
||||||
<SaveState orgslug={params.orgslug} />
|
<SaveState orgslug={params.orgslug} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -1,107 +1,172 @@
|
||||||
'use client';
|
'use client'
|
||||||
import { useOrg } from '@components/Contexts/OrgContext';
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import { useSession } from '@components/Contexts/SessionContext';
|
import { useSession } from '@components/Contexts/SessionContext'
|
||||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||||
import LearnHouseDashboardLogo from '@public/dashLogo.png';
|
import LearnHouseDashboardLogo from '@public/dashLogo.png'
|
||||||
import { logout } from '@services/auth/auth';
|
import { logout } from '@services/auth/auth'
|
||||||
import { BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react'
|
import { BookCopy, Home, LogOut, School, Settings, Users } from 'lucide-react'
|
||||||
import Image from 'next/image';
|
import Image from 'next/image'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import UserAvatar from '../../Objects/UserAvatar';
|
import UserAvatar from '../../Objects/UserAvatar'
|
||||||
import AdminAuthorization from '@components/Security/AdminAuthorization';
|
import AdminAuthorization from '@components/Security/AdminAuthorization'
|
||||||
|
|
||||||
function LeftMenu() {
|
function LeftMenu() {
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const session = useSession() as any;
|
const session = useSession() as any
|
||||||
const [loading, setLoading] = React.useState(true);
|
const [loading, setLoading] = React.useState(true)
|
||||||
const route = useRouter();
|
const route = useRouter()
|
||||||
|
|
||||||
function waitForEverythingToLoad() {
|
function waitForEverythingToLoad() {
|
||||||
if (org && session) {
|
if (org && session) {
|
||||||
return true;
|
return true
|
||||||
}
|
}
|
||||||
return false;
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logOutUI() {
|
async function logOutUI() {
|
||||||
const res = await logout();
|
const res = await logout()
|
||||||
if (res) {
|
if (res) {
|
||||||
route.push('/login');
|
route.push('/login')
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (waitForEverythingToLoad()) {
|
if (waitForEverythingToLoad()) {
|
||||||
setLoading(false);
|
setLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}, [loading])
|
||||||
, [loading])
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
style={{ background: "linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(271.56% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(20 19 19)" }}
|
style={{
|
||||||
className='flex flex-col w-[90px] bg-black h-screen text-white shadow-xl'>
|
background:
|
||||||
<div className='flex flex-col h-full'>
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(271.56% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(20 19 19)',
|
||||||
<div className='flex h-20 mt-6'>
|
}}
|
||||||
<Link className='flex flex-col items-center mx-auto space-y-3' href={"/"}>
|
className="flex flex-col w-[90px] bg-black h-screen text-white shadow-xl"
|
||||||
<ToolTip content={'Back to Home'} slateBlack sideOffset={8} side='right' >
|
>
|
||||||
<Image alt="Learnhouse logo" width={40} src={LearnHouseDashboardLogo} />
|
<div className="flex flex-col h-full">
|
||||||
|
<div className="flex h-20 mt-6">
|
||||||
|
<Link
|
||||||
|
className="flex flex-col items-center mx-auto space-y-3"
|
||||||
|
href={'/'}
|
||||||
|
>
|
||||||
|
<ToolTip
|
||||||
|
content={'Back to Home'}
|
||||||
|
slateBlack
|
||||||
|
sideOffset={8}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
alt="Learnhouse logo"
|
||||||
|
width={40}
|
||||||
|
src={LearnHouseDashboardLogo}
|
||||||
|
/>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
<ToolTip content={'Your Organization'} slateBlack sideOffset={8} side='right' >
|
<ToolTip
|
||||||
<div className='py-1 px-3 bg-black/40 opacity-40 rounded-md text-[10px] justify-center text-center'>{org?.name}</div>
|
content={'Your Organization'}
|
||||||
|
slateBlack
|
||||||
|
sideOffset={8}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
<div className="py-1 px-3 bg-black/40 opacity-40 rounded-md text-[10px] justify-center text-center">
|
||||||
|
{org?.name}
|
||||||
|
</div>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex grow flex-col justify-center space-y-5 items-center mx-auto'>
|
<div className="flex grow flex-col justify-center space-y-5 items-center mx-auto">
|
||||||
{/* <ToolTip content={"Back to " + org?.name + "'s Home"} slateBlack sideOffset={8} side='right' >
|
{/* <ToolTip content={"Back to " + org?.name + "'s Home"} slateBlack sideOffset={8} side='right' >
|
||||||
<Link className='bg-white text-black hover:text-white rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/`} ><ArrowLeft className='hover:text-white' size={18} /></Link>
|
<Link className='bg-white text-black hover:text-white rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/`} ><ArrowLeft className='hover:text-white' size={18} /></Link>
|
||||||
</ToolTip> */}
|
</ToolTip> */}
|
||||||
<AdminAuthorization authorizationMode="component">
|
<AdminAuthorization authorizationMode="component">
|
||||||
<ToolTip content={"Home"} slateBlack sideOffset={8} side='right' >
|
<ToolTip content={'Home'} slateBlack sideOffset={8} side="right">
|
||||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash`} ><Home size={18} /></Link>
|
<Link
|
||||||
|
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
|
||||||
|
href={`/dash`}
|
||||||
|
>
|
||||||
|
<Home size={18} />
|
||||||
|
</Link>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
<ToolTip content={"Courses"} slateBlack sideOffset={8} side='right' >
|
<ToolTip content={'Courses'} slateBlack sideOffset={8} side="right">
|
||||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/courses`} ><BookCopy size={18} /></Link>
|
<Link
|
||||||
|
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
|
||||||
|
href={`/dash/courses`}
|
||||||
|
>
|
||||||
|
<BookCopy size={18} />
|
||||||
|
</Link>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
<ToolTip content={"Users"} slateBlack sideOffset={8} side='right' >
|
<ToolTip content={'Users'} slateBlack sideOffset={8} side="right">
|
||||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/users/settings/users`} ><Users size={18} /></Link>
|
<Link
|
||||||
|
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
|
||||||
|
href={`/dash/users/settings/users`}
|
||||||
|
>
|
||||||
|
<Users size={18} />
|
||||||
|
</Link>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
<ToolTip content={"Organization"} slateBlack sideOffset={8} side='right' >
|
<ToolTip
|
||||||
<Link className='bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear' href={`/dash/org/settings/general`} ><School size={18} /></Link>
|
content={'Organization'}
|
||||||
|
slateBlack
|
||||||
|
sideOffset={8}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
className="bg-white/5 rounded-lg p-2 hover:bg-white/10 transition-all ease-linear"
|
||||||
|
href={`/dash/org/settings/general`}
|
||||||
|
>
|
||||||
|
<School size={18} />
|
||||||
|
</Link>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
</AdminAuthorization>
|
</AdminAuthorization>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex flex-col mx-auto pb-7 space-y-2'>
|
<div className="flex flex-col mx-auto pb-7 space-y-2">
|
||||||
|
|
||||||
<div className="flex items-center flex-col space-y-2">
|
<div className="flex items-center flex-col space-y-2">
|
||||||
<ToolTip content={'@' + session.user.username} slateBlack sideOffset={8} side='right' >
|
<ToolTip
|
||||||
<div className='mx-auto'>
|
content={'@' + session.user.username}
|
||||||
<UserAvatar border='border-4' width={35} />
|
slateBlack
|
||||||
|
sideOffset={8}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
<div className="mx-auto">
|
||||||
|
<UserAvatar border="border-4" width={35} />
|
||||||
</div>
|
</div>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
<div className='flex items-center flex-col space-y-1'>
|
<div className="flex items-center flex-col space-y-1">
|
||||||
<ToolTip content={session.user.username + "'s Settings"} slateBlack sideOffset={8} side='right' >
|
<ToolTip
|
||||||
<Link href={'/dash/user-account/settings/general'} className='py-3'>
|
content={session.user.username + "'s Settings"}
|
||||||
<Settings className='mx-auto text-neutral-400 cursor-pointer' size={18} />
|
slateBlack
|
||||||
|
sideOffset={8}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
href={'/dash/user-account/settings/general'}
|
||||||
|
className="py-3"
|
||||||
|
>
|
||||||
|
<Settings
|
||||||
|
className="mx-auto text-neutral-400 cursor-pointer"
|
||||||
|
size={18}
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
<ToolTip content={'Logout'} slateBlack sideOffset={8} side='right' >
|
<ToolTip
|
||||||
<LogOut onClick={() => logOutUI()} className='mx-auto text-neutral-400 cursor-pointer' size={14} />
|
content={'Logout'}
|
||||||
|
slateBlack
|
||||||
|
sideOffset={8}
|
||||||
|
side="right"
|
||||||
|
>
|
||||||
|
<LogOut
|
||||||
|
onClick={() => logOutUI()}
|
||||||
|
className="mx-auto text-neutral-400 cursor-pointer"
|
||||||
|
size={14}
|
||||||
|
/>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default LeftMenu
|
export default LeftMenu
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,113 +1,126 @@
|
||||||
'use client';
|
'use client'
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { updateCourseOrderStructure } from '@services/courses/chapters';
|
import { updateCourseOrderStructure } from '@services/courses/chapters'
|
||||||
import { revalidateTags } from '@services/utils/ts/requests';
|
import { revalidateTags } from '@services/utils/ts/requests'
|
||||||
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext'
|
import {
|
||||||
|
useCourse,
|
||||||
|
useCourseDispatch,
|
||||||
|
} from '@components/Contexts/CourseContext'
|
||||||
import { Check, SaveAllIcon, Timer } from 'lucide-react'
|
import { Check, SaveAllIcon, Timer } from 'lucide-react'
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { mutate } from 'swr';
|
import { mutate } from 'swr'
|
||||||
import { updateCourse } from '@services/courses/courses';
|
import { updateCourse } from '@services/courses/courses'
|
||||||
|
|
||||||
function SaveState(props: { orgslug: string }) {
|
function SaveState(props: { orgslug: string }) {
|
||||||
const course = useCourse() as any;
|
const course = useCourse() as any
|
||||||
const router = useRouter();
|
const router = useRouter()
|
||||||
const saved = course ? course.isSaved : true;
|
const saved = course ? course.isSaved : true
|
||||||
const dispatchCourse = useCourseDispatch() as any;
|
const dispatchCourse = useCourseDispatch() as any
|
||||||
const course_structure = course.courseStructure;
|
const course_structure = course.courseStructure
|
||||||
|
|
||||||
const saveCourseState = async () => {
|
const saveCourseState = async () => {
|
||||||
// Course order
|
// Course order
|
||||||
if (saved) return;
|
if (saved) return
|
||||||
await changeOrderBackend();
|
await changeOrderBackend()
|
||||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||||
// Course metadata
|
// Course metadata
|
||||||
await changeMetadataBackend();
|
await changeMetadataBackend()
|
||||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||||
await revalidateTags(['courses'], props.orgslug)
|
await revalidateTags(['courses'], props.orgslug)
|
||||||
dispatchCourse({ type: 'setIsSaved' })
|
dispatchCourse({ type: 'setIsSaved' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Course Order
|
// Course Order
|
||||||
const changeOrderBackend = async () => {
|
const changeOrderBackend = async () => {
|
||||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||||
await updateCourseOrderStructure(course.courseStructure.course_uuid, course.courseOrder);
|
await updateCourseOrderStructure(
|
||||||
|
course.courseStructure.course_uuid,
|
||||||
|
course.courseOrder
|
||||||
|
)
|
||||||
await revalidateTags(['courses'], props.orgslug)
|
await revalidateTags(['courses'], props.orgslug)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
dispatchCourse({ type: 'setIsSaved' })
|
dispatchCourse({ type: 'setIsSaved' })
|
||||||
}
|
}
|
||||||
|
|
||||||
// Course metadata
|
// Course metadata
|
||||||
const changeMetadataBackend = async () => {
|
const changeMetadataBackend = async () => {
|
||||||
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
|
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
|
||||||
await updateCourse(course.courseStructure.course_uuid, course.courseStructure);
|
await updateCourse(
|
||||||
|
course.courseStructure.course_uuid,
|
||||||
|
course.courseStructure
|
||||||
|
)
|
||||||
await revalidateTags(['courses'], props.orgslug)
|
await revalidateTags(['courses'], props.orgslug)
|
||||||
router.refresh();
|
router.refresh()
|
||||||
dispatchCourse({ type: 'setIsSaved' })
|
dispatchCourse({ type: 'setIsSaved' })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const handleCourseOrder = (course_structure: any) => {
|
const handleCourseOrder = (course_structure: any) => {
|
||||||
const chapters = course_structure.chapters;
|
const chapters = course_structure.chapters
|
||||||
const chapter_order_by_ids = chapters.map((chapter: any) => {
|
const chapter_order_by_ids = chapters.map((chapter: any) => {
|
||||||
return {
|
return {
|
||||||
chapter_id: chapter.id,
|
chapter_id: chapter.id,
|
||||||
activities_order_by_ids: chapter.activities.map((activity: any) => {
|
activities_order_by_ids: chapter.activities.map((activity: any) => {
|
||||||
return {
|
return {
|
||||||
activity_id: activity.id
|
activity_id: activity.id,
|
||||||
|
}
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
dispatchCourse({
|
||||||
|
type: 'setCourseOrder',
|
||||||
|
payload: { chapter_order_by_ids: chapter_order_by_ids },
|
||||||
})
|
})
|
||||||
dispatchCourse({ type: 'setCourseOrder', payload: { chapter_order_by_ids: chapter_order_by_ids } })
|
|
||||||
dispatchCourse({ type: 'setIsNotSaved' })
|
dispatchCourse({ type: 'setIsNotSaved' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const initOrderPayload = () => {
|
const initOrderPayload = () => {
|
||||||
if (course_structure && course_structure.chapters) {
|
if (course_structure && course_structure.chapters) {
|
||||||
handleCourseOrder(course_structure);
|
handleCourseOrder(course_structure)
|
||||||
dispatchCourse({ type: 'setIsSaved' })
|
dispatchCourse({ type: 'setIsSaved' })
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const changeOrderPayload = () => {
|
const changeOrderPayload = () => {
|
||||||
if (course_structure && course_structure.chapters) {
|
if (course_structure && course_structure.chapters) {
|
||||||
handleCourseOrder(course_structure);
|
handleCourseOrder(course_structure)
|
||||||
dispatchCourse({ type: 'setIsNotSaved' })
|
dispatchCourse({ type: 'setIsNotSaved' })
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (course_structure?.chapters) {
|
if (course_structure?.chapters) {
|
||||||
initOrderPayload();
|
initOrderPayload()
|
||||||
}
|
}
|
||||||
if (course_structure?.chapters && !saved) {
|
if (course_structure?.chapters && !saved) {
|
||||||
changeOrderPayload();
|
changeOrderPayload()
|
||||||
}
|
}
|
||||||
}, [course_structure]); // This effect depends on the `course_structure` variable
|
}, [course_structure]) // This effect depends on the `course_structure` variable
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex space-x-4'>
|
<div className="flex space-x-4">
|
||||||
{saved ? <></> : <div className='text-gray-600 flex space-x-2 items-center antialiased'>
|
{saved ? (
|
||||||
|
<></>
|
||||||
|
) : (
|
||||||
|
<div className="text-gray-600 flex space-x-2 items-center antialiased">
|
||||||
<Timer size={15} />
|
<Timer size={15} />
|
||||||
<div>
|
<div>Unsaved changes</div>
|
||||||
Unsaved changes
|
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
</div>}
|
<div
|
||||||
<div className={`px-4 py-2 rounded-lg drop-shadow-md cursor-pointer flex space-x-2 items-center font-bold antialiased transition-all ease-linear ` + (saved ? 'bg-gray-600 text-white' : 'bg-black text-white border hover:bg-gray-900 ')
|
className={
|
||||||
} onClick={saveCourseState}>
|
`px-4 py-2 rounded-lg drop-shadow-md cursor-pointer flex space-x-2 items-center font-bold antialiased transition-all ease-linear ` +
|
||||||
|
(saved
|
||||||
|
? 'bg-gray-600 text-white'
|
||||||
|
: 'bg-black text-white border hover:bg-gray-900 ')
|
||||||
|
}
|
||||||
|
onClick={saveCourseState}
|
||||||
|
>
|
||||||
{saved ? <Check size={20} /> : <SaveAllIcon size={20} />}
|
{saved ? <Check size={20} /> : <SaveAllIcon size={20} />}
|
||||||
{saved ? <div className=''>Saved</div> : <div className=''>Save</div>}
|
{saved ? <div className="">Saved</div> : <div className="">Save</div>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,40 +1,44 @@
|
||||||
import { updateProfile } from '@services/settings/profile';
|
import { updateProfile } from '@services/settings/profile'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import { Formik, Form, Field } from 'formik';
|
import { Formik, Form, Field } from 'formik'
|
||||||
import { useSession } from '@components/Contexts/SessionContext';
|
import { useSession } from '@components/Contexts/SessionContext'
|
||||||
import { ArrowBigUpDash, Check, FileWarning, Info, UploadCloud } from 'lucide-react';
|
import {
|
||||||
import UserAvatar from '@components/Objects/UserAvatar';
|
ArrowBigUpDash,
|
||||||
import { updateUserAvatar } from '@services/users/users';
|
Check,
|
||||||
|
FileWarning,
|
||||||
|
Info,
|
||||||
|
UploadCloud,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import UserAvatar from '@components/Objects/UserAvatar'
|
||||||
|
import { updateUserAvatar } from '@services/users/users'
|
||||||
|
|
||||||
function UserEditGeneral() {
|
function UserEditGeneral() {
|
||||||
const session = useSession() as any;
|
const session = useSession() as any
|
||||||
const [localAvatar, setLocalAvatar] = React.useState(null) as any;
|
const [localAvatar, setLocalAvatar] = React.useState(null) as any
|
||||||
const [isLoading, setIsLoading] = React.useState(false) as any;
|
const [isLoading, setIsLoading] = React.useState(false) as any
|
||||||
const [error, setError] = React.useState() as any;
|
const [error, setError] = React.useState() as any
|
||||||
const [success, setSuccess] = React.useState('') as any;
|
const [success, setSuccess] = React.useState('') as any
|
||||||
|
|
||||||
const handleFileChange = async (event: any) => {
|
const handleFileChange = async (event: any) => {
|
||||||
const file = event.target.files[0];
|
const file = event.target.files[0]
|
||||||
setLocalAvatar(file);
|
setLocalAvatar(file)
|
||||||
setIsLoading(true);
|
setIsLoading(true)
|
||||||
const res = await updateUserAvatar(session.user.user_uuid, file)
|
const res = await updateUserAvatar(session.user.user_uuid, file)
|
||||||
// wait for 1 second to show loading animation
|
// wait for 1 second to show loading animation
|
||||||
await new Promise(r => setTimeout(r, 1500));
|
await new Promise((r) => setTimeout(r, 1500))
|
||||||
if (res.success === false) {
|
if (res.success === false) {
|
||||||
setError(res.HTTPmessage);
|
setError(res.HTTPmessage)
|
||||||
} else {
|
} else {
|
||||||
setIsLoading(false);
|
setIsLoading(false)
|
||||||
setError('');
|
setError('')
|
||||||
setSuccess('Avatar Updated');
|
setSuccess('Avatar Updated')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {}, [session, session.user])
|
||||||
}
|
|
||||||
, [session, session.user])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">
|
||||||
{session.user && (
|
{session.user && (
|
||||||
<Formik
|
<Formik
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
|
|
@ -47,17 +51,14 @@ function UserEditGeneral() {
|
||||||
}}
|
}}
|
||||||
onSubmit={(values, { setSubmitting }) => {
|
onSubmit={(values, { setSubmitting }) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
setSubmitting(false)
|
||||||
setSubmitting(false);
|
|
||||||
updateProfile(values, session.user.id)
|
updateProfile(values, session.user.id)
|
||||||
}, 400);
|
}, 400)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
<div className='flex space-x-8'>
|
<div className="flex space-x-8">
|
||||||
|
|
||||||
<Form className="max-w-md">
|
<Form className="max-w-md">
|
||||||
|
|
||||||
<label className="block mb-2 font-bold" htmlFor="email">
|
<label className="block mb-2 font-bold" htmlFor="email">
|
||||||
Email
|
Email
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -104,7 +105,6 @@ function UserEditGeneral() {
|
||||||
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||||
type="bio"
|
type="bio"
|
||||||
name="bio"
|
name="bio"
|
||||||
|
|
||||||
/>
|
/>
|
||||||
<button
|
<button
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|
@ -114,63 +114,77 @@ function UserEditGeneral() {
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</Form>
|
</Form>
|
||||||
<div className='flex flex-col grow justify-center align-middle space-y-3'>
|
<div className="flex flex-col grow justify-center align-middle space-y-3">
|
||||||
<label className="flex mx-auto mb-2 font-bold " >
|
<label className="flex mx-auto mb-2 font-bold ">Avatar</label>
|
||||||
Avatar
|
|
||||||
</label>
|
|
||||||
{error && (
|
{error && (
|
||||||
<div className="flex justify-center mx-auto bg-red-200 rounded-md text-red-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
|
<div className="flex justify-center mx-auto bg-red-200 rounded-md text-red-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
|
||||||
<FileWarning size={16} className='mr-2' />
|
<FileWarning size={16} className="mr-2" />
|
||||||
<div className="text-sm font-semibold first-letter:uppercase">{error}</div>
|
<div className="text-sm font-semibold first-letter:uppercase">
|
||||||
|
{error}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{success && (
|
{success && (
|
||||||
<div className="flex justify-center mx-auto bg-green-200 rounded-md text-green-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
|
<div className="flex justify-center mx-auto bg-green-200 rounded-md text-green-950 space-x-1 px-4 items-center p-2 transition-all shadow-sm">
|
||||||
<Check size={16} className='mr-2' />
|
<Check size={16} className="mr-2" />
|
||||||
<div className="text-sm font-semibold first-letter:uppercase">{success}</div>
|
<div className="text-sm font-semibold first-letter:uppercase">
|
||||||
|
{success}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex flex-col space-y-3">
|
||||||
|
<div className="w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow mx-20">
|
||||||
<div className='w-auto bg-gray-50 rounded-xl outline outline-1 outline-gray-200 h-[200px] shadow mx-20'>
|
<div className="flex flex-col justify-center items-center mt-10">
|
||||||
|
|
||||||
<div className='flex flex-col justify-center items-center mt-10'>
|
|
||||||
|
|
||||||
{localAvatar ? (
|
{localAvatar ? (
|
||||||
<UserAvatar border='border-8' width={100} avatar_url={URL.createObjectURL(localAvatar)} />
|
<UserAvatar
|
||||||
|
border="border-8"
|
||||||
|
width={100}
|
||||||
|
avatar_url={URL.createObjectURL(localAvatar)}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UserAvatar border='border-8' width={100} />
|
<UserAvatar border="border-8" width={100} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{isLoading ? (<div className='flex justify-center items-center'>
|
{isLoading ? (
|
||||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
<div className="flex justify-center items-center">
|
||||||
<div
|
<input
|
||||||
className='font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
|
type="file"
|
||||||
>
|
id="fileInput"
|
||||||
<ArrowBigUpDash size={16} className='mr-2' />
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
|
<div className="font-bold animate-pulse antialiased items-center bg-green-200 text-gray text-sm rounded-md px-4 py-2 mt-4 flex">
|
||||||
|
<ArrowBigUpDash size={16} className="mr-2" />
|
||||||
<span>Uploading</span>
|
<span>Uploading</span>
|
||||||
</div>
|
</div>
|
||||||
</div>) : (
|
</div>
|
||||||
<div className='flex justify-center items-center'>
|
) : (
|
||||||
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
|
<div className="flex justify-center items-center">
|
||||||
|
<input
|
||||||
|
type="file"
|
||||||
|
id="fileInput"
|
||||||
|
style={{ display: 'none' }}
|
||||||
|
onChange={handleFileChange}
|
||||||
|
/>
|
||||||
<button
|
<button
|
||||||
className='font-bold antialiased items-center text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
|
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 py-2 mt-4 flex"
|
||||||
onClick={() => document.getElementById('fileInput')?.click()}
|
onClick={() =>
|
||||||
|
document.getElementById('fileInput')?.click()
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<UploadCloud size={16} className='mr-2' />
|
<UploadCloud size={16} className="mr-2" />
|
||||||
<span>Change Thumbnail</span>
|
<span>Change Thumbnail</span>
|
||||||
</button>
|
</button>
|
||||||
</div> )}
|
|
||||||
</div>
|
</div>
|
||||||
<div className='flex text-xs space-x-2 items-center text-gray-500 justify-center'>
|
)}
|
||||||
<Info size={13} /><p>Recommended size 100x100</p>
|
</div>
|
||||||
|
<div className="flex text-xs space-x-2 items-center text-gray-500 justify-center">
|
||||||
|
<Info size={13} />
|
||||||
|
<p>Recommended size 100x100</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,28 @@
|
||||||
import { useSession } from '@components/Contexts/SessionContext';
|
import { useSession } from '@components/Contexts/SessionContext'
|
||||||
import { updatePassword } from '@services/settings/password';
|
import { updatePassword } from '@services/settings/password'
|
||||||
import { Formik, Form, Field } from 'formik';
|
import { Formik, Form, Field } from 'formik'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
|
|
||||||
function UserEditPassword() {
|
function UserEditPassword() {
|
||||||
const session = useSession() as any;
|
const session = useSession() as any
|
||||||
|
|
||||||
const updatePasswordUI = async (values: any) => {
|
const updatePasswordUI = async (values: any) => {
|
||||||
let user_id = session.user.user_id;
|
let user_id = session.user.user_id
|
||||||
await updatePassword(user_id, values)
|
await updatePassword(user_id, values)
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {}, [session])
|
||||||
}
|
|
||||||
, [session])
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
|
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">
|
||||||
<Formik
|
<Formik
|
||||||
initialValues={{ old_password: '', new_password: '' }}
|
initialValues={{ old_password: '', new_password: '' }}
|
||||||
enableReinitialize
|
enableReinitialize
|
||||||
onSubmit={(values, { setSubmitting }) => {
|
onSubmit={(values, { setSubmitting }) => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
setSubmitting(false);
|
setSubmitting(false)
|
||||||
updatePasswordUI(values)
|
updatePasswordUI(values)
|
||||||
}, 400);
|
}, 400)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{({ isSubmitting }) => (
|
{({ isSubmitting }) => (
|
||||||
|
|
@ -56,7 +53,6 @@ function UserEditPassword() {
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</Formik>
|
</Formik>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,21 +1,28 @@
|
||||||
import { useOrg } from '@components/Contexts/OrgContext'
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||||
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
|
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import { Globe, Shield, X } from 'lucide-react'
|
import { Globe, Shield, X } from 'lucide-react'
|
||||||
import Link from 'next/link';
|
import Link from 'next/link'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import useSWR, { mutate } from 'swr';
|
import useSWR, { mutate } from 'swr'
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs'
|
||||||
import { changeSignupMechanism, createInviteCode, deleteInviteCode } from '@services/organizations/invites';
|
import {
|
||||||
import Toast from '@components/StyledElements/Toast/Toast';
|
changeSignupMechanism,
|
||||||
import toast from 'react-hot-toast';
|
createInviteCode,
|
||||||
import { useRouter } from 'next/navigation';
|
deleteInviteCode,
|
||||||
|
} from '@services/organizations/invites'
|
||||||
|
import Toast from '@components/StyledElements/Toast/Toast'
|
||||||
|
import toast from 'react-hot-toast'
|
||||||
|
import { useRouter } from 'next/navigation'
|
||||||
|
|
||||||
function OrgAccess() {
|
function OrgAccess() {
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const { data: invites } = useSWR(org ? `${getAPIUrl()}orgs/${org?.id}/invites` : null, swrFetcher);
|
const { data: invites } = useSWR(
|
||||||
|
org ? `${getAPIUrl()}orgs/${org?.id}/invites` : null,
|
||||||
|
swrFetcher
|
||||||
|
)
|
||||||
const [isLoading, setIsLoading] = React.useState(false)
|
const [isLoading, setIsLoading] = React.useState(false)
|
||||||
const [joinMethod, setJoinMethod] = React.useState('closed')
|
const [joinMethod, setJoinMethod] = React.useState('closed')
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
@ -24,8 +31,7 @@ function OrgAccess() {
|
||||||
if (org) {
|
if (org) {
|
||||||
if (org.config.config.GeneralConfig.users.signup_mechanism == 'open') {
|
if (org.config.config.GeneralConfig.users.signup_mechanism == 'open') {
|
||||||
setJoinMethod('open')
|
setJoinMethod('open')
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
setJoinMethod('inviteOnly')
|
setJoinMethod('inviteOnly')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -35,22 +41,18 @@ function OrgAccess() {
|
||||||
let res = await createInviteCode(org.id)
|
let res = await createInviteCode(org.id)
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteInvite(invite: any) {
|
async function deleteInvite(invite: any) {
|
||||||
let res = await deleteInviteCode(org.id, invite.invite_code_uuid)
|
let res = await deleteInviteCode(org.id, invite.invite_code_uuid)
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function changeJoinMethod(method: 'open' | 'inviteOnly') {
|
async function changeJoinMethod(method: 'open' | 'inviteOnly') {
|
||||||
|
|
@ -58,8 +60,7 @@ function OrgAccess() {
|
||||||
if (res.status == 200) {
|
if (res.status == 200) {
|
||||||
router.refresh()
|
router.refresh()
|
||||||
mutate(`${getAPIUrl()}orgs/slug/${org?.slug}`)
|
mutate(`${getAPIUrl()}orgs/slug/${org?.slug}`)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -69,92 +70,145 @@ function OrgAccess() {
|
||||||
getOrgJoinMethod()
|
getOrgJoinMethod()
|
||||||
setIsLoading(false)
|
setIsLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}, [org, invites])
|
||||||
, [org, invites])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<>
|
<>
|
||||||
<Toast></Toast>
|
<Toast></Toast>
|
||||||
{!isLoading ? (<>
|
{!isLoading ? (
|
||||||
|
<>
|
||||||
<div className="h-6"></div>
|
<div className="h-6"></div>
|
||||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 anit '>
|
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 anit ">
|
||||||
<div className='flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 '>
|
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 ">
|
||||||
<h1 className='font-bold text-xl text-gray-800'>Join method</h1>
|
<h1 className="font-bold text-xl text-gray-800">Join method</h1>
|
||||||
<h2 className='text-gray-500 text-md'> Choose how users can join your organization </h2>
|
<h2 className="text-gray-500 text-md">
|
||||||
|
{' '}
|
||||||
|
Choose how users can join your organization{' '}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex space-x-2 mx-auto'>
|
<div className="flex space-x-2 mx-auto">
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
confirmationButtonText='Change to open '
|
confirmationButtonText="Change to open "
|
||||||
confirmationMessage='Are you sure you want to change the signup mechanism to open ? This will allow users to join your organization freely.'
|
confirmationMessage="Are you sure you want to change the signup mechanism to open ? This will allow users to join your organization freely."
|
||||||
dialogTitle={'Change to open ?'}
|
dialogTitle={'Change to open ?'}
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
<div className='w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all'>
|
<div className="w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all">
|
||||||
{joinMethod == 'open' ? <div className='bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg'>Active</div> : null}
|
{joinMethod == 'open' ? (
|
||||||
<div className='flex flex-col space-y-1 justify-center items-center h-full'>
|
<div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg">
|
||||||
<Globe className='text-slate-400' size={40}></Globe>
|
Active
|
||||||
<div className='text-2xl text-slate-700 font-bold'>Open</div>
|
|
||||||
<div className='text-gray-400 text-center'>Users can join freely from the signup page</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
) : null}
|
||||||
functionToExecute={() => { changeJoinMethod('open') }}
|
<div className="flex flex-col space-y-1 justify-center items-center h-full">
|
||||||
status='info'
|
<Globe className="text-slate-400" size={40}></Globe>
|
||||||
|
<div className="text-2xl text-slate-700 font-bold">
|
||||||
|
Open
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-center">
|
||||||
|
Users can join freely from the signup page
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
functionToExecute={() => {
|
||||||
|
changeJoinMethod('open')
|
||||||
|
}}
|
||||||
|
status="info"
|
||||||
></ConfirmationModal>
|
></ConfirmationModal>
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
confirmationButtonText='Change to closed '
|
confirmationButtonText="Change to closed "
|
||||||
confirmationMessage='Are you sure you want to change the signup mechanism to closed ? This will allow users to join your organization only by invitation.'
|
confirmationMessage="Are you sure you want to change the signup mechanism to closed ? This will allow users to join your organization only by invitation."
|
||||||
dialogTitle={'Change to closed ?'}
|
dialogTitle={'Change to closed ?'}
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
<div className='w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all'>
|
<div className="w-full h-[160px] bg-slate-100 rounded-lg cursor-pointer hover:bg-slate-200 ease-linear transition-all">
|
||||||
{joinMethod == 'inviteOnly' ? <div className='bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg'>Active</div> : null}
|
{joinMethod == 'inviteOnly' ? (
|
||||||
<div className='flex flex-col space-y-1 justify-center items-center h-full'>
|
<div className="bg-green-200 text-green-600 font-bold w-fit my-3 mx-3 absolute text-sm px-3 py-1 rounded-lg">
|
||||||
<Shield className='text-slate-400' size={40}></Shield>
|
Active
|
||||||
<div className='text-2xl text-slate-700 font-bold'>Closed</div>
|
|
||||||
<div className='text-gray-400 text-center'>Users can join only by invitation</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
) : null}
|
||||||
functionToExecute={() => { changeJoinMethod('inviteOnly') }}
|
<div className="flex flex-col space-y-1 justify-center items-center h-full">
|
||||||
status='info'
|
<Shield className="text-slate-400" size={40}></Shield>
|
||||||
|
<div className="text-2xl text-slate-700 font-bold">
|
||||||
|
Closed
|
||||||
|
</div>
|
||||||
|
<div className="text-gray-400 text-center">
|
||||||
|
Users can join only by invitation
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
functionToExecute={() => {
|
||||||
|
changeJoinMethod('inviteOnly')
|
||||||
|
}}
|
||||||
|
status="info"
|
||||||
></ConfirmationModal>
|
></ConfirmationModal>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className={joinMethod == 'open' ? 'opacity-20 pointer-events-none' : 'pointer-events-auto'}>
|
<div
|
||||||
<div className='flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mt-3 mb-3 '>
|
className={
|
||||||
<h1 className='font-bold text-xl text-gray-800'>Invite codes</h1>
|
joinMethod == 'open'
|
||||||
<h2 className='text-gray-500 text-md'>Invite codes can be copied and used to join your organization </h2>
|
? 'opacity-20 pointer-events-none'
|
||||||
|
: 'pointer-events-auto'
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mt-3 mb-3 ">
|
||||||
|
<h1 className="font-bold text-xl text-gray-800">
|
||||||
|
Invite codes
|
||||||
|
</h1>
|
||||||
|
<h2 className="text-gray-500 text-md">
|
||||||
|
Invite codes can be copied and used to join your organization{' '}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
||||||
<thead className='bg-gray-100 text-gray-500 rounded-xl uppercase'>
|
<thead className="bg-gray-100 text-gray-500 rounded-xl uppercase">
|
||||||
<tr className='font-bolder text-sm'>
|
<tr className="font-bolder text-sm">
|
||||||
<th className='py-3 px-4'>Code</th>
|
<th className="py-3 px-4">Code</th>
|
||||||
<th className='py-3 px-4'>Signup link</th>
|
<th className="py-3 px-4">Signup link</th>
|
||||||
<th className='py-3 px-4'>Expiration date</th>
|
<th className="py-3 px-4">Expiration date</th>
|
||||||
<th className='py-3 px-4'>Actions</th>
|
<th className="py-3 px-4">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<>
|
<>
|
||||||
<tbody className='mt-5 bg-white rounded-md' >
|
<tbody className="mt-5 bg-white rounded-md">
|
||||||
{invites?.map((invite: any) => (
|
{invites?.map((invite: any) => (
|
||||||
<tr key={invite.invite_code_uuid} className='border-b border-gray-100 text-sm'>
|
<tr
|
||||||
<td className='py-3 px-4'>{invite.invite_code}</td>
|
key={invite.invite_code_uuid}
|
||||||
<td className='py-3 px-4 '>
|
className="border-b border-gray-100 text-sm"
|
||||||
<Link className='outline bg-gray-50 text-gray-600 px-2 py-1 rounded-md outline-gray-300 outline-dashed outline-1' target='_blank' href={getUriWithOrg(org?.slug, `/signup?inviteCode=${invite.invite_code}`)}>
|
>
|
||||||
{getUriWithOrg(org?.slug, `/signup?inviteCode=${invite.invite_code}`)}
|
<td className="py-3 px-4">{invite.invite_code}</td>
|
||||||
|
<td className="py-3 px-4 ">
|
||||||
|
<Link
|
||||||
|
className="outline bg-gray-50 text-gray-600 px-2 py-1 rounded-md outline-gray-300 outline-dashed outline-1"
|
||||||
|
target="_blank"
|
||||||
|
href={getUriWithOrg(
|
||||||
|
org?.slug,
|
||||||
|
`/signup?inviteCode=${invite.invite_code}`
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{getUriWithOrg(
|
||||||
|
org?.slug,
|
||||||
|
`/signup?inviteCode=${invite.invite_code}`
|
||||||
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
</td>
|
</td>
|
||||||
<td className='py-3 px-4'>{dayjs(invite.expiration_date).add(1, 'year').format('DD/MM/YYYY')} </td>
|
<td className="py-3 px-4">
|
||||||
<td className='py-3 px-4'>
|
{dayjs(invite.expiration_date)
|
||||||
|
.add(1, 'year')
|
||||||
|
.format('DD/MM/YYYY')}{' '}
|
||||||
|
</td>
|
||||||
|
<td className="py-3 px-4">
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
confirmationButtonText='Delete Code'
|
confirmationButtonText="Delete Code"
|
||||||
confirmationMessage='Are you sure you want remove this invite code ?'
|
confirmationMessage="Are you sure you want remove this invite code ?"
|
||||||
dialogTitle={'Delete code ?'}
|
dialogTitle={'Delete code ?'}
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
<button className='mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100'>
|
<button className="mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100">
|
||||||
<X className='w-4 h-4' />
|
<X className="w-4 h-4" />
|
||||||
<span> Delete code</span>
|
<span> Delete code</span>
|
||||||
</button>}
|
</button>
|
||||||
functionToExecute={() => { deleteInvite(invite) }}
|
}
|
||||||
status='warning'
|
functionToExecute={() => {
|
||||||
|
deleteInvite(invite)
|
||||||
|
}}
|
||||||
|
status="warning"
|
||||||
></ConfirmationModal>
|
></ConfirmationModal>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
@ -162,12 +216,19 @@ function OrgAccess() {
|
||||||
</tbody>
|
</tbody>
|
||||||
</>
|
</>
|
||||||
</table>
|
</table>
|
||||||
<button onClick={() => createInvite()} className='mt-3 mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100'>
|
<button
|
||||||
<Shield className='w-4 h-4' />
|
onClick={() => createInvite()}
|
||||||
|
className="mt-3 mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-green-700 rounded-md font-bold items-center text-sm text-green-100"
|
||||||
|
>
|
||||||
|
<Shield className="w-4 h-4" />
|
||||||
<span> Create invite code</span>
|
<span> Create invite code</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div></>) : <PageLoading />}
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<PageLoading />
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,37 @@
|
||||||
import { useOrg } from '@components/Contexts/OrgContext';
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import PageLoading from '@components/Objects/Loaders/PageLoading';
|
import PageLoading from '@components/Objects/Loaders/PageLoading'
|
||||||
import RolesUpdate from '@components/Objects/Modals/Dash/OrgUsers/RolesUpdate';
|
import RolesUpdate from '@components/Objects/Modals/Dash/OrgUsers/RolesUpdate'
|
||||||
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
|
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
|
||||||
import Modal from '@components/StyledElements/Modal/Modal';
|
import Modal from '@components/StyledElements/Modal/Modal'
|
||||||
import Toast from '@components/StyledElements/Toast/Toast';
|
import Toast from '@components/StyledElements/Toast/Toast'
|
||||||
import { getAPIUrl } from '@services/config/config';
|
import { getAPIUrl } from '@services/config/config'
|
||||||
import { removeUserFromOrg } from '@services/organizations/orgs';
|
import { removeUserFromOrg } from '@services/organizations/orgs'
|
||||||
import { swrFetcher } from '@services/utils/ts/requests';
|
import { swrFetcher } from '@services/utils/ts/requests'
|
||||||
import { KeyRound, LogOut } from 'lucide-react';
|
import { KeyRound, LogOut } from 'lucide-react'
|
||||||
import React, { useEffect } from 'react'
|
import React, { useEffect } from 'react'
|
||||||
import toast from 'react-hot-toast';
|
import toast from 'react-hot-toast'
|
||||||
import useSWR, { mutate } from 'swr';
|
import useSWR, { mutate } from 'swr'
|
||||||
|
|
||||||
function OrgUsers() {
|
function OrgUsers() {
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const { data: orgUsers } = useSWR(org ? `${getAPIUrl()}orgs/${org?.id}/users` : null, swrFetcher);
|
const { data: orgUsers } = useSWR(
|
||||||
const [rolesModal, setRolesModal] = React.useState(false);
|
org ? `${getAPIUrl()}orgs/${org?.id}/users` : null,
|
||||||
const [selectedUser, setSelectedUser] = React.useState(null) as any;
|
swrFetcher
|
||||||
const [isLoading, setIsLoading] = React.useState(true);
|
)
|
||||||
|
const [rolesModal, setRolesModal] = React.useState(false)
|
||||||
|
const [selectedUser, setSelectedUser] = React.useState(null) as any
|
||||||
|
const [isLoading, setIsLoading] = React.useState(true)
|
||||||
|
|
||||||
const handleRolesModal = (user_uuid: any) => {
|
const handleRolesModal = (user_uuid: any) => {
|
||||||
setSelectedUser(user_uuid);
|
setSelectedUser(user_uuid)
|
||||||
setRolesModal(!rolesModal);
|
setRolesModal(!rolesModal)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRemoveUser = async (user_id: any) => {
|
const handleRemoveUser = async (user_id: any) => {
|
||||||
const res = await removeUserFromOrg(org.id, user_id);
|
const res = await removeUserFromOrg(org.id, user_id)
|
||||||
if (res.status === 200) {
|
if (res.status === 200) {
|
||||||
await mutate(`${getAPIUrl()}orgs/${org.id}/users`);
|
await mutate(`${getAPIUrl()}orgs/${org.id}/users`)
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
toast.error('Error ' + res.status + ': ' + res.data.detail)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -43,76 +45,98 @@ function OrgUsers() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{isLoading ? <div><PageLoading /></div> :
|
{isLoading ? (
|
||||||
|
<div>
|
||||||
|
<PageLoading />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<>
|
<>
|
||||||
<Toast></Toast>
|
<Toast></Toast>
|
||||||
<div className="h-6"></div>
|
<div className="h-6"></div>
|
||||||
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 '>
|
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-4 py-4 ">
|
||||||
<div className='flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 '>
|
<div className="flex flex-col bg-gray-50 -space-y-1 px-5 py-3 rounded-md mb-3 ">
|
||||||
<h1 className='font-bold text-xl text-gray-800'>Active users</h1>
|
<h1 className="font-bold text-xl text-gray-800">Active users</h1>
|
||||||
<h2 className='text-gray-500 text-md'> Manage your organization users, assign roles and permissions </h2>
|
<h2 className="text-gray-500 text-md">
|
||||||
|
{' '}
|
||||||
|
Manage your organization users, assign roles and permissions{' '}
|
||||||
|
</h2>
|
||||||
</div>
|
</div>
|
||||||
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
<table className="table-auto w-full text-left whitespace-nowrap rounded-md overflow-hidden">
|
||||||
<thead className='bg-gray-100 text-gray-500 rounded-xl uppercase'>
|
<thead className="bg-gray-100 text-gray-500 rounded-xl uppercase">
|
||||||
<tr className='font-bolder text-sm'>
|
<tr className="font-bolder text-sm">
|
||||||
<th className='py-3 px-4'>User</th>
|
<th className="py-3 px-4">User</th>
|
||||||
<th className='py-3 px-4'>Role</th>
|
<th className="py-3 px-4">Role</th>
|
||||||
<th className='py-3 px-4'>Actions</th>
|
<th className="py-3 px-4">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<>
|
<>
|
||||||
<tbody className='mt-5 bg-white rounded-md' >
|
<tbody className="mt-5 bg-white rounded-md">
|
||||||
{orgUsers?.map((user: any) => (
|
{orgUsers?.map((user: any) => (
|
||||||
<tr key={user.user.id} className='border-b border-gray-200 border-dashed'>
|
<tr
|
||||||
<td className='py-3 px-4 flex space-x-2 items-center'>
|
key={user.user.id}
|
||||||
<span>{user.user.first_name + ' ' + user.user.last_name}</span>
|
className="border-b border-gray-200 border-dashed"
|
||||||
<span className='text-xs bg-neutral-100 p-1 px-2 rounded-full text-neutral-400 font-semibold'>@{user.user.username}</span>
|
>
|
||||||
|
<td className="py-3 px-4 flex space-x-2 items-center">
|
||||||
|
<span>
|
||||||
|
{user.user.first_name + ' ' + user.user.last_name}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs bg-neutral-100 p-1 px-2 rounded-full text-neutral-400 font-semibold">
|
||||||
|
@{user.user.username}
|
||||||
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td className='py-3 px-4'>{user.role.name}</td>
|
<td className="py-3 px-4">{user.role.name}</td>
|
||||||
<td className='py-3 px-4 flex space-x-2 items-end'>
|
<td className="py-3 px-4 flex space-x-2 items-end">
|
||||||
<Modal
|
<Modal
|
||||||
isDialogOpen={rolesModal && selectedUser === user.user.user_uuid}
|
isDialogOpen={
|
||||||
onOpenChange={() => handleRolesModal(user.user.user_uuid)}
|
rolesModal && selectedUser === user.user.user_uuid
|
||||||
|
}
|
||||||
|
onOpenChange={() =>
|
||||||
|
handleRolesModal(user.user.user_uuid)
|
||||||
|
}
|
||||||
minHeight="no-min"
|
minHeight="no-min"
|
||||||
dialogContent={
|
dialogContent={
|
||||||
<RolesUpdate
|
<RolesUpdate
|
||||||
alreadyAssignedRole={user.role.role_uuid}
|
alreadyAssignedRole={user.role.role_uuid}
|
||||||
setRolesModal={setRolesModal}
|
setRolesModal={setRolesModal}
|
||||||
user={user} />
|
user={user}
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
dialogTitle="Update Role"
|
dialogTitle="Update Role"
|
||||||
dialogDescription={"Update @" + user.user.username + "'s role"}
|
dialogDescription={
|
||||||
|
'Update @' + user.user.username + "'s role"
|
||||||
|
}
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
<button className='flex space-x-2 hover:cursor-pointer p-1 px-3 bg-yellow-700 rounded-md font-bold items-center text-sm text-yellow-100'>
|
<button className="flex space-x-2 hover:cursor-pointer p-1 px-3 bg-yellow-700 rounded-md font-bold items-center text-sm text-yellow-100">
|
||||||
<KeyRound className='w-4 h-4' />
|
<KeyRound className="w-4 h-4" />
|
||||||
<span> Edit Role</span>
|
<span> Edit Role</span>
|
||||||
</button>}
|
</button>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
<ConfirmationModal
|
<ConfirmationModal
|
||||||
confirmationButtonText='Remove User'
|
confirmationButtonText="Remove User"
|
||||||
confirmationMessage='Are you sure you want remove this user from the organization?'
|
confirmationMessage="Are you sure you want remove this user from the organization?"
|
||||||
dialogTitle={'Delete ' + user.user.username + ' ?'}
|
dialogTitle={'Delete ' + user.user.username + ' ?'}
|
||||||
dialogTrigger={
|
dialogTrigger={
|
||||||
<button className='mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100'>
|
<button className="mr-2 flex space-x-2 hover:cursor-pointer p-1 px-3 bg-rose-700 rounded-md font-bold items-center text-sm text-rose-100">
|
||||||
<LogOut className='w-4 h-4' />
|
<LogOut className="w-4 h-4" />
|
||||||
<span> Remove from organization</span>
|
<span> Remove from organization</span>
|
||||||
</button>}
|
</button>
|
||||||
functionToExecute={() => { handleRemoveUser(user.user.id) }}
|
}
|
||||||
status='warning'
|
functionToExecute={() => {
|
||||||
|
handleRemoveUser(user.user.id)
|
||||||
|
}}
|
||||||
|
status="warning"
|
||||||
></ConfirmationModal>
|
></ConfirmationModal>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
||||||
))}
|
))}
|
||||||
</tbody>
|
</tbody>
|
||||||
</>
|
</>
|
||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,143 +1,188 @@
|
||||||
import { useSession } from '@components/Contexts/SessionContext'
|
import { useSession } from '@components/Contexts/SessionContext'
|
||||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
import {
|
||||||
import { AlertTriangle, BadgeInfo, NotebookTabs } from 'lucide-react';
|
sendActivityAIChatMessage,
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
startActivityAIChatSession,
|
||||||
|
} from '@services/ai/ai'
|
||||||
|
import { AlertTriangle, BadgeInfo, NotebookTabs } from 'lucide-react'
|
||||||
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import { FlaskConical, MessageCircle, X } from 'lucide-react'
|
import { FlaskConical, MessageCircle, X } from 'lucide-react'
|
||||||
import Image from 'next/image';
|
import Image from 'next/image'
|
||||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
import learnhouseAI_icon from 'public/learnhouse_ai_simple.png'
|
||||||
import learnhouseAI_logo_black from "public/learnhouse_ai_black_logo.png";
|
import learnhouseAI_logo_black from 'public/learnhouse_ai_black_logo.png'
|
||||||
import React, { useEffect, useRef } from 'react'
|
import React, { useEffect, useRef } from 'react'
|
||||||
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
|
import {
|
||||||
import useGetAIFeatures from '../../../AI/Hooks/useGetAIFeatures';
|
AIChatBotStateTypes,
|
||||||
import UserAvatar from '@components/Objects/UserAvatar';
|
useAIChatBot,
|
||||||
|
useAIChatBotDispatch,
|
||||||
|
} from '@components/Contexts/AI/AIChatBotContext'
|
||||||
|
import useGetAIFeatures from '../../../AI/Hooks/useGetAIFeatures'
|
||||||
|
import UserAvatar from '@components/Objects/UserAvatar'
|
||||||
|
|
||||||
type AIActivityAskProps = {
|
type AIActivityAskProps = {
|
||||||
activity: any;
|
activity: any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function AIActivityAsk(props: AIActivityAskProps) {
|
function AIActivityAsk(props: AIActivityAskProps) {
|
||||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' });
|
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' })
|
||||||
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false);
|
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false)
|
||||||
const dispatchAIChatBot = useAIChatBotDispatch() as any;
|
const dispatchAIChatBot = useAIChatBotDispatch() as any
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (is_ai_feature_enabled) {
|
if (is_ai_feature_enabled) {
|
||||||
setIsButtonAvailable(true);
|
setIsButtonAvailable(true)
|
||||||
}
|
}
|
||||||
}
|
}, [is_ai_feature_enabled])
|
||||||
, [is_ai_feature_enabled]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isButtonAvailable && (
|
{isButtonAvailable && (
|
||||||
<div >
|
<div>
|
||||||
<ActivityChatMessageBox activity={props.activity} />
|
<ActivityChatMessageBox activity={props.activity} />
|
||||||
<div
|
<div
|
||||||
onClick={() => dispatchAIChatBot({ type: 'setIsModalOpen' })}
|
onClick={() => dispatchAIChatBot({ type: 'setIsModalOpen' })}
|
||||||
style={{
|
style={{
|
||||||
background: 'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
|
background:
|
||||||
|
'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
|
||||||
}}
|
}}
|
||||||
className="rounded-full px-5 drop-shadow-md flex items-center space-x-1.5 p-2.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105">
|
className="rounded-full px-5 drop-shadow-md flex items-center space-x-1.5 p-2.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105"
|
||||||
{" "}
|
>
|
||||||
|
{' '}
|
||||||
<i>
|
<i>
|
||||||
<Image className='outline outline-1 outline-neutral-200/20 rounded-md' width={20} src={learnhouseAI_icon} alt="" />
|
<Image
|
||||||
</i>{" "}
|
className="outline outline-1 outline-neutral-200/20 rounded-md"
|
||||||
|
width={20}
|
||||||
|
src={learnhouseAI_icon}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</i>{' '}
|
||||||
<i className="not-italic text-xs font-bold">Ask AI</i>
|
<i className="not-italic text-xs font-bold">Ask AI</i>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AIMessage = {
|
export type AIMessage = {
|
||||||
sender: string;
|
sender: string
|
||||||
message: any;
|
message: any
|
||||||
type: 'ai' | 'user';
|
type: 'ai' | 'user'
|
||||||
}
|
}
|
||||||
|
|
||||||
type ActivityChatMessageBoxProps = {
|
type ActivityChatMessageBoxProps = {
|
||||||
activity: any;
|
activity: any
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
|
function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
|
||||||
const session = useSession() as any;
|
const session = useSession() as any
|
||||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
|
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
|
||||||
const dispatchAIChatBot = useAIChatBotDispatch() as any;
|
const dispatchAIChatBot = useAIChatBotDispatch() as any
|
||||||
|
|
||||||
// TODO : come up with a better way to handle this
|
// TODO : come up with a better way to handle this
|
||||||
const inputClass = aiChatBotState.isWaitingForResponse
|
const inputClass = aiChatBotState.isWaitingForResponse
|
||||||
? 'ring-1 ring-inset ring-white/10 bg-gray-950/40 w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30 opacity-30 '
|
? 'ring-1 ring-inset ring-white/10 bg-gray-950/40 w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30 opacity-30 '
|
||||||
: 'ring-1 ring-inset ring-white/10 bg-gray-950/40 w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30';
|
: 'ring-1 ring-inset ring-white/10 bg-gray-950/40 w-full rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30'
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (aiChatBotState.isModalOpen) {
|
if (aiChatBotState.isModalOpen) {
|
||||||
document.body.style.overflow = 'hidden';
|
document.body.style.overflow = 'hidden'
|
||||||
} else {
|
} else {
|
||||||
document.body.style.overflow = 'unset';
|
document.body.style.overflow = 'unset'
|
||||||
}
|
}
|
||||||
}, [aiChatBotState.isModalOpen]);
|
}, [aiChatBotState.isModalOpen])
|
||||||
|
|
||||||
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
// Perform the sending action here
|
// Perform the sending action here
|
||||||
sendMessage(event.currentTarget.value);
|
sendMessage(event.currentTarget.value)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: event.currentTarget.value });
|
await dispatchAIChatBot({
|
||||||
|
type: 'setChatInputValue',
|
||||||
|
payload: event.currentTarget.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendMessage = async (message: string) => {
|
const sendMessage = async (message: string) => {
|
||||||
if (aiChatBotState.aichat_uuid) {
|
if (aiChatBotState.aichat_uuid) {
|
||||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
await dispatchAIChatBot({
|
||||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
type: 'addMessage',
|
||||||
const response = await sendActivityAIChatMessage(message, aiChatBotState.aichat_uuid, props.activity.activity_uuid)
|
payload: { sender: 'user', message: message, type: 'user' },
|
||||||
|
})
|
||||||
|
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
|
||||||
|
const response = await sendActivityAIChatMessage(
|
||||||
|
message,
|
||||||
|
aiChatBotState.aichat_uuid,
|
||||||
|
props.activity.activity_uuid
|
||||||
|
)
|
||||||
if (response.success == false) {
|
if (response.success == false) {
|
||||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
await dispatchAIChatBot({
|
||||||
return;
|
type: 'setError',
|
||||||
|
payload: {
|
||||||
|
isError: true,
|
||||||
|
status: response.status,
|
||||||
|
error_message: response.data.detail,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
await dispatchAIChatBot({
|
||||||
|
type: 'addMessage',
|
||||||
|
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
await dispatchAIChatBot({
|
||||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
type: 'addMessage',
|
||||||
const response = await startActivityAIChatSession(message, props.activity.activity_uuid)
|
payload: { sender: 'user', message: message, type: 'user' },
|
||||||
|
})
|
||||||
|
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
|
||||||
|
const response = await startActivityAIChatSession(
|
||||||
|
message,
|
||||||
|
props.activity.activity_uuid
|
||||||
|
)
|
||||||
if (response.success == false) {
|
if (response.success == false) {
|
||||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
await dispatchAIChatBot({
|
||||||
return;
|
type: 'setError',
|
||||||
|
payload: {
|
||||||
|
isError: true,
|
||||||
|
status: response.status,
|
||||||
|
error_message: response.data.detail,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
await dispatchAIChatBot({ type: 'setAichat_uuid', payload: response.data.aichat_uuid });
|
await dispatchAIChatBot({
|
||||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
type: 'setAichat_uuid',
|
||||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
payload: response.data.aichat_uuid,
|
||||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
})
|
||||||
|
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
|
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||||
|
await dispatchAIChatBot({
|
||||||
|
type: 'addMessage',
|
||||||
|
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
dispatchAIChatBot({ type: 'setIsModalClose' });
|
dispatchAIChatBot({ type: 'setIsModalClose' })
|
||||||
}
|
}
|
||||||
|
|
||||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
const messagesEndRef = useRef<HTMLDivElement>(null)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesEndRef.current) {
|
if (messagesEndRef.current) {
|
||||||
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
|
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
|
||||||
}
|
}
|
||||||
|
}, [aiChatBotState.messages, session])
|
||||||
}, [aiChatBotState.messages, session]);
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
|
|
@ -147,68 +192,126 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
|
||||||
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
|
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
|
||||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||||
exit={{ y: 50, opacity: 0, filter: 'blur(25px)' }}
|
exit={{ y: 50, opacity: 0, filter: 'blur(25px)' }}
|
||||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2 }}
|
transition={{
|
||||||
className='fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center '
|
type: 'spring',
|
||||||
|
bounce: 0.35,
|
||||||
|
duration: 1.7,
|
||||||
|
mass: 0.2,
|
||||||
|
velocity: 2,
|
||||||
|
}}
|
||||||
|
className="fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center "
|
||||||
style={{ pointerEvents: 'none' }}
|
style={{ pointerEvents: 'none' }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(2 1 25 / 98%)'
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(2 1 25 / 98%)',
|
||||||
}}
|
}}
|
||||||
className="bg-black z-50 rounded-2xl max-w-screen-2xl w-10/12 my-10 mx-auto h-[350px] fixed bottom-0 left-1/2 transform -translate-x-1/2 shadow-lg ring-1 ring-inset ring-white/10 text-white p-4 flex-col-reverse backdrop-blur-md">
|
className="bg-black z-50 rounded-2xl max-w-screen-2xl w-10/12 my-10 mx-auto h-[350px] fixed bottom-0 left-1/2 transform -translate-x-1/2 shadow-lg ring-1 ring-inset ring-white/10 text-white p-4 flex-col-reverse backdrop-blur-md"
|
||||||
<div className='flex flex-row-reverse pb-3 justify-between items-center'>
|
>
|
||||||
<div className='flex space-x-2 items-center'>
|
<div className="flex flex-row-reverse pb-3 justify-between items-center">
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
<X size={20} className='text-white/50 hover:cursor-pointer bg-white/10 p-1 rounded-full items-center' onClick={closeModal} />
|
<X
|
||||||
|
size={20}
|
||||||
|
className="text-white/50 hover:cursor-pointer bg-white/10 p-1 rounded-full items-center"
|
||||||
|
onClick={closeModal}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={`flex space-x-2 items-center -ml-[100px] ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''}`}>
|
<div
|
||||||
<Image className={`outline outline-1 outline-neutral-200/20 rounded-lg ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''}`} width={24} src={learnhouseAI_icon} alt="" />
|
className={`flex space-x-2 items-center -ml-[100px] ${
|
||||||
<span className='text-sm font-semibold text-white/70'> AI</span>
|
aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
className={`outline outline-1 outline-neutral-200/20 rounded-lg ${
|
||||||
|
aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
|
||||||
|
}`}
|
||||||
|
width={24}
|
||||||
|
src={learnhouseAI_icon}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<span className="text-sm font-semibold text-white/70">
|
||||||
|
{' '}
|
||||||
|
AI
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className='bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center'>
|
<div className="bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center">
|
||||||
<FlaskConical size={14} />
|
<FlaskConical size={14} />
|
||||||
<span className='text-xs font-semibold antialiased '>Experimental</span>
|
<span className="text-xs font-semibold antialiased ">
|
||||||
|
Experimental
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div className={`w-100 h-0.5 bg-white/5 rounded-full mx-auto mb-3 ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''}`}></div>
|
<div
|
||||||
{aiChatBotState.messages.length > 0 && !aiChatBotState.error.isError ? (
|
className={`w-100 h-0.5 bg-white/5 rounded-full mx-auto mb-3 ${
|
||||||
<div className='flex-col h-[237px] w-full space-y-4 overflow-scroll scrollbar-w-2 scrollbar scrollbar-thumb-white/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full'>
|
aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
|
||||||
{aiChatBotState.messages.map((message: AIMessage, index: number) => {
|
}`}
|
||||||
|
></div>
|
||||||
|
{aiChatBotState.messages.length > 0 &&
|
||||||
|
!aiChatBotState.error.isError ? (
|
||||||
|
<div className="flex-col h-[237px] w-full space-y-4 overflow-scroll scrollbar-w-2 scrollbar scrollbar-thumb-white/20 scrollbar-thumb-rounded-full scrollbar-track-rounded-full">
|
||||||
|
{aiChatBotState.messages.map(
|
||||||
|
(message: AIMessage, index: number) => {
|
||||||
return (
|
return (
|
||||||
<AIMessage key={index} message={message} animated={message.sender == 'ai' ? true : false} />
|
<AIMessage
|
||||||
|
key={index}
|
||||||
|
message={message}
|
||||||
|
animated={message.sender == 'ai' ? true : false}
|
||||||
|
/>
|
||||||
)
|
)
|
||||||
})}
|
}
|
||||||
|
)}
|
||||||
<div ref={messagesEndRef} />
|
<div ref={messagesEndRef} />
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<AIMessagePlaceHolder sendMessage={sendMessage} activity_uuid={props.activity.activity_uuid} />
|
<AIMessagePlaceHolder
|
||||||
|
sendMessage={sendMessage}
|
||||||
|
activity_uuid={props.activity.activity_uuid}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
{aiChatBotState.error.isError && (
|
{aiChatBotState.error.isError && (
|
||||||
<div className='flex items-center h-[237px]'>
|
<div className="flex items-center h-[237px]">
|
||||||
<div className='flex flex-col mx-auto w-[600px] space-y-2 p-5 rounded-lg bg-red-500/20 outline outline-1 outline-red-500'>
|
<div className="flex flex-col mx-auto w-[600px] space-y-2 p-5 rounded-lg bg-red-500/20 outline outline-1 outline-red-500">
|
||||||
<AlertTriangle size={20} className='text-red-500' />
|
<AlertTriangle size={20} className="text-red-500" />
|
||||||
<div className='flex flex-col'>
|
<div className="flex flex-col">
|
||||||
<h3 className='font-semibold text-red-200'>Something wrong happened</h3>
|
<h3 className="font-semibold text-red-200">
|
||||||
<span className='text-red-100 text-sm '>{aiChatBotState.error.error_message}</span>
|
Something wrong happened
|
||||||
|
</h3>
|
||||||
|
<span className="text-red-100 text-sm ">
|
||||||
|
{aiChatBotState.error.error_message}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
)
|
<div className="flex space-x-2 items-center">
|
||||||
}
|
<div className="">
|
||||||
<div className='flex space-x-2 items-center'>
|
<UserAvatar
|
||||||
<div className=''>
|
rounded="rounded-lg"
|
||||||
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
|
border="border-2"
|
||||||
|
width={35}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full'>
|
<div className="w-full">
|
||||||
<input onKeyDown={handleKeyDown} onChange={handleChange} disabled={aiChatBotState.isWaitingForResponse} value={aiChatBotState.chatInputValue} placeholder='Ask AI About this Lecture' type="text" className={inputClass} name="" id="" />
|
<input
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
onChange={handleChange}
|
||||||
|
disabled={aiChatBotState.isWaitingForResponse}
|
||||||
|
value={aiChatBotState.chatInputValue}
|
||||||
|
placeholder="Ask AI About this Lecture"
|
||||||
|
type="text"
|
||||||
|
className={inputClass}
|
||||||
|
name=""
|
||||||
|
id=""
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className=''>
|
<div className="">
|
||||||
<MessageCircle size={20} className='text-white/50 hover:cursor-pointer' onClick={() => sendMessage(aiChatBotState.chatInputValue)} />
|
<MessageCircle
|
||||||
|
size={20}
|
||||||
|
className="text-white/50 hover:cursor-pointer"
|
||||||
|
onClick={() => sendMessage(aiChatBotState.chatInputValue)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -220,33 +323,45 @@ function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
|
||||||
}
|
}
|
||||||
|
|
||||||
type AIMessageProps = {
|
type AIMessageProps = {
|
||||||
message: AIMessage;
|
message: AIMessage
|
||||||
animated: boolean;
|
animated: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
function AIMessage(props: AIMessageProps) {
|
function AIMessage(props: AIMessageProps) {
|
||||||
const session = useSession() as any;
|
const session = useSession() as any
|
||||||
|
|
||||||
const words = props.message.message.split(' ');
|
const words = props.message.message.split(' ')
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className='flex space-x-2 w-full antialiased font-medium'>
|
<div className="flex space-x-2 w-full antialiased font-medium">
|
||||||
<div className=''>
|
<div className="">
|
||||||
{props.message.sender == 'ai' ? (
|
{props.message.sender == 'ai' ? (
|
||||||
<UserAvatar rounded='rounded-lg' border='border-2' predefined_avatar='ai' width={35} />
|
<UserAvatar
|
||||||
|
rounded="rounded-lg"
|
||||||
|
border="border-2"
|
||||||
|
predefined_avatar="ai"
|
||||||
|
width={35}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
|
<UserAvatar rounded="rounded-lg" border="border-2" width={35} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className='w-full'>
|
<div className="w-full">
|
||||||
<p className='w-full rounded-lg outline-none px-2 py-1 text-white text-md placeholder:text-white/30' id="">
|
<p
|
||||||
|
className="w-full rounded-lg outline-none px-2 py-1 text-white text-md placeholder:text-white/30"
|
||||||
|
id=""
|
||||||
|
>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{words.map((word: string, i: number) => (
|
{words.map((word: string, i: number) => (
|
||||||
<motion.span
|
<motion.span
|
||||||
key={i}
|
key={i}
|
||||||
initial={props.animated ? { opacity: 0, y: -10 } : { opacity: 1, y: 0 }}
|
initial={
|
||||||
|
props.animated ? { opacity: 0, y: -10 } : { opacity: 1, y: 0 }
|
||||||
|
}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={props.animated ? { opacity: 0, y: 10 } : { opacity: 1, y: 0 }}
|
exit={
|
||||||
|
props.animated ? { opacity: 0, y: 10 } : { opacity: 1, y: 0 }
|
||||||
|
}
|
||||||
transition={props.animated ? { delay: i * 0.1 } : {}}
|
transition={props.animated ? { delay: i * 0.1 } : {}}
|
||||||
>
|
>
|
||||||
{word + ' '}
|
{word + ' '}
|
||||||
|
|
@ -259,27 +374,41 @@ function AIMessage(props: AIMessageProps) {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const AIMessagePlaceHolder = (props: { activity_uuid: string, sendMessage: any }) => {
|
const AIMessagePlaceHolder = (props: {
|
||||||
const session = useSession() as any;
|
activity_uuid: string
|
||||||
const [feedbackModal, setFeedbackModal] = React.useState(false);
|
sendMessage: any
|
||||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
|
}) => {
|
||||||
|
const session = useSession() as any
|
||||||
|
const [feedbackModal, setFeedbackModal] = React.useState(false)
|
||||||
|
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
|
||||||
|
|
||||||
if (!aiChatBotState.error.isError) {
|
if (!aiChatBotState.error.isError) {
|
||||||
return <div className='flex-col h-[237px] w-full'>
|
return (
|
||||||
<div className='flex flex-col text-center justify-center pt-12'>
|
<div className="flex-col h-[237px] w-full">
|
||||||
|
<div className="flex flex-col text-center justify-center pt-12">
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
|
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
|
||||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||||
exit={{ y: 50, opacity: 0, }}
|
exit={{ y: 50, opacity: 0 }}
|
||||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2, delay: 0.17 }}
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
|
bounce: 0.35,
|
||||||
|
duration: 1.7,
|
||||||
|
mass: 0.2,
|
||||||
|
velocity: 2,
|
||||||
|
delay: 0.17,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
|
<Image
|
||||||
<Image width={100} className='mx-auto' src={learnhouseAI_logo_black} alt="" />
|
width={100}
|
||||||
<p className='pt-3 text-2xl font-semibold text-white/70 flex justify-center space-x-2 items-center'>
|
className="mx-auto"
|
||||||
<span className='items-center'>Hello</span>
|
src={learnhouseAI_logo_black}
|
||||||
<span className='capitalize flex space-x-2 items-center'>
|
alt=""
|
||||||
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
|
/>
|
||||||
|
<p className="pt-3 text-2xl font-semibold text-white/70 flex justify-center space-x-2 items-center">
|
||||||
|
<span className="items-center">Hello</span>
|
||||||
|
<span className="capitalize flex space-x-2 items-center">
|
||||||
|
<UserAvatar rounded="rounded-lg" border="border-2" width={35} />
|
||||||
<span>{session.user.username},</span>
|
<span>{session.user.username},</span>
|
||||||
</span>
|
</span>
|
||||||
<span>how can we help today ?</span>
|
<span>how can we help today ?</span>
|
||||||
|
|
@ -288,21 +417,40 @@ const AIMessagePlaceHolder = (props: { activity_uuid: string, sendMessage: any }
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
|
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
|
||||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||||
exit={{ y: 50, opacity: 0, }}
|
exit={{ y: 50, opacity: 0 }}
|
||||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2, delay: 0.27 }}
|
transition={{
|
||||||
|
type: 'spring',
|
||||||
className='questions flex space-x-3 mx-auto pt-6 flex-wrap justify-center'
|
bounce: 0.35,
|
||||||
|
duration: 1.7,
|
||||||
|
mass: 0.2,
|
||||||
|
velocity: 2,
|
||||||
|
delay: 0.27,
|
||||||
|
}}
|
||||||
|
className="questions flex space-x-3 mx-auto pt-6 flex-wrap justify-center"
|
||||||
>
|
>
|
||||||
<AIChatPredefinedQuestion sendMessage={props.sendMessage} label='about' />
|
<AIChatPredefinedQuestion
|
||||||
<AIChatPredefinedQuestion sendMessage={props.sendMessage} label='flashcards' />
|
sendMessage={props.sendMessage}
|
||||||
<AIChatPredefinedQuestion sendMessage={props.sendMessage} label='examples' />
|
label="about"
|
||||||
|
/>
|
||||||
|
<AIChatPredefinedQuestion
|
||||||
|
sendMessage={props.sendMessage}
|
||||||
|
label="flashcards"
|
||||||
|
/>
|
||||||
|
<AIChatPredefinedQuestion
|
||||||
|
sendMessage={props.sendMessage}
|
||||||
|
label="examples"
|
||||||
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const AIChatPredefinedQuestion = (props: { sendMessage: any, label: string }) => {
|
const AIChatPredefinedQuestion = (props: {
|
||||||
|
sendMessage: any
|
||||||
|
label: string
|
||||||
|
}) => {
|
||||||
function getQuestion(label: string) {
|
function getQuestion(label: string) {
|
||||||
if (label === 'about') {
|
if (label === 'about') {
|
||||||
return `What is this Activity about ?`
|
return `What is this Activity about ?`
|
||||||
|
|
@ -314,14 +462,16 @@ const AIChatPredefinedQuestion = (props: { sendMessage: any, label: string }) =>
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={() => props.sendMessage(getQuestion(props.label))} className='flex space-x-1.5 items-center bg-white/5 cursor-pointer px-4 py-1.5 rounded-xl outline outline-1 outline-neutral-100/10 text-xs font-semibold text-white/40 hover:text-white/60 hover:bg-white/10 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
<div
|
||||||
|
onClick={() => props.sendMessage(getQuestion(props.label))}
|
||||||
|
className="flex space-x-1.5 items-center bg-white/5 cursor-pointer px-4 py-1.5 rounded-xl outline outline-1 outline-neutral-100/10 text-xs font-semibold text-white/40 hover:text-white/60 hover:bg-white/10 hover:outline-neutral-200/40 delay-75 ease-linear transition-all"
|
||||||
|
>
|
||||||
{props.label === 'about' && <BadgeInfo size={15} />}
|
{props.label === 'about' && <BadgeInfo size={15} />}
|
||||||
{props.label === 'flashcards' && <NotebookTabs size={15} />}
|
{props.label === 'flashcards' && <NotebookTabs size={15} />}
|
||||||
{props.label === 'examples' && <div className='text-white/50'>Ex</div>}
|
{props.label === 'examples' && <div className="text-white/50">Ex</div>}
|
||||||
<span>{getQuestion(props.label)}</span>
|
<span>{getQuestion(props.label)}</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default AIActivityAsk
|
export default AIActivityAsk
|
||||||
|
|
@ -1,24 +1,34 @@
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import { getActivityMediaDirectory } from "@services/media/media";
|
import { getActivityMediaDirectory } from '@services/media/media'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
|
|
||||||
function DocumentPdfActivity({ activity, course }: { activity: any; course: any }) {
|
function DocumentPdfActivity({
|
||||||
const org = useOrg() as any;
|
activity,
|
||||||
|
course,
|
||||||
|
}: {
|
||||||
|
activity: any
|
||||||
|
course: any
|
||||||
|
}) {
|
||||||
|
const org = useOrg() as any
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log(activity);
|
console.log(activity)
|
||||||
}, [activity, org]);
|
}, [activity, org])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="m-8 bg-zinc-900 rounded-md mt-14">
|
<div className="m-8 bg-zinc-900 rounded-md mt-14">
|
||||||
<iframe
|
<iframe
|
||||||
className="rounded-lg w-full h-[900px]"
|
className="rounded-lg w-full h-[900px]"
|
||||||
src={getActivityMediaDirectory(org?.org_uuid, course?.course_uuid, activity.activity_uuid, activity.content.filename, 'documentpdf')}
|
src={getActivityMediaDirectory(
|
||||||
|
org?.org_uuid,
|
||||||
|
course?.course_uuid,
|
||||||
|
activity.activity_uuid,
|
||||||
|
activity.content.filename,
|
||||||
|
'documentpdf'
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default DocumentPdfActivity;
|
export default DocumentPdfActivity
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,71 +1,113 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import { Editor } from '@tiptap/core';
|
import { Editor } from '@tiptap/core'
|
||||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
import learnhouseAI_icon from 'public/learnhouse_ai_simple.png'
|
||||||
import Image from 'next/image';
|
import Image from 'next/image'
|
||||||
import { BookOpen, FormInput, Languages, MoreVertical } from 'lucide-react';
|
import { BookOpen, FormInput, Languages, MoreVertical } from 'lucide-react'
|
||||||
import { BubbleMenu } from '@tiptap/react';
|
import { BubbleMenu } from '@tiptap/react'
|
||||||
import ToolTip from '@components/StyledElements/Tooltip/Tooltip';
|
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||||
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
|
import {
|
||||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
AIChatBotStateTypes,
|
||||||
import useGetAIFeatures from '../../../../AI/Hooks/useGetAIFeatures';
|
useAIChatBot,
|
||||||
|
useAIChatBotDispatch,
|
||||||
|
} from '@components/Contexts/AI/AIChatBotContext'
|
||||||
|
import {
|
||||||
|
sendActivityAIChatMessage,
|
||||||
|
startActivityAIChatSession,
|
||||||
|
} from '@services/ai/ai'
|
||||||
|
import useGetAIFeatures from '../../../../AI/Hooks/useGetAIFeatures'
|
||||||
|
|
||||||
type AICanvaToolkitProps = {
|
type AICanvaToolkitProps = {
|
||||||
editor: Editor,
|
editor: Editor
|
||||||
activity: any
|
activity: any
|
||||||
}
|
}
|
||||||
|
|
||||||
function AICanvaToolkit(props: AICanvaToolkitProps) {
|
function AICanvaToolkit(props: AICanvaToolkitProps) {
|
||||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' });
|
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' })
|
||||||
const [isBubbleMenuAvailable, setIsButtonAvailable] = React.useState(false);
|
const [isBubbleMenuAvailable, setIsButtonAvailable] = React.useState(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (is_ai_feature_enabled) {
|
if (is_ai_feature_enabled) {
|
||||||
setIsButtonAvailable(true);
|
setIsButtonAvailable(true)
|
||||||
}
|
}
|
||||||
}, [is_ai_feature_enabled])
|
}, [is_ai_feature_enabled])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isBubbleMenuAvailable && <BubbleMenu className="w-fit" tippyOptions={{ duration: 100 }} editor={props.editor}>
|
{isBubbleMenuAvailable && (
|
||||||
<div style={{ background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgba(2, 1, 25, 0.98)' }}
|
<BubbleMenu
|
||||||
className='py-1 h-10 px-2 w-max text-white rounded-xl shadow-md cursor-pointer flex items-center space-x-2 antialiased'
|
className="w-fit"
|
||||||
|
tippyOptions={{ duration: 100 }}
|
||||||
|
editor={props.editor}
|
||||||
>
|
>
|
||||||
<div className='flex w-full space-x-2 font-bold text-white/80'><Image className='outline outline-1 outline-neutral-200/10 rounded-lg' width={24} src={learnhouseAI_icon} alt="" /> <div>AI</div> </div>
|
<div
|
||||||
|
style={{
|
||||||
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgba(2, 1, 25, 0.98)',
|
||||||
|
}}
|
||||||
|
className="py-1 h-10 px-2 w-max text-white rounded-xl shadow-md cursor-pointer flex items-center space-x-2 antialiased"
|
||||||
|
>
|
||||||
|
<div className="flex w-full space-x-2 font-bold text-white/80">
|
||||||
|
<Image
|
||||||
|
className="outline outline-1 outline-neutral-200/10 rounded-lg"
|
||||||
|
width={24}
|
||||||
|
src={learnhouseAI_icon}
|
||||||
|
alt=""
|
||||||
|
/>{' '}
|
||||||
|
<div>AI</div>{' '}
|
||||||
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<MoreVertical className='text-white/50' size={12} />
|
<MoreVertical className="text-white/50" size={12} />
|
||||||
</div>
|
</div>
|
||||||
<div className='flex space-x-2'>
|
<div className="flex space-x-2">
|
||||||
<AIActionButton editor={props.editor} activity={props.activity} label='Explain' />
|
<AIActionButton
|
||||||
<AIActionButton editor={props.editor} activity={props.activity} label='Summarize' />
|
editor={props.editor}
|
||||||
<AIActionButton editor={props.editor} activity={props.activity} label='Translate' />
|
activity={props.activity}
|
||||||
<AIActionButton editor={props.editor} activity={props.activity} label='Examples' />
|
label="Explain"
|
||||||
|
/>
|
||||||
|
<AIActionButton
|
||||||
|
editor={props.editor}
|
||||||
|
activity={props.activity}
|
||||||
|
label="Summarize"
|
||||||
|
/>
|
||||||
|
<AIActionButton
|
||||||
|
editor={props.editor}
|
||||||
|
activity={props.activity}
|
||||||
|
label="Translate"
|
||||||
|
/>
|
||||||
|
<AIActionButton
|
||||||
|
editor={props.editor}
|
||||||
|
activity={props.activity}
|
||||||
|
label="Examples"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</BubbleMenu>}
|
</BubbleMenu>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function AIActionButton(props: { editor: Editor, label: string, activity: any }) {
|
function AIActionButton(props: {
|
||||||
const dispatchAIChatBot = useAIChatBotDispatch() as any;
|
editor: Editor
|
||||||
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
|
label: string
|
||||||
|
activity: any
|
||||||
|
}) {
|
||||||
|
const dispatchAIChatBot = useAIChatBotDispatch() as any
|
||||||
|
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
|
||||||
|
|
||||||
async function handleAction(label: string) {
|
async function handleAction(label: string) {
|
||||||
const selection = getTipTapEditorSelectedText();
|
const selection = getTipTapEditorSelectedText()
|
||||||
const prompt = getPrompt(label, selection);
|
const prompt = getPrompt(label, selection)
|
||||||
dispatchAIChatBot({ type: 'setIsModalOpen' });
|
dispatchAIChatBot({ type: 'setIsModalOpen' })
|
||||||
await sendMessage(prompt);
|
await sendMessage(prompt)
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTipTapEditorSelectedText = () => {
|
const getTipTapEditorSelectedText = () => {
|
||||||
const selection = props.editor.state.selection;
|
const selection = props.editor.state.selection
|
||||||
const from = selection.from;
|
const from = selection.from
|
||||||
const to = selection.to;
|
const to = selection.to
|
||||||
const text = props.editor.state.doc.textBetween(from, to);
|
const text = props.editor.state.doc.textBetween(from, to)
|
||||||
return text;
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPrompt = (label: string, selection: string) => {
|
const getPrompt = (label: string, selection: string) => {
|
||||||
|
|
@ -82,45 +124,92 @@ function AIActionButton(props: { editor: Editor, label: string, activity: any })
|
||||||
|
|
||||||
const sendMessage = async (message: string) => {
|
const sendMessage = async (message: string) => {
|
||||||
if (aiChatBotState.aichat_uuid) {
|
if (aiChatBotState.aichat_uuid) {
|
||||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
await dispatchAIChatBot({
|
||||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
type: 'addMessage',
|
||||||
const response = await sendActivityAIChatMessage(message, aiChatBotState.aichat_uuid, props.activity.activity_uuid)
|
payload: { sender: 'user', message: message, type: 'user' },
|
||||||
|
})
|
||||||
|
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
|
||||||
|
const response = await sendActivityAIChatMessage(
|
||||||
|
message,
|
||||||
|
aiChatBotState.aichat_uuid,
|
||||||
|
props.activity.activity_uuid
|
||||||
|
)
|
||||||
if (response.success == false) {
|
if (response.success == false) {
|
||||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
await dispatchAIChatBot({
|
||||||
return;
|
type: 'setError',
|
||||||
|
payload: {
|
||||||
|
isError: true,
|
||||||
|
status: response.status,
|
||||||
|
error_message: response.data.detail,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
await dispatchAIChatBot({
|
||||||
|
type: 'addMessage',
|
||||||
|
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
await dispatchAIChatBot({
|
||||||
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
|
type: 'addMessage',
|
||||||
const response = await startActivityAIChatSession(message, props.activity.activity_uuid)
|
payload: { sender: 'user', message: message, type: 'user' },
|
||||||
|
})
|
||||||
|
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
|
||||||
|
const response = await startActivityAIChatSession(
|
||||||
|
message,
|
||||||
|
props.activity.activity_uuid
|
||||||
|
)
|
||||||
if (response.success == false) {
|
if (response.success == false) {
|
||||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||||
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
await dispatchAIChatBot({
|
||||||
return;
|
type: 'setError',
|
||||||
|
payload: {
|
||||||
|
isError: true,
|
||||||
|
status: response.status,
|
||||||
|
error_message: response.data.detail,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return
|
||||||
}
|
}
|
||||||
await dispatchAIChatBot({ type: 'setAichat_uuid', payload: response.data.aichat_uuid });
|
await dispatchAIChatBot({
|
||||||
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
|
type: 'setAichat_uuid',
|
||||||
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
|
payload: response.data.aichat_uuid,
|
||||||
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
})
|
||||||
|
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
|
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
|
||||||
|
await dispatchAIChatBot({
|
||||||
|
type: 'addMessage',
|
||||||
|
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const tooltipLabel = props.label === 'Explain' ? 'Explain a word or a sentence with AI' : props.label === 'Summarize' ? 'Summarize a long paragraph or text with AI' : props.label === 'Translate' ? 'Translate to different languages with AI' : 'Give examples to understand better with AI'
|
const tooltipLabel =
|
||||||
|
props.label === 'Explain'
|
||||||
|
? 'Explain a word or a sentence with AI'
|
||||||
|
: props.label === 'Summarize'
|
||||||
|
? 'Summarize a long paragraph or text with AI'
|
||||||
|
: props.label === 'Translate'
|
||||||
|
? 'Translate to different languages with AI'
|
||||||
|
: 'Give examples to understand better with AI'
|
||||||
return (
|
return (
|
||||||
<div className='flex space-x-2' >
|
<div className="flex space-x-2">
|
||||||
<ToolTip sideOffset={10} slateBlack content={tooltipLabel}>
|
<ToolTip sideOffset={10} slateBlack content={tooltipLabel}>
|
||||||
<button onClick={() => handleAction(props.label)} className='flex space-x-1.5 items-center bg-white/10 px-2 py-0.5 rounded-md outline outline-1 outline-neutral-200/20 text-sm font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
<button
|
||||||
|
onClick={() => handleAction(props.label)}
|
||||||
|
className="flex space-x-1.5 items-center bg-white/10 px-2 py-0.5 rounded-md outline outline-1 outline-neutral-200/20 text-sm font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all"
|
||||||
|
>
|
||||||
{props.label === 'Explain' && <BookOpen size={16} />}
|
{props.label === 'Explain' && <BookOpen size={16} />}
|
||||||
{props.label === 'Summarize' && <FormInput size={16} />}
|
{props.label === 'Summarize' && <FormInput size={16} />}
|
||||||
{props.label === 'Translate' && <Languages size={16} />}
|
{props.label === 'Translate' && <Languages size={16} />}
|
||||||
{props.label === 'Examples' && <div className='text-white/50'>Ex</div>}
|
{props.label === 'Examples' && (
|
||||||
|
<div className="text-white/50">Ex</div>
|
||||||
|
)}
|
||||||
<div>{props.label}</div>
|
<div>{props.label}</div>
|
||||||
</button>
|
</button>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,34 @@
|
||||||
import { useEditor, EditorContent } from "@tiptap/react";
|
import { useEditor, EditorContent } from '@tiptap/react'
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import styled from "styled-components"
|
import styled from 'styled-components'
|
||||||
import Youtube from "@tiptap/extension-youtube";
|
import Youtube from '@tiptap/extension-youtube'
|
||||||
// Custom Extensions
|
// Custom Extensions
|
||||||
import InfoCallout from "@components/Objects/Editor/Extensions/Callout/Info/InfoCallout";
|
import InfoCallout from '@components/Objects/Editor/Extensions/Callout/Info/InfoCallout'
|
||||||
import WarningCallout from "@components/Objects/Editor/Extensions/Callout/Warning/WarningCallout";
|
import WarningCallout from '@components/Objects/Editor/Extensions/Callout/Warning/WarningCallout'
|
||||||
import ImageBlock from "@components/Objects/Editor/Extensions/Image/ImageBlock";
|
import ImageBlock from '@components/Objects/Editor/Extensions/Image/ImageBlock'
|
||||||
import VideoBlock from "@components/Objects/Editor/Extensions/Video/VideoBlock";
|
import VideoBlock from '@components/Objects/Editor/Extensions/Video/VideoBlock'
|
||||||
import MathEquationBlock from "@components/Objects/Editor/Extensions/MathEquation/MathEquationBlock";
|
import MathEquationBlock from '@components/Objects/Editor/Extensions/MathEquation/MathEquationBlock'
|
||||||
import PDFBlock from "@components/Objects/Editor/Extensions/PDF/PDFBlock";
|
import PDFBlock from '@components/Objects/Editor/Extensions/PDF/PDFBlock'
|
||||||
import { OrderedList } from "@tiptap/extension-ordered-list";
|
import { OrderedList } from '@tiptap/extension-ordered-list'
|
||||||
import QuizBlock from "@components/Objects/Editor/Extensions/Quiz/QuizBlock";
|
import QuizBlock from '@components/Objects/Editor/Extensions/Quiz/QuizBlock'
|
||||||
|
|
||||||
// Lowlight
|
// Lowlight
|
||||||
import { common, createLowlight } from 'lowlight'
|
import { common, createLowlight } from 'lowlight'
|
||||||
const lowlight = createLowlight(common)
|
const lowlight = createLowlight(common)
|
||||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||||||
import css from 'highlight.js/lib/languages/css'
|
import css from 'highlight.js/lib/languages/css'
|
||||||
import js from 'highlight.js/lib/languages/javascript'
|
import js from 'highlight.js/lib/languages/javascript'
|
||||||
import ts from 'highlight.js/lib/languages/typescript'
|
import ts from 'highlight.js/lib/languages/typescript'
|
||||||
import html from 'highlight.js/lib/languages/xml'
|
import html from 'highlight.js/lib/languages/xml'
|
||||||
import python from 'highlight.js/lib/languages/python'
|
import python from 'highlight.js/lib/languages/python'
|
||||||
import java from 'highlight.js/lib/languages/java'
|
import java from 'highlight.js/lib/languages/java'
|
||||||
import { NoTextInput } from "@components/Objects/Editor/Extensions/NoTextInput/NoTextInput";
|
import { NoTextInput } from '@components/Objects/Editor/Extensions/NoTextInput/NoTextInput'
|
||||||
import EditorOptionsProvider from "@components/Contexts/Editor/EditorContext";
|
import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext'
|
||||||
import AICanvaToolkit from "./AI/AICanvaToolkit";
|
import AICanvaToolkit from './AI/AICanvaToolkit'
|
||||||
|
|
||||||
|
|
||||||
interface Editor {
|
interface Editor {
|
||||||
content: string;
|
content: string
|
||||||
activity: any;
|
activity: any
|
||||||
}
|
}
|
||||||
|
|
||||||
function Canva(props: Editor) {
|
function Canva(props: Editor) {
|
||||||
|
|
@ -38,8 +37,7 @@ function Canva(props: Editor) {
|
||||||
* Another workaround is implemented below to disable the editor from being edited by the user by setting the caret-color to transparent and using a custom extension to filter out transactions that add/edit/remove text.
|
* Another workaround is implemented below to disable the editor from being edited by the user by setting the caret-color to transparent and using a custom extension to filter out transactions that add/edit/remove text.
|
||||||
* To let the various Custom Extensions know that the editor is not editable, React context (EditorOptionsProvider) will be used instead of props.extension.options.editable.
|
* To let the various Custom Extensions know that the editor is not editable, React context (EditorOptionsProvider) will be used instead of props.extension.options.editable.
|
||||||
*/
|
*/
|
||||||
const isEditable = true;
|
const isEditable = true
|
||||||
|
|
||||||
|
|
||||||
// Code Block Languages for Lowlight
|
// Code Block Languages for Lowlight
|
||||||
lowlight.register('html', html)
|
lowlight.register('html', html)
|
||||||
|
|
@ -49,7 +47,6 @@ function Canva(props: Editor) {
|
||||||
lowlight.register('python', python)
|
lowlight.register('python', python)
|
||||||
lowlight.register('java', java)
|
lowlight.register('java', java)
|
||||||
|
|
||||||
|
|
||||||
const editor: any = useEditor({
|
const editor: any = useEditor({
|
||||||
editable: isEditable,
|
editable: isEditable,
|
||||||
extensions: [
|
extensions: [
|
||||||
|
|
@ -90,41 +87,35 @@ function Canva(props: Editor) {
|
||||||
CodeBlockLowlight.configure({
|
CodeBlockLowlight.configure({
|
||||||
lowlight,
|
lowlight,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
],
|
],
|
||||||
|
|
||||||
content: props.content,
|
content: props.content,
|
||||||
});
|
})
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
||||||
<EditorOptionsProvider options={{ isEditable: false }}>
|
<EditorOptionsProvider options={{ isEditable: false }}>
|
||||||
<CanvaWrapper>
|
<CanvaWrapper>
|
||||||
<AICanvaToolkit activity={props.activity} editor={editor} />
|
<AICanvaToolkit activity={props.activity} editor={editor} />
|
||||||
<EditorContent editor={editor} />
|
<EditorContent editor={editor} />
|
||||||
</CanvaWrapper>
|
</CanvaWrapper>
|
||||||
</EditorOptionsProvider>
|
</EditorOptionsProvider>
|
||||||
|
)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const CanvaWrapper = styled.div`
|
const CanvaWrapper = styled.div`
|
||||||
width: 100%;
|
width: 100%;
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|
||||||
.bubble-menu {
|
.bubble-menu {
|
||||||
display: flex;
|
display: flex;
|
||||||
background-color: #0D0D0D;
|
background-color: #0d0d0d;
|
||||||
padding: 0.2rem;
|
padding: 0.2rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
|
|
||||||
button {
|
button {
|
||||||
border: none;
|
border: none;
|
||||||
background: none;
|
background: none;
|
||||||
color: #FFF;
|
color: #fff;
|
||||||
font-size: 0.85rem;
|
font-size: 0.85rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
padding: 0 0.2rem;
|
padding: 0 0.2rem;
|
||||||
|
|
@ -135,12 +126,11 @@ const CanvaWrapper = styled.div`
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// disable chrome outline
|
// disable chrome outline
|
||||||
|
|
||||||
.ProseMirror {
|
.ProseMirror {
|
||||||
|
|
||||||
// Workaround to disable editor from being edited by the user.
|
// Workaround to disable editor from being edited by the user.
|
||||||
caret-color: transparent;
|
caret-color: transparent;
|
||||||
|
|
||||||
|
|
@ -176,13 +166,13 @@ const CanvaWrapper = styled.div`
|
||||||
margin-bottom: 10px;
|
margin-bottom: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul, ol {
|
ul,
|
||||||
|
ol {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
outline-style: none !important;
|
outline-style: none !important;
|
||||||
|
|
@ -194,7 +184,7 @@ const CanvaWrapper = styled.div`
|
||||||
background: #0d0d0d;
|
background: #0d0d0d;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: "JetBrainsMono", monospace;
|
font-family: 'JetBrainsMono', monospace;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|
@ -256,9 +246,7 @@ const CanvaWrapper = styled.div`
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default Canva
|
||||||
`;
|
|
||||||
|
|
||||||
export default Canva;
|
|
||||||
|
|
|
||||||
|
|
@ -1,68 +1,70 @@
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import YouTube from 'react-youtube';
|
import YouTube from 'react-youtube'
|
||||||
import { getActivityMediaDirectory } from "@services/media/media";
|
import { getActivityMediaDirectory } from '@services/media/media'
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
|
|
||||||
function VideoActivity({ activity, course }: { activity: any; course: any }) {
|
function VideoActivity({ activity, course }: { activity: any; course: any }) {
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const [videoId, setVideoId] = React.useState('');
|
const [videoId, setVideoId] = React.useState('')
|
||||||
|
|
||||||
function getYouTubeEmbed(url: any) {
|
function getYouTubeEmbed(url: any) {
|
||||||
// Extract video ID from the YouTube URL
|
// Extract video ID from the YouTube URL
|
||||||
var videoId = url.match(/(?:\?v=|\/embed\/|\/\d\/|\/vi\/|\/v\/|https?:\/\/(?:www\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^#\&\?\/]+)/)[1];
|
var videoId = url.match(
|
||||||
|
/(?:\?v=|\/embed\/|\/\d\/|\/vi\/|\/v\/|https?:\/\/(?:www\.)?youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))([^#\&\?\/]+)/
|
||||||
|
)[1]
|
||||||
|
|
||||||
// Create the embed object
|
// Create the embed object
|
||||||
var embedObject = {
|
var embedObject = {
|
||||||
videoId: videoId,
|
videoId: videoId,
|
||||||
width: 560,
|
width: 560,
|
||||||
height: 315
|
height: 315,
|
||||||
};
|
|
||||||
|
|
||||||
return embedObject;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return embedObject
|
||||||
|
}
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
console.log(activity);
|
console.log(activity)
|
||||||
}, [activity, org]);
|
}, [activity, org])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{activity &&
|
{activity && (
|
||||||
<>
|
<>
|
||||||
{activity.activity_sub_type === 'SUBTYPE_VIDEO_HOSTED' && (
|
{activity.activity_sub_type === 'SUBTYPE_VIDEO_HOSTED' && (
|
||||||
<div className="m-8 bg-zinc-900 rounded-md mt-14">
|
<div className="m-8 bg-zinc-900 rounded-md mt-14">
|
||||||
<video className="rounded-lg w-full h-[500px]" controls
|
<video
|
||||||
src={getActivityMediaDirectory(org?.org_uuid, course?.course_uuid, activity.activity_uuid, activity.content?.filename, 'video')}
|
className="rounded-lg w-full h-[500px]"
|
||||||
|
controls
|
||||||
|
src={getActivityMediaDirectory(
|
||||||
|
org?.org_uuid,
|
||||||
|
course?.course_uuid,
|
||||||
|
activity.activity_uuid,
|
||||||
|
activity.content?.filename,
|
||||||
|
'video'
|
||||||
|
)}
|
||||||
></video>
|
></video>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{activity.activity_sub_type === 'SUBTYPE_VIDEO_YOUTUBE' && (
|
{activity.activity_sub_type === 'SUBTYPE_VIDEO_YOUTUBE' && (
|
||||||
<div>
|
<div>
|
||||||
<YouTube
|
<YouTube
|
||||||
className="rounded-md overflow-hidden m-8 bg-zinc-900 mt-14"
|
className="rounded-md overflow-hidden m-8 bg-zinc-900 mt-14"
|
||||||
opts={
|
opts={{
|
||||||
{
|
|
||||||
width: '1300',
|
width: '1300',
|
||||||
height: '500',
|
height: '500',
|
||||||
playerVars: {
|
playerVars: {
|
||||||
autoplay: 0,
|
autoplay: 0,
|
||||||
},
|
},
|
||||||
|
}}
|
||||||
}
|
videoId={videoId}
|
||||||
}
|
/>
|
||||||
videoId={videoId} />
|
|
||||||
</div>
|
</div>
|
||||||
)}</>}
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default VideoActivity
|
||||||
|
|
||||||
|
|
||||||
export default VideoActivity;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,272 +1,387 @@
|
||||||
import React from 'react'
|
import React from 'react'
|
||||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
import learnhouseAI_icon from 'public/learnhouse_ai_simple.png'
|
||||||
import { motion, AnimatePresence } from 'framer-motion';
|
import { motion, AnimatePresence } from 'framer-motion'
|
||||||
import Image from 'next/image';
|
import Image from 'next/image'
|
||||||
import { AlertTriangle, BetweenHorizontalStart, FastForward, Feather, FileStack, HelpCircle, Languages, MoreVertical, X } from 'lucide-react';
|
import {
|
||||||
import { Editor } from '@tiptap/react';
|
AlertTriangle,
|
||||||
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from '@components/Contexts/AI/AIEditorContext';
|
BetweenHorizontalStart,
|
||||||
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
|
FastForward,
|
||||||
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures';
|
Feather,
|
||||||
|
FileStack,
|
||||||
|
HelpCircle,
|
||||||
|
Languages,
|
||||||
|
MoreVertical,
|
||||||
|
X,
|
||||||
|
} from 'lucide-react'
|
||||||
|
import { Editor } from '@tiptap/react'
|
||||||
|
import {
|
||||||
|
AIEditorStateTypes,
|
||||||
|
useAIEditor,
|
||||||
|
useAIEditorDispatch,
|
||||||
|
} from '@components/Contexts/AI/AIEditorContext'
|
||||||
|
import {
|
||||||
|
sendActivityAIChatMessage,
|
||||||
|
startActivityAIChatSession,
|
||||||
|
} from '@services/ai/ai'
|
||||||
|
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures'
|
||||||
|
|
||||||
type AIEditorToolkitProps = {
|
type AIEditorToolkitProps = {
|
||||||
editor: Editor,
|
editor: Editor
|
||||||
activity: any
|
activity: any
|
||||||
}
|
}
|
||||||
|
|
||||||
type AIPromptsLabels = {
|
type AIPromptsLabels = {
|
||||||
label: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate',
|
label:
|
||||||
|
| 'Writer'
|
||||||
|
| 'ContinueWriting'
|
||||||
|
| 'MakeLonger'
|
||||||
|
| 'GenerateQuiz'
|
||||||
|
| 'Translate'
|
||||||
selection: string
|
selection: string
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AIEditorToolkit(props: AIEditorToolkitProps) {
|
function AIEditorToolkit(props: AIEditorToolkitProps) {
|
||||||
const dispatchAIEditor = useAIEditorDispatch() as any;
|
const dispatchAIEditor = useAIEditorDispatch() as any
|
||||||
const aiEditorState = useAIEditor() as AIEditorStateTypes;
|
const aiEditorState = useAIEditor() as AIEditorStateTypes
|
||||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' });
|
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' })
|
||||||
const [isToolkitAvailable, setIsToolkitAvailable] = React.useState(true);
|
const [isToolkitAvailable, setIsToolkitAvailable] = React.useState(true)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (is_ai_feature_enabled) {
|
if (is_ai_feature_enabled) {
|
||||||
setIsToolkitAvailable(true);
|
setIsToolkitAvailable(true)
|
||||||
}
|
}
|
||||||
}, [is_ai_feature_enabled])
|
}, [is_ai_feature_enabled])
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{isToolkitAvailable && <div className='flex space-x-2'>
|
{isToolkitAvailable && (
|
||||||
|
<div className="flex space-x-2">
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{aiEditorState.isModalOpen && <motion.div
|
{aiEditorState.isModalOpen && (
|
||||||
|
<motion.div
|
||||||
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
|
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
|
||||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||||
exit={{ y: 50, opacity: 0, filter: 'blur(3px)' }}
|
exit={{ y: 50, opacity: 0, filter: 'blur(3px)' }}
|
||||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2 }}
|
transition={{
|
||||||
className='fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center '
|
type: 'spring',
|
||||||
|
bounce: 0.35,
|
||||||
|
duration: 1.7,
|
||||||
|
mass: 0.2,
|
||||||
|
velocity: 2,
|
||||||
|
}}
|
||||||
|
className="fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center "
|
||||||
style={{ pointerEvents: 'none' }}
|
style={{ pointerEvents: 'none' }}
|
||||||
>
|
>
|
||||||
<>
|
<>
|
||||||
{aiEditorState.isFeedbackModalOpen && <UserFeedbackModal activity={props.activity} editor={props.editor} />}
|
{aiEditorState.isFeedbackModalOpen && (
|
||||||
|
<UserFeedbackModal
|
||||||
|
activity={props.activity}
|
||||||
|
editor={props.editor}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(2 1 25 / 98%)'
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(2 1 25 / 98%)',
|
||||||
}}
|
}}
|
||||||
className="z-50 rounded-2xl max-w-screen-2xl my-10 mx-auto w-fit fixed bottom-0 left-1/2 transform -translate-x-1/2 shadow-xl ring-1 ring-inset ring-white/10 text-white p-3 flex-col-reverse backdrop-blur-md">
|
className="z-50 rounded-2xl max-w-screen-2xl my-10 mx-auto w-fit fixed bottom-0 left-1/2 transform -translate-x-1/2 shadow-xl ring-1 ring-inset ring-white/10 text-white p-3 flex-col-reverse backdrop-blur-md"
|
||||||
<div className='flex space-x-2'>
|
>
|
||||||
<div className='pr-1'>
|
<div className="flex space-x-2">
|
||||||
<div className='flex w-full space-x-2 font-bold text-white/80 items-center'>
|
<div className="pr-1">
|
||||||
<Image className='outline outline-1 outline-neutral-200/20 rounded-lg' width={24} src={learnhouseAI_icon} alt="" />
|
<div className="flex w-full space-x-2 font-bold text-white/80 items-center">
|
||||||
<div className='flex items-center'>AI Editor <span className='text-[10px] px-2 py-1 rounded-3xl ml-3 bg-white/10 uppercase'>PRE-ALPHA</span></div>
|
<Image
|
||||||
<MoreVertical className='text-white/50' size={12} />
|
className="outline outline-1 outline-neutral-200/20 rounded-lg"
|
||||||
|
width={24}
|
||||||
|
src={learnhouseAI_icon}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
<div className="flex items-center">
|
||||||
|
AI Editor{' '}
|
||||||
|
<span className="text-[10px] px-2 py-1 rounded-3xl ml-3 bg-white/10 uppercase">
|
||||||
|
PRE-ALPHA
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<MoreVertical className="text-white/50" size={12} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className='tools flex space-x-2'>
|
<div className="tools flex space-x-2">
|
||||||
<AiEditorToolButton label='Writer' />
|
<AiEditorToolButton label="Writer" />
|
||||||
<AiEditorToolButton label='ContinueWriting' />
|
<AiEditorToolButton label="ContinueWriting" />
|
||||||
<AiEditorToolButton label='MakeLonger' />
|
<AiEditorToolButton label="MakeLonger" />
|
||||||
|
|
||||||
<AiEditorToolButton label='Translate' />
|
<AiEditorToolButton label="Translate" />
|
||||||
|
</div>
|
||||||
|
<div className="flex space-x-2 items-center">
|
||||||
|
<X
|
||||||
|
onClick={() =>
|
||||||
|
Promise.all([
|
||||||
|
dispatchAIEditor({ type: 'setIsModalClose' }),
|
||||||
|
dispatchAIEditor({
|
||||||
|
type: 'setIsFeedbackModalClose',
|
||||||
|
}),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
size={20}
|
||||||
|
className="text-white/50 hover:cursor-pointer bg-white/10 p-1 rounded-full items-center"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex space-x-2 items-center'>
|
|
||||||
<X onClick={() => Promise.all([dispatchAIEditor({ type: 'setIsModalClose' }), dispatchAIEditor({ type: 'setIsFeedbackModalClose' })])} size={20} className='text-white/50 hover:cursor-pointer bg-white/10 p-1 rounded-full items-center' />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div></>
|
|
||||||
</motion.div>}
|
|
||||||
</AnimatePresence>
|
|
||||||
</div>}
|
|
||||||
</>
|
</>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const UserFeedbackModal = (props: AIEditorToolkitProps) => {
|
const UserFeedbackModal = (props: AIEditorToolkitProps) => {
|
||||||
const dispatchAIEditor = useAIEditorDispatch() as any;
|
const dispatchAIEditor = useAIEditorDispatch() as any
|
||||||
const aiEditorState = useAIEditor() as AIEditorStateTypes;
|
const aiEditorState = useAIEditor() as AIEditorStateTypes
|
||||||
|
|
||||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
await dispatchAIEditor({ type: 'setChatInputValue', payload: event.currentTarget.value });
|
await dispatchAIEditor({
|
||||||
|
type: 'setChatInputValue',
|
||||||
|
payload: event.currentTarget.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendReqWithMessage = async (message: string) => {
|
const sendReqWithMessage = async (message: string) => {
|
||||||
if (aiEditorState.aichat_uuid) {
|
if (aiEditorState.aichat_uuid) {
|
||||||
await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
await dispatchAIEditor({
|
||||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
type: 'addMessage',
|
||||||
const response = await sendActivityAIChatMessage(message, aiEditorState.aichat_uuid, props.activity.activity_uuid)
|
payload: { sender: 'user', message: message, type: 'user' },
|
||||||
|
})
|
||||||
|
await dispatchAIEditor({ type: 'setIsWaitingForResponse' })
|
||||||
|
const response = await sendActivityAIChatMessage(
|
||||||
|
message,
|
||||||
|
aiEditorState.aichat_uuid,
|
||||||
|
props.activity.activity_uuid
|
||||||
|
)
|
||||||
if (response.success === false) {
|
if (response.success === false) {
|
||||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
await dispatchAIEditor({ type: 'setIsModalClose' });
|
await dispatchAIEditor({ type: 'setIsModalClose' })
|
||||||
// wait for 200ms before opening the modal again
|
// wait for 200ms before opening the modal again
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
await dispatchAIEditor({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
await dispatchAIEditor({
|
||||||
await dispatchAIEditor({ type: 'setIsModalOpen' });
|
type: 'setError',
|
||||||
return '';
|
payload: {
|
||||||
|
isError: true,
|
||||||
|
status: response.status,
|
||||||
|
error_message: response.data.detail,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await dispatchAIEditor({ type: 'setIsModalOpen' })
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
await dispatchAIEditor({ type: 'setChatInputValue', payload: '' });
|
await dispatchAIEditor({ type: 'setChatInputValue', payload: '' })
|
||||||
await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
await dispatchAIEditor({
|
||||||
return response.data.message;
|
type: 'addMessage',
|
||||||
|
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
|
||||||
|
})
|
||||||
|
return response.data.message
|
||||||
} else {
|
} else {
|
||||||
await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
|
await dispatchAIEditor({
|
||||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
type: 'addMessage',
|
||||||
const response = await startActivityAIChatSession(message, props.activity.activity_uuid)
|
payload: { sender: 'user', message: message, type: 'user' },
|
||||||
|
})
|
||||||
|
await dispatchAIEditor({ type: 'setIsWaitingForResponse' })
|
||||||
|
const response = await startActivityAIChatSession(
|
||||||
|
message,
|
||||||
|
props.activity.activity_uuid
|
||||||
|
)
|
||||||
if (response.success === false) {
|
if (response.success === false) {
|
||||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
await dispatchAIEditor({ type: 'setIsModalClose' });
|
await dispatchAIEditor({ type: 'setIsModalClose' })
|
||||||
// wait for 200ms before opening the modal again
|
// wait for 200ms before opening the modal again
|
||||||
await new Promise(resolve => setTimeout(resolve, 200));
|
await new Promise((resolve) => setTimeout(resolve, 200))
|
||||||
await dispatchAIEditor({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
|
await dispatchAIEditor({
|
||||||
await dispatchAIEditor({ type: 'setIsModalOpen' });
|
type: 'setError',
|
||||||
return '';
|
payload: {
|
||||||
|
isError: true,
|
||||||
|
status: response.status,
|
||||||
|
error_message: response.data.detail,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
await dispatchAIEditor({ type: 'setIsModalOpen' })
|
||||||
|
return ''
|
||||||
}
|
}
|
||||||
await dispatchAIEditor({ type: 'setAichat_uuid', payload: response.data.aichat_uuid });
|
await dispatchAIEditor({
|
||||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
type: 'setAichat_uuid',
|
||||||
await dispatchAIEditor({ type: 'setChatInputValue', payload: '' });
|
payload: response.data.aichat_uuid,
|
||||||
await dispatchAIEditor({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
|
})
|
||||||
return response.data.message;
|
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
|
await dispatchAIEditor({ type: 'setChatInputValue', payload: '' })
|
||||||
|
await dispatchAIEditor({
|
||||||
|
type: 'addMessage',
|
||||||
|
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
|
||||||
|
})
|
||||||
|
return response.data.message
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyPress = async (event: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyPress = async (
|
||||||
|
event: React.KeyboardEvent<HTMLInputElement>
|
||||||
|
) => {
|
||||||
if (event.key === 'Enter') {
|
if (event.key === 'Enter') {
|
||||||
await handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue);
|
await handleOperation(
|
||||||
|
aiEditorState.selectedTool,
|
||||||
|
aiEditorState.chatInputValue
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOperation = async (label: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate', message: string) => {
|
const handleOperation = async (
|
||||||
|
label:
|
||||||
|
| 'Writer'
|
||||||
|
| 'ContinueWriting'
|
||||||
|
| 'MakeLonger'
|
||||||
|
| 'GenerateQuiz'
|
||||||
|
| 'Translate',
|
||||||
|
message: string
|
||||||
|
) => {
|
||||||
// Set selected tool
|
// Set selected tool
|
||||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
await dispatchAIEditor({ type: 'setSelectedTool', payload: label })
|
||||||
|
|
||||||
// Check what operation that was
|
// Check what operation that was
|
||||||
if (label === 'Writer') {
|
if (label === 'Writer') {
|
||||||
let ai_message = '';
|
let ai_message = ''
|
||||||
let prompt = getPrompt({ label: label, selection: message });
|
let prompt = getPrompt({ label: label, selection: message })
|
||||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true });
|
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true })
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false });
|
await dispatchAIEditor({
|
||||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
type: 'setIsUserInputEnabled',
|
||||||
ai_message = await sendReqWithMessage(prompt);
|
payload: false,
|
||||||
await fillEditorWithText(ai_message);
|
})
|
||||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIEditor({ type: 'setIsWaitingForResponse' })
|
||||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true });
|
ai_message = await sendReqWithMessage(prompt)
|
||||||
|
await fillEditorWithText(ai_message)
|
||||||
|
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
|
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true })
|
||||||
}
|
}
|
||||||
} else if (label === 'ContinueWriting') {
|
} else if (label === 'ContinueWriting') {
|
||||||
let ai_message = '';
|
let ai_message = ''
|
||||||
let text_selection = getTipTapEditorSelectedTextGlobal();
|
let text_selection = getTipTapEditorSelectedTextGlobal()
|
||||||
let prompt = getPrompt({ label: label, selection: text_selection });
|
let prompt = getPrompt({ label: label, selection: text_selection })
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
await dispatchAIEditor({ type: 'setIsWaitingForResponse' })
|
||||||
ai_message = await sendReqWithMessage(prompt);
|
ai_message = await sendReqWithMessage(prompt)
|
||||||
const message_without_original_text = await removeSentences(text_selection, ai_message);
|
const message_without_original_text = await removeSentences(
|
||||||
await fillEditorWithText(message_without_original_text);
|
text_selection,
|
||||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
ai_message
|
||||||
|
)
|
||||||
|
await fillEditorWithText(message_without_original_text)
|
||||||
|
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
}
|
}
|
||||||
} else if (label === 'MakeLonger') {
|
} else if (label === 'MakeLonger') {
|
||||||
let ai_message = '';
|
let ai_message = ''
|
||||||
let text_selection = getTipTapEditorSelectedText();
|
let text_selection = getTipTapEditorSelectedText()
|
||||||
let prompt = getPrompt({ label: label, selection: text_selection });
|
let prompt = getPrompt({ label: label, selection: text_selection })
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
await dispatchAIEditor({ type: 'setIsWaitingForResponse' })
|
||||||
ai_message = await sendReqWithMessage(prompt);
|
ai_message = await sendReqWithMessage(prompt)
|
||||||
await replaceSelectedTextWithText(ai_message);
|
await replaceSelectedTextWithText(ai_message)
|
||||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
}
|
}
|
||||||
} else if (label === 'GenerateQuiz') {
|
} else if (label === 'GenerateQuiz') {
|
||||||
// will be implemented in future stages
|
// will be implemented in future stages
|
||||||
} else if (label === 'Translate') {
|
} else if (label === 'Translate') {
|
||||||
let ai_message = '';
|
let ai_message = ''
|
||||||
let text_selection = getTipTapEditorSelectedText();
|
let text_selection = getTipTapEditorSelectedText()
|
||||||
let prompt = getPrompt({ label: label, selection: text_selection });
|
let prompt = getPrompt({ label: label, selection: text_selection })
|
||||||
if (prompt) {
|
if (prompt) {
|
||||||
await dispatchAIEditor({ type: 'setIsWaitingForResponse' });
|
await dispatchAIEditor({ type: 'setIsWaitingForResponse' })
|
||||||
ai_message = await sendReqWithMessage(prompt);
|
ai_message = await sendReqWithMessage(prompt)
|
||||||
await replaceSelectedTextWithText(ai_message);
|
await replaceSelectedTextWithText(ai_message)
|
||||||
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' });
|
await dispatchAIEditor({ type: 'setIsNoLongerWaitingForResponse' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const removeSentences = async (textToRemove: string, originalText: string) => {
|
const removeSentences = async (
|
||||||
const phrase = textToRemove.toLowerCase();
|
textToRemove: string,
|
||||||
const original = originalText.toLowerCase();
|
originalText: string
|
||||||
|
) => {
|
||||||
|
const phrase = textToRemove.toLowerCase()
|
||||||
|
const original = originalText.toLowerCase()
|
||||||
|
|
||||||
if (original.includes(phrase)) {
|
if (original.includes(phrase)) {
|
||||||
const regex = new RegExp(phrase, 'g');
|
const regex = new RegExp(phrase, 'g')
|
||||||
const newText = original.replace(regex, '');
|
const newText = original.replace(regex, '')
|
||||||
return newText;
|
return newText
|
||||||
} else {
|
} else {
|
||||||
return originalText;
|
return originalText
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fillEditorWithText(text: string) {
|
async function fillEditorWithText(text: string) {
|
||||||
const words = text.split(' ');
|
const words = text.split(' ')
|
||||||
|
|
||||||
for (let i = 0; i < words.length; i++) {
|
for (let i = 0; i < words.length; i++) {
|
||||||
const textNode = {
|
const textNode = {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: words[i],
|
text: words[i],
|
||||||
};
|
}
|
||||||
|
|
||||||
props.editor.chain().focus().insertContent(textNode).run();
|
props.editor.chain().focus().insertContent(textNode).run()
|
||||||
|
|
||||||
// Add a space after each word except the last one
|
// Add a space after each word except the last one
|
||||||
if (i < words.length - 1) {
|
if (i < words.length - 1) {
|
||||||
const spaceNode = {
|
const spaceNode = {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: ' ',
|
text: ' ',
|
||||||
};
|
}
|
||||||
|
|
||||||
props.editor.chain().focus().insertContent(spaceNode).run();
|
props.editor.chain().focus().insertContent(spaceNode).run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for 0.3 seconds before adding the next word
|
// Wait for 0.3 seconds before adding the next word
|
||||||
await new Promise(resolve => setTimeout(resolve, 120));
|
await new Promise((resolve) => setTimeout(resolve, 120))
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function replaceSelectedTextWithText(text: string) {
|
async function replaceSelectedTextWithText(text: string) {
|
||||||
const words = text.split(' ');
|
const words = text.split(' ')
|
||||||
|
|
||||||
// Delete the selected text
|
// Delete the selected text
|
||||||
props.editor.chain().focus().deleteSelection().run();
|
props.editor.chain().focus().deleteSelection().run()
|
||||||
|
|
||||||
for (let i = 0; i < words.length; i++) {
|
for (let i = 0; i < words.length; i++) {
|
||||||
const textNode = {
|
const textNode = {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: words[i],
|
text: words[i],
|
||||||
};
|
}
|
||||||
|
|
||||||
props.editor.chain().focus().insertContent(textNode).run();
|
props.editor.chain().focus().insertContent(textNode).run()
|
||||||
|
|
||||||
// Add a space after each word except the last one
|
// Add a space after each word except the last one
|
||||||
if (i < words.length - 1) {
|
if (i < words.length - 1) {
|
||||||
const spaceNode = {
|
const spaceNode = {
|
||||||
type: 'text',
|
type: 'text',
|
||||||
text: ' ',
|
text: ' ',
|
||||||
};
|
}
|
||||||
|
|
||||||
props.editor.chain().focus().insertContent(spaceNode).run();
|
props.editor.chain().focus().insertContent(spaceNode).run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for 0.3 seconds before adding the next word
|
// Wait for 0.3 seconds before adding the next word
|
||||||
await new Promise(resolve => setTimeout(resolve, 120));
|
await new Promise((resolve) => setTimeout(resolve, 120))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getPrompt = (args: AIPromptsLabels) => {
|
const getPrompt = (args: AIPromptsLabels) => {
|
||||||
const { label, selection } = args;
|
const { label, selection } = args
|
||||||
|
|
||||||
if (label === 'Writer') {
|
if (label === 'Writer') {
|
||||||
return `Write 3 sentences about ${selection}`;
|
return `Write 3 sentences about ${selection}`
|
||||||
} else if (label === 'ContinueWriting') {
|
} else if (label === 'ContinueWriting') {
|
||||||
return `Continue writing 3 more sentences based on "${selection}"`;
|
return `Continue writing 3 more sentences based on "${selection}"`
|
||||||
} else if (label === 'MakeLonger') {
|
} else if (label === 'MakeLonger') {
|
||||||
return `Make longer this text longer : "${selection}"`;
|
return `Make longer this text longer : "${selection}"`
|
||||||
} else if (label === 'GenerateQuiz') {
|
} else if (label === 'GenerateQuiz') {
|
||||||
return `Generate a quiz about "${selection}", only return an array of objects, every object should respect the following interface:
|
return `Generate a quiz about "${selection}", only return an array of objects, every object should respect the following interface:
|
||||||
interface Answer {
|
interface Answer {
|
||||||
|
|
@ -280,99 +395,144 @@ const UserFeedbackModal = (props: AIEditorToolkitProps) => {
|
||||||
type: "multiple_choice"
|
type: "multiple_choice"
|
||||||
answers: Answer[];
|
answers: Answer[];
|
||||||
}
|
}
|
||||||
" `;
|
" `
|
||||||
} else if (label === 'Translate') {
|
} else if (label === 'Translate') {
|
||||||
return `Translate "${selection}" to the ` + aiEditorState.chatInputValue + ` language`;
|
return (
|
||||||
|
`Translate "${selection}" to the ` +
|
||||||
|
aiEditorState.chatInputValue +
|
||||||
|
` language`
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTipTapEditorSelectedTextGlobal = () => {
|
const getTipTapEditorSelectedTextGlobal = () => {
|
||||||
// Get the entire node/paragraph that the user is in
|
// Get the entire node/paragraph that the user is in
|
||||||
const pos = props.editor.state.selection.$from.pos; // get the cursor position
|
const pos = props.editor.state.selection.$from.pos // get the cursor position
|
||||||
const resolvedPos = props.editor.state.doc.resolve(pos); // resolve the position in the document
|
const resolvedPos = props.editor.state.doc.resolve(pos) // resolve the position in the document
|
||||||
const start = resolvedPos.before(1); // get the start position of the node
|
const start = resolvedPos.before(1) // get the start position of the node
|
||||||
const end = resolvedPos.after(1); // get the end position of the node
|
const end = resolvedPos.after(1) // get the end position of the node
|
||||||
const paragraph = props.editor.state.doc.textBetween(start, end, '\n', '\n'); // get the text of the node
|
const paragraph = props.editor.state.doc.textBetween(start, end, '\n', '\n') // get the text of the node
|
||||||
return paragraph;
|
return paragraph
|
||||||
}
|
}
|
||||||
|
|
||||||
const getTipTapEditorSelectedText = () => {
|
const getTipTapEditorSelectedText = () => {
|
||||||
const selection = props.editor.state.selection;
|
const selection = props.editor.state.selection
|
||||||
const from = selection.from;
|
const from = selection.from
|
||||||
const to = selection.to;
|
const to = selection.to
|
||||||
const text = props.editor.state.doc.textBetween(from, to);
|
const text = props.editor.state.doc.textBetween(from, to)
|
||||||
return text;
|
return text
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
|
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
|
||||||
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
|
||||||
exit={{ y: 50, opacity: 0, filter: 'blur(3px)' }}
|
exit={{ y: 50, opacity: 0, filter: 'blur(3px)' }}
|
||||||
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2 }}
|
transition={{
|
||||||
className='backdrop-blur-md fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center '
|
type: 'spring',
|
||||||
|
bounce: 0.35,
|
||||||
|
duration: 1.7,
|
||||||
|
mass: 0.2,
|
||||||
|
velocity: 2,
|
||||||
|
}}
|
||||||
|
className="backdrop-blur-md fixed top-0 left-0 w-full h-full z-50 flex justify-center items-center "
|
||||||
style={{ pointerEvents: 'none' }}
|
style={{ pointerEvents: 'none' }}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
style={{
|
style={{
|
||||||
pointerEvents: 'auto',
|
pointerEvents: 'auto',
|
||||||
background: 'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(2 1 25 / 95%)'
|
background:
|
||||||
|
'linear-gradient(0deg, rgba(0, 0, 0, 0.2) 0%, rgba(0, 0, 0, 0.2) 100%), radial-gradient(105.16% 105.16% at 50% -5.16%, rgba(255, 255, 255, 0.18) 0%, rgba(0, 0, 0, 0) 100%), rgb(2 1 25 / 95%)',
|
||||||
}}
|
}}
|
||||||
className="backdrop-blur-md z-50 rounded-2xl max-w-screen-2xl my-10 mx-auto w-[500px] h-[200px] fixed bottom-16 left-1/2 transform -translate-x-1/2 shadow-xl ring-1 ring-inset ring-white/10 text-white p-3 flex-col-reverse">
|
className="backdrop-blur-md z-50 rounded-2xl max-w-screen-2xl my-10 mx-auto w-[500px] h-[200px] fixed bottom-16 left-1/2 transform -translate-x-1/2 shadow-xl ring-1 ring-inset ring-white/10 text-white p-3 flex-col-reverse"
|
||||||
<div className='flex space-x-2 justify-center'>
|
>
|
||||||
<Image className='outline outline-1 outline-neutral-200/20 rounded-lg' width={24} src={learnhouseAI_icon} alt="" />
|
<div className="flex space-x-2 justify-center">
|
||||||
|
<Image
|
||||||
|
className="outline outline-1 outline-neutral-200/20 rounded-lg"
|
||||||
|
width={24}
|
||||||
|
src={learnhouseAI_icon}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className='flex h-[115px] justify-center mx-auto antialiased'>
|
<div className="flex h-[115px] justify-center mx-auto antialiased">
|
||||||
<div className='flex items-center justify-center '>
|
<div className="flex items-center justify-center ">
|
||||||
<AiEditorActionScreen handleOperation={handleOperation} />
|
<AiEditorActionScreen handleOperation={handleOperation} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{aiEditorState.isUserInputEnabled && !aiEditorState.error.isError && <div className="flex items-center space-x-2 cursor-pointer">
|
{aiEditorState.isUserInputEnabled && !aiEditorState.error.isError && (
|
||||||
<input onKeyDown={handleKeyPress} value={aiEditorState.chatInputValue} onChange={handleChange} placeholder='Ask AI' className='ring-1 ring-inset ring-white/20 w-full bg-gray-950/20 rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30'></input>
|
<div className="flex items-center space-x-2 cursor-pointer">
|
||||||
<div onClick={() => handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue)} className='bg-white/10 px-3 rounded-md outline outline-1 outline-neutral-200/20 py-2 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
<input
|
||||||
<BetweenHorizontalStart size={20} className='text-white/50 hover:cursor-pointer' />
|
onKeyDown={handleKeyPress}
|
||||||
|
value={aiEditorState.chatInputValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Ask AI"
|
||||||
|
className="ring-1 ring-inset ring-white/20 w-full bg-gray-950/20 rounded-lg outline-none px-4 py-2 text-white text-sm placeholder:text-white/30"
|
||||||
|
></input>
|
||||||
|
<div
|
||||||
|
onClick={() =>
|
||||||
|
handleOperation(
|
||||||
|
aiEditorState.selectedTool,
|
||||||
|
aiEditorState.chatInputValue
|
||||||
|
)
|
||||||
|
}
|
||||||
|
className="bg-white/10 px-3 rounded-md outline outline-1 outline-neutral-200/20 py-2 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all"
|
||||||
|
>
|
||||||
|
<BetweenHorizontalStart
|
||||||
|
size={20}
|
||||||
|
className="text-white/50 hover:cursor-pointer"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const AiEditorToolButton = (props: any) => {
|
const AiEditorToolButton = (props: any) => {
|
||||||
const dispatchAIEditor = useAIEditorDispatch() as any;
|
const dispatchAIEditor = useAIEditorDispatch() as any
|
||||||
const aiEditorState = useAIEditor() as AIEditorStateTypes;
|
const aiEditorState = useAIEditor() as AIEditorStateTypes
|
||||||
|
|
||||||
const handleToolButtonClick = async (label: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate') => {
|
const handleToolButtonClick = async (
|
||||||
|
label:
|
||||||
|
| 'Writer'
|
||||||
|
| 'ContinueWriting'
|
||||||
|
| 'MakeLonger'
|
||||||
|
| 'GenerateQuiz'
|
||||||
|
| 'Translate'
|
||||||
|
) => {
|
||||||
if (label === 'Writer') {
|
if (label === 'Writer') {
|
||||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
await dispatchAIEditor({ type: 'setSelectedTool', payload: label })
|
||||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true });
|
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: true })
|
||||||
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' });
|
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' })
|
||||||
}
|
}
|
||||||
if (label === 'ContinueWriting') {
|
if (label === 'ContinueWriting') {
|
||||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
await dispatchAIEditor({ type: 'setSelectedTool', payload: label })
|
||||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false });
|
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false })
|
||||||
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' });
|
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' })
|
||||||
}
|
}
|
||||||
if (label === 'MakeLonger') {
|
if (label === 'MakeLonger') {
|
||||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
await dispatchAIEditor({ type: 'setSelectedTool', payload: label })
|
||||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false });
|
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false })
|
||||||
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' });
|
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' })
|
||||||
}
|
}
|
||||||
if (label === 'GenerateQuiz') {
|
if (label === 'GenerateQuiz') {
|
||||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
await dispatchAIEditor({ type: 'setSelectedTool', payload: label })
|
||||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false });
|
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false })
|
||||||
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' });
|
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' })
|
||||||
}
|
}
|
||||||
if (label === 'Translate') {
|
if (label === 'Translate') {
|
||||||
await dispatchAIEditor({ type: 'setSelectedTool', payload: label });
|
await dispatchAIEditor({ type: 'setSelectedTool', payload: label })
|
||||||
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false });
|
await dispatchAIEditor({ type: 'setIsUserInputEnabled', payload: false })
|
||||||
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' });
|
await dispatchAIEditor({ type: 'setIsFeedbackModalOpen' })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button onClick={() => handleToolButtonClick(props.label)} className='flex space-x-1.5 items-center bg-white/10 px-2 py-0.5 rounded-md outline outline-1 outline-neutral-200/20 text-sm font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
<button
|
||||||
|
onClick={() => handleToolButtonClick(props.label)}
|
||||||
|
className="flex space-x-1.5 items-center bg-white/10 px-2 py-0.5 rounded-md outline outline-1 outline-neutral-200/20 text-sm font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all"
|
||||||
|
>
|
||||||
{props.label === 'Writer' && <Feather size={14} />}
|
{props.label === 'Writer' && <Feather size={14} />}
|
||||||
{props.label === 'ContinueWriting' && <FastForward size={14} />}
|
{props.label === 'ContinueWriting' && <FastForward size={14} />}
|
||||||
{props.label === 'MakeLonger' && <FileStack size={14} />}
|
{props.label === 'MakeLonger' && <FileStack size={14} />}
|
||||||
|
|
@ -383,79 +543,139 @@ const AiEditorToolButton = (props: any) => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const AiEditorActionScreen = ({ handleOperation }: { handleOperation: any }) => {
|
const AiEditorActionScreen = ({
|
||||||
const dispatchAIEditor = useAIEditorDispatch() as any;
|
handleOperation,
|
||||||
const aiEditorState = useAIEditor() as AIEditorStateTypes;
|
}: {
|
||||||
|
handleOperation: any
|
||||||
|
}) => {
|
||||||
|
const dispatchAIEditor = useAIEditorDispatch() as any
|
||||||
|
const aiEditorState = useAIEditor() as AIEditorStateTypes
|
||||||
|
|
||||||
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
await dispatchAIEditor({ type: 'setChatInputValue', payload: event.currentTarget.value });
|
await dispatchAIEditor({
|
||||||
|
type: 'setChatInputValue',
|
||||||
|
payload: event.currentTarget.value,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{aiEditorState.selectedTool === 'Writer' && !aiEditorState.isWaitingForResponse && !aiEditorState.error.isError &&
|
{aiEditorState.selectedTool === 'Writer' &&
|
||||||
<div className='text-xl text-white/90 font-extrabold space-x-2'>
|
!aiEditorState.isWaitingForResponse &&
|
||||||
|
!aiEditorState.error.isError && (
|
||||||
|
<div className="text-xl text-white/90 font-extrabold space-x-2">
|
||||||
<span>Write about...</span>
|
<span>Write about...</span>
|
||||||
</div>}
|
</div>
|
||||||
{aiEditorState.selectedTool === 'ContinueWriting' && !aiEditorState.isWaitingForResponse && !aiEditorState.error.isError &&
|
)}
|
||||||
<div className='flex flex-col mx-auto justify-center align-middle items-center'>
|
{aiEditorState.selectedTool === 'ContinueWriting' &&
|
||||||
<p className='mx-auto flex p-2 text-white/80 mt-4 font-bold justify-center text-sm align-middle'>Place your cursor at the end of a sentence to continue writing </p>
|
!aiEditorState.isWaitingForResponse &&
|
||||||
<div onClick={() => {
|
!aiEditorState.error.isError && (
|
||||||
handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue)
|
<div className="flex flex-col mx-auto justify-center align-middle items-center">
|
||||||
}} className='flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
<p className="mx-auto flex p-2 text-white/80 mt-4 font-bold justify-center text-sm align-middle">
|
||||||
|
Place your cursor at the end of a sentence to continue writing{' '}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
handleOperation(
|
||||||
|
aiEditorState.selectedTool,
|
||||||
|
aiEditorState.chatInputValue
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
className="flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all"
|
||||||
|
>
|
||||||
<FastForward size={24} />
|
<FastForward size={24} />
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>
|
||||||
{aiEditorState.selectedTool === 'MakeLonger' && !aiEditorState.isWaitingForResponse && !aiEditorState.error.isError &&
|
)}
|
||||||
<div className='flex flex-col mx-auto justify-center align-middle items-center'>
|
{aiEditorState.selectedTool === 'MakeLonger' &&
|
||||||
<p className='mx-auto flex p-2 text-white/80 mt-4 font-bold justify-center text-sm align-middle'>Select text to make longer </p>
|
!aiEditorState.isWaitingForResponse &&
|
||||||
<div onClick={() => {
|
!aiEditorState.error.isError && (
|
||||||
handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue)
|
<div className="flex flex-col mx-auto justify-center align-middle items-center">
|
||||||
}} className='flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
<p className="mx-auto flex p-2 text-white/80 mt-4 font-bold justify-center text-sm align-middle">
|
||||||
|
Select text to make longer{' '}
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
handleOperation(
|
||||||
|
aiEditorState.selectedTool,
|
||||||
|
aiEditorState.chatInputValue
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
className="flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all"
|
||||||
|
>
|
||||||
<FileStack size={24} />
|
<FileStack size={24} />
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
|
||||||
{aiEditorState.selectedTool === 'Translate' && !aiEditorState.isWaitingForResponse && !aiEditorState.error.isError &&
|
|
||||||
<div className='flex flex-col mx-auto justify-center align-middle items-center'>
|
|
||||||
<div className='mx-auto flex p-2 text-white/80 mt-4 font-bold justify-center text-sm align-middle space-x-6'>
|
|
||||||
<p>Translate selected text to </p>
|
|
||||||
<input value={aiEditorState.chatInputValue} onChange={handleChange} placeholder='Japanese, Arabic, German, etc. ' className='ring-1 ring-inset ring-white/20 w-full bg-gray-950/20 rounded-lg outline-none px-4 py- text-white text-sm placeholder:text-white/30'></input>
|
|
||||||
</div>
|
</div>
|
||||||
<div onClick={() => {
|
)}
|
||||||
handleOperation(aiEditorState.selectedTool, aiEditorState.chatInputValue)
|
{aiEditorState.selectedTool === 'Translate' &&
|
||||||
}} className='flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all'>
|
!aiEditorState.isWaitingForResponse &&
|
||||||
|
!aiEditorState.error.isError && (
|
||||||
|
<div className="flex flex-col mx-auto justify-center align-middle items-center">
|
||||||
|
<div className="mx-auto flex p-2 text-white/80 mt-4 font-bold justify-center text-sm align-middle space-x-6">
|
||||||
|
<p>Translate selected text to </p>
|
||||||
|
<input
|
||||||
|
value={aiEditorState.chatInputValue}
|
||||||
|
onChange={handleChange}
|
||||||
|
placeholder="Japanese, Arabic, German, etc. "
|
||||||
|
className="ring-1 ring-inset ring-white/20 w-full bg-gray-950/20 rounded-lg outline-none px-4 py- text-white text-sm placeholder:text-white/30"
|
||||||
|
></input>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
onClick={() => {
|
||||||
|
handleOperation(
|
||||||
|
aiEditorState.selectedTool,
|
||||||
|
aiEditorState.chatInputValue
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
className="flex cursor-pointer space-x-1.5 p-4 mt-4 items-center bg-white/10 rounded-md outline outline-1 outline-neutral-200/20 text-2xl font-semibold text-white/70 hover:bg-white/20 hover:outline-neutral-200/40 delay-75 ease-linear transition-all"
|
||||||
|
>
|
||||||
<Languages size={24} />
|
<Languages size={24} />
|
||||||
</div>
|
</div>
|
||||||
</div>}
|
</div>
|
||||||
{aiEditorState.isWaitingForResponse && !aiEditorState.error.isError && <div className='flex flex-col mx-auto justify-center align-middle items-center'>
|
)}
|
||||||
<svg className="animate-spin mt-10 h-10 w-10 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
{aiEditorState.isWaitingForResponse && !aiEditorState.error.isError && (
|
||||||
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
<div className="flex flex-col mx-auto justify-center align-middle items-center">
|
||||||
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
<svg
|
||||||
|
className="animate-spin mt-10 h-10 w-10 text-white"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<circle
|
||||||
|
className="opacity-25"
|
||||||
|
cx="12"
|
||||||
|
cy="12"
|
||||||
|
r="10"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="4"
|
||||||
|
></circle>
|
||||||
|
<path
|
||||||
|
className="opacity-75"
|
||||||
|
fill="currentColor"
|
||||||
|
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||||
|
></path>
|
||||||
</svg>
|
</svg>
|
||||||
<p className='font-bold mt-4 text-white/90'>Thinking...</p>
|
<p className="font-bold mt-4 text-white/90">Thinking...</p>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{aiEditorState.error.isError && (
|
{aiEditorState.error.isError && (
|
||||||
<div className='flex items-center h-auto pt-7'>
|
<div className="flex items-center h-auto pt-7">
|
||||||
<div className='flex flex-col mx-auto w-full space-y-2 p-5 rounded-lg bg-red-500/20 outline outline-1 outline-red-500'>
|
<div className="flex flex-col mx-auto w-full space-y-2 p-5 rounded-lg bg-red-500/20 outline outline-1 outline-red-500">
|
||||||
<AlertTriangle size={20} className='text-red-500' />
|
<AlertTriangle size={20} className="text-red-500" />
|
||||||
<div className='flex flex-col'>
|
<div className="flex flex-col">
|
||||||
<h3 className='font-semibold text-red-200'>Something wrong happened</h3>
|
<h3 className="font-semibold text-red-200">
|
||||||
<span className='text-red-100 text-sm '>{aiEditorState.error.error_message}</span>
|
Something wrong happened
|
||||||
|
</h3>
|
||||||
|
<span className="text-red-100 text-sm ">
|
||||||
|
{aiEditorState.error.error_message}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
)}
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export default AIEditorToolkit
|
export default AIEditorToolkit
|
||||||
|
|
@ -1,77 +1,79 @@
|
||||||
'use client';
|
'use client'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import { useEditor, EditorContent } from "@tiptap/react";
|
import { useEditor, EditorContent } from '@tiptap/react'
|
||||||
import StarterKit from "@tiptap/starter-kit";
|
import StarterKit from '@tiptap/starter-kit'
|
||||||
import learnhouseIcon from "public/learnhouse_icon.png";
|
import learnhouseIcon from 'public/learnhouse_icon.png'
|
||||||
import { ToolbarButtons } from "./Toolbar/ToolbarButtons";
|
import { ToolbarButtons } from './Toolbar/ToolbarButtons'
|
||||||
import { motion } from "framer-motion";
|
import { motion } from 'framer-motion'
|
||||||
import Image from "next/image";
|
import Image from 'next/image'
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components'
|
||||||
import { DividerVerticalIcon, SlashIcon } from "@radix-ui/react-icons";
|
import { DividerVerticalIcon, SlashIcon } from '@radix-ui/react-icons'
|
||||||
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
|
import learnhouseAI_icon from 'public/learnhouse_ai_simple.png'
|
||||||
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from "@components/Contexts/AI/AIEditorContext";
|
import {
|
||||||
|
AIEditorStateTypes,
|
||||||
|
useAIEditor,
|
||||||
|
useAIEditorDispatch,
|
||||||
|
} from '@components/Contexts/AI/AIEditorContext'
|
||||||
|
|
||||||
// extensions
|
// extensions
|
||||||
import InfoCallout from "./Extensions/Callout/Info/InfoCallout";
|
import InfoCallout from './Extensions/Callout/Info/InfoCallout'
|
||||||
import WarningCallout from "./Extensions/Callout/Warning/WarningCallout";
|
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 { Eye } from "lucide-react";
|
import { Eye } 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/StyledElements/Tooltip/Tooltip";
|
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
|
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
|
||||||
import { OrderedList } from "@tiptap/extension-ordered-list";
|
import { OrderedList } from '@tiptap/extension-ordered-list'
|
||||||
|
|
||||||
|
|
||||||
// Lowlight
|
// Lowlight
|
||||||
import { common, createLowlight } from 'lowlight'
|
import { common, createLowlight } from 'lowlight'
|
||||||
const lowlight = createLowlight(common)
|
const lowlight = createLowlight(common)
|
||||||
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight';
|
import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight'
|
||||||
import css from 'highlight.js/lib/languages/css'
|
import css from 'highlight.js/lib/languages/css'
|
||||||
import js from 'highlight.js/lib/languages/javascript'
|
import js from 'highlight.js/lib/languages/javascript'
|
||||||
import ts from 'highlight.js/lib/languages/typescript'
|
import ts from 'highlight.js/lib/languages/typescript'
|
||||||
import html from 'highlight.js/lib/languages/xml'
|
import html from 'highlight.js/lib/languages/xml'
|
||||||
import python from 'highlight.js/lib/languages/python'
|
import python from 'highlight.js/lib/languages/python'
|
||||||
import java from 'highlight.js/lib/languages/java'
|
import java from 'highlight.js/lib/languages/java'
|
||||||
import { CourseProvider } from "@components/Contexts/CourseContext";
|
import { CourseProvider } from '@components/Contexts/CourseContext'
|
||||||
import { useSession } from "@components/Contexts/SessionContext";
|
import { useSession } from '@components/Contexts/SessionContext'
|
||||||
import AIEditorToolkit from "./AI/AIEditorToolkit";
|
import AIEditorToolkit from './AI/AIEditorToolkit'
|
||||||
import useGetAIFeatures from "@components/AI/Hooks/useGetAIFeatures";
|
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures'
|
||||||
import UserAvatar from "../UserAvatar";
|
import UserAvatar from '../UserAvatar'
|
||||||
|
|
||||||
|
|
||||||
interface Editor {
|
interface Editor {
|
||||||
content: string;
|
content: string
|
||||||
ydoc: any;
|
ydoc: any
|
||||||
provider: any;
|
provider: any
|
||||||
activity: any;
|
activity: any
|
||||||
course: any;
|
course: any
|
||||||
org: any;
|
org: any
|
||||||
setContent: (content: string) => void;
|
setContent: (content: string) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
function Editor(props: Editor) {
|
function Editor(props: Editor) {
|
||||||
const session = useSession() as any;
|
const session = useSession() as any
|
||||||
const dispatchAIEditor = useAIEditorDispatch() as any;
|
const dispatchAIEditor = useAIEditorDispatch() as any
|
||||||
const aiEditorState = useAIEditor() as AIEditorStateTypes;
|
const aiEditorState = useAIEditor() as AIEditorStateTypes
|
||||||
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' });
|
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' })
|
||||||
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false);
|
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false)
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (is_ai_feature_enabled) {
|
if (is_ai_feature_enabled) {
|
||||||
setIsButtonAvailable(true);
|
setIsButtonAvailable(true)
|
||||||
}
|
}
|
||||||
}, [is_ai_feature_enabled])
|
}, [is_ai_feature_enabled])
|
||||||
|
|
||||||
// remove course_ from course_uuid
|
// remove course_ from course_uuid
|
||||||
const course_uuid = props.course.course_uuid.substring(7);
|
const course_uuid = props.course.course_uuid.substring(7)
|
||||||
|
|
||||||
// remove activity_ from activity_uuid
|
// remove activity_ from activity_uuid
|
||||||
const activity_uuid = props.activity.activity_uuid.substring(9);
|
const activity_uuid = props.activity.activity_uuid.substring(9)
|
||||||
|
|
||||||
// Code Block Languages for Lowlight
|
// Code Block Languages for Lowlight
|
||||||
lowlight.register('html', html)
|
lowlight.register('html', html)
|
||||||
|
|
@ -124,7 +126,6 @@ function Editor(props: Editor) {
|
||||||
lowlight,
|
lowlight,
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|
||||||
// Register the document with Tiptap
|
// Register the document with Tiptap
|
||||||
// Collaboration.configure({
|
// Collaboration.configure({
|
||||||
// document: props.ydoc,
|
// document: props.ydoc,
|
||||||
|
|
@ -140,7 +141,7 @@ function Editor(props: Editor) {
|
||||||
],
|
],
|
||||||
|
|
||||||
content: props.content,
|
content: props.content,
|
||||||
});
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page>
|
<Page>
|
||||||
|
|
@ -150,7 +151,7 @@ function Editor(props: Editor) {
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
key="modal"
|
key="modal"
|
||||||
transition={{
|
transition={{
|
||||||
type: "spring",
|
type: 'spring',
|
||||||
stiffness: 360,
|
stiffness: 360,
|
||||||
damping: 70,
|
damping: 70,
|
||||||
delay: 0.02,
|
delay: 0.02,
|
||||||
|
|
@ -161,14 +162,26 @@ function Editor(props: Editor) {
|
||||||
<EditorDocSection>
|
<EditorDocSection>
|
||||||
<EditorInfoWrapper>
|
<EditorInfoWrapper>
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
|
<EditorInfoLearnHouseLogo
|
||||||
|
width={25}
|
||||||
|
height={25}
|
||||||
|
src={learnhouseIcon}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
</Link>
|
</Link>
|
||||||
<Link target="_blank" href={`/course/${course_uuid}`}>
|
<Link target="_blank" href={`/course/${course_uuid}`}>
|
||||||
<EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)}`} alt=""></EditorInfoThumbnail>
|
<EditorInfoThumbnail
|
||||||
|
src={`${getCourseThumbnailMediaDirectory(
|
||||||
|
props.org?.org_uuid,
|
||||||
|
props.course.course_uuid,
|
||||||
|
props.course.thumbnail_image
|
||||||
|
)}`}
|
||||||
|
alt=""
|
||||||
|
></EditorInfoThumbnail>
|
||||||
</Link>
|
</Link>
|
||||||
<EditorInfoDocName>
|
<EditorInfoDocName>
|
||||||
{" "}
|
{' '}
|
||||||
<b>{props.course.name}</b> <SlashIcon /> {props.activity.name}{" "}
|
<b>{props.course.name}</b> <SlashIcon /> {props.activity.name}{' '}
|
||||||
</EditorInfoDocName>
|
</EditorInfoDocName>
|
||||||
</EditorInfoWrapper>
|
</EditorInfoWrapper>
|
||||||
<EditorButtonsWrapper>
|
<EditorButtonsWrapper>
|
||||||
|
|
@ -177,39 +190,82 @@ function Editor(props: Editor) {
|
||||||
</EditorDocSection>
|
</EditorDocSection>
|
||||||
<EditorUsersSection className="space-x-2">
|
<EditorUsersSection className="space-x-2">
|
||||||
<div>
|
<div>
|
||||||
<div className="transition-all ease-linear text-teal-100 rounded-md hover:cursor-pointer" >
|
<div className="transition-all ease-linear text-teal-100 rounded-md hover:cursor-pointer">
|
||||||
{isButtonAvailable && <div
|
{isButtonAvailable && (
|
||||||
onClick={() => dispatchAIEditor({ type: aiEditorState.isModalOpen ? 'setIsModalClose' : 'setIsModalOpen' })}
|
<div
|
||||||
|
onClick={() =>
|
||||||
|
dispatchAIEditor({
|
||||||
|
type: aiEditorState.isModalOpen
|
||||||
|
? 'setIsModalClose'
|
||||||
|
: 'setIsModalOpen',
|
||||||
|
})
|
||||||
|
}
|
||||||
style={{
|
style={{
|
||||||
background: 'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
|
background:
|
||||||
|
'conic-gradient(from 32deg at 53.75% 50%, rgb(35, 40, 93) 4deg, rgba(20, 0, 52, 0.95) 59deg, rgba(164, 45, 238, 0.88) 281deg)',
|
||||||
}}
|
}}
|
||||||
className="rounded-md px-3 py-2 drop-shadow-md flex items-center space-x-1.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105">
|
className="rounded-md px-3 py-2 drop-shadow-md flex items-center space-x-1.5 text-sm text-white hover:cursor-pointer transition delay-150 duration-300 ease-in-out hover:scale-105"
|
||||||
{" "}
|
>
|
||||||
|
{' '}
|
||||||
<i>
|
<i>
|
||||||
<Image className='' width={20} src={learnhouseAI_icon} alt="" />
|
<Image
|
||||||
</i>{" "}
|
className=""
|
||||||
|
width={20}
|
||||||
|
src={learnhouseAI_icon}
|
||||||
|
alt=""
|
||||||
|
/>
|
||||||
|
</i>{' '}
|
||||||
<i className="not-italic text-xs font-bold">AI Editor</i>
|
<i className="not-italic text-xs font-bold">AI Editor</i>
|
||||||
</div>}
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
|
<DividerVerticalIcon
|
||||||
|
style={{
|
||||||
|
marginTop: 'auto',
|
||||||
|
marginBottom: 'auto',
|
||||||
|
color: 'grey',
|
||||||
|
opacity: '0.5',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
<EditorLeftOptionsSection className="space-x-2 ">
|
<EditorLeftOptionsSection className="space-x-2 ">
|
||||||
<div className="bg-sky-600 hover:bg-sky-700 transition-all ease-linear px-3 py-2 font-black text-sm shadow text-teal-100 rounded-lg hover:cursor-pointer" onClick={() => props.setContent(editor.getJSON())}> Save </div>
|
<div
|
||||||
|
className="bg-sky-600 hover:bg-sky-700 transition-all ease-linear px-3 py-2 font-black text-sm shadow text-teal-100 rounded-lg hover:cursor-pointer"
|
||||||
|
onClick={() => props.setContent(editor.getJSON())}
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
Save{' '}
|
||||||
|
</div>
|
||||||
<ToolTip content="Preview">
|
<ToolTip content="Preview">
|
||||||
<Link target="_blank" href={`/course/${course_uuid}/activity/${activity_uuid}`}>
|
<Link
|
||||||
|
target="_blank"
|
||||||
|
href={`/course/${course_uuid}/activity/${activity_uuid}`}
|
||||||
|
>
|
||||||
<div className="flex bg-neutral-600 hover:bg-neutral-700 transition-all ease-linear h-9 px-3 py-2 font-black justify-center items-center text-sm shadow text-neutral-100 rounded-lg hover:cursor-pointer">
|
<div className="flex bg-neutral-600 hover:bg-neutral-700 transition-all ease-linear h-9 px-3 py-2 font-black justify-center items-center text-sm shadow text-neutral-100 rounded-lg hover:cursor-pointer">
|
||||||
<Eye className="mx-auto items-center" size={15} />
|
<Eye className="mx-auto items-center" size={15} />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</ToolTip>
|
</ToolTip>
|
||||||
</EditorLeftOptionsSection>
|
</EditorLeftOptionsSection>
|
||||||
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
|
<DividerVerticalIcon
|
||||||
|
style={{
|
||||||
|
marginTop: 'auto',
|
||||||
|
marginBottom: 'auto',
|
||||||
|
color: 'grey',
|
||||||
|
opacity: '0.5',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
<EditorUserProfileWrapper>
|
<EditorUserProfileWrapper>
|
||||||
{!session.isAuthenticated && <span>Loading</span>}
|
{!session.isAuthenticated && <span>Loading</span>}
|
||||||
{session.isAuthenticated && <UserAvatar width={40} border="border-4" rounded="rounded-full"/>}
|
{session.isAuthenticated && (
|
||||||
|
<UserAvatar
|
||||||
|
width={40}
|
||||||
|
border="border-4"
|
||||||
|
rounded="rounded-full"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</EditorUserProfileWrapper>
|
</EditorUserProfileWrapper>
|
||||||
|
|
||||||
</EditorUsersSection>
|
</EditorUsersSection>
|
||||||
</EditorTop>
|
</EditorTop>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
@ -217,7 +273,7 @@ function Editor(props: Editor) {
|
||||||
initial={{ opacity: 0, scale: 0.99 }}
|
initial={{ opacity: 0, scale: 0.99 }}
|
||||||
animate={{ opacity: 1, scale: 1 }}
|
animate={{ opacity: 1, scale: 1 }}
|
||||||
transition={{
|
transition={{
|
||||||
type: "spring",
|
type: 'spring',
|
||||||
stiffness: 360,
|
stiffness: 360,
|
||||||
damping: 70,
|
damping: 70,
|
||||||
delay: 0.5,
|
delay: 0.5,
|
||||||
|
|
@ -231,7 +287,7 @@ function Editor(props: Editor) {
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</CourseProvider>
|
</CourseProvider>
|
||||||
</Page>
|
</Page>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const Page = styled.div`
|
const Page = styled.div`
|
||||||
|
|
@ -240,12 +296,15 @@ const Page = styled.div`
|
||||||
padding-top: 30px;
|
padding-top: 30px;
|
||||||
|
|
||||||
// dots background
|
// dots background
|
||||||
background-image: radial-gradient(#4744446b 1px, transparent 1px), radial-gradient(#4744446b 1px, transparent 1px);
|
background-image: radial-gradient(#4744446b 1px, transparent 1px),
|
||||||
background-position: 0 0, 25px 25px;
|
radial-gradient(#4744446b 1px, transparent 1px);
|
||||||
|
background-position:
|
||||||
|
0 0,
|
||||||
|
25px 25px;
|
||||||
background-size: 50px 50px;
|
background-size: 50px 50px;
|
||||||
background-attachment: fixed;
|
background-attachment: fixed;
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const EditorTop = styled.div`
|
const EditorTop = styled.div`
|
||||||
border-radius: 15px;
|
border-radius: 15px;
|
||||||
|
|
@ -259,35 +318,34 @@ const EditorTop = styled.div`
|
||||||
position: fixed;
|
position: fixed;
|
||||||
z-index: 303;
|
z-index: 303;
|
||||||
width: -webkit-fill-available;
|
width: -webkit-fill-available;
|
||||||
`;
|
`
|
||||||
|
|
||||||
// Inside EditorTop
|
// Inside EditorTop
|
||||||
const EditorDocSection = styled.div`
|
const EditorDocSection = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
`;
|
`
|
||||||
const EditorUsersSection = styled.div`
|
const EditorUsersSection = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const EditorLeftOptionsSection = styled.div`
|
const EditorLeftOptionsSection = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
`;
|
`
|
||||||
|
|
||||||
|
|
||||||
// Inside EditorDocSection
|
// Inside EditorDocSection
|
||||||
const EditorInfoWrapper = styled.div`
|
const EditorInfoWrapper = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-bottom: 5px;
|
margin-bottom: 5px;
|
||||||
`;
|
`
|
||||||
const EditorButtonsWrapper = styled.div``;
|
const EditorButtonsWrapper = styled.div``
|
||||||
|
|
||||||
// Inside EditorUsersSection
|
// Inside EditorUsersSection
|
||||||
const EditorUserProfileWrapper = styled.div`
|
const EditorUserProfileWrapper = styled.div`
|
||||||
|
|
@ -295,14 +353,14 @@ const EditorUserProfileWrapper = styled.div`
|
||||||
svg {
|
svg {
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
// Inside EditorInfoWrapper
|
// Inside EditorInfoWrapper
|
||||||
//..todo
|
//..todo
|
||||||
const EditorInfoLearnHouseLogo = styled(Image)`
|
const EditorInfoLearnHouseLogo = styled(Image)`
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-right: 0px;
|
margin-right: 0px;
|
||||||
`;
|
`
|
||||||
const EditorInfoDocName = styled.div`
|
const EditorInfoDocName = styled.div`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
@ -317,8 +375,7 @@ const EditorInfoDocName = styled.div`
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
color: #353535;
|
color: #353535;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
|
|
||||||
const EditorInfoThumbnail = styled.img`
|
const EditorInfoThumbnail = styled.img`
|
||||||
height: 25px;
|
height: 25px;
|
||||||
|
|
@ -331,7 +388,7 @@ const EditorInfoThumbnail = styled.img`
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
export const EditorContentWrapper = styled.div`
|
export const EditorContentWrapper = styled.div`
|
||||||
margin: 40px;
|
margin: 40px;
|
||||||
|
|
@ -344,7 +401,6 @@ export const EditorContentWrapper = styled.div`
|
||||||
// disable chrome outline
|
// disable chrome outline
|
||||||
|
|
||||||
.ProseMirror {
|
.ProseMirror {
|
||||||
|
|
||||||
h1 {
|
h1 {
|
||||||
font-size: 30px;
|
font-size: 30px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
|
|
@ -396,7 +452,7 @@ export const EditorContentWrapper = styled.div`
|
||||||
background: #0d0d0d;
|
background: #0d0d0d;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-family: "JetBrainsMono", monospace;
|
font-family: 'JetBrainsMono', monospace;
|
||||||
padding: 0.75rem 1rem;
|
padding: 0.75rem 1rem;
|
||||||
|
|
||||||
code {
|
code {
|
||||||
|
|
@ -458,7 +514,6 @@ export const EditorContentWrapper = styled.div`
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
iframe {
|
iframe {
|
||||||
|
|
@ -472,15 +527,12 @@ export const EditorContentWrapper = styled.div`
|
||||||
outline: 0px solid transparent;
|
outline: 0px solid transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul, ol {
|
ul,
|
||||||
|
ol {
|
||||||
padding: 0 1rem;
|
padding: 0 1rem;
|
||||||
padding-left: 20px;
|
padding-left: 20px;
|
||||||
list-style-type: decimal;
|
list-style-type: decimal;
|
||||||
}
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
export default Editor
|
||||||
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
||||||
export default Editor;
|
|
||||||
|
|
|
||||||
|
|
@ -1,61 +1,66 @@
|
||||||
'use client';
|
'use client'
|
||||||
import { default as React, } from "react";
|
import { default as React } from 'react'
|
||||||
import * as Y from "yjs";
|
import * as Y from 'yjs'
|
||||||
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 'react-hot-toast'
|
||||||
import Toast from "@components/StyledElements/Toast/Toast";
|
import Toast from '@components/StyledElements/Toast/Toast'
|
||||||
import { OrgProvider } from "@components/Contexts/OrgContext";
|
import { OrgProvider } from '@components/Contexts/OrgContext'
|
||||||
|
|
||||||
interface EditorWrapperProps {
|
interface EditorWrapperProps {
|
||||||
content: string;
|
content: string
|
||||||
activity: any;
|
activity: any
|
||||||
course: any
|
course: any
|
||||||
org: any;
|
org: any
|
||||||
}
|
}
|
||||||
|
|
||||||
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>({})
|
||||||
const [ydocState, setYdocState] = React.useState<any>({});
|
const [ydocState, setYdocState] = React.useState<any>({})
|
||||||
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
|
||||||
|
|
||||||
toast.promise(
|
toast.promise(updateActivity(activity, activity.activity_uuid), {
|
||||||
updateActivity(activity, activity.activity_uuid),
|
|
||||||
{
|
|
||||||
loading: 'Saving...',
|
loading: 'Saving...',
|
||||||
success: <b>Activity saved!</b>,
|
success: <b>Activity saved!</b>,
|
||||||
error: <b>Could not save.</b>,
|
error: <b>Could not save.</b>,
|
||||||
}
|
})
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) {
|
if (isLoading) {
|
||||||
createRTCProvider();
|
createRTCProvider()
|
||||||
return <div>Loading...</div>;
|
return <div>Loading...</div>
|
||||||
} else {
|
} else {
|
||||||
return <>
|
return (
|
||||||
|
<>
|
||||||
<Toast></Toast>
|
<Toast></Toast>
|
||||||
<OrgProvider orgslug={props.org.slug}>
|
<OrgProvider orgslug={props.org.slug}>
|
||||||
<Editor org={props.org} course={props.course} activity={props.activity} content={props.content} setContent={setContent} provider={providerState} ydoc={ydocState}></Editor>;
|
<Editor
|
||||||
|
org={props.org}
|
||||||
|
course={props.course}
|
||||||
|
activity={props.activity}
|
||||||
|
content={props.content}
|
||||||
|
setContent={setContent}
|
||||||
|
provider={providerState}
|
||||||
|
ydoc={ydocState}
|
||||||
|
></Editor>
|
||||||
|
;
|
||||||
</OrgProvider>
|
</OrgProvider>
|
||||||
</>
|
</>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EditorWrapper;
|
export default EditorWrapper
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
import { mergeAttributes, Node } from "@tiptap/core";
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||||
|
|
||||||
import InfoCalloutComponent from "./InfoCalloutComponent";
|
import InfoCalloutComponent from './InfoCalloutComponent'
|
||||||
|
|
||||||
export default Node.create({
|
export default Node.create({
|
||||||
name: "calloutInfo",
|
name: 'calloutInfo',
|
||||||
group: "block",
|
group: 'block',
|
||||||
draggable: true,
|
draggable: true,
|
||||||
content: "text*",
|
content: 'text*',
|
||||||
|
|
||||||
// TODO : multi line support
|
// TODO : multi line support
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
tag: "callout-info",
|
tag: 'callout-info',
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
|
return ['callout-info', mergeAttributes(HTMLAttributes), 0]
|
||||||
},
|
},
|
||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer(InfoCalloutComponent);
|
return ReactNodeViewRenderer(InfoCalloutComponent)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,38 @@
|
||||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||||
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
|
||||||
import { AlertCircle } from "lucide-react";
|
import { AlertCircle } from 'lucide-react'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components'
|
||||||
|
|
||||||
function InfoCalloutComponent(props: any) {
|
function InfoCalloutComponent(props: any) {
|
||||||
const editorState = useEditorProvider() as any;
|
const editorState = useEditorProvider() as any
|
||||||
const isEditable = editorState.isEditable;
|
const isEditable = editorState.isEditable
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper>
|
<NodeViewWrapper>
|
||||||
<InfoCalloutWrapper className="flex space-x-2 items-center bg-blue-200 rounded-lg text-blue-900 px-3 shadow-inner" contentEditable={isEditable}>
|
<InfoCalloutWrapper
|
||||||
<AlertCircle /> <NodeViewContent contentEditable={isEditable} className="content" />
|
className="flex space-x-2 items-center bg-blue-200 rounded-lg text-blue-900 px-3 shadow-inner"
|
||||||
|
contentEditable={isEditable}
|
||||||
|
>
|
||||||
|
<AlertCircle />{' '}
|
||||||
|
<NodeViewContent contentEditable={isEditable} className="content" />
|
||||||
</InfoCalloutWrapper>
|
</InfoCalloutWrapper>
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const InfoCalloutWrapper = styled.div`
|
const InfoCalloutWrapper = styled.div`
|
||||||
svg{
|
svg {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border: ${(props) => (props.contentEditable ? "2px dashed #1f3a8a12" : "none")};
|
border: ${(props) =>
|
||||||
|
props.contentEditable ? '2px dashed #1f3a8a12' : 'none'};
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
|
export default InfoCalloutComponent
|
||||||
|
|
||||||
export default InfoCalloutComponent;
|
|
||||||
|
|
|
||||||
|
|
@ -1,29 +1,29 @@
|
||||||
import { mergeAttributes, Node } from "@tiptap/core";
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||||
|
|
||||||
import WarningCalloutComponent from "./WarningCalloutComponent";
|
import WarningCalloutComponent from './WarningCalloutComponent'
|
||||||
|
|
||||||
export default Node.create({
|
export default Node.create({
|
||||||
name: "calloutWarning",
|
name: 'calloutWarning',
|
||||||
group: "block",
|
group: 'block',
|
||||||
draggable: true,
|
draggable: true,
|
||||||
content: "text*",
|
content: 'text*',
|
||||||
|
|
||||||
// TODO : multi line support
|
// TODO : multi line support
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
tag: "callout-warning",
|
tag: 'callout-warning',
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
|
return ['callout-info', mergeAttributes(HTMLAttributes), 0]
|
||||||
},
|
},
|
||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer(WarningCalloutComponent);
|
return ReactNodeViewRenderer(WarningCalloutComponent)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,27 @@
|
||||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||||
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from 'lucide-react'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components'
|
||||||
|
|
||||||
function WarningCalloutComponent(props: any) {
|
function WarningCalloutComponent(props: any) {
|
||||||
const editorState = useEditorProvider() as any;
|
const editorState = useEditorProvider() as any
|
||||||
const isEditable = editorState.isEditable;
|
const isEditable = editorState.isEditable
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper>
|
<NodeViewWrapper>
|
||||||
<CalloutWrapper className="flex space-x-2 items-center bg-yellow-200 rounded-lg text-yellow-900 px-3 shadow-inner" contentEditable={isEditable}>
|
<CalloutWrapper
|
||||||
<AlertTriangle /> <NodeViewContent contentEditable={isEditable} className="content" />
|
className="flex space-x-2 items-center bg-yellow-200 rounded-lg text-yellow-900 px-3 shadow-inner"
|
||||||
|
contentEditable={isEditable}
|
||||||
|
>
|
||||||
|
<AlertTriangle />{' '}
|
||||||
|
<NodeViewContent contentEditable={isEditable} className="content" />
|
||||||
</CalloutWrapper>
|
</CalloutWrapper>
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const CalloutWrapper = styled.div`
|
const CalloutWrapper = styled.div`
|
||||||
|
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
}
|
}
|
||||||
|
|
@ -27,10 +29,11 @@ const CalloutWrapper = styled.div`
|
||||||
.content {
|
.content {
|
||||||
margin: 5px;
|
margin: 5px;
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
|
border: ${(props) =>
|
||||||
|
props.contentEditable ? '2px dashed #713f1117' : 'none'};
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
||||||
const DragHandle = styled.div`
|
const DragHandle = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|
@ -40,6 +43,6 @@ const DragHandle = styled.div`
|
||||||
height: 100%;
|
height: 100%;
|
||||||
cursor: move;
|
cursor: move;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
`;
|
`
|
||||||
|
|
||||||
export default WarningCalloutComponent;
|
export default WarningCalloutComponent
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { mergeAttributes, Node } from "@tiptap/core";
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||||
|
|
||||||
import ImageBlockComponent from "./ImageBlockComponent";
|
import ImageBlockComponent from './ImageBlockComponent'
|
||||||
|
|
||||||
export default Node.create({
|
export default Node.create({
|
||||||
name: "blockImage",
|
name: 'blockImage',
|
||||||
group: "block",
|
group: 'block',
|
||||||
|
|
||||||
atom: true,
|
atom: true,
|
||||||
|
|
||||||
|
|
@ -17,22 +17,22 @@ export default Node.create({
|
||||||
size: {
|
size: {
|
||||||
width: 300,
|
width: 300,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
tag: "block-image",
|
tag: 'block-image',
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ["block-image", mergeAttributes(HTMLAttributes), 0];
|
return ['block-image', mergeAttributes(HTMLAttributes), 0]
|
||||||
},
|
},
|
||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer(ImageBlockComponent);
|
return ReactNodeViewRenderer(ImageBlockComponent)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,95 +1,136 @@
|
||||||
import { NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewWrapper } from '@tiptap/react'
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from 'react'
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components'
|
||||||
import { Resizable } from 're-resizable';
|
import { Resizable } from 're-resizable'
|
||||||
import { AlertTriangle, Image, Loader } from "lucide-react";
|
import { AlertTriangle, Image, Loader } from 'lucide-react'
|
||||||
import { uploadNewImageFile } from "../../../../../services/blocks/Image/images";
|
import { uploadNewImageFile } from '../../../../../services/blocks/Image/images'
|
||||||
import { UploadIcon } from "@radix-ui/react-icons";
|
import { UploadIcon } from '@radix-ui/react-icons'
|
||||||
import { getActivityBlockMediaDirectory } from "@services/media/media";
|
import { getActivityBlockMediaDirectory } from '@services/media/media'
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import { useCourse } from "@components/Contexts/CourseContext";
|
import { useCourse } from '@components/Contexts/CourseContext'
|
||||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||||
|
|
||||||
function ImageBlockComponent(props: any) {
|
function ImageBlockComponent(props: any) {
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const course = useCourse() as any;
|
const course = useCourse() as any
|
||||||
const editorState = useEditorProvider() as any;
|
const editorState = useEditorProvider() as any
|
||||||
|
|
||||||
const isEditable = editorState.isEditable;
|
const isEditable = editorState.isEditable
|
||||||
const [image, setImage] = React.useState(null);
|
const [image, setImage] = React.useState(null)
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false)
|
||||||
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
|
const [blockObject, setblockObject] = React.useState(
|
||||||
const [imageSize, setImageSize] = React.useState({ width: props.node.attrs.size ? props.node.attrs.size.width : 300 });
|
props.node.attrs.blockObject
|
||||||
const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null;
|
)
|
||||||
|
const [imageSize, setImageSize] = React.useState({
|
||||||
|
width: props.node.attrs.size ? props.node.attrs.size.width : 300,
|
||||||
|
})
|
||||||
|
const fileId = blockObject
|
||||||
|
? `${blockObject.content.file_id}.${blockObject.content.file_format}`
|
||||||
|
: null
|
||||||
const handleImageChange = (event: React.ChangeEvent<any>) => {
|
const handleImageChange = (event: React.ChangeEvent<any>) => {
|
||||||
setImage(event.target.files[0]);
|
setImage(event.target.files[0])
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: any) => {
|
const handleSubmit = async (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setIsLoading(true);
|
setIsLoading(true)
|
||||||
let object = await uploadNewImageFile(image, props.extension.options.activity.activity_uuid);
|
let object = await uploadNewImageFile(
|
||||||
setIsLoading(false);
|
image,
|
||||||
setblockObject(object);
|
props.extension.options.activity.activity_uuid
|
||||||
|
)
|
||||||
|
setIsLoading(false)
|
||||||
|
setblockObject(object)
|
||||||
props.updateAttributes({
|
props.updateAttributes({
|
||||||
blockObject: object,
|
blockObject: object,
|
||||||
size: imageSize,
|
size: imageSize,
|
||||||
});
|
})
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
}
|
}
|
||||||
, [course, org]);
|
|
||||||
|
useEffect(() => {}, [course, org])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper className="block-image">
|
<NodeViewWrapper className="block-image">
|
||||||
{!blockObject && isEditable && (
|
{!blockObject && isEditable && (
|
||||||
<BlockImageWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={isEditable}>
|
<BlockImageWrapper
|
||||||
|
className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2"
|
||||||
|
contentEditable={isEditable}
|
||||||
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
|
<Loader
|
||||||
|
className="animate-spin animate-pulse text-gray-200"
|
||||||
|
size={50}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<Image className="text-gray-200" size={50} />
|
<Image className="text-gray-200" size={50} />
|
||||||
</div>
|
</div>
|
||||||
<input className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500" onChange={handleImageChange} type="file" name="" id="" />
|
<input
|
||||||
<button className='p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex' onClick={handleSubmit}><UploadIcon></UploadIcon><p>Submit</p></button>
|
className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500"
|
||||||
|
onChange={handleImageChange}
|
||||||
|
type="file"
|
||||||
|
name=""
|
||||||
|
id=""
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
<UploadIcon></UploadIcon>
|
||||||
|
<p>Submit</p>
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</BlockImageWrapper>
|
</BlockImageWrapper>
|
||||||
)}
|
)}
|
||||||
{blockObject && (
|
{blockObject && (
|
||||||
<Resizable defaultSize={{ width: imageSize.width, height: "100%" }}
|
<Resizable
|
||||||
|
defaultSize={{ width: imageSize.width, height: '100%' }}
|
||||||
handleStyles={{
|
handleStyles={{
|
||||||
right: { position: 'unset', width: 7, height: 30, borderRadius: 20, cursor: 'col-resize', backgroundColor: 'black', opacity: '0.3', margin: 'auto', marginLeft: 5 },
|
right: {
|
||||||
|
position: 'unset',
|
||||||
|
width: 7,
|
||||||
|
height: 30,
|
||||||
|
borderRadius: 20,
|
||||||
|
cursor: 'col-resize',
|
||||||
|
backgroundColor: 'black',
|
||||||
|
opacity: '0.3',
|
||||||
|
margin: 'auto',
|
||||||
|
marginLeft: 5,
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
style={{
|
||||||
|
margin: 'auto',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
height: '100%',
|
||||||
}}
|
}}
|
||||||
style={{ margin: "auto", display: "flex", justifyContent: "center", alignItems: "center", height: "100%" }}
|
|
||||||
maxWidth={1000}
|
maxWidth={1000}
|
||||||
minWidth={200}
|
minWidth={200}
|
||||||
onResizeStop={(e, direction, ref, d) => {
|
onResizeStop={(e, direction, ref, d) => {
|
||||||
props.updateAttributes({
|
props.updateAttributes({
|
||||||
size: {
|
size: {
|
||||||
width: imageSize.width + d.width,
|
width: imageSize.width + d.width,
|
||||||
}
|
},
|
||||||
});
|
})
|
||||||
setImageSize({
|
setImageSize({
|
||||||
width: imageSize.width + d.width,
|
width: imageSize.width + d.width,
|
||||||
});
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|
||||||
<img
|
<img
|
||||||
src={`${getActivityBlockMediaDirectory(org?.org_uuid,
|
src={`${getActivityBlockMediaDirectory(
|
||||||
|
org?.org_uuid,
|
||||||
course?.courseStructure.course_uuid,
|
course?.courseStructure.course_uuid,
|
||||||
props.extension.options.activity.activity_uuid,
|
props.extension.options.activity.activity_uuid,
|
||||||
blockObject.block_uuid,
|
blockObject.block_uuid,
|
||||||
blockObject ? fileId : ' ', 'imageBlock')}`}
|
blockObject ? fileId : ' ',
|
||||||
|
'imageBlock'
|
||||||
|
)}`}
|
||||||
alt=""
|
alt=""
|
||||||
className="rounded-lg shadow "
|
className="rounded-lg shadow "
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|
||||||
</Resizable>
|
</Resizable>
|
||||||
)}
|
)}
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
|
|
@ -98,29 +139,24 @@ function ImageBlockComponent(props: any) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ImageBlockComponent;
|
export default ImageBlockComponent
|
||||||
|
|
||||||
const BlockImageWrapper = styled.div`
|
const BlockImageWrapper = styled.div`
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
`
|
||||||
`;
|
|
||||||
|
|
||||||
const BlockImage = styled.div`
|
const BlockImage = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
|
|
||||||
// center
|
// center
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
`
|
||||||
|
|
||||||
|
|
||||||
`;
|
|
||||||
|
|
|
||||||
|
|
@ -1,35 +1,35 @@
|
||||||
import { mergeAttributes, Node } from "@tiptap/core";
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||||
|
|
||||||
import MathEquationBlockComponent from "./MathEquationBlockComponent";
|
import MathEquationBlockComponent from './MathEquationBlockComponent'
|
||||||
|
|
||||||
export default Node.create({
|
export default Node.create({
|
||||||
name: "blockMathEquation",
|
name: 'blockMathEquation',
|
||||||
group: "block",
|
group: 'block',
|
||||||
|
|
||||||
atom: true,
|
atom: true,
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
return {
|
return {
|
||||||
math_equation: {
|
math_equation: {
|
||||||
default: "",
|
default: '',
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
tag: "block-math-equation",
|
tag: 'block-math-equation',
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ["block-math-equation", mergeAttributes(HTMLAttributes), 0];
|
return ['block-math-equation', mergeAttributes(HTMLAttributes), 0]
|
||||||
},
|
},
|
||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer(MathEquationBlockComponent);
|
return ReactNodeViewRenderer(MathEquationBlockComponent)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,31 +1,31 @@
|
||||||
import { NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewWrapper } from '@tiptap/react'
|
||||||
import React from "react";
|
import React from 'react'
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components'
|
||||||
import "katex/dist/katex.min.css";
|
import 'katex/dist/katex.min.css'
|
||||||
import { BlockMath } from "react-katex";
|
import { BlockMath } from 'react-katex'
|
||||||
import { Save } from "lucide-react";
|
import { Save } from 'lucide-react'
|
||||||
import Link from "next/link";
|
import Link from 'next/link'
|
||||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||||
|
|
||||||
function MathEquationBlockComponent(props: any) {
|
function MathEquationBlockComponent(props: any) {
|
||||||
const [equation, setEquation] = React.useState(props.node.attrs.math_equation);
|
const [equation, setEquation] = React.useState(props.node.attrs.math_equation)
|
||||||
const [isEditing, setIsEditing] = React.useState(true);
|
const [isEditing, setIsEditing] = React.useState(true)
|
||||||
const editorState = useEditorProvider() as any;
|
const editorState = useEditorProvider() as any
|
||||||
const isEditable = editorState.isEditable;
|
const isEditable = editorState.isEditable
|
||||||
|
|
||||||
const handleEquationChange = (event: React.ChangeEvent<any>) => {
|
const handleEquationChange = (event: React.ChangeEvent<any>) => {
|
||||||
setEquation(event.target.value);
|
setEquation(event.target.value)
|
||||||
props.updateAttributes({
|
props.updateAttributes({
|
||||||
math_equation: equation,
|
math_equation: equation,
|
||||||
});
|
})
|
||||||
};
|
}
|
||||||
|
|
||||||
const saveEquation = () => {
|
const saveEquation = () => {
|
||||||
props.updateAttributes({
|
props.updateAttributes({
|
||||||
math_equation: equation,
|
math_equation: equation,
|
||||||
});
|
})
|
||||||
//setIsEditing(false);
|
//setIsEditing(false);
|
||||||
};
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper className="block-math-equation">
|
<NodeViewWrapper className="block-math-equation">
|
||||||
|
|
@ -34,24 +34,38 @@ function MathEquationBlockComponent(props: any) {
|
||||||
{isEditing && isEditable && (
|
{isEditing && isEditable && (
|
||||||
<>
|
<>
|
||||||
<EditBar>
|
<EditBar>
|
||||||
<input value={equation} onChange={handleEquationChange} placeholder="Insert a Math Equation (LaTeX) " type="text" />
|
<input
|
||||||
|
value={equation}
|
||||||
|
onChange={handleEquationChange}
|
||||||
|
placeholder="Insert a Math Equation (LaTeX) "
|
||||||
|
type="text"
|
||||||
|
/>
|
||||||
<button className="opacity-1" onClick={() => saveEquation()}>
|
<button className="opacity-1" onClick={() => saveEquation()}>
|
||||||
<Save size={15}></Save>
|
<Save size={15}></Save>
|
||||||
</button>
|
</button>
|
||||||
</EditBar>
|
</EditBar>
|
||||||
<span className="pt-2 text-zinc-500 text-sm">Please refer to this <Link className="text-zinc-900 after:content-['↗']" href="https://katex.org/docs/supported.html" target="_blank"> guide</Link> for supported TeX functions </span>
|
<span className="pt-2 text-zinc-500 text-sm">
|
||||||
|
Please refer to this{' '}
|
||||||
|
<Link
|
||||||
|
className="text-zinc-900 after:content-['↗']"
|
||||||
|
href="https://katex.org/docs/supported.html"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
{' '}
|
||||||
|
guide
|
||||||
|
</Link>{' '}
|
||||||
|
for supported TeX functions{' '}
|
||||||
|
</span>
|
||||||
</>
|
</>
|
||||||
|
|
||||||
)}
|
)}
|
||||||
</MathEqWrapper>
|
</MathEqWrapper>
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MathEquationBlockComponent;
|
export default MathEquationBlockComponent
|
||||||
|
|
||||||
const MathEqWrapper = styled.div`
|
const MathEqWrapper = styled.div``
|
||||||
`;
|
|
||||||
|
|
||||||
const EditBar = styled.div`
|
const EditBar = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -82,7 +96,7 @@ const EditBar = styled.div`
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
color: #494949;
|
color: #494949;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
font-family: "DM Sans", sans-serif;
|
font-family: 'DM Sans', sans-serif;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
&:focus {
|
&:focus {
|
||||||
outline: none;
|
outline: none;
|
||||||
|
|
@ -92,4 +106,4 @@ const EditBar = styled.div`
|
||||||
color: #49494936;
|
color: #49494936;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Extension } from '@tiptap/core';
|
import { Extension } from '@tiptap/core'
|
||||||
import { Plugin, PluginKey } from 'prosemirror-state';
|
import { Plugin, PluginKey } from 'prosemirror-state'
|
||||||
|
|
||||||
export const NoTextInput = Extension.create({
|
export const NoTextInput = Extension.create({
|
||||||
name: 'noTextInput',
|
name: 'noTextInput',
|
||||||
|
|
@ -10,12 +10,20 @@ export const NoTextInput = Extension.create({
|
||||||
key: new PluginKey('noTextInput'),
|
key: new PluginKey('noTextInput'),
|
||||||
filterTransaction: (transaction) => {
|
filterTransaction: (transaction) => {
|
||||||
// If the transaction is adding text, stop it
|
// If the transaction is adding text, stop it
|
||||||
return !transaction.docChanged || transaction.steps.every((step) => {
|
return (
|
||||||
const { slice } = step.toJSON();
|
!transaction.docChanged ||
|
||||||
return !slice || !slice.content.some((node: { type: string; }) => node.type === 'text');
|
transaction.steps.every((step) => {
|
||||||
});
|
const { slice } = step.toJSON()
|
||||||
|
return (
|
||||||
|
!slice ||
|
||||||
|
!slice.content.some(
|
||||||
|
(node: { type: string }) => node.type === 'text'
|
||||||
|
)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
)
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
];
|
]
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { mergeAttributes, Node } from "@tiptap/core";
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||||
|
|
||||||
import PDFBlockComponent from "./PDFBlockComponent";
|
import PDFBlockComponent from './PDFBlockComponent'
|
||||||
|
|
||||||
export default Node.create({
|
export default Node.create({
|
||||||
name: "blockPDF",
|
name: 'blockPDF',
|
||||||
group: "block",
|
group: 'block',
|
||||||
|
|
||||||
atom: true,
|
atom: true,
|
||||||
|
|
||||||
|
|
@ -14,22 +14,22 @@ export default Node.create({
|
||||||
blockObject: {
|
blockObject: {
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
tag: "block-pdf",
|
tag: 'block-pdf',
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ["block-pdf", mergeAttributes(HTMLAttributes), 0];
|
return ['block-pdf', mergeAttributes(HTMLAttributes), 0]
|
||||||
},
|
},
|
||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer(PDFBlockComponent);
|
return ReactNodeViewRenderer(PDFBlockComponent)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
|
||||||
|
|
@ -1,56 +1,79 @@
|
||||||
import { NodeViewWrapper } from "@tiptap/react";
|
import { NodeViewWrapper } from '@tiptap/react'
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect } from 'react'
|
||||||
import styled from "styled-components";
|
import styled from 'styled-components'
|
||||||
import { AlertTriangle, FileText, Loader } from "lucide-react";
|
import { AlertTriangle, FileText, Loader } from 'lucide-react'
|
||||||
import { uploadNewPDFFile } from "../../../../../services/blocks/Pdf/pdf";
|
import { uploadNewPDFFile } from '../../../../../services/blocks/Pdf/pdf'
|
||||||
import { UploadIcon } from "@radix-ui/react-icons";
|
import { UploadIcon } from '@radix-ui/react-icons'
|
||||||
import { getActivityBlockMediaDirectory } from "@services/media/media";
|
import { getActivityBlockMediaDirectory } from '@services/media/media'
|
||||||
import { useOrg } from "@components/Contexts/OrgContext";
|
import { useOrg } from '@components/Contexts/OrgContext'
|
||||||
import { useCourse } from "@components/Contexts/CourseContext";
|
import { useCourse } from '@components/Contexts/CourseContext'
|
||||||
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
|
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
|
||||||
|
|
||||||
function PDFBlockComponent(props: any) {
|
function PDFBlockComponent(props: any) {
|
||||||
const org = useOrg() as any;
|
const org = useOrg() as any
|
||||||
const course = useCourse() as any;
|
const course = useCourse() as any
|
||||||
const [pdf, setPDF] = React.useState(null);
|
const [pdf, setPDF] = React.useState(null)
|
||||||
const [isLoading, setIsLoading] = React.useState(false);
|
const [isLoading, setIsLoading] = React.useState(false)
|
||||||
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
|
const [blockObject, setblockObject] = React.useState(
|
||||||
const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null;
|
props.node.attrs.blockObject
|
||||||
const editorState = useEditorProvider() as any;
|
)
|
||||||
const isEditable = editorState.isEditable;
|
const fileId = blockObject
|
||||||
|
? `${blockObject.content.file_id}.${blockObject.content.file_format}`
|
||||||
|
: null
|
||||||
|
const editorState = useEditorProvider() as any
|
||||||
|
const isEditable = editorState.isEditable
|
||||||
|
|
||||||
const handlePDFChange = (event: React.ChangeEvent<any>) => {
|
const handlePDFChange = (event: React.ChangeEvent<any>) => {
|
||||||
setPDF(event.target.files[0]);
|
setPDF(event.target.files[0])
|
||||||
};
|
}
|
||||||
|
|
||||||
const handleSubmit = async (e: any) => {
|
const handleSubmit = async (e: any) => {
|
||||||
e.preventDefault();
|
e.preventDefault()
|
||||||
setIsLoading(true);
|
setIsLoading(true)
|
||||||
let object = await uploadNewPDFFile(pdf, props.extension.options.activity.activity_uuid);
|
let object = await uploadNewPDFFile(
|
||||||
setIsLoading(false);
|
pdf,
|
||||||
setblockObject(object);
|
props.extension.options.activity.activity_uuid
|
||||||
|
)
|
||||||
|
setIsLoading(false)
|
||||||
|
setblockObject(object)
|
||||||
props.updateAttributes({
|
props.updateAttributes({
|
||||||
blockObject: object,
|
blockObject: object,
|
||||||
});
|
})
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
}
|
}
|
||||||
, [course, org]);
|
|
||||||
|
useEffect(() => {}, [course, org])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<NodeViewWrapper className="block-pdf">
|
<NodeViewWrapper className="block-pdf">
|
||||||
{!blockObject && (
|
{!blockObject && (
|
||||||
<BlockPDFWrapper className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2" contentEditable={isEditable}>
|
<BlockPDFWrapper
|
||||||
|
className="flex items-center space-x-3 py-7 bg-gray-50 rounded-xl text-gray-900 px-3 border-dashed border-gray-150 border-2"
|
||||||
|
contentEditable={isEditable}
|
||||||
|
>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
|
<Loader
|
||||||
|
className="animate-spin animate-pulse text-gray-200"
|
||||||
|
size={50}
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<FileText className="text-gray-200" size={50} />
|
<FileText className="text-gray-200" size={50} />
|
||||||
</div>
|
</div>
|
||||||
<input className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500" onChange={handlePDFChange} type="file" name="" id="" />
|
<input
|
||||||
<button className='p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex' onClick={handleSubmit}><UploadIcon></UploadIcon><p>Submit</p></button>
|
className="p-3 rounded-lg file:mr-4 file:py-2 file:px-4 file:rounded-full file:border-0 hover:file:cursor-pointer file:bg-gray-200 cursor-pointer file:text-gray-500"
|
||||||
|
onChange={handlePDFChange}
|
||||||
|
type="file"
|
||||||
|
name=""
|
||||||
|
id=""
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
className="p-2 px-3 bg-gray-200 rounded-lg text-gray-500 hover:bg-gray-300 transition space-x-2 items-center flex"
|
||||||
|
onClick={handleSubmit}
|
||||||
|
>
|
||||||
|
<UploadIcon></UploadIcon>
|
||||||
|
<p>Submit</p>
|
||||||
|
</button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</BlockPDFWrapper>
|
</BlockPDFWrapper>
|
||||||
|
|
@ -59,11 +82,14 @@ function PDFBlockComponent(props: any) {
|
||||||
<BlockPDF>
|
<BlockPDF>
|
||||||
<iframe
|
<iframe
|
||||||
className="shadow rounded-lg h-96 w-full object-scale-down bg-black"
|
className="shadow rounded-lg h-96 w-full object-scale-down bg-black"
|
||||||
src={`${getActivityBlockMediaDirectory(org?.org_uuid,
|
src={`${getActivityBlockMediaDirectory(
|
||||||
|
org?.org_uuid,
|
||||||
course?.courseStructure.course_uuid,
|
course?.courseStructure.course_uuid,
|
||||||
props.extension.options.activity.activity_uuid,
|
props.extension.options.activity.activity_uuid,
|
||||||
blockObject.block_uuid,
|
blockObject.block_uuid,
|
||||||
blockObject ? fileId : ' ', 'pdfBlock')}`}
|
blockObject ? fileId : ' ',
|
||||||
|
'pdfBlock'
|
||||||
|
)}`}
|
||||||
/>
|
/>
|
||||||
</BlockPDF>
|
</BlockPDF>
|
||||||
)}
|
)}
|
||||||
|
|
@ -73,19 +99,18 @@ function PDFBlockComponent(props: any) {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</NodeViewWrapper>
|
</NodeViewWrapper>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default PDFBlockComponent;
|
export default PDFBlockComponent
|
||||||
|
|
||||||
const BlockPDFWrapper = styled.div`
|
const BlockPDFWrapper = styled.div`
|
||||||
|
|
||||||
// center
|
// center
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
`;
|
`
|
||||||
|
|
||||||
const BlockPDF = styled.div`
|
const BlockPDF = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
@ -97,5 +122,5 @@ const BlockPDF = styled.div`
|
||||||
// cover
|
// cover
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
}
|
}
|
||||||
`;
|
`
|
||||||
const PDFNotFound = styled.div``;
|
const PDFNotFound = styled.div``
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
import { mergeAttributes, Node } from "@tiptap/core";
|
import { mergeAttributes, Node } from '@tiptap/core'
|
||||||
import { ReactNodeViewRenderer } from "@tiptap/react";
|
import { ReactNodeViewRenderer } from '@tiptap/react'
|
||||||
|
|
||||||
import QuizBlockComponent from "./QuizBlockComponent";
|
import QuizBlockComponent from './QuizBlockComponent'
|
||||||
|
|
||||||
export default Node.create({
|
export default Node.create({
|
||||||
name: "blockQuiz",
|
name: 'blockQuiz',
|
||||||
group: "block",
|
group: 'block',
|
||||||
atom: true,
|
atom: true,
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
|
|
@ -16,22 +16,22 @@ export default Node.create({
|
||||||
questions: {
|
questions: {
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
};
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
parseHTML() {
|
parseHTML() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
tag: "block-quiz",
|
tag: 'block-quiz',
|
||||||
},
|
},
|
||||||
];
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
renderHTML({ HTMLAttributes }) {
|
renderHTML({ HTMLAttributes }) {
|
||||||
return ["block-quiz", mergeAttributes(HTMLAttributes), 0];
|
return ['block-quiz', mergeAttributes(HTMLAttributes), 0]
|
||||||
},
|
},
|
||||||
|
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return ReactNodeViewRenderer(QuizBlockComponent);
|
return ReactNodeViewRenderer(QuizBlockComponent)
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue