diff --git a/.github/workflows/playwright.yml b/.github/workflows/playwright.yml index a94b6417..02c9074e 100644 --- a/.github/workflows/playwright.yml +++ b/.github/workflows/playwright.yml @@ -17,6 +17,8 @@ jobs: run: npm install -g yarn && yarn - name: Install Playwright Browsers run: yarn playwright install --with-deps + - name: Build application + run: yarn build - name: Run Playwright tests run: yarn playwright test - uses: actions/upload-artifact@v4 diff --git a/app/claims/ClaimsPageClient.tsx b/app/claims/ClaimsPageClient.tsx index 8023c691..3e14d1d6 100644 --- a/app/claims/ClaimsPageClient.tsx +++ b/app/claims/ClaimsPageClient.tsx @@ -518,21 +518,9 @@ const ClaimsPageClient: React.FC = () => { > {getCredentialName(claim)} - - {getTimeAgo(claim.issuanceDate || new Date().toISOString())} - - {claim.credentialSubject?.name} - {getCredentialType(claim)} -{' '} - {getTimeDifference( - claim.issuanceDate || new Date().toISOString() - )} + {getCredentialType(claim)} )} diff --git a/app/components/cards.tsx b/app/components/cards.tsx index e32a36e2..2b59a0ff 100644 --- a/app/components/cards.tsx +++ b/app/components/cards.tsx @@ -45,6 +45,15 @@ const Card = ({ showDuration = true, showEvidence = true }: CardProps) => { + // Responsive sizing based on width prop + const isSmallCard = width === '160px' + const cardFontSizes = { + title: isSmallCard ? '12px' : '14px', + description: isSmallCard ? '8px' : '9px', + criteria: isSmallCard ? '8px' : '9px', + evidence: isSmallCard ? '8px' : '9px', + label: isSmallCard ? '9px' : '10px' + } return ( {description} @@ -184,7 +190,7 @@ const Card = ({ ( @@ -139,8 +146,8 @@ const Footer: React.FC = () => { /> } - text='lc.support@allskillscount.org' - href='mailto:lc.support@allskillscount.org' + text='support@linkedcreds.allskillsscount.com' + isEmailActions={true} /> ) : isTablet ? ( @@ -194,8 +201,8 @@ const Footer: React.FC = () => { /> } - text='lc.support@allskillscount.org' - href='mailto:lc.support@allskillscount.org' + text='support@linkedcreds.allskillsscount.com' + isEmailActions={true} /> @@ -235,8 +242,8 @@ const Footer: React.FC = () => { /> } - text='lc.support@allskillscount.org' - href='mailto:lc.support@allskillscount.org' + text='support@linkedcreds.allskillsscount.com' + isEmailActions={true} /> )} @@ -250,9 +257,19 @@ interface FooterItemProps { text: string href?: string isSourceCode?: boolean + isEmailActions?: boolean } -const FooterItem: React.FC = ({ icon, text, href, isSourceCode }) => { +const FooterItem: React.FC = ({ + icon, + text, + href, + isSourceCode, + isEmailActions +}) => { + const [snackbarOpen, setSnackbarOpen] = React.useState(false) + const emailAddress = 'support@linkedcreds.allskillsscount.com' + const textStyle = { color: '#ffffff', fontFamily: 'Nunito Sans, sans-serif', @@ -287,6 +304,64 @@ const FooterItem: React.FC = ({ icon, text, href, isSourceCode https://github.com/Cooperation-org/linked-claims-author + ) : isEmailActions ? ( + + {emailAddress} + + + + setSnackbarOpen(false)} + message='Email copied to clipboard' + anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }} + /> + ) : ( {text} )} diff --git a/app/components/hamburgerMenu/HamburgerMenu.tsx b/app/components/hamburgerMenu/HamburgerMenu.tsx index 359ec6a2..e5a94f6a 100644 --- a/app/components/hamburgerMenu/HamburgerMenu.tsx +++ b/app/components/hamburgerMenu/HamburgerMenu.tsx @@ -28,7 +28,13 @@ const HamburgerMenu = () => { return ( <> @@ -37,11 +43,13 @@ const HamburgerMenu = () => { {/* Header Section */} @@ -62,9 +70,9 @@ const HamburgerMenu = () => { @@ -327,7 +335,7 @@ const HamburgerMenu = () => { - + { - {/* Logo and Name */} - + @@ -60,27 +63,34 @@ const NavBar = () => { LinkedCreds - + {/* Navigation Links and Sign Button */} {session && ( @@ -95,12 +105,13 @@ const NavBar = () => { > Add a New Skill @@ -123,12 +134,13 @@ const NavBar = () => { > Import Skill Credential @@ -151,12 +163,13 @@ const NavBar = () => { > My Skills @@ -179,12 +192,13 @@ const NavBar = () => { > Analytics @@ -202,19 +216,29 @@ const NavBar = () => { - + Help & FAQ {isActive('/help') && ( - + )} @@ -225,13 +249,16 @@ const NavBar = () => { {session ? ( - - + )} @@ -230,50 +306,188 @@ const GenericCredentialViewer: React.FC = ({ {/* Issuer Information */} {issuer.name && ( - + Issued By - {issuer.name} + + {issuer.name} + {issuer.url && ( - + {issuer.url} )} {issuer.email && ( - + {issuer.email} )} )} - {/* Subject/Achievement Information */} - {subject.achievement && ( + {/* Subject/Achievement Information (handles array or object) */} + {subject?.achievement && ( - + Achievement Details - {subject.achievement.name && ( - + {subject?.achievement?.name && ( + {subject.achievement.name} )} - {subject.achievement.description && ( - {subject.achievement.description} + {subject?.achievement?.description && ( + + {subject.achievement.description} + )} - {subject.achievement.criteria && ( + {getCriteriaText() && ( - Criteria: - {typeof subject.achievement.criteria === 'string' ? ( - {subject.achievement.criteria} - ) : ( - subject.achievement.criteria.narrative && ( - {subject.achievement.criteria.narrative} - ) - )} + + Criteria: + + + {getCriteriaText()} + + + )} + + )} + + {/* Additional info (issuer/subject IDs) */} + {(issuerId || subjectId) && ( + + + Identifiers + + {issuerId && ( + + + Issuer ID: + + + {issuerId} + + + )} + {subjectId && ( + + + Subject ID: + + + {subjectId} + )} @@ -282,12 +496,23 @@ const GenericCredentialViewer: React.FC = ({ {/* Dates */} {credential.issuanceDate && ( - + Issued: {new Date(credential.issuanceDate).toLocaleDateString()} )} {credential.expirationDate && ( - + Expires: {new Date(credential.expirationDate).toLocaleDateString()} )} @@ -295,41 +520,83 @@ const GenericCredentialViewer: React.FC = ({ {/* Credential Status */} - + Credential Status - - + + - Has a valid digital signature + + Has a valid digital signature + + {credential.credentialStatus && ( - - + + - Has credential status information + + Has credential status information + )} {/* Raw JSON Preview (collapsed by default) */}
- View Raw JSON + + View Raw JSON + -
+          
             {JSON.stringify(credential, null, 2)}
           
diff --git a/e2e/credential-creation.spec.ts b/e2e/credential-creation.spec.ts index 4c5f5b85..5826ad7c 100644 --- a/e2e/credential-creation.spec.ts +++ b/e2e/credential-creation.spec.ts @@ -8,16 +8,14 @@ test.describe('Credential Creation', () => { test('credential form page loads', async ({ page }) => { await expect(page).toHaveURL(/.*credentialForm.*/); - // Check for Google Drive connection step or form elements - // Use .first() to avoid strict mode violation when multiple elements match + // Form is dynamically loaded (ssr: false), so wait for Step 0 or form to appear const googleDriveText = page.getByText(/first.*login.*google.*drive/i).first(); + const continueButton = page.getByRole('button', { name: /continue without saving/i }); const form = page.locator('form').first(); - // Either the Google Drive step text or the form should be visible - const hasGoogleDriveStep = await googleDriveText.isVisible().catch(() => false); - const hasForm = await form.isVisible().catch(() => false); - - expect(hasGoogleDriveStep || hasForm).toBeTruthy(); + await expect( + googleDriveText.or(continueButton).or(form).first() + ).toBeVisible({ timeout: 15000 }); }); test('can navigate through form steps', async ({ page }) => { @@ -78,17 +76,27 @@ test.describe('Credential Creation', () => { }); test('form validation works', async ({ page }) => { + // Wait for form to load (dynamically loaded with ssr: false) + await expect( + page.getByText(/first.*login.*google.*drive/i).or( + page.getByRole('button', { name: /continue without saving/i }) + ).first() + ).toBeVisible({ timeout: 15000 }); + const continueButton = page.getByRole('button', { name: /continue without saving/i }); if (await continueButton.isVisible()) { - await continueButton.click(); - await page.waitForTimeout(1000); + await continueButton.scrollIntoViewIfNeeded(); + await continueButton.click({ force: true }); + await page.waitForTimeout(500); // Allow Slide transition (timeout 500ms) } - - const nextButton = page.getByRole('button', { name: /next|continue/i }); - - await expect(nextButton).toBeVisible(); - - await expect(nextButton).toBeDisabled(); + // Wait for Step 1 - use label or input (label may render first) + await expect( + page.locator('input[name="fullName"]').or(page.getByText(/please confirm your name|name \(required\)/i)).first() + ).toBeVisible({ timeout: 10000 }); + + // Next button is in the form footer - use locator to avoid matching "Continue without Saving" + const nextButton = page.locator('button').filter({ hasText: /^next$/i }); + await expect(nextButton).toBeVisible({ timeout: 5000 }); const errorMessages = page.getByText(/required|please enter|invalid/i); const hasErrors = await errorMessages.isVisible().catch(() => false);