feat: working UI with the database

This commit is contained in:
swve 2025-07-16 18:47:04 +02:00
parent b0d492a116
commit 306230174e
4 changed files with 417 additions and 173 deletions

View file

@ -11,6 +11,7 @@ import { useRouter } from 'next/navigation'
import React, { useEffect, useState } from 'react' import React, { useEffect, useState } from 'react'
import { mutate } from 'swr' import { mutate } from 'swr'
import { updateCourse } from '@services/courses/courses' import { updateCourse } from '@services/courses/courses'
import { updateCertification } from '@services/courses/certifications'
import { useLHSession } from '@components/Contexts/LHSessionContext' import { useLHSession } from '@components/Contexts/LHSessionContext'
function SaveState(props: { orgslug: string }) { function SaveState(props: { orgslug: string }) {
@ -32,6 +33,8 @@ function SaveState(props: { orgslug: string }) {
// Course metadata // Course metadata
await changeMetadataBackend() await changeMetadataBackend()
mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`) mutate(`${getAPIUrl()}courses/${course.courseStructure.course_uuid}/meta?with_unpublished_activities=${withUnpublishedActivities}`)
// Certification data (if present)
await saveCertificationData()
await revalidateTags(['courses'], props.orgslug) await revalidateTags(['courses'], props.orgslug)
dispatchCourse({ type: 'setIsSaved' }) dispatchCourse({ type: 'setIsSaved' })
} finally { } finally {
@ -66,6 +69,24 @@ function SaveState(props: { orgslug: string }) {
dispatchCourse({ type: 'setIsSaved' }) dispatchCourse({ type: 'setIsSaved' })
} }
// Certification data
const saveCertificationData = async () => {
if (course.courseStructure._certificationData) {
const certData = course.courseStructure._certificationData;
try {
await updateCertification(
certData.certification_uuid,
certData.config,
session.data?.tokens?.access_token
);
console.log('Certification data saved successfully');
} catch (error) {
console.error('Failed to save certification data:', error);
// Don't throw error to prevent breaking the main save flow
}
}
}
const handleCourseOrder = (course_structure: any) => { const handleCourseOrder = (course_structure: any) => {
const chapters = course_structure.chapters const chapters = course_structure.chapters
const chapter_order_by_ids = chapters.map((chapter: any) => { const chapter_order_by_ids = chapters.map((chapter: any) => {

View file

@ -1,15 +1,21 @@
import FormLayout, { import {
FormField, FormField,
FormLabelAndMessage, FormLabelAndMessage,
Input, Input,
Textarea, Textarea,
} from '@components/Objects/StyledElements/Form/Form'; } from '@components/Objects/StyledElements/Form/Form';
import { useFormik } from 'formik'; import { useFormik } from 'formik';
import { AlertTriangle, Award, CheckCircle, FileText, Settings } from 'lucide-react'; import { AlertTriangle, Award, FileText, Settings } from 'lucide-react';
import CertificatePreview from './CertificatePreview'; import CertificatePreview from './CertificatePreview';
import * as Form from '@radix-ui/react-form'; import * as Form from '@radix-ui/react-form';
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext'; import { useCourse, useCourseDispatch } from '@components/Contexts/CourseContext';
import { useLHSession } from '@components/Contexts/LHSessionContext';
import {
createCertification,
updateCertification,
deleteCertification
} from '@services/courses/certifications';
import { import {
CustomSelect, CustomSelect,
CustomSelectContent, CustomSelectContent,
@ -17,6 +23,9 @@ import {
CustomSelectTrigger, CustomSelectTrigger,
CustomSelectValue, CustomSelectValue,
} from "../EditCourseGeneral/CustomSelect"; } from "../EditCourseGeneral/CustomSelect";
import useSWR, { mutate } from 'swr';
import { getAPIUrl } from '@services/config/config';
import toast from 'react-hot-toast';
type EditCourseCertificationProps = { type EditCourseCertificationProps = {
orgslug: string orgslug: string
@ -43,9 +52,56 @@ const validate = (values: any) => {
function EditCourseCertification(props: EditCourseCertificationProps) { function EditCourseCertification(props: EditCourseCertificationProps) {
const [error, setError] = useState(''); const [error, setError] = useState('');
const [isCreating, setIsCreating] = useState(false);
const course = useCourse(); const course = useCourse();
const dispatchCourse = useCourseDispatch() as any; const dispatchCourse = useCourseDispatch() as any;
const { isLoading, courseStructure } = course as any; const { isLoading, courseStructure } = course as any;
const session = useLHSession() as any;
const access_token = session?.data?.tokens?.access_token;
// Fetch existing certifications
const { data: certifications, error: certificationsError, mutate: mutateCertifications } = useSWR(
courseStructure?.course_uuid && access_token ?
`certifications/course/${courseStructure.course_uuid}` : null,
async () => {
if (!courseStructure?.course_uuid || !access_token) return null;
const result = await fetch(
`${getAPIUrl()}certifications/course/${courseStructure.course_uuid}`,
{
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${access_token}`,
},
credentials: 'include',
}
);
const response = await result.json();
if (result.status === 200) {
return {
success: true,
data: response,
status: result.status,
HTTPmessage: result.statusText,
};
} else {
return {
success: false,
data: response,
status: result.status,
HTTPmessage: result.statusText,
};
}
}
);
const existingCertification = certifications?.data?.[0]; // Assuming one certification per course
const hasExistingCertification = !!existingCertification;
// Create initial values object // Create initial values object
const getInitialValues = () => { const getInitialValues = () => {
@ -64,13 +120,16 @@ function EditCourseCertification(props: EditCourseCertificationProps) {
return ''; return '';
}; };
// Use existing certification data if available, otherwise fall back to course data
const config = existingCertification?.config || {};
return { return {
enable_certification: courseStructure?.enable_certification || false, enable_certification: hasExistingCertification,
certification_name: courseStructure?.certification_name || courseStructure?.name || '', certification_name: config.certification_name || courseStructure?.name || '',
certification_description: courseStructure?.certification_description || courseStructure?.description || '', certification_description: config.certification_description || courseStructure?.description || '',
certification_type: courseStructure?.certification_type || 'completion', certification_type: config.certification_type || 'completion',
certificate_pattern: courseStructure?.certificate_pattern || 'professional', certificate_pattern: config.certificate_pattern || 'professional',
certificate_instructor: courseStructure?.certificate_instructor || getInstructorName(), certificate_instructor: config.certificate_instructor || getInstructorName(),
}; };
}; };
@ -78,26 +137,85 @@ function EditCourseCertification(props: EditCourseCertificationProps) {
initialValues: getInitialValues(), initialValues: getInitialValues(),
validate, validate,
onSubmit: async values => { onSubmit: async values => {
try { // This is no longer used - saving is handled by the main Save button
// Add your submission logic here
dispatchCourse({ type: 'setIsSaved' });
} catch (e) {
setError('Failed to save certification settings.');
}
}, },
enableReinitialize: true, enableReinitialize: true,
}) as any; }) as any;
// Reset form when courseStructure changes // Handle enabling/disabling certification
const handleCertificationToggle = async (enabled: boolean) => {
if (enabled && !hasExistingCertification) {
// Create new certification
setIsCreating(true);
try {
const config = {
certification_name: formik.values.certification_name || courseStructure?.name || '',
certification_description: formik.values.certification_description || courseStructure?.description || '',
certification_type: formik.values.certification_type || 'completion',
certificate_pattern: formik.values.certificate_pattern || 'professional',
certificate_instructor: formik.values.certificate_instructor || '',
};
const result = await createCertification(
courseStructure.id,
config,
access_token
);
// createCertification uses errorHandling which returns JSON directly on success
if (result) {
toast.success('Certification created successfully');
mutateCertifications();
formik.setFieldValue('enable_certification', true);
} else {
throw new Error('Failed to create certification');
}
} catch (e) {
setError('Failed to create certification.');
toast.error('Failed to create certification');
formik.setFieldValue('enable_certification', false);
} finally {
setIsCreating(false);
}
} else if (!enabled && hasExistingCertification) {
// Delete existing certification
try {
const result = await deleteCertification(
existingCertification.certification_uuid,
access_token
);
// deleteCertification uses errorHandling which returns JSON directly on success
if (result) {
toast.success('Certification removed successfully');
mutateCertifications();
formik.setFieldValue('enable_certification', false);
} else {
throw new Error('Failed to delete certification');
}
} catch (e) {
setError('Failed to remove certification.');
toast.error('Failed to remove certification');
formik.setFieldValue('enable_certification', true);
}
} else {
formik.setFieldValue('enable_certification', enabled);
}
};
// Reset form when certifications data changes
useEffect(() => { useEffect(() => {
if (courseStructure && !isLoading) { if (certifications && !isLoading) {
const newValues = getInitialValues(); const newValues = getInitialValues();
formik.resetForm({ values: newValues }); formik.resetForm({ values: newValues });
} }
}, [courseStructure, isLoading]); }, [certifications, isLoading]);
// Handle form changes - update course context with certification data
useEffect(() => { useEffect(() => {
if (!isLoading) { if (!isLoading && hasExistingCertification) {
const formikValues = formik.values as any; const formikValues = formik.values as any;
const initialValues = formik.initialValues as any; const initialValues = formik.initialValues as any;
const valuesChanged = Object.keys(formikValues).some( const valuesChanged = Object.keys(formikValues).some(
@ -106,19 +224,35 @@ function EditCourseCertification(props: EditCourseCertificationProps) {
if (valuesChanged) { if (valuesChanged) {
dispatchCourse({ type: 'setIsNotSaved' }); dispatchCourse({ type: 'setIsNotSaved' });
// Store certification data in course context so it gets saved with the main save button
const updatedCourse = { const updatedCourse = {
...courseStructure, ...courseStructure,
...formikValues, // Store certification data for the main save functionality
_certificationData: {
certification_uuid: existingCertification.certification_uuid,
config: {
certification_name: formikValues.certification_name,
certification_description: formikValues.certification_description,
certification_type: formikValues.certification_type,
certificate_pattern: formikValues.certificate_pattern,
certificate_instructor: formikValues.certificate_instructor,
}
}
}; };
dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse }); dispatchCourse({ type: 'setCourseStructure', payload: updatedCourse });
} }
} }
}, [formik.values, isLoading]); }, [formik.values, isLoading, hasExistingCertification, existingCertification]);
if (isLoading || !courseStructure) { if (isLoading || !courseStructure || (courseStructure.course_uuid && access_token && certifications === undefined)) {
return <div>Loading...</div>; return <div>Loading...</div>;
} }
if (certificationsError) {
return <div>Error loading certifications</div>;
}
return ( return (
<div> <div>
{courseStructure && ( {courseStructure && (
@ -139,10 +273,16 @@ function EditCourseCertification(props: EditCourseCertificationProps) {
type="checkbox" type="checkbox"
className="sr-only peer" className="sr-only peer"
checked={formik.values.enable_certification} checked={formik.values.enable_certification}
onChange={(e) => formik.setFieldValue('enable_certification', e.target.checked)} onChange={(e) => handleCertificationToggle(e.target.checked)}
disabled={isCreating}
/> />
<div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div> <div className="w-11 h-6 bg-gray-200 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-blue-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-gray-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-blue-600"></div>
</label> </label>
{isCreating && (
<div className="animate-spin">
<Settings size={16} />
</div>
)}
</div> </div>
</div> </div>
@ -153,162 +293,160 @@ function EditCourseCertification(props: EditCourseCertificationProps) {
</div> </div>
)} )}
{/* Certification Configuration */} {/* Certification Configuration - Only show if enabled and has existing certification */}
{formik.values.enable_certification && ( {formik.values.enable_certification && hasExistingCertification && (
<div className="grid grid-cols-1 lg:grid-cols-5 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-5 gap-6">
{/* Form Section */} {/* Form Section */}
<div className="lg:col-span-3"> <div className="lg:col-span-3">
<FormLayout onSubmit={formik.handleSubmit}> <Form.Root className="space-y-6">
<div className="space-y-6"> {/* Basic Information Section */}
{/* Basic Information Section */} <div className="flex flex-col bg-gray-50 -space-y-1 px-3 sm:px-5 py-3 rounded-md mb-3">
<div className="flex flex-col bg-gray-50 -space-y-1 px-3 sm:px-5 py-3 rounded-md mb-3"> <h3 className="font-bold text-md text-gray-800 flex items-center gap-2">
<h3 className="font-bold text-md text-gray-800 flex items-center gap-2"> <FileText size={16} />
<FileText size={16} /> Basic Information
Basic Information </h3>
</h3> <p className="text-gray-500 text-xs sm:text-sm">
<p className="text-gray-500 text-xs sm:text-sm"> Configure the basic details of your certification
Configure the basic details of your certification </p>
</p> </div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6"> <div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Certification Name */} {/* Certification Name */}
<FormField name="certification_name"> <FormField name="certification_name">
<FormLabelAndMessage
label="Certification Name"
message={formik.errors.certification_name}
/>
<Form.Control asChild>
<Input
style={{ backgroundColor: 'white' }}
onChange={formik.handleChange}
value={formik.values.certification_name}
type="text"
placeholder="e.g., Advanced JavaScript Certification"
required
/>
</Form.Control>
</FormField>
{/* Certification Type */}
<FormField name="certification_type">
<FormLabelAndMessage label="Certification Type" />
<Form.Control asChild>
<CustomSelect
value={formik.values.certification_type}
onValueChange={(value) => {
if (!value) return;
formik.setFieldValue('certification_type', value);
}}
>
<CustomSelectTrigger className="w-full bg-white">
<CustomSelectValue>
{formik.values.certification_type === 'completion' ? 'Course Completion' :
formik.values.certification_type === 'achievement' ? 'Achievement Based' :
formik.values.certification_type === 'assessment' ? 'Assessment Based' :
formik.values.certification_type === 'participation' ? 'Participation' :
formik.values.certification_type === 'mastery' ? 'Skill Mastery' :
formik.values.certification_type === 'professional' ? 'Professional Development' :
formik.values.certification_type === 'continuing' ? 'Continuing Education' :
formik.values.certification_type === 'workshop' ? 'Workshop Attendance' :
formik.values.certification_type === 'specialization' ? 'Specialization' : 'Course Completion'}
</CustomSelectValue>
</CustomSelectTrigger>
<CustomSelectContent>
<CustomSelectItem value="completion">Course Completion</CustomSelectItem>
<CustomSelectItem value="achievement">Achievement Based</CustomSelectItem>
<CustomSelectItem value="assessment">Assessment Based</CustomSelectItem>
<CustomSelectItem value="participation">Participation</CustomSelectItem>
<CustomSelectItem value="mastery">Skill Mastery</CustomSelectItem>
<CustomSelectItem value="professional">Professional Development</CustomSelectItem>
<CustomSelectItem value="continuing">Continuing Education</CustomSelectItem>
<CustomSelectItem value="workshop">Workshop Attendance</CustomSelectItem>
<CustomSelectItem value="specialization">Specialization</CustomSelectItem>
</CustomSelectContent>
</CustomSelect>
</Form.Control>
</FormField>
</div>
{/* Certification Description */}
<FormField name="certification_description">
<FormLabelAndMessage <FormLabelAndMessage
label="Certification Description" label="Certification Name"
message={formik.errors.certification_description} message={formik.errors.certification_name}
/> />
<Form.Control asChild> <Form.Control asChild>
<Textarea <Input
style={{ backgroundColor: 'white', height: '120px', minHeight: '120px' }} style={{ backgroundColor: 'white' }}
onChange={formik.handleChange} onChange={formik.handleChange}
value={formik.values.certification_description} value={formik.values.certification_name}
placeholder="Describe what this certification represents and its value..." type="text"
placeholder="e.g., Advanced JavaScript Certification"
required required
/> />
</Form.Control> </Form.Control>
</FormField> </FormField>
{/* Certificate Design Section */} {/* Certification Type */}
<div className="flex flex-col bg-gray-50 -space-y-1 px-3 sm:px-5 py-3 rounded-md mb-3"> <FormField name="certification_type">
<h3 className="font-bold text-md text-gray-800 flex items-center gap-2"> <FormLabelAndMessage label="Certification Type" />
<Award size={16} />
Certificate Design
</h3>
<p className="text-gray-500 text-xs sm:text-sm">
Choose a decorative pattern for your certificate
</p>
</div>
{/* Pattern Selection */}
<FormField name="certificate_pattern">
<FormLabelAndMessage label="Certificate Pattern" />
<Form.Control asChild> <Form.Control asChild>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3"> <CustomSelect
{[ value={formik.values.certification_type}
{ value: 'royal', name: 'Royal', description: 'Ornate with crown motifs' }, onValueChange={(value) => {
{ value: 'tech', name: 'Tech', description: 'Circuit-inspired patterns' }, if (!value) return;
{ value: 'nature', name: 'Nature', description: 'Organic leaf patterns' }, formik.setFieldValue('certification_type', value);
{ value: 'geometric', name: 'Geometric', description: 'Abstract shapes & lines' }, }}
{ value: 'vintage', name: 'Vintage', description: 'Art deco styling' }, >
{ value: 'waves', name: 'Waves', description: 'Flowing water patterns' }, <CustomSelectTrigger className="w-full bg-white">
{ value: 'minimal', name: 'Minimal', description: 'Clean and simple' }, <CustomSelectValue>
{ value: 'professional', name: 'Professional', description: 'Business-ready design' }, {formik.values.certification_type === 'completion' ? 'Course Completion' :
{ value: 'academic', name: 'Academic', description: 'Traditional university style' }, formik.values.certification_type === 'achievement' ? 'Achievement Based' :
{ value: 'modern', name: 'Modern', description: 'Contemporary clean lines' } formik.values.certification_type === 'assessment' ? 'Assessment Based' :
].map((pattern) => ( formik.values.certification_type === 'participation' ? 'Participation' :
<div formik.values.certification_type === 'mastery' ? 'Skill Mastery' :
key={pattern.value} formik.values.certification_type === 'professional' ? 'Professional Development' :
className={`p-3 border-2 rounded-lg cursor-pointer transition-all ${ formik.values.certification_type === 'continuing' ? 'Continuing Education' :
formik.values.certificate_pattern === pattern.value formik.values.certification_type === 'workshop' ? 'Workshop Attendance' :
? 'border-blue-500 bg-blue-50' formik.values.certification_type === 'specialization' ? 'Specialization' : 'Course Completion'}
: 'border-gray-200 hover:border-gray-300' </CustomSelectValue>
}`} </CustomSelectTrigger>
onClick={() => formik.setFieldValue('certificate_pattern', pattern.value)} <CustomSelectContent>
> <CustomSelectItem value="completion">Course Completion</CustomSelectItem>
<div className="text-center"> <CustomSelectItem value="achievement">Achievement Based</CustomSelectItem>
<div className="text-sm font-medium text-gray-900">{pattern.name}</div> <CustomSelectItem value="assessment">Assessment Based</CustomSelectItem>
<div className="text-xs text-gray-500 mt-1">{pattern.description}</div> <CustomSelectItem value="participation">Participation</CustomSelectItem>
</div> <CustomSelectItem value="mastery">Skill Mastery</CustomSelectItem>
</div> <CustomSelectItem value="professional">Professional Development</CustomSelectItem>
))} <CustomSelectItem value="continuing">Continuing Education</CustomSelectItem>
</div> <CustomSelectItem value="workshop">Workshop Attendance</CustomSelectItem>
</Form.Control> <CustomSelectItem value="specialization">Specialization</CustomSelectItem>
</FormField> </CustomSelectContent>
</CustomSelect>
{/* Custom Instructor */}
<FormField name="certificate_instructor">
<FormLabelAndMessage label="Instructor Name (Optional)" />
<Form.Control asChild>
<Input
style={{ backgroundColor: 'white' }}
onChange={formik.handleChange}
value={formik.values.certificate_instructor}
type="text"
placeholder="e.g., Dr. Jane Smith"
/>
</Form.Control> </Form.Control>
</FormField> </FormField>
</div> </div>
</FormLayout>
{/* Certification Description */}
<FormField name="certification_description">
<FormLabelAndMessage
label="Certification Description"
message={formik.errors.certification_description}
/>
<Form.Control asChild>
<Textarea
style={{ backgroundColor: 'white', height: '120px', minHeight: '120px' }}
onChange={formik.handleChange}
value={formik.values.certification_description}
placeholder="Describe what this certification represents and its value..."
required
/>
</Form.Control>
</FormField>
{/* Certificate Design Section */}
<div className="flex flex-col bg-gray-50 -space-y-1 px-3 sm:px-5 py-3 rounded-md mb-3">
<h3 className="font-bold text-md text-gray-800 flex items-center gap-2">
<Award size={16} />
Certificate Design
</h3>
<p className="text-gray-500 text-xs sm:text-sm">
Choose a decorative pattern for your certificate
</p>
</div>
{/* Pattern Selection */}
<FormField name="certificate_pattern">
<FormLabelAndMessage label="Certificate Pattern" />
<Form.Control asChild>
<div className="grid grid-cols-2 lg:grid-cols-5 gap-3">
{[
{ value: 'royal', name: 'Royal', description: 'Ornate with crown motifs' },
{ value: 'tech', name: 'Tech', description: 'Circuit-inspired patterns' },
{ value: 'nature', name: 'Nature', description: 'Organic leaf patterns' },
{ value: 'geometric', name: 'Geometric', description: 'Abstract shapes & lines' },
{ value: 'vintage', name: 'Vintage', description: 'Art deco styling' },
{ value: 'waves', name: 'Waves', description: 'Flowing water patterns' },
{ value: 'minimal', name: 'Minimal', description: 'Clean and simple' },
{ value: 'professional', name: 'Professional', description: 'Business-ready design' },
{ value: 'academic', name: 'Academic', description: 'Traditional university style' },
{ value: 'modern', name: 'Modern', description: 'Contemporary clean lines' }
].map((pattern) => (
<div
key={pattern.value}
className={`p-3 border-2 rounded-lg cursor-pointer transition-all ${
formik.values.certificate_pattern === pattern.value
? 'border-blue-500 bg-blue-50'
: 'border-gray-200 hover:border-gray-300'
}`}
onClick={() => formik.setFieldValue('certificate_pattern', pattern.value)}
>
<div className="text-center">
<div className="text-sm font-medium text-gray-900">{pattern.name}</div>
<div className="text-xs text-gray-500 mt-1">{pattern.description}</div>
</div>
</div>
))}
</div>
</Form.Control>
</FormField>
{/* Custom Instructor */}
<FormField name="certificate_instructor">
<FormLabelAndMessage label="Instructor Name (Optional)" />
<Form.Control asChild>
<Input
style={{ backgroundColor: 'white' }}
onChange={formik.handleChange}
value={formik.values.certificate_instructor}
type="text"
placeholder="e.g., Dr. Jane Smith"
/>
</Form.Control>
</FormField>
</Form.Root>
</div> </div>
{/* Preview Section */} {/* Preview Section */}
@ -348,14 +486,28 @@ function EditCourseCertification(props: EditCourseCertificationProps) {
</p> </p>
<button <button
type="button" type="button"
onClick={() => formik.setFieldValue('enable_certification', true)} onClick={() => handleCertificationToggle(true)}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500" disabled={isCreating}
className="inline-flex items-center gap-2 px-4 py-2 text-sm font-medium text-blue-600 bg-blue-50 border border-blue-200 rounded-lg hover:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
> >
<Award size={16} /> <Award size={16} />
Enable Certification {isCreating ? 'Creating...' : 'Enable Certification'}
</button> </button>
</div> </div>
)} )}
{/* Creating State - when toggle is on but no certification exists yet */}
{formik.values.enable_certification && !hasExistingCertification && isCreating && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-8 text-center">
<div className="animate-spin mx-auto mb-4">
<Settings className="w-16 h-16 text-blue-500" />
</div>
<h3 className="font-medium text-blue-700 mb-2">Creating Certification...</h3>
<p className="text-sm text-blue-600">
Please wait while we set up your course certification.
</p>
</div>
)}
</div> </div>
</div> </div>
)} )}

View file

@ -6,6 +6,7 @@ interface CustomSelectProps {
onValueChange: (value: string) => void; onValueChange: (value: string) => void;
placeholder?: string; placeholder?: string;
className?: string; className?: string;
disabled?: boolean;
children: React.ReactNode; children: React.ReactNode;
} }
@ -18,6 +19,7 @@ interface CustomSelectItemProps {
interface CustomSelectTriggerProps { interface CustomSelectTriggerProps {
children: React.ReactNode; children: React.ReactNode;
className?: string; className?: string;
disabled?: boolean;
} }
interface CustomSelectContentProps { interface CustomSelectContentProps {
@ -31,6 +33,7 @@ const CustomSelectContext = React.createContext<{
selectedValue: string; selectedValue: string;
setSelectedValue: (value: string) => void; setSelectedValue: (value: string) => void;
onValueChange: (value: string) => void; onValueChange: (value: string) => void;
disabled?: boolean;
} | null>(null); } | null>(null);
export const CustomSelect: React.FC<CustomSelectProps> = ({ export const CustomSelect: React.FC<CustomSelectProps> = ({
@ -38,6 +41,7 @@ export const CustomSelect: React.FC<CustomSelectProps> = ({
onValueChange, onValueChange,
placeholder, placeholder,
className = '', className = '',
disabled = false,
children children
}) => { }) => {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -48,6 +52,7 @@ export const CustomSelect: React.FC<CustomSelectProps> = ({
}, [value]); }, [value]);
const handleValueChange = (newValue: string) => { const handleValueChange = (newValue: string) => {
if (disabled) return;
setSelectedValue(newValue); setSelectedValue(newValue);
onValueChange(newValue); onValueChange(newValue);
setIsOpen(false); setIsOpen(false);
@ -60,7 +65,8 @@ export const CustomSelect: React.FC<CustomSelectProps> = ({
setIsOpen, setIsOpen,
selectedValue, selectedValue,
setSelectedValue, setSelectedValue,
onValueChange: handleValueChange onValueChange: handleValueChange,
disabled
}} }}
> >
<div className={`relative ${className}`}> <div className={`relative ${className}`}>
@ -72,20 +78,23 @@ export const CustomSelect: React.FC<CustomSelectProps> = ({
export const CustomSelectTrigger: React.FC<CustomSelectTriggerProps> = ({ export const CustomSelectTrigger: React.FC<CustomSelectTriggerProps> = ({
children, children,
className = '' className = '',
disabled = false
}) => { }) => {
const context = React.useContext(CustomSelectContext); const context = React.useContext(CustomSelectContext);
if (!context) { if (!context) {
throw new Error('CustomSelectTrigger must be used within CustomSelect'); throw new Error('CustomSelectTrigger must be used within CustomSelect');
} }
const { isOpen, setIsOpen } = context; const { isOpen, setIsOpen, disabled: contextDisabled } = context;
const isDisabled = disabled || contextDisabled;
return ( return (
<button <button
type="button" type="button"
disabled={isDisabled}
className={`flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 ${className}`} className={`flex h-9 w-full items-center justify-between whitespace-nowrap rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-xs ring-offset-background placeholder:text-muted-foreground focus:outline-hidden focus:ring-1 focus:ring-ring disabled:cursor-not-allowed disabled:opacity-50 [&>span]:line-clamp-1 ${className}`}
onClick={() => setIsOpen(!isOpen)} onClick={() => !isDisabled && setIsOpen(!isOpen)}
> >
{children} {children}
<ChevronDown className={`h-4 w-4 opacity-50 transition-transform ${isOpen ? 'rotate-180' : ''}`} /> <ChevronDown className={`h-4 w-4 opacity-50 transition-transform ${isOpen ? 'rotate-180' : ''}`} />
@ -102,9 +111,9 @@ export const CustomSelectContent: React.FC<CustomSelectContentProps> = ({
throw new Error('CustomSelectContent must be used within CustomSelect'); throw new Error('CustomSelectContent must be used within CustomSelect');
} }
const { isOpen } = context; const { isOpen, disabled } = context;
if (!isOpen) return null; if (!isOpen || disabled) return null;
return ( return (
<div className={`absolute z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 slide-in-from-top-2 ${className}`}> <div className={`absolute z-50 max-h-96 min-w-[8rem] overflow-hidden rounded-md border bg-popover text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 slide-in-from-top-2 ${className}`}>
@ -125,12 +134,12 @@ export const CustomSelectItem: React.FC<CustomSelectItemProps> = ({
throw new Error('CustomSelectItem must be used within CustomSelect'); throw new Error('CustomSelectItem must be used within CustomSelect');
} }
const { selectedValue, onValueChange } = context; const { selectedValue, onValueChange, disabled } = context;
return ( return (
<div <div
className={`relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground ${className}`} className={`relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-2 pr-8 text-sm outline-hidden focus:bg-accent focus:text-accent-foreground hover:bg-accent hover:text-accent-foreground ${disabled ? 'opacity-50 cursor-not-allowed' : ''} ${className}`}
onClick={() => onValueChange(value)} onClick={() => !disabled && onValueChange(value)}
> >
{children} {children}
{selectedValue === value && ( {selectedValue === value && (

View file

@ -0,0 +1,62 @@
import { getAPIUrl } from '@services/config/config'
import {
RequestBodyWithAuthHeader,
errorHandling,
getResponseMetadata,
} from '@services/utils/ts/requests'
/*
This file includes certification-related API calls
GET requests are called from the frontend using SWR (https://swr.vercel.app/)
*/
export async function getCourseCertifications(
course_uuid: string,
next: any,
access_token: string
) {
const result = await fetch(
`${getAPIUrl()}certifications/course/${course_uuid}`,
RequestBodyWithAuthHeader('GET', null, next, access_token)
)
const res = await getResponseMetadata(result)
return res
}
export async function createCertification(
course_id: number,
config: any,
access_token: string
) {
const result = await fetch(
`${getAPIUrl()}certifications/`,
RequestBodyWithAuthHeader('POST', { course_id, config }, null, access_token)
)
const res = await errorHandling(result)
return res
}
export async function updateCertification(
certification_uuid: string,
config: any,
access_token: string
) {
const result = await fetch(
`${getAPIUrl()}certifications/${certification_uuid}`,
RequestBodyWithAuthHeader('PUT', { config }, null, access_token)
)
const res = await errorHandling(result)
return res
}
export async function deleteCertification(
certification_uuid: string,
access_token: string
) {
const result = await fetch(
`${getAPIUrl()}certifications/${certification_uuid}`,
RequestBodyWithAuthHeader('DELETE', null, null, access_token)
)
const res = await errorHandling(result)
return res
}