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,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