@learncard/react 3.0.0 → 3.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/{BoostGenericCard-8767d920.js → BoostGenericCard-87cd2091.js} +3 -3
- package/dist/cjs/{BoostGenericCard-8767d920.js.map → BoostGenericCard-87cd2091.js.map} +1 -1
- package/dist/cjs/{CertificateDisplayCard-1d4bca84.js → CertificateDisplayCard-d4300c7d.js} +16 -11
- package/dist/cjs/CertificateDisplayCard-d4300c7d.js.map +1 -0
- package/dist/cjs/{MeritBadgeDisplayCard-0b5241e4.js → MeritBadgeDisplayCard-30ef639d.js} +16 -10
- package/dist/cjs/MeritBadgeDisplayCard-30ef639d.js.map +1 -0
- package/dist/cjs/{MeritBadgeSkillsCount-0bd63bcd.js → MeritBadgeSkillsCount-a6127a40.js} +2 -2
- package/dist/cjs/{MeritBadgeSkillsCount-0bd63bcd.js.map → MeritBadgeSkillsCount-a6127a40.js.map} +1 -1
- package/dist/cjs/{SchoolIDCard-0462e21c.js → SchoolIDCard-20e87b6a.js} +3 -1184
- package/dist/cjs/SchoolIDCard-20e87b6a.js.map +1 -0
- package/dist/cjs/{VCCard-c4767894.js → VCCard-679fceaa.js} +3 -3
- package/dist/cjs/{VCCard-c4767894.js.map → VCCard-679fceaa.js.map} +1 -1
- package/dist/cjs/{VCDisplayBackFace-c73ce3f6.js → VCDisplayBackFace-65e36d2b.js} +3 -3
- package/dist/cjs/{VCDisplayBackFace-c73ce3f6.js.map → VCDisplayBackFace-65e36d2b.js.map} +1 -1
- package/dist/cjs/{VCDisplayCard-7832f605.js → VCDisplayCard-51e917ab.js} +3 -3
- package/dist/cjs/{VCDisplayCard-7832f605.js.map → VCDisplayCard-51e917ab.js.map} +1 -1
- package/dist/cjs/{VCDisplayCard2-7ae4e64b.js → VCDisplayCard2-9567ffe2.js} +9 -8
- package/dist/cjs/{VCDisplayCard2-7ae4e64b.js.map → VCDisplayCard2-9567ffe2.js.map} +1 -1
- package/dist/cjs/{VCVerificationPill-0f27e8e0.js → VCVerificationPill-d4ee15ca.js} +2 -2
- package/dist/cjs/{VCVerificationPill-0f27e8e0.js.map → VCVerificationPill-d4ee15ca.js.map} +1 -1
- package/dist/cjs/{VerifierStateBadgeAndText-6bc07df3.js → VerifierStateBadgeAndText-04c90074.js} +2 -2
- package/dist/cjs/{VerifierStateBadgeAndText-6bc07df3.js.map → VerifierStateBadgeAndText-04c90074.js.map} +1 -1
- package/dist/cjs/{credential.helpers-da94e829.js → credential.helpers-14544e48.js} +240 -6
- package/dist/cjs/credential.helpers-14544e48.js.map +1 -0
- package/dist/cjs/{i18n-ddbb18e7.js → i18n-0648eec1.js} +3 -1
- package/dist/cjs/i18n-0648eec1.js.map +1 -0
- package/dist/cjs/index-47efbf5b.js +1186 -0
- package/dist/cjs/index-47efbf5b.js.map +1 -0
- package/dist/cjs/{index-da255c9a.js → index-a03dec43.js} +3 -1
- package/dist/{esm/index-cb3a593b.js.map → cjs/index-a03dec43.js.map} +1 -1
- package/dist/cjs/index.js +16 -15
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/index14.js +16 -15
- package/dist/cjs/index14.js.map +1 -1
- package/dist/cjs/index22.js +7 -7
- package/dist/cjs/index31.js +2 -1
- package/dist/cjs/index31.js.map +1 -1
- package/dist/cjs/index39.js +13 -12
- package/dist/cjs/index39.js.map +1 -1
- package/dist/cjs/index4.js +5 -5
- package/dist/cjs/index40.js +5 -5
- package/dist/cjs/index41.js +6 -6
- package/dist/cjs/index42.js +9 -8
- package/dist/cjs/index42.js.map +1 -1
- package/dist/cjs/index43.js +2 -2
- package/dist/cjs/index46.js +2 -2
- package/dist/cjs/index7.js +6 -6
- package/dist/cjs/{types.esm-ad9cf6de.js → types.esm-55f8d7fb.js} +3633 -1573
- package/dist/cjs/types.esm-55f8d7fb.js.map +1 -0
- package/dist/esm/{BoostGenericCard-fec5d413.js → BoostGenericCard-dd2b515a.js} +3 -3
- package/dist/esm/{BoostGenericCard-fec5d413.js.map → BoostGenericCard-dd2b515a.js.map} +1 -1
- package/dist/esm/{CertificateDisplayCard-c0748577.js → CertificateDisplayCard-46c13c49.js} +16 -11
- package/dist/esm/CertificateDisplayCard-46c13c49.js.map +1 -0
- package/dist/esm/{MeritBadgeDisplayCard-0433ad9d.js → MeritBadgeDisplayCard-9f95dd08.js} +16 -10
- package/dist/esm/MeritBadgeDisplayCard-9f95dd08.js.map +1 -0
- package/dist/esm/{MeritBadgeSkillsCount-434bc6ed.js → MeritBadgeSkillsCount-c20f9f60.js} +2 -2
- package/dist/esm/{MeritBadgeSkillsCount-434bc6ed.js.map → MeritBadgeSkillsCount-c20f9f60.js.map} +1 -1
- package/dist/esm/{SchoolIDCard-de65b2bd.js → SchoolIDCard-d7b84a54.js} +2 -1183
- package/dist/esm/SchoolIDCard-d7b84a54.js.map +1 -0
- package/dist/esm/{VCCard-c8816edc.js → VCCard-b49261ab.js} +3 -3
- package/dist/esm/{VCCard-c8816edc.js.map → VCCard-b49261ab.js.map} +1 -1
- package/dist/esm/{VCDisplayBackFace-b6451634.js → VCDisplayBackFace-eb2cad59.js} +3 -3
- package/dist/esm/{VCDisplayBackFace-b6451634.js.map → VCDisplayBackFace-eb2cad59.js.map} +1 -1
- package/dist/esm/{VCDisplayCard-31a8f581.js → VCDisplayCard-1400714d.js} +3 -3
- package/dist/esm/{VCDisplayCard-31a8f581.js.map → VCDisplayCard-1400714d.js.map} +1 -1
- package/dist/esm/{VCDisplayCard2-8ef4a6b6.js → VCDisplayCard2-b6ff7873.js} +9 -8
- package/dist/esm/{VCDisplayCard2-8ef4a6b6.js.map → VCDisplayCard2-b6ff7873.js.map} +1 -1
- package/dist/esm/{VCVerificationPill-bd76c5bc.js → VCVerificationPill-65f7901a.js} +2 -2
- package/dist/esm/{VCVerificationPill-bd76c5bc.js.map → VCVerificationPill-65f7901a.js.map} +1 -1
- package/dist/esm/{VerifierStateBadgeAndText-12066b2a.js → VerifierStateBadgeAndText-1588afeb.js} +2 -2
- package/dist/esm/{VerifierStateBadgeAndText-12066b2a.js.map → VerifierStateBadgeAndText-1588afeb.js.map} +1 -1
- package/dist/esm/{credential.helpers-f55a4962.js → credential.helpers-4c72d304.js} +240 -6
- package/dist/esm/credential.helpers-4c72d304.js.map +1 -0
- package/dist/esm/{i18n-35209bfa.js → i18n-b142d48e.js} +3 -1
- package/dist/esm/i18n-b142d48e.js.map +1 -0
- package/dist/esm/{index-cb3a593b.js → index-75c5b01c.js} +2 -2
- package/dist/{cjs/index-da255c9a.js.map → esm/index-75c5b01c.js.map} +1 -1
- package/dist/esm/index-cbd2e9db.js +1184 -0
- package/dist/esm/index-cbd2e9db.js.map +1 -0
- package/dist/esm/index.js +16 -15
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index14.js +16 -15
- package/dist/esm/index14.js.map +1 -1
- package/dist/esm/index22.js +7 -7
- package/dist/esm/index31.js +2 -1
- package/dist/esm/index31.js.map +1 -1
- package/dist/esm/index39.js +13 -12
- package/dist/esm/index39.js.map +1 -1
- package/dist/esm/index4.js +5 -5
- package/dist/esm/index40.js +5 -5
- package/dist/esm/index41.js +6 -6
- package/dist/esm/index42.js +9 -8
- package/dist/esm/index42.js.map +1 -1
- package/dist/esm/index43.js +2 -2
- package/dist/esm/index46.js +2 -2
- package/dist/esm/index7.js +6 -6
- package/dist/esm/{types.esm-383b69a5.js → types.esm-a70402df.js} +3633 -1573
- package/dist/esm/types.esm-a70402df.js.map +1 -0
- package/package.json +9 -9
- package/src/components/CertificateDisplayCard/CertificateAwardDate.test.tsx +67 -0
- package/src/components/CertificateDisplayCard/CertificateBackFace.tsx +14 -7
- package/src/components/CertificateDisplayCard/CertificateFrontFace.tsx +11 -2
- package/src/components/MeritBadgeDisplayCard/MeritBadgeBackFace.tsx +14 -7
- package/src/components/MeritBadgeDisplayCard/MeritBadgeFrontFace.tsx +11 -6
- package/src/helpers/credential.helpers.test.ts +96 -0
- package/src/helpers/credential.helpers.tsx +23 -4
- package/src/i18n/index.tsx +2 -0
- package/dist/cjs/CertificateDisplayCard-1d4bca84.js.map +0 -1
- package/dist/cjs/MeritBadgeDisplayCard-0b5241e4.js.map +0 -1
- package/dist/cjs/SchoolIDCard-0462e21c.js.map +0 -1
- package/dist/cjs/credential.helpers-da94e829.js.map +0 -1
- package/dist/cjs/i18n-ddbb18e7.js.map +0 -1
- package/dist/cjs/types.esm-ad9cf6de.js.map +0 -1
- package/dist/esm/CertificateDisplayCard-c0748577.js.map +0 -1
- package/dist/esm/MeritBadgeDisplayCard-0433ad9d.js.map +0 -1
- package/dist/esm/SchoolIDCard-de65b2bd.js.map +0 -1
- package/dist/esm/credential.helpers-f55a4962.js.map +0 -1
- package/dist/esm/i18n-35209bfa.js.map +0 -1
- package/dist/esm/types.esm-383b69a5.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@learncard/react",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"main": "dist/cjs",
|
|
6
6
|
"module": "dist/esm",
|
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
"author": "Learning Economy Foundation (www.learningeconomy.io)",
|
|
26
26
|
"license": "MIT",
|
|
27
27
|
"devDependencies": {
|
|
28
|
-
"@learncard/types": "5.
|
|
28
|
+
"@learncard/types": "5.24.0",
|
|
29
29
|
"@rollup/plugin-commonjs": "^22.0.0",
|
|
30
30
|
"@rollup/plugin-json": "^4.1.0",
|
|
31
31
|
"@rollup/plugin-node-resolve": "^13.3.0",
|
|
32
|
-
"@storybook/addon-essentials": "8.6.
|
|
33
|
-
"@storybook/react-vite": "8.6.
|
|
34
|
-
"@storybook/test": "8.6.
|
|
32
|
+
"@storybook/addon-essentials": "8.6.19",
|
|
33
|
+
"@storybook/react-vite": "8.6.19",
|
|
34
|
+
"@storybook/test": "8.6.19",
|
|
35
35
|
"@svgr/rollup": "^6.2.1",
|
|
36
36
|
"@testing-library/jest-dom": "^5.16.4",
|
|
37
37
|
"@testing-library/react": "^13.0.0",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"dts-bundle-generator": "^6.10.0",
|
|
45
45
|
"esbuild": "^0.27.1",
|
|
46
46
|
"glob": "^8.0.3",
|
|
47
|
-
"happy-dom": "^
|
|
47
|
+
"happy-dom": "^21.0.4",
|
|
48
48
|
"postcss": "^8.5.10",
|
|
49
49
|
"postcss-loader": "^6.2.1",
|
|
50
50
|
"rollup": "^2.71.1",
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
55
55
|
"rollup-plugin-smart-asset": "^2.1.2",
|
|
56
56
|
"rollup-plugin-styles": "^4.0.0",
|
|
57
|
-
"storybook": "8.6.
|
|
57
|
+
"storybook": "8.6.19",
|
|
58
58
|
"style-loader": "^3.3.1",
|
|
59
59
|
"tailwindcss": "^4.3.3",
|
|
60
60
|
"vite": "^6.4.3",
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
"@tailwindcss/node": "^4.3.3"
|
|
63
63
|
},
|
|
64
64
|
"dependencies": {
|
|
65
|
-
"@learncard/helpers": "1.7.
|
|
66
|
-
"@learncard/init": "2.5.
|
|
65
|
+
"@learncard/helpers": "1.7.1",
|
|
66
|
+
"@learncard/init": "2.5.7",
|
|
67
67
|
"date-fns": "^2.28.0",
|
|
68
68
|
"numeral": "^2.0.6",
|
|
69
69
|
"pretty-bytes": "^6.0.0",
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @vitest-environment jsdom
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import { render, screen, cleanup } from '@testing-library/react';
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest';
|
|
5
|
+
import CertificateFrontFace from './CertificateFrontFace';
|
|
6
|
+
import CertificateBackFace from './CertificateBackFace';
|
|
7
|
+
import MeritBadgeFrontFace from '../MeritBadgeDisplayCard/MeritBadgeFrontFace';
|
|
8
|
+
import MeritBadgeBackFace from '../MeritBadgeDisplayCard/MeritBadgeBackFace';
|
|
9
|
+
import { I18nProvider } from '../../i18n';
|
|
10
|
+
import type { VC } from '@learncard/types';
|
|
11
|
+
|
|
12
|
+
const credential = {
|
|
13
|
+
'@context': ['https://www.w3.org/ns/credentials/v2'],
|
|
14
|
+
type: ['VerifiableCredential', 'OpenBadgeCredential'],
|
|
15
|
+
issuer: { id: 'did:example:issuer', name: 'Example school' },
|
|
16
|
+
validFrom: '2026-10-26T12:00:00Z',
|
|
17
|
+
awardedDate: '2027-10-01T00:00:00Z',
|
|
18
|
+
credentialSubject: {
|
|
19
|
+
id: 'did:example:learner',
|
|
20
|
+
achievement: { name: 'Web Foundations', description: 'Completed the course.' },
|
|
21
|
+
},
|
|
22
|
+
} as unknown as VC;
|
|
23
|
+
|
|
24
|
+
afterEach(cleanup);
|
|
25
|
+
|
|
26
|
+
describe.each([
|
|
27
|
+
['certificate front', (value: VC) => <CertificateFrontFace credential={value} />],
|
|
28
|
+
[
|
|
29
|
+
'certificate back',
|
|
30
|
+
(value: VC) => <CertificateBackFace credential={value} verificationItems={[]} />,
|
|
31
|
+
],
|
|
32
|
+
['award front', (value: VC) => <MeritBadgeFrontFace credential={value} />],
|
|
33
|
+
['award back', (value: VC) => <MeritBadgeBackFace credential={value} verificationItems={[]} />],
|
|
34
|
+
] as const)('%s award date', (_, renderFace) => {
|
|
35
|
+
it('shows the mapped award day instead of the issuance day', () => {
|
|
36
|
+
render(renderFace(credential));
|
|
37
|
+
expect(screen.getByText(/^Awarded on Oct 01, 2027(?: by)?$/)).toBeTruthy();
|
|
38
|
+
expect(screen.queryByText(/Awarded on Oct 26, 2026/)).toBeNull();
|
|
39
|
+
});
|
|
40
|
+
it('retains the legacy issuance-date fallback', () => {
|
|
41
|
+
render(renderFace({ ...credential, awardedDate: undefined }));
|
|
42
|
+
expect(screen.getByText(/^Awarded on Oct 26, 2026(?: by)?$/)).toBeTruthy();
|
|
43
|
+
});
|
|
44
|
+
it('retains the issuance-date fallback when the award date is invalid', () => {
|
|
45
|
+
render(renderFace({ ...credential, awardedDate: 'not-a-date' }));
|
|
46
|
+
expect(screen.getByText(/^Awarded on Oct 26, 2026(?: by)?$/)).toBeTruthy();
|
|
47
|
+
});
|
|
48
|
+
it('omits the date for an undated draft', () => {
|
|
49
|
+
render(renderFace({ ...credential, awardedDate: undefined, validFrom: undefined }));
|
|
50
|
+
expect(screen.queryByText(/Awarded on/)).toBeNull();
|
|
51
|
+
});
|
|
52
|
+
it('resolves the award label through the host translation adapter', () => {
|
|
53
|
+
render(
|
|
54
|
+
<I18nProvider
|
|
55
|
+
locale="fr"
|
|
56
|
+
resolve={(key, params) =>
|
|
57
|
+
key.startsWith('credential.awardedOn')
|
|
58
|
+
? `Décernée le ${params?.date}`
|
|
59
|
+
: undefined
|
|
60
|
+
}
|
|
61
|
+
>
|
|
62
|
+
{renderFace(credential)}
|
|
63
|
+
</I18nProvider>
|
|
64
|
+
);
|
|
65
|
+
expect(screen.getByText('Décernée le Oct 01, 2027')).toBeTruthy();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { useT } from '../../i18n';
|
|
2
3
|
import { getCategoryDarkColor, getInfoFromCredential } from '../../helpers/credential.helpers';
|
|
3
4
|
import { VC, VerificationItem } from '@learncard/types';
|
|
4
5
|
import {
|
|
@@ -44,8 +45,10 @@ export const CertificateBackFace: React.FC<CertificateBackFaceProps> = ({
|
|
|
44
45
|
handleViewFrontFace,
|
|
45
46
|
customLinkedCredentialsComponent,
|
|
46
47
|
}) => {
|
|
48
|
+
const t = useT();
|
|
47
49
|
const { createdAt, credentialSubject } = getInfoFromCredential(credential, 'MMM dd, yyyy', {
|
|
48
50
|
uppercaseDate: false,
|
|
51
|
+
dateType: 'awarded',
|
|
49
52
|
});
|
|
50
53
|
|
|
51
54
|
const { description } = credentialSubject?.achievement ?? {};
|
|
@@ -83,13 +86,17 @@ export const CertificateBackFace: React.FC<CertificateBackFaceProps> = ({
|
|
|
83
86
|
)}
|
|
84
87
|
|
|
85
88
|
<TruncateTextBox headerText="About" text={description}>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
{createdAt && (
|
|
90
|
+
<span
|
|
91
|
+
className={`text-grayscale-600 font-poppins text-[12px] font-[600] w-full ${
|
|
92
|
+
description
|
|
93
|
+
? 'pt-[10px] border-t border-solid border-grayscale-200'
|
|
94
|
+
: ''
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
{t('credential.awardedOn', { date: createdAt })}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
93
100
|
</TruncateTextBox>
|
|
94
101
|
|
|
95
102
|
{criteria && <TruncateTextBox headerText="Criteria" text={criteria} />}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { useT } from '../../i18n';
|
|
2
3
|
|
|
3
4
|
import CertificateCornerIcon from './CertificateCornerIcon';
|
|
4
5
|
import CertificateImageDisplay from './CertificateImageDisplay';
|
|
@@ -68,6 +69,7 @@ export const CertificateFrontFace: React.FC<CertificateFrontFaceProps> = ({
|
|
|
68
69
|
hideFrontFaceDetails,
|
|
69
70
|
onVerifierClick,
|
|
70
71
|
}) => {
|
|
72
|
+
const t = useT();
|
|
71
73
|
const {
|
|
72
74
|
title = '',
|
|
73
75
|
createdAt,
|
|
@@ -75,7 +77,10 @@ export const CertificateFrontFace: React.FC<CertificateFrontFaceProps> = ({
|
|
|
75
77
|
issuee: _issuee = '',
|
|
76
78
|
credentialSubject,
|
|
77
79
|
imageUrl,
|
|
78
|
-
} = getInfoFromCredential(credential, 'MMM dd, yyyy', {
|
|
80
|
+
} = getInfoFromCredential(credential, 'MMM dd, yyyy', {
|
|
81
|
+
uppercaseDate: false,
|
|
82
|
+
dateType: 'awarded',
|
|
83
|
+
});
|
|
79
84
|
|
|
80
85
|
const issuee = issueeOverride || _issuee;
|
|
81
86
|
const issuer = issuerOverride || _issuer;
|
|
@@ -218,7 +223,11 @@ export const CertificateFrontFace: React.FC<CertificateFrontFaceProps> = ({
|
|
|
218
223
|
</>
|
|
219
224
|
)}
|
|
220
225
|
</span>
|
|
221
|
-
{!hideIssueDate &&
|
|
226
|
+
{!hideIssueDate && createdAt && (
|
|
227
|
+
<span className="font-jacques">
|
|
228
|
+
{t('credential.awardedOn', { date: createdAt })}
|
|
229
|
+
</span>
|
|
230
|
+
)}
|
|
222
231
|
</div>
|
|
223
232
|
|
|
224
233
|
<div className="flex flex-col gap-[10px] items-center">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { useT } from '../../i18n';
|
|
2
3
|
import { getCategoryDarkColor, getInfoFromCredential } from '../../helpers/credential.helpers';
|
|
3
4
|
import { VC, VerificationItem } from '@learncard/types';
|
|
4
5
|
import {
|
|
@@ -44,8 +45,10 @@ export const MeritBadgeBackFace: React.FC<MeritBadgeBackFaceProps> = ({
|
|
|
44
45
|
handleViewFrontFace,
|
|
45
46
|
customLinkedCredentialsComponent,
|
|
46
47
|
}) => {
|
|
48
|
+
const t = useT();
|
|
47
49
|
const { createdAt, credentialSubject } = getInfoFromCredential(credential, 'MMM dd, yyyy', {
|
|
48
50
|
uppercaseDate: false,
|
|
51
|
+
dateType: 'awarded',
|
|
49
52
|
});
|
|
50
53
|
|
|
51
54
|
const { description } = credentialSubject?.achievement ?? {};
|
|
@@ -83,13 +86,17 @@ export const MeritBadgeBackFace: React.FC<MeritBadgeBackFaceProps> = ({
|
|
|
83
86
|
)}
|
|
84
87
|
|
|
85
88
|
<TruncateTextBox headerText="About" text={description}>
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
89
|
+
{createdAt && (
|
|
90
|
+
<span
|
|
91
|
+
className={`text-grayscale-600 font-poppins text-[12px] font-[600] w-full ${
|
|
92
|
+
description
|
|
93
|
+
? 'pt-[10px] border-t border-solid border-grayscale-200'
|
|
94
|
+
: ''
|
|
95
|
+
}`}
|
|
96
|
+
>
|
|
97
|
+
{t('credential.awardedOn', { date: createdAt })}
|
|
98
|
+
</span>
|
|
99
|
+
)}
|
|
93
100
|
</TruncateTextBox>
|
|
94
101
|
|
|
95
102
|
{criteria && <TruncateTextBox headerText="Criteria" text={criteria} />}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
+
import { useT } from '../../i18n';
|
|
2
3
|
|
|
3
4
|
import MeritBadgeCornerIcon from './MeritBadgeCornerIcon';
|
|
4
5
|
import MeritBadgeImageDisplay from './MeritBadgeImageDisplay';
|
|
@@ -66,6 +67,7 @@ export const MeritBadgeFrontFace: React.FC<MeritBadgeFrontFaceProps> = ({
|
|
|
66
67
|
hideFrontFaceDetails,
|
|
67
68
|
onVerifierClick,
|
|
68
69
|
}) => {
|
|
70
|
+
const t = useT();
|
|
69
71
|
const {
|
|
70
72
|
title = '',
|
|
71
73
|
createdAt,
|
|
@@ -73,7 +75,10 @@ export const MeritBadgeFrontFace: React.FC<MeritBadgeFrontFaceProps> = ({
|
|
|
73
75
|
issuee: _issuee = '',
|
|
74
76
|
credentialSubject,
|
|
75
77
|
imageUrl,
|
|
76
|
-
} = getInfoFromCredential(credential, 'MMM dd, yyyy', {
|
|
78
|
+
} = getInfoFromCredential(credential, 'MMM dd, yyyy', {
|
|
79
|
+
uppercaseDate: false,
|
|
80
|
+
dateType: 'awarded',
|
|
81
|
+
});
|
|
77
82
|
|
|
78
83
|
const issuee = issueeOverride || _issuee;
|
|
79
84
|
const issuer = issuerOverride || _issuer;
|
|
@@ -205,11 +210,11 @@ export const MeritBadgeFrontFace: React.FC<MeritBadgeFrontFaceProps> = ({
|
|
|
205
210
|
<div className="flex flex-col w-full">
|
|
206
211
|
<div className="text-[14px] text-grayscale-900 flex flex-col items-center w-full mb-[10px]">
|
|
207
212
|
<span className="font-jacques flex gap-[5px] items-center w-full text-ellipsis whitespace-nowrap overflow-hidden justify-center">
|
|
208
|
-
{issueeName === '0 person'
|
|
209
|
-
'Not yet awarded'
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
+
{issueeName === '0 person'
|
|
214
|
+
? 'Not yet awarded'
|
|
215
|
+
: createdAt
|
|
216
|
+
? t('credential.awardedOnBy', { date: createdAt })
|
|
217
|
+
: t('credential.by')}
|
|
213
218
|
</span>
|
|
214
219
|
</div>
|
|
215
220
|
|
|
@@ -106,3 +106,99 @@ describe('Credential Helpers', () => {
|
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
108
|
});
|
|
109
|
+
|
|
110
|
+
describe('award-date formatting', () => {
|
|
111
|
+
it.each(['awarded', 'issuance'] as const)(
|
|
112
|
+
'falls back safely for a large malformed award date in %s mode',
|
|
113
|
+
dateType => {
|
|
114
|
+
const options = { uppercaseDate: false, dateType } as const;
|
|
115
|
+
const malformed = ' '.repeat(100_000);
|
|
116
|
+
expect(
|
|
117
|
+
getInfoFromCredential(
|
|
118
|
+
{ ...VC2CredentialWithValidFrom, awardedDate: malformed } as never,
|
|
119
|
+
'MMM dd, yyyy',
|
|
120
|
+
options
|
|
121
|
+
).createdAt
|
|
122
|
+
).toBe(
|
|
123
|
+
getInfoFromCredential(VC2CredentialWithValidFrom as never, 'MMM dd, yyyy', options)
|
|
124
|
+
.createdAt
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
it.each(['2027-10-01T00:30+02', '2027-10-01T00:30+0200', '2027-10-01T00:30+02:00'])(
|
|
129
|
+
'recognizes an explicit award timezone in %s',
|
|
130
|
+
awardedDate => {
|
|
131
|
+
expect(
|
|
132
|
+
getInfoFromCredential(
|
|
133
|
+
{ ...VC2CredentialWithValidFrom, awardedDate } as never,
|
|
134
|
+
'MMM dd, yyyy',
|
|
135
|
+
{ uppercaseDate: false, dateType: 'awarded' }
|
|
136
|
+
).createdAt
|
|
137
|
+
).toBe('Sep 30, 2027');
|
|
138
|
+
}
|
|
139
|
+
);
|
|
140
|
+
it.each(['20271001', '2027-10-01T00:00', '2027-10-01T00:00:00', '2027-10-01T20:00:00'])(
|
|
141
|
+
'shows a consistent calendar award day for %s',
|
|
142
|
+
awardedDate => {
|
|
143
|
+
expect(
|
|
144
|
+
getInfoFromCredential(
|
|
145
|
+
{ ...VC2CredentialWithValidFrom, awardedDate } as never,
|
|
146
|
+
'MMM dd, yyyy',
|
|
147
|
+
{ uppercaseDate: false, dateType: 'awarded' }
|
|
148
|
+
).createdAt
|
|
149
|
+
).toBe('Oct 01, 2027');
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
it('uses the standard award date only when requested and retains issuance metadata', () => {
|
|
153
|
+
const credential = { ...VC2CredentialWithValidFrom, awardedDate: '2027-10-01T00:00:00Z' };
|
|
154
|
+
expect(
|
|
155
|
+
getInfoFromCredential(credential as never, 'MMM dd, yyyy', {
|
|
156
|
+
uppercaseDate: false,
|
|
157
|
+
dateType: 'awarded',
|
|
158
|
+
}).createdAt
|
|
159
|
+
).toBe('Oct 01, 2027');
|
|
160
|
+
expect(getInfoFromCredential(credential as never).createdAt).toBe(
|
|
161
|
+
getInfoFromCredential(VC2CredentialWithValidFrom as never).createdAt
|
|
162
|
+
);
|
|
163
|
+
});
|
|
164
|
+
it('falls back for missing or invalid award dates without inventing a date for an undated draft', () => {
|
|
165
|
+
expect(
|
|
166
|
+
getInfoFromCredential(
|
|
167
|
+
{ ...VC2CredentialWithValidFrom, awardedDate: 'not-a-date' } as never,
|
|
168
|
+
'MMM dd, yyyy',
|
|
169
|
+
{ uppercaseDate: false, dateType: 'awarded' }
|
|
170
|
+
).createdAt
|
|
171
|
+
).toBe(
|
|
172
|
+
getInfoFromCredential(VC2CredentialWithValidFrom as never, 'MMM dd, yyyy', {
|
|
173
|
+
uppercaseDate: false,
|
|
174
|
+
}).createdAt
|
|
175
|
+
);
|
|
176
|
+
expect(
|
|
177
|
+
getInfoFromCredential(VC2CredentialNoDate as never, 'MMM dd, yyyy', {
|
|
178
|
+
dateType: 'awarded',
|
|
179
|
+
}).createdAt
|
|
180
|
+
).toBe('');
|
|
181
|
+
});
|
|
182
|
+
it('keeps the UTC award day stable at a timezone boundary', () => {
|
|
183
|
+
expect(
|
|
184
|
+
getInfoFromCredential(
|
|
185
|
+
{
|
|
186
|
+
...VC2CredentialWithValidFrom,
|
|
187
|
+
awardedDate: '2027-10-01T23:30:00-02:00',
|
|
188
|
+
} as never,
|
|
189
|
+
'MMM dd, yyyy',
|
|
190
|
+
{ uppercaseDate: false, dateType: 'awarded' }
|
|
191
|
+
).createdAt
|
|
192
|
+
).toBe('Oct 02, 2027');
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('treats a date-only award as a UTC calendar day', () => {
|
|
197
|
+
expect(
|
|
198
|
+
getInfoFromCredential(
|
|
199
|
+
{ ...VC2CredentialWithValidFrom, awardedDate: '2027-10-01' } as never,
|
|
200
|
+
'MMM dd, yyyy',
|
|
201
|
+
{ uppercaseDate: false, dateType: 'awarded' }
|
|
202
|
+
).createdAt
|
|
203
|
+
).toBe('Oct 01, 2027');
|
|
204
|
+
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { format } from 'date-fns';
|
|
2
|
+
import { format, isValid, parseISO } from 'date-fns';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
5
|
Image,
|
|
@@ -43,7 +43,9 @@ export const getImageFromProfile = (profile: Profile): string => {
|
|
|
43
43
|
export const getInfoFromCredential = (
|
|
44
44
|
credential: VC | AchievementCredential,
|
|
45
45
|
dateFormat: string = 'dd MMM yyyy',
|
|
46
|
-
options: { uppercaseDate?: boolean } = {
|
|
46
|
+
options: { uppercaseDate?: boolean; dateType?: 'issuance' | 'awarded' } = {
|
|
47
|
+
uppercaseDate: true,
|
|
48
|
+
}
|
|
47
49
|
): CredentialInfo => {
|
|
48
50
|
const { issuer, issuanceDate, validFrom } = credential;
|
|
49
51
|
|
|
@@ -57,12 +59,29 @@ export const getInfoFromCredential = (
|
|
|
57
59
|
|
|
58
60
|
// Support both VC 2.0 (validFrom) and VC 1.0 (issuanceDate)
|
|
59
61
|
// Gracefully handle missing dates
|
|
60
|
-
const
|
|
62
|
+
const awardDateText = typeof credential.awardedDate === 'string' ? credential.awardedDate : '';
|
|
63
|
+
const awardedDate =
|
|
64
|
+
options.dateType === 'awarded' && awardDateText ? parseISO(awardDateText) : undefined;
|
|
65
|
+
const validAwardedDate = awardedDate && isValid(awardedDate) ? awardedDate : undefined;
|
|
66
|
+
const dateValue = validAwardedDate ?? (validFrom || issuanceDate);
|
|
61
67
|
let createdAt = '';
|
|
62
68
|
|
|
63
69
|
if (dateValue) {
|
|
64
70
|
try {
|
|
65
|
-
|
|
71
|
+
const date = dateValue instanceof Date ? dateValue : new Date(dateValue);
|
|
72
|
+
// Offset-less ISO values describe a calendar day. Only explicit
|
|
73
|
+
// instants need UTC conversion, matching the Details date helper.
|
|
74
|
+
// Inspect at most six suffix characters; never scan arbitrary input
|
|
75
|
+
// with a backtracking wildcard when looking for a timezone.
|
|
76
|
+
const hasAwardOffset =
|
|
77
|
+
validAwardedDate &&
|
|
78
|
+
(awardDateText.includes('T') || awardDateText.includes(' ')) &&
|
|
79
|
+
/(?:Z|[+-]\d{2}(?::?\d{2})?)$/i.test(awardDateText.slice(-6));
|
|
80
|
+
const displayDate =
|
|
81
|
+
validAwardedDate && hasAwardOffset
|
|
82
|
+
? new Date(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())
|
|
83
|
+
: date;
|
|
84
|
+
createdAt = format(displayDate, dateFormat);
|
|
66
85
|
if (options.uppercaseDate) {
|
|
67
86
|
createdAt = createdAt.toUpperCase();
|
|
68
87
|
}
|
package/src/i18n/index.tsx
CHANGED
|
@@ -45,6 +45,8 @@ export const EN_DEFAULTS: Record<string, string> = {
|
|
|
45
45
|
'credential.close': 'Close',
|
|
46
46
|
'credential.by': 'By',
|
|
47
47
|
'credential.verified': 'Verified Credential',
|
|
48
|
+
'credential.awardedOn': 'Awarded on {date}',
|
|
49
|
+
'credential.awardedOnBy': 'Awarded on {date} by',
|
|
48
50
|
'verification.title': 'Credential Verifications',
|
|
49
51
|
'verification.infoText':
|
|
50
52
|
'Credential verifications check the cryptographic proof of digital credentials to ensure their authenticity and accuracy.',
|