feat: format with prettier

This commit is contained in:
swve 2024-02-09 21:22:15 +01:00
parent 03fb09c3d6
commit a147ad6610
164 changed files with 11257 additions and 8154 deletions

View file

@ -1,19 +1,19 @@
import { NextRequest, NextResponse } from "next/server";
import { revalidateTag } from "next/cache";
import { NextRequest, NextResponse } from 'next/server'
import { revalidateTag } from 'next/cache'
export async function GET(request: NextRequest) {
const tag: any = request.nextUrl.searchParams.get("tag");
revalidateTag(tag);
const tag: any = request.nextUrl.searchParams.get('tag')
revalidateTag(tag)
return NextResponse.json(
{ revalidated: true, now: Date.now(), tag },
{
status: 200,
headers: {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
}
);
)
}

View file

@ -1,9 +1,6 @@
import PageLoading from "@components/Objects/Loaders/PageLoading";
import PageLoading from '@components/Objects/Loaders/PageLoading'
export default function Loading() {
// Or a custom loading skeleton component
return (
<PageLoading></PageLoading>
)
}
// Or a custom loading skeleton component
return <PageLoading></PageLoading>
}

View file

@ -1,52 +1,72 @@
import { default as React, } from "react";
import EditorWrapper from "@components/Objects/Editor/EditorWrapper";
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
import { cookies } from "next/headers";
import { Metadata } from "next";
import { getActivityWithAuthHeader } from "@services/courses/activities";
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
import { getOrganizationContextInfoWithId } from "@services/organizations/orgs";
import SessionProvider from "@components/Contexts/SessionContext";
import EditorOptionsProvider from "@components/Contexts/Editor/EditorContext";
import AIEditorProvider from "@components/Contexts/AI/AIEditorContext";
import { default as React } from 'react'
import EditorWrapper from '@components/Objects/Editor/EditorWrapper'
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses'
import { cookies } from 'next/headers'
import { Metadata } from 'next'
import { getActivityWithAuthHeader } from '@services/courses/activities'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import { getOrganizationContextInfoWithId } from '@services/organizations/orgs'
import SessionProvider from '@components/Contexts/SessionContext'
import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext'
import AIEditorProvider from '@components/Contexts/AI/AIEditorContext'
type MetadataProps = {
params: { orgslug: string, courseid: string, activityid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
params: { orgslug: string; courseid: string; activityid: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const cookieStore = cookies();
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
// Get Org context information
const course_meta = await getCourseMetadataWithAuthHeader(params.courseid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
// Get Org context information
const course_meta = await getCourseMetadataWithAuthHeader(
params.courseid,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null
)
return {
title: `Edit - ${course_meta.name} Activity`,
description: course_meta.mini_description,
};
}
}
const EditActivity = async (params: any) => {
const cookieStore = cookies();
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const activityuuid = params.params.activityuuid;
const courseid = params.params.courseid;
const courseInfo = await getCourseMetadataWithAuthHeader(courseid, { 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'] });
const activityuuid = params.params.activityuuid
const courseid = params.params.courseid
const courseInfo = await getCourseMetadataWithAuthHeader(
courseid,
{ 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 (
<EditorOptionsProvider options={{ isEditable: true }}>
<AIEditorProvider>
<SessionProvider>
<EditorWrapper org={org} course={courseInfo} activity={activity} content={activity.content}></EditorWrapper>
<EditorWrapper
org={org}
course={courseInfo}
activity={activity}
content={activity.content}
></EditorWrapper>
</SessionProvider>
</AIEditorProvider>
</EditorOptionsProvider>
);
)
}
export default EditActivity;
export default EditActivity

View file

@ -1 +1 @@
export const EDITOR = "main";
export const EDITOR = 'main'

View file

@ -1,24 +1,24 @@
"use client";
'use client'
import * as Sentry from "@sentry/nextjs";
import NextError from "next/error";
import { useEffect } from "react";
import * as Sentry from '@sentry/nextjs'
import NextError from 'next/error'
import { useEffect } from 'react'
export default function GlobalError({
error,
error,
}: {
error: Error & { digest?: string };
error: Error & { digest?: string }
}) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
useEffect(() => {
Sentry.captureException(error)
}, [error])
return (
<html>
<body>
{/* This is the default Next.js error component but it doesn't allow omitting the statusCode property yet. */}
<NextError statusCode={undefined as any} />
</body>
</html>
);
}
return (
<html>
<body>
{/* This is the default Next.js error component but it doesn't allow omitting the statusCode property yet. */}
<NextError statusCode={undefined as any} />
</body>
</html>
)
}

View file

@ -5,87 +5,118 @@ import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWra
import { useRouter, useSearchParams } from 'next/navigation'
import { Suspense } from 'react'
function InstallClient() {
return (
<GeneralWrapperStyled>
<Suspense>
<>
<Stepscomp />
</>
</Suspense>
</GeneralWrapperStyled>
)
return (
<GeneralWrapperStyled>
<Suspense>
<>
<Stepscomp />
</>
</Suspense>
</GeneralWrapperStyled>
)
}
const Stepscomp = () => {
const searchParams = useSearchParams()
const router = useRouter()
const step: any = parseInt(searchParams.get('step') || '0');
const [stepNumber, setStepNumber] = React.useState(step)
const [stepsState, setStepsState] = React.useState(INSTALL_STEPS)
const searchParams = useSearchParams()
const router = useRouter()
const step: any = parseInt(searchParams.get('step') || '0')
const [stepNumber, setStepNumber] = React.useState(step)
const [stepsState, setStepsState] = React.useState(INSTALL_STEPS)
function handleStepChange(stepNumber: number) {
setStepNumber(stepNumber)
router.push(`/install?step=${stepNumber}`)
}
function handleStepChange(stepNumber: number) {
setStepNumber(stepNumber)
router.push(`/install?step=${stepNumber}`)
}
useEffect(() => {
setStepNumber(step)
}, [step])
useEffect(() => {
setStepNumber(step)
}, [step])
return (
<div>
<div className='flex justify-center '>
<div className='grow'>
<LearnHouseLogo />
</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="flex space-x-8">
{stepsState.map((step, index) => (
<div
key={index}
className={`flex items-center cursor-pointer space-x-2`}
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' : ''}`}>
{index}
</div>
<div>{step.name}</div>
</div>
))}
</div>
</div>
</div>
<div className="flex pt-8 flex-col" >
<h1 className='font-bold text-3xl'>{stepsState[stepNumber].name}</h1>
<div className="pt-8">
{stepsState[stepNumber].component}
</div>
</div>
return (
<div>
<div className="flex justify-center ">
<div className="grow">
<LearnHouseLogo />
</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="flex space-x-8">
{stepsState.map((step, index) => (
<div
key={index}
className={`flex items-center cursor-pointer space-x-2`}
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' : ''
}`}
>
{index}
</div>
<div>{step.name}</div>
</div>
))}
</div>
</div>
</div>
<div className="flex pt-8 flex-col">
<h1 className="font-bold text-3xl">{stepsState[stepNumber].name}</h1>
<div className="pt-8">{stepsState[stepNumber].component}</div>
</div>
</div>
)
}
const LearnHouseLogo = () => {
return (
<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="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>
<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 offset="0.442708" stopOpacity="0.1" />
</radialGradient>
</defs>
</svg>
)
return (
<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="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>
<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 offset="0.442708" stopOpacity="0.1" />
</radialGradient>
</defs>
</svg>
)
}
export default InstallClient
export default InstallClient

View file

@ -1,18 +1,17 @@
import React from 'react'
import InstallClient from './install'
export const metadata = {
title: "Install LearnHouse",
description: "Install Learnhouse on your server",
title: 'Install LearnHouse',
description: 'Install Learnhouse on your server',
}
function InstallPage() {
return (
<div className='bg-white h-screen'>
<InstallClient />
</div>
)
return (
<div className="bg-white h-screen">
<InstallClient />
</div>
)
}
export default InstallPage
export default InstallPage

View file

@ -1,131 +1,178 @@
"use client";
import FormLayout, { ButtonBlack, FormField, FormLabelAndMessage, Input } from '@components/StyledElements/Form/Form'
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';
'use client'
import FormLayout, {
ButtonBlack,
FormField,
FormLabelAndMessage,
Input,
} from '@components/StyledElements/Form/Form'
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 { BarLoader } from 'react-spinners';
import useSWR from "swr";
import { BarLoader } from 'react-spinners'
import useSWR from 'swr'
const validate = (values: any) => {
const errors: any = {};
const errors: any = {}
if (!values.email) {
errors.email = 'Required';
}
else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address';
}
if (!values.email) {
errors.email = 'Required'
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
errors.email = 'Invalid email address'
}
if (!values.password) {
errors.password = 'Required';
}
else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
if (!values.password) {
errors.password = 'Required'
} else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters'
}
if (!values.confirmPassword) {
errors.confirmPassword = 'Required';
}
else if (values.confirmPassword !== values.password) {
errors.confirmPassword = 'Passwords must match';
}
if (!values.confirmPassword) {
errors.confirmPassword = 'Required'
} else if (values.confirmPassword !== values.password) {
errors.confirmPassword = 'Passwords must match'
}
if (!values.username) {
errors.username = 'Required';
}
else if (values.username.length < 3) {
errors.username = 'Username must be at least 3 characters';
}
if (!values.username) {
errors.username = 'Required'
} else if (values.username.length < 3) {
errors.username = 'Username must be at least 3 characters'
}
return errors;
};
function AccountCreation() {
const [isSubmitting, setIsSubmitting] = React.useState(false);
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
const router = useRouter(
)
const formik = useFormik({
initialValues: {
org_slug: '',
email: '',
password: '',
confirmPassword: '',
username: '',
},
validate,
onSubmit: async values => {
let finalvalueswithoutpasswords = { ...values, 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 createNewUserInstall({email:values.email,username:values.username,password:values.password},install.data[1].slug)
// await 2 seconds
setTimeout(() => {
setIsSubmitting(false)
}, 2000)
router.push('/install?step=4')
},
});
return (
<div>
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email">
<FormLabelAndMessage label='Email' message={formik.errors.email} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
</Form.Control>
</FormField>
{/* for password */}
<FormField name="password">
<FormLabelAndMessage label='Password' message={formik.errors.password} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
</Form.Control>
</FormField>
{/* for confirm password */}
<FormField name="confirmPassword">
<FormLabelAndMessage label='Confirm Password' message={formik.errors.confirmPassword} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.confirmPassword} type="password" required />
</Form.Control>
</FormField>
{/* for username */}
<FormField name="username">
<FormLabelAndMessage label='Username' message={formik.errors.username} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
</Form.Control>
</FormField>
<div className="flex flex-row-reverse py-4">
<Form.Submit asChild>
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
: "Create Admin Account"}
</ButtonBlack>
</Form.Submit>
</div>
</FormLayout>
</div>
)
return errors
}
export default AccountCreation
function AccountCreation() {
const [isSubmitting, setIsSubmitting] = React.useState(false)
const {
data: install,
error: error,
isLoading,
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
const router = useRouter()
const formik = useFormik({
initialValues: {
org_slug: '',
email: '',
password: '',
confirmPassword: '',
username: '',
},
validate,
onSubmit: async (values) => {
let finalvalueswithoutpasswords = {
...values,
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 createNewUserInstall(
{
email: values.email,
username: values.username,
password: values.password,
},
install.data[1].slug
)
// await 2 seconds
setTimeout(() => {
setIsSubmitting(false)
}, 2000)
router.push('/install?step=4')
},
})
return (
<div>
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email">
<FormLabelAndMessage label="Email" message={formik.errors.email} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.email}
type="email"
required
/>
</Form.Control>
</FormField>
{/* for password */}
<FormField name="password">
<FormLabelAndMessage
label="Password"
message={formik.errors.password}
/>
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.password}
type="password"
required
/>
</Form.Control>
</FormField>
{/* for confirm password */}
<FormField name="confirmPassword">
<FormLabelAndMessage
label="Confirm Password"
message={formik.errors.confirmPassword}
/>
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.confirmPassword}
type="password"
required
/>
</Form.Control>
</FormField>
{/* for username */}
<FormField name="username">
<FormLabelAndMessage
label="Username"
message={formik.errors.username}
/>
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.username}
type="text"
required
/>
</Form.Control>
</FormField>
<div className="flex flex-row-reverse py-4">
<Form.Submit asChild>
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
{isSubmitting ? (
<BarLoader
cssOverride={{ borderRadius: 60 }}
width={60}
color="#ffffff"
/>
) : (
'Create Admin Account'
)}
</ButtonBlack>
</Form.Submit>
</div>
</FormLayout>
</div>
)
}
export default AccountCreation

View file

@ -1,45 +1,49 @@
import { getAPIUrl } from '@services/config/config';
import { createDefaultElements, updateInstall } from '@services/install/install';
import { swrFetcher } from '@services/utils/ts/requests';
import { useRouter } from 'next/navigation';
import { getAPIUrl } from '@services/config/config'
import { createDefaultElements, updateInstall } from '@services/install/install'
import { swrFetcher } from '@services/utils/ts/requests'
import { useRouter } from 'next/navigation'
import React from 'react'
import useSWR from "swr";
import useSWR from 'swr'
function DefaultElements() {
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [isSubmitted, setIsSubmitted] = React.useState(false);
const router = useRouter()
const {
data: install,
error: error,
isLoading,
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
const [isSubmitting, setIsSubmitting] = React.useState(false)
const [isSubmitted, setIsSubmitted] = React.useState(false)
const router = useRouter()
function createDefElementsAndUpdateInstall() {
try {
createDefaultElements()
// add an {} to the install.data object
function createDefElementsAndUpdateInstall() {
try {
createDefaultElements()
// add an {} to the install.data object
let install_data = { ...install.data, 2: { status: 'OK' } }
updateInstall(install_data, 3)
// await 2 seconds
setTimeout(() => {
setIsSubmitting(false)
}, 2000)
let install_data = { ...install.data, 2: { status: 'OK' } }
router.push('/install?step=3')
setIsSubmitted(true)
}
catch (e) {
}
}
updateInstall(install_data, 3)
// await 2 seconds
setTimeout(() => {
setIsSubmitting(false)
}, 2000)
return (
<div className='flex py-10 justify-center items-center space-x-3'>
<h1>Install Default Elements </h1>
<div onClick={createDefElementsAndUpdateInstall} className='p-3 font-bold bg-gray-200 text-gray-900 rounded-lg hover:cursor-pointer' >
Install
</div>
</div>
)
router.push('/install?step=3')
setIsSubmitted(true)
} catch (e) {}
}
return (
<div className="flex py-10 justify-center items-center space-x-3">
<h1>Install Default Elements </h1>
<div
onClick={createDefElementsAndUpdateInstall}
className="p-3 font-bold bg-gray-200 text-gray-900 rounded-lg hover:cursor-pointer"
>
Install
</div>
</div>
)
}
export default DefaultElements
export default DefaultElements

View file

@ -2,18 +2,32 @@ import { Check, Link } from 'lucide-react'
import React from 'react'
function DisableInstallMode() {
return (
<div className='p-4 bg-green-300 text-green-950 rounded-md flex space-x-4 items-center'>
<div>
<Check size={32} />
</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 className='flex space-x-2 items-center'>
<Link size={20} />
<a rel='noreferrer' target='_blank' className="text-blue-950 font-medium" href="http://docs.learnhouse.app">LearnHouse Docs</a>
</div></div>
return (
<div className="p-4 bg-green-300 text-green-950 rounded-md flex space-x-4 items-center">
<div>
<Check size={32} />
</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 className="flex space-x-2 items-center">
<Link size={20} />
<a
rel="noreferrer"
target="_blank"
className="text-blue-950 font-medium"
href="http://docs.learnhouse.app"
>
LearnHouse Docs
</a>
</div>
)
</div>
</div>
)
}
export default DisableInstallMode
export default DisableInstallMode

View file

@ -1,39 +1,42 @@
import { getAPIUrl } from '@services/config/config';
import { updateInstall } from '@services/install/install';
import { swrFetcher } from '@services/utils/ts/requests';
import { getAPIUrl } from '@services/config/config'
import { updateInstall } from '@services/install/install'
import { swrFetcher } from '@services/utils/ts/requests'
import { Check } from 'lucide-react'
import { useRouter } from 'next/navigation';
import { useRouter } from 'next/navigation'
import React from 'react'
import useSWR from "swr";
import useSWR from 'swr'
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()
async function finishInstall() {
let install_data = { ...install.data, 5: { status: 'OK' } }
let data = await updateInstall(install_data, 6)
if (data) {
router.push('/install?step=6')
}
else {
} else {
}
}
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>
<br />
<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
</div>
</div>
)
}
export default Finish
export default Finish

View file

@ -1,69 +1,80 @@
import PageLoading from '@components/Objects/Loaders/PageLoading';
import { getAPIUrl } from '@services/config/config';
import { swrFetcher } from '@services/utils/ts/requests';
import { useRouter } from 'next/navigation';
import PageLoading from '@components/Objects/Loaders/PageLoading'
import { getAPIUrl } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests'
import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import useSWR, { mutate } from "swr";
import useSWR, { mutate } from 'swr'
function GetStarted() {
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
const router = useRouter()
async function startInstallation() {
let res = await fetch(`${getAPIUrl()}install/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({})
})
if (res.status == 200) {
mutate(`${getAPIUrl()}install/latest`)
router.refresh();
router.push(`/install?step=1`)
}
const {
data: install,
error: error,
isLoading,
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
const router = useRouter()
async function startInstallation() {
let res = await fetch(`${getAPIUrl()}install/start`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({}),
})
if (res.status == 200) {
mutate(`${getAPIUrl()}install/latest`)
router.refresh()
router.push(`/install?step=1`)
}
}
function redirectToStep() {
const step = install.step
router.push(`/install?step=${step}`)
}
function redirectToStep() {
const step = install.step
router.push(`/install?step=${step}`)
}
useEffect(() => {
if (install) {
redirectToStep()
}
}, [install])
if (error) return <div className='flex py-10 justify-center items-center space-x-3'>
<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' >
Start
</div>
</div>
if (isLoading) return <PageLoading />
useEffect(() => {
if (install) {
return (
<div>
<div className='flex py-10 justify-center items-center space-x-3'>
<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' >
Continue
</div>
<div onClick={startInstallation} className='p-3 font-bold bg-green-200 text-green-900 rounded-lg hover:cursor-pointer' >
Start
</div>
</div>
</div>
)
redirectToStep()
}
}, [install])
if (error)
return (
<div className="flex py-10 justify-center items-center space-x-3">
<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"
>
Start
</div>
</div>
)
if (isLoading) return <PageLoading />
if (install) {
return (
<div>
<div className="flex py-10 justify-center items-center space-x-3">
<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"
>
Continue
</div>
<div
onClick={startInstallation}
className="p-3 font-bold bg-green-200 text-green-900 rounded-lg hover:cursor-pointer"
>
Start
</div>
</div>
</div>
)
}
}
export default GetStarted
export default GetStarted

View file

@ -1,137 +1,166 @@
import FormLayout, { ButtonBlack, FormField, FormLabelAndMessage, Input } from '@components/StyledElements/Form/Form'
import * as Form from '@radix-ui/react-form';
import { useFormik } from 'formik';
import { BarLoader } from 'react-spinners';
import FormLayout, {
ButtonBlack,
FormField,
FormLabelAndMessage,
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 { swrFetcher } from '@services/utils/ts/requests';
import { getAPIUrl } from '@services/config/config';
import useSWR from "swr";
import { createNewOrgInstall, updateInstall } from '@services/install/install';
import { useRouter } from 'next/navigation';
import { Check } from 'lucide-react';
import { swrFetcher } from '@services/utils/ts/requests'
import { getAPIUrl } from '@services/config/config'
import useSWR from 'swr'
import { createNewOrgInstall, updateInstall } from '@services/install/install'
import { useRouter } from 'next/navigation'
import { Check } from 'lucide-react'
const validate = (values: any) => {
const errors: any = {};
const errors: any = {}
if (!values.name) {
errors.name = 'Required';
}
if (!values.name) {
errors.name = 'Required'
}
if (!values.description) {
errors.description = 'Required';
}
if (!values.description) {
errors.description = 'Required'
}
if (!values.slug) {
errors.slug = 'Required';
}
if (!values.slug) {
errors.slug = 'Required'
}
if (!values.email) {
errors.email = 'Required';
}
else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address';
}
if (!values.email) {
errors.email = 'Required'
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
errors.email = 'Invalid email address'
}
return errors;
};
function OrgCreation() {
const { data: install, error: error, isLoading } = useSWR(`${getAPIUrl()}install/latest`, swrFetcher);
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [isSubmitted, setIsSubmitted] = React.useState(false);
const router = useRouter()
function createOrgAndUpdateInstall(values: any) {
try {
createNewOrgInstall(values)
install.data = {
1: values
}
let install_data = { ...install.data, 1: values }
updateInstall(install_data, 2)
// await 2 seconds
setTimeout(() => {
setIsSubmitting(false)
}, 2000)
router.push('/install?step=2')
setIsSubmitted(true)
}
catch (e) {
}
}
const formik = useFormik({
initialValues: {
name: '',
description: '',
slug: '',
email: '',
},
validate,
onSubmit: values => {
createOrgAndUpdateInstall(values)
},
});
return (
<div>
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage label='Name' message={formik.errors.name} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.name} type="text" required />
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage label='Description' message={formik.errors.description} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.description} type="text" required />
</Form.Control>
</FormField>
<FormField name="slug">
<FormLabelAndMessage label='Slug' message={formik.errors.slug} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.slug} type="text" required />
</Form.Control>
</FormField>
{/* for username */}
<FormField name="email">
<FormLabelAndMessage label='Email' message={formik.errors.email} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
</Form.Control>
</FormField>
<div className="flex flex-row-reverse py-4">
<Form.Submit asChild>
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
{isSubmitting ? <BarLoader cssOverride={{ borderRadius: 60, }} width={60} color="#ffffff" />
: "Create Organization"}
</ButtonBlack>
</Form.Submit>
</div>
{isSubmitted && <div className='flex space-x-3'> <Check /> Organization Created Successfully</div>}
</FormLayout>
</div>
)
return errors
}
export default OrgCreation
function OrgCreation() {
const {
data: install,
error: error,
isLoading,
} = useSWR(`${getAPIUrl()}install/latest`, swrFetcher)
const [isSubmitting, setIsSubmitting] = React.useState(false)
const [isSubmitted, setIsSubmitted] = React.useState(false)
const router = useRouter()
function createOrgAndUpdateInstall(values: any) {
try {
createNewOrgInstall(values)
install.data = {
1: values,
}
let install_data = { ...install.data, 1: values }
updateInstall(install_data, 2)
// await 2 seconds
setTimeout(() => {
setIsSubmitting(false)
}, 2000)
router.push('/install?step=2')
setIsSubmitted(true)
} catch (e) {}
}
const formik = useFormik({
initialValues: {
name: '',
description: '',
slug: '',
email: '',
},
validate,
onSubmit: (values) => {
createOrgAndUpdateInstall(values)
},
})
return (
<div>
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage label="Name" message={formik.errors.name} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.name}
type="text"
required
/>
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage
label="Description"
message={formik.errors.description}
/>
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.description}
type="text"
required
/>
</Form.Control>
</FormField>
<FormField name="slug">
<FormLabelAndMessage label="Slug" message={formik.errors.slug} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.slug}
type="text"
required
/>
</Form.Control>
</FormField>
{/* for username */}
<FormField name="email">
<FormLabelAndMessage label="Email" message={formik.errors.email} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.email}
type="email"
required
/>
</Form.Control>
</FormField>
<div className="flex flex-row-reverse py-4">
<Form.Submit asChild>
<ButtonBlack type="submit" css={{ marginTop: 10 }}>
{isSubmitting ? (
<BarLoader
cssOverride={{ borderRadius: 60 }}
width={60}
color="#ffffff"
/>
) : (
'Create Organization'
)}
</ButtonBlack>
</Form.Submit>
</div>
{isSubmitted && (
<div className="flex space-x-3">
{' '}
<Check /> Organization Created Successfully
</div>
)}
</FormLayout>
</div>
)
}
export default OrgCreation

View file

@ -1,43 +1,46 @@
import { getAPIUrl } from '@services/config/config';
import { createSampleDataInstall, updateInstall } from '@services/install/install';
import { swrFetcher } from '@services/utils/ts/requests';
import { useRouter } from 'next/navigation';
import { getAPIUrl } from '@services/config/config'
import {
createSampleDataInstall,
updateInstall,
} from '@services/install/install'
import { swrFetcher } from '@services/utils/ts/requests'
import { useRouter } from 'next/navigation'
import React from 'react'
import useSWR from "swr";
import useSWR from 'swr'
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()
function createSampleData() {
try {
let username = install.data[3].username
let slug = install.data[1].slug
createSampleDataInstall(username, slug)
let install_data = { ...install.data, 4: { status: 'OK' } }
updateInstall(install_data, 5)
router.push('/install?step=5')
}
catch (e) {
}
} catch (e) {}
}
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>
<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
</div>
</div>
)
}
export default SampleData
export default SampleData

View file

@ -1,53 +1,52 @@
import AccountCreation from "./account_creation";
import DefaultElements from "./default_elements";
import DisableInstallMode from "./disable_install_mode";
import Finish from "./finish";
import GetStarted from "./get_started";
import OrgCreation from "./org_creation";
import SampleData from "./sample_data";
import AccountCreation from './account_creation'
import DefaultElements from './default_elements'
import DisableInstallMode from './disable_install_mode'
import Finish from './finish'
import GetStarted from './get_started'
import OrgCreation from './org_creation'
import SampleData from './sample_data'
export const INSTALL_STEPS = [
{
id: "INSTALL_STATUS",
name: "Get started",
id: 'INSTALL_STATUS',
name: 'Get started',
component: <GetStarted />,
completed: false,
},
{
id: "ORGANIZATION_CREATION",
name: "Organization Creation",
id: 'ORGANIZATION_CREATION',
name: 'Organization Creation',
component: <OrgCreation />,
completed: false,
},
{
id: "DEFAULT_ELEMENTS",
name: "Default Elements",
id: 'DEFAULT_ELEMENTS',
name: 'Default Elements',
component: <DefaultElements />,
completed: false,
},
{
id: "ACCOUNT_CREATION",
name: "Account Creation",
id: 'ACCOUNT_CREATION',
name: 'Account Creation',
component: <AccountCreation />,
completed: false,
},
{
id: "SAMPLE_DATA",
name: "Sample Data",
id: 'SAMPLE_DATA',
name: 'Sample Data',
component: <SampleData />,
completed: false,
},
{
id: "FINISH",
name: "Finish",
id: 'FINISH',
name: 'Finish',
component: <Finish />,
completed: false,
},
{
id: "DISABLING_INSTALLATION_MODE",
name: "Disabling Installation Mode",
id: 'DISABLING_INSTALLATION_MODE',
name: 'Disabling Installation Mode',
component: <DisableInstallMode />,
completed: false,
},
];
]

View file

@ -1,15 +1,19 @@
"use client";
import "../styles/globals.css";
import StyledComponentsRegistry from "../components/Utils/libs/styled-registry";
'use client'
import '../styles/globals.css'
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 = {
hidden: { opacity: 0, x: 0, y: 0 },
enter: { opacity: 1, x: 0, y: 0 },
exit: { opacity: 0, x: 0, y: 0 },
};
}
return (
<html className="" lang="en">
<head />
@ -20,7 +24,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
initial="hidden" // Set the initial state to variants.hidden
animate="enter" // Animated state to variants.enter
exit="exit" // Exit state (used later) to variants.exit
transition={{ type: "linear" }} // Set the transition to linear
transition={{ type: 'linear' }} // Set the transition to linear
className=""
>
{children}
@ -28,5 +32,5 @@ export default function RootLayout({ children }: { children: React.ReactNode })
</StyledComponentsRegistry>
</body>
</html>
);
)
}

View file

@ -1,40 +1,47 @@
"use client";
import React from "react";
import { createNewOrganization } from "../../../services/organizations/orgs";
'use client'
import React from 'react'
import { createNewOrganization } from '../../../services/organizations/orgs'
const Organizations = () => {
const [name, setName] = React.useState("");
const [description, setDescription] = React.useState("");
const [email, setEmail] = React.useState("");
const [slug, setSlug] = React.useState("");
const [name, setName] = React.useState('')
const [description, setDescription] = React.useState('')
const [email, setEmail] = React.useState('')
const [slug, setSlug] = React.useState('')
const handleNameChange = (e: any) => {
setName(e.target.value);
};
setName(e.target.value)
}
const handleDescriptionChange = (e: any) => {
setDescription(e.target.value);
};
setDescription(e.target.value)
}
const handleEmailChange = (e: any) => {
setEmail(e.target.value);
};
setEmail(e.target.value)
}
const handleSlugChange = (e: any) => {
setSlug(e.target.value);
};
setSlug(e.target.value)
}
const handleSubmit = async (e: any) => {
e.preventDefault();
e.preventDefault()
let logo = ''
const status = await createNewOrganization({ name, description, email, logo, slug, default: false });
alert(JSON.stringify(status));
};
const status = await createNewOrganization({
name,
description,
email,
logo,
slug,
default: false,
})
alert(JSON.stringify(status))
}
return (
<div>
<div className="font-bold text-lg">New Organization</div>
<div className="font-bold text-lg">New Organization</div>
Name: <input onChange={handleNameChange} type="text" />
<br />
Description: <input onChange={handleDescriptionChange} type="text" />
@ -45,7 +52,7 @@ const Organizations = () => {
<br />
<button onClick={handleSubmit}>Create</button>
</div>
);
};
)
}
export default Organizations;
export default Organizations

View file

@ -1,23 +1,30 @@
"use client"; //todo: use server components
import Link from "next/link";
import React from "react";
import { deleteOrganizationFromBackend } from "@services/organizations/orgs";
import useSWR, { mutate } from "swr";
import { swrFetcher } from "@services/utils/ts/requests";
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
'use client' //todo: use server components
import Link from 'next/link'
import React from 'react'
import { deleteOrganizationFromBackend } from '@services/organizations/orgs'
import useSWR, { mutate } from 'swr'
import { swrFetcher } from '@services/utils/ts/requests'
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
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) {
const response = await deleteOrganizationFromBackend(org_id);
response && mutate(`${getAPIUrl()}orgs/user/page/1/limit/10`, organizations.filter((org: any) => org.org_id !== 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)
)
}
return (
<>
<div className="font-bold text-lg">
Your Organizations{" "}
Your Organizations{' '}
<Link href="/organizations/new">
<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>
{organizations.map((org: any) => (
<div key={org.org_id} 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>
<div
key={org.org_id}
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>
<button
onClick={() => deleteOrganization(org.org_id)}
@ -46,9 +58,8 @@ const Organizations = () => {
))}
</div>
)}
</>
);
};
)
}
export default Organizations;
export default Organizations

View file

@ -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 { useEffect } from 'react'
import ErrorUI from '@components/StyledElements/Error/Error';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
error: Error
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
console.error(error)
}, [error])
return (
<div>
<ErrorUI></ErrorUI>
</div>
);
}
)
}

View file

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

View file

@ -1,27 +1,34 @@
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
import { getUriWithOrg } from "@services/config/config";
import { getCollectionByIdWithAuthHeader } from "@services/courses/collections";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import { Metadata } from "next";
import { cookies } from "next/headers";
import Link from "next/link";
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import { getUriWithOrg } from '@services/config/config'
import { getCollectionByIdWithAuthHeader } from '@services/courses/collections'
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { Metadata } from 'next'
import { cookies } from 'next/headers'
import Link from 'next/link'
type MetadataProps = {
params: { orgslug: string, courseid: string, collectionid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
params: { orgslug: string; courseid: string; collectionid: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const cookieStore = cookies();
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const col = await getCollectionByIdWithAuthHeader(params.collectionid, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
const col = await getCollectionByIdWithAuthHeader(
params.collectionid,
access_token ? access_token : null,
{ revalidate: 0, tags: ['collections'] }
)
// SEO
return {
@ -34,48 +41,66 @@ export async function generateMetadata(
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
}
'max-image-preview': 'large',
},
},
openGraph: {
title: `Collection : ${col.name}${org.name}`,
description: `${col.description} `,
type: 'website',
},
};
}
}
const CollectionPage = async (params: any) => {
const cookieStore = cookies();
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const org = await getOrganizationContextInfo(params.params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const orgslug = params.params.orgslug;
const col = await getCollectionByIdWithAuthHeader(params.params.collectionid, access_token ? access_token : null, { revalidate: 0, tags: ['collections'] });
const org = await getOrganizationContextInfo(params.params.orgslug, {
revalidate: 1800,
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) => {
return courseid.replace("course_", "")
return courseid.replace('course_', '')
}
return (
<GeneralWrapperStyled>
<h2 className="text-sm font-bold text-gray-400">Collection</h2>
<h1 className="text-3xl font-bold">{col.name}</h1>
<br />
<div className="home_courses flex flex-wrap">
{col.courses.map((course: any) => (
<div className="pr-8" key={course.course_uuid}>
<Link
href={getUriWithOrg(
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>
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>
</div>
))}
</div>
</GeneralWrapperStyled>
)
}
return <GeneralWrapperStyled>
<h2 className="text-sm font-bold text-gray-400">Collection</h2>
<h1 className="text-3xl font-bold">{col.name}</h1>
<br />
<div className="home_courses flex flex-wrap">
{col.courses.map((course: any) => (
<div className="pr-8" key={course.course_uuid}>
<Link href={getUriWithOrg(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>
<h2 className="font-bold text-lg w-[250px] py-2">{course.name}</h2>
</div>
))}
</div>
</GeneralWrapperStyled>;
};
export default CollectionPage;
export default CollectionPage

View file

@ -1,9 +1,6 @@
import PageLoading from "@components/Objects/Loaders/PageLoading";
import PageLoading from '@components/Objects/Loaders/PageLoading'
export default function Loading() {
// Or a custom loading skeleton component
return (
<PageLoading></PageLoading>
)
}
// Or a custom loading skeleton component
return <PageLoading></PageLoading>
}

View file

@ -1,37 +1,41 @@
"use client";
import { useRouter } from "next/navigation";
import React, { useState } from "react";
import { createCollection } from "@services/courses/collections";
import useSWR from "swr";
import { getAPIUrl, getUriWithOrg } from "@services/config/config";
import { revalidateTags, swrFetcher } from "@services/utils/ts/requests";
import { useOrg } from "@components/Contexts/OrgContext";
'use client'
import { useRouter } from 'next/navigation'
import React, { useState } from 'react'
import { createCollection } from '@services/courses/collections'
import useSWR from 'swr'
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
import { revalidateTags, swrFetcher } from '@services/utils/ts/requests'
import { useOrg } from '@components/Contexts/OrgContext'
function NewCollection(params: any) {
const org = useOrg() as any;
const orgslug = params.params.orgslug;
const [name, setName] = React.useState("");
const [description, setDescription] = React.useState("");
const [selectedCourses, setSelectedCourses] = React.useState([]) as any;
const router = useRouter();
const { data: courses, error: error } = useSWR(`${getAPIUrl()}courses/org_slug/${orgslug}/page/1/limit/10`, swrFetcher);
const [isPublic, setIsPublic] = useState('true');
const org = useOrg() as any
const orgslug = params.params.orgslug
const [name, setName] = React.useState('')
const [description, setDescription] = React.useState('')
const [selectedCourses, setSelectedCourses] = React.useState([]) as any
const router = useRouter()
const { data: courses, error: error } = useSWR(
`${getAPIUrl()}courses/org_slug/${orgslug}/page/1/limit/10`,
swrFetcher
)
const [isPublic, setIsPublic] = useState('true')
const handleVisibilityChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
setIsPublic(e.target.value);
};
setIsPublic(e.target.value)
}
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setName(event.target.value);
};
setName(event.target.value)
}
const handleDescriptionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setDescription(event.target.value);
};
const handleDescriptionChange = (
event: React.ChangeEvent<HTMLInputElement>
) => {
setDescription(event.target.value)
}
const handleSubmit = async (e: any) => {
e.preventDefault();
e.preventDefault()
const collection = {
name: name,
@ -39,19 +43,17 @@ function NewCollection(params: any) {
courses: selectedCourses,
public: isPublic,
org_id: org.id,
};
await createCollection(collection);
await revalidateTags(["collections"], org.slug);
}
await createCollection(collection)
await revalidateTags(['collections'], org.slug)
// reload the page
router.refresh();
router.refresh()
// wait for 2s before reloading the page
setTimeout(() => {
router.push(getUriWithOrg(orgslug, "/collections"));
}
, 1000);
};
router.push(getUriWithOrg(orgslug, '/collections'))
}, 1000)
}
return (
<>
@ -75,36 +77,46 @@ function NewCollection(params: any) {
<option value="true">Public Collection </option>
</select>
{!courses ? (
<p className="text-gray-500">Loading...</p>
) : (
<div className="space-y-4 p-3">
<p>Courses</p>
{courses.map((course: any) => (
<div key={course.course_uuid} className="flex items-center space-x-2">
<p className="text-gray-500">Loading...</p>
) : (
<div className="space-y-4 p-3">
<p>Courses</p>
{courses.map((course: any) => (
<div
key={course.course_uuid}
className="flex items-center space-x-2"
>
<input
type="checkbox"
id={course.id}
name={course.name}
value={course.id}
onChange={(e) => {
if (e.target.checked) {
setSelectedCourses([...selectedCourses, course.id])
} else {
setSelectedCourses(
selectedCourses.filter(
(course_uuid: any) =>
course_uuid !== course.course_uuid
)
)
}
}}
className="text-blue-500 rounded focus:ring-2 focus:ring-blue-500"
/>
<input
type="checkbox"
id={course.id}
name={course.name}
value={course.id}
onChange={(e) => {
if (e.target.checked) {
setSelectedCourses([...selectedCourses, course.id]);
}
else {
setSelectedCourses(selectedCourses.filter((course_uuid: any) => course_uuid !== course.course_uuid));
}
}}
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>
</div>
))}
</div>
)}
<label
htmlFor={course.course_uuid}
className="text-sm text-gray-700"
>
{course.name}
</label>
</div>
))}
</div>
)}
<input
type="text"
@ -121,9 +133,8 @@ function NewCollection(params: any) {
Submit
</button>
</div>
</>
);
)
}
export default NewCollection;
export default NewCollection

View file

@ -1,103 +1,151 @@
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
import TypeOfContentTitle from "@components/StyledElements/Titles/TypeOfContentTitle";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { getUriWithOrg } from "@services/config/config";
import { getOrgCollectionsWithAuthHeader } from "@services/courses/collections";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import { Metadata } from "next";
import { cookies } from "next/headers";
import Link from "next/link";
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
import CollectionThumbnail from "@components/Objects/Thumbnails/CollectionThumbnail";
import NewCollectionButton from "@components/StyledElements/Buttons/NewCollectionButton";
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import { getUriWithOrg } from '@services/config/config'
import { getOrgCollectionsWithAuthHeader } from '@services/courses/collections'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { Metadata } from 'next'
import { cookies } from 'next/headers'
import Link from 'next/link'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail'
import NewCollectionButton from '@components/StyledElements/Buttons/NewCollectionButton'
type MetadataProps = {
params: { orgslug: string, courseid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
params: { orgslug: string; courseid: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
// SEO
return {
title: `Collections — ${org.name}`,
description: `Collections of courses from ${org.name}`,
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
}
},
openGraph: {
title: `Collections — ${org.name}`,
description: `Collections of courses from ${org.name}`,
type: 'website',
},
};
// SEO
return {
title: `Collections — ${org.name}`,
description: `Collections of courses from ${org.name}`,
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
},
},
openGraph: {
title: `Collections — ${org.name}`,
description: `Collections of courses from ${org.name}`,
type: 'website',
},
}
}
const CollectionsPage = async (params: any) => {
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const orgslug = params.params.orgslug;
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: ['collections'] });
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const orgslug = params.params.orgslug
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: ['collections'] }
)
return (
<GeneralWrapperStyled>
<div className="flex justify-between" >
<TypeOfContentTitle title="Collections" type="col" />
<AuthenticatedClientElement
ressourceType="collections"
action="create"
checkMethod='roles' orgId={org_id}>
<Link className="flex justify-center" href={getUriWithOrg(orgslug, "/collections/new")}>
<NewCollectionButton />
</Link>
</AuthenticatedClientElement>
return (
<GeneralWrapperStyled>
<div className="flex justify-between">
<TypeOfContentTitle title="Collections" type="col" />
<AuthenticatedClientElement
ressourceType="collections"
action="create"
checkMethod="roles"
orgId={org_id}
>
<Link
className="flex justify-center"
href={getUriWithOrg(orgslug, '/collections/new')}
>
<NewCollectionButton />
</Link>
</AuthenticatedClientElement>
</div>
<div className="home_collections flex flex-wrap">
{collections.map((collection: any) => (
<div
className="flex flex-col py-1 px-3"
key={collection.collection_uuid}
>
<CollectionThumbnail
collection={collection}
orgslug={orgslug}
org_id={org_id}
/>
</div>
))}
{collections.length == 0 && (
<div className="flex mx-auto h-[400px]">
<div className="flex flex-col justify-center text-center items-center space-y-5">
<div className="mx-auto">
<svg
width="120"
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>
</div>
<div className="space-y-0">
<h1 className="text-3xl font-bold text-gray-600">
No collections yet
</h1>
<p className="text-lg text-gray-400">
Create a collection to group courses together
</p>
</div>
<AuthenticatedClientElement
checkMethod="roles"
ressourceType="collections"
action="create"
orgId={org_id}
>
<Link href={getUriWithOrg(orgslug, '/collections/new')}>
<NewCollectionButton />
</Link>
</AuthenticatedClientElement>
</div>
<div className="home_collections flex flex-wrap">
{collections.map((collection: any) => (
<div className="flex flex-col py-1 px-3" key={collection.collection_uuid}>
<CollectionThumbnail collection={collection} orgslug={orgslug} org_id={org_id} />
</div>
))}
{collections.length == 0 &&
<div className="flex mx-auto h-[400px]">
<div className="flex flex-col justify-center text-center items-center space-y-5">
<div className='mx-auto'>
<svg width="120" 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>
</div>
<div className="space-y-0">
<h1 className="text-3xl font-bold text-gray-600">No collections yet</h1>
<p className="text-lg text-gray-400">Create a collection to group courses together</p>
</div>
<AuthenticatedClientElement checkMethod='roles'
ressourceType="collections"
action="create"
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
<NewCollectionButton />
</Link>
</AuthenticatedClientElement>
</div>
</div>
}
</div>
</GeneralWrapperStyled>
);
</div>
)}
</div>
</GeneralWrapperStyled>
)
}
export default CollectionsPage
export default CollectionsPage

View file

@ -1,53 +1,50 @@
"use client";
import Link from "next/link";
import { getUriWithOrg } from "@services/config/config";
import Canva from "@components/Objects/Activities/DynamicCanva/DynamicCanva";
import VideoActivity from "@components/Objects/Activities/Video/Video";
import { Check, MoreVertical } from "lucide-react";
import { markActivityAsComplete } from "@services/courses/activity";
import DocumentPdfActivity from "@components/Objects/Activities/DocumentPdf/DocumentPdf";
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { useRouter } from "next/navigation";
import AuthenticatedClientElement from "@components/Security/AuthenticatedClientElement";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
import { useOrg } from "@components/Contexts/OrgContext";
import { CourseProvider } from "@components/Contexts/CourseContext";
import AIActivityAsk from "@components/Objects/Activities/AI/AIActivityAsk";
import AIChatBotProvider from "@components/Contexts/AI/AIChatBotContext";
'use client'
import Link from 'next/link'
import { getUriWithOrg } from '@services/config/config'
import Canva from '@components/Objects/Activities/DynamicCanva/DynamicCanva'
import VideoActivity from '@components/Objects/Activities/Video/Video'
import { Check, MoreVertical } from 'lucide-react'
import { markActivityAsComplete } from '@services/courses/activity'
import DocumentPdfActivity from '@components/Objects/Activities/DocumentPdf/DocumentPdf'
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import { useRouter } from 'next/navigation'
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { useOrg } from '@components/Contexts/OrgContext'
import { CourseProvider } from '@components/Contexts/CourseContext'
import AIActivityAsk from '@components/Objects/Activities/AI/AIActivityAsk'
import AIChatBotProvider from '@components/Contexts/AI/AIChatBotContext'
interface ActivityClientProps {
activityid: string;
courseuuid: string;
orgslug: string;
activity: any;
course: any;
activityid: string
courseuuid: string
orgslug: string
activity: any
course: any
}
function ActivityClient(props: ActivityClientProps) {
const activityid = props.activityid;
const courseuuid = props.courseuuid;
const orgslug = props.orgslug;
const activity = props.activity;
const course = props.course;
const org = useOrg() as any;
const activityid = props.activityid
const courseuuid = props.courseuuid
const orgslug = props.orgslug
const activity = props.activity
const course = props.course
const org = useOrg() as any
function getChapterNameByActivityId(course: any, activity_id: any) {
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++) {
let activity = chapter.activities[j];
let activity = chapter.activities[j]
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 (
<>
<CourseProvider courseuuid={course?.course_uuid}>
@ -56,90 +53,147 @@ function ActivityClient(props: ActivityClientProps) {
<div className="space-y-4 pt-4">
<div className="flex space-x-6">
<div className="flex">
<Link 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
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>
</div>
<div className="flex flex-col -space-y-1">
<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>
<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 flex-col -space-y-1">
<p className="font-bold text-gray-700 text-md">Chapter : {getChapterNameByActivityId(course, activity.id)}</p>
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase" >{activity.name}</h1>
<p className="font-bold text-gray-700 text-md">
Chapter : {getChapterNameByActivityId(course, activity.id)}
</p>
<h1 className="font-bold text-gray-950 text-2xl first-letter:uppercase">
{activity.name}
</h1>
</div>
<div className="flex space-x-1 items-center">
<AuthenticatedClientElement checkMethod="authentication">
<AIActivityAsk activity={activity} />
<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>
</div>
</div>
{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>
{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 */}
{activity.activity_type == "TYPE_VIDEO" && <VideoActivity course={course} activity={activity} />}
{activity.activity_type == "TYPE_DOCUMENT" && <DocumentPdfActivity course={course} activity={activity} />}
{activity.activity_type == 'TYPE_VIDEO' && (
<VideoActivity course={course} activity={activity} />
)}
{activity.activity_type == 'TYPE_DOCUMENT' && (
<DocumentPdfActivity
course={course}
activity={activity}
/>
)}
</div>
</div>
) : (<div></div>)}
{<div style={{ height: "100px" }}></div>}
) : (
<div></div>
)}
{<div style={{ height: '100px' }}></div>}
</div>
</GeneralWrapperStyled>
</AIChatBotProvider>
</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

View file

@ -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 { useEffect } from 'react'
import ErrorUI from '@components/StyledElements/Error/Error';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
error: Error
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
console.error(error)
}, [error])
return (
<div>
<ErrorUI></ErrorUI>
</div>
);
}
)
}

View file

@ -1,9 +1,6 @@
import PageLoading from "@components/Objects/Loaders/PageLoading";
import PageLoading from '@components/Objects/Loaders/PageLoading'
export default function Loading() {
// Or a custom loading skeleton component
return (
<PageLoading></PageLoading>
)
}
// Or a custom loading skeleton component
return <PageLoading></PageLoading>
}

View file

@ -1,71 +1,90 @@
import { getActivityWithAuthHeader } from "@services/courses/activities";
import { getCourseMetadataWithAuthHeader } from "@services/courses/courses";
import { cookies } from "next/headers";
import ActivityClient from "./activity";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import { Metadata } from "next";
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
import { getActivityWithAuthHeader } from '@services/courses/activities'
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses'
import { cookies } from 'next/headers'
import ActivityClient from './activity'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { Metadata } from 'next'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
type MetadataProps = {
params: { orgslug: string, courseuuid: string, activityid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
params: { orgslug: string; courseuuid: string; activityid: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, 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)
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
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
return {
title: activity.name + `${course_meta.name} Course`,
description: course_meta.description,
keywords: course_meta.learnings,
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
}
},
openGraph: {
title: activity.name + `${course_meta.name} Course`,
description: course_meta.description,
publishedTime: course_meta.creation_date,
tags: course_meta.learnings,
},
};
// SEO
return {
title: activity.name + `${course_meta.name} Course`,
description: course_meta.description,
keywords: course_meta.learnings,
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
},
},
openGraph: {
title: activity.name + `${course_meta.name} Course`,
description: course_meta.description,
publishedTime: course_meta.creation_date,
tags: course_meta.learnings,
},
}
}
const ActivityPage = async (params: any) => {
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const activityid = params.params.activityid;
const courseuuid = params.params.courseuuid;
const orgslug = params.params.orgslug;
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const activityid = params.params.activityid
const courseuuid = params.params.courseuuid
const orgslug = params.params.orgslug
const course_meta = await getCourseMetadataWithAuthHeader(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 (
<>
<ActivityClient
activityid={activityid}
courseuuid={courseuuid}
orgslug={orgslug}
activity={activity}
course={course_meta}
/></>
)
const course_meta = await getCourseMetadataWithAuthHeader(
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 (
<>
<ActivityClient
activityid={activityid}
courseuuid={courseuuid}
orgslug={orgslug}
activity={activity}
course={course_meta}
/>
</>
)
}
export default ActivityPage
export default ActivityPage

View file

@ -1,63 +1,66 @@
"use client";
import { removeCourse, startCourse } from "@services/courses/activity";
import Link from "next/link";
import React, { useEffect, useState } from "react";
import { getUriWithOrg } from "@services/config/config";
import PageLoading from "@components/Objects/Loaders/PageLoading";
import { revalidateTags } from "@services/utils/ts/requests";
import ActivityIndicators from "@components/Pages/Courses/ActivityIndicators";
import { useRouter } from "next/navigation";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { getCourseThumbnailMediaDirectory, getUserAvatarMediaDirectory } 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";
'use client'
import { removeCourse, startCourse } from '@services/courses/activity'
import Link from 'next/link'
import React, { useEffect, useState } from 'react'
import { getUriWithOrg } from '@services/config/config'
import PageLoading from '@components/Objects/Loaders/PageLoading'
import { revalidateTags } from '@services/utils/ts/requests'
import ActivityIndicators from '@components/Pages/Courses/ActivityIndicators'
import { useRouter } from 'next/navigation'
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import {
getCourseThumbnailMediaDirectory,
getUserAvatarMediaDirectory,
} 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 [user, setUser] = useState<any>({});
const [learnings, setLearnings] = useState<any>([]);
const courseuuid = props.courseuuid;
const orgslug = props.orgslug;
const course = props.course;
const org = useOrg() as any;
const router = useRouter();
const [user, setUser] = useState<any>({})
const [learnings, setLearnings] = useState<any>([])
const courseuuid = props.courseuuid
const orgslug = props.orgslug
const course = props.course
const org = useOrg() as any
const router = useRouter()
function getLearningTags() {
// create array of learnings from a string object (comma separated)
let learnings = course?.learnings ? course?.learnings.split(",") : [];
setLearnings(learnings);
let learnings = course?.learnings ? course?.learnings.split(',') : []
setLearnings(learnings)
}
async function startCourseUI() {
// Create activity
await startCourse("course_" + courseuuid, orgslug);
await revalidateTags(['courses'], orgslug);
router.refresh();
await startCourse('course_' + courseuuid, orgslug)
await revalidateTags(['courses'], orgslug)
router.refresh()
// refresh page (FIX for Next.js BUG)
// window.location.reload();
}
function isCourseStarted() {
const runs = course.trail?.runs;
if (!runs) return false;
return runs.some((run: any) => run.status === "STATUS_IN_PROGRESS" && run.course_id === course.id);
const runs = course.trail?.runs
if (!runs) return false
return runs.some(
(run: any) =>
run.status === 'STATUS_IN_PROGRESS' && run.course_id === course.id
)
}
async function quitCourse() {
// Close activity
let activity = await removeCourse("course_" + courseuuid, orgslug);
let activity = await removeCourse('course_' + courseuuid, orgslug)
// Mutate course
await revalidateTags(['courses'], orgslug);
router.refresh();
await revalidateTags(['courses'], orgslug)
router.refresh()
}
useEffect(() => {
getLearningTags();
}
, [org, course]);
getLearningTags()
}, [org, course])
return (
<>
@ -67,20 +70,35 @@ const CourseClient = (props: any) => {
<GeneralWrapperStyled>
<div className="pb-3">
<p className="text-md font-bold text-gray-400 pb-2">Course</p>
<h1 className="text-3xl -mt-3 font-bold">
{course.name}
</h1>
<h1 className="text-3xl -mt-3 font-bold">{course.name}</h1>
</div>
{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={{ backgroundImage: `url('../empty_thumbnail.png')`, backgroundSize: 'auto' }}>
</div>
}
{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={{
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="course_metadata_left grow space-y-2">
@ -89,143 +107,223 @@ const CourseClient = (props: any) => {
<p className="py-5 px-5">{course.description}</p>
</div>
{learnings.length > 0 && learnings[0] !== "null" &&
{learnings.length > 0 && learnings[0] !== 'null' && (
<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">
{learnings.map((learning: any) => {
return (
<div key={learning}
className="flex space-x-2 items-center font-semibold text-gray-500">
<div
key={learning}
className="flex space-x-2 items-center font-semibold text-gray-500"
>
<div className="px-2 py-2 rounded-full">
<Check className="text-gray-400" size={15} />
</div>
<p>{learning}</p>
</div>
);
}
)}
)
})}
</div>
</div>
}
)}
<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">
{course.chapters.map((chapter: any) => {
return (
<div
key={chapter}
className=""
>
<div 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">
<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 ">
{chapter.activities.length} Activities
</p>
</div>
<div
className="py-3"
>{chapter.activities.map((activity: any) => {
return (
<>
<p className="flex text-md">
</p>
<div className="flex space-x-1 py-2 px-4 items-center">
<div className="courseicon items-center flex space-x-2 text-neutral-400">
{activity.activity_type === "TYPE_DYNAMIC" &&
<div className="bg-gray-100 px-2 py-2 rounded-full">
<Sparkles className="text-gray-400" size={13} />
</div>
}
{activity.activity_type === "TYPE_VIDEO" &&
<div className="bg-gray-100 px-2 py-2 rounded-full">
<Video className="text-gray-400" size={13} />
</div>
}
{activity.activity_type === "TYPE_DOCUMENT" &&
<div className="bg-gray-100 px-2 py-2 rounded-full">
<File className="text-gray-400" size={13} />
</div>
}
<div className="py-3">
{chapter.activities.map((activity: any) => {
return (
<>
<p className="flex text-md"></p>
<div className="flex space-x-1 py-2 px-4 items-center">
<div className="courseicon items-center flex space-x-2 text-neutral-400">
{activity.activity_type ===
'TYPE_DYNAMIC' && (
<div className="bg-gray-100 px-2 py-2 rounded-full">
<Sparkles
className="text-gray-400"
size={13}
/>
</div>
)}
{activity.activity_type === 'TYPE_VIDEO' && (
<div className="bg-gray-100 px-2 py-2 rounded-full">
<Video
className="text-gray-400"
size={13}
/>
</div>
)}
{activity.activity_type ===
'TYPE_DOCUMENT' && (
<div className="bg-gray-100 px-2 py-2 rounded-full">
<File
className="text-gray-400"
size={13}
/>
</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"
>
<p>{activity.name}</p>
</Link>
<div className="flex ">
{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"
>
<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>
<ArrowRight size={13} />
</div>
</Link>
</>
)}
{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"
>
<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>
<ArrowRight size={13} />
</div>
</Link>
</>
)}
{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"
>
<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>
<ArrowRight size={13} />
</div>
</Link>
</>
)}
</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">
<p>{activity.name}</p>
</Link>
<div className="flex ">
{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">
<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>
<ArrowRight size={13} /></div>
</Link>
</>
}
{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">
<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>
<ArrowRight size={13} /></div>
</Link>
</>
}
{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">
<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>
<ArrowRight size={13} /></div>
</Link>
</>
}
</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">
{user &&
{user && (
<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="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">
{course.authors[0].first_name && course.authors[0].last_name && (
<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>
</div>)}
{!course.authors[0].first_name && !course.authors[0].last_name && (
<div className="flex space-x-2 items-center">
<p>@{course.authors[0].username}</p>
</div>)}
{course.authors[0].first_name &&
course.authors[0].last_name && (
<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>
</div>
)}
{!course.authors[0].first_name &&
!course.authors[0].last_name && (
<div className="flex space-x-2 items-center">
<p>@{course.authors[0].username}</p>
</div>
)}
</div>
</div>
</div>
}
)}
{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
</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>
</GeneralWrapperStyled>
)}
</>
);
};
)
}
export default CourseClient;
export default CourseClient

View file

@ -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 { useEffect } from 'react'
import ErrorUI from '@components/StyledElements/Error/Error';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
error: Error
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
console.error(error)
}, [error])
return (
<div>
<ErrorUI></ErrorUI>
</div>
);
}
)
}

View file

@ -1,9 +1,6 @@
import PageLoading from "@components/Objects/Loaders/PageLoading";
import PageLoading from '@components/Objects/Loaders/PageLoading'
export default function Loading() {
// Or a custom loading skeleton component
return (
<PageLoading></PageLoading>
)
}
// Or a custom loading skeleton component
return <PageLoading></PageLoading>
}

View file

@ -1,65 +1,78 @@
import React from 'react'
import CourseClient from './course'
import { cookies } from 'next/headers';
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses';
import { getOrganizationContextInfo } from '@services/organizations/orgs';
import { Metadata } from 'next';
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
import { cookies } from 'next/headers'
import { getCourseMetadataWithAuthHeader } from '@services/courses/courses'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { Metadata } from 'next'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
type MetadataProps = {
params: { orgslug: string, courseuuid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
const course_meta = await getCourseMetadataWithAuthHeader(params.courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
// SEO
return {
title: course_meta.name + `${org.name}`,
description: course_meta.description,
keywords: course_meta.learnings,
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
}
},
openGraph: {
title: course_meta.name + `${org.name}`,
description: course_meta.description ? course_meta.description : '',
type: 'article',
publishedTime: course_meta.creation_date ? course_meta.creation_date : '',
tags: course_meta.learnings ? course_meta.learnings : [],
},
};
params: { orgslug: string; courseuuid: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
const course_meta = await getCourseMetadataWithAuthHeader(
params.courseuuid,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null
)
// SEO
return {
title: course_meta.name + `${org.name}`,
description: course_meta.description,
keywords: course_meta.learnings,
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
},
},
openGraph: {
title: course_meta.name + `${org.name}`,
description: course_meta.description ? course_meta.description : '',
type: 'article',
publishedTime: course_meta.creation_date ? course_meta.creation_date : '',
tags: course_meta.learnings ? course_meta.learnings : [],
},
}
}
const CoursePage = async (params: any) => {
const cookieStore = cookies();
const courseuuid = params.params.courseuuid
const orgslug = params.params.orgslug;
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const course_meta = await getCourseMetadataWithAuthHeader(courseuuid, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null)
const cookieStore = cookies()
const courseuuid = params.params.courseuuid
const orgslug = params.params.orgslug
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const course_meta = await getCourseMetadataWithAuthHeader(
courseuuid,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null
)
return (
<div>
<CourseClient courseuuid={courseuuid} orgslug={orgslug} course={course_meta} />
</div>
)
return (
<div>
<CourseClient
courseuuid={courseuuid}
orgslug={orgslug}
course={course_meta}
/>
</div>
)
}
export default CoursePage
export default CoursePage

View file

@ -1,113 +1,138 @@
'use client';
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse';
import Modal from '@components/StyledElements/Modal/Modal';
'use client'
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse'
import Modal from '@components/StyledElements/Modal/Modal'
import React from 'react'
import { useSearchParams } from 'next/navigation';
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
import { useSearchParams } from 'next/navigation'
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'
interface CourseProps {
orgslug: string;
courses: any;
org_id: string;
orgslug: string
courses: any
org_id: string
}
function Courses(props: CourseProps) {
const orgslug = props.orgslug;
const courses = props.courses;
const searchParams = useSearchParams();
const isCreatingCourse = searchParams.get('new') ? true : false;
const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse);
const orgslug = props.orgslug
const courses = props.courses
const searchParams = useSearchParams()
const isCreatingCourse = searchParams.get('new') ? true : false
const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse)
async function closeNewCourseModal() {
setNewCourseModal(false);
}
async function closeNewCourseModal() {
setNewCourseModal(false)
}
return (
<div>
<GeneralWrapperStyled>
<div className='flex flex-wrap justify-between'>
<TypeOfContentTitle title="Courses" type="cou" />
<AuthenticatedClientElement checkMethod='roles'
action='create'
ressourceType='courses'
orgId={props.org_id}>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>}
/>
</AuthenticatedClientElement>
</div>
<div className="flex flex-wrap">
{courses.map((course: any) => (
<div className="px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} />
</div>
))}
{courses.length == 0 &&
<div className="flex mx-auto h-[400px]">
<div className="flex flex-col justify-center text-center items-center space-y-5">
<div className='mx-auto'>
<svg width="120" 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>
</div>
<div className="space-y-0">
<h1 className="text-3xl font-bold text-gray-600">No courses yet</h1>
<p className="text-lg text-gray-400">Create a course to add content</p>
</div>
<AuthenticatedClientElement
action='create'
ressourceType='courses'
checkMethod='roles' orgId={props.org_id}>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>}
/>
</AuthenticatedClientElement>
</div>
</div>
}
</div>
</GeneralWrapperStyled>
return (
<div>
<GeneralWrapperStyled>
<div className="flex flex-wrap justify-between">
<TypeOfContentTitle title="Courses" type="cou" />
<AuthenticatedClientElement
checkMethod="roles"
action="create"
ressourceType="courses"
orgId={props.org_id}
>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={
<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>
}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>
}
/>
</AuthenticatedClientElement>
</div>
)
<div className="flex flex-wrap">
{courses.map((course: any) => (
<div className="px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} />
</div>
))}
{courses.length == 0 && (
<div className="flex mx-auto h-[400px]">
<div className="flex flex-col justify-center text-center items-center space-y-5">
<div className="mx-auto">
<svg
width="120"
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>
</div>
<div className="space-y-0">
<h1 className="text-3xl font-bold text-gray-600">
No courses yet
</h1>
<p className="text-lg text-gray-400">
Create a course to add content
</p>
</div>
<AuthenticatedClientElement
action="create"
ressourceType="courses"
checkMethod="roles"
orgId={props.org_id}
>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={
<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>
}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>
}
/>
</AuthenticatedClientElement>
</div>
</div>
)}
</div>
</GeneralWrapperStyled>
</div>
)
}
export default Courses
export default Courses

View file

@ -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 { useEffect } from 'react'
import ErrorUI from '@components/StyledElements/Error/Error';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
error: Error
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
console.error(error)
}, [error])
return (
<div>
<ErrorUI></ErrorUI>
</div>
);
}
)
}

View file

@ -1,9 +1,6 @@
import PageLoading from "@components/Objects/Loaders/PageLoading";
import PageLoading from '@components/Objects/Loaders/PageLoading'
export default function Loading() {
// Or a custom loading skeleton component
return (
<PageLoading></PageLoading>
)
}
// Or a custom loading skeleton component
return <PageLoading></PageLoading>
}

View file

@ -1,27 +1,28 @@
import React from "react";
import Courses from "./courses";
import { getOrgCoursesWithAuthHeader } from "@services/courses/courses";
import { Metadata } from "next";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import { cookies } from "next/headers";
import { getAccessTokenFromRefreshTokenCookie } from "@services/auth/auth";
import React from 'react'
import Courses from './courses'
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses'
import { Metadata } from 'next'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { cookies } from 'next/headers'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
type MetadataProps = {
params: { orgslug: string };
searchParams: { [key: string]: string | string[] | undefined };
};
params: { orgslug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
// SEO
return {
title: "Courses — " + org.name,
title: 'Courses — ' + org.name,
description: org.description,
keywords: `${org.name}, ${org.description}, courses, learning, education, online learning, edu, online courses, ${org.name} courses`,
robots: {
@ -31,30 +32,36 @@ export async function generateMetadata(
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
}
'max-image-preview': 'large',
},
},
openGraph: {
title: "Courses — " + org.name,
title: 'Courses — ' + org.name,
description: org.description,
type: 'website',
},
};
}
}
const CoursesPage = async (params: any) => {
const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
const cookieStore = cookies();
const orgslug = params.params.orgslug
const org = await getOrganizationContextInfo(orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
const cookieStore = cookies()
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 (
<div>
<Courses org_id={org.org_id} orgslug={orgslug} courses={courses} />
</div>
);
};
export default CoursesPage;
)
}
export default CoursesPage

View file

@ -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 { useEffect } from 'react'
import ErrorUI from '@components/StyledElements/Error/Error';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error;
reset: () => void;
error: Error
reset: () => void
}) {
useEffect(() => {
// Log the error to an error reporting service
console.error(error);
}, [error]);
console.error(error)
}, [error])
return (
<div>
<ErrorUI ></ErrorUI>
<ErrorUI></ErrorUI>
</div>
);
}
)
}

View file

@ -1,8 +1,14 @@
import "@styles/globals.css";
import { Menu } from "@components/Objects/Menu/Menu";
import SessionProvider from "@components/Contexts/SessionContext";
import '@styles/globals.css'
import { Menu } from '@components/Objects/Menu/Menu'
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 (
<>
<SessionProvider>
@ -10,5 +16,5 @@ export default function RootLayout({ children, params }: { children: React.React
{children}
</SessionProvider>
</>
);
)
}

View file

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

View file

@ -1,30 +1,33 @@
export const dynamic = 'force-dynamic';
import { Metadata } from 'next';
import { getUriWithOrg } from "@services/config/config";
import { getOrgCoursesWithAuthHeader } from "@services/courses/courses";
import Link from "next/link";
import { getOrgCollectionsWithAuthHeader } from "@services/courses/collections";
import { getOrganizationContextInfo } from '@services/organizations/orgs';
import { cookies } from 'next/headers';
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper';
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle';
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
import NewCollectionButton from '@components/StyledElements/Buttons/NewCollectionButton';
export const dynamic = 'force-dynamic'
import { Metadata } from 'next'
import { getUriWithOrg } from '@services/config/config'
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses'
import Link from 'next/link'
import { getOrgCollectionsWithAuthHeader } from '@services/courses/collections'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { cookies } from 'next/headers'
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'
import CollectionThumbnail from '@components/Objects/Thumbnails/CollectionThumbnail'
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'
import NewCollectionButton from '@components/StyledElements/Buttons/NewCollectionButton'
type MetadataProps = {
params: { orgslug: string };
searchParams: { [key: string]: string | string[] | undefined };
};
params: { orgslug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
// SEO
return {
@ -37,83 +40,127 @@ export async function generateMetadata(
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
}
'max-image-preview': 'large',
},
},
openGraph: {
title: `Home — ${org.name}`,
description: org.description,
type: 'website',
},
};
}
}
const OrgHomePage = async (params: any) => {
const orgslug = params.params.orgslug;
const cookieStore = cookies();
const orgslug = params.params.orgslug
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const courses = await getOrgCoursesWithAuthHeader(orgslug, { 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'] });
const courses = await getOrgCoursesWithAuthHeader(
orgslug,
{ 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 (
<div>
<GeneralWrapperStyled>
{/* Collections */}
<div className='flex items-center '>
<div className='flex grow'>
<div className="flex items-center ">
<div className="flex grow">
<TypeOfContentTitle title="Collections" type="col" />
</div>
<AuthenticatedClientElement
checkMethod='roles'
ressourceType='collections'
action='create'
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/collections/new")}>
checkMethod="roles"
ressourceType="collections"
action="create"
orgId={org_id}
>
<Link href={getUriWithOrg(orgslug, '/collections/new')}>
<NewCollectionButton />
</Link>
</AuthenticatedClientElement>
</div>
<div className="home_collections flex flex-wrap">
{collections.map((collection: any) => (
<div className="flex flex-col py-3 px-3" key={collection.collection_id}>
<CollectionThumbnail collection={collection} orgslug={orgslug} org_id={org.org_id} />
<div
className="flex flex-col py-3 px-3"
key={collection.collection_id}
>
<CollectionThumbnail
collection={collection}
orgslug={orgslug}
org_id={org.org_id}
/>
</div>
))}
{collections.length == 0 &&
{collections.length == 0 && (
<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 space-y-3">
<div className='mx-auto'>
<svg width="50" 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" />
<div className="mx-auto">
<svg
width="50"
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>
</div>
<div className="space-y-0">
<h1 className="text-xl font-bold text-gray-600">No collections yet</h1>
<p className="text-md text-gray-400">Create a collection to group courses together</p>
<h1 className="text-xl font-bold text-gray-600">
No collections yet
</h1>
<p className="text-md text-gray-400">
Create a collection to group courses together
</p>
</div>
</div>
</div>
</div>
}
)}
</div>
{/* Courses */}
<div className='h-5'></div>
<div className='flex items-center '>
<div className='flex grow'>
<div className="h-5"></div>
<div className="flex items-center ">
<div className="flex grow">
<TypeOfContentTitle title="Courses" type="cou" />
</div>
<AuthenticatedClientElement
ressourceType='courses'
action='create'
checkMethod='roles'
orgId={org_id}>
<Link href={getUriWithOrg(orgslug, "/courses?new=true")}>
ressourceType="courses"
action="create"
checkMethod="roles"
orgId={org_id}
>
<Link href={getUriWithOrg(orgslug, '/courses?new=true')}>
<NewCourseButton />
</Link>
</AuthenticatedClientElement>
@ -124,30 +171,52 @@ const OrgHomePage = async (params: any) => {
<CourseThumbnail course={course} orgslug={orgslug} />
</div>
))}
{courses.length == 0 &&
{courses.length == 0 && (
<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 space-y-3">
<div className='mx-auto'>
<svg width="50" 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" />
<div className="mx-auto">
<svg
width="50"
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>
</div>
<div className="space-y-0">
<h1 className="text-xl font-bold text-gray-600">No courses yet</h1>
<p className="text-md text-gray-400">Create a course to add content</p>
<h1 className="text-xl font-bold text-gray-600">
No courses yet
</h1>
<p className="text-md text-gray-400">
Create a course to add content
</p>
</div>
</div>
</div>
</div>
}
)}
</div>
</GeneralWrapperStyled>
</div>
)
}
);
};
export default OrgHomePage;
export default OrgHomePage

View file

@ -1,33 +1,36 @@
import React from "react";
import { Metadata } from "next";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import Trail from "./trail";
import React from 'react'
import { Metadata } from 'next'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import Trail from './trail'
type MetadataProps = {
params: { orgslug: string };
searchParams: { [key: string]: string | string[] | undefined };
};
params: { orgslug: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
return {
title: "Trail — " + org.name,
description: 'Check your progress using trail and easily navigate through your courses.',
};
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
return {
title: 'Trail — ' + org.name,
description:
'Check your progress using trail and easily navigate through your courses.',
}
}
const TrailPage = async (params: any) => {
let orgslug = params.params.orgslug;
let orgslug = params.params.orgslug
return (
<div>
<Trail orgslug={orgslug} />
</div>
);
};
return (
<div>
<Trail orgslug={orgslug} />
</div>
)
}
export default TrailPage;
export default TrailPage

View file

@ -1,23 +1,24 @@
"use client";
import { useOrg } from "@components/Contexts/OrgContext";
import PageLoading from "@components/Objects/Loaders/PageLoading";
import TrailCourseElement from "@components/Pages/Trail/TrailCourseElement";
import TypeOfContentTitle from "@components/StyledElements/Titles/TypeOfContentTitle";
import GeneralWrapperStyled from "@components/StyledElements/Wrappers/GeneralWrapper";
import { getAPIUrl } from "@services/config/config";
import { swrFetcher } from "@services/utils/ts/requests";
import React, { useEffect } from "react";
import useSWR from "swr";
'use client'
import { useOrg } from '@components/Contexts/OrgContext'
import PageLoading from '@components/Objects/Loaders/PageLoading'
import TrailCourseElement from '@components/Pages/Trail/TrailCourseElement'
import TypeOfContentTitle from '@components/StyledElements/Titles/TypeOfContentTitle'
import GeneralWrapperStyled from '@components/StyledElements/Wrappers/GeneralWrapper'
import { getAPIUrl } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests'
import React, { useEffect } from 'react'
import useSWR from 'swr'
function Trail(params: any) {
let orgslug = params.orgslug;
const org = useOrg() as any;
const orgID = org?.id;
const { data: trail, error: error } = useSWR(`${getAPIUrl()}trail/org/${orgID}/trail`, swrFetcher);
let orgslug = params.orgslug
const org = useOrg() as any
const orgID = org?.id
const { data: trail, error: error } = useSWR(
`${getAPIUrl()}trail/org/${orgID}/trail`,
swrFetcher
)
useEffect(() => {
}
, [trail,org]);
useEffect(() => {}, [trail, org])
return (
<GeneralWrapperStyled>
@ -28,19 +29,17 @@ function Trail(params: any) {
<div className="space-y-6">
{trail.runs.map((run: any) => (
<>
<TrailCourseElement run={run} course={run.course} orgslug={orgslug} />
<TrailCourseElement
run={run}
course={run.course}
orgslug={orgslug}
/>
</>
))}
</div>
)}
</GeneralWrapperStyled>
);
)
}
export default Trail;
export default Trail

View file

@ -1,108 +1,140 @@
'use client';
'use client'
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse';
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail';
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement';
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton';
import Modal from '@components/StyledElements/Modal/Modal';
import { useSearchParams } from 'next/navigation';
import CreateCourseModal from '@components/Objects/Modals/Course/Create/CreateCourse'
import CourseThumbnail from '@components/Objects/Thumbnails/CourseThumbnail'
import AuthenticatedClientElement from '@components/Security/AuthenticatedClientElement'
import NewCourseButton from '@components/StyledElements/Buttons/NewCourseButton'
import Modal from '@components/StyledElements/Modal/Modal'
import { useSearchParams } from 'next/navigation'
import React from 'react'
type CourseProps = {
orgslug: string;
courses: any;
org_id: string;
orgslug: string
courses: any
org_id: string
}
function CoursesHome(params: CourseProps) {
const searchParams = useSearchParams();
const isCreatingCourse = searchParams.get('new') ? true : false;
const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse);
const orgslug = params.orgslug;
const courses = params.courses;
const searchParams = useSearchParams()
const isCreatingCourse = searchParams.get('new') ? true : false
const [newCourseModal, setNewCourseModal] = React.useState(isCreatingCourse)
const orgslug = params.orgslug
const courses = params.courses
async function closeNewCourseModal() {
setNewCourseModal(false)
}
async function closeNewCourseModal() {
setNewCourseModal(false);
}
return (
<div className="h-full w-full bg-[#f8f8f8]">
<div>
<div className="pl-10 mr-10 tracking-tighter">
<BreadCrumbs type="courses" />
return (
<div className='h-full w-full bg-[#f8f8f8]'>
<div >
<div className='pl-10 mr-10 tracking-tighter'>
<BreadCrumbs type='courses' />
<div className='w-100 flex justify-between'>
<div className='pt-3 flex font-bold text-4xl'>Courses</div>
<AuthenticatedClientElement checkMethod='roles'
action='create'
ressourceType='courses'
orgId={params.org_id}>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>}
/>
</AuthenticatedClientElement>
</div>
</div>
</div>
<div className="flex flex-wrap mx-8 mt-7">
{courses.map((course: any) => (
<div className="px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} />
</div>
))}
{courses.length == 0 &&
<div className="flex mx-auto h-[400px]">
<div className="flex flex-col justify-center text-center items-center space-y-5">
<div className='mx-auto'>
<svg width="120" 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>
</div>
<div className="space-y-0">
<h1 className="text-3xl font-bold text-gray-600">No courses yet</h1>
<p className="text-lg text-gray-400">Create a course to add content</p>
</div>
<AuthenticatedClientElement
action='create'
ressourceType='courses'
checkMethod='roles' orgId={params.org_id}>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>}
/>
</AuthenticatedClientElement>
</div>
</div>
<div className="w-100 flex justify-between">
<div className="pt-3 flex font-bold text-4xl">Courses</div>
<AuthenticatedClientElement
checkMethod="roles"
action="create"
ressourceType="courses"
orgId={params.org_id}
>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={
<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>
}
</div>
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>
}
/>
</AuthenticatedClientElement>
</div>
</div>
)
</div>
<div className="flex flex-wrap mx-8 mt-7">
{courses.map((course: any) => (
<div className="px-3" key={course.course_uuid}>
<CourseThumbnail course={course} orgslug={orgslug} />
</div>
))}
{courses.length == 0 && (
<div className="flex mx-auto h-[400px]">
<div className="flex flex-col justify-center text-center items-center space-y-5">
<div className="mx-auto">
<svg
width="120"
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>
</div>
<div className="space-y-0">
<h1 className="text-3xl font-bold text-gray-600">
No courses yet
</h1>
<p className="text-lg text-gray-400">
Create a course to add content
</p>
</div>
<AuthenticatedClientElement
action="create"
ressourceType="courses"
checkMethod="roles"
orgId={params.org_id}
>
<Modal
isDialogOpen={newCourseModal}
onOpenChange={setNewCourseModal}
minHeight="md"
dialogContent={
<CreateCourseModal
closeModal={closeNewCourseModal}
orgslug={orgslug}
></CreateCourseModal>
}
dialogTitle="Create Course"
dialogDescription="Create a new course"
dialogTrigger={
<button>
<NewCourseButton />
</button>
}
/>
</AuthenticatedClientElement>
</div>
</div>
)}
</div>
</div>
)
}
export default CoursesHome
export default CoursesHome

View file

@ -1,72 +1,93 @@
'use client';
'use client'
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 { CourseProvider } from '../../../../../../../../components/Contexts/CourseContext';
import Link from 'next/link';
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop';
import { motion } from 'framer-motion';
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral';
import { GalleryVerticalEnd, Info } from 'lucide-react';
import { CourseProvider } from '../../../../../../../../components/Contexts/CourseContext'
import Link from 'next/link'
import { CourseOverviewTop } from '@components/Dashboard/UI/CourseOverviewTop'
import { motion } from 'framer-motion'
import EditCourseGeneral from '@components/Dashboard/Course/EditCourseGeneral/EditCourseGeneral'
import { GalleryVerticalEnd, Info } from 'lucide-react'
export type CourseOverviewParams = {
orgslug: string,
courseuuid: string,
subpage: string
orgslug: string
courseuuid: string
subpage: string
}
function CourseOverviewPage({ params }: { params: CourseOverviewParams }) {
function getEntireCourseUUID(courseuuid: string) {
// add course_ to uuid
return `course_${courseuuid}`
}
function getEntireCourseUUID(courseuuid: string) {
// add course_ to uuid
return `course_${courseuuid}`
}
return (
<div className='h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]'>
<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)]'>
<CourseOverviewTop params={params} />
<div className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/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} />
<div>General</div>
</div>
</div>
</Link>
<Link href={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} />
<div>Content</div>
</div>
</div>
</Link>
</div>
return (
<div className="h-screen w-full bg-[#f8f8f8] grid grid-rows-[auto,1fr]">
<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)]">
<CourseOverviewTop params={params} />
<div className="flex space-x-5 font-black text-sm">
<Link
href={
getUriWithOrg(params.orgslug, '') +
`/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} />
<div>General</div>
</div>
<motion.div
initial={{ opacity: 0, }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
className='h-full overflow-y-auto'
>
{params.subpage == 'content' ? <EditCourseStructure orgslug={params.orgslug} /> : ''}
{params.subpage == 'general' ? <EditCourseGeneral orgslug={params.orgslug} /> : ''}
</motion.div>
</CourseProvider>
</div>
</Link>
<Link
href={
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} />
<div>Content</div>
</div>
</div>
</Link>
</div>
</div>
)
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
className="h-full overflow-y-auto"
>
{params.subpage == 'content' ? (
<EditCourseStructure orgslug={params.orgslug} />
) : (
''
)}
{params.subpage == 'general' ? (
<EditCourseGeneral orgslug={params.orgslug} />
) : (
''
)}
</motion.div>
</CourseProvider>
</div>
)
}
export default CourseOverviewPage
export default CourseOverviewPage

View file

@ -1,57 +1,63 @@
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth';
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses';
import { getOrganizationContextInfo } from '@services/organizations/orgs';
import { Metadata } from 'next';
import { cookies } from 'next/headers';
import { getAccessTokenFromRefreshTokenCookie } from '@services/auth/auth'
import { getOrgCoursesWithAuthHeader } from '@services/courses/courses'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import { Metadata } from 'next'
import { cookies } from 'next/headers'
import React from 'react'
import CoursesHome from './client';
import CoursesHome from './client'
type MetadataProps = {
params: { orgslug: string };
searchParams: { [key: string]: string | string[] | undefined };
};
params: { orgslug: string }
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
const org = await getOrganizationContextInfo(params.orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
// Get Org context information
const org = await getOrganizationContextInfo(params.orgslug, { revalidate: 1800, tags: ['organizations'] });
// SEO
return {
title: "Courses — " + org.name,
description: org.description,
keywords: `${org.name}, ${org.description}, courses, learning, education, online learning, edu, online courses, ${org.name} courses`,
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: true,
"max-image-preview": "large",
}
},
openGraph: {
title: "Courses — " + org.name,
description: org.description,
type: 'website',
},
};
// SEO
return {
title: 'Courses — ' + org.name,
description: org.description,
keywords: `${org.name}, ${org.description}, courses, learning, education, online learning, edu, online courses, ${org.name} courses`,
robots: {
index: true,
follow: true,
nocache: true,
googleBot: {
index: true,
follow: true,
'max-image-preview': 'large',
},
},
openGraph: {
title: 'Courses — ' + org.name,
description: org.description,
type: 'website',
},
}
}
async function CoursesPage(params: any) {
const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 1800, tags: ['organizations'] });
const cookieStore = cookies();
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const courses = await getOrgCoursesWithAuthHeader(orgslug, { revalidate: 0, tags: ['courses'] }, access_token ? access_token : null);
const orgslug = params.params.orgslug
const org = await getOrganizationContextInfo(orgslug, {
revalidate: 1800,
tags: ['organizations'],
})
const cookieStore = cookies()
const access_token = await getAccessTokenFromRefreshTokenCookie(cookieStore)
const courses = await getOrgCoursesWithAuthHeader(
orgslug,
{ revalidate: 0, tags: ['courses'] },
access_token ? access_token : null
)
return (
<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

View file

@ -3,21 +3,25 @@ import LeftMenu from '@components/Dashboard/UI/LeftMenu'
import AdminAuthorization from '@components/Security/AdminAuthorization'
import React from 'react'
function DashboardLayout({ children, params }: { children: React.ReactNode, params: any }) {
return (
<>
<SessionProvider>
<AdminAuthorization authorizationMode="page">
<div className='flex'>
<LeftMenu />
<div className='flex w-full'>
{children}
</div>
</div>
</AdminAuthorization>
</SessionProvider>
</>
)
function DashboardLayout({
children,
params,
}: {
children: React.ReactNode
params: any
}) {
return (
<>
<SessionProvider>
<AdminAuthorization authorizationMode="page">
<div className="flex">
<LeftMenu />
<div className="flex w-full">{children}</div>
</div>
</AdminAuthorization>
</SessionProvider>
</>
)
}
export default DashboardLayout
export default DashboardLayout

View file

@ -1,4 +1,4 @@
'use client';
'use client'
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { getUriWithOrg } from '@services/config/config'
import { Info } from 'lucide-react'
@ -8,44 +8,54 @@ import { motion } from 'framer-motion'
import OrgEditGeneral from '@components/Dashboard/Org/OrgEditGeneral/OrgEditGeneral'
export type OrgParams = {
subpage: string
orgslug: string
subpage: string
orgslug: string
}
function OrgPage({ params }: { params: OrgParams }) {
return (
<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)]'>
<BreadCrumbs type='org' ></BreadCrumbs>
<div className='my-2 tracking-tighter'>
<div className='w-100 flex justify-between'>
<div className='pt-3 flex font-bold text-4xl'>Organization Settings</div>
</div>
</div>
<div className='flex space-x-5 font-black text-sm'>
<Link href={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} />
<div>General</div>
</div>
</div>
</Link>
</div>
return (
<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)]">
<BreadCrumbs type="org"></BreadCrumbs>
<div className="my-2 tracking-tighter">
<div className="w-100 flex justify-between">
<div className="pt-3 flex font-bold text-4xl">
Organization Settings
</div>
<div className='h-6'></div>
<motion.div
initial={{ opacity: 0, }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
>
{params.subpage == 'general' ? <OrgEditGeneral /> : ''}
</motion.div>
</div>
</div>
)
<div className="flex space-x-5 font-black text-sm">
<Link
href={
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} />
<div>General</div>
</div>
</div>
</Link>
</div>
</div>
<div className="h-6"></div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
>
{params.subpage == 'general' ? <OrgEditGeneral /> : ''}
</motion.div>
</div>
)
}
export default OrgPage
export default OrgPage

View file

@ -6,59 +6,89 @@ import Link from 'next/link'
import AdminAuthorization from '@components/Security/AdminAuthorization'
function DashboardHome() {
return (
<div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3">
<div className='mx-auto pb-10'>
<Image alt='learnhouse logo' width={230} src={learnhousetextlogo}></Image>
return (
<div className="flex items-center justify-center mx-auto min-h-screen flex-col space-x-3">
<div className="mx-auto pb-10">
<Image
alt="learnhouse logo"
width={230}
src={learnhousetextlogo}
></Image>
</div>
<AdminAuthorization authorizationMode="component">
<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"
>
<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>
<AdminAuthorization authorizationMode="component">
<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'>
<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>
</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'>
<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>
</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'>
<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>
</Link>
</div>
</AdminAuthorization>
<div className='flex flex-col space-y-10 '>
<AdminAuthorization authorizationMode="component">
<div className='h-1 w-[100px] bg-neutral-200 rounded-full mx-auto'></div>
<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'>
<BookCopy className=' text-gray-100' size={20}></BookCopy>
<div className=' text-sm font-bold text-gray-100'>Learn LearnHouse</div>
</Link>
</div>
<div className='mx-auto mt-[40px] w-28 h-1 bg-neutral-200 rounded-full'></div>
</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'>
<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>
</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"
>
<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>
</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"
>
<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>
</Link>
</div>
)
</AdminAuthorization>
<div className="flex flex-col space-y-10 ">
<AdminAuthorization authorizationMode="component">
<div className="h-1 w-[100px] bg-neutral-200 rounded-full mx-auto"></div>
<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"
>
<BookCopy className=" text-gray-100" size={20}></BookCopy>
<div className=" text-sm font-bold text-gray-100">
Learn LearnHouse
</div>
</Link>
</div>
<div className="mx-auto mt-[40px] w-28 h-1 bg-neutral-200 rounded-full"></div>
</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"
>
<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>
</Link>
</div>
</div>
)
}
export default DashboardHome
export default DashboardHome

View file

@ -1,70 +1,90 @@
'use client';
'use client'
import React, { useEffect } from 'react'
import { motion } from 'framer-motion';
import UserEditGeneral from '@components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral';
import UserEditPassword from '@components/Dashboard/UserAccount/UserEditPassword/UserEditPassword';
import Link from 'next/link';
import { getUriWithOrg } from '@services/config/config';
import { Info, Lock } from 'lucide-react';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs';
import { useSession } from '@components/Contexts/SessionContext';
import { motion } from 'framer-motion'
import UserEditGeneral from '@components/Dashboard/UserAccount/UserEditGeneral/UserEditGeneral'
import UserEditPassword from '@components/Dashboard/UserAccount/UserEditPassword/UserEditPassword'
import Link from 'next/link'
import { getUriWithOrg } from '@services/config/config'
import { Info, Lock } from 'lucide-react'
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { useSession } from '@components/Contexts/SessionContext'
export type SettingsParams = {
subpage: string
orgslug: string
subpage: string
orgslug: string
}
function SettingsPage({ params }: { params: SettingsParams }) {
const session = useSession() as any;
const session = useSession() as any
useEffect(() => {}, [session])
useEffect(() => {
}
, [session])
return (
<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)]'>
<BreadCrumbs type='user' last_breadcrumb={session?.user?.username} ></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 className='flex space-x-5 font-black text-sm'>
<Link href={getUriWithOrg(params.orgslug, "") + `/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} />
<div>General</div>
</div>
</div>
</Link>
<Link href={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} />
<div>Password</div>
</div>
</div>
</Link>
</div>
</div>
<div className='h-6'></div>
<motion.div
initial={{ opacity: 0, }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
className='h-full overflow-y-auto'
>
{params.subpage == 'general' ? <UserEditGeneral /> : ''}
{params.subpage == 'security' ? <UserEditPassword /> : ''}
</motion.div>
return (
<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)]">
<BreadCrumbs
type="user"
last_breadcrumb={session?.user?.username}
></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 className="flex space-x-5 font-black text-sm">
<Link
href={
getUriWithOrg(params.orgslug, '') +
`/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} />
<div>General</div>
</div>
</div>
</Link>
<Link
href={
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} />
<div>Password</div>
</div>
</div>
</Link>
</div>
</div>
<div className="h-6"></div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
className="h-full overflow-y-auto"
>
{params.subpage == 'general' ? <UserEditGeneral /> : ''}
{params.subpage == 'security' ? <UserEditPassword /> : ''}
</motion.div>
</div>
)
}
export default SettingsPage
export default SettingsPage

View file

@ -1,101 +1,128 @@
'use client';
'use client'
import React, { useEffect } from 'react'
import { motion } from 'framer-motion';
import Link from 'next/link';
import { getUriWithOrg } from '@services/config/config';
import { ScanEye, UserPlus, Users } from 'lucide-react';
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs';
import { useSession } from '@components/Contexts/SessionContext';
import { useOrg } from '@components/Contexts/OrgContext';
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers';
import OrgAccess from '@components/Dashboard/Users/OrgAccess/OrgAccess';
import { motion } from 'framer-motion'
import Link from 'next/link'
import { getUriWithOrg } from '@services/config/config'
import { ScanEye, UserPlus, Users } from 'lucide-react'
import BreadCrumbs from '@components/Dashboard/UI/BreadCrumbs'
import { useSession } from '@components/Contexts/SessionContext'
import { useOrg } from '@components/Contexts/OrgContext'
import OrgUsers from '@components/Dashboard/Users/OrgUsers/OrgUsers'
import OrgAccess from '@components/Dashboard/Users/OrgAccess/OrgAccess'
export type SettingsParams = {
subpage: string
orgslug: string
subpage: string
orgslug: string
}
function UsersSettingsPage({ params }: { params: SettingsParams }) {
const session = useSession() as any;
const org = useOrg() as any;
const [H1Label, setH1Label] = React.useState('')
const [H2Label, setH2Label] = React.useState('')
const session = useSession() as any
const org = useOrg() as any
const [H1Label, setH1Label] = React.useState('')
const [H2Label, setH2Label] = React.useState('')
function handleLabels() {
if (params.subpage == 'users') {
setH1Label('Users')
setH2Label('Manage your organization users, assign roles and permissions')
}
if (params.subpage == 'signups') {
setH1Label('Signup Access')
setH2Label('Choose from where users can join your organization')
}
if (params.subpage == 'add') {
setH1Label('Invite users')
setH2Label('Invite users to join your organization')
}
function handleLabels() {
if (params.subpage == 'users') {
setH1Label('Users')
setH2Label('Manage your organization users, assign roles and permissions')
}
useEffect(() => {
handleLabels()
if (params.subpage == 'signups') {
setH1Label('Signup Access')
setH2Label('Choose from where users can join your organization')
}
, [session, org, params.subpage, params])
if (params.subpage == 'add') {
setH1Label('Invite users')
setH2Label('Invite users to join your organization')
}
}
return (
<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)]'>
<BreadCrumbs type='orgusers' ></BreadCrumbs>
<div className='my-2 py-3'>
<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='flex font-medium text-gray-400 text-md'>{H2Label} </div>
</div>
</div>
<div className='flex space-x-5 font-black text-sm'>
<Link href={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`}>
useEffect(() => {
handleLabels()
}, [session, org, params.subpage, params])
<div className='flex items-center space-x-2.5 mx-2'>
<Users size={16} />
<div>Users</div>
</div>
</div>
</Link>
<Link href={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} />
<div>Invite users</div>
</div>
</div>
</Link>
<Link href={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} />
<div>Signup Access</div>
</div>
</div>
</Link>
</div>
return (
<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)]">
<BreadCrumbs type="orgusers"></BreadCrumbs>
<div className="my-2 py-3">
<div className="w-100 flex flex-col space-y-1">
<div className="pt-3 flex font-bold text-4xl tracking-tighter">
{H1Label}
</div>
<motion.div
initial={{ opacity: 0, }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.10, type: "spring", stiffness: 80 }}
className='h-full overflow-y-auto'
>
{params.subpage == 'users' ? <OrgUsers /> : ''}
{params.subpage == 'signups' ? <OrgAccess /> : ''}
</motion.div>
<div className="flex font-medium text-gray-400 text-md">
{H2Label}{' '}
</div>
</div>
</div>
)
<div className="flex space-x-5 font-black text-sm">
<Link
href={
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} />
<div>Users</div>
</div>
</div>
</Link>
<Link
href={
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} />
<div>Invite users</div>
</div>
</div>
</Link>
<Link
href={
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} />
<div>Signup Access</div>
</div>
</div>
</Link>
</div>
</div>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.1, type: 'spring', stiffness: 80 }}
className="h-full overflow-y-auto"
>
{params.subpage == 'users' ? <OrgUsers /> : ''}
{params.subpage == 'signups' ? <OrgAccess /> : ''}
</motion.div>
</div>
)
}
export default UsersSettingsPage
export default UsersSettingsPage

View file

@ -1,17 +1,20 @@
'use client';
import { OrgProvider } from "@components/Contexts/OrgContext";
import SessionProvider from "@components/Contexts/SessionContext";
import "@styles/globals.css";
export default function RootLayout({ children, params }: { children: React.ReactNode, params: any }) {
'use client'
import { OrgProvider } from '@components/Contexts/OrgContext'
import SessionProvider from '@components/Contexts/SessionContext'
import '@styles/globals.css'
export default function RootLayout({
children,
params,
}: {
children: React.ReactNode
params: any
}) {
return (
<div>
<OrgProvider orgslug={params.orgslug}>
<SessionProvider>
{children}
</SessionProvider>
<SessionProvider>{children}</SessionProvider>
</OrgProvider>
</div>
);
)
}

View file

@ -1,138 +1,174 @@
"use client";;
import learnhouseIcon from "public/learnhouse_bigicon_1.png";
import FormLayout, { FormField, FormLabelAndMessage, Input } from '@components/StyledElements/Form/Form';
import Image from 'next/image';
import * as Form from '@radix-ui/react-form';
import { useFormik } from 'formik';
import { getOrgLogoMediaDirectory } from "@services/media/media";
import React from "react";
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";
'use client'
import learnhouseIcon from 'public/learnhouse_bigicon_1.png'
import FormLayout, {
FormField,
FormLabelAndMessage,
Input,
} from '@components/StyledElements/Form/Form'
import Image from 'next/image'
import * as Form from '@radix-ui/react-form'
import { useFormik } from 'formik'
import { getOrgLogoMediaDirectory } from '@services/media/media'
import React from 'react'
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 {
org: any;
org: any
}
const validate = (values: any) => {
const errors: any = {};
const errors: any = {}
if (!values.email) {
errors.email = 'Required';
}
else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address';
}
if (!values.email) {
errors.email = 'Required'
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
errors.email = 'Invalid email address'
}
if (!values.password) {
errors.password = 'Required';
}
else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
if (!values.password) {
errors.password = 'Required'
} else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters'
}
return errors;
};
return errors
}
const LoginClient = (props: LoginClientProps) => {
const [isSubmitting, setIsSubmitting] = React.useState(false);
const router = useRouter();
const [error, setError] = React.useState('');
const formik = useFormik({
initialValues: {
email: '',
password: '',
},
validate,
onSubmit: async values => {
setIsSubmitting(true);
let res = await loginAndGetToken(values.email, values.password);
let message = await res.json();
if (res.status == 200) {
router.push(`/`);
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 (
<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 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="m-auto flex space-x-4 items-center flex-wrap">
<div>Login to </div>
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" >
{props.org?.logo_image ? (
<img
src={`${getOrgLogoMediaDirectory(props.org.org_uuid, props.org?.logo_image)}`}
alt="Learnhouse"
style={{ width: "auto", height: 70 }}
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="" />
)}
</div>
<div className="font-bold text-xl">{props.org?.name}</div>
</div>
</div>
</div>
<div className="left-login-part bg-white flex flex-row">
<div className="login-form m-auto w-72">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email">
<FormLabelAndMessage label='Email' message={formik.errors.email} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
</Form.Control>
</FormField>
{/* for password */}
<FormField name="password">
<FormLabelAndMessage label='Password' message={formik.errors.password} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
</Form.Control>
</FormField>
<div className="flex py-4">
<Form.Submit asChild>
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
{isSubmitting ? "Loading..."
: "Login"}
</button>
</Form.Submit>
</div>
</FormLayout>
</div>
</div>
const [isSubmitting, setIsSubmitting] = React.useState(false)
const router = useRouter()
const [error, setError] = React.useState('')
const formik = useFormik({
initialValues: {
email: '',
password: '',
},
validate,
onSubmit: async (values) => {
setIsSubmitting(true)
let res = await loginAndGetToken(values.email, values.password)
let message = await res.json()
if (res.status == 200) {
router.push(`/`)
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 (
<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 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="m-auto flex space-x-4 items-center flex-wrap">
<div>Login to </div>
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]">
{props.org?.logo_image ? (
<img
src={`${getOrgLogoMediaDirectory(
props.org.org_uuid,
props.org?.logo_image
)}`}
alt="Learnhouse"
style={{ width: 'auto', height: 70 }}
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=""
/>
)}
</div>
<div className="font-bold text-xl">{props.org?.name}</div>
</div>
</div>
</div>
<div className="left-login-part bg-white flex flex-row">
<div className="login-form m-auto w-72">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email">
<FormLabelAndMessage
label="Email"
message={formik.errors.email}
/>
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.email}
type="email"
required
/>
</Form.Control>
</FormField>
{/* for password */}
<FormField name="password">
<FormLabelAndMessage
label="Password"
message={formik.errors.password}
/>
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.password}
type="password"
required
/>
</Form.Control>
</FormField>
<div className="flex py-4">
<Form.Submit asChild>
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer">
{isSubmitting ? 'Loading...' : 'Login'}
</button>
</Form.Submit>
</div>
</FormLayout>
</div>
</div>
</div>
)
}
export default LoginClient

View file

@ -1,35 +1,39 @@
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import LoginClient from "./login";
import { Metadata } from 'next';
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import LoginClient from './login'
import { Metadata } from 'next'
type MetadataProps = {
params: { orgslug: string, courseid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
params: { orgslug: string; courseid: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const orgslug = params.orgslug;
// Get Org context information
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const orgslug = params.orgslug
// Get Org context information
const org = await getOrganizationContextInfo(orgslug, {
revalidate: 0,
tags: ['organizations'],
})
return {
title: 'Login' + `${org.name}`,
};
}
}
const Login = async (params: any) => {
const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
const orgslug = params.params.orgslug
const org = await getOrganizationContextInfo(orgslug, {
revalidate: 0,
tags: ['organizations'],
})
return (
<div>
<LoginClient org={org}></LoginClient>
</div>
);
};
)
}
export default Login;
export default Login

View file

@ -1,166 +1,186 @@
"use client";
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
'use client'
import { useFormik } from 'formik'
import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } 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';
import FormLayout, {
FormField,
FormLabelAndMessage,
Input,
Textarea,
} 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 errors: any = {};
const errors: any = {}
if (!values.email) {
errors.email = 'Required';
}
else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address';
}
if (!values.email) {
errors.email = 'Required'
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
errors.email = 'Invalid email address'
}
if (!values.password) {
errors.password = 'Required';
}
else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
if (!values.password) {
errors.password = 'Required'
} else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters'
}
if (!values.username) {
errors.username = 'Required';
}
if (!values.username) {
errors.username = 'Required'
}
if (!values.username || values.username.length < 4) {
errors.username = 'Username must be at least 4 characters';
}
if (!values.username || values.username.length < 4) {
errors.username = 'Username must be at least 4 characters'
}
if (!values.bio) {
errors.bio = 'Required';
}
if (!values.bio) {
errors.bio = 'Required'
}
return errors;
};
return errors
}
interface InviteOnlySignUpProps {
inviteCode: string;
inviteCode: string
}
function InviteOnlySignUpComponent(props : InviteOnlySignUpProps) {
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({
initialValues: {
org_slug: org?.slug,
org_id: org?.id,
email: '',
password: '',
username: '',
bio: '',
first_name: '',
last_name: '',
},
validate,
onSubmit: async values => {
setError('')
setMessage('')
setIsSubmitting(true);
let res = await signUpWithInviteCode(values, props.inviteCode);
let message = await res.json();
if (res.status == 200) {
//router.push(`/login`);
setMessage('Your account was successfully created')
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);
}
function InviteOnlySignUpComponent(props: InviteOnlySignUpProps) {
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({
initialValues: {
org_slug: org?.slug,
org_id: org?.id,
email: '',
password: '',
username: '',
bio: '',
first_name: '',
last_name: '',
},
validate,
onSubmit: async (values) => {
setError('')
setMessage('')
setIsSubmitting(true)
let res = await signUpWithInviteCode(values, props.inviteCode)
let message = await res.json()
if (res.status == 200) {
//router.push(`/login`);
setMessage('Your account was successfully created')
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(() => {}, [org])
useEffect(() => {
}
, [org]);
return (
<div className="login-form m-auto w-72">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
{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 space-x-2">
<Check size={18} />
<div className="font-bold text-sm">{message}</div>
</div>
<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>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email">
<FormLabelAndMessage label="Email" message={formik.errors.email} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.email}
type="email"
required
/>
</Form.Control>
</FormField>
{/* for password */}
<FormField name="password">
<FormLabelAndMessage
label="Password"
message={formik.errors.password}
/>
return (
<div className="login-form m-auto w-72">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
{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 space-x-2'>
<Check size={18} />
<div className="font-bold text-sm">{message}</div>
</div>
<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>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email">
<FormLabelAndMessage label='Email' message={formik.errors.email} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
</Form.Control>
</FormField>
{/* for password */}
<FormField name="password">
<FormLabelAndMessage label='Password' message={formik.errors.password} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.password}
type="password"
required
/>
</Form.Control>
</FormField>
{/* for username */}
<FormField name="username">
<FormLabelAndMessage
label="Username"
message={formik.errors.username}
/>
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
</Form.Control>
</FormField>
{/* for username */}
<FormField name="username">
<FormLabelAndMessage label='Username' message={formik.errors.username} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.username}
type="text"
required
/>
</Form.Control>
</FormField>
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
</Form.Control>
</FormField>
{/* for bio */}
<FormField name="bio">
<FormLabelAndMessage label="Bio" message={formik.errors.bio} />
{/* for bio */}
<FormField name="bio">
<FormLabelAndMessage label='Bio' message={formik.errors.bio} />
<Form.Control asChild>
<Textarea
onChange={formik.handleChange}
value={formik.values.bio}
required
/>
</Form.Control>
</FormField>
<Form.Control asChild>
<Textarea onChange={formik.handleChange} value={formik.values.bio} required />
</Form.Control>
</FormField>
<div className="flex py-4">
<Form.Submit asChild>
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
{isSubmitting ? "Loading..."
: "Create an account & Join"}
</button>
</Form.Submit>
</div>
</FormLayout>
</div>
)
<div className="flex py-4">
<Form.Submit asChild>
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer">
{isSubmitting ? 'Loading...' : 'Create an account & Join'}
</button>
</Form.Submit>
</div>
</FormLayout>
</div>
)
}
export default InviteOnlySignUpComponent
export default InviteOnlySignUpComponent

View file

@ -1,163 +1,182 @@
"use client";
import { useFormik } from 'formik';
import { useRouter } from 'next/navigation';
'use client'
import { useFormik } from 'formik'
import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } 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';
import FormLayout, {
FormField,
FormLabelAndMessage,
Input,
Textarea,
} 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 errors: any = {};
const errors: any = {}
if (!values.email) {
errors.email = 'Required';
}
else if (
!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)
) {
errors.email = 'Invalid email address';
}
if (!values.email) {
errors.email = 'Required'
} else if (!/^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i.test(values.email)) {
errors.email = 'Invalid email address'
}
if (!values.password) {
errors.password = 'Required';
}
else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters';
}
if (!values.password) {
errors.password = 'Required'
} else if (values.password.length < 8) {
errors.password = 'Password must be at least 8 characters'
}
if (!values.username) {
errors.username = 'Required';
}
if (!values.username) {
errors.username = 'Required'
}
if (!values.username || values.username.length < 4) {
errors.username = 'Username must be at least 4 characters';
}
if (!values.username || values.username.length < 4) {
errors.username = 'Username must be at least 4 characters'
}
if (!values.bio) {
errors.bio = 'Required';
}
if (!values.bio) {
errors.bio = 'Required'
}
return errors;
};
function OpenSignUpComponent() {
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({
initialValues: {
org_slug: org?.slug,
org_id: org?.id,
email: '',
password: '',
username: '',
bio: '',
first_name: '',
last_name: '',
},
validate,
onSubmit: async values => {
setError('')
setMessage('')
setIsSubmitting(true);
let res = await signup(values);
let message = await res.json();
if (res.status == 200) {
//router.push(`/login`);
setMessage('Your account was successfully created')
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(() => {
}
, [org]);
return (
<div className="login-form m-auto w-72">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
{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 space-x-2'>
<Check size={18} />
<div className="font-bold text-sm">{message}</div>
</div>
<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>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email">
<FormLabelAndMessage label='Email' message={formik.errors.email} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.email} type="email" required />
</Form.Control>
</FormField>
{/* for password */}
<FormField name="password">
<FormLabelAndMessage label='Password' message={formik.errors.password} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.password} type="password" required />
</Form.Control>
</FormField>
{/* for username */}
<FormField name="username">
<FormLabelAndMessage label='Username' message={formik.errors.username} />
<Form.Control asChild>
<Input onChange={formik.handleChange} value={formik.values.username} type="text" required />
</Form.Control>
</FormField>
{/* for bio */}
<FormField name="bio">
<FormLabelAndMessage label='Bio' message={formik.errors.bio} />
<Form.Control asChild>
<Textarea onChange={formik.handleChange} value={formik.values.bio} required />
</Form.Control>
</FormField>
<div className="flex py-4">
<Form.Submit asChild>
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer" >
{isSubmitting ? "Loading..."
: "Create an account"}
</button>
</Form.Submit>
</div>
</FormLayout>
</div>
)
return errors
}
export default OpenSignUpComponent
function OpenSignUpComponent() {
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({
initialValues: {
org_slug: org?.slug,
org_id: org?.id,
email: '',
password: '',
username: '',
bio: '',
first_name: '',
last_name: '',
},
validate,
onSubmit: async (values) => {
setError('')
setMessage('')
setIsSubmitting(true)
let res = await signup(values)
let message = await res.json()
if (res.status == 200) {
//router.push(`/login`);
setMessage('Your account was successfully created')
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(() => {}, [org])
return (
<div className="login-form m-auto w-72">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
{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 space-x-2">
<Check size={18} />
<div className="font-bold text-sm">{message}</div>
</div>
<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>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="email">
<FormLabelAndMessage label="Email" message={formik.errors.email} />
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.email}
type="email"
required
/>
</Form.Control>
</FormField>
{/* for password */}
<FormField name="password">
<FormLabelAndMessage
label="Password"
message={formik.errors.password}
/>
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.password}
type="password"
required
/>
</Form.Control>
</FormField>
{/* for username */}
<FormField name="username">
<FormLabelAndMessage
label="Username"
message={formik.errors.username}
/>
<Form.Control asChild>
<Input
onChange={formik.handleChange}
value={formik.values.username}
type="text"
required
/>
</Form.Control>
</FormField>
{/* for bio */}
<FormField name="bio">
<FormLabelAndMessage label="Bio" message={formik.errors.bio} />
<Form.Control asChild>
<Textarea
onChange={formik.handleChange}
value={formik.values.bio}
required
/>
</Form.Control>
</FormField>
<div className="flex py-4">
<Form.Submit asChild>
<button className="w-full bg-black text-white font-bold text-center p-2 rounded-md shadow-md hover:cursor-pointer">
{isSubmitting ? 'Loading...' : 'Create an account'}
</button>
</Form.Submit>
</div>
</FormLayout>
</div>
)
}
export default OpenSignUpComponent

View file

@ -1,37 +1,42 @@
import { Metadata } from "next";
import { getOrganizationContextInfo } from "@services/organizations/orgs";
import SignUpClient from "./signup";
import { Suspense } from "react";
import PageLoading from "@components/Objects/Loaders/PageLoading";
import { Metadata } from 'next'
import { getOrganizationContextInfo } from '@services/organizations/orgs'
import SignUpClient from './signup'
import { Suspense } from 'react'
import PageLoading from '@components/Objects/Loaders/PageLoading'
type MetadataProps = {
params: { orgslug: string, courseid: string };
searchParams: { [key: string]: string | string[] | undefined };
};
params: { orgslug: string; courseid: string }
searchParams: { [key: string]: string | string[] | undefined }
}
export async function generateMetadata(
{ params }: MetadataProps,
): Promise<Metadata> {
const orgslug = params.orgslug;
// Get Org context information
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
export async function generateMetadata({
params,
}: MetadataProps): Promise<Metadata> {
const orgslug = params.orgslug
// Get Org context information
const org = await getOrganizationContextInfo(orgslug, {
revalidate: 0,
tags: ['organizations'],
})
return {
title: 'Sign up' + `${org.name}`,
};
}
}
const SignUp = async (params: any) => {
const orgslug = params.params.orgslug;
const org = await getOrganizationContextInfo(orgslug, { revalidate: 0, tags: ['organizations'] });
const orgslug = params.params.orgslug
const org = await getOrganizationContextInfo(orgslug, {
revalidate: 0,
tags: ['organizations'],
})
return (
<>
<Suspense fallback={<PageLoading/>}>
<Suspense fallback={<PageLoading />}>
<SignUpClient org={org} />
</Suspense>
</>
);
};
export default SignUp;
)
}
export default SignUp

View file

@ -1,179 +1,213 @@
"use client";
import learnhouseIcon from "public/learnhouse_bigicon_1.png";
import Image from 'next/image';
import { getOrgLogoMediaDirectory } from '@services/media/media';
import Link from 'next/link';
import { getUriWithOrg } from '@services/config/config';
import { useSession } from "@components/Contexts/SessionContext";
import React, { useEffect } from "react";
import { MailWarning, Shield, UserPlus } from "lucide-react";
import { useOrg } from "@components/Contexts/OrgContext";
import UserAvatar from "@components/Objects/UserAvatar";
import OpenSignUpComponent from "./OpenSignup";
import InviteOnlySignUpComponent from "./InviteOnlySignUp";
import { useRouter, useSearchParams } from "next/navigation";
import { validateInviteCode } from "@services/organizations/invites";
import PageLoading from "@components/Objects/Loaders/PageLoading";
import Toast from "@components/StyledElements/Toast/Toast";
import toast from "react-hot-toast";
'use client'
import learnhouseIcon from 'public/learnhouse_bigicon_1.png'
import Image from 'next/image'
import { getOrgLogoMediaDirectory } from '@services/media/media'
import Link from 'next/link'
import { getUriWithOrg } from '@services/config/config'
import { useSession } from '@components/Contexts/SessionContext'
import React, { useEffect } from 'react'
import { MailWarning, Shield, UserPlus } from 'lucide-react'
import { useOrg } from '@components/Contexts/OrgContext'
import UserAvatar from '@components/Objects/UserAvatar'
import OpenSignUpComponent from './OpenSignup'
import InviteOnlySignUpComponent from './InviteOnlySignUp'
import { useRouter, useSearchParams } from 'next/navigation'
import { validateInviteCode } from '@services/organizations/invites'
import PageLoading from '@components/Objects/Loaders/PageLoading'
import Toast from '@components/StyledElements/Toast/Toast'
import toast from 'react-hot-toast'
interface SignUpClientProps {
org: any;
org: any
}
function SignUpClient(props: SignUpClientProps) {
const session = useSession() as any;
const [joinMethod, setJoinMethod] = React.useState('open');
const [inviteCode, setInviteCode] = React.useState('');
const searchParams = useSearchParams()
const inviteCodeParam = searchParams.get('inviteCode')
const session = useSession() as any
const [joinMethod, setJoinMethod] = React.useState('open')
const [inviteCode, setInviteCode] = React.useState('')
const searchParams = useSearchParams()
const inviteCodeParam = searchParams.get('inviteCode')
useEffect(() => {
if (props.org.config) {
setJoinMethod(props.org?.config?.config?.GeneralConfig.users.signup_mechanism);
console.log(props.org?.config?.config?.GeneralConfig.users.signup_mechanism)
}
if (inviteCodeParam) {
setInviteCode(inviteCodeParam);
}
useEffect(() => {
if (props.org.config) {
setJoinMethod(
props.org?.config?.config?.GeneralConfig.users.signup_mechanism
)
console.log(
props.org?.config?.config?.GeneralConfig.users.signup_mechanism
)
}
, [props.org, inviteCodeParam]);
if (inviteCodeParam) {
setInviteCode(inviteCodeParam)
}
}, [props.org, inviteCodeParam])
return (
<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 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-3/4 flex flex-row text-white">
<div className="m-auto flex space-x-4 items-center flex-wrap">
<div>You've been invited to join </div>
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]" >
{props.org?.logo_image ? (
<img
src={`${getOrgLogoMediaDirectory(props.org.org_uuid, props.org?.logo_image)}`}
alt="Learnhouse"
style={{ width: "auto", height: 70 }}
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="" />
)}
</div>
<div className="font-bold text-xl">{props.org?.name}</div>
</div>
</div>
</div>
<div className="left-join-part bg-white flex flex-row">
{joinMethod == 'open' && (
session.isAuthenticated ? <LoggedInJoinScreen inviteCode={inviteCode} /> : <OpenSignUpComponent />
)}
{joinMethod == 'inviteOnly' && (
inviteCode ? (
session.isAuthenticated ? <LoggedInJoinScreen /> : <InviteOnlySignUpComponent inviteCode={inviteCode} />
) : <NoTokenScreen />
)}
</div>
return (
<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 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-3/4 flex flex-row text-white">
<div className="m-auto flex space-x-4 items-center flex-wrap">
<div>You've been invited to join </div>
<div className="shadow-[0px_4px_16px_rgba(0,0,0,0.02)]">
{props.org?.logo_image ? (
<img
src={`${getOrgLogoMediaDirectory(
props.org.org_uuid,
props.org?.logo_image
)}`}
alt="Learnhouse"
style={{ width: 'auto', height: 70 }}
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=""
/>
)}
</div>
<div className="font-bold text-xl">{props.org?.name}</div>
</div>
</div>
</div>
<div className="left-join-part bg-white flex flex-row">
{joinMethod == 'open' &&
(session.isAuthenticated ? (
<LoggedInJoinScreen inviteCode={inviteCode} />
) : (
<OpenSignUpComponent />
))}
{joinMethod == 'inviteOnly' &&
(inviteCode ? (
session.isAuthenticated ? (
<LoggedInJoinScreen />
) : (
<InviteOnlySignUpComponent inviteCode={inviteCode} />
)
) : (
<NoTokenScreen />
))}
</div>
</div>
)
}
const LoggedInJoinScreen = (props: any) => {
const session = useSession() as any;
const org = useOrg() as any;
const [isLoading, setIsLoading] = React.useState(true);
useEffect(() => {
if (session && org) {
setIsLoading(false);
}
const session = useSession() as any
const org = useOrg() as any
const [isLoading, setIsLoading] = React.useState(true)
useEffect(() => {
if (session && org) {
setIsLoading(false)
}
, [org, session]);
return (
<div className="flex flex-row items-center mx-auto">
<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'>
<span className='items-center'>Hi</span>
<span className='capitalize flex space-x-2 items-center'>
<UserAvatar rounded='rounded-xl' border='border-4' width={35} />
<span>{session.user.username},</span>
</span>
<span>join {org?.name} ?</span>
</p>
<button 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">
<UserPlus size={18} />
<p>Join </p>
</button>
</div>
</div>
)
}, [org, session])
return (
<div className="flex flex-row items-center mx-auto">
<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">
<span className="items-center">Hi</span>
<span className="capitalize flex space-x-2 items-center">
<UserAvatar rounded="rounded-xl" border="border-4" width={35} />
<span>{session.user.username},</span>
</span>
<span>join {org?.name} ?</span>
</p>
<button 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">
<UserPlus size={18} />
<p>Join </p>
</button>
</div>
</div>
)
}
const NoTokenScreen = (props: any) => {
const session = useSession() as any;
const org = useOrg() as any;
const router = useRouter();
const [isLoading, setIsLoading] = React.useState(true);
const [inviteCode, setInviteCode] = React.useState('');
const [messsage, setMessage] = React.useState('bruh');
const session = useSession() as any
const org = useOrg() as any
const router = useRouter()
const [isLoading, setIsLoading] = React.useState(true)
const [inviteCode, setInviteCode] = React.useState('')
const [messsage, setMessage] = React.useState('bruh')
const handleInviteCodeChange = (e: any) => {
setInviteCode(e.target.value);
const handleInviteCodeChange = (e: any) => {
setInviteCode(e.target.value)
}
const validateCode = async () => {
setIsLoading(true)
let res = await validateInviteCode(org?.id, inviteCode)
//wait for 1s
if (res.success) {
toast.success(
"Invite code is valid, you'll be redirected to the signup page in a few seconds"
)
setTimeout(() => {
router.push(`/signup?inviteCode=${inviteCode}`)
}, 2000)
} else {
toast.error('Invite code is invalid')
setIsLoading(false)
}
}
const validateCode = async () => {
setIsLoading(true);
let res = await validateInviteCode(org?.id, inviteCode);
//wait for 1s
if (res.success) {
toast.success("Invite code is valid, you'll be redirected to the signup page in a few seconds");
setTimeout(() => {
router.push(`/signup?inviteCode=${inviteCode}`);
}, 2000);
}
else {
toast.error("Invite code is invalid");
setIsLoading(false);
}
useEffect(() => {
if (session && org) {
setIsLoading(false)
}
}, [org, session])
useEffect(() => {
if (session && org) {
setIsLoading(false);
}
}
, [org, session]);
return (
<div className="flex flex-row items-center mx-auto">
<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">
<p className="flex space-x-2 text-lg font-medium text-red-800 items-center">
<MailWarning size={18} />
<span>An invite code is required to join {org?.name}</span>
</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" />
<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} />
<p>Submit </p>
</button>
</div>}
return (
<div className="flex flex-row items-center mx-auto">
<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">
<p className="flex space-x-2 text-lg font-medium text-red-800 items-center">
<MailWarning size={18} />
<span>An invite code is required to join {org?.name}</span>
</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"
/>
<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} />
<p>Submit </p>
</button>
</div>
)}
</div>
)
}
export default SignUpClient
export default SignUpClient

View file

@ -1,9 +1,9 @@
"use client";
import { motion } from "framer-motion";
import styled from "styled-components";
import learnhouseBigIcon from "public/learnhouse_bigicon.png";
import Image from "next/legacy/image";
import Link from "next/link";
'use client'
import { motion } from 'framer-motion'
import styled from 'styled-components'
import learnhouseBigIcon from 'public/learnhouse_bigicon.png'
import Image from 'next/legacy/image'
import Link from 'next/link'
export default function Home() {
return (
@ -12,14 +12,20 @@ export default function Home() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
type: "spring",
type: 'spring',
stiffness: 260,
damping: 70,
delay: 0.2,
}}
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>
<br />
<br />
@ -29,7 +35,7 @@ export default function Home() {
initial={{ opacity: 0, y: 20 }}
animate={{ opacity: 1, y: 0 }}
transition={{
type: "spring",
type: 'spring',
stiffness: 260,
damping: 70,
delay: 0.8,
@ -37,19 +43,19 @@ export default function Home() {
exit={{ opacity: 1 }}
>
<div>
<Link href={"/organizations"}>
<Link href={'/organizations'}>
<OrgsButton>See Organizations</OrgsButton>
</Link>
<br />
<br />
<Link href={"/login"}>
<Link href={'/login'}>
<OrgsButton>Login</OrgsButton>
</Link>
</div>
</motion.div>
</HomePage>
);
};
)
}
const OrgsButton = styled.button`
background: #151515;
@ -63,7 +69,7 @@ const OrgsButton = styled.button`
margin: 0 10px;
margin: auto;
cursor: pointer;
font-family: "DM Sans";
font-family: 'DM Sans';
font-weight: 500;
border-radius: 12px;
-webkit-transition: all 0.2s ease-in-out;
@ -71,7 +77,7 @@ const OrgsButton = styled.button`
&:hover {
background: #191919;
}
`;
`
const HomePage = styled.div`
display: flex;
@ -86,5 +92,4 @@ const HomePage = styled.div`
img {
width: 60px;
}
`;
`

View file

@ -2,44 +2,43 @@ import { useOrg } from '@components/Contexts/OrgContext'
import React from 'react'
interface UseGetAIFeatures {
feature: 'editor' | 'activity_ask' | 'course_ask' | 'global_ai_ask',
feature: 'editor' | 'activity_ask' | 'course_ask' | 'global_ai_ask'
}
function useGetAIFeatures(props: UseGetAIFeatures) {
const org = useOrg() as any
const [isEnabled, setisEnabled] = React.useState(false)
function checkAvailableAIFeaturesOnOrg(feature: string) {
const config = org?.config?.config?.AIConfig;
const config = org?.config?.config?.AIConfig
if (!config) {
console.log("AI or Organization config is not defined.");
return false;
console.log('AI or Organization config is not defined.')
return false
}
if (!config.enabled) {
console.log("AI is not enabled for this Organization.");
return false;
console.log('AI is not enabled for this Organization.')
return false
}
if (!config.features[feature]) {
console.log(`Feature ${feature} is not enabled for this Organization.`);
return false;
console.log(`Feature ${feature} is not enabled for this Organization.`)
return false
}
return true;
return true
}
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)
setisEnabled(isEnabledStatus)
}
}, [org])
return isEnabled
}
export default useGetAIFeatures
export default useGetAIFeatures

View file

@ -1,76 +1,74 @@
'use client';
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk';
'use client'
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk'
import React, { createContext, useContext, useReducer } from 'react'
export const AIChatBotContext = createContext(null) as any;
export const AIChatBotDispatchContext = createContext(null) as any;
export const AIChatBotContext = createContext(null) as any
export const AIChatBotDispatchContext = createContext(null) as any
export type AIChatBotStateTypes = {
messages: AIMessage[],
isModalOpen: boolean,
aichat_uuid: string,
isWaitingForResponse: boolean,
chatInputValue: string
error: AIError
messages: AIMessage[]
isModalOpen: boolean
aichat_uuid: string
isWaitingForResponse: boolean
chatInputValue: string
error: AIError
}
type AIError = {
isError: boolean
status: number
error_message: string
isError: boolean
status: number
error_message: string
}
function AIChatBotProvider({ children }: { children: React.ReactNode }) {
const [aiChatBotState, dispatchAIChatBot] = useReducer(aiChatBotReducer,
{
messages: [] as AIMessage[],
isModalOpen: false,
aichat_uuid: null,
isWaitingForResponse: false,
chatInputValue: '',
error: { isError: false, status: 0, error_message: ' ' } as AIError
}
);
return (
<AIChatBotContext.Provider value={aiChatBotState}>
<AIChatBotDispatchContext.Provider value={dispatchAIChatBot}>
{children}
</AIChatBotDispatchContext.Provider>
</AIChatBotContext.Provider>
)
const [aiChatBotState, dispatchAIChatBot] = useReducer(aiChatBotReducer, {
messages: [] as AIMessage[],
isModalOpen: false,
aichat_uuid: null,
isWaitingForResponse: false,
chatInputValue: '',
error: { isError: false, status: 0, error_message: ' ' } as AIError,
})
return (
<AIChatBotContext.Provider value={aiChatBotState}>
<AIChatBotDispatchContext.Provider value={dispatchAIChatBot}>
{children}
</AIChatBotDispatchContext.Provider>
</AIChatBotContext.Provider>
)
}
export default AIChatBotProvider
export function useAIChatBot() {
return useContext(AIChatBotContext);
return useContext(AIChatBotContext)
}
export function useAIChatBotDispatch() {
return useContext(AIChatBotDispatchContext);
return useContext(AIChatBotDispatchContext)
}
function aiChatBotReducer(state: any, action: any) {
switch (action.type) {
case 'setMessages':
return { ...state, messages: action.payload };
case 'addMessage':
return { ...state, messages: [...state.messages, action.payload] };
case 'setIsModalOpen':
return { ...state, isModalOpen: true };
case 'setIsModalClose':
return { ...state, isModalOpen: false };
case 'setAichat_uuid':
return { ...state, aichat_uuid: action.payload };
case 'setIsWaitingForResponse':
return { ...state, isWaitingForResponse: true };
case 'setIsNoLongerWaitingForResponse':
return { ...state, isWaitingForResponse: false };
case 'setChatInputValue':
return { ...state, chatInputValue: action.payload };
case 'setError':
return { ...state, error: action.payload };
switch (action.type) {
case 'setMessages':
return { ...state, messages: action.payload }
case 'addMessage':
return { ...state, messages: [...state.messages, action.payload] }
case 'setIsModalOpen':
return { ...state, isModalOpen: true }
case 'setIsModalClose':
return { ...state, isModalOpen: false }
case 'setAichat_uuid':
return { ...state, aichat_uuid: action.payload }
case 'setIsWaitingForResponse':
return { ...state, isWaitingForResponse: true }
case 'setIsNoLongerWaitingForResponse':
return { ...state, isWaitingForResponse: false }
case 'setChatInputValue':
return { ...state, chatInputValue: action.payload }
case 'setError':
return { ...state, error: action.payload }
default:
throw new Error(`Unhandled action type: ${action.type}`)
}
}
default:
throw new Error(`Unhandled action type: ${action.type}`)
}
}

View file

@ -1,92 +1,93 @@
'use client';
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk';
'use client'
import { AIMessage } from '@components/Objects/Activities/AI/AIActivityAsk'
import React, { createContext, useContext, useReducer } from 'react'
export const AIEditorContext = createContext(null) as any;
export const AIEditorDispatchContext = createContext(null) as any;
export const AIEditorContext = createContext(null) as any
export const AIEditorDispatchContext = createContext(null) as any
export type AIEditorStateTypes = {
messages: AIMessage[],
isModalOpen: boolean,
isFeedbackModalOpen: boolean,
aichat_uuid: string,
isWaitingForResponse: boolean,
chatInputValue: string,
selectedTool: 'Writer' | 'ContinueWriting' | 'MakeLonger' | 'GenerateQuiz' | 'Translate'
isUserInputEnabled: boolean
error: AIError
messages: AIMessage[]
isModalOpen: boolean
isFeedbackModalOpen: boolean
aichat_uuid: string
isWaitingForResponse: boolean
chatInputValue: string
selectedTool:
| 'Writer'
| 'ContinueWriting'
| 'MakeLonger'
| 'GenerateQuiz'
| 'Translate'
isUserInputEnabled: boolean
error: AIError
}
type AIError = {
isError: boolean
status: number
error_message: string
isError: boolean
status: number
error_message: string
}
function AIEditorProvider({ children }: { children: React.ReactNode }) {
const [aIEditorState, dispatchAIEditor] = useReducer(aIEditorReducer,
{
messages: [] as AIMessage[],
isModalOpen: false,
isFeedbackModalOpen: false,
aichat_uuid: null,
isWaitingForResponse: false,
chatInputValue: '',
selectedTool: 'Writer',
isUserInputEnabled: true,
error: { isError: false, status: 0, error_message: ' ' } as AIError
}
);
return (
<AIEditorContext.Provider value={aIEditorState}>
<AIEditorDispatchContext.Provider value={dispatchAIEditor}>
{children}
</AIEditorDispatchContext.Provider>
</AIEditorContext.Provider>
)
const [aIEditorState, dispatchAIEditor] = useReducer(aIEditorReducer, {
messages: [] as AIMessage[],
isModalOpen: false,
isFeedbackModalOpen: false,
aichat_uuid: null,
isWaitingForResponse: false,
chatInputValue: '',
selectedTool: 'Writer',
isUserInputEnabled: true,
error: { isError: false, status: 0, error_message: ' ' } as AIError,
})
return (
<AIEditorContext.Provider value={aIEditorState}>
<AIEditorDispatchContext.Provider value={dispatchAIEditor}>
{children}
</AIEditorDispatchContext.Provider>
</AIEditorContext.Provider>
)
}
export default AIEditorProvider
export function useAIEditor() {
return useContext(AIEditorContext);
return useContext(AIEditorContext)
}
export function useAIEditorDispatch() {
return useContext(AIEditorDispatchContext);
return useContext(AIEditorDispatchContext)
}
function aIEditorReducer(state: any, action: any) {
switch (action.type) {
case 'setMessages':
return { ...state, messages: action.payload };
case 'addMessage':
return { ...state, messages: [...state.messages, action.payload] };
case 'setIsModalOpen':
return { ...state, isModalOpen: true };
case 'setIsModalClose':
return { ...state, isModalOpen: false };
case 'setAichat_uuid':
return { ...state, aichat_uuid: action.payload };
case 'setIsWaitingForResponse':
return { ...state, isWaitingForResponse: true };
case 'setIsNoLongerWaitingForResponse':
return { ...state, isWaitingForResponse: false };
case 'setChatInputValue':
return { ...state, chatInputValue: action.payload };
case 'setSelectedTool':
return { ...state, selectedTool: action.payload };
case 'setIsFeedbackModalOpen':
return { ...state, isFeedbackModalOpen: true };
case 'setIsFeedbackModalClose':
return { ...state, isFeedbackModalOpen: false };
case 'setIsUserInputEnabled':
return { ...state, isUserInputEnabled: action.payload };
case 'setError':
return { ...state, error: action.payload };
switch (action.type) {
case 'setMessages':
return { ...state, messages: action.payload }
case 'addMessage':
return { ...state, messages: [...state.messages, action.payload] }
case 'setIsModalOpen':
return { ...state, isModalOpen: true }
case 'setIsModalClose':
return { ...state, isModalOpen: false }
case 'setAichat_uuid':
return { ...state, aichat_uuid: action.payload }
case 'setIsWaitingForResponse':
return { ...state, isWaitingForResponse: true }
case 'setIsNoLongerWaitingForResponse':
return { ...state, isWaitingForResponse: false }
case 'setChatInputValue':
return { ...state, chatInputValue: action.payload }
case 'setSelectedTool':
return { ...state, selectedTool: action.payload }
case 'setIsFeedbackModalOpen':
return { ...state, isFeedbackModalOpen: true }
case 'setIsFeedbackModalClose':
return { ...state, isFeedbackModalOpen: false }
case 'setIsUserInputEnabled':
return { ...state, isUserInputEnabled: action.payload }
case 'setError':
return { ...state, error: action.payload }
default:
throw new Error(`Unhandled action type: ${action.type}`)
}
}
default:
throw new Error(`Unhandled action type: ${action.type}`)
}
}

View file

@ -1,63 +1,70 @@
'use client';
import PageLoading from '@components/Objects/Loaders/PageLoading';
import { getAPIUrl } from '@services/config/config';
import { swrFetcher } from '@services/utils/ts/requests';
'use client'
import PageLoading from '@components/Objects/Loaders/PageLoading'
import { getAPIUrl } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests'
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 CourseDispatchContext = createContext(null) as any;
export const CourseContext = createContext(null) as any
export const CourseDispatchContext = createContext(null) as any
export function CourseProvider({ children, courseuuid }: { children: React.ReactNode, courseuuid: string }) {
const { data: courseStructureData } = useSWR(`${getAPIUrl()}courses/${courseuuid}/meta`, swrFetcher);
const [courseStructure, dispatchCourseStructure] = useReducer(courseReducer,
{
courseStructure: courseStructureData ? courseStructureData : {},
courseOrder: {},
isSaved: true
}
);
export function CourseProvider({
children,
courseuuid,
}: {
children: React.ReactNode
courseuuid: string
}) {
const { data: courseStructureData } = useSWR(
`${getAPIUrl()}courses/${courseuuid}/meta`,
swrFetcher
)
const [courseStructure, dispatchCourseStructure] = useReducer(courseReducer, {
courseStructure: courseStructureData ? courseStructureData : {},
courseOrder: {},
isSaved: true,
})
// When courseStructureData is loaded, update the state
useEffect(() => {
if (courseStructureData) {
dispatchCourseStructure({
type: 'setCourseStructure',
payload: courseStructureData,
})
}
}, [courseStructureData])
// When courseStructureData is loaded, update the state
useEffect(() => {
if (courseStructureData) {
dispatchCourseStructure({ type: 'setCourseStructure', payload: courseStructureData });
}
}, [courseStructureData]);
if (!courseStructureData) return <PageLoading></PageLoading>
if (!courseStructureData) return <PageLoading></PageLoading>
return (
<CourseContext.Provider value={courseStructure}>
<CourseDispatchContext.Provider value={dispatchCourseStructure}>
{children}
</CourseDispatchContext.Provider>
</CourseContext.Provider>
)
return (
<CourseContext.Provider value={courseStructure}>
<CourseDispatchContext.Provider value={dispatchCourseStructure}>
{children}
</CourseDispatchContext.Provider>
</CourseContext.Provider>
)
}
export function useCourse() {
return useContext(CourseContext);
return useContext(CourseContext)
}
export function useCourseDispatch() {
return useContext(CourseDispatchContext);
return useContext(CourseDispatchContext)
}
function courseReducer(state: any, action: any) {
switch (action.type) {
case 'setCourseStructure':
return { ...state, courseStructure: action.payload };
case 'setCourseOrder':
return { ...state, courseOrder: action.payload };
case 'setIsSaved':
return { ...state, isSaved: true };
case 'setIsNotSaved':
return { ...state, isSaved: false };
default:
throw new Error(`Unhandled action type: ${action.type}`);
}
}
switch (action.type) {
case 'setCourseStructure':
return { ...state, courseStructure: action.payload }
case 'setCourseOrder':
return { ...state, courseOrder: action.payload }
case 'setIsSaved':
return { ...state, isSaved: true }
case 'setIsNotSaved':
return { ...state, isSaved: false }
default:
throw new Error(`Unhandled action type: ${action.type}`)
}
}

View file

@ -1,32 +1,30 @@
'use client';
'use client'
import React, { useState } from 'react'
export const EditorProviderContext = React.createContext(null) as any;
export const EditorProviderContext = React.createContext(null) as any
type EditorProviderProps = {
children: React.ReactNode
options: EditorProviderState
children: React.ReactNode
options: EditorProviderState
}
type EditorProviderState = {
isEditable: boolean
isEditable: boolean
}
function EditorOptionsProvider({ children, options }: EditorProviderProps) {
const [editorOptions, setEditorOptions] = useState<EditorProviderState>(options);
const [editorOptions, setEditorOptions] =
useState<EditorProviderState>(options)
return (
<EditorProviderContext.Provider value={editorOptions}>
{children}
</EditorProviderContext.Provider>
)
return (
<EditorProviderContext.Provider value={editorOptions}>
{children}
</EditorProviderContext.Provider>
)
}
export default EditorOptionsProvider
export function useEditorProvider() {
return React.useContext(EditorProviderContext);
return React.useContext(EditorProviderContext)
}

View file

@ -1,25 +1,25 @@
'use client';
import { getAPIUrl } from '@services/config/config';
import { swrFetcher } from '@services/utils/ts/requests';
'use client'
import { getAPIUrl } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests'
import React, { useContext, useEffect } from 'react'
import useSWR from 'swr';
import { createContext } from 'react';
import useSWR from 'swr'
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 }) {
const { data: org } = useSWR(`${getAPIUrl()}orgs/slug/${orgslug}`, swrFetcher);
useEffect(() => {
export function OrgProvider({
children,
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() {
return useContext(OrgContext);
return useContext(OrgContext)
}

View file

@ -1,59 +1,77 @@
'use client';
import { getNewAccessTokenUsingRefreshToken, getUserSession } from '@services/auth/auth';
'use client'
import {
getNewAccessTokenUsingRefreshToken,
getUserSession,
} from '@services/auth/auth'
import React, { useContext, createContext, useEffect } from 'react'
export const SessionContext = createContext({}) as any;
export const SessionContext = createContext({}) as any
type Session = {
access_token: string;
user: any;
roles: any;
isLoading: boolean;
isAuthenticated: boolean;
access_token: string
user: any
roles: any
isLoading: boolean
isAuthenticated: boolean
}
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() {
let data = await getNewAccessTokenUsingRefreshToken();
return data.access_token;
async function getNewAccessTokenUsingRefreshTokenUI() {
let data = await getNewAccessTokenUsingRefreshToken()
return data.access_token
}
async function checkSession() {
// Get new access token using refresh token
const access_token = await getNewAccessTokenUsingRefreshTokenUI()
if (access_token) {
// Get user session info
const user_session = await getUserSession(access_token)
// Set session
setSession({
access_token: access_token,
user: user_session.user,
roles: user_session.roles,
isLoading: false,
isAuthenticated: true,
})
}
async function checkSession() {
// Get new access token using refresh token
const access_token = await getNewAccessTokenUsingRefreshTokenUI();
if (access_token) {
// Get user session info
const user_session = await getUserSession(access_token);
// Set session
setSession({ access_token: access_token, user: user_session.user, roles: user_session.roles, isLoading: false, isAuthenticated: true });
}
if (!access_token) {
setSession({ access_token: "", user: {}, roles: {}, isLoading: false, isAuthenticated: false });
}
if (!access_token) {
setSession({
access_token: '',
user: {},
roles: {},
isLoading: false,
isAuthenticated: false,
})
}
}
useEffect(() => {
// Check session
checkSession()
}, [])
useEffect(() => {
// Check session
checkSession();
}, [])
return (
<SessionContext.Provider value={session}>
{children}
</SessionContext.Provider>
)
return (
<SessionContext.Provider value={session}>
{children}
</SessionContext.Provider>
)
}
export function useSession() {
return useContext(SessionContext);
return useContext(SessionContext)
}
export default SessionProvider
export default SessionProvider

View file

@ -1,166 +1,208 @@
import FormLayout, { FormField, FormLabelAndMessage, Input, Textarea } from '@components/StyledElements/Form/Form';
import { useFormik } from 'formik';
import FormLayout, {
FormField,
FormLabelAndMessage,
Input,
Textarea,
} from '@components/StyledElements/Form/Form'
import { useFormik } from 'formik'
import { AlertTriangle } from 'lucide-react'
import * as Switch from '@radix-ui/react-switch';
import * as Form from '@radix-ui/react-form';
import * as Switch from '@radix-ui/react-switch'
import * as Form from '@radix-ui/react-form'
import React from 'react'
import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext';
import ThumbnailUpdate from './ThumbnailUpdate';
import { useCourse, useCourseDispatch } from '../../../Contexts/CourseContext'
import ThumbnailUpdate from './ThumbnailUpdate'
type EditCourseStructureProps = {
orgslug: string,
course_uuid?: string,
orgslug: string
course_uuid?: string
}
const validate = (values: any) => {
const errors: any = {};
const errors: any = {}
if (!values.name) {
errors.name = 'Required';
}
if (!values.name) {
errors.name = 'Required'
}
if (values.name.length > 100) {
errors.name = 'Must be 100 characters or less';
}
if (values.name.length > 100) {
errors.name = 'Must be 100 characters or less'
}
if (!values.description) {
errors.description = 'Required'
}
if (!values.description) {
errors.description = 'Required';
if (values.description.length > 1000) {
errors.description = 'Must be 1000 characters or less'
}
}
if (!values.learnings) {
errors.learnings = 'Required'
}
if (values.description.length > 1000) {
errors.description = 'Must be 1000 characters or less';
}
if (!values.learnings) {
errors.learnings = 'Required';
}
return errors;
};
function EditCourseGeneral(props: EditCourseStructureProps) {
const [error, setError] = React.useState('');
const course = useCourse() as any;
const dispatchCourse = useCourseDispatch() as any;
const courseStructure = course.courseStructure;
const formik = useFormik({
initialValues: {
name: String(courseStructure.name),
description: String(courseStructure.description),
about: String(courseStructure.about),
learnings: String(courseStructure.learnings),
tags: String(courseStructure.tags),
public: String(courseStructure.public),
},
validate,
onSubmit: async values => {
},
enableReinitialize: true,
});
React.useEffect(() => {
// This code will run whenever form values are updated
if (formik.values !== formik.initialValues) {
dispatchCourse({ type: 'setIsNotSaved' });
const updatedCourse = {
...courseStructure,
name: formik.values.name,
description: formik.values.description,
about: formik.values.about,
learnings: formik.values.learnings,
tags: formik.values.tags,
public: formik.values.public,
}
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse });
}
}, [course, formik.values, formik.initialValues]);
return (
<div> <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 && (
<div className="editcourse-form">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage label='Name' message={formik.errors.name} />
<Form.Control asChild>
<Input style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.name} type="text" required />
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage label='Description' message={formik.errors.description} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.description} required />
</Form.Control>
</FormField>
<FormField name="about">
<FormLabelAndMessage label='About' message={formik.errors.about} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.about} required />
</Form.Control>
</FormField>
<FormField name="learnings">
<FormLabelAndMessage label='Learnings' message={formik.errors.learnings} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.learnings} required />
</Form.Control>
</FormField>
<FormField name="tags">
<FormLabelAndMessage label='Tags' message={formik.errors.tags} />
<Form.Control asChild>
<Textarea style={{ backgroundColor: "white" }} onChange={formik.handleChange} value={formik.values.tags} required />
</Form.Control>
</FormField>
<FormField name="thumbnail">
<FormLabelAndMessage label='Thumbnail' />
<Form.Control asChild>
<ThumbnailUpdate />
</Form.Control>
</FormField>
<FormField className="flex items-center h-10" name="public">
<div className='flex my-auto items-center'>
<label className="text-black text-[15px] leading-none pr-[15px]" htmlFor="public-course">
Public Course
</label>
<Switch.Root
className="w-[42px] h-[25px] bg-neutral-200 rounded-full relative data-[state=checked]:bg-neutral-500 outline-none cursor-default"
id="public-course"
onCheckedChange={checked => formik.setFieldValue('public', checked)}
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.Root>
</div>
</FormField>
</FormLayout>
</div>
)}
</div>
</div>
)
return errors
}
export default EditCourseGeneral
function EditCourseGeneral(props: EditCourseStructureProps) {
const [error, setError] = React.useState('')
const course = useCourse() as any
const dispatchCourse = useCourseDispatch() as any
const courseStructure = course.courseStructure
const formik = useFormik({
initialValues: {
name: String(courseStructure.name),
description: String(courseStructure.description),
about: String(courseStructure.about),
learnings: String(courseStructure.learnings),
tags: String(courseStructure.tags),
public: String(courseStructure.public),
},
validate,
onSubmit: async (values) => {},
enableReinitialize: true,
})
React.useEffect(() => {
// This code will run whenever form values are updated
if (formik.values !== formik.initialValues) {
dispatchCourse({ type: 'setIsNotSaved' })
const updatedCourse = {
...courseStructure,
name: formik.values.name,
description: formik.values.description,
about: formik.values.about,
learnings: formik.values.learnings,
tags: formik.values.tags,
public: formik.values.public,
}
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse })
}
}, [course, formik.values, formik.initialValues])
return (
<div>
{' '}
<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 && (
<div className="editcourse-form">
{error && (
<div className="flex justify-center bg-red-200 rounded-md text-red-950 space-x-2 items-center p-4 transition-all shadow-sm">
<AlertTriangle size={18} />
<div className="font-bold text-sm">{error}</div>
</div>
)}
<FormLayout onSubmit={formik.handleSubmit}>
<FormField name="name">
<FormLabelAndMessage
label="Name"
message={formik.errors.name}
/>
<Form.Control asChild>
<Input
style={{ backgroundColor: 'white' }}
onChange={formik.handleChange}
value={formik.values.name}
type="text"
required
/>
</Form.Control>
</FormField>
<FormField name="description">
<FormLabelAndMessage
label="Description"
message={formik.errors.description}
/>
<Form.Control asChild>
<Textarea
style={{ backgroundColor: 'white' }}
onChange={formik.handleChange}
value={formik.values.description}
required
/>
</Form.Control>
</FormField>
<FormField name="about">
<FormLabelAndMessage
label="About"
message={formik.errors.about}
/>
<Form.Control asChild>
<Textarea
style={{ backgroundColor: 'white' }}
onChange={formik.handleChange}
value={formik.values.about}
required
/>
</Form.Control>
</FormField>
<FormField name="learnings">
<FormLabelAndMessage
label="Learnings"
message={formik.errors.learnings}
/>
<Form.Control asChild>
<Textarea
style={{ backgroundColor: 'white' }}
onChange={formik.handleChange}
value={formik.values.learnings}
required
/>
</Form.Control>
</FormField>
<FormField name="tags">
<FormLabelAndMessage
label="Tags"
message={formik.errors.tags}
/>
<Form.Control asChild>
<Textarea
style={{ backgroundColor: 'white' }}
onChange={formik.handleChange}
value={formik.values.tags}
required
/>
</Form.Control>
</FormField>
<FormField name="thumbnail">
<FormLabelAndMessage label="Thumbnail" />
<Form.Control asChild>
<ThumbnailUpdate />
</Form.Control>
</FormField>
<FormField className="flex items-center h-10" name="public">
<div className="flex my-auto items-center">
<label
className="text-black text-[15px] leading-none pr-[15px]"
htmlFor="public-course"
>
Public Course
</label>
<Switch.Root
className="w-[42px] h-[25px] bg-neutral-200 rounded-full relative data-[state=checked]:bg-neutral-500 outline-none cursor-default"
id="public-course"
onCheckedChange={(checked) =>
formik.setFieldValue('public', checked)
}
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.Root>
</div>
</FormField>
</FormLayout>
</div>
)}
</div>
</div>
)
}
export default EditCourseGeneral

View file

@ -1,79 +1,100 @@
import { useCourse } from '@components/Contexts/CourseContext';
import { useOrg } from '@components/Contexts/OrgContext';
import { getAPIUrl } from '@services/config/config';
import { updateCourseThumbnail } from '@services/courses/courses';
import { getCourseThumbnailMediaDirectory } from '@services/media/media';
import { ArrowBigUpDash, UploadCloud } from 'lucide-react';
import { useCourse } from '@components/Contexts/CourseContext'
import { useOrg } from '@components/Contexts/OrgContext'
import { getAPIUrl } from '@services/config/config'
import { updateCourseThumbnail } from '@services/courses/courses'
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { ArrowBigUpDash, UploadCloud } from 'lucide-react'
import React from 'react'
import { mutate } from 'swr';
import { mutate } from 'swr'
function ThumbnailUpdate() {
const course = useCourse() as any;
const org = useOrg() as any;
const [localThumbnail, setLocalThumbnail] = React.useState(null) as any;
const [isLoading, setIsLoading] = React.useState(false) as any;
const [error, setError] = React.useState('') as any;
const course = useCourse() as any
const org = useOrg() as any
const [localThumbnail, setLocalThumbnail] = React.useState(null) as any
const [isLoading, setIsLoading] = React.useState(false) as any
const [error, setError] = React.useState('') as any
const handleFileChange = async (event: any) => {
const file = event.target.files[0];
setLocalThumbnail(file);
setIsLoading(true);
const res = await updateCourseThumbnail(course.courseStructure.course_uuid, file)
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
// wait for 1 second to show loading animation
await new Promise(r => setTimeout(r, 1500));
if (res.success === false) {
setError(res.HTTPmessage);
} else {
setIsLoading(false);
setError('');
}
};
return (
<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'>
<div className='flex flex-col justify-center items-center'>
{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="text-sm font-semibold">{error}</div>
</div>
)}
{localThumbnail ? (
<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' />
)}
</div>
{isLoading ? (<div className='flex justify-center items-center'>
<input type="file" id="fileInput" 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>
</div>
</div>) : (
<div className='flex justify-center items-center'>
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
<button
className='font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex'
onClick={() => document.getElementById('fileInput')?.click()}
>
<UploadCloud size={16} className='mr-2' />
<span>Change Thumbnail</span>
</button>
</div>
)}
</div>
</div>
</div>
const handleFileChange = async (event: any) => {
const file = event.target.files[0]
setLocalThumbnail(file)
setIsLoading(true)
const res = await updateCourseThumbnail(
course.courseStructure.course_uuid,
file
)
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
// wait for 1 second to show loading animation
await new Promise((r) => setTimeout(r, 1500))
if (res.success === false) {
setError(res.HTTPmessage)
} else {
setIsLoading(false)
setError('')
}
}
return (
<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">
<div className="flex flex-col justify-center items-center">
{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="text-sm font-semibold">{error}</div>
</div>
)}
{localThumbnail ? (
<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"
/>
)}
</div>
{isLoading ? (
<div className="flex justify-center items-center">
<input
type="file"
id="fileInput"
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>
</div>
</div>
) : (
<div className="flex justify-center items-center">
<input
type="file"
id="fileInput"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<button
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 mt-6 flex"
onClick={() => document.getElementById('fileInput')?.click()}
>
<UploadCloud size={16} className="mr-2" />
<span>Change Thumbnail</span>
</button>
</div>
)}
</div>
</div>
</div>
)
}
export default ThumbnailUpdate
export default ThumbnailUpdate

View file

@ -1,93 +1,116 @@
import { useCourse } from '@components/Contexts/CourseContext';
import NewActivityModal from '@components/Objects/Modals/Activities/Create/NewActivity';
import Modal from '@components/StyledElements/Modal/Modal';
import { getAPIUrl } from '@services/config/config';
import { createActivity, createExternalVideoActivity, createFileActivity } from '@services/courses/activities';
import { getOrganizationContextInfoWithoutCredentials } from '@services/organizations/orgs';
import { revalidateTags } from '@services/utils/ts/requests';
import { useCourse } from '@components/Contexts/CourseContext'
import NewActivityModal from '@components/Objects/Modals/Activities/Create/NewActivity'
import Modal from '@components/StyledElements/Modal/Modal'
import { getAPIUrl } from '@services/config/config'
import {
createActivity,
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 { useRouter } from 'next/navigation';
import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import { mutate } from 'swr';
import { mutate } from 'swr'
type NewActivityButtonProps = {
chapterId: string,
orgslug: string
chapterId: string
orgslug: string
}
function NewActivityButton(props: NewActivityButtonProps) {
const [newActivityModal, setNewActivityModal] = React.useState(false);
const router = useRouter();
const course = useCourse() as any;
const [newActivityModal, setNewActivityModal] = React.useState(false)
const router = useRouter()
const course = useCourse() as any
const openNewActivityModal = async (chapterId: any) => {
setNewActivityModal(true);
};
const openNewActivityModal = async (chapterId: any) => {
setNewActivityModal(true)
}
const closeNewActivityModal = async () => {
setNewActivityModal(false);
};
const closeNewActivityModal = async () => {
setNewActivityModal(false)
}
// Submit new activity
const submitActivity = async (activity: any) => {
let org = await getOrganizationContextInfoWithoutCredentials(props.orgslug, { revalidate: 1800 });
await createActivity(activity, props.chapterId, org.org_id);
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
setNewActivityModal(false);
await revalidateTags(['courses'], props.orgslug);
router.refresh();
};
// Submit File Upload
const submitFileActivity = async (file: any, type: any, activity: any, chapterId: string) => {
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
const submitExternalVideo = async (external_video_data: any, activity: any, chapterId: string) => {
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(() => { }
, [course])
return (
<div className='flex justify-center'>
<Modal
isDialogOpen={newActivityModal}
onOpenChange={setNewActivityModal}
minHeight="no-min"
addDefCloseButton={false}
dialogContent={<NewActivityModal
closeModal={closeNewActivityModal}
submitFileActivity={submitFileActivity}
submitExternalVideo={submitExternalVideo}
submitActivity={submitActivity}
chapterId={props.chapterId}
course={course}
></NewActivityModal>}
dialogTitle="Create Activity"
dialogDescription="Choose between types of activities to add to the course"
/>
<div onClick={() => {
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">
<Layers className="" size={17} />
<div className="text-sm mx-auto my-auto items-center font-bold">Add Activity</div>
</div>
</div>
// Submit new activity
const submitActivity = async (activity: any) => {
let org = await getOrganizationContextInfoWithoutCredentials(
props.orgslug,
{ revalidate: 1800 }
)
await createActivity(activity, props.chapterId, org.org_id)
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
setNewActivityModal(false)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
}
// Submit File Upload
const submitFileActivity = async (
file: any,
type: any,
activity: any,
chapterId: string
) => {
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
const submitExternalVideo = async (
external_video_data: any,
activity: any,
chapterId: string
) => {
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(() => {}, [course])
return (
<div className="flex justify-center">
<Modal
isDialogOpen={newActivityModal}
onOpenChange={setNewActivityModal}
minHeight="no-min"
addDefCloseButton={false}
dialogContent={
<NewActivityModal
closeModal={closeNewActivityModal}
submitFileActivity={submitFileActivity}
submitExternalVideo={submitExternalVideo}
submitActivity={submitActivity}
chapterId={props.chapterId}
course={course}
></NewActivityModal>
}
dialogTitle="Create Activity"
dialogDescription="Choose between types of activities to add to the course"
/>
<div
onClick={() => {
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"
>
<Layers className="" size={17} />
<div className="text-sm mx-auto my-auto items-center font-bold">
Add Activity
</div>
</div>
</div>
)
}
export default NewActivityButton
export default NewActivityButton

View file

@ -2,7 +2,16 @@ import ConfirmationModal from '@components/StyledElements/ConfirmationModal/Conf
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
import { deleteActivity, updateActivity } from '@services/courses/activities'
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 { useRouter } from 'next/navigation'
import React from 'react'
@ -10,124 +19,210 @@ import { Draggable } from 'react-beautiful-dnd'
import { mutate } from 'swr'
type ActivitiyElementProps = {
orgslug: string,
activity: any,
activityIndex: any,
course_uuid: string
orgslug: string
activity: any
activityIndex: any
course_uuid: string
}
interface ModifiedActivityInterface {
activityId: string;
activityName: string;
activityId: string
activityName: string
}
function ActivityElement(props: ActivitiyElementProps) {
const router = useRouter();
const [modifiedActivity, setModifiedActivity] = React.useState<ModifiedActivityInterface | undefined>(undefined);
const [selectedActivity, setSelectedActivity] = React.useState<string | undefined>(undefined);
const activityUUID = props.activity.activity_uuid;
const router = useRouter()
const [modifiedActivity, setModifiedActivity] = React.useState<
ModifiedActivityInterface | undefined
>(undefined)
const [selectedActivity, setSelectedActivity] = React.useState<
string | undefined
>(undefined)
const activityUUID = props.activity.activity_uuid
async function deleteActivityUI() {
await deleteActivity(props.activity.activity_uuid);
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
await revalidateTags(['courses'], props.orgslug);
router.refresh();
async function deleteActivityUI() {
await deleteActivity(props.activity.activity_uuid)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
}
async function updateActivityName(activityId: string) {
if (
modifiedActivity?.activityId === activityId &&
selectedActivity !== undefined
) {
setSelectedActivity(undefined)
let modifiedActivityCopy = {
name: modifiedActivity.activityName,
description: '',
type: props.activity.type,
content: props.activity.content,
}
await updateActivity(modifiedActivityCopy, activityUUID)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
}
}
async function updateActivityName(activityId: string) {
if ((modifiedActivity?.activityId === activityId) && selectedActivity !== undefined) {
setSelectedActivity(undefined);
let modifiedActivityCopy = {
name: modifiedActivity.activityName,
description: '',
type: props.activity.type,
content: props.activity.content,
}
return (
<Draggable
key={props.activity.activity_uuid}
draggableId={props.activity.activity_uuid}
index={props.activityIndex}
>
{(provided, snapshot) => (
<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"
key={props.activity.id}
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
{/* Activity Type Icon */}
<ActivityTypeIndicator activityType={props.activity.activity_type} />
await updateActivity(modifiedActivityCopy, activityUUID)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
await revalidateTags(['courses'], props.orgslug)
router.refresh();
}
}
return (
<Draggable key={props.activity.activity_uuid} draggableId={props.activity.activity_uuid} index={props.activityIndex}>
{(provided, snapshot) => (
<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"
key={props.activity.id}
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
{/* Centered Activity Name */}
<div className="grow items-center space-x-2 flex mx-auto justify-center">
{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">
<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,
})
}
/>
<button
onClick={() => updateActivityName(props.activity.id)}
className="bg-transparent text-neutral-700 hover:cursor-pointer hover:text-neutral-900"
>
{/* Activity Type Icon */}
<ActivityTypeIndicator activityType={props.activity.activity_type} />
{/* Centered Activity Name */}
<div className="grow items-center space-x-2 flex mx-auto justify-center">
{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">
<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 })} />
<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>
</div>) : (<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>
{/* Edit and View Button */}
<div className="flex flex-row space-x-2">
{props.activity.activity_type === "TYPE_DYNAMIC" && <>
<Link
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"
rel="noopener noreferrer">
<div className="text-sky-100 font-bold text-xs" >Edit </div>
</Link>
</>}
<Link
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"
rel="noopener noreferrer">
<Eye strokeWidth={2} size={15} className="text-gray-600" />
</Link>
</div>
{/* Delete Button */}
<div className="flex flex-row pr-3 space-x-1 items-center">
<MoreVertical size={15} className="text-gray-300" />
<ConfirmationModal
confirmationMessage="Are you sure you want to delete this activity ?"
confirmationButtonText="Delete Activity"
dialogTitle={"Delete " + props.activity.name + " ?"}
dialogTrigger={
<div
className=" hover:cursor-pointer p-1 px-5 bg-red-600 rounded-md"
rel="noopener noreferrer">
<X size={15} className="text-rose-200 font-bold" />
</div>}
functionToExecute={() => deleteActivityUI()}
status='warning'
></ConfirmationModal></div>
</div>
<Save
size={11}
onClick={() => updateActivityName(props.activity.id)}
/>
</button>
</div>
) : (
<p className="first-letter:uppercase"> {props.activity.name} </p>
)}
</Draggable>
)
<Pencil
onClick={() => setSelectedActivity(props.activity.id)}
size={12}
className="text-neutral-400 hover:cursor-pointer"
/>
</div>
{/* Edit and View Button */}
<div className="flex flex-row space-x-2">
{props.activity.activity_type === 'TYPE_DYNAMIC' && (
<>
<Link
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"
rel="noopener noreferrer"
>
<div className="text-sky-100 font-bold text-xs">Edit </div>
</Link>
</>
)}
<Link
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"
rel="noopener noreferrer"
>
<Eye strokeWidth={2} size={15} className="text-gray-600" />
</Link>
</div>
{/* Delete Button */}
<div className="flex flex-row pr-3 space-x-1 items-center">
<MoreVertical size={15} className="text-gray-300" />
<ConfirmationModal
confirmationMessage="Are you sure you want to delete this activity ?"
confirmationButtonText="Delete Activity"
dialogTitle={'Delete ' + props.activity.name + ' ?'}
dialogTrigger={
<div
className=" hover:cursor-pointer p-1 px-5 bg-red-600 rounded-md"
rel="noopener noreferrer"
>
<X size={15} className="text-rose-200 font-bold" />
</div>
}
functionToExecute={() => deleteActivityUI()}
status="warning"
></ConfirmationModal>
</div>
</div>
)}
</Draggable>
)
}
const ActivityTypeIndicator = (props: { activityType: string }) => {
return (
<div className="px-3 text-gray-300 space-x-1 w-28" >
{props.activityType === "TYPE_VIDEO" && <>
<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></>}
{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>
)
return (
<div className="px-3 text-gray-300 space-x-1 w-28">
{props.activityType === 'TYPE_VIDEO' && (
<>
<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>
</>
)}
{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>
)
}
export default ActivityElement
export default ActivityElement

View file

@ -1,129 +1,185 @@
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
import { Hexagon, MoreHorizontal, MoreVertical, Pencil, Save, X } from 'lucide-react';
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
import {
Hexagon,
MoreHorizontal,
MoreVertical,
Pencil,
Save,
X,
} from 'lucide-react'
import React from 'react'
import { Draggable, Droppable } from 'react-beautiful-dnd';
import ActivityElement from './ActivityElement';
import NewActivityButton from '../Buttons/NewActivityButton';
import { deleteChapter, updateChapter } from '@services/courses/chapters';
import { revalidateTags } from '@services/utils/ts/requests';
import { useRouter } from 'next/navigation';
import { getAPIUrl } from '@services/config/config';
import { mutate } from 'swr';
import { Draggable, Droppable } from 'react-beautiful-dnd'
import ActivityElement from './ActivityElement'
import NewActivityButton from '../Buttons/NewActivityButton'
import { deleteChapter, updateChapter } from '@services/courses/chapters'
import { revalidateTags } from '@services/utils/ts/requests'
import { useRouter } from 'next/navigation'
import { getAPIUrl } from '@services/config/config'
import { mutate } from 'swr'
type ChapterElementProps = {
chapter: any,
chapterIndex: number,
orgslug: string
course_uuid: string
chapter: any
chapterIndex: number
orgslug: string
course_uuid: string
}
interface ModifiedChapterInterface {
chapterId: string;
chapterName: string;
chapterId: string
chapterName: string
}
function ChapterElement(props: ChapterElementProps) {
const activities = props.chapter.activities || [];
const [modifiedChapter, setModifiedChapter] = React.useState<ModifiedChapterInterface | undefined>(undefined);
const [selectedChapter, setSelectedChapter] = React.useState<string | undefined>(undefined);
const activities = props.chapter.activities || []
const [modifiedChapter, setModifiedChapter] = React.useState<
ModifiedChapterInterface | undefined
>(undefined)
const [selectedChapter, setSelectedChapter] = React.useState<
string | undefined
>(undefined)
const router = useRouter();
const router = useRouter()
const deleteChapterUI = async () => {
await deleteChapter(props.chapter.id);
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
await revalidateTags(['courses'], props.orgslug);
router.refresh();
};
const deleteChapterUI = async () => {
await deleteChapter(props.chapter.id)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
}
async function updateChapterName(chapterId: string) {
if (modifiedChapter?.chapterId === chapterId) {
setSelectedChapter(undefined);
let modifiedChapterCopy = {
name: modifiedChapter.chapterName,
}
await updateChapter(chapterId, modifiedChapterCopy)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`);
await revalidateTags(['courses'], props.orgslug)
router.refresh();
}
async function updateChapterName(chapterId: string) {
if (modifiedChapter?.chapterId === chapterId) {
setSelectedChapter(undefined)
let modifiedChapterCopy = {
name: modifiedChapter.chapterName,
}
await updateChapter(chapterId, modifiedChapterCopy)
mutate(`${getAPIUrl()}courses/${props.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
}
}
return (
<Draggable
key={props.chapter.chapter_uuid}
draggableId={props.chapter.chapter_uuid}
index={props.chapterIndex}
return (
<Draggable
key={props.chapter.chapter_uuid}
draggableId={props.chapter.chapter_uuid}
index={props.chapterIndex}
>
{(provided, snapshot) => (
<div
className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 pt-6"
key={props.chapter.chapter_uuid}
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
>
{(provided, snapshot) => (
<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="bg-neutral-100 rounded-md p-2">
<Hexagon
strokeWidth={3}
size={16}
className="text-neutral-600 "
/>
</div>
<div className="flex space-x-2 items-center">
{selectedChapter === props.chapter.id ? (
<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,
})
}
/>
<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>
</div>
) : (
<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>
<MoreVertical size={15} className="text-gray-300" />
<ConfirmationModal
confirmationButtonText="Delete Chapter"
confirmationMessage="Are you sure you want to delete this chapter?"
dialogTitle={'Delete ' + props.chapter.name + ' ?'}
dialogTrigger={
<div
className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 pt-6"
key={props.chapter.chapter_uuid}
{...provided.draggableProps}
{...provided.dragHandleProps}
ref={provided.innerRef}
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"
>
<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="bg-neutral-100 rounded-md p-2">
<Hexagon strokeWidth={3} size={16} className="text-neutral-600 " />
</div>
<div className="flex space-x-2 items-center">
{selectedChapter === props.chapter.id ?
(<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 })} />
<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>
</div>) : (<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>
<MoreVertical size={15} className="text-gray-300" />
<ConfirmationModal
confirmationButtonText="Delete Chapter"
confirmationMessage="Are you sure you want to delete this chapter?"
dialogTitle={"Delete " + props.chapter.name + " ?"}
dialogTrigger={
<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"
rel="noopener noreferrer">
<X size={15} className="text-rose-200 font-bold" />
<p>Delete Chapter</p>
</div>}
functionToExecute={() => deleteChapterUI()}
status='warning'
></ConfirmationModal>
</div>
<Droppable key={props.chapter.chapter_uuid} droppableId={props.chapter.chapter_uuid} type="activity">
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
<div className="flex flex-col">
{activities.map((activity: any, index: any) => {
return (
<div key={index} className="flex items-center ">
<ActivityElement
orgslug={props.orgslug}
course_uuid={props.course_uuid}
activityIndex={index}
activity={activity} />
</div>
)
})}
{provided.placeholder}
</div>
</div>
)}
</Droppable>
<NewActivityButton orgslug={props.orgslug} chapterId={props.chapter.id} />
<div className='h-6'>
<div className='flex items-center'><MoreHorizontal size={19} className="text-gray-300 mx-auto" /></div>
</div>
<X size={15} className="text-rose-200 font-bold" />
<p>Delete Chapter</p>
</div>
}
functionToExecute={() => deleteChapterUI()}
status="warning"
></ConfirmationModal>
</div>
<Droppable
key={props.chapter.chapter_uuid}
droppableId={props.chapter.chapter_uuid}
type="activity"
>
{(provided) => (
<div {...provided.droppableProps} ref={provided.innerRef}>
<div className="flex flex-col">
{activities.map((activity: any, index: any) => {
return (
<div key={index} className="flex items-center ">
<ActivityElement
orgslug={props.orgslug}
course_uuid={props.course_uuid}
activityIndex={index}
activity={activity}
/>
</div>
)
})}
{provided.placeholder}
</div>
</div>
)}
</Draggable>
)
</Droppable>
<NewActivityButton
orgslug={props.orgslug}
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>
)}
</Draggable>
)
}
export default ChapterElement
export default ChapterElement

View file

@ -1,149 +1,184 @@
'use client';
import { getAPIUrl } from '@services/config/config';
import { revalidateTags } from '@services/utils/ts/requests';
'use client'
import { getAPIUrl } from '@services/config/config'
import { revalidateTags } from '@services/utils/ts/requests'
import React, { useEffect, useState } from 'react'
import { DragDropContext, Droppable } from 'react-beautiful-dnd';
import { mutate } from 'swr';
import ChapterElement from './DraggableElements/ChapterElement';
import PageLoading from '@components/Objects/Loaders/PageLoading';
import { createChapter } from '@services/courses/chapters';
import { useRouter } from 'next/navigation';
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext';
import { Hexagon } from 'lucide-react';
import Modal from '@components/StyledElements/Modal/Modal';
import NewChapterModal from '@components/Objects/Modals/Chapters/NewChapter';
import { DragDropContext, Droppable } from 'react-beautiful-dnd'
import { mutate } from 'swr'
import ChapterElement from './DraggableElements/ChapterElement'
import PageLoading from '@components/Objects/Loaders/PageLoading'
import { createChapter } from '@services/courses/chapters'
import { useRouter } from 'next/navigation'
import {
useCourse,
useCourseDispatch,
} 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 = {
orgslug: string,
course_uuid?: string,
orgslug: string
course_uuid?: string
}
export type OrderPayload = {
chapter_order_by_ids: [
export type OrderPayload =
| {
chapter_order_by_ids: [
{
chapter_id: string,
activities_order_by_ids: [
{
activity_id: string
}
]
}
],
} | undefined
chapter_id: string
activities_order_by_ids: [
{
activity_id: string
},
]
},
]
}
| undefined
const EditCourseStructure = (props: EditCourseStructureProps) => {
const router = useRouter();
// Check window availability
const [winReady, setwinReady] = useState(false);
const router = useRouter()
// Check window availability
const [winReady, setwinReady] = useState(false)
const dispatchCourse = useCourseDispatch() as any;
const dispatchCourse = useCourseDispatch() as any
const [order, setOrder] = useState<OrderPayload>();
const course = useCourse() as any;
const course_structure = course ? course.courseStructure : {};
const course_uuid = course ? course.courseStructure.course_uuid : '';
const [order, setOrder] = useState<OrderPayload>()
const course = useCourse() as any
const course_structure = course ? course.courseStructure : {}
const course_uuid = course ? course.courseStructure.course_uuid : ''
// New Chapter creation
const [newChapterModal, setNewChapterModal] = useState(false);
// New Chapter creation
const [newChapterModal, setNewChapterModal] = useState(false)
const closeNewChapterModal = async () => {
setNewChapterModal(false);
};
// Submit new chapter
const submitChapter = async (chapter: any) => {
await createChapter(chapter);
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
await revalidateTags(['courses'], props.orgslug);
router.refresh();
setNewChapterModal(false);
};
const updateStructure = (result: any) => {
const { destination, source, draggableId, type } = result;
if (!destination) return;
if (destination.droppableId === source.droppableId && destination.index === source.index) return;
if (type === 'chapter') {
const newChapterOrder = Array.from(course_structure.chapters);
newChapterOrder.splice(source.index, 1);
newChapterOrder.splice(destination.index, 0, course_structure.chapters[source.index]);
dispatchCourse({ type: 'setCourseStructure', payload: { ...course_structure, chapters: newChapterOrder } })
dispatchCourse({ type: 'setIsNotSaved' })
}
if (type === 'activity') {
const newChapterOrder = Array.from(course_structure.chapters);
const sourceChapter = newChapterOrder.find((chapter: any) => chapter.chapter_uuid === source.droppableId) as any;
const destinationChapter = newChapterOrder.find((chapter: any) => chapter.chapter_uuid === destination.droppableId) ? 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' })
}
}
useEffect(() => {
setwinReady(true);
}, [props.course_uuid, course_structure, course]);
if (!course) return <PageLoading></PageLoading>
return (
<div className='flex flex-col'>
<div className="h-6"></div>
{winReady ?
<DragDropContext onDragEnd={updateStructure}>
<Droppable type='chapter' droppableId='chapters'>
{(provided) => (
<div
className='space-y-4'
{...provided.droppableProps}
ref={provided.innerRef}>
{course_structure.chapters && course_structure.chapters.map((chapter: any, index: any) => {
return (
<ChapterElement
key={chapter.chapter_uuid}
chapterIndex={index}
orgslug={props.orgslug}
course_uuid={course_uuid}
chapter={chapter} />
)
})}
{provided.placeholder}
</div>
)}
</Droppable>
{/* New Chapter Modal */}
<Modal
isDialogOpen={newChapterModal}
onOpenChange={setNewChapterModal}
minHeight="sm"
dialogContent={<NewChapterModal
course={course ? course.courseStructure : null}
closeModal={closeNewChapterModal}
submitChapter={submitChapter}
></NewChapterModal>}
dialogTitle="Create chapter"
dialogDescription="Add a new chapter to the course"
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='mx-auto flex space-x-2 items-center hover:cursor-pointer'>
<Hexagon strokeWidth={3} size={16} className="text-white text-sm " />
<div className='font-bold text-sm'>Add Chapter</div></div>
</div>
}
/>
</DragDropContext>
: <></>}
</div>
const closeNewChapterModal = async () => {
setNewChapterModal(false)
}
// Submit new chapter
const submitChapter = async (chapter: any) => {
await createChapter(chapter)
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
setNewChapterModal(false)
}
const updateStructure = (result: any) => {
const { destination, source, draggableId, type } = result
if (!destination) return
if (
destination.droppableId === source.droppableId &&
destination.index === source.index
)
return
if (type === 'chapter') {
const newChapterOrder = Array.from(course_structure.chapters)
newChapterOrder.splice(source.index, 1)
newChapterOrder.splice(
destination.index,
0,
course_structure.chapters[source.index]
)
dispatchCourse({
type: 'setCourseStructure',
payload: { ...course_structure, chapters: newChapterOrder },
})
dispatchCourse({ type: 'setIsNotSaved' })
}
if (type === 'activity') {
const newChapterOrder = Array.from(course_structure.chapters)
const sourceChapter = newChapterOrder.find(
(chapter: any) => chapter.chapter_uuid === source.droppableId
) as any
const destinationChapter = newChapterOrder.find(
(chapter: any) => chapter.chapter_uuid === destination.droppableId
)
? 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' })
}
}
useEffect(() => {
setwinReady(true)
}, [props.course_uuid, course_structure, course])
if (!course) return <PageLoading></PageLoading>
return (
<div className="flex flex-col">
<div className="h-6"></div>
{winReady ? (
<DragDropContext onDragEnd={updateStructure}>
<Droppable type="chapter" droppableId="chapters">
{(provided) => (
<div
className="space-y-4"
{...provided.droppableProps}
ref={provided.innerRef}
>
{course_structure.chapters &&
course_structure.chapters.map((chapter: any, index: any) => {
return (
<ChapterElement
key={chapter.chapter_uuid}
chapterIndex={index}
orgslug={props.orgslug}
course_uuid={course_uuid}
chapter={chapter}
/>
)
})}
{provided.placeholder}
</div>
)}
</Droppable>
{/* New Chapter Modal */}
<Modal
isDialogOpen={newChapterModal}
onOpenChange={setNewChapterModal}
minHeight="sm"
dialogContent={
<NewChapterModal
course={course ? course.courseStructure : null}
closeModal={closeNewChapterModal}
submitChapter={submitChapter}
></NewChapterModal>
}
dialogTitle="Create chapter"
dialogDescription="Add a new chapter to the course"
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="mx-auto flex space-x-2 items-center hover:cursor-pointer">
<Hexagon
strokeWidth={3}
size={16}
className="text-white text-sm "
/>
<div className="font-bold text-sm">Add Chapter</div>
</div>
</div>
}
/>
</DragDropContext>
) : (
<></>
)}
</div>
)
}
export default EditCourseStructure
export default EditCourseStructure

View file

@ -1,77 +1,75 @@
"use client";
'use client'
import React, { useEffect, useState } from 'react'
import { Field, Form, Formik } from 'formik';
import { updateOrganization, uploadOrganizationLogo } from '@services/settings/org';
import { UploadCloud } from 'lucide-react';
import { revalidateTags } from '@services/utils/ts/requests';
import { useRouter } from 'next/navigation';
import { useOrg } from '@components/Contexts/OrgContext';
import { Field, Form, Formik } from 'formik'
import {
updateOrganization,
uploadOrganizationLogo,
} from '@services/settings/org'
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 {
name: string;
description: string;
slug: string;
logo: string;
email: string;
name: string
description: string
slug: string
logo: string
email: string
}
function OrgEditGeneral(props: any) {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const router = useRouter();
const org = useOrg() as any;
const [selectedFile, setSelectedFile] = useState<File | null>(null)
const router = useRouter()
const org = useOrg() as any
// ...
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files && event.target.files.length > 0) {
const file = event.target.files[0];
setSelectedFile(file);
const file = event.target.files[0]
setSelectedFile(file)
}
};
}
const uploadLogo = async () => {
if (selectedFile) {
let org_id = org.id;
await uploadOrganizationLogo(org_id, selectedFile);
setSelectedFile(null); // Reset the selected file
await revalidateTags(['organizations'], org.slug);
router.refresh();
let org_id = org.id
await uploadOrganizationLogo(org_id, selectedFile)
setSelectedFile(null) // Reset the selected file
await revalidateTags(['organizations'], org.slug)
router.refresh()
}
};
}
let orgValues: OrganizationValues = {
name: org?.name,
description: org?.description,
slug: org?.slug,
logo: org?.logo,
email: org?.email
email: org?.email,
}
const updateOrg = async (values: OrganizationValues) => {
let org_id = org.id;
await updateOrganization(org_id, values);
let org_id = org.id
await updateOrganization(org_id, values)
// Mutate the org
await revalidateTags(['organizations'], org.slug);
router.refresh();
await revalidateTags(['organizations'], org.slug)
router.refresh()
}
useEffect(() => {
}
, [org])
useEffect(() => {}, [org])
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
enableReinitialize
enableReinitialize
initialValues={orgValues}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false);
setSubmitting(false)
updateOrg(values)
}, 400);
}, 400)
}}
>
{({ isSubmitting }) => (
@ -115,7 +113,6 @@ function OrgEditGeneral(props: any) {
</button>
</div>
<label className="block mb-2 font-bold" htmlFor="slug">
Slug
</label>
@ -143,11 +140,10 @@ function OrgEditGeneral(props: any) {
Submit
</button>
</Form>
)}
</Formik>
</div>
)
}
export default OrgEditGeneral
export default OrgEditGeneral

View file

@ -4,30 +4,68 @@ import Link from 'next/link'
import React from 'react'
type BreadCrumbsProps = {
type: 'courses' | 'user' | 'users' | 'org' | 'orgusers'
last_breadcrumb?: string
type: 'courses' | 'user' | 'users' | 'org' | 'orgusers'
last_breadcrumb?: string
}
function BreadCrumbs(props: BreadCrumbsProps) {
const course = useCourse() as any;
const course = useCourse() as any
return (
<div>
<div className='h-7'></div>
<div className='text-gray-400 tracking-tight font-medium text-sm flex 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 == '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> : ''}
<div className='flex items-center space-x-1 first-letter:uppercase'>
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
<div className='first-letter:uppercase'> {props.last_breadcrumb}</div>
</div></div></div>
return (
<div>
<div className="h-7"></div>
<div className="text-gray-400 tracking-tight font-medium text-sm flex 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 == '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>
) : (
''
)}
<div className="flex items-center space-x-1 first-letter:uppercase">
{props.last_breadcrumb ? <ChevronRight size={17} /> : ''}
<div className="first-letter:uppercase">
{' '}
{props.last_breadcrumb}
</div>
</div>
</div>
)
</div>
</div>
)
}
export default BreadCrumbs
export default BreadCrumbs

View file

@ -1,43 +1,66 @@
import { useCourse } from "@components/Contexts/CourseContext";
import { useEffect } from "react";
import BreadCrumbs from "./BreadCrumbs";
import SaveState from "./SaveState";
import { CourseOverviewParams } from "app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page";
import { getUriWithOrg } from "@services/config/config";
import { useOrg } from "@components/Contexts/OrgContext";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
import Link from "next/link";
import Image from "next/image";
import EmptyThumbnailImage from '../../../public/empty_thumbnail.png';
import { useCourse } from '@components/Contexts/CourseContext'
import { useEffect } from 'react'
import BreadCrumbs from './BreadCrumbs'
import SaveState from './SaveState'
import { CourseOverviewParams } from 'app/orgs/[orgslug]/dash/courses/course/[courseuuid]/[subpage]/page'
import { getUriWithOrg } from '@services/config/config'
import { useOrg } from '@components/Contexts/OrgContext'
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import Link from 'next/link'
import Image from 'next/image'
import EmptyThumbnailImage from '../../../public/empty_thumbnail.png'
export function CourseOverviewTop({ params }: { params: CourseOverviewParams }) {
const course = useCourse() as any;
const org = useOrg() as any;
export function CourseOverviewTop({
params,
}: {
params: CourseOverviewParams
}) {
const course = useCourse() as any
const org = useOrg() as any
useEffect(() => { }
, [course, org])
useEffect(() => {}, [course, org])
return (
<>
<BreadCrumbs type='courses' last_breadcrumb={course.courseStructure.name} ></BreadCrumbs>
<div className='flex'>
<div className='flex py-5 grow items-center'>
<Link 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>
<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-black font-bold text-xl -mt-1 first-letter:uppercase'>{course.courseStructure.name}</div>
</div>
</div>
<div className='flex items-center'>
<SaveState orgslug={params.orgslug} />
</div>
return (
<>
<BreadCrumbs
type="courses"
last_breadcrumb={course.courseStructure.name}
></BreadCrumbs>
<div className="flex">
<div className="flex py-5 grow items-center">
<Link
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>
<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-black font-bold text-xl -mt-1 first-letter:uppercase">
{course.courseStructure.name}
</div>
</>
)
}
</div>
</div>
<div className="flex items-center">
<SaveState orgslug={params.orgslug} />
</div>
</div>
</>
)
}

View file

@ -1,107 +1,172 @@
'use client';
import { useOrg } from '@components/Contexts/OrgContext';
import { useSession } from '@components/Contexts/SessionContext';
'use client'
import { useOrg } from '@components/Contexts/OrgContext'
import { useSession } from '@components/Contexts/SessionContext'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import LearnHouseDashboardLogo from '@public/dashLogo.png';
import { logout } from '@services/auth/auth';
import LearnHouseDashboardLogo from '@public/dashLogo.png'
import { logout } from '@services/auth/auth'
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 { useRouter } from 'next/navigation';
import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import UserAvatar from '../../Objects/UserAvatar';
import AdminAuthorization from '@components/Security/AdminAuthorization';
import UserAvatar from '../../Objects/UserAvatar'
import AdminAuthorization from '@components/Security/AdminAuthorization'
function LeftMenu() {
const org = useOrg() as any;
const session = useSession() as any;
const [loading, setLoading] = React.useState(true);
const route = useRouter();
const org = useOrg() as any
const session = useSession() as any
const [loading, setLoading] = React.useState(true)
const route = useRouter()
function waitForEverythingToLoad() {
if (org && session) {
return true;
}
return false;
function waitForEverythingToLoad() {
if (org && session) {
return true
}
return false
}
async function logOutUI() {
const res = await logout();
if (res) {
route.push('/login');
}
async function logOutUI() {
const res = await logout()
if (res) {
route.push('/login')
}
}
useEffect(() => {
if (waitForEverythingToLoad()) {
setLoading(false);
}
useEffect(() => {
if (waitForEverythingToLoad()) {
setLoading(false)
}
, [loading])
}, [loading])
return (
<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)" }}
className='flex flex-col w-[90px] bg-black h-screen text-white shadow-xl'>
<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 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>
</Link>
</div>
<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' >
return (
<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)',
}}
className="flex flex-col w-[90px] bg-black h-screen text-white shadow-xl"
>
<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
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>
</Link>
</div>
<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' >
<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> */}
<AdminAuthorization authorizationMode="component">
<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>
</ToolTip>
<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>
</ToolTip>
<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>
</ToolTip>
<ToolTip 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>
</AdminAuthorization>
</div>
<div className='flex flex-col mx-auto pb-7 space-y-2'>
<div className="flex items-center flex-col space-y-2">
<ToolTip content={'@' + session.user.username} slateBlack sideOffset={8} side='right' >
<div className='mx-auto'>
<UserAvatar border='border-4' width={35} />
</div>
</ToolTip>
<div className='flex items-center flex-col space-y-1'>
<ToolTip content={session.user.username + "'s Settings"} 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>
</ToolTip>
<ToolTip content={'Logout'} slateBlack sideOffset={8} side='right' >
<LogOut onClick={() => logOutUI()} className='mx-auto text-neutral-400 cursor-pointer' size={14} />
</ToolTip>
</div>
</div>
</div>
</div>
<AdminAuthorization authorizationMode="component">
<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>
</ToolTip>
<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>
</ToolTip>
<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>
</ToolTip>
<ToolTip
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>
</AdminAuthorization>
</div>
)
<div className="flex flex-col mx-auto pb-7 space-y-2">
<div className="flex items-center flex-col space-y-2">
<ToolTip
content={'@' + session.user.username}
slateBlack
sideOffset={8}
side="right"
>
<div className="mx-auto">
<UserAvatar border="border-4" width={35} />
</div>
</ToolTip>
<div className="flex items-center flex-col space-y-1">
<ToolTip
content={session.user.username + "'s Settings"}
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>
</ToolTip>
<ToolTip
content={'Logout'}
slateBlack
sideOffset={8}
side="right"
>
<LogOut
onClick={() => logOutUI()}
className="mx-auto text-neutral-400 cursor-pointer"
size={14}
/>
</ToolTip>
</div>
</div>
</div>
</div>
</div>
)
}
export default LeftMenu

View file

@ -1,114 +1,127 @@
'use client';
import { getAPIUrl } from '@services/config/config';
import { updateCourseOrderStructure } from '@services/courses/chapters';
import { revalidateTags } from '@services/utils/ts/requests';
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext'
'use client'
import { getAPIUrl } from '@services/config/config'
import { updateCourseOrderStructure } from '@services/courses/chapters'
import { revalidateTags } from '@services/utils/ts/requests'
import {
useCourse,
useCourseDispatch,
} from '@components/Contexts/CourseContext'
import { Check, SaveAllIcon, Timer } from 'lucide-react'
import { useRouter } from 'next/navigation';
import { useRouter } from 'next/navigation'
import React, { useEffect } from 'react'
import { mutate } from 'swr';
import { updateCourse } from '@services/courses/courses';
import { mutate } from 'swr'
import { updateCourse } from '@services/courses/courses'
function SaveState(props: { orgslug: string }) {
const course = useCourse() as any;
const router = useRouter();
const saved = course ? course.isSaved : true;
const dispatchCourse = useCourseDispatch() as any;
const course_structure = course.courseStructure;
const saveCourseState = async () => {
// Course order
if (saved) return;
await changeOrderBackend();
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
// Course metadata
await changeMetadataBackend();
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
await revalidateTags(['courses'], props.orgslug)
dispatchCourse({ type: 'setIsSaved' })
}
//
// Course Order
const changeOrderBackend = async () => {
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
await updateCourseOrderStructure(course.courseStructure.course_uuid, course.courseOrder);
await revalidateTags(['courses'], props.orgslug)
router.refresh();
dispatchCourse({ type: 'setIsSaved' })
}
const course = useCourse() as any
const router = useRouter()
const saved = course ? course.isSaved : true
const dispatchCourse = useCourseDispatch() as any
const course_structure = course.courseStructure
const saveCourseState = async () => {
// Course order
if (saved) return
await changeOrderBackend()
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
// Course metadata
const changeMetadataBackend = async () => {
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`);
await updateCourse(course.courseStructure.course_uuid, course.courseStructure);
await revalidateTags(['courses'], props.orgslug)
router.refresh();
dispatchCourse({ type: 'setIsSaved' })
}
const handleCourseOrder = (course_structure: any) => {
const chapters = course_structure.chapters;
const chapter_order_by_ids = chapters.map((chapter: any) => {
return {
chapter_id: chapter.id,
activities_order_by_ids: chapter.activities.map((activity: any) => {
return {
activity_id: activity.id
}
})
}
})
dispatchCourse({ type: 'setCourseOrder', payload: { chapter_order_by_ids: chapter_order_by_ids } })
dispatchCourse({ type: 'setIsNotSaved' })
}
const initOrderPayload = () => {
if (course_structure && course_structure.chapters) {
handleCourseOrder(course_structure);
dispatchCourse({ type: 'setIsSaved' })
}
}
const changeOrderPayload = () => {
if (course_structure && course_structure.chapters) {
handleCourseOrder(course_structure);
dispatchCourse({ type: 'setIsNotSaved' })
}
}
useEffect(() => {
if (course_structure?.chapters) {
initOrderPayload();
}
if (course_structure?.chapters && !saved) {
changeOrderPayload();
}
}, [course_structure]); // This effect depends on the `course_structure` variable
return (
<div className='flex space-x-4'>
{saved ? <></> : <div className='text-gray-600 flex space-x-2 items-center antialiased'>
<Timer size={15} />
<div>
Unsaved changes
</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 ')
} onClick={saveCourseState}>
{saved ? <Check size={20} /> : <SaveAllIcon size={20} />}
{saved ? <div className=''>Saved</div> : <div className=''>Save</div>}
</div>
</div>
await changeMetadataBackend()
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
await revalidateTags(['courses'], props.orgslug)
dispatchCourse({ type: 'setIsSaved' })
}
//
// Course Order
const changeOrderBackend = async () => {
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
await updateCourseOrderStructure(
course.courseStructure.course_uuid,
course.courseOrder
)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
dispatchCourse({ type: 'setIsSaved' })
}
// Course metadata
const changeMetadataBackend = async () => {
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta`)
await updateCourse(
course.courseStructure.course_uuid,
course.courseStructure
)
await revalidateTags(['courses'], props.orgslug)
router.refresh()
dispatchCourse({ type: 'setIsSaved' })
}
const handleCourseOrder = (course_structure: any) => {
const chapters = course_structure.chapters
const chapter_order_by_ids = chapters.map((chapter: any) => {
return {
chapter_id: chapter.id,
activities_order_by_ids: chapter.activities.map((activity: any) => {
return {
activity_id: activity.id,
}
}),
}
})
dispatchCourse({
type: 'setCourseOrder',
payload: { chapter_order_by_ids: chapter_order_by_ids },
})
dispatchCourse({ type: 'setIsNotSaved' })
}
const initOrderPayload = () => {
if (course_structure && course_structure.chapters) {
handleCourseOrder(course_structure)
dispatchCourse({ type: 'setIsSaved' })
}
}
const changeOrderPayload = () => {
if (course_structure && course_structure.chapters) {
handleCourseOrder(course_structure)
dispatchCourse({ type: 'setIsNotSaved' })
}
}
useEffect(() => {
if (course_structure?.chapters) {
initOrderPayload()
}
if (course_structure?.chapters && !saved) {
changeOrderPayload()
}
}, [course_structure]) // This effect depends on the `course_structure` variable
return (
<div className="flex space-x-4">
{saved ? (
<></>
) : (
<div className="text-gray-600 flex space-x-2 items-center antialiased">
<Timer size={15} />
<div>Unsaved changes</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 ')
}
onClick={saveCourseState}
>
{saved ? <Check size={20} /> : <SaveAllIcon size={20} />}
{saved ? <div className="">Saved</div> : <div className="">Save</div>}
</div>
</div>
)
}
export default SaveState
export default SaveState

View file

@ -1,40 +1,44 @@
import { updateProfile } from '@services/settings/profile';
import { updateProfile } from '@services/settings/profile'
import React, { useEffect } from 'react'
import { Formik, Form, Field } from 'formik';
import { useSession } from '@components/Contexts/SessionContext';
import { ArrowBigUpDash, Check, FileWarning, Info, UploadCloud } from 'lucide-react';
import UserAvatar from '@components/Objects/UserAvatar';
import { updateUserAvatar } from '@services/users/users';
import { Formik, Form, Field } from 'formik'
import { useSession } from '@components/Contexts/SessionContext'
import {
ArrowBigUpDash,
Check,
FileWarning,
Info,
UploadCloud,
} from 'lucide-react'
import UserAvatar from '@components/Objects/UserAvatar'
import { updateUserAvatar } from '@services/users/users'
function UserEditGeneral() {
const session = useSession() as any;
const [localAvatar, setLocalAvatar] = React.useState(null) as any;
const [isLoading, setIsLoading] = React.useState(false) as any;
const [error, setError] = React.useState() as any;
const [success, setSuccess] = React.useState('') as any;
const session = useSession() as any
const [localAvatar, setLocalAvatar] = React.useState(null) as any
const [isLoading, setIsLoading] = React.useState(false) as any
const [error, setError] = React.useState() as any
const [success, setSuccess] = React.useState('') as any
const handleFileChange = async (event: any) => {
const file = event.target.files[0];
setLocalAvatar(file);
setIsLoading(true);
const file = event.target.files[0]
setLocalAvatar(file)
setIsLoading(true)
const res = await updateUserAvatar(session.user.user_uuid, file)
// 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) {
setError(res.HTTPmessage);
setError(res.HTTPmessage)
} else {
setIsLoading(false);
setError('');
setSuccess('Avatar Updated');
setIsLoading(false)
setError('')
setSuccess('Avatar Updated')
}
};
useEffect(() => {
}
, [session, session.user])
useEffect(() => {}, [session, session.user])
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 && (
<Formik
enableReinitialize
@ -47,17 +51,14 @@ function UserEditGeneral() {
}}
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false);
setSubmitting(false)
updateProfile(values, session.user.id)
}, 400);
}, 400)
}}
>
{({ isSubmitting }) => (
<div className='flex space-x-8'>
<div className="flex space-x-8">
<Form className="max-w-md">
<label className="block mb-2 font-bold" htmlFor="email">
Email
</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"
type="bio"
name="bio"
/>
<button
type="submit"
@ -114,63 +114,77 @@ function UserEditGeneral() {
Submit
</button>
</Form>
<div className='flex flex-col grow justify-center align-middle space-y-3'>
<label className="flex mx-auto mb-2 font-bold " >
Avatar
</label>
<div className="flex flex-col grow justify-center align-middle space-y-3">
<label className="flex mx-auto mb-2 font-bold ">Avatar</label>
{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">
<FileWarning size={16} className='mr-2' />
<div className="text-sm font-semibold first-letter:uppercase">{error}</div>
</div>
)}
{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">
<Check size={16} className='mr-2' />
<div className="text-sm font-semibold first-letter:uppercase">{success}</div>
</div>
)}
<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" />
<div className="text-sm font-semibold first-letter:uppercase">
{error}
</div>
</div>
)}
{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">
<Check size={16} className="mr-2" />
<div className="text-sm font-semibold first-letter:uppercase">
{success}
</div>
</div>
)}
<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='flex flex-col justify-center items-center mt-10'>
<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">
{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>
{isLoading ? (<div className='flex justify-center items-center'>
<input type="file" id="fileInput" 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>
{isLoading ? (
<div className="flex justify-center items-center">
<input
type="file"
id="fileInput"
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>
</div>
</div>) : (
<div className='flex justify-center items-center'>
<input type="file" id="fileInput" style={{ display: 'none' }} onChange={handleFileChange} />
<button
className='font-bold antialiased items-center text-gray text-sm rounded-md px-4 py-2 mt-4 flex'
onClick={() => document.getElementById('fileInput')?.click()}
>
<UploadCloud size={16} className='mr-2' />
<span>Change Thumbnail</span>
</button>
</div> )}
</div>
) : (
<div className="flex justify-center items-center">
<input
type="file"
id="fileInput"
style={{ display: 'none' }}
onChange={handleFileChange}
/>
<button
className="font-bold antialiased items-center text-gray text-sm rounded-md px-4 py-2 mt-4 flex"
onClick={() =>
document.getElementById('fileInput')?.click()
}
>
<UploadCloud size={16} className="mr-2" />
<span>Change Thumbnail</span>
</button>
</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 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>
)}
</Formik>
)}
@ -178,4 +192,4 @@ function UserEditGeneral() {
)
}
export default UserEditGeneral
export default UserEditGeneral

View file

@ -1,66 +1,62 @@
import { useSession } from '@components/Contexts/SessionContext';
import { updatePassword } from '@services/settings/password';
import { Formik, Form, Field } from 'formik';
import { useSession } from '@components/Contexts/SessionContext'
import { updatePassword } from '@services/settings/password'
import { Formik, Form, Field } from 'formik'
import React, { useEffect } from 'react'
function UserEditPassword() {
const session = useSession() as any;
const session = useSession() as any
const updatePasswordUI = async (values: any) => {
let user_id = session.user.user_id;
await updatePassword(user_id, values)
}
const updatePasswordUI = async (values: any) => {
let user_id = session.user.user_id
await updatePassword(user_id, values)
}
useEffect(() => {
}
, [session])
useEffect(() => {}, [session])
return (
<div className="ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5">
<Formik
initialValues={{ old_password: '', new_password: '' }}
enableReinitialize
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false)
updatePasswordUI(values)
}, 400)
}}
>
{({ isSubmitting }) => (
<Form className="max-w-md">
<label className="block mb-2 font-bold" htmlFor="old_password">
Old Password
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="password"
name="old_password"
/>
return (
<div className='ml-10 mr-10 mx-auto bg-white rounded-xl shadow-sm px-6 py-5'>
<Formik
initialValues={{ old_password: '', new_password: '' }}
enableReinitialize
onSubmit={(values, { setSubmitting }) => {
setTimeout(() => {
setSubmitting(false);
updatePasswordUI(values)
}, 400);
}}
<label className="block mb-2 font-bold" htmlFor="new_password">
New Password
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="password"
name="new_password"
/>
<button
type="submit"
disabled={isSubmitting}
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500"
>
{({ isSubmitting }) => (
<Form className="max-w-md">
<label className="block mb-2 font-bold" htmlFor="old_password">
Old Password
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="password"
name="old_password"
/>
<label className="block mb-2 font-bold" htmlFor="new_password">
New Password
</label>
<Field
className="w-full px-4 py-2 mb-4 border rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
type="password"
name="new_password"
/>
<button
type="submit"
disabled={isSubmitting}
className="px-6 py-3 text-white bg-black rounded-lg shadow-md hover:bg-black focus:outline-none focus:ring-2 focus:ring-blue-500"
>
Submit
</button>
</Form>
)}
</Formik>
</div>
)
Submit
</button>
</Form>
)}
</Formik>
</div>
)
}
export default UserEditPassword
export default UserEditPassword

View file

@ -1,175 +1,236 @@
import { useOrg } from '@components/Contexts/OrgContext'
import PageLoading from '@components/Objects/Loaders/PageLoading';
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
import { getAPIUrl, getUriWithOrg } from '@services/config/config';
import { swrFetcher } from '@services/utils/ts/requests';
import PageLoading from '@components/Objects/Loaders/PageLoading'
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
import { getAPIUrl, getUriWithOrg } from '@services/config/config'
import { swrFetcher } from '@services/utils/ts/requests'
import { Globe, Shield, X } from 'lucide-react'
import Link from 'next/link';
import Link from 'next/link'
import React, { useEffect } from 'react'
import useSWR, { mutate } from 'swr';
import dayjs from 'dayjs';
import { changeSignupMechanism, createInviteCode, deleteInviteCode } from '@services/organizations/invites';
import Toast from '@components/StyledElements/Toast/Toast';
import toast from 'react-hot-toast';
import { useRouter } from 'next/navigation';
import useSWR, { mutate } from 'swr'
import dayjs from 'dayjs'
import {
changeSignupMechanism,
createInviteCode,
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() {
const org = useOrg() as any;
const { data: invites } = useSWR(org ? `${getAPIUrl()}orgs/${org?.id}/invites` : null, swrFetcher);
const [isLoading, setIsLoading] = React.useState(false)
const [joinMethod, setJoinMethod] = React.useState('closed')
const router = useRouter()
const org = useOrg() as any
const { data: invites } = useSWR(
org ? `${getAPIUrl()}orgs/${org?.id}/invites` : null,
swrFetcher
)
const [isLoading, setIsLoading] = React.useState(false)
const [joinMethod, setJoinMethod] = React.useState('closed')
const router = useRouter()
async function getOrgJoinMethod() {
if (org) {
if (org.config.config.GeneralConfig.users.signup_mechanism == 'open') {
setJoinMethod('open')
}
else {
setJoinMethod('inviteOnly')
}
}
async function getOrgJoinMethod() {
if (org) {
if (org.config.config.GeneralConfig.users.signup_mechanism == 'open') {
setJoinMethod('open')
} else {
setJoinMethod('inviteOnly')
}
}
}
async function createInvite() {
let res = await createInviteCode(org.id)
if (res.status == 200) {
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
}
else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
async function createInvite() {
let res = await createInviteCode(org.id)
if (res.status == 200) {
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
} else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
}
async function deleteInvite(invite: any) {
let res = await deleteInviteCode(org.id, invite.invite_code_uuid)
if (res.status == 200) {
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
}
else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
async function deleteInvite(invite: any) {
let res = await deleteInviteCode(org.id, invite.invite_code_uuid)
if (res.status == 200) {
mutate(`${getAPIUrl()}orgs/${org.id}/invites`)
} else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
}
async function changeJoinMethod(method: 'open' | 'inviteOnly') {
let res = await changeSignupMechanism(org.id, method)
if (res.status == 200) {
router.refresh()
mutate(`${getAPIUrl()}orgs/slug/${org?.slug}`)
}
else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
async function changeJoinMethod(method: 'open' | 'inviteOnly') {
let res = await changeSignupMechanism(org.id, method)
if (res.status == 200) {
router.refresh()
mutate(`${getAPIUrl()}orgs/slug/${org?.slug}`)
} else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
}
useEffect(() => {
if (invites && org) {
getOrgJoinMethod()
setIsLoading(false)
}
useEffect(() => {
if (invites && org) {
getOrgJoinMethod()
setIsLoading(false)
}
, [org, invites])
return (
}, [org, invites])
return (
<>
<Toast></Toast>
{!isLoading ? (
<>
<Toast></Toast>
{!isLoading ? (<>
<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='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>
<h2 className='text-gray-500 text-md'> Choose how users can join your organization </h2>
<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="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>
<h2 className="text-gray-500 text-md">
{' '}
Choose how users can join your organization{' '}
</h2>
</div>
<div className="flex space-x-2 mx-auto">
<ConfirmationModal
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."
dialogTitle={'Change to open ?'}
dialogTrigger={
<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}
<div className="flex flex-col space-y-1 justify-center items-center h-full">
<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 className='flex space-x-2 mx-auto'>
<ConfirmationModal
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.'
dialogTitle={'Change to open ?'}
</div>
}
functionToExecute={() => {
changeJoinMethod('open')
}}
status="info"
></ConfirmationModal>
<ConfirmationModal
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."
dialogTitle={'Change to closed ?'}
dialogTrigger={
<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}
<div className="flex flex-col space-y-1 justify-center items-center h-full">
<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>
</div>
<div
className={
joinMethod == 'open'
? '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>
<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">
<tr className="font-bolder text-sm">
<th className="py-3 px-4">Code</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">Actions</th>
</tr>
</thead>
<>
<tbody className="mt-5 bg-white rounded-md">
{invites?.map((invite: any) => (
<tr
key={invite.invite_code_uuid}
className="border-b border-gray-100 text-sm"
>
<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>
</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">
<ConfirmationModal
confirmationButtonText="Delete Code"
confirmationMessage="Are you sure you want remove this invite code ?"
dialogTitle={'Delete code ?'}
dialogTrigger={
<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}
<div className='flex flex-col space-y-1 justify-center items-center h-full'>
<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
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.'
dialogTitle={'Change to closed ?'}
dialogTrigger={
<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}
<div className='flex flex-col space-y-1 justify-center items-center h-full'>
<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>
</div>
<div className={joinMethod == 'open' ? '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>
<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'>
<tr className='font-bolder text-sm'>
<th className='py-3 px-4'>Code</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'>Actions</th>
</tr>
</thead>
<>
<tbody className='mt-5 bg-white rounded-md' >
{invites?.map((invite: any) => (
<tr key={invite.invite_code_uuid} className='border-b border-gray-100 text-sm'>
<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>
</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'>
<ConfirmationModal
confirmationButtonText='Delete Code'
confirmationMessage='Are you sure you want remove this invite code ?'
dialogTitle={'Delete code ?'}
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'>
<X className='w-4 h-4' />
<span> Delete code</span>
</button>}
functionToExecute={() => { deleteInvite(invite) }}
status='warning'
></ConfirmationModal>
</td>
</tr>
))}
</tbody>
</>
</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'>
<Shield className='w-4 h-4' />
<span> Create invite code</span>
</button>
</div>
</div></>) : <PageLoading />}
<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" />
<span> Delete code</span>
</button>
}
functionToExecute={() => {
deleteInvite(invite)
}}
status="warning"
></ConfirmationModal>
</td>
</tr>
))}
</tbody>
</>
</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"
>
<Shield className="w-4 h-4" />
<span> Create invite code</span>
</button>
</div>
</div>
</>
)
) : (
<PageLoading />
)}
</>
)
}
export default OrgAccess
export default OrgAccess

View file

@ -1,120 +1,144 @@
import { useOrg } from '@components/Contexts/OrgContext';
import PageLoading from '@components/Objects/Loaders/PageLoading';
import RolesUpdate from '@components/Objects/Modals/Dash/OrgUsers/RolesUpdate';
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal';
import Modal from '@components/StyledElements/Modal/Modal';
import Toast from '@components/StyledElements/Toast/Toast';
import { getAPIUrl } from '@services/config/config';
import { removeUserFromOrg } from '@services/organizations/orgs';
import { swrFetcher } from '@services/utils/ts/requests';
import { KeyRound, LogOut } from 'lucide-react';
import { useOrg } from '@components/Contexts/OrgContext'
import PageLoading from '@components/Objects/Loaders/PageLoading'
import RolesUpdate from '@components/Objects/Modals/Dash/OrgUsers/RolesUpdate'
import ConfirmationModal from '@components/StyledElements/ConfirmationModal/ConfirmationModal'
import Modal from '@components/StyledElements/Modal/Modal'
import Toast from '@components/StyledElements/Toast/Toast'
import { getAPIUrl } from '@services/config/config'
import { removeUserFromOrg } from '@services/organizations/orgs'
import { swrFetcher } from '@services/utils/ts/requests'
import { KeyRound, LogOut } from 'lucide-react'
import React, { useEffect } from 'react'
import toast from 'react-hot-toast';
import useSWR, { mutate } from 'swr';
import toast from 'react-hot-toast'
import useSWR, { mutate } from 'swr'
function OrgUsers() {
const org = useOrg() as any;
const { data: orgUsers } = useSWR(org ? `${getAPIUrl()}orgs/${org?.id}/users` : null, swrFetcher);
const [rolesModal, setRolesModal] = React.useState(false);
const [selectedUser, setSelectedUser] = React.useState(null) as any;
const [isLoading, setIsLoading] = React.useState(true);
const org = useOrg() as any
const { data: orgUsers } = useSWR(
org ? `${getAPIUrl()}orgs/${org?.id}/users` : null,
swrFetcher
)
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) => {
setSelectedUser(user_uuid);
setRolesModal(!rolesModal);
const handleRolesModal = (user_uuid: any) => {
setSelectedUser(user_uuid)
setRolesModal(!rolesModal)
}
const handleRemoveUser = async (user_id: any) => {
const res = await removeUserFromOrg(org.id, user_id)
if (res.status === 200) {
await mutate(`${getAPIUrl()}orgs/${org.id}/users`)
} else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
}
const handleRemoveUser = async (user_id: any) => {
const res = await removeUserFromOrg(org.id, user_id);
if (res.status === 200) {
await mutate(`${getAPIUrl()}orgs/${org.id}/users`);
}
else {
toast.error('Error ' + res.status + ': ' + res.data.detail)
}
useEffect(() => {
if (orgUsers) {
setIsLoading(false)
console.log(orgUsers)
}
}, [org, orgUsers])
useEffect(() => {
if (orgUsers) {
setIsLoading(false)
console.log(orgUsers)
}
}, [org, orgUsers])
return (
return (
<div>
{isLoading ? (
<div>
{isLoading ? <div><PageLoading /></div> :
<>
<Toast></Toast>
<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='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>
<h2 className='text-gray-500 text-md'> Manage your organization users, assign roles and permissions </h2>
</div>
<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'>
<tr className='font-bolder text-sm'>
<th className='py-3 px-4'>User</th>
<th className='py-3 px-4'>Role</th>
<th className='py-3 px-4'>Actions</th>
</tr>
</thead>
<>
<tbody className='mt-5 bg-white rounded-md' >
{orgUsers?.map((user: any) => (
<tr key={user.user.id} className='border-b border-gray-200 border-dashed'>
<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 className='py-3 px-4'>{user.role.name}</td>
<td className='py-3 px-4 flex space-x-2 items-end'>
<Modal
isDialogOpen={rolesModal && selectedUser === user.user.user_uuid}
onOpenChange={() => handleRolesModal(user.user.user_uuid)}
minHeight="no-min"
dialogContent={
<RolesUpdate
alreadyAssignedRole={user.role.role_uuid}
setRolesModal={setRolesModal}
user={user} />
}
dialogTitle="Update Role"
dialogDescription={"Update @" + user.user.username + "'s role"}
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'>
<KeyRound className='w-4 h-4' />
<span> Edit Role</span>
</button>}
/>
<ConfirmationModal
confirmationButtonText='Remove User'
confirmationMessage='Are you sure you want remove this user from the organization?'
dialogTitle={'Delete ' + user.user.username + ' ?'}
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'>
<LogOut className='w-4 h-4' />
<span> Remove from organization</span>
</button>}
functionToExecute={() => { handleRemoveUser(user.user.id) }}
status='warning'
></ConfirmationModal>
</td>
</tr>
))}
</tbody>
</>
</table>
</div>
</>
}
<PageLoading />
</div>
)
) : (
<>
<Toast></Toast>
<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="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>
<h2 className="text-gray-500 text-md">
{' '}
Manage your organization users, assign roles and permissions{' '}
</h2>
</div>
<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">
<tr className="font-bolder text-sm">
<th className="py-3 px-4">User</th>
<th className="py-3 px-4">Role</th>
<th className="py-3 px-4">Actions</th>
</tr>
</thead>
<>
<tbody className="mt-5 bg-white rounded-md">
{orgUsers?.map((user: any) => (
<tr
key={user.user.id}
className="border-b border-gray-200 border-dashed"
>
<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 className="py-3 px-4">{user.role.name}</td>
<td className="py-3 px-4 flex space-x-2 items-end">
<Modal
isDialogOpen={
rolesModal && selectedUser === user.user.user_uuid
}
onOpenChange={() =>
handleRolesModal(user.user.user_uuid)
}
minHeight="no-min"
dialogContent={
<RolesUpdate
alreadyAssignedRole={user.role.role_uuid}
setRolesModal={setRolesModal}
user={user}
/>
}
dialogTitle="Update Role"
dialogDescription={
'Update @' + user.user.username + "'s role"
}
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">
<KeyRound className="w-4 h-4" />
<span> Edit Role</span>
</button>
}
/>
<ConfirmationModal
confirmationButtonText="Remove User"
confirmationMessage="Are you sure you want remove this user from the organization?"
dialogTitle={'Delete ' + user.user.username + ' ?'}
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">
<LogOut className="w-4 h-4" />
<span> Remove from organization</span>
</button>
}
functionToExecute={() => {
handleRemoveUser(user.user.id)
}}
status="warning"
></ConfirmationModal>
</td>
</tr>
))}
</tbody>
</>
</table>
</div>
</>
)}
</div>
)
}
export default OrgUsers
export default OrgUsers

View file

@ -1,327 +1,477 @@
import { useSession } from '@components/Contexts/SessionContext'
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
import { AlertTriangle, BadgeInfo, NotebookTabs } from 'lucide-react';
import { motion, AnimatePresence } from 'framer-motion';
import {
sendActivityAIChatMessage,
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 Image from 'next/image';
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
import learnhouseAI_logo_black from "public/learnhouse_ai_black_logo.png";
import Image from 'next/image'
import learnhouseAI_icon from 'public/learnhouse_ai_simple.png'
import learnhouseAI_logo_black from 'public/learnhouse_ai_black_logo.png'
import React, { useEffect, useRef } from 'react'
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
import useGetAIFeatures from '../../../AI/Hooks/useGetAIFeatures';
import UserAvatar from '@components/Objects/UserAvatar';
import {
AIChatBotStateTypes,
useAIChatBot,
useAIChatBotDispatch,
} from '@components/Contexts/AI/AIChatBotContext'
import useGetAIFeatures from '../../../AI/Hooks/useGetAIFeatures'
import UserAvatar from '@components/Objects/UserAvatar'
type AIActivityAskProps = {
activity: any;
activity: any
}
function AIActivityAsk(props: AIActivityAskProps) {
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' });
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false);
const dispatchAIChatBot = useAIChatBotDispatch() as any;
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' })
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false)
const dispatchAIChatBot = useAIChatBotDispatch() as any
useEffect(() => {
if (is_ai_feature_enabled) {
setIsButtonAvailable(true);
}
useEffect(() => {
if (is_ai_feature_enabled) {
setIsButtonAvailable(true)
}
, [is_ai_feature_enabled]);
}, [is_ai_feature_enabled])
return (
<>
{isButtonAvailable && (
<div >
<ActivityChatMessageBox activity={props.activity} />
<div
onClick={() => dispatchAIChatBot({ type: 'setIsModalOpen' })}
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)',
}}
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>
<Image 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>
</div>
</div>
)}
</>
)
return (
<>
{isButtonAvailable && (
<div>
<ActivityChatMessageBox activity={props.activity} />
<div
onClick={() => dispatchAIChatBot({ type: 'setIsModalOpen' })}
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)',
}}
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>
<Image
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>
</div>
</div>
)}
</>
)
}
export type AIMessage = {
sender: string;
message: any;
type: 'ai' | 'user';
sender: string
message: any
type: 'ai' | 'user'
}
type ActivityChatMessageBoxProps = {
activity: any;
activity: any
}
function ActivityChatMessageBox(props: ActivityChatMessageBoxProps) {
const session = useSession() as any;
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
const dispatchAIChatBot = useAIChatBotDispatch() as any;
const session = useSession() as any
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
const dispatchAIChatBot = useAIChatBotDispatch() as any
// TODO : come up with a better way to handle this
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';
// TODO : come up with a better way to handle this
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'
useEffect(() => {
if (aiChatBotState.isModalOpen) {
document.body.style.overflow = 'hidden';
} else {
document.body.style.overflow = 'unset';
}
}, [aiChatBotState.isModalOpen]);
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
// Perform the sending action here
sendMessage(event.currentTarget.value);
}
useEffect(() => {
if (aiChatBotState.isModalOpen) {
document.body.style.overflow = 'hidden'
} else {
document.body.style.overflow = 'unset'
}
}, [aiChatBotState.isModalOpen])
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
await dispatchAIChatBot({ type: 'setChatInputValue', payload: event.currentTarget.value });
function handleKeyDown(event: React.KeyboardEvent<HTMLInputElement>) {
if (event.key === 'Enter') {
// Perform the sending action here
sendMessage(event.currentTarget.value)
}
}
const sendMessage = async (message: string) => {
if (aiChatBotState.aichat_uuid) {
await dispatchAIChatBot({ type: 'addMessage', 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) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
return;
}
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
const handleChange = async (event: React.ChangeEvent<HTMLInputElement>) => {
await dispatchAIChatBot({
type: 'setChatInputValue',
payload: event.currentTarget.value,
})
}
} else {
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
const response = await startActivityAIChatSession(message, props.activity.activity_uuid)
if (response.success == false) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
await dispatchAIChatBot({ 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({ type: 'setIsNoLongerWaitingForResponse' });
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
}
const sendMessage = async (message: string) => {
if (aiChatBotState.aichat_uuid) {
await dispatchAIChatBot({
type: 'addMessage',
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) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
await dispatchAIChatBot({
type: 'setError',
payload: {
isError: true,
status: response.status,
error_message: response.data.detail,
},
})
return
}
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
await dispatchAIChatBot({
type: 'addMessage',
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
})
} else {
await dispatchAIChatBot({
type: 'addMessage',
payload: { sender: 'user', message: message, type: 'user' },
})
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
const response = await startActivityAIChatSession(
message,
props.activity.activity_uuid
)
if (response.success == false) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
await dispatchAIChatBot({
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({ type: 'setIsNoLongerWaitingForResponse' })
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
await dispatchAIChatBot({
type: 'addMessage',
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
})
}
}
function closeModal() {
dispatchAIChatBot({ type: 'setIsModalClose' });
function closeModal() {
dispatchAIChatBot({ type: 'setIsModalClose' })
}
const messagesEndRef = useRef<HTMLDivElement>(null)
useEffect(() => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' })
}
}, [aiChatBotState.messages, session])
const messagesEndRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (messagesEndRef.current) {
messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
}
}, [aiChatBotState.messages, session]);
return (
<AnimatePresence>
{aiChatBotState.isModalOpen && (
<>
<motion.div
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
exit={{ y: 50, opacity: 0, filter: 'blur(25px)' }}
transition={{ 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' }}
>
<div
style={{
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%)'
}}
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'>
<X size={20} className='text-white/50 hover:cursor-pointer bg-white/10 p-1 rounded-full items-center' onClick={closeModal} />
</div>
<div className={`flex space-x-2 items-center -ml-[100px] ${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 className='bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center'>
<FlaskConical size={14} />
<span className='text-xs font-semibold antialiased '>Experimental</span>
</div>
</div>
<div className={`w-100 h-0.5 bg-white/5 rounded-full mx-auto mb-3 ${aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''}`}></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 (
<AIMessage key={index} message={message} animated={message.sender == 'ai' ? true : false} />
)
})}
<div ref={messagesEndRef} />
</div>
) : (
<AIMessagePlaceHolder sendMessage={sendMessage} activity_uuid={props.activity.activity_uuid} />
)}
{aiChatBotState.error.isError && (
<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'>
<AlertTriangle size={20} className='text-red-500' />
<div className='flex flex-col'>
<h3 className='font-semibold text-red-200'>Something wrong happened</h3>
<span className='text-red-100 text-sm '>{aiChatBotState.error.error_message}</span>
</div>
</div>
</div>
)
}
<div className='flex space-x-2 items-center'>
<div className=''>
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
</div>
<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="" />
</div>
<div className=''>
<MessageCircle size={20} className='text-white/50 hover:cursor-pointer' onClick={() => sendMessage(aiChatBotState.chatInputValue)} />
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
)
return (
<AnimatePresence>
{aiChatBotState.isModalOpen && (
<>
<motion.div
initial={{ y: 20, opacity: 0.3, filter: 'blur(5px)' }}
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
exit={{ y: 50, opacity: 0, filter: 'blur(25px)' }}
transition={{
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' }}
>
<div
style={{
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%)',
}}
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">
<X
size={20}
className="text-white/50 hover:cursor-pointer bg-white/10 p-1 rounded-full items-center"
onClick={closeModal}
/>
</div>
<div
className={`flex space-x-2 items-center -ml-[100px] ${
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 className="bg-white/5 text-white/40 py-0.5 px-3 flex space-x-1 rounded-full items-center">
<FlaskConical size={14} />
<span className="text-xs font-semibold antialiased ">
Experimental
</span>
</div>
</div>
<div
className={`w-100 h-0.5 bg-white/5 rounded-full mx-auto mb-3 ${
aiChatBotState.isWaitingForResponse ? 'animate-pulse' : ''
}`}
></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 (
<AIMessage
key={index}
message={message}
animated={message.sender == 'ai' ? true : false}
/>
)
}
)}
<div ref={messagesEndRef} />
</div>
) : (
<AIMessagePlaceHolder
sendMessage={sendMessage}
activity_uuid={props.activity.activity_uuid}
/>
)}
{aiChatBotState.error.isError && (
<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">
<AlertTriangle size={20} className="text-red-500" />
<div className="flex flex-col">
<h3 className="font-semibold text-red-200">
Something wrong happened
</h3>
<span className="text-red-100 text-sm ">
{aiChatBotState.error.error_message}
</span>
</div>
</div>
</div>
)}
<div className="flex space-x-2 items-center">
<div className="">
<UserAvatar
rounded="rounded-lg"
border="border-2"
width={35}
/>
</div>
<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=""
/>
</div>
<div className="">
<MessageCircle
size={20}
className="text-white/50 hover:cursor-pointer"
onClick={() => sendMessage(aiChatBotState.chatInputValue)}
/>
</div>
</div>
</div>
</motion.div>
</>
)}
</AnimatePresence>
)
}
type AIMessageProps = {
message: AIMessage;
animated: boolean;
message: AIMessage
animated: boolean
}
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 (
<div className="flex space-x-2 w-full antialiased font-medium">
<div className="">
{props.message.sender == 'ai' ? (
<UserAvatar
rounded="rounded-lg"
border="border-2"
predefined_avatar="ai"
width={35}
/>
) : (
<UserAvatar rounded="rounded-lg" border="border-2" width={35} />
)}
</div>
<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=""
>
<AnimatePresence>
{words.map((word: string, i: number) => (
<motion.span
key={i}
initial={
props.animated ? { opacity: 0, y: -10 } : { opacity: 1, y: 0 }
}
animate={{ opacity: 1, y: 0 }}
exit={
props.animated ? { opacity: 0, y: 10 } : { opacity: 1, y: 0 }
}
transition={props.animated ? { delay: i * 0.1 } : {}}
>
{word + ' '}
</motion.span>
))}
</AnimatePresence>
</p>
</div>
</div>
)
}
const AIMessagePlaceHolder = (props: {
activity_uuid: string
sendMessage: any
}) => {
const session = useSession() as any
const [feedbackModal, setFeedbackModal] = React.useState(false)
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
if (!aiChatBotState.error.isError) {
return (
<div className='flex space-x-2 w-full antialiased font-medium'>
<div className=''>
{props.message.sender == 'ai' ? (
<UserAvatar rounded='rounded-lg' border='border-2' predefined_avatar='ai' width={35} />
) : (
<UserAvatar rounded='rounded-lg' border='border-2' width={35} />
)}
</div>
<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="">
<AnimatePresence>
{words.map((word: string, i: number) => (
<motion.span
key={i}
initial={props.animated ? { opacity: 0, y: -10 } : { opacity: 1, y: 0 }}
animate={{ opacity: 1, y: 0 }}
exit={props.animated ? { opacity: 0, y: 10 } : { opacity: 1, y: 0 }}
transition={props.animated ? { delay: i * 0.1 } : {}}
>
{word + ' '}
</motion.span>
))}
</AnimatePresence>
</p>
</div>
<div className="flex-col h-[237px] w-full">
<div className="flex flex-col text-center justify-center pt-12">
<motion.div
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
exit={{ y: 50, opacity: 0 }}
transition={{
type: 'spring',
bounce: 0.35,
duration: 1.7,
mass: 0.2,
velocity: 2,
delay: 0.17,
}}
>
<Image
width={100}
className="mx-auto"
src={learnhouseAI_logo_black}
alt=""
/>
<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>
<span>how can we help today ?</span>
</p>
</motion.div>
<motion.div
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
exit={{ y: 50, opacity: 0 }}
transition={{
type: 'spring',
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
sendMessage={props.sendMessage}
label="flashcards"
/>
<AIChatPredefinedQuestion
sendMessage={props.sendMessage}
label="examples"
/>
</motion.div>
</div>
</div>
)
}
}
const AIMessagePlaceHolder = (props: { activity_uuid: string, sendMessage: any }) => {
const session = useSession() as any;
const [feedbackModal, setFeedbackModal] = React.useState(false);
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
if (!aiChatBotState.error.isError) {
return <div className='flex-col h-[237px] w-full'>
<div className='flex flex-col text-center justify-center pt-12'>
<motion.div
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
exit={{ y: 50, opacity: 0, }}
transition={{ type: "spring", bounce: 0.35, duration: 1.7, mass: 0.2, velocity: 2, delay: 0.17 }}
>
<Image width={100} className='mx-auto' src={learnhouseAI_logo_black} alt="" />
<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>
<span>how can we help today ?</span>
</p>
</motion.div>
<motion.div
initial={{ y: 20, opacity: 0, filter: 'blur(5px)' }}
animate={{ y: 0, opacity: 1, filter: 'blur(0px)' }}
exit={{ y: 50, opacity: 0, }}
transition={{ type: "spring", 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 sendMessage={props.sendMessage} label='flashcards' />
<AIChatPredefinedQuestion sendMessage={props.sendMessage} label='examples' />
</motion.div>
</div>
</div>
const AIChatPredefinedQuestion = (props: {
sendMessage: any
label: string
}) => {
function getQuestion(label: string) {
if (label === 'about') {
return `What is this Activity about ?`
} else if (label === 'flashcards') {
return `Generate flashcards about this Activity`
} else if (label === 'examples') {
return `Explain this Activity in practical examples`
}
}
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"
>
{props.label === 'about' && <BadgeInfo size={15} />}
{props.label === 'flashcards' && <NotebookTabs size={15} />}
{props.label === 'examples' && <div className="text-white/50">Ex</div>}
<span>{getQuestion(props.label)}</span>
</div>
)
}
const AIChatPredefinedQuestion = (props: { sendMessage: any, label: string }) => {
function getQuestion(label: string) {
if (label === 'about') {
return `What is this Activity about ?`
} else if (label === 'flashcards') {
return `Generate flashcards about this Activity`
} else if (label === 'examples') {
return `Explain this Activity in practical examples`
}
}
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'>
{props.label === 'about' && <BadgeInfo size={15} />}
{props.label === 'flashcards' && <NotebookTabs size={15} />}
{props.label === 'examples' && <div className='text-white/50'>Ex</div>}
<span>{getQuestion(props.label)}</span>
</div>
)
}
export default AIActivityAsk
export default AIActivityAsk

View file

@ -1,24 +1,34 @@
import { useOrg } from "@components/Contexts/OrgContext";
import { getActivityMediaDirectory } from "@services/media/media";
import React from "react";
import { useOrg } from '@components/Contexts/OrgContext'
import { getActivityMediaDirectory } from '@services/media/media'
import React from 'react'
function DocumentPdfActivity({ activity, course }: { activity: any; course: any }) {
const org = useOrg() as any;
function DocumentPdfActivity({
activity,
course,
}: {
activity: any
course: any
}) {
const org = useOrg() as any
React.useEffect(() => {
console.log(activity);
}, [activity, org]);
console.log(activity)
}, [activity, org])
return (
<div className="m-8 bg-zinc-900 rounded-md mt-14">
<iframe
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>
);
)
}
export default DocumentPdfActivity;
export default DocumentPdfActivity

View file

@ -1,131 +1,220 @@
import React from 'react'
import { Editor } from '@tiptap/core';
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
import Image from 'next/image';
import { BookOpen, FormInput, Languages, MoreVertical } from 'lucide-react';
import { BubbleMenu } from '@tiptap/react';
import ToolTip from '@components/StyledElements/Tooltip/Tooltip';
import { AIChatBotStateTypes, useAIChatBot, useAIChatBotDispatch } from '@components/Contexts/AI/AIChatBotContext';
import { sendActivityAIChatMessage, startActivityAIChatSession } from '@services/ai/ai';
import useGetAIFeatures from '../../../../AI/Hooks/useGetAIFeatures';
import { Editor } from '@tiptap/core'
import learnhouseAI_icon from 'public/learnhouse_ai_simple.png'
import Image from 'next/image'
import { BookOpen, FormInput, Languages, MoreVertical } from 'lucide-react'
import { BubbleMenu } from '@tiptap/react'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import {
AIChatBotStateTypes,
useAIChatBot,
useAIChatBotDispatch,
} from '@components/Contexts/AI/AIChatBotContext'
import {
sendActivityAIChatMessage,
startActivityAIChatSession,
} from '@services/ai/ai'
import useGetAIFeatures from '../../../../AI/Hooks/useGetAIFeatures'
type AICanvaToolkitProps = {
editor: Editor,
activity: any
editor: Editor
activity: any
}
function AICanvaToolkit(props: AICanvaToolkitProps) {
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' });
const [isBubbleMenuAvailable, setIsButtonAvailable] = React.useState(false);
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'activity_ask' })
const [isBubbleMenuAvailable, setIsButtonAvailable] = React.useState(false)
React.useEffect(() => {
if (is_ai_feature_enabled) {
setIsButtonAvailable(true);
}
}, [is_ai_feature_enabled])
React.useEffect(() => {
if (is_ai_feature_enabled) {
setIsButtonAvailable(true)
}
}, [is_ai_feature_enabled])
return (
<>
{isBubbleMenuAvailable && <BubbleMenu className="w-fit" tippyOptions={{ duration: 100 }} editor={props.editor}>
<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>
<MoreVertical className='text-white/50' size={12} />
</div>
<div className='flex space-x-2'>
<AIActionButton editor={props.editor} activity={props.activity} 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>
</BubbleMenu>}
</>
)
return (
<>
{isBubbleMenuAvailable && (
<BubbleMenu
className="w-fit"
tippyOptions={{ duration: 100 }}
editor={props.editor}
>
<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>
<MoreVertical className="text-white/50" size={12} />
</div>
<div className="flex space-x-2">
<AIActionButton
editor={props.editor}
activity={props.activity}
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>
</BubbleMenu>
)}
</>
)
}
function AIActionButton(props: { editor: Editor, label: string, activity: any }) {
const dispatchAIChatBot = useAIChatBotDispatch() as any;
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes;
function AIActionButton(props: {
editor: Editor
label: string
activity: any
}) {
const dispatchAIChatBot = useAIChatBotDispatch() as any
const aiChatBotState = useAIChatBot() as AIChatBotStateTypes
async function handleAction(label: string) {
const selection = getTipTapEditorSelectedText();
const prompt = getPrompt(label, selection);
dispatchAIChatBot({ type: 'setIsModalOpen' });
await sendMessage(prompt);
async function handleAction(label: string) {
const selection = getTipTapEditorSelectedText()
const prompt = getPrompt(label, selection)
dispatchAIChatBot({ type: 'setIsModalOpen' })
await sendMessage(prompt)
}
const getTipTapEditorSelectedText = () => {
const selection = props.editor.state.selection
const from = selection.from
const to = selection.to
const text = props.editor.state.doc.textBetween(from, to)
return text
}
const getPrompt = (label: string, selection: string) => {
if (label === 'Explain') {
return `Explain this part of the course "${selection}" keep this course context in mind.`
} else if (label === 'Summarize') {
return `Summarize this "${selection}" with the course context in mind.`
} else if (label === 'Translate') {
return `Translate "${selection}" to another language.`
} else {
return `Give examples to understand "${selection}" better, if possible give context in the course.`
}
}
const getTipTapEditorSelectedText = () => {
const selection = props.editor.state.selection;
const from = selection.from;
const to = selection.to;
const text = props.editor.state.doc.textBetween(from, to);
return text;
const sendMessage = async (message: string) => {
if (aiChatBotState.aichat_uuid) {
await dispatchAIChatBot({
type: 'addMessage',
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) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
await dispatchAIChatBot({
type: 'setError',
payload: {
isError: true,
status: response.status,
error_message: response.data.detail,
},
})
return
}
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
await dispatchAIChatBot({
type: 'addMessage',
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
})
} else {
await dispatchAIChatBot({
type: 'addMessage',
payload: { sender: 'user', message: message, type: 'user' },
})
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' })
const response = await startActivityAIChatSession(
message,
props.activity.activity_uuid
)
if (response.success == false) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' })
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
await dispatchAIChatBot({
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({ type: 'setIsNoLongerWaitingForResponse' })
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' })
await dispatchAIChatBot({
type: 'addMessage',
payload: { sender: 'ai', message: response.data.message, type: 'ai' },
})
}
}
const getPrompt = (label: string, selection: string) => {
if (label === 'Explain') {
return `Explain this part of the course "${selection}" keep this course context in mind.`
} else if (label === 'Summarize') {
return `Summarize this "${selection}" with the course context in mind.`
} else if (label === 'Translate') {
return `Translate "${selection}" to another language.`
} else {
return `Give examples to understand "${selection}" better, if possible give context in the course.`
}
}
const sendMessage = async (message: string) => {
if (aiChatBotState.aichat_uuid) {
await dispatchAIChatBot({ type: 'addMessage', 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) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
await dispatchAIChatBot({ type: 'setError', payload: { isError: true, status: response.status, error_message: response.data.detail } });
return;
}
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'ai', message: response.data.message, type: 'ai' } });
} else {
await dispatchAIChatBot({ type: 'addMessage', payload: { sender: 'user', message: message, type: 'user' } });
await dispatchAIChatBot({ type: 'setIsWaitingForResponse' });
const response = await startActivityAIChatSession(message, props.activity.activity_uuid)
if (response.success == false) {
await dispatchAIChatBot({ type: 'setIsNoLongerWaitingForResponse' });
await dispatchAIChatBot({ type: 'setChatInputValue', payload: '' });
await dispatchAIChatBot({ 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({ 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'
return (
<div className='flex space-x-2' >
<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'>
{props.label === 'Explain' && <BookOpen size={16} />}
{props.label === 'Summarize' && <FormInput size={16} />}
{props.label === 'Translate' && <Languages size={16} />}
{props.label === 'Examples' && <div className='text-white/50'>Ex</div>}
<div>{props.label}</div>
</button>
</ToolTip>
</div>
)
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 (
<div className="flex space-x-2">
<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"
>
{props.label === 'Explain' && <BookOpen size={16} />}
{props.label === 'Summarize' && <FormInput size={16} />}
{props.label === 'Translate' && <Languages size={16} />}
{props.label === 'Examples' && (
<div className="text-white/50">Ex</div>
)}
<div>{props.label}</div>
</button>
</ToolTip>
</div>
)
}
export default AICanvaToolkit
export default AICanvaToolkit

View file

@ -1,45 +1,43 @@
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import styled from "styled-components"
import Youtube from "@tiptap/extension-youtube";
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import styled from 'styled-components'
import Youtube from '@tiptap/extension-youtube'
// Custom Extensions
import InfoCallout from "@components/Objects/Editor/Extensions/Callout/Info/InfoCallout";
import WarningCallout from "@components/Objects/Editor/Extensions/Callout/Warning/WarningCallout";
import ImageBlock from "@components/Objects/Editor/Extensions/Image/ImageBlock";
import VideoBlock from "@components/Objects/Editor/Extensions/Video/VideoBlock";
import MathEquationBlock from "@components/Objects/Editor/Extensions/MathEquation/MathEquationBlock";
import PDFBlock from "@components/Objects/Editor/Extensions/PDF/PDFBlock";
import { OrderedList } from "@tiptap/extension-ordered-list";
import QuizBlock from "@components/Objects/Editor/Extensions/Quiz/QuizBlock";
import InfoCallout from '@components/Objects/Editor/Extensions/Callout/Info/InfoCallout'
import WarningCallout from '@components/Objects/Editor/Extensions/Callout/Warning/WarningCallout'
import ImageBlock from '@components/Objects/Editor/Extensions/Image/ImageBlock'
import VideoBlock from '@components/Objects/Editor/Extensions/Video/VideoBlock'
import MathEquationBlock from '@components/Objects/Editor/Extensions/MathEquation/MathEquationBlock'
import PDFBlock from '@components/Objects/Editor/Extensions/PDF/PDFBlock'
import { OrderedList } from '@tiptap/extension-ordered-list'
import QuizBlock from '@components/Objects/Editor/Extensions/Quiz/QuizBlock'
// Lowlight
import { common, createLowlight } from 'lowlight'
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 js from 'highlight.js/lib/languages/javascript'
import ts from 'highlight.js/lib/languages/typescript'
import html from 'highlight.js/lib/languages/xml'
import python from 'highlight.js/lib/languages/python'
import java from 'highlight.js/lib/languages/java'
import { NoTextInput } from "@components/Objects/Editor/Extensions/NoTextInput/NoTextInput";
import EditorOptionsProvider from "@components/Contexts/Editor/EditorContext";
import AICanvaToolkit from "./AI/AICanvaToolkit";
import { NoTextInput } from '@components/Objects/Editor/Extensions/NoTextInput/NoTextInput'
import EditorOptionsProvider from '@components/Contexts/Editor/EditorContext'
import AICanvaToolkit from './AI/AICanvaToolkit'
interface Editor {
content: string;
activity: any;
content: string
activity: any
}
function Canva(props: Editor) {
/**
* Important Note : This is a workaround to enable user interaction features to be implemented easily, like text selection, AI features and other planned features, this is set to true but otherwise it should be set to false.
* 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.
*/
const isEditable = true;
/**
* Important Note : This is a workaround to enable user interaction features to be implemented easily, like text selection, AI features and other planned features, this is set to true but otherwise it should be set to false.
* 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.
*/
const isEditable = true
// Code Block Languages for Lowlight
lowlight.register('html', html)
@ -49,7 +47,6 @@ function Canva(props: Editor) {
lowlight.register('python', python)
lowlight.register('java', java)
const editor: any = useEditor({
editable: isEditable,
extensions: [
@ -90,62 +87,55 @@ function Canva(props: Editor) {
CodeBlockLowlight.configure({
lowlight,
}),
],
content: props.content,
});
})
return (
<EditorOptionsProvider options={{ isEditable: false }}>
<CanvaWrapper>
<AICanvaToolkit activity={props.activity} editor={editor} />
<EditorContent editor={editor} />
</CanvaWrapper>
</EditorOptionsProvider>
);
)
}
const CanvaWrapper = styled.div`
width: 100%;
margin: 0 auto;
.bubble-menu {
display: flex;
background-color: #0D0D0D;
padding: 0.2rem;
border-radius: 0.5rem;
display: flex;
background-color: #0d0d0d;
padding: 0.2rem;
border-radius: 0.5rem;
button {
border: none;
background: none;
color: #FFF;
font-size: 0.85rem;
font-weight: 500;
padding: 0 0.2rem;
opacity: 0.6;
button {
border: none;
background: none;
color: #fff;
font-size: 0.85rem;
font-weight: 500;
padding: 0 0.2rem;
opacity: 0.6;
&:hover,
&.is-active {
opacity: 1;
&:hover,
&.is-active {
opacity: 1;
}
}
}
}
// disable chrome outline
.ProseMirror {
// Workaround to disable editor from being edited by the user.
caret-color: transparent;
h1 {
font-size: 30px;
font-size: 30px;
font-weight: 600;
margin-bottom: 10px;
}
@ -176,13 +166,13 @@ const CanvaWrapper = styled.div`
margin-bottom: 10px;
}
ul, ol {
ul,
ol {
padding: 0 1rem;
padding-left: 20px;
list-style-type: decimal;
}
&:focus {
outline: none !important;
outline-style: none !important;
@ -191,74 +181,72 @@ const CanvaWrapper = styled.div`
// Code Block
pre {
background: #0d0d0d;
border-radius: 0.5rem;
color: #fff;
font-family: "JetBrainsMono", monospace;
padding: 0.75rem 1rem;
background: #0d0d0d;
border-radius: 0.5rem;
color: #fff;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
.hljs-comment,
.hljs-quote {
color: #616161;
}
.hljs-comment,
.hljs-quote {
color: #616161;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f98181;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f98181;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #fbbc88;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #fbbc88;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #b9f18d;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #b9f18d;
}
.hljs-title,
.hljs-section {
color: #faf594;
}
.hljs-title,
.hljs-section {
color: #faf594;
}
.hljs-keyword,
.hljs-selector-tag {
color: #70cff8;
}
.hljs-keyword,
.hljs-selector-tag {
color: #70cff8;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
.hljs-strong {
font-weight: 700;
}
}
}
}
`
`;
export default Canva;
export default Canva

View file

@ -1,68 +1,70 @@
import React from "react";
import YouTube from 'react-youtube';
import { getActivityMediaDirectory } from "@services/media/media";
import { useOrg } from "@components/Contexts/OrgContext";
import React from 'react'
import YouTube from 'react-youtube'
import { getActivityMediaDirectory } from '@services/media/media'
import { useOrg } from '@components/Contexts/OrgContext'
function VideoActivity({ activity, course }: { activity: any; course: any }) {
const org = useOrg() as any;
const [videoId, setVideoId] = React.useState('');
const org = useOrg() as any
const [videoId, setVideoId] = React.useState('')
function getYouTubeEmbed(url: any) {
// 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
var embedObject = {
videoId: videoId,
width: 560,
height: 315
};
height: 315,
}
return embedObject;
return embedObject
}
React.useEffect(() => {
console.log(activity);
}, [activity, org]);
console.log(activity)
}, [activity, org])
return (
<div>
{activity &&
{activity && (
<>
{activity.activity_sub_type === 'SUBTYPE_VIDEO_HOSTED' && (
<div className="m-8 bg-zinc-900 rounded-md mt-14">
<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
className="rounded-lg w-full h-[500px]"
controls
src={getActivityMediaDirectory(
org?.org_uuid,
course?.course_uuid,
activity.activity_uuid,
activity.content?.filename,
'video'
)}
></video>
</div>
)}
{activity.activity_sub_type === 'SUBTYPE_VIDEO_YOUTUBE' && (
<div>
<YouTube
className="rounded-md overflow-hidden m-8 bg-zinc-900 mt-14"
opts={
{
width: '1300',
height: '500',
playerVars: {
autoplay: 0,
},
}
}
videoId={videoId} />
opts={{
width: '1300',
height: '500',
playerVars: {
autoplay: 0,
},
}}
videoId={videoId}
/>
</div>
)}</>}
)}
</>
)}
</div>
);
)
}
export default VideoActivity;
export default VideoActivity

File diff suppressed because it is too large Load diff

View file

@ -1,77 +1,79 @@
'use client';
import React from "react";
import { useEditor, EditorContent } from "@tiptap/react";
import StarterKit from "@tiptap/starter-kit";
import learnhouseIcon from "public/learnhouse_icon.png";
import { ToolbarButtons } from "./Toolbar/ToolbarButtons";
import { motion } from "framer-motion";
import Image from "next/image";
import styled from "styled-components";
import { DividerVerticalIcon, SlashIcon } from "@radix-ui/react-icons";
import learnhouseAI_icon from "public/learnhouse_ai_simple.png";
import { AIEditorStateTypes, useAIEditor, useAIEditorDispatch } from "@components/Contexts/AI/AIEditorContext";
'use client'
import React from 'react'
import { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
import learnhouseIcon from 'public/learnhouse_icon.png'
import { ToolbarButtons } from './Toolbar/ToolbarButtons'
import { motion } from 'framer-motion'
import Image from 'next/image'
import styled from 'styled-components'
import { DividerVerticalIcon, SlashIcon } from '@radix-ui/react-icons'
import learnhouseAI_icon from 'public/learnhouse_ai_simple.png'
import {
AIEditorStateTypes,
useAIEditor,
useAIEditorDispatch,
} from '@components/Contexts/AI/AIEditorContext'
// extensions
import InfoCallout from "./Extensions/Callout/Info/InfoCallout";
import WarningCallout from "./Extensions/Callout/Warning/WarningCallout";
import ImageBlock from "./Extensions/Image/ImageBlock";
import Youtube from "@tiptap/extension-youtube";
import VideoBlock from "./Extensions/Video/VideoBlock";
import { Eye } from "lucide-react";
import MathEquationBlock from "./Extensions/MathEquation/MathEquationBlock";
import PDFBlock from "./Extensions/PDF/PDFBlock";
import QuizBlock from "./Extensions/Quiz/QuizBlock";
import ToolTip from "@components/StyledElements/Tooltip/Tooltip";
import Link from "next/link";
import { getCourseThumbnailMediaDirectory } from "@services/media/media";
import { OrderedList } from "@tiptap/extension-ordered-list";
import InfoCallout from './Extensions/Callout/Info/InfoCallout'
import WarningCallout from './Extensions/Callout/Warning/WarningCallout'
import ImageBlock from './Extensions/Image/ImageBlock'
import Youtube from '@tiptap/extension-youtube'
import VideoBlock from './Extensions/Video/VideoBlock'
import { Eye } from 'lucide-react'
import MathEquationBlock from './Extensions/MathEquation/MathEquationBlock'
import PDFBlock from './Extensions/PDF/PDFBlock'
import QuizBlock from './Extensions/Quiz/QuizBlock'
import ToolTip from '@components/StyledElements/Tooltip/Tooltip'
import Link from 'next/link'
import { getCourseThumbnailMediaDirectory } from '@services/media/media'
import { OrderedList } from '@tiptap/extension-ordered-list'
// Lowlight
import { common, createLowlight } from 'lowlight'
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 js from 'highlight.js/lib/languages/javascript'
import ts from 'highlight.js/lib/languages/typescript'
import html from 'highlight.js/lib/languages/xml'
import python from 'highlight.js/lib/languages/python'
import java from 'highlight.js/lib/languages/java'
import { CourseProvider } from "@components/Contexts/CourseContext";
import { useSession } from "@components/Contexts/SessionContext";
import AIEditorToolkit from "./AI/AIEditorToolkit";
import useGetAIFeatures from "@components/AI/Hooks/useGetAIFeatures";
import UserAvatar from "../UserAvatar";
import { CourseProvider } from '@components/Contexts/CourseContext'
import { useSession } from '@components/Contexts/SessionContext'
import AIEditorToolkit from './AI/AIEditorToolkit'
import useGetAIFeatures from '@components/AI/Hooks/useGetAIFeatures'
import UserAvatar from '../UserAvatar'
interface Editor {
content: string;
ydoc: any;
provider: any;
activity: any;
course: any;
org: any;
setContent: (content: string) => void;
content: string
ydoc: any
provider: any
activity: any
course: any
org: any
setContent: (content: string) => void
}
function Editor(props: Editor) {
const session = useSession() as any;
const dispatchAIEditor = useAIEditorDispatch() as any;
const aiEditorState = useAIEditor() as AIEditorStateTypes;
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' });
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false);
const session = useSession() as any
const dispatchAIEditor = useAIEditorDispatch() as any
const aiEditorState = useAIEditor() as AIEditorStateTypes
const is_ai_feature_enabled = useGetAIFeatures({ feature: 'editor' })
const [isButtonAvailable, setIsButtonAvailable] = React.useState(false)
React.useEffect(() => {
if (is_ai_feature_enabled) {
setIsButtonAvailable(true);
setIsButtonAvailable(true)
}
}, [is_ai_feature_enabled])
// 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
const activity_uuid = props.activity.activity_uuid.substring(9);
const activity_uuid = props.activity.activity_uuid.substring(9)
// Code Block Languages for Lowlight
lowlight.register('html', html)
@ -124,7 +126,6 @@ function Editor(props: Editor) {
lowlight,
}),
// Register the document with Tiptap
// Collaboration.configure({
// document: props.ydoc,
@ -140,98 +141,153 @@ function Editor(props: Editor) {
],
content: props.content,
});
})
return (
<Page>
<CourseProvider courseuuid={props.course.course_uuid}>
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
key="modal"
transition={{
type: "spring",
stiffness: 360,
damping: 70,
delay: 0.02,
}}
exit={{ opacity: 0 }}
>
<EditorTop className="fixed bg-white bg-opacity-95 backdrop-blur backdrop-brightness-125">
<EditorDocSection>
<EditorInfoWrapper>
<Link href="/">
<EditorInfoLearnHouseLogo width={25} height={25} src={learnhouseIcon} alt="" />
</Link>
<Link target="_blank" href={`/course/${course_uuid}`}>
<EditorInfoThumbnail src={`${getCourseThumbnailMediaDirectory(props.org?.org_uuid, props.course.course_uuid, props.course.thumbnail_image)}`} alt=""></EditorInfoThumbnail>
</Link>
<EditorInfoDocName>
{" "}
<b>{props.course.name}</b> <SlashIcon /> {props.activity.name}{" "}
</EditorInfoDocName>
</EditorInfoWrapper>
<EditorButtonsWrapper>
<ToolbarButtons editor={editor} />
</EditorButtonsWrapper>
</EditorDocSection>
<EditorUsersSection className="space-x-2">
<div>
<div className="transition-all ease-linear text-teal-100 rounded-md hover:cursor-pointer" >
{isButtonAvailable && <div
onClick={() => dispatchAIEditor({ type: aiEditorState.isModalOpen ? 'setIsModalClose' : 'setIsModalOpen' })}
<CourseProvider courseuuid={props.course.course_uuid}>
<motion.div
initial={{ opacity: 0, scale: 0.98 }}
animate={{ opacity: 1, scale: 1 }}
key="modal"
transition={{
type: 'spring',
stiffness: 360,
damping: 70,
delay: 0.02,
}}
exit={{ opacity: 0 }}
>
<EditorTop className="fixed bg-white bg-opacity-95 backdrop-blur backdrop-brightness-125">
<EditorDocSection>
<EditorInfoWrapper>
<Link href="/">
<EditorInfoLearnHouseLogo
width={25}
height={25}
src={learnhouseIcon}
alt=""
/>
</Link>
<Link target="_blank" href={`/course/${course_uuid}`}>
<EditorInfoThumbnail
src={`${getCourseThumbnailMediaDirectory(
props.org?.org_uuid,
props.course.course_uuid,
props.course.thumbnail_image
)}`}
alt=""
></EditorInfoThumbnail>
</Link>
<EditorInfoDocName>
{' '}
<b>{props.course.name}</b> <SlashIcon /> {props.activity.name}{' '}
</EditorInfoDocName>
</EditorInfoWrapper>
<EditorButtonsWrapper>
<ToolbarButtons editor={editor} />
</EditorButtonsWrapper>
</EditorDocSection>
<EditorUsersSection className="space-x-2">
<div>
<div className="transition-all ease-linear text-teal-100 rounded-md hover:cursor-pointer">
{isButtonAvailable && (
<div
onClick={() =>
dispatchAIEditor({
type: aiEditorState.isModalOpen
? 'setIsModalClose'
: 'setIsModalOpen',
})
}
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>
<Image className='' width={20} src={learnhouseAI_icon} alt="" />
</i>{" "}
<Image
className=""
width={20}
src={learnhouseAI_icon}
alt=""
/>
</i>{' '}
<i className="not-italic text-xs font-bold">AI Editor</i>
</div>}
</div>
</div>
)}
</div>
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
<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>
<ToolTip content="Preview">
<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">
<Eye className="mx-auto items-center" size={15} />
</div>
</Link>
</ToolTip>
</EditorLeftOptionsSection>
<DividerVerticalIcon style={{ marginTop: "auto", marginBottom: "auto", color: "grey", opacity: '0.5' }} />
</div>
<DividerVerticalIcon
style={{
marginTop: 'auto',
marginBottom: 'auto',
color: 'grey',
opacity: '0.5',
}}
/>
<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>
<ToolTip content="Preview">
<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">
<Eye className="mx-auto items-center" size={15} />
</div>
</Link>
</ToolTip>
</EditorLeftOptionsSection>
<DividerVerticalIcon
style={{
marginTop: 'auto',
marginBottom: 'auto',
color: 'grey',
opacity: '0.5',
}}
/>
<EditorUserProfileWrapper>
{!session.isAuthenticated && <span>Loading</span>}
{session.isAuthenticated && <UserAvatar width={40} border="border-4" rounded="rounded-full"/>}
</EditorUserProfileWrapper>
</EditorUsersSection>
</EditorTop>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.99 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
type: "spring",
stiffness: 360,
damping: 70,
delay: 0.5,
}}
exit={{ opacity: 0 }}
>
<EditorContentWrapper>
<AIEditorToolkit activity={props.activity} editor={editor} />
<EditorContent editor={editor} />
</EditorContentWrapper>
</motion.div>
</CourseProvider>
<EditorUserProfileWrapper>
{!session.isAuthenticated && <span>Loading</span>}
{session.isAuthenticated && (
<UserAvatar
width={40}
border="border-4"
rounded="rounded-full"
/>
)}
</EditorUserProfileWrapper>
</EditorUsersSection>
</EditorTop>
</motion.div>
<motion.div
initial={{ opacity: 0, scale: 0.99 }}
animate={{ opacity: 1, scale: 1 }}
transition={{
type: 'spring',
stiffness: 360,
damping: 70,
delay: 0.5,
}}
exit={{ opacity: 0 }}
>
<EditorContentWrapper>
<AIEditorToolkit activity={props.activity} editor={editor} />
<EditorContent editor={editor} />
</EditorContentWrapper>
</motion.div>
</CourseProvider>
</Page>
);
)
}
const Page = styled.div`
@ -240,12 +296,15 @@ const Page = styled.div`
padding-top: 30px;
// dots background
background-image: radial-gradient(#4744446b 1px, transparent 1px), radial-gradient(#4744446b 1px, transparent 1px);
background-position: 0 0, 25px 25px;
background-image: radial-gradient(#4744446b 1px, transparent 1px),
radial-gradient(#4744446b 1px, transparent 1px);
background-position:
0 0,
25px 25px;
background-size: 50px 50px;
background-attachment: fixed;
background-repeat: repeat;
`;
`
const EditorTop = styled.div`
border-radius: 15px;
@ -259,35 +318,34 @@ const EditorTop = styled.div`
position: fixed;
z-index: 303;
width: -webkit-fill-available;
`;
`
// Inside EditorTop
const EditorDocSection = styled.div`
display: flex;
flex-direction: column;
`;
`
const EditorUsersSection = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
`;
`
const EditorLeftOptionsSection = styled.div`
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
`;
`
// Inside EditorDocSection
const EditorInfoWrapper = styled.div`
display: flex;
flex-direction: row;
margin-bottom: 5px;
`;
const EditorButtonsWrapper = styled.div``;
`
const EditorButtonsWrapper = styled.div``
// Inside EditorUsersSection
const EditorUserProfileWrapper = styled.div`
@ -295,14 +353,14 @@ const EditorUserProfileWrapper = styled.div`
svg {
border-radius: 7px;
}
`;
`
// Inside EditorInfoWrapper
//..todo
const EditorInfoLearnHouseLogo = styled(Image)`
border-radius: 6px;
margin-right: 0px;
`;
`
const EditorInfoDocName = styled.div`
font-size: 16px;
justify-content: center;
@ -317,8 +375,7 @@ const EditorInfoDocName = styled.div`
padding: 3px;
color: #353535;
}
`;
`
const EditorInfoThumbnail = styled.img`
height: 25px;
@ -331,7 +388,7 @@ const EditorInfoThumbnail = styled.img`
&:hover {
cursor: pointer;
}
`;
`
export const EditorContentWrapper = styled.div`
margin: 40px;
@ -344,9 +401,8 @@ export const EditorContentWrapper = styled.div`
// disable chrome outline
.ProseMirror {
h1 {
font-size: 30px;
font-size: 30px;
font-weight: 600;
margin-top: 10px;
margin-bottom: 10px;
@ -393,72 +449,71 @@ export const EditorContentWrapper = styled.div`
// Code Block
pre {
background: #0d0d0d;
border-radius: 0.5rem;
color: #fff;
font-family: "JetBrainsMono", monospace;
padding: 0.75rem 1rem;
background: #0d0d0d;
border-radius: 0.5rem;
color: #fff;
font-family: 'JetBrainsMono', monospace;
padding: 0.75rem 1rem;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
code {
background: none;
color: inherit;
font-size: 0.8rem;
padding: 0;
}
.hljs-comment,
.hljs-quote {
color: #616161;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f98181;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #fbbc88;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #b9f18d;
}
.hljs-title,
.hljs-section {
color: #faf594;
}
.hljs-keyword,
.hljs-selector-tag {
color: #70cff8;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}
.hljs-comment,
.hljs-quote {
color: #616161;
}
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name,
.hljs-regexp,
.hljs-link,
.hljs-name,
.hljs-selector-id,
.hljs-selector-class {
color: #f98181;
}
.hljs-number,
.hljs-meta,
.hljs-built_in,
.hljs-builtin-name,
.hljs-literal,
.hljs-type,
.hljs-params {
color: #fbbc88;
}
.hljs-string,
.hljs-symbol,
.hljs-bullet {
color: #b9f18d;
}
.hljs-title,
.hljs-section {
color: #faf594;
}
.hljs-keyword,
.hljs-selector-tag {
color: #70cff8;
}
.hljs-emphasis {
font-style: italic;
}
.hljs-strong {
font-weight: 700;
}
}
}
iframe {
@ -472,15 +527,12 @@ export const EditorContentWrapper = styled.div`
outline: 0px solid transparent;
}
ul, ol {
ul,
ol {
padding: 0 1rem;
padding-left: 20px;
list-style-type: decimal;
}
`
`;
export default Editor;
export default Editor

View file

@ -1,61 +1,66 @@
'use client';
import { default as React, } from "react";
import * as Y from "yjs";
import Editor from "./Editor";
import { updateActivity } from "@services/courses/activities";
import { toast } from "react-hot-toast";
import Toast from "@components/StyledElements/Toast/Toast";
import { OrgProvider } from "@components/Contexts/OrgContext";
'use client'
import { default as React } from 'react'
import * as Y from 'yjs'
import Editor from './Editor'
import { updateActivity } from '@services/courses/activities'
import { toast } from 'react-hot-toast'
import Toast from '@components/StyledElements/Toast/Toast'
import { OrgProvider } from '@components/Contexts/OrgContext'
interface EditorWrapperProps {
content: string;
activity: any;
content: string
activity: any
course: any
org: any;
org: any
}
function EditorWrapper(props: EditorWrapperProps): JSX.Element {
// A new Y document
const ydoc = new Y.Doc();
const [providerState, setProviderState] = React.useState<any>({});
const [ydocState, setYdocState] = React.useState<any>({});
const [isLoading, setIsLoading] = React.useState(true);
const ydoc = new Y.Doc()
const [providerState, setProviderState] = React.useState<any>({})
const [ydocState, setYdocState] = React.useState<any>({})
const [isLoading, setIsLoading] = React.useState(true)
function createRTCProvider() {
// const provider = new WebrtcProvider(props.activity.activity_id, ydoc);
// setYdocState(ydoc);
// setProviderState(provider);
setIsLoading(false);
setIsLoading(false)
}
async function setContent(content: any) {
let activity = props.activity;
activity.content = content;
let activity = props.activity
activity.content = content
toast.promise(
updateActivity(activity, activity.activity_uuid),
{
loading: 'Saving...',
success: <b>Activity saved!</b>,
error: <b>Could not save.</b>,
}
);
toast.promise(updateActivity(activity, activity.activity_uuid), {
loading: 'Saving...',
success: <b>Activity saved!</b>,
error: <b>Could not save.</b>,
})
}
if (isLoading) {
createRTCProvider();
return <div>Loading...</div>;
createRTCProvider()
return <div>Loading...</div>
} else {
return <>
<Toast></Toast>
<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>;
</OrgProvider>
</>
return (
<>
<Toast></Toast>
<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>
;
</OrgProvider>
</>
)
}
}
export default EditorWrapper;
export default EditorWrapper

View file

@ -1,29 +1,29 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import InfoCalloutComponent from "./InfoCalloutComponent";
import InfoCalloutComponent from './InfoCalloutComponent'
export default Node.create({
name: "calloutInfo",
group: "block",
name: 'calloutInfo',
group: 'block',
draggable: true,
content: "text*",
content: 'text*',
// TODO : multi line support
parseHTML() {
return [
{
tag: "callout-info",
tag: 'callout-info',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
return ['callout-info', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(InfoCalloutComponent);
return ReactNodeViewRenderer(InfoCalloutComponent)
},
});
})

View file

@ -1,35 +1,38 @@
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
import { AlertCircle } from "lucide-react";
import React from "react";
import styled from "styled-components";
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
import { AlertCircle } from 'lucide-react'
import React from 'react'
import styled from 'styled-components'
function InfoCalloutComponent(props: any) {
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
return (
<NodeViewWrapper>
<InfoCalloutWrapper 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
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>
</NodeViewWrapper>
);
)
}
const InfoCalloutWrapper = styled.div`
svg{
svg {
padding: 3px;
}
.content {
margin: 5px;
padding: 0.5rem;
border: ${(props) => (props.contentEditable ? "2px dashed #1f3a8a12" : "none")};
border: ${(props) =>
props.contentEditable ? '2px dashed #1f3a8a12' : 'none'};
border-radius: 0.5rem;
}
`;
`
export default InfoCalloutComponent;
export default InfoCalloutComponent

View file

@ -1,29 +1,29 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import WarningCalloutComponent from "./WarningCalloutComponent";
import WarningCalloutComponent from './WarningCalloutComponent'
export default Node.create({
name: "calloutWarning",
group: "block",
name: 'calloutWarning',
group: 'block',
draggable: true,
content: "text*",
content: 'text*',
// TODO : multi line support
parseHTML() {
return [
{
tag: "callout-warning",
tag: 'callout-warning',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["callout-info", mergeAttributes(HTMLAttributes), 0];
return ['callout-info', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(WarningCalloutComponent);
return ReactNodeViewRenderer(WarningCalloutComponent)
},
});
})

View file

@ -1,25 +1,27 @@
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import { NodeViewContent, NodeViewWrapper } from "@tiptap/react";
import { AlertTriangle } from "lucide-react";
import React from "react";
import styled from "styled-components";
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
import { NodeViewContent, NodeViewWrapper } from '@tiptap/react'
import { AlertTriangle } from 'lucide-react'
import React from 'react'
import styled from 'styled-components'
function WarningCalloutComponent(props: any) {
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
return (
<NodeViewWrapper>
<CalloutWrapper 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
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>
</NodeViewWrapper>
);
)
}
const CalloutWrapper = styled.div`
svg {
padding: 3px;
}
@ -27,10 +29,11 @@ const CalloutWrapper = styled.div`
.content {
margin: 5px;
padding: 0.5rem;
border: ${(props) => (props.contentEditable ? "2px dashed #713f1117" : "none")};
border: ${(props) =>
props.contentEditable ? '2px dashed #713f1117' : 'none'};
border-radius: 0.5rem;
}
`;
`
const DragHandle = styled.div`
position: absolute;
@ -40,6 +43,6 @@ const DragHandle = styled.div`
height: 100%;
cursor: move;
z-index: 1;
`;
`
export default WarningCalloutComponent;
export default WarningCalloutComponent

View file

@ -1,11 +1,11 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import ImageBlockComponent from "./ImageBlockComponent";
import ImageBlockComponent from './ImageBlockComponent'
export default Node.create({
name: "blockImage",
group: "block",
name: 'blockImage',
group: 'block',
atom: true,
@ -17,22 +17,22 @@ export default Node.create({
size: {
width: 300,
},
};
}
},
parseHTML() {
return [
{
tag: "block-image",
tag: 'block-image',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["block-image", mergeAttributes(HTMLAttributes), 0];
return ['block-image', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(ImageBlockComponent);
return ReactNodeViewRenderer(ImageBlockComponent)
},
});
})

View file

@ -1,95 +1,136 @@
import { NodeViewWrapper } from "@tiptap/react";
import React, { useEffect } from "react";
import styled from "styled-components";
import { Resizable } from 're-resizable';
import { AlertTriangle, Image, Loader } from "lucide-react";
import { uploadNewImageFile } from "../../../../../services/blocks/Image/images";
import { UploadIcon } from "@radix-ui/react-icons";
import { getActivityBlockMediaDirectory } from "@services/media/media";
import { useOrg } from "@components/Contexts/OrgContext";
import { useCourse } from "@components/Contexts/CourseContext";
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import { NodeViewWrapper } from '@tiptap/react'
import React, { useEffect } from 'react'
import styled from 'styled-components'
import { Resizable } from 're-resizable'
import { AlertTriangle, Image, Loader } from 'lucide-react'
import { uploadNewImageFile } from '../../../../../services/blocks/Image/images'
import { UploadIcon } from '@radix-ui/react-icons'
import { getActivityBlockMediaDirectory } from '@services/media/media'
import { useOrg } from '@components/Contexts/OrgContext'
import { useCourse } from '@components/Contexts/CourseContext'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
function ImageBlockComponent(props: any) {
const org = useOrg() as any;
const course = useCourse() as any;
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const [image, setImage] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
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 org = useOrg() as any
const course = useCourse() as any
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
const [image, setImage] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(false)
const [blockObject, setblockObject] = React.useState(
props.node.attrs.blockObject
)
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>) => {
setImage(event.target.files[0]);
};
setImage(event.target.files[0])
}
const handleSubmit = async (e: any) => {
e.preventDefault();
setIsLoading(true);
let object = await uploadNewImageFile(image, props.extension.options.activity.activity_uuid);
setIsLoading(false);
setblockObject(object);
e.preventDefault()
setIsLoading(true)
let object = await uploadNewImageFile(
image,
props.extension.options.activity.activity_uuid
)
setIsLoading(false)
setblockObject(object)
props.updateAttributes({
blockObject: object,
size: imageSize,
});
};
useEffect(() => {
})
}
, [course, org]);
useEffect(() => {}, [course, org])
return (
<NodeViewWrapper className="block-image">
{!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 ? (
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
<Loader
className="animate-spin animate-pulse text-gray-200"
size={50}
/>
) : (
<>
<div>
<Image className="text-gray-200" size={50} />
</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="" />
<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>
<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=""
/>
<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>
)}
{blockObject && (
<Resizable defaultSize={{ width: imageSize.width, height: "100%" }}
<Resizable
defaultSize={{ width: imageSize.width, height: '100%' }}
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}
minWidth={200}
onResizeStop={(e, direction, ref, d) => {
props.updateAttributes({
size: {
width: imageSize.width + d.width,
}
});
},
})
setImageSize({
width: imageSize.width + d.width,
});
})
}}
>
<img
src={`${getActivityBlockMediaDirectory(org?.org_uuid,
src={`${getActivityBlockMediaDirectory(
org?.org_uuid,
course?.courseStructure.course_uuid,
props.extension.options.activity.activity_uuid,
blockObject.block_uuid,
blockObject ? fileId : ' ', 'imageBlock')}`}
blockObject ? fileId : ' ',
'imageBlock'
)}`}
alt=""
className="rounded-lg shadow "
/>
</Resizable>
)}
{isLoading && (
@ -98,29 +139,24 @@ function ImageBlockComponent(props: any) {
</div>
)}
</NodeViewWrapper>
);
)
}
export default ImageBlockComponent;
export default ImageBlockComponent
const BlockImageWrapper = styled.div`
align-items: center;
justify-content: center;
text-align: center;
font-size: 14px;
`;
`
const BlockImage = styled.div`
display: flex;
// center
align-items: center;
justify-content: center;
text-align: center;
font-size: 14px;
`;
`

View file

@ -1,35 +1,35 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import MathEquationBlockComponent from "./MathEquationBlockComponent";
import MathEquationBlockComponent from './MathEquationBlockComponent'
export default Node.create({
name: "blockMathEquation",
group: "block",
name: 'blockMathEquation',
group: 'block',
atom: true,
addAttributes() {
return {
math_equation: {
default: "",
default: '',
},
};
}
},
parseHTML() {
return [
{
tag: "block-math-equation",
tag: 'block-math-equation',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["block-math-equation", mergeAttributes(HTMLAttributes), 0];
return ['block-math-equation', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(MathEquationBlockComponent);
return ReactNodeViewRenderer(MathEquationBlockComponent)
},
});
})

View file

@ -1,31 +1,31 @@
import { NodeViewWrapper } from "@tiptap/react";
import React from "react";
import styled from "styled-components";
import "katex/dist/katex.min.css";
import { BlockMath } from "react-katex";
import { Save } from "lucide-react";
import Link from "next/link";
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import { NodeViewWrapper } from '@tiptap/react'
import React from 'react'
import styled from 'styled-components'
import 'katex/dist/katex.min.css'
import { BlockMath } from 'react-katex'
import { Save } from 'lucide-react'
import Link from 'next/link'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
function MathEquationBlockComponent(props: any) {
const [equation, setEquation] = React.useState(props.node.attrs.math_equation);
const [isEditing, setIsEditing] = React.useState(true);
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const [equation, setEquation] = React.useState(props.node.attrs.math_equation)
const [isEditing, setIsEditing] = React.useState(true)
const editorState = useEditorProvider() as any
const isEditable = editorState.isEditable
const handleEquationChange = (event: React.ChangeEvent<any>) => {
setEquation(event.target.value);
setEquation(event.target.value)
props.updateAttributes({
math_equation: equation,
});
};
})
}
const saveEquation = () => {
props.updateAttributes({
math_equation: equation,
});
})
//setIsEditing(false);
};
}
return (
<NodeViewWrapper className="block-math-equation">
@ -34,24 +34,38 @@ function MathEquationBlockComponent(props: any) {
{isEditing && isEditable && (
<>
<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()}>
<Save size={15}></Save>
</button>
</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>
</NodeViewWrapper>
);
)
}
export default MathEquationBlockComponent;
export default MathEquationBlockComponent
const MathEqWrapper = styled.div`
`;
const MathEqWrapper = styled.div``
const EditBar = styled.div`
display: flex;
@ -82,7 +96,7 @@ const EditBar = styled.div`
font-size: 14px;
color: #494949;
width: 100%;
font-family: "DM Sans", sans-serif;
font-family: 'DM Sans', sans-serif;
padding-left: 10px;
&:focus {
outline: none;
@ -92,4 +106,4 @@ const EditBar = styled.div`
color: #49494936;
}
}
`;
`

View file

@ -1,21 +1,29 @@
import { Extension } from '@tiptap/core';
import { Plugin, PluginKey } from 'prosemirror-state';
import { Extension } from '@tiptap/core'
import { Plugin, PluginKey } from 'prosemirror-state'
export const NoTextInput = Extension.create({
name: 'noTextInput',
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('noTextInput'),
filterTransaction: (transaction) => {
// If the transaction is adding text, stop it
return !transaction.docChanged || transaction.steps.every((step) => {
const { slice } = step.toJSON();
return !slice || !slice.content.some((node: { type: string; }) => node.type === 'text');
});
},
}),
];
},
});
name: 'noTextInput',
addProseMirrorPlugins() {
return [
new Plugin({
key: new PluginKey('noTextInput'),
filterTransaction: (transaction) => {
// If the transaction is adding text, stop it
return (
!transaction.docChanged ||
transaction.steps.every((step) => {
const { slice } = step.toJSON()
return (
!slice ||
!slice.content.some(
(node: { type: string }) => node.type === 'text'
)
)
})
)
},
}),
]
},
})

View file

@ -1,11 +1,11 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import PDFBlockComponent from "./PDFBlockComponent";
import PDFBlockComponent from './PDFBlockComponent'
export default Node.create({
name: "blockPDF",
group: "block",
name: 'blockPDF',
group: 'block',
atom: true,
@ -14,22 +14,22 @@ export default Node.create({
blockObject: {
default: null,
},
};
}
},
parseHTML() {
return [
{
tag: "block-pdf",
tag: 'block-pdf',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["block-pdf", mergeAttributes(HTMLAttributes), 0];
return ['block-pdf', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(PDFBlockComponent);
return ReactNodeViewRenderer(PDFBlockComponent)
},
});
})

View file

@ -1,56 +1,79 @@
import { NodeViewWrapper } from "@tiptap/react";
import React, { useEffect } from "react";
import styled from "styled-components";
import { AlertTriangle, FileText, Loader } from "lucide-react";
import { uploadNewPDFFile } from "../../../../../services/blocks/Pdf/pdf";
import { UploadIcon } from "@radix-ui/react-icons";
import { getActivityBlockMediaDirectory } from "@services/media/media";
import { useOrg } from "@components/Contexts/OrgContext";
import { useCourse } from "@components/Contexts/CourseContext";
import { useEditorProvider } from "@components/Contexts/Editor/EditorContext";
import { NodeViewWrapper } from '@tiptap/react'
import React, { useEffect } from 'react'
import styled from 'styled-components'
import { AlertTriangle, FileText, Loader } from 'lucide-react'
import { uploadNewPDFFile } from '../../../../../services/blocks/Pdf/pdf'
import { UploadIcon } from '@radix-ui/react-icons'
import { getActivityBlockMediaDirectory } from '@services/media/media'
import { useOrg } from '@components/Contexts/OrgContext'
import { useCourse } from '@components/Contexts/CourseContext'
import { useEditorProvider } from '@components/Contexts/Editor/EditorContext'
function PDFBlockComponent(props: any) {
const org = useOrg() as any;
const course = useCourse() as any;
const [pdf, setPDF] = React.useState(null);
const [isLoading, setIsLoading] = React.useState(false);
const [blockObject, setblockObject] = React.useState(props.node.attrs.blockObject);
const fileId = blockObject ? `${blockObject.content.file_id}.${blockObject.content.file_format}` : null;
const editorState = useEditorProvider() as any;
const isEditable = editorState.isEditable;
const org = useOrg() as any
const course = useCourse() as any
const [pdf, setPDF] = React.useState(null)
const [isLoading, setIsLoading] = React.useState(false)
const [blockObject, setblockObject] = React.useState(
props.node.attrs.blockObject
)
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>) => {
setPDF(event.target.files[0]);
};
setPDF(event.target.files[0])
}
const handleSubmit = async (e: any) => {
e.preventDefault();
setIsLoading(true);
let object = await uploadNewPDFFile(pdf, props.extension.options.activity.activity_uuid);
setIsLoading(false);
setblockObject(object);
e.preventDefault()
setIsLoading(true)
let object = await uploadNewPDFFile(
pdf,
props.extension.options.activity.activity_uuid
)
setIsLoading(false)
setblockObject(object)
props.updateAttributes({
blockObject: object,
});
};
useEffect(() => {
})
}
, [course, org]);
useEffect(() => {}, [course, org])
return (
<NodeViewWrapper className="block-pdf">
{!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 ? (
<Loader className="animate-spin animate-pulse text-gray-200" size={50} />
<Loader
className="animate-spin animate-pulse text-gray-200"
size={50}
/>
) : (
<>
<div>
<FileText className="text-gray-200" size={50} />
</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="" />
<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>
<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=""
/>
<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>
@ -59,11 +82,14 @@ function PDFBlockComponent(props: any) {
<BlockPDF>
<iframe
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,
props.extension.options.activity.activity_uuid,
blockObject.block_uuid,
blockObject ? fileId : ' ', 'pdfBlock')}`}
blockObject ? fileId : ' ',
'pdfBlock'
)}`}
/>
</BlockPDF>
)}
@ -73,19 +99,18 @@ function PDFBlockComponent(props: any) {
</div>
)}
</NodeViewWrapper>
);
)
}
export default PDFBlockComponent;
export default PDFBlockComponent
const BlockPDFWrapper = styled.div`
// center
align-items: center;
justify-content: center;
text-align: center;
font-size: 14px;
`;
`
const BlockPDF = styled.div`
display: flex;
@ -97,5 +122,5 @@ const BlockPDF = styled.div`
// cover
object-fit: cover;
}
`;
const PDFNotFound = styled.div``;
`
const PDFNotFound = styled.div``

View file

@ -1,11 +1,11 @@
import { mergeAttributes, Node } from "@tiptap/core";
import { ReactNodeViewRenderer } from "@tiptap/react";
import { mergeAttributes, Node } from '@tiptap/core'
import { ReactNodeViewRenderer } from '@tiptap/react'
import QuizBlockComponent from "./QuizBlockComponent";
import QuizBlockComponent from './QuizBlockComponent'
export default Node.create({
name: "blockQuiz",
group: "block",
name: 'blockQuiz',
group: 'block',
atom: true,
addAttributes() {
@ -16,22 +16,22 @@ export default Node.create({
questions: {
default: [],
},
};
}
},
parseHTML() {
return [
{
tag: "block-quiz",
tag: 'block-quiz',
},
];
]
},
renderHTML({ HTMLAttributes }) {
return ["block-quiz", mergeAttributes(HTMLAttributes), 0];
return ['block-quiz', mergeAttributes(HTMLAttributes), 0]
},
addNodeView() {
return ReactNodeViewRenderer(QuizBlockComponent);
return ReactNodeViewRenderer(QuizBlockComponent)
},
});
})

Some files were not shown because too many files have changed in this diff Show more