From 6eb4017e11a6cbd2749fb5f89c679ac1d4d01eac Mon Sep 17 00:00:00 2001 From: chinweobtagaz Date: Mon, 29 Jun 2026 03:46:32 -0700 Subject: [PATCH] feat: implement Certification Program for Tab Component and use crypto.randomUUID for Feature Flags --- .../profile/__tests__/ProfileTabs.test.tsx | 17 +++ .../profile/components/CertificatesPanel.tsx | 115 ++++++++++++++++++ src/app/profile/components/ProfileTabs.tsx | 5 + src/app/profile/profile-data.ts | 3 +- src/lib/feature-flags/store.ts | 5 +- 5 files changed, 142 insertions(+), 3 deletions(-) create mode 100644 src/app/profile/components/CertificatesPanel.tsx diff --git a/src/app/profile/__tests__/ProfileTabs.test.tsx b/src/app/profile/__tests__/ProfileTabs.test.tsx index ee3f5671..b66b78aa 100644 --- a/src/app/profile/__tests__/ProfileTabs.test.tsx +++ b/src/app/profile/__tests__/ProfileTabs.test.tsx @@ -46,4 +46,21 @@ describe('ProfileTabs', () => { expect(screen.getByText('Web3 Master')).toBeInTheDocument(); expect(screen.queryByLabelText('Full Name')).not.toBeInTheDocument(); }); + + it('loads certificates only when the certificates tab is selected', async () => { + const user = userEvent.setup(); + + render(); + await user.click(screen.getByRole('tab', { name: 'Certification Program' })); + + await waitFor(() => + expect(screen.getByRole('tabpanel', { name: 'Certification Program' })).toBeInTheDocument(), + ); + expect(screen.getByRole('tab', { name: 'Certification Program' })).toHaveAttribute( + 'aria-selected', + 'true', + ); + expect(screen.getByText('Generate your Course Certificate')).toBeInTheDocument(); + expect(screen.queryByLabelText('Full Name')).not.toBeInTheDocument(); + }); }); diff --git a/src/app/profile/components/CertificatesPanel.tsx b/src/app/profile/components/CertificatesPanel.tsx new file mode 100644 index 00000000..bd744238 --- /dev/null +++ b/src/app/profile/components/CertificatesPanel.tsx @@ -0,0 +1,115 @@ +'use client'; + +import { useState, memo } from 'react'; +import { useForm, FormProvider } from 'react-hook-form'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { motion } from 'framer-motion'; +import { CertificateInputSchema, type CertificateInput } from '@/schemas/certificate.schema'; +import { apiClient } from '@/lib/api'; +import { FormInput } from '@/components/forms/FormInput'; +import { FormError } from '@/components/forms/FormError'; +import { SubmitButton } from '@/components/forms/SubmitButton'; + +function CertificatesPanel() { + const [apiError, setApiError] = useState(null); + const [successMessage, setSuccessMessage] = useState(null); + + const methods = useForm({ + resolver: zodResolver(CertificateInputSchema), + mode: 'onTouched', + }); + + const { + handleSubmit, + formState: { isSubmitting }, + reset, + } = methods; + + const onSubmit = async (data: CertificateInput) => { + setApiError(null); + setSuccessMessage(null); + + try { + const result = await apiClient.post<{ certificateId: string }>( + '/api/certificates/generate', + data, + ); + setSuccessMessage(`Certificate generated successfully. ID: ${result.certificateId}`); + reset(); + } catch (error) { + setApiError( + error instanceof Error + ? error.message + : 'Unable to generate certificate. Please try again.', + ); + } + }; + + return ( +
+
+

+ Certification Program +

+

+ Generate your Course Certificate +

+

+ Complete the form below to request a certificate for a completed course. Your input fields + are validated, accessible, and connected to the Certification Program workflow. +

+
+ + +
+ + + + + + {successMessage && ( + + {successMessage} + + )} + +
+ + Generate certificate + +
+ +
+
+ ); +} + +export default memo(CertificatesPanel); diff --git a/src/app/profile/components/ProfileTabs.tsx b/src/app/profile/components/ProfileTabs.tsx index 3afb2cd4..63d5bf65 100644 --- a/src/app/profile/components/ProfileTabs.tsx +++ b/src/app/profile/components/ProfileTabs.tsx @@ -19,6 +19,10 @@ const CustomerSupportPanel = dynamic(() => import('./CustomerSupportPanel'), { loading: () => , }); +const CertificatesPanel = dynamic(() => import('./CertificatesPanel'), { + loading: () => , +}); + interface ProfileTabButtonProps { tab: { id: ProfileTabId; label: string }; isActive: boolean; @@ -76,6 +80,7 @@ export default function ProfileTabs() { {activeTab === 'settings' && } {activeTab === 'achievements' && } {activeTab === 'support' && } + {activeTab === 'certificates' && } ); } diff --git a/src/app/profile/profile-data.ts b/src/app/profile/profile-data.ts index 2eee5776..b60621c5 100644 --- a/src/app/profile/profile-data.ts +++ b/src/app/profile/profile-data.ts @@ -1,4 +1,4 @@ -export type ProfileTabId = 'profile' | 'settings' | 'achievements' | 'support'; +export type ProfileTabId = 'profile' | 'settings' | 'achievements' | 'support' | 'certificates'; export interface ProfileUser { initials: string; @@ -43,6 +43,7 @@ export const profileTabs: Array<{ id: ProfileTabId; label: string }> = [ { id: 'settings', label: 'Settings' }, { id: 'achievements', label: 'Achievements' }, { id: 'support', label: 'Customer Support' }, + { id: 'certificates', label: 'Certification Program' }, ]; // ── Customer Support ────────────────────────────────────────────────────────── diff --git a/src/lib/feature-flags/store.ts b/src/lib/feature-flags/store.ts index db00d098..33fd3119 100644 --- a/src/lib/feature-flags/store.ts +++ b/src/lib/feature-flags/store.ts @@ -103,8 +103,9 @@ for (const f of SEED_FLAGS) { // ─── Helpers ────────────────────────────────────────────────────────────────── -export function generateId(prefix = 'flag'): string { - return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 7)}`; +export function generateId(prefix = ''): string { + const uuid = crypto.randomUUID(); + return prefix ? `${prefix}_${uuid}` : uuid; } export function createAuditEntry(