mirror of
https://github.com/rzmk/learnhouse.git
synced 2025-12-19 04:19:25 +00:00
feat: format with prettier
This commit is contained in:
parent
03fb09c3d6
commit
a147ad6610
164 changed files with 11257 additions and 8154 deletions
|
|
@ -1,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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue