From d04984c9c9fbcffb755a4262af31d20302c746d2 Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Thu, 2 Apr 2026 18:29:42 +0400 Subject: [PATCH 01/66] CORE-5249: add table --- src/assets/icons/successFilledTick.svg | 4 + src/features/Fair/Fair.tsx | 109 ++++++---- .../Fair/FairCertificationFeesTable.tsx | 71 +++++++ .../FairCertificationMembersFeesTable.tsx | 123 +++++++++++ src/features/Fair/fairPricingTypes.ts | 27 +++ src/features/Fair/fairPricingUtils.ts | 24 +++ src/features/Fair/styles.css | 193 +++++++++++++++++- 7 files changed, 512 insertions(+), 39 deletions(-) create mode 100644 src/assets/icons/successFilledTick.svg create mode 100644 src/features/Fair/FairCertificationFeesTable.tsx create mode 100644 src/features/Fair/FairCertificationMembersFeesTable.tsx create mode 100644 src/features/Fair/fairPricingTypes.ts create mode 100644 src/features/Fair/fairPricingUtils.ts diff --git a/src/assets/icons/successFilledTick.svg b/src/assets/icons/successFilledTick.svg new file mode 100644 index 00000000..c66d2b6c --- /dev/null +++ b/src/assets/icons/successFilledTick.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/features/Fair/Fair.tsx b/src/features/Fair/Fair.tsx index b8be0147..8aa1b079 100644 --- a/src/features/Fair/Fair.tsx +++ b/src/features/Fair/Fair.tsx @@ -1,7 +1,13 @@ -import { CrPaper, Markdown } from '@oacore/core-ui'; +import {CrMessage, CrPaper, Markdown} from '@oacore/core-ui'; import { toAbsoluteAssetUrl } from '@/utils/contentUtils'; +import success from '@/assets/icons/successFilledTick.svg'; +import { FairCertificationFeesTable } from './FairCertificationFeesTable'; +import { FairCertificationMembersFeesTable } from './FairCertificationMembersFeesTable'; +import { parseFairPricingTable } from './fairPricingUtils'; import './styles.css'; +export type { FairPricingTable, FairPricingPrice, FairPricingHeader, FairPricingSubRow, FairPricingPriceType } from './fairPricingTypes'; + export type FairCertificationHowItWorks = { title: string; description: string; @@ -31,6 +37,8 @@ export type FairCertificationData = { applicationProcess: { applicationProcess: FairCertificationApplicationProcess }; certificates: { certificates: FairCertificationCertificate[] }; + table?: unknown; + tableMembers?: unknown; }; export type FairFeatureProps = { @@ -41,6 +49,13 @@ export const FairFeature = ({ data }: FairFeatureProps) => { const howItWorks = data.howItWorks.howItWorks; const applicationProcess = data.applicationProcess.applicationProcess; const certificates = data.certificates.certificates; + const bundle = data.table && typeof data.table === 'object' ? (data.table as Record) : undefined; + + const feesTable = + parseFairPricingTable(data.table) ?? parseFairPricingTable(bundle?.table); + + const membersTable = + parseFairPricingTable(data.tableMembers) ?? parseFairPricingTable(bundle?.tableMembers); const howItWorksImageUrl = toAbsoluteAssetUrl(howItWorks.image); @@ -48,43 +63,65 @@ export const FairFeature = ({ data }: FairFeatureProps) => {
-
-
- -
-

- FAIR Repository Certification -

- {/*TODO TEMP*/} -
Demo
- - {howItWorks.description} - -

- +

+
+ +
+

- Read more about CORE FAIR Certification on our website. - - + {/*TODO TEMP*/} +
Demo
+ + {howItWorks.description} + +

+ + Read more about CORE FAIR Certification on our website. + + + Register your interest + + {/*TODO render based on view*/} + + + Your request has been submitted. We will contact you as soon as possible to arrange the payment. After this you will get access to the FAIR certification report. + +

+

+ {/* TODO enable based on condition */} + {(feesTable || membersTable) && ( +
- Register your interest - -

+
+ {feesTable ? : null} + {membersTable ? : null} +
+
+ )}
>; + +export const FairCertificationFeesTable = ({ config }: FairCertificationFeesTableProps) => { + const incomeHeaders = config.headers.filter( + (h): h is FairPricingHeader & { type: 'low' | 'middle' | 'high' } => + h.type === 'low' || h.type === 'middle' || h.type === 'high' + ); + + const dataSource: FeesRecord[] = config.certification.subRows.map((row) => { + const record: FeesRecord = { + key: row.title, + band: ( + <> + {row.title} + {row.caption} + + ), + }; + for (const h of incomeHeaders) { + const price = findPrice(row.prices, h.type); + const original = price?.original; + record[h.type] = original != null ? formatGbp(original) : '—'; + } + return record; + }); + + const columns: ColumnsType = [ + { + title: {config.title}, + align: 'center', + children: config.headers.map((headerCell, idx) => { + const isBand = idx === 0 || headerCell.type == null; + return { + title: ( + {headerCell.name} + ), + dataIndex: (isBand ? 'band' : headerCell.type) as keyof FeesRecord, + align: 'center' as const, + }; + }), + }, + ]; + + return ( +
+ + className="fair-certification-pricing-table" + bordered + pagination={false} + size="middle" + showSorterTooltip={false} + columns={columns} + dataSource={dataSource} + /> +
+ ); +}; diff --git a/src/features/Fair/FairCertificationMembersFeesTable.tsx b/src/features/Fair/FairCertificationMembersFeesTable.tsx new file mode 100644 index 00000000..2385d33a --- /dev/null +++ b/src/features/Fair/FairCertificationMembersFeesTable.tsx @@ -0,0 +1,123 @@ +import type { ReactNode } from 'react'; +import { Table } from 'antd'; +import type { ColumnsType } from 'antd/es/table'; +import { Markdown } from '@oacore/core-ui'; +import type { FairPricingHeader, FairPricingTable } from './fairPricingTypes'; +import { findPrice, formatGbp } from './fairPricingUtils'; + +export type FairCertificationMembersFeesTableProps = { + config: FairPricingTable; +}; + +type MembersRecord = { + key: string; + band: ReactNode; + sustaining?: ReactNode; +} & Partial>; + +export const FairCertificationMembersFeesTable = ({ + config, +}: FairCertificationMembersFeesTableProps) => { + const sustainingHeader = config.headers.find((h) => h.type === 'sustaining'); + const supportingHeaders = config.headers.filter( + (h): h is FairPricingHeader & { type: 'low' | 'middle' | 'high' } => + h.type === 'low' || h.type === 'middle' || h.type === 'high' + ); + const bandHeader = config.headers[0]; + + const dataSource: MembersRecord[] = config.certification.subRows.map((row) => { + const record: MembersRecord = { + key: row.title, + band: ( + <> + {row.title} + {row.caption} + + ), + }; + for (const h of supportingHeaders) { + const price = findPrice(row.prices, h.type); + const discounted = price?.discounted; + const original = price?.original; + record[h.type] = + discounted != null && original != null ? ( +
+ {formatGbp(discounted)} + {formatGbp(original)} +
+ ) : ( + '—' + ); + } + if (sustainingHeader) { + record.sustaining = findPrice(row.prices, 'sustaining')?.free ? ( + Free + ) : ( + '—' + ); + } + return record; + }); + + const secondRowChildren: ColumnsType = [ + { + title: '\u00a0', + align: 'center', + children: [ + { + title: bandHeader ? ( + {bandHeader.name} + ) : null, + dataIndex: 'band', + align: 'center', + }, + ], + }, + { + title: 'Supporting', + align: 'center', + children: supportingHeaders.map((h) => ({ + title: {h.name}, + dataIndex: h.type, + align: 'center' as const, + })), + }, + ]; + + if (sustainingHeader) { + secondRowChildren.push({ + title: sustainingHeader.name, + align: 'center', + children: [ + { + title: , + dataIndex: 'sustaining', + align: 'center' as const, + className: 'fair-certification-pricing-td--sustaining', + }, + ], + }); + } + + const columns: ColumnsType = [ + { + title: {config.title}, + align: 'center', + children: secondRowChildren, + }, + ]; + + return ( +
+ + className="fair-certification-pricing-table fair-certification-pricing-table--members" + bordered + pagination={false} + size="middle" + showSorterTooltip={false} + columns={columns} + dataSource={dataSource} + /> +
+ ); +}; diff --git a/src/features/Fair/fairPricingTypes.ts b/src/features/Fair/fairPricingTypes.ts new file mode 100644 index 00000000..2124f158 --- /dev/null +++ b/src/features/Fair/fairPricingTypes.ts @@ -0,0 +1,27 @@ +export type FairPricingPriceType = 'low' | 'middle' | 'high' | 'sustaining'; + +export type FairPricingPrice = { + type: FairPricingPriceType; + original?: number; + discounted?: number; + free?: boolean; +}; + +export type FairPricingHeader = { + name: string; + type?: FairPricingPriceType; +}; + +export type FairPricingSubRow = { + title: string; + caption: string; + prices: FairPricingPrice[]; +}; + +export type FairPricingTable = { + title: string; + headers: FairPricingHeader[]; + certification: { + subRows: FairPricingSubRow[]; + }; +}; diff --git a/src/features/Fair/fairPricingUtils.ts b/src/features/Fair/fairPricingUtils.ts new file mode 100644 index 00000000..c3f768b3 --- /dev/null +++ b/src/features/Fair/fairPricingUtils.ts @@ -0,0 +1,24 @@ +import type { + FairPricingPrice, + FairPricingPriceType, + FairPricingTable, +} from './fairPricingTypes'; + +/** CMS may send the table object directly or under `table` / `tableMembers`. */ +export const parseFairPricingTable = (value: unknown): FairPricingTable | undefined => { + if (!value || typeof value !== 'object') { + return undefined; + } + const o = value as Record; + if (typeof o.title === 'string' && Array.isArray(o.headers)) { + return value as FairPricingTable; + } + return undefined; +}; + +export const formatGbp = (value: number): string => `£${value.toLocaleString('en-GB')}`; + +export const findPrice = ( + prices: FairPricingPrice[], + type: FairPricingPriceType +): FairPricingPrice | undefined => prices.find((p) => p.type === type); diff --git a/src/features/Fair/styles.css b/src/features/Fair/styles.css index a2d9def0..e450b118 100644 --- a/src/features/Fair/styles.css +++ b/src/features/Fair/styles.css @@ -8,7 +8,7 @@ box-sizing: border-box; width: 100%; margin: 0 0 40px; - padding: 40px 24px 48px; + padding: 40px 100px 48px; background: #f5f5f5; border-radius: 2px; } @@ -203,7 +203,8 @@ color: #212121; font-size: 24px; font-weight: 500; - line-height: 130%; /* 31.2px */ + line-height: 130%; + /* 31.2px */ letter-spacing: 0.036px; } @@ -214,7 +215,8 @@ font-size: 16px; font-style: normal; font-weight: 400; - line-height: 24px; /* 150% */ + line-height: 24px; + /* 150% */ letter-spacing: 0.026px; } @@ -274,6 +276,13 @@ letter-spacing: 0.026px; } +/*success view*/ +.success-certification-intro-text { + gap: 14px; + color: var(--color-text-secondary) !important; + background: #ECF6DF; +} + @media (max-width: 1280px) { .certificate-item-wrapper { grid-template-columns: repeat(2, 1fr); @@ -284,4 +293,182 @@ .certificate-item-wrapper { grid-template-columns: 1fr; } + .fair-certification-intro { + padding: 20px 16px; + } +} + +/* FAIR Certification pricing tables */ +.fair-certification-pricing { + box-sizing: border-box; + width: 100%; +} + +.fair-certification-pricing-tables { + display: flex; + width: 100%; + flex-direction: column; + gap: 40px; +} + +@media (max-width: 767px) { + .fair-certification-pricing-tables { + gap: 28px; + } +} + +/* + * Scroll: only horizontal on narrow viewports. With overflow-x: auto, overflow-y must not be + * `visible` (it computes to auto and adds a vertical scrollbar). Use hidden — height still grows with content. + */ +.fair-certification-pricing-table-responsive { + width: 100%; +} + +@media (max-width: 767px) { + .fair-certification-pricing-table-responsive { + overflow-x: auto; + overflow-y: hidden; + -webkit-overflow-scrolling: touch; + } + + .fair-certification-pricing-table-responsive > .fair-certification-pricing-table.ant-table-wrapper { + width: max-content; + min-width: 100%; + } +} + +.fair-certification-pricing-table.ant-table-wrapper { + width: 100%; +} + +.fair-certification-pricing-table.ant-table-wrapper .ant-table { + width: 100%; + color: #212121; + font-size: 15px; + line-height: 1.35; + background: #fff; +} + +.fair-certification-pricing-table.ant-table-wrapper .ant-table-container { + border-radius: 0; +} + +@media (min-width: 768px) { + .fair-certification-pricing-table-responsive .fair-certification-pricing-table.ant-table-wrapper .ant-table { + table-layout: fixed; + } +} + +@media (max-width: 767px) { + .fair-certification-pricing-table-responsive .fair-certification-pricing-table.ant-table-wrapper .ant-table { + table-layout: auto; + } +} + +.fair-certification-pricing-table.ant-table-wrapper .ant-table-thead > tr > th, +.fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody > tr > td { + border-color: #d0d0d0; + padding: 12px 14px; + text-align: center; + vertical-align: middle; +} + +.fair-certification-pricing-table.ant-table-wrapper .ant-table-thead > tr > th { + background: #fff; + font-weight: 700; +} + +.fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody > tr > td { + font-variant-numeric: tabular-nums; +} + +.fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody > tr.ant-table-row:hover > td { + background: #fff; +} + +@media (max-width: 767px) { + .fair-certification-pricing-table-responsive .fair-certification-pricing-table.ant-table-wrapper .ant-table-thead > tr > th, + .fair-certification-pricing-table-responsive .fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody > tr > td { + padding: 8px 10px; + font-size: 13px; + } + + .fair-certification-pricing-table-responsive .fair-certification-table-title-row { + font-size: 14px; + } + + .fair-certification-pricing-table-responsive .fair-certification-band-caption { + font-size: 12px; + } + + .fair-certification-pricing-table-responsive .fair-certification-pricing-header-md { + font-size: 12px; + } +} + +.fair-certification-table-title-row { + font-size: 16px; + font-weight: 700; + text-align: center; +} + +.fair-certification-pricing-th-empty { + display: block; + min-height: 1em; +} + +.fair-certification-pricing-header-md { + display: block; + margin: 0; + text-align: center; +} + +.fair-certification-pricing-header-md p { + margin: 0; +} + +.fair-certification-pricing-header-md a { + color: inherit; + text-decoration: underline; + text-underline-offset: 2px; +} + +.fair-certification-band-title { + display: block; +} + +.fair-certification-band-caption { + display: block; + margin-top: 4px; + font-weight: 400; + font-size: 13px; + color: #424242; +} + +.fair-certification-price-stack { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; +} + +.fair-certification-price-discounted { + color: #a65e17; + font-weight: 600; +} + +.fair-certification-price-struck { + color: #212121; + text-decoration: line-through; + font-weight: 400; +} + +.fair-certification-pricing-td--sustaining { + vertical-align: middle; +} + +.fair-certification-price-free { + color: #a65e17; + font-weight: 700; } From 2603028130a71ff0488924bff5b1aceb71f0d8ec Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Fri, 3 Apr 2026 14:16:28 +0400 Subject: [PATCH 02/66] CORE: fair header --- src/assets/img/certificatePlaceholder.svg | 52 ++++ src/features/Fair/Fair.tsx | 222 +++++++++--------- .../Fair/components/ApprovedFairView.tsx | 50 ++++ .../FairCertificationFeesTable.tsx | 4 +- .../FairCertificationMembersFeesTable.tsx | 4 +- .../Fair/{ => hooks}/fairPricingUtils.ts | 2 +- src/features/Fair/styles.css | 35 +++ src/features/Fair/texts/fair.json | 10 + .../Fair/{ => types}/fairPricingTypes.ts | 0 src/pages/FAIRCertification.tsx | 7 +- 10 files changed, 270 insertions(+), 116 deletions(-) create mode 100644 src/assets/img/certificatePlaceholder.svg create mode 100644 src/features/Fair/components/ApprovedFairView.tsx rename src/features/Fair/{ => components}/FairCertificationFeesTable.tsx (93%) rename src/features/Fair/{ => components}/FairCertificationMembersFeesTable.tsx (95%) rename src/features/Fair/{ => hooks}/fairPricingUtils.ts (95%) create mode 100644 src/features/Fair/texts/fair.json rename src/features/Fair/{ => types}/fairPricingTypes.ts (100%) diff --git a/src/assets/img/certificatePlaceholder.svg b/src/assets/img/certificatePlaceholder.svg new file mode 100644 index 00000000..2c535a86 --- /dev/null +++ b/src/assets/img/certificatePlaceholder.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/features/Fair/Fair.tsx b/src/features/Fair/Fair.tsx index 8aa1b079..3938e9b6 100644 --- a/src/features/Fair/Fair.tsx +++ b/src/features/Fair/Fair.tsx @@ -1,12 +1,12 @@ -import {CrMessage, CrPaper, Markdown} from '@oacore/core-ui'; +import {CrFeatureLayout, CrMessage, CrPaper, Markdown} from '@oacore/core-ui'; import { toAbsoluteAssetUrl } from '@/utils/contentUtils'; import success from '@/assets/icons/successFilledTick.svg'; -import { FairCertificationFeesTable } from './FairCertificationFeesTable'; -import { FairCertificationMembersFeesTable } from './FairCertificationMembersFeesTable'; -import { parseFairPricingTable } from './fairPricingUtils'; +import { FairCertificationFeesTable } from './components/FairCertificationFeesTable.tsx'; +import { FairCertificationMembersFeesTable } from './components/FairCertificationMembersFeesTable.tsx'; +import { parseFairPricingTable } from './hooks/fairPricingUtils.ts'; import './styles.css'; -export type { FairPricingTable, FairPricingPrice, FairPricingHeader, FairPricingSubRow, FairPricingPriceType } from './fairPricingTypes'; +export type { FairPricingTable, FairPricingPrice, FairPricingHeader, FairPricingSubRow, FairPricingPriceType } from './types/fairPricingTypes.ts'; export type FairCertificationHowItWorks = { title: string; @@ -60,118 +60,120 @@ export const FairFeature = ({ data }: FairFeatureProps) => { const howItWorksImageUrl = toAbsoluteAssetUrl(howItWorks.image); return ( - -
-
-
-
-
- -
-

- FAIR Repository Certification -

- {/*TODO TEMP*/} -
Demo
- - {howItWorks.description} - -

- - Read more about CORE FAIR Certification on our website. - - + +

+
+
+
+
+ +
+

- Register your interest - - {/*TODO render based on view*/} - + {/*TODO TEMP*/} +
Demo
+ + {howItWorks.description} + +

+ + Read more about CORE FAIR Certification on our website. + + + Register your interest + + {/*TODO render based on view*/} + + + Your request has been submitted. We will contact you as soon as possible to arrange the payment. After this you will get access to the FAIR certification report. + +

+

+ {/* TODO enable based on condition */} + {(feesTable || membersTable) && ( +
- - Your request has been submitted. We will contact you as soon as possible to arrange the payment. After this you will get access to the FAIR certification report. - -

+
+ {feesTable ? : null} + {membersTable ? : null} +
+
+ )}
- {/* TODO enable based on condition */} - {(feesTable || membersTable) && ( -
-
- {feesTable ? : null} - {membersTable ? : null} -
-
- )} -
-
-
-

+
- How to Get Started -

-
- {applicationProcess.steps.map((step, index) => ( -
-
+

+ How to Get Started +

+
+ {applicationProcess.steps.map((step, index) => ( +
+
{step.step ?? index + 1} - -
-
-

{step.title}

- - {step.description} - + +
+
+

{step.title}

+ + {step.description} + +
-
- ))} -
-
-
-

- CORE FAIR Repository Certificates -

-
- {certificates.map((cert, index) => ( -
- -

{cert.title}

- {cert.description} -
- ))} -
-
-
-
+ ))} +
+ +
+

+ CORE FAIR Repository Certificates +

+
+ {certificates.map((cert, index) => ( +
+ +

{cert.title}

+ {cert.description} +
+ ))} +
+
+ +
+ ); }; diff --git a/src/features/Fair/components/ApprovedFairView.tsx b/src/features/Fair/components/ApprovedFairView.tsx new file mode 100644 index 00000000..872298c0 --- /dev/null +++ b/src/features/Fair/components/ApprovedFairView.tsx @@ -0,0 +1,50 @@ +import {CrFeatureLayout, CrPaper, Markdown} from '@oacore/core-ui'; +import placeholder from '@/assets/img/certificatePlaceholder.svg'; + +import '../styles.css'; +import {Button} from 'antd'; + +import fairTexts from '../texts/fair.json'; + +// TODO: replace with API data (status, dates, submission count, actions). +export const ApprovedFairView = () => { + const { approvedView } = fairTexts; + + return ( + + +
+ + +
+
+
+

{approvedView.title}

+ + {approvedView.certificationDescription} + + + {approvedView.lastReportUpdateLine} + + + {approvedView.submissionsLine} + +
+
+ +
+
+
+
+ ); +}; diff --git a/src/features/Fair/FairCertificationFeesTable.tsx b/src/features/Fair/components/FairCertificationFeesTable.tsx similarity index 93% rename from src/features/Fair/FairCertificationFeesTable.tsx rename to src/features/Fair/components/FairCertificationFeesTable.tsx index ba9d9a6f..450acd49 100644 --- a/src/features/Fair/FairCertificationFeesTable.tsx +++ b/src/features/Fair/components/FairCertificationFeesTable.tsx @@ -2,8 +2,8 @@ import type { ReactNode } from 'react'; import { Table } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { Markdown } from '@oacore/core-ui'; -import type { FairPricingHeader, FairPricingTable } from './fairPricingTypes'; -import { findPrice, formatGbp } from './fairPricingUtils'; +import type { FairPricingHeader, FairPricingTable } from '../types/fairPricingTypes.ts'; +import { findPrice, formatGbp } from '../hooks/fairPricingUtils.ts'; export type FairCertificationFeesTableProps = { config: FairPricingTable; diff --git a/src/features/Fair/FairCertificationMembersFeesTable.tsx b/src/features/Fair/components/FairCertificationMembersFeesTable.tsx similarity index 95% rename from src/features/Fair/FairCertificationMembersFeesTable.tsx rename to src/features/Fair/components/FairCertificationMembersFeesTable.tsx index 2385d33a..37da0e0b 100644 --- a/src/features/Fair/FairCertificationMembersFeesTable.tsx +++ b/src/features/Fair/components/FairCertificationMembersFeesTable.tsx @@ -2,8 +2,8 @@ import type { ReactNode } from 'react'; import { Table } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import { Markdown } from '@oacore/core-ui'; -import type { FairPricingHeader, FairPricingTable } from './fairPricingTypes'; -import { findPrice, formatGbp } from './fairPricingUtils'; +import type { FairPricingHeader, FairPricingTable } from '../types/fairPricingTypes.ts'; +import { findPrice, formatGbp } from '../hooks/fairPricingUtils.ts'; export type FairCertificationMembersFeesTableProps = { config: FairPricingTable; diff --git a/src/features/Fair/fairPricingUtils.ts b/src/features/Fair/hooks/fairPricingUtils.ts similarity index 95% rename from src/features/Fair/fairPricingUtils.ts rename to src/features/Fair/hooks/fairPricingUtils.ts index c3f768b3..ac1a9a85 100644 --- a/src/features/Fair/fairPricingUtils.ts +++ b/src/features/Fair/hooks/fairPricingUtils.ts @@ -2,7 +2,7 @@ import type { FairPricingPrice, FairPricingPriceType, FairPricingTable, -} from './fairPricingTypes'; +} from '../types/fairPricingTypes.ts'; /** CMS may send the table object directly or under `table` / `tableMembers`. */ export const parseFairPricingTable = (value: unknown): FairPricingTable | undefined => { diff --git a/src/features/Fair/styles.css b/src/features/Fair/styles.css index e450b118..fe9b5528 100644 --- a/src/features/Fair/styles.css +++ b/src/features/Fair/styles.css @@ -472,3 +472,38 @@ color: #a65e17; font-weight: 700; } +/*header cer*/ +.fair-button-wrapper { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 8px; + margin-bottom: 48px; +} + +.fair-certification-header-wrapper { + display: flex; + justify-content: space-between; + align-items: center; + gap: 50px; +} + +.fair-certification-header-inner-wrapper { + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: center; +} + +.fair-certification-title { + font-size: 32px; + font-style: normal; + font-weight: 500; + line-height: 130%; + margin: 0; +} + +.fair-certification-description { + margin: 10px 0 26px 0; + color: #666; +} diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json new file mode 100644 index 00000000..32e67632 --- /dev/null +++ b/src/features/Fair/texts/fair.json @@ -0,0 +1,10 @@ +{ + "approvedView": { + "title": "FAIR certification: Not certified", + "certificationDescription": "*Your repository is not certified yet. In the report below you can review the estimation to what extent your repository correspond to the FAIR principles.*", + "lastReportUpdateLine": "**Date of the last report update:** 26.05.2025", + "submissionsLine": "**Number of submissions:** 0", + "aboutButtonLabel": "About CORE FAIR Certification", + "downloadReportButtonLabel": "Download report" + } +} diff --git a/src/features/Fair/fairPricingTypes.ts b/src/features/Fair/types/fairPricingTypes.ts similarity index 100% rename from src/features/Fair/fairPricingTypes.ts rename to src/features/Fair/types/fairPricingTypes.ts diff --git a/src/pages/FAIRCertification.tsx b/src/pages/FAIRCertification.tsx index 9297a789..0a3d007b 100644 --- a/src/pages/FAIRCertification.tsx +++ b/src/pages/FAIRCertification.tsx @@ -2,6 +2,7 @@ import { useDocumentTitle } from '@hooks/useDocumentTitle.ts'; import { useState, useEffect, useRef } from 'react'; import retrieveContent from '@/utils/retrieveContent'; import { FairFeature, type FairCertificationData } from '@features/Fair/Fair.tsx'; +// import {ApprovedFairView} from '@features/Fair/components/ApprovedFairView.tsx'; const loadFairCertification = async (ref?: string): Promise => { return (await retrieveContent('fair-certification', { @@ -30,6 +31,10 @@ export function FAIRCertificationPage() { if (!stateData) { return null; } + // TODO RENDER VIEW BASED ON STATUS - return ; + return ( + // true ? : + + ); } From 0fce0acd6a0cb05166fcc4833eecfd71012fd123 Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Mon, 6 Apr 2026 17:03:50 +0400 Subject: [PATCH 03/66] CORE: add dropdowns --- .../Fair/components/ApprovedFairView.tsx | 42 +--- .../Fair/components/FairDocHeader.tsx | 45 ++++ .../components/FairPrinciplesCollapse.tsx | 232 ++++++++++++++++++ src/features/Fair/styles.css | 163 ++++++++++-- src/features/Fair/texts/fair.json | 41 ++++ 5 files changed, 459 insertions(+), 64 deletions(-) create mode 100644 src/features/Fair/components/FairDocHeader.tsx create mode 100644 src/features/Fair/components/FairPrinciplesCollapse.tsx diff --git a/src/features/Fair/components/ApprovedFairView.tsx b/src/features/Fair/components/ApprovedFairView.tsx index 872298c0..e19bc32f 100644 --- a/src/features/Fair/components/ApprovedFairView.tsx +++ b/src/features/Fair/components/ApprovedFairView.tsx @@ -1,49 +1,17 @@ -import {CrFeatureLayout, CrPaper, Markdown} from '@oacore/core-ui'; -import placeholder from '@/assets/img/certificatePlaceholder.svg'; - +import {CrFeatureLayout, CrPaper} from '@oacore/core-ui'; import '../styles.css'; -import {Button} from 'antd'; -import fairTexts from '../texts/fair.json'; +import {FairDocHeader} from '@features/Fair/components/FairDocHeader.tsx'; +import {FairPrinciplesCollapse} from '@features/Fair/components/FairPrinciplesCollapse.tsx'; // TODO: replace with API data (status, dates, submission count, actions). export const ApprovedFairView = () => { - const { approvedView } = fairTexts; return ( -
- - -
-
-
-

{approvedView.title}

- - {approvedView.certificationDescription} - - - {approvedView.lastReportUpdateLine} - - - {approvedView.submissionsLine} - -
-
- -
-
+ +
); diff --git a/src/features/Fair/components/FairDocHeader.tsx b/src/features/Fair/components/FairDocHeader.tsx new file mode 100644 index 00000000..02d58380 --- /dev/null +++ b/src/features/Fair/components/FairDocHeader.tsx @@ -0,0 +1,45 @@ +import fairTexts from '@features/Fair/texts/fair.json'; +import {Markdown} from '@oacore/core-ui'; +import {Button} from 'antd'; +import placeholder from '@/assets/img/certificatePlaceholder.svg'; +import '../styles.css'; + +export const FairDocHeader = () => { + const { approvedView } = fairTexts; + + return ( + <> +
+ + +
+
+
+

{approvedView.title}

+ + {approvedView.certificationDescription} + + + {approvedView.lastReportUpdateLine} + + + {approvedView.submissionsLine} + +
+
+ +
+
+ + ); +}; diff --git a/src/features/Fair/components/FairPrinciplesCollapse.tsx b/src/features/Fair/components/FairPrinciplesCollapse.tsx new file mode 100644 index 00000000..c7992794 --- /dev/null +++ b/src/features/Fair/components/FairPrinciplesCollapse.tsx @@ -0,0 +1,232 @@ +import {DownOutlined} from '@ant-design/icons'; +import fairTexts from '@features/Fair/texts/fair.json'; +import {Button, Collapse, Form, Input, Typography} from 'antd'; +import type {CollapseProps} from 'antd'; +import {useMemo} from 'react'; + +import '../styles.css'; + +export type FairPrinciplesCollapseProps = { + onSave?: () => void; + onSubmit?: () => void; +}; + +const {Title, Paragraph} = Typography; + +export const FairPrinciplesCollapse = ({ + onSave, + onSubmit, +}: FairPrinciplesCollapseProps) => { + const {principlesAccordion} = fairTexts; + const [operationalForm] = Form.useForm(); + + const handleSave = () => { + onSave?.(); + }; + + const handleSubmit = () => { + onSubmit?.(); + }; + + const collapseItems: CollapseProps['items'] = useMemo( + () => [ + { + key: 'findable', + label: ( +
+ + {principlesAccordion.findable.title} + + + {principlesAccordion.findable.description} + +
+ ), + children: ( + + {principlesAccordion.findable.expandedDetail} + + ), + }, + { + key: 'accessible', + label: ( +
+ + {principlesAccordion.accessible.title} + + + {principlesAccordion.accessible.description} + +
+ ), + children: ( + + {principlesAccordion.accessible.expandedDetail} + + ), + }, + { + key: 'interoperable', + label: ( +
+ + {principlesAccordion.interoperable.title} + + + {principlesAccordion.interoperable.description} + +
+ ), + children: ( + + {principlesAccordion.interoperable.expandedDetail} + + ), + }, + { + key: 'reusable', + label: ( +
+ + {principlesAccordion.reusable.title} + + + {principlesAccordion.reusable.description} + +
+ ), + children: ( + + {principlesAccordion.reusable.expandedDetail} + + ), + }, + { + key: 'fitForFunders', + label: ( +
+ + {principlesAccordion.fitForFunders.title} + + + {principlesAccordion.fitForFunders.description} + +
+ ), + children: ( + + {principlesAccordion.fitForFunders.expandedDetail} + + ), + }, + { + key: 'fitForFuture', + label: ( +
+ + {principlesAccordion.fitForFuture.title} + + + {principlesAccordion.fitForFuture.description} + +
+ ), + children: ( + + {principlesAccordion.fitForFuture.expandedDetail} + + ), + }, + { + key: 'operational', + label: ( +
+ + {principlesAccordion.operational.title} + + + {principlesAccordion.operational.description} + +
+ ), + children: ( +
+ + + + + + +
+ ), + }, + ], + [operationalForm, principlesAccordion], + ); + + const renderExpandIcon: NonNullable = (panelProps) => { + const isActive = Boolean(panelProps.isActive); + return ( + + + + ); + }; + + return ( +
+ +
+ + +
+
+ ); +}; diff --git a/src/features/Fair/styles.css b/src/features/Fair/styles.css index fe9b5528..aa09e65c 100644 --- a/src/features/Fair/styles.css +++ b/src/features/Fair/styles.css @@ -9,7 +9,7 @@ width: 100%; margin: 0 0 40px; padding: 40px 100px 48px; - background: #f5f5f5; + background: var(--color-bg-layout); border-radius: 2px; } @@ -57,7 +57,7 @@ } .fair-certification-intro-read-more { - color: #a65e17; + color: var(--color-primary); font-size: 16px; font-weight: 400; line-height: 1.4; @@ -66,7 +66,7 @@ } .fair-certification-intro-read-more:hover { - color: #8a4d12; + color: var(--color-primary-dark); } .fair-certification-intro-register { @@ -76,8 +76,8 @@ min-height: 44px; padding: 8px 16px; border-radius: 2px; - background: #a65e17; - color: #fff; + background: var(--color-primary); + color: var(--color-text-white); font-size: 16px; font-weight: 500; text-decoration: none; @@ -87,8 +87,8 @@ } .fair-certification-intro-register:hover { - background: #8a4d12; - color: #fff; + background: var(--color-primary-dark); + color: var(--color-text-white); } .fair-certification-intro-text p { @@ -110,9 +110,9 @@ margin-bottom: 16px; width: max-content; border-radius: 2px; - background: #8bc34a; + background: var(--success); padding: 3px 7px; - color: #fff; + color: var(--color-text-white); font-size: 16px; font-style: normal; font-weight: 400; @@ -121,7 +121,7 @@ } .fair-certification-markdown { - color: #212121; + color: var(--color-text-secondary);; font-size: 16px; font-weight: 400; line-height: 1.4; @@ -167,8 +167,8 @@ width: 40px; height: 40px; border-radius: 50%; - background: #a65e17; - color: #fff; + background: var(--color-primary); + color: var(--color-text-white); font-size: 18px; font-style: normal; font-weight: 700; @@ -180,7 +180,7 @@ width: 2px; min-height: 24px; margin-top: 8px; - background: #e0e0e0; + background: var(--gray-300); } .fair-certification-step:last-child .fair-certification-step-track-line { @@ -200,7 +200,7 @@ .fair-certification-feature-title { margin: 0 0 16px; font-style: normal; - color: #212121; + color: var(--color-text-secondary);; font-size: 24px; font-weight: 500; line-height: 130%; @@ -211,7 +211,7 @@ .fair-certification-step-markdown p { margin: 0 0 10px; - color: #212121; + color: var(--color-text-secondary);; font-size: 16px; font-style: normal; font-weight: 400; @@ -225,7 +225,7 @@ } .certification-section-title { - color: #212121; + color: var(--color-text-secondary); text-align: center; font-size: 32px; font-style: normal; @@ -247,7 +247,7 @@ align-items: center; gap: 16px; border-radius: 6px; - background: #f5f5f5; + background: var(--color-bg-layout); } .cert-image { @@ -256,7 +256,7 @@ } .certification-title { - color: #212121; + color: var(--color-text-secondary); text-align: center; font-size: 24px; font-style: normal; @@ -267,7 +267,7 @@ } .certification-markdown { - color: #000; + color: var(--color-dark); font-size: 16px; font-style: normal; font-weight: 400; @@ -344,10 +344,10 @@ .fair-certification-pricing-table.ant-table-wrapper .ant-table { width: 100%; - color: #212121; + color: var(--color-text-secondary); font-size: 15px; line-height: 1.35; - background: #fff; + background: var(--color-text-white); } .fair-certification-pricing-table.ant-table-wrapper .ant-table-container { @@ -368,14 +368,14 @@ .fair-certification-pricing-table.ant-table-wrapper .ant-table-thead > tr > th, .fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody > tr > td { - border-color: #d0d0d0; + border-color: #B9B9B9; padding: 12px 14px; text-align: center; vertical-align: middle; } .fair-certification-pricing-table.ant-table-wrapper .ant-table-thead > tr > th { - background: #fff; + background: var(--color-text-white); font-weight: 700; } @@ -384,7 +384,7 @@ } .fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody > tr.ant-table-row:hover > td { - background: #fff; + background: var(--color-text-white); } @media (max-width: 767px) { @@ -454,12 +454,12 @@ } .fair-certification-price-discounted { - color: #a65e17; + color: var(--color-primary); font-weight: 600; } .fair-certification-price-struck { - color: #212121; + color: var(--color-text-secondary); text-decoration: line-through; font-weight: 400; } @@ -469,7 +469,7 @@ } .fair-certification-price-free { - color: #a65e17; + color: var(--color-primary); font-weight: 700; } /*header cer*/ @@ -507,3 +507,112 @@ margin: 10px 0 26px 0; color: #666; } + +/* Approved view — FAIR principles accordion (Ant Design Collapse) */ +.fair-principles-accordion-section { + box-sizing: border-box; + margin-top: 40px; + padding: 32px 0; + border-top: 2px solid var(--color-primary); + border-bottom: 2px solid var(--color-primary); +} + +.fair-principles-collapse { + background: transparent; +} + +.fair-principles-collapse .ant-collapse-item { + margin: 0 0 32px; + border: 1px solid var(--color-border); + border-radius: 8px !important; + background: var(--color-text-white); + overflow: hidden; +} + +.fair-principles-collapse .ant-collapse-item:last-child { + margin-bottom: 0; +} + +.fair-principles-collapse .ant-collapse-item .ant-collapse-header { + align-items: flex-start; + padding: 24px; +} + +.fair-principles-collapse .ant-collapse-header-text { + flex: 1; + min-width: 0; +} + +.fair-principles-collapse .ant-collapse-expand-icon { + display: flex; + align-items: center; + justify-content: center; + padding-inline-end: 0; +} + +.fair-principles-collapse-label { + display: flex; + flex-direction: column; + gap: 8px; + padding-right: 12px; + text-align: left; +} + +.fair-principles-collapse-label .fair-principles-collapse-title { + margin: 0; + color: var(--color-text-secondary); + font-size: 24px; + font-style: normal; + font-weight: 500; + line-height: 130%; + letter-spacing: 0.036px; +} + +.fair-principles-collapse-label .fair-principles-collapse-desc { + margin: 0; + font-size: 14px; + font-weight: 400; + color: var(--color-text-secondary); + font-style: normal; + line-height: 130%; /* 18.2px */ + letter-spacing: 0.042px; +} + + +.fair-principles-collapse .ant-collapse-content-box { + padding: 24px; +} + +/**/ +.fair-principles-collapse-expand-icon { + height: 59px; + border-radius: 4px; + background: var(--color-bg-layout); + transition: transform 0.2s ease; + display: flex; + width: 62px; + padding: 3px 0; + justify-content: center; + align-items: center; + gap: 10px; + align-self: stretch; +} + +.fair-principles-collapse-chevron { + font-size: 16px; + color: var(--color-primary); +} + +.fair-principles-operational-form { + width: 100%; + max-width: 720px; +} + +.fair-principles-accordion-actions { + display: flex; + justify-content: flex-end; + align-items: center; + gap: 14px; + margin-top: 32px; +} + diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json index 32e67632..0900e4d4 100644 --- a/src/features/Fair/texts/fair.json +++ b/src/features/Fair/texts/fair.json @@ -6,5 +6,46 @@ "submissionsLine": "**Number of submissions:** 0", "aboutButtonLabel": "About CORE FAIR Certification", "downloadReportButtonLabel": "Download report" + }, + "principlesAccordion": { + "sectionAriaLabel": "FAIR principles and certification questions", + "saveButtonLabel": "Save", + "submitButtonLabel": "Submit", + "findable": { + "title": "Findable", + "description": "The first step in (re)using data is to find them. Metadata and data should be easy to find for both humans and computers. Machine-readable metadata are essential for automatic discovery of datasets and services, so this is an essential component of the FAIRification process.", + "expandedDetail": "Metadata and data should be easy to find for both humans and computers. Machine-readable metadata are essential for automatic discovery of datasets and services, so this is an essential component of the FAIRification process." + }, + "accessible": { + "title": "Accessible", + "description": "Once the user finds the required data, she/he/they need to know how they can be accessed, possibly including authentication and authorisation.", + "expandedDetail": "Once the user finds the required data, she/he/they need to know how they can be accessed, possibly including authentication and authorisation." + }, + "interoperable": { + "title": "Interoperable", + "description": "The data usually need to be integrated with other data. In addition, the data need to interoperate with applications or workflows for analysis, storage, and processing.", + "expandedDetail": "The data usually need to be integrated with other data. In addition, the data need to interoperate with applications or workflows for analysis, storage, and processing." + }, + "reusable": { + "title": "Reusable", + "description": "The ultimate goal of FAIR is to optimise the reuse of data. To achieve this, metadata and data should be well-described so that they can be replicated and/or combined in different settings.", + "expandedDetail": "The ultimate goal of FAIR is to optimise the reuse of data. To achieve this, metadata and data should be well-described so that they can be replicated and/or combined in different settings." + }, + "fitForFunders": { + "title": "Fit for funders", + "description": "The first step in (re)using data is to find them. Metadata and data should be easy to find for both humans and computers. Machine-readable metadata are essential for automatic discovery of datasets and services, so this is an essential component of the FAIRification process.", + "expandedDetail": "The first step in (re)using data is to find them. Metadata and data should be easy to find for both humans and computers. Machine-readable metadata are essential for automatic discovery of datasets and services, so this is an essential component of the FAIRification process." + }, + "fitForFuture": { + "title": "Fit for future", + "description": "The first step in (re)using data is to find them. Metadata and data should be easy to find for both humans and computers. Machine-readable metadata are essential for automatic discovery of datasets and services, so this is an essential component of the FAIRification process.", + "expandedDetail": "The first step in (re)using data is to find them. Metadata and data should be easy to find for both humans and computers. Machine-readable metadata are essential for automatic discovery of datasets and services, so this is an essential component of the FAIRification process." + }, + "operational": { + "title": "Operational / Environmental considerations", + "description": "We have run an automatic check of your repository. To get certification please answer following questions:", + "questionRepositoryPolicy": "Describe your repository’s data retention and access policy.", + "questionEnvironmental": "Describe any environmental or operational considerations relevant to your repository." + } } } From efcca8c9d6a874930f8fc88e04a7a47e8d3c1d2d Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Tue, 7 Apr 2026 19:01:26 +0400 Subject: [PATCH 04/66] CORE: add progress --- .../Fair/components/ApprovedFairView.tsx | 2 + .../components/FairSubmissionProgress.tsx | 80 +++++++++++ src/features/Fair/styles.css | 128 ++++++++++++++++-- src/features/Fair/texts/fair.json | 7 + 4 files changed, 205 insertions(+), 12 deletions(-) create mode 100644 src/features/Fair/components/FairSubmissionProgress.tsx diff --git a/src/features/Fair/components/ApprovedFairView.tsx b/src/features/Fair/components/ApprovedFairView.tsx index e19bc32f..6f34f7a3 100644 --- a/src/features/Fair/components/ApprovedFairView.tsx +++ b/src/features/Fair/components/ApprovedFairView.tsx @@ -3,6 +3,7 @@ import '../styles.css'; import {FairDocHeader} from '@features/Fair/components/FairDocHeader.tsx'; import {FairPrinciplesCollapse} from '@features/Fair/components/FairPrinciplesCollapse.tsx'; +import {FairSubmissionProgress} from '@features/Fair/components/FairSubmissionProgress.tsx'; // TODO: replace with API data (status, dates, submission count, actions). export const ApprovedFairView = () => { @@ -12,6 +13,7 @@ export const ApprovedFairView = () => { + ); diff --git a/src/features/Fair/components/FairSubmissionProgress.tsx b/src/features/Fair/components/FairSubmissionProgress.tsx new file mode 100644 index 00000000..a5c33374 --- /dev/null +++ b/src/features/Fair/components/FairSubmissionProgress.tsx @@ -0,0 +1,80 @@ +import { CheckOutlined } from '@ant-design/icons'; +import fairTexts from '@features/Fair/texts/fair.json'; +import { Steps } from 'antd'; +import type { StepsProps } from 'antd'; +import classNames from 'classnames'; +import '../styles.css'; + +export type FairSubmissionProgressStep = { + label: string; + /** Middle “unlimited submissions” row: dots icon + muted title. */ + ellipsis?: boolean; +}; + +export type FairSubmissionProgressProps = { + steps?: FairSubmissionProgressStep[]; +}; + +const CheckIcon = () => ( + + + +); + +const EllipsisIcon = () => ( + + + + + +); + +// TODO: replace with API-driven steps when available. +const defaultSteps = ( + copy: typeof fairTexts.submissionProgress, +): FairSubmissionProgressStep[] => [ + { label: copy.firstSubmission }, + { label: copy.secondSubmission }, + { label: copy.unlimitedSubmissions, ellipsis: true }, + { label: copy.gettingCertificate }, +]; + +const toAntItems = ( + steps: FairSubmissionProgressStep[], +): NonNullable => + steps.map(({ label, ellipsis }) => ({ + status: 'finish' as const, + icon: ellipsis ? : , + title: ( + + {label} + + ), + })); + +export const FairSubmissionProgress = ({ + steps: stepsFromProps, +}: FairSubmissionProgressProps) => { + const { submissionProgress } = fairTexts; + const items = toAntItems(stepsFromProps ?? defaultSteps(submissionProgress)); + + return ( +
+

+ {submissionProgress.title} +

+ +
+ ); +}; diff --git a/src/features/Fair/styles.css b/src/features/Fair/styles.css index aa09e65c..faa08975 100644 --- a/src/features/Fair/styles.css +++ b/src/features/Fair/styles.css @@ -121,7 +121,7 @@ } .fair-certification-markdown { - color: var(--color-text-secondary);; + color: var(--color-text-secondary); font-size: 16px; font-weight: 400; line-height: 1.4; @@ -200,7 +200,7 @@ .fair-certification-feature-title { margin: 0 0 16px; font-style: normal; - color: var(--color-text-secondary);; + color: var(--color-text-secondary); font-size: 24px; font-weight: 500; line-height: 130%; @@ -211,7 +211,7 @@ .fair-certification-step-markdown p { margin: 0 0 10px; - color: var(--color-text-secondary);; + color: var(--color-text-secondary); font-size: 16px; font-style: normal; font-weight: 400; @@ -293,6 +293,7 @@ .certificate-item-wrapper { grid-template-columns: 1fr; } + .fair-certification-intro { padding: 20px 16px; } @@ -332,7 +333,7 @@ -webkit-overflow-scrolling: touch; } - .fair-certification-pricing-table-responsive > .fair-certification-pricing-table.ant-table-wrapper { + .fair-certification-pricing-table-responsive>.fair-certification-pricing-table.ant-table-wrapper { width: max-content; min-width: 100%; } @@ -366,30 +367,31 @@ } } -.fair-certification-pricing-table.ant-table-wrapper .ant-table-thead > tr > th, -.fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody > tr > td { +.fair-certification-pricing-table.ant-table-wrapper .ant-table-thead>tr>th, +.fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody>tr>td { border-color: #B9B9B9; padding: 12px 14px; text-align: center; vertical-align: middle; } -.fair-certification-pricing-table.ant-table-wrapper .ant-table-thead > tr > th { +.fair-certification-pricing-table.ant-table-wrapper .ant-table-thead>tr>th { background: var(--color-text-white); font-weight: 700; } -.fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody > tr > td { +.fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody>tr>td { font-variant-numeric: tabular-nums; } -.fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody > tr.ant-table-row:hover > td { +.fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody>tr.ant-table-row:hover>td { background: var(--color-text-white); } @media (max-width: 767px) { - .fair-certification-pricing-table-responsive .fair-certification-pricing-table.ant-table-wrapper .ant-table-thead > tr > th, - .fair-certification-pricing-table-responsive .fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody > tr > td { + + .fair-certification-pricing-table-responsive .fair-certification-pricing-table.ant-table-wrapper .ant-table-thead>tr>th, + .fair-certification-pricing-table-responsive .fair-certification-pricing-table.ant-table-wrapper .ant-table-tbody>tr>td { padding: 8px 10px; font-size: 13px; } @@ -472,6 +474,7 @@ color: var(--color-primary); font-weight: 700; } + /*header cer*/ .fair-button-wrapper { display: flex; @@ -574,7 +577,8 @@ font-weight: 400; color: var(--color-text-secondary); font-style: normal; - line-height: 130%; /* 18.2px */ + line-height: 130%; + /* 18.2px */ letter-spacing: 0.042px; } @@ -616,3 +620,103 @@ margin-top: 32px; } +/* Approved view — submission progress (Ant Design Steps vertical, Frame 1196) */ +.fair-submission-progress { + box-sizing: border-box; + padding-top: 32px; +} + +.fair-submission-progress__title { + margin: 0 0 48px 0; + color: var(--color-text-secondary); + font-style: normal; + font-weight: 500; + line-height: 130%; + letter-spacing: 0.036px; + text-align: left; + font-size: 32px; +} + +/* Custom step icons (grey check circles + ellipsis) */ +.fair-submission-progress__icon-check { + display: flex; + width: 40px; + height: 40px; + align-items: center; + justify-content: center; + border-radius: 50%; + background: var(--color-text-disabled); + color: var(--color-text-white); +} + +.fair-submission-progress__check-icon { + font-size: 14px; +} + +.fair-submission-progress__icon-ellipsis { + display: flex; + width: 40px; + height: 40px; + align-items: center; + justify-content: center; + gap: 3px; +} + +.fair-submission-progress__ellipsis-dot { + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--gray-600); +} + +.fair-submission-progress__step-title { + color: var(--color-text-secondary); + font-style: normal; + font-weight: 500; + font-size: 24px; + line-height: 130%; /* 31.2px */ + letter-spacing: 0.036px; +} + +.fair-submission-progress__step-title--muted { + padding-top: 2px; + font-weight: 400; + color: #666; + font-size: 16px; + font-style: normal; + line-height: 130%; + letter-spacing: 0.026px; +} + +/* Ant Design Steps — vertical layout to match design */ +.fair-submission-progress-steps.ant-steps-vertical .ant-steps-item-finish .ant-steps-item-rail { + border-inline-start-color: var(--color-border); + height: 20px; +} + +.fair-submission-progress-steps .ant-steps-item-custom .ant-steps-item-icon { + width: 40px; + height: 40px; + border: none; + background: transparent; + font-size: inherit; + line-height: 40px; +} + +.fair-submission-progress-steps.ant-steps-vertical .ant-steps-item-finish .ant-steps-item-title { + padding-inline-end: 0; +} + +.fair-submission-progress-steps .ant-steps-item-finish .ant-steps-item-title { + margin-top: 0; +} + +.fair-submission-progress-steps .ant-steps-item-title { + color: var(--color-text-secondary); + font-size: 24px; + font-style: normal; + font-weight: 500; + line-height: 130%; + /* 31.2px */ + letter-spacing: 0.036px; +} diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json index 0900e4d4..339883e6 100644 --- a/src/features/Fair/texts/fair.json +++ b/src/features/Fair/texts/fair.json @@ -1,4 +1,11 @@ { + "submissionProgress": { + "title": "Progress of the submission", + "firstSubmission": "1st submission", + "secondSubmission": "2nd submission", + "unlimitedSubmissions": "Unlimited number of submission", + "gettingCertificate": "Getting certificate" + }, "approvedView": { "title": "FAIR certification: Not certified", "certificationDescription": "*Your repository is not certified yet. In the report below you can review the estimation to what extent your repository correspond to the FAIR principles.*", From 6e7e335a3b8817fbc9534aa15aeb37c9a334627c Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Wed, 8 Apr 2026 15:51:56 +0400 Subject: [PATCH 05/66] CORE: add collapsable data --- .../components/FairPrincipleQuestionBlock.tsx | 77 +++++ .../FairPrincipleSectionContent.tsx | 33 ++ .../components/FairPrinciplesCollapse.tsx | 210 +++--------- .../components/FairPrinciplesCollapsible.tsx | 100 ++++++ src/features/Fair/styles.css | 107 +++++- src/features/Fair/texts/fair.json | 320 +++++++++++++++++- .../Fair/types/fairPrinciples.types.ts | 35 ++ .../utils/getFairQuestionStatusClassName.ts | 16 + 8 files changed, 713 insertions(+), 185 deletions(-) create mode 100644 src/features/Fair/components/FairPrincipleQuestionBlock.tsx create mode 100644 src/features/Fair/components/FairPrincipleSectionContent.tsx create mode 100644 src/features/Fair/components/FairPrinciplesCollapsible.tsx create mode 100644 src/features/Fair/types/fairPrinciples.types.ts create mode 100644 src/features/Fair/utils/getFairQuestionStatusClassName.ts diff --git a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx new file mode 100644 index 00000000..bc4cf548 --- /dev/null +++ b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx @@ -0,0 +1,77 @@ +import {InfoOutlined} from '@ant-design/icons'; +import {Markdown} from '@oacore/core-ui'; +import {Form, Input} from 'antd'; + +import type {FairQuestionItem} from '@features/Fair/types/fairPrinciples.types'; +import {getFairQuestionStatusClassName} from '@features/Fair/utils/getFairQuestionStatusClassName'; + +import '../../Usrn/style.css'; +import '../styles.css'; + +export type FairPrincipleQuestionBlockProps = { + item: FairQuestionItem; + recommendationHeading: string; + openQuestionBadge: string; +}; + +export const FairPrincipleQuestionBlock = ({ + item, + recommendationHeading, + openQuestionBadge, +}: FairPrincipleQuestionBlockProps) => { + const statusClass = getFairQuestionStatusClassName(item.statusLabel); + const displayStatus = item.statusLabel ?? '—'; + const isOpenQuestion = Boolean(item.openQuestion); + + return ( +
+
+
+

{item.code}

+

{item.question}

+
+ {displayStatus} +
+ +
+
+ {item.description} + {item.statusNote ? ( +

+ {item.statusNote} +

+ ) : null} +
+ +
+ + {/*TODO ADD BAR CHART IF HAVE*/} + + + {item.recommendation ? ( +
+ +
+
{recommendationHeading}
+
+ {item.recommendation} +
+
+
+ ) : null} + + {isOpenQuestion ? ( +
+ {openQuestionBadge} + + + +
+ ) : null} +
+ ); +}; diff --git a/src/features/Fair/components/FairPrincipleSectionContent.tsx b/src/features/Fair/components/FairPrincipleSectionContent.tsx new file mode 100644 index 00000000..02592391 --- /dev/null +++ b/src/features/Fair/components/FairPrincipleSectionContent.tsx @@ -0,0 +1,33 @@ +import {FairPrincipleQuestionBlock} from '@features/Fair/components/FairPrincipleQuestionBlock'; +import type {FairPrincipleSection} from '@features/Fair/types/fairPrinciples.types'; + +import '../styles.css'; + +export type FairPrincipleSectionContentProps = { + section: FairPrincipleSection; + recommendationHeading: string; + openQuestionBadge: string; +}; + +export const FairPrincipleSectionContent = ({ + section, + recommendationHeading, + openQuestionBadge, +}: FairPrincipleSectionContentProps) => { + if (!section.items?.length) { + return null; + } + + return ( +
+ {section.items.map((item) => ( + + ))} +
+ ); +}; diff --git a/src/features/Fair/components/FairPrinciplesCollapse.tsx b/src/features/Fair/components/FairPrinciplesCollapse.tsx index c7992794..b2dd7bda 100644 --- a/src/features/Fair/components/FairPrinciplesCollapse.tsx +++ b/src/features/Fair/components/FairPrinciplesCollapse.tsx @@ -1,7 +1,15 @@ -import {DownOutlined} from '@ant-design/icons'; import fairTexts from '@features/Fair/texts/fair.json'; -import {Button, Collapse, Form, Input, Typography} from 'antd'; -import type {CollapseProps} from 'antd'; +import {FairPrincipleSectionContent} from '@features/Fair/components/FairPrincipleSectionContent'; +import { + FairPrinciplesCollapsible, + type FairPrinciplesCollapsibleSection, +} from '@features/Fair/components/FairPrinciplesCollapsible'; +import { + FAIR_PRINCIPLE_SECTION_KEYS, + type FairPrincipleSection, + type FairPrincipleSectionKey, +} from '@features/Fair/types/fairPrinciples.types'; +import {Button, Form, Typography} from 'antd'; import {useMemo} from 'react'; import '../styles.css'; @@ -9,6 +17,8 @@ import '../styles.css'; export type FairPrinciplesCollapseProps = { onSave?: () => void; onSubmit?: () => void; + /** Collapsible presentation: `default` (full FAIR styling) or `compact` (tighter panels). */ + collapsibleVariant?: 'default' | 'compact'; }; const {Title, Paragraph} = Typography; @@ -16,9 +26,13 @@ const {Title, Paragraph} = Typography; export const FairPrinciplesCollapse = ({ onSave, onSubmit, + collapsibleVariant = 'default', }: FairPrinciplesCollapseProps) => { const {principlesAccordion} = fairTexts; - const [operationalForm] = Form.useForm(); + const [openAnswersForm] = Form.useForm(); + + const recommendationHeading = principlesAccordion.recommendationHeading ?? 'Recommendation'; + const openQuestionBadge = principlesAccordion.openQuestionBadge ?? 'Open question'; const handleSave = () => { onSave?.(); @@ -28,186 +42,42 @@ export const FairPrinciplesCollapse = ({ onSubmit?.(); }; - const collapseItems: CollapseProps['items'] = useMemo( - () => [ - { - key: 'findable', - label: ( -
- - {principlesAccordion.findable.title} - - - {principlesAccordion.findable.description} - -
- ), - children: ( - - {principlesAccordion.findable.expandedDetail} - - ), - }, - { - key: 'accessible', - label: ( -
- - {principlesAccordion.accessible.title} - - - {principlesAccordion.accessible.description} - -
- ), - children: ( - - {principlesAccordion.accessible.expandedDetail} - - ), - }, - { - key: 'interoperable', - label: ( -
- - {principlesAccordion.interoperable.title} - - - {principlesAccordion.interoperable.description} - -
- ), - children: ( - - {principlesAccordion.interoperable.expandedDetail} - - ), - }, - { - key: 'reusable', - label: ( -
- - {principlesAccordion.reusable.title} - - - {principlesAccordion.reusable.description} - -
- ), - children: ( - - {principlesAccordion.reusable.expandedDetail} - - ), - }, - { - key: 'fitForFunders', - label: ( -
- - {principlesAccordion.fitForFunders.title} - - - {principlesAccordion.fitForFunders.description} - -
- ), - children: ( - - {principlesAccordion.fitForFunders.expandedDetail} - - ), - }, - { - key: 'fitForFuture', + const collapsibleSections: FairPrinciplesCollapsibleSection[] = useMemo(() => { + return FAIR_PRINCIPLE_SECTION_KEYS.map((key: FairPrincipleSectionKey) => { + const section = principlesAccordion[key] as FairPrincipleSection; + return { + key, label: (
- {principlesAccordion.fitForFuture.title} + {section.title} - - {principlesAccordion.fitForFuture.description} - + {section.description}
), children: ( - - {principlesAccordion.fitForFuture.expandedDetail} - + ), - }, - { - key: 'operational', - label: ( -
- - {principlesAccordion.operational.title} - - - {principlesAccordion.operational.description} - -
- ), - children: ( -
- - - - - - -
- ), - }, - ], - [operationalForm, principlesAccordion], - ); - - const renderExpandIcon: NonNullable = (panelProps) => { - const isActive = Boolean(panelProps.isActive); - return ( - - - - ); - }; + }; + }); + }, [openQuestionBadge, principlesAccordion, recommendationHeading]); return (
- +
+ +
+ + + {displayStatus} - -
+ {item.description &&
{item.description} {item.statusNote ? ( @@ -44,25 +44,13 @@ export const FairPrincipleQuestionBlock = ({
+ } {/*TODO ADD BAR CHART IF HAVE*/} - {item.recommendation ? ( -
- -
-
{recommendationHeading}
-
- {item.recommendation} -
-
-
- ) : null} - {isOpenQuestion ? (
- {openQuestionBadge}
) : null} + + {item.recommendation ? ( +
+ +
+
{recommendationHeading}
+
+ {item.recommendation} +
+
+
+ ) : null}
); }; diff --git a/src/features/Fair/components/FairPrincipleSectionContent.tsx b/src/features/Fair/components/FairPrincipleSectionContent.tsx index 02592391..9c23d8bf 100644 --- a/src/features/Fair/components/FairPrincipleSectionContent.tsx +++ b/src/features/Fair/components/FairPrincipleSectionContent.tsx @@ -6,13 +6,11 @@ import '../styles.css'; export type FairPrincipleSectionContentProps = { section: FairPrincipleSection; recommendationHeading: string; - openQuestionBadge: string; }; export const FairPrincipleSectionContent = ({ section, recommendationHeading, - openQuestionBadge, }: FairPrincipleSectionContentProps) => { if (!section.items?.length) { return null; @@ -24,7 +22,6 @@ export const FairPrincipleSectionContent = ({ ))} diff --git a/src/features/Fair/components/FairPrinciplesCollapse.tsx b/src/features/Fair/components/FairPrinciplesCollapse.tsx index b2dd7bda..823af940 100644 --- a/src/features/Fair/components/FairPrinciplesCollapse.tsx +++ b/src/features/Fair/components/FairPrinciplesCollapse.tsx @@ -32,7 +32,6 @@ export const FairPrinciplesCollapse = ({ const [openAnswersForm] = Form.useForm(); const recommendationHeading = principlesAccordion.recommendationHeading ?? 'Recommendation'; - const openQuestionBadge = principlesAccordion.openQuestionBadge ?? 'Open question'; const handleSave = () => { onSave?.(); @@ -57,14 +56,13 @@ export const FairPrinciplesCollapse = ({ ), children: ( ), }; }); - }, [openQuestionBadge, principlesAccordion, recommendationHeading]); + }, [ principlesAccordion, recommendationHeading]); return (
.ant-collapse-item:last-child { + border: 1px solid var(--color-border); +} + .fair-principles-collapse .ant-collapse-item .ant-collapse-header { align-items: flex-start; padding: 24px; @@ -641,25 +645,14 @@ .fair-principles__panel-body { display: flex; flex-direction: column; - gap: 0; + gap: 24px; } .fair-principles__question { - padding: 8px 0 24px; - border-bottom: 1px solid var(--color-border); -} - -.fair-principles__question:last-child { - padding-bottom: 0; - border-bottom: none; -} - -.fair-principles__question-code { - margin: 0 0 6px; - font-size: 13px; - font-weight: 600; - color: var(--color-text-tertiary, #888); - letter-spacing: 0.02em; + border-radius: 10px; + border: 1px solid var(--gray-300); + background: var(--color-bg-container); + padding: 24px; } .fair-principles__status-note { @@ -692,10 +685,15 @@ line-height: 1.1; } -.fair-principles__open-block { - margin-top: 16px; - padding-top: 16px; - border-top: 1px dashed var(--color-border); +/*.fair-principles__open-block {*/ +/* margin-top: 16px;*/ +/* padding-top: 16px;*/ +/* border-top: 1px dashed var(--color-border);*/ +/*}*/ + +.fair-principles__open-field textarea { + height: 108px; + margin-top: 24px; } .fair-principles__open-badge { @@ -822,4 +820,43 @@ line-height: 130%; /* 31.2px */ letter-spacing: 0.036px; -} \ No newline at end of file +} + +.required-link { + background: #F5F5F5; + min-width: 80px; + text-align: center; + font-weight: 500; + padding: 8px 14px; + border-radius: 2px; + font-size: 16px; + text-transform: capitalize; + align-content: center; + width: max-content; + color: #9E9E9E; +} + +.support-status__row { + display: grid; + grid-template-columns: 1fr auto auto; + gap: 12px; + align-items: stretch; + margin: 15px 0 0 0; +} + +.support-status__question-wrap { + background: #F5F5F5; + display: flex; + align-items: center; + padding: 12px 16px; + color: var(--color-text-secondary); + font-size: 16px; + font-style: normal; + font-weight: 500; + line-height: 130%; /* 20.8px */ + letter-spacing: 0.01px; +} + +.file-icon { + font-size: 20px; +} diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json index 81e5f623..e8e5c297 100644 --- a/src/features/Fair/texts/fair.json +++ b/src/features/Fair/texts/fair.json @@ -46,7 +46,13 @@ "openQuestion": true, "answerPlaceholder": "Write your answer here …", "question": "How many metadata records are there in your repository?", - "description": "**Indexed content**\n\nThe number of records CORE is able to index from your digital repository. If this number doesn't match your records it might mean you have issues in exposing your records in an interoperable way.", + "statusLabel": "Open question" + }, + { + "id": "", + "code": "", + "question": "Indexed content", + "description": "The number of records CORE is able to index from your digital repository. If this number doesn't match your records it might mean you have issues in exposing your records in an interoperable way.", "statusLabel": "Yes" } ] @@ -108,7 +114,7 @@ "code": "R(3.2)", "question": "Do your metadata use COAR vocabularies?", "description": "COAR (Confederation of Open Access Repositories) vocabularies are structured sets of terms organized to provide a common framework for the sharing and integration of data across different systems. They comprise three types: Access rights, resource types and version types.", - "recommendation": "A metadata profile such as RIOXX can provide an easy way to share and use COAR vocabularies in your metadata.\n\n**Access rights:** describe the access status of a resource, such as “embargoed access” or “restricted access”.\n\n**Resource type:** describe the type of resource, such as “patent”, “bibliography” or “conference presentation”.\n\n**Version type:** distinguish the various states of an academic article, including the version of record (VOR), the Author’s Original (AO) and the Author’s Accepted Manuscript (AM).", + "recommendation": "A metadata profile such as RIOXX can provide an easy way to share and use COAR vocabularies in your metadata.\n\nAccess rights: describe the access status of a resource, such as “embargoed access” or “restricted access”.\n\nResource type: describe the type of resource, such as “patent”, “bibliography” or “conference presentation”.\n\nVersion type: distinguish the various states of an academic article, including the version of record (VOR), the Author’s Original (AO) and the Author’s Accepted Manuscript (AM).", "statusLabel": "Yes" }, { @@ -117,8 +123,6 @@ "question": "Does your repository support persistent management of records?", "description": "A repository with persistent management ensures that metadata and content are not lost, that links remain valid and that researchers can reliably cite and access resources over time.", "recommendation": "It is recommended that the repository implements a recognized system for persistent identifiers like DOI or ROR. Clear policies should be in place for record versioning, updates and withdrawal, ensuring transparency and trustworthiness. Documentation of these policies should be openly available to users.", - "openQuestion": true, - "answerPlaceholder": "Write your answer here …", "statusLabel": "Open question" }, { @@ -127,7 +131,7 @@ "question": "Is your repository configured for the OAI resolver and does it work?", "description": "An OAI (Open Archives Initiative) identifier is a unique identifier of a metadata record. CORE can resolve any OAI identifier directly to your repository page.", "statusNote": "Error — We are not able to resolve OAI identifiers from your repository to the repository landing pages.", - "recommendation": "To register your OAI mapping, navigate to the **Settings** tab in the dashboard for your repository. To route directly to the repository, it is necessary to provide mapping in the CORE Repository page between the OAI prefix of a repository and the currently used URL for the repository metadata record display page or splash page. The redirection will change instantly. More information about OAI resolver and its configuration can be found in the CORE Data Provider’s Guide: Repository configuration, OAI identifiers.\n\n**Membership documentation:** Get your OAI identifiers resolved to your repository — see the OAI Identifier documentation page.", + "recommendation": "To register your OAI mapping, navigate to the **Settings** tab in the dashboard for your repository. To route directly to the repository, it is necessary to provide mapping in the CORE Repository page between the OAI prefix of a repository and the currently used URL for the repository metadata record display page or splash page. The redirection will change instantly. More information about OAI resolver and its configuration can be found in the \n- [CORE Data Provider’s Guide: Repository configuration, OAI identifiers](https://core.ac.uk/documentation/data-providers-guide#oai-identifier).\n- [Membership documentation: Get your OAI identifiers resolved to your repository](https://core.ac.uk/documentation/membership-documentation#OAI-identifiers) \n- [OAI Identifier documentation page](https://core.ac.uk/documentation/oai-resolver).", "statusLabel": "No" }, { @@ -165,7 +169,7 @@ "code": "R(4.1)", "question": "Do you provide data availability statements?", "description": "A data availability statement, also sometimes called a ‘data access statement’, is a short, mandatory section in a paper that tells the reader where the research data associated with a paper is available and under what conditions the data can be accessed.", - "recommendation": "Here are some recommendations on how to increase the number of DAS in your repository:\n\n- Include links where applicable to the data set. For example, the paper may state “The data that support the findings of this study are openly available in [repository name] at https://doi.org/[doi]”.\n- Provide a dedicated metadata field for DAS, not just full text in the description.\n- Provide researchers with approved, compliant DAS text for all scenarios.\n\nCORE provides several tools under the DAS identification tab: monitoring statistics about DAS in your repository, and a Data Availability Statement checker.", + "recommendation": "Here are some recommendations on how to increase the number of DAS in your repository:\n\n- Include links where applicable to the data set. For example, the paper may state “The data that support the findings of this study are openly available in [repository name] at https://doi.org/[doi]”.\n- Provide a dedicated metadata field for DAS, not just full text in the description.\n- Provide researchers with approved, compliant DAS text for all scenarios.\n- CORE provides several tools under the [DAS identification tab]():\n\n monitoring statistics about DAS in your repository, and a Data Availability Statement checker.", "statusLabel": "Yes" }, { @@ -190,13 +194,13 @@ }, "fitForFunders": { "title": "Fit for funders", - "description": "Questions in this section help demonstrate that research outputs can be identified and traced by funding bodies, with clear provenance and funding metadata.", + "description": "APPLIES ONLY TO UK REPOS. Only a recommendation for everybody else. The repository records and exposes details about the organizations or agencies that financially supported the research.", "items": [ { "id": "r51", "code": "R(5.1)", "question": "Is your research identifiable by funding bodies?", - "description": "**Applies only to UK repositories.** Only a recommendation for everybody else. The repository records and exposes details about the organizations or agencies that financially supported the research.", + "description": "Applies only to UK repositories. Only a recommendation for everybody else. The repository records and exposes details about the organizations or agencies that financially supported the research.", "recommendation": "This typically includes the funder name, grant number or project ID and ideally a persistent identifier for the funder — a ROR ID or Crossref Funder Registry ID.", "statusLabel": "Yes" }, @@ -271,7 +275,7 @@ "code": "R(7.1)", "question": "How do you manage deposits? Is there someone to check them?", "description": "Deposit management refers to the process the repository uses to handle new submissions of research outputs.", - "recommendation": "Define a clear deposit workflow: self-deposit, mediated deposit or curated. Assign repository staff (curators or managers) to review submissions for metadata quality, copyright, licensing and compliance with institutional and funder policies. Provide support for depositors: templates, checklists, FAQs.\n\n- **Self-deposited** — by researchers, authors or administrative staff.\n- **Mediated** — by librarians or repository managers on behalf of authors.\n- **Curated** — before publication to ensure metadata quality, copyright compliance and adherence to repository policies.", + "recommendation": "Define a clear deposit workflow: self-deposit, mediated deposit or curated. Assign repository staff (curators or managers) to review submissions for metadata quality, copyright, licensing and compliance with institutional and funder policies. Provide support for depositors: templates, checklists, FAQs.\n\n- Self-deposited — by researchers, authors or administrative staff.\n- Mediated — by librarians or repository managers on behalf of authors.\n- Curated — before publication to ensure metadata quality, copyright compliance and adherence to repository policies.", "openQuestion": true, "answerPlaceholder": "Write your answer here …", "statusLabel": "Open question" @@ -300,7 +304,7 @@ "id": "r74", "code": "R(7.4)", "question": "How do you manage the preservation of content in your repository?", - "description": "Preservation of content in a repository refers to the set of policies, workflows and technical measures that ensure digital objects remain **available** (can be found and opened), **authentic** (guaranteed to be original) and **usable** (can be realistically reused).", + "description": "Preservation of content in a repository refers to the set of policies, workflows and technical measures that ensure digital objects remain\n- available (can be found and opened),\n- authentic (guaranteed to be original)\n- usable (can be realistically reused).", "recommendation": "- Define and publish a preservation policy that outlines the repository’s commitment to long-term access and specifies responsibilities.\n- Implement redundant storage and backup strategies: multiple servers, offsite copies, cloud storage.\n- Use fixity checks (checksums, hashes) to regularly verify file integrity and detect corruption.\n- Prefer open and widely used file formats (PDF, TXT, CSV) for better long-term sustainability.\n- Record and expose technical metadata (file format, size, checksum) to support digital preservation workflows.", "openQuestion": true, "answerPlaceholder": "Write your answer here …", @@ -310,7 +314,7 @@ "id": "r75", "code": "R(7.5)", "question": "How do you encourage researchers to get their ORCID?", - "description": "Encouraging researchers to obtain an ORCID ensures that their work is uniquely identifiable and correctly attributed, regardless of name variations, institutional changes or publication platforms.", + "description": "Encouraging researchers to obtain an ORCID (Open Researcher and Contributor ID) ensures that their work is uniquely identifiable and correctly attributed, regardless of name variations, institutional changes or publication platforms. ORCID adoption strengthens the quality of repository metadata, improves interoperability with publishers and funders and helps researchers gain recognition for their contributions.", "recommendation": "- Promote ORCID awareness through training sessions, repository guides and onboarding materials.\n- Provide step-by-step instructions and links for registering an ORCID ID.\n- Enable ORCID authentication and login in the repository so researchers can connect their profile during deposit.\n- Encourage depositors to include their ORCID in the metadata of every submission.\n- Where possible set ORCID as a mandatory or strongly recommended field in deposit workflows.\n- Integrate with the ORCID API to push publications and datasets from the repository directly to a researcher’s ORCID profile.", "openQuestion": true, "answerPlaceholder": "Write your answer here …", @@ -320,8 +324,8 @@ "id": "r76", "code": "R(7.6)", "question": "What are you doing to promote FAIR principles?", - "description": "The repository actively implements policies, technologies and community practices to make research outputs more discoverable, usable and sustainable.", - "recommendation": "- **Clear policies** — Publish policies that explicitly reference FAIR principles.\n- **Metadata standards** — Require rich, standardized metadata (DataCite, Dublin Core, OpenAIRE Guidelines).\n- **Persistent identifiers** — Support DOIs, ORCIDs, RORs and project IDs.\n- **Infrastructure** — Provide OAI-PMH, ResourceSync and APIs.\n- **Licensing and reuse** — Encourage open licenses (CC-BY, CC0).\n- **Data linking** — Promote Data Availability Statements and links to datasets and source code.\n- **Quality control** — Review deposits for metadata completeness.\n- **Awareness and training** — Workshops and guidance for researchers.\n- **Community engagement** — Participate in FAIR-related initiatives (GO FAIR, COAR).", + "description": "The repository actively implements policies, technologies and community practices to make research outputs more discoverable, usable and sustainable. ", + "recommendation": "- Clear policies — Publish policies that explicitly reference FAIR principles.\n- Metadata standards — Require rich, standardized metadata (DataCite, Dublin Core, OpenAIRE Guidelines).\n- Persistent identifiers — Support DOIs, ORCIDs, RORs and project IDs.\n- Infrastructure — Provide OAI-PMH, ResourceSync and APIs.\n- Licensing and reuse — Encourage open licenses (CC-BY, CC0).\n- Data linking — Promote Data Availability Statements and links to datasets and source code.\n- Quality control — Review deposits for metadata completeness.\n- Awareness and training — Workshops and guidance for researchers.\n- Community engagement — Participate in FAIR-related initiatives (GO FAIR, COAR).", "openQuestion": true, "answerPlaceholder": "Write your answer here …", "statusLabel": "Open question" @@ -330,7 +334,7 @@ "id": "r77", "code": "R(7.7)", "question": "What are you doing to promote research reproducibility?", - "description": "Promoting research reproducibility means ensuring that other researchers can verify, replicate and build upon the results deposited in the repository, including linking publications with underlying datasets, methods and documentation.", + "description": "Promoting research reproducibility means ensuring that other researchers can verify, replicate and build upon the results deposited in the repository. This goes beyond simply storing outputs: it requires linking publications with their underlying datasets, methods and documentation, as well as providing transparent metadata and persistent identifiers.", "recommendation": "- Require or encourage Data Availability Statements linking publications to underlying datasets.\n- Enable persistent identifiers (DOIs for publications and datasets, ORCID for authors, ROR for institutions).\n- Support versioning so users can access previous iterations of datasets or papers.\n- Provide metadata fields for methods and protocols.\n- Encourage open licenses so that others can reuse data and code legally.\n- Facilitate interoperability using standard metadata schemas.\n- Promote best practices through guidance, training and templates.\n- Integrate with external registries where relevant.\n- Provide transparency in curation.", "openQuestion": true, "answerPlaceholder": "Write your answer here …", @@ -340,7 +344,7 @@ "id": "r78", "code": "R(7.8)", "question": "How do you support Open Science at your institution?", - "description": "Implementing policies, infrastructure and cultural practices that make research outputs more transparent, accessible and reusable — including open access to publications, sharing research data, supporting reproducibility and enabling collaboration.", + "description": "Implementing policies, infrastructure and cultural practices that make research outputs more transparent, accessible and reusable. This includes promoting open access to publications, sharing research data, supporting reproducibility and enabling collaboration across disciplines and communities.", "recommendation": "- Adopt an institutional Open Science policy aligned with national and European guidelines (EOSC, Plan S).\n- Provide infrastructure such as institutional repositories, data archives and integration with ORCID, DOI and ROR identifiers.\n- Promote FAIR data practices by training researchers in data management, metadata standards and reproducibility.\n- Support Open Access publishing through funding or agreements with publishers or institutional repositories (Green/Gold OA).\n- Encourage Data and Code Availability Statements.\n- Offer training on open research practices, licensing and copyright.\n- Recognize and reward Open Science practices in research evaluation where appropriate.\n- Collaborate with external infrastructures (OpenAIRE, EOSC, COAR).\n- Provide long-term preservation services.\n- Engage the community through workshops and awareness campaigns.", "openQuestion": true, "answerPlaceholder": "Write your answer here …", diff --git a/src/features/Usrn/style.css b/src/features/Usrn/style.css index e310996f..21f54984 100644 --- a/src/features/Usrn/style.css +++ b/src/features/Usrn/style.css @@ -244,20 +244,7 @@ border: none; } -.support-status__row { - display: grid; - grid-template-columns: 1fr auto; - gap: 12px; - align-items: stretch; - margin: 15px 0 0 0; -} -.support-status__question-wrap { - background: #f3f3f3; - display: flex; - align-items: center; - padding: 12px; -} .support-status__question { margin: 0; From 513c66093a6561365acaa0187b1aaa011ed57fdf Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Mon, 27 Apr 2026 13:51:58 +0400 Subject: [PATCH 07/66] CORE: add api sections --- .../Fair/components/ApprovedFairView.tsx | 34 ++++++- .../components/FairPrincipleQuestionBlock.tsx | 17 +++- .../FairPrincipleSectionContent.tsx | 11 ++- .../components/FairPrinciplesCollapse.tsx | 10 +- src/features/Fair/texts/fair.json | 98 +++++++------------ .../Fair/utils/resolveFairQuestionStatus.ts | 86 ++++++++++++++++ .../Usrn/utils/getCardStatusConfig.ts | 4 + src/features/Validator/hooks/useRioxxStats.ts | 4 +- 8 files changed, 190 insertions(+), 74 deletions(-) create mode 100644 src/features/Fair/utils/resolveFairQuestionStatus.ts diff --git a/src/features/Fair/components/ApprovedFairView.tsx b/src/features/Fair/components/ApprovedFairView.tsx index 6f34f7a3..858ddcf0 100644 --- a/src/features/Fair/components/ApprovedFairView.tsx +++ b/src/features/Fair/components/ApprovedFairView.tsx @@ -4,15 +4,45 @@ import '../styles.css'; import {FairDocHeader} from '@features/Fair/components/FairDocHeader.tsx'; import {FairPrinciplesCollapse} from '@features/Fair/components/FairPrinciplesCollapse.tsx'; import {FairSubmissionProgress} from '@features/Fair/components/FairSubmissionProgress.tsx'; +import { useDataProviderStore } from '@/store/dataProviderStore'; +import { useRioxxStats } from '@features/Validator/hooks/useRioxxStats'; +import { useUsrnData } from '@features/Usrn/hooks/useUsrnData'; +import { useIrusStats } from '@/hooks/useIrusStats'; +import { useDataProviderStatistics } from '@/hooks/useDataProviderStatistics'; +import { useDoiStatistics } from '@features/Doi/hooks/useDoiStatistics'; +import type { FairRepositoryStatusParams } from '@features/Fair/utils/resolveFairQuestionStatus'; -// TODO: replace with API data (status, dates, submission count, actions). export const ApprovedFairView = () => { + const { selectedDataProvider, selectedSetSpec, statistics, doiStatistics } = useDataProviderStore(); + const rorId = selectedDataProvider?.rorData?.rorId ?? null; + const { rioxx } = useRioxxStats(selectedDataProvider?.id); + const { usrnData } = useUsrnData(selectedDataProvider?.id); + const { irus } = useIrusStats(selectedDataProvider?.id); + + useDataProviderStatistics(selectedDataProvider?.id ?? null, selectedSetSpec); + useDoiStatistics(selectedDataProvider?.id ?? null, selectedSetSpec); + + const repositoryStatus: FairRepositoryStatusParams = { + rioxx: rioxx ?? undefined, + statistics: statistics ?? undefined, + internalStatistics: + statistics != null || doiStatistics != null + ? { + fullTextCount: statistics?.countFulltext, + metadataCount: statistics?.countMetadata, + doiCount: doiStatistics?.dataProviderDoiCount, + } + : undefined, + usrn: usrnData ?? null, + irus, + rorId, + }; return ( - + diff --git a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx index d59c383e..ea9168f4 100644 --- a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx +++ b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx @@ -4,6 +4,10 @@ import {Form, Input} from 'antd'; import type {FairQuestionItem} from '@features/Fair/types/fairPrinciples.types'; import {getFairQuestionStatusClassName} from '@features/Fair/utils/getFairQuestionStatusClassName'; +import { + resolveFairQuestionStatusLabel, + type FairRepositoryStatusParams, +} from '@features/Fair/utils/resolveFairQuestionStatus'; import '../../Usrn/style.css'; import '../styles.css'; @@ -11,14 +15,23 @@ import '../styles.css'; export type FairPrincipleQuestionBlockProps = { item: FairQuestionItem; recommendationHeading: string; + openQuestionLabel: string; + repositoryStatus?: FairRepositoryStatusParams | null; }; export const FairPrincipleQuestionBlock = ({ item, recommendationHeading, + openQuestionLabel, + repositoryStatus, }: FairPrincipleQuestionBlockProps) => { - const statusClass = getFairQuestionStatusClassName(item.statusLabel); - const displayStatus = item.statusLabel ?? '—'; + const displayStatus = + resolveFairQuestionStatusLabel({ + item, + openQuestionLabel, + repositoryStatus, + }) ?? '—'; + const statusClass = getFairQuestionStatusClassName(displayStatus); const isOpenQuestion = Boolean(item.openQuestion); return ( diff --git a/src/features/Fair/components/FairPrincipleSectionContent.tsx b/src/features/Fair/components/FairPrincipleSectionContent.tsx index 9c23d8bf..1e3d012d 100644 --- a/src/features/Fair/components/FairPrincipleSectionContent.tsx +++ b/src/features/Fair/components/FairPrincipleSectionContent.tsx @@ -1,16 +1,21 @@ import {FairPrincipleQuestionBlock} from '@features/Fair/components/FairPrincipleQuestionBlock'; import type {FairPrincipleSection} from '@features/Fair/types/fairPrinciples.types'; +import type { FairRepositoryStatusParams } from '@features/Fair/utils/resolveFairQuestionStatus'; import '../styles.css'; export type FairPrincipleSectionContentProps = { section: FairPrincipleSection; recommendationHeading: string; + openQuestionLabel: string; + repositoryStatus?: FairRepositoryStatusParams | null; }; export const FairPrincipleSectionContent = ({ section, recommendationHeading, + openQuestionLabel, + repositoryStatus, }: FairPrincipleSectionContentProps) => { if (!section.items?.length) { return null; @@ -18,11 +23,13 @@ export const FairPrincipleSectionContent = ({ return (
- {section.items.map((item) => ( + {section.items.map((item, index) => ( ))}
diff --git a/src/features/Fair/components/FairPrinciplesCollapse.tsx b/src/features/Fair/components/FairPrinciplesCollapse.tsx index 823af940..38b0ce46 100644 --- a/src/features/Fair/components/FairPrinciplesCollapse.tsx +++ b/src/features/Fair/components/FairPrinciplesCollapse.tsx @@ -9,6 +9,7 @@ import { type FairPrincipleSection, type FairPrincipleSectionKey, } from '@features/Fair/types/fairPrinciples.types'; +import type { FairRepositoryStatusParams } from '@features/Fair/utils/resolveFairQuestionStatus'; import {Button, Form, Typography} from 'antd'; import {useMemo} from 'react'; @@ -19,6 +20,8 @@ export type FairPrinciplesCollapseProps = { onSubmit?: () => void; /** Collapsible presentation: `default` (full FAIR styling) or `compact` (tighter panels). */ collapsibleVariant?: 'default' | 'compact'; + /** When set, principle rows with a USRN mapping show Yes/No from the same APIs as USRN. */ + repositoryStatus?: FairRepositoryStatusParams | null; }; const {Title, Paragraph} = Typography; @@ -27,6 +30,7 @@ export const FairPrinciplesCollapse = ({ onSave, onSubmit, collapsibleVariant = 'default', + repositoryStatus, }: FairPrinciplesCollapseProps) => { const {principlesAccordion} = fairTexts; const [openAnswersForm] = Form.useForm(); @@ -41,6 +45,8 @@ export const FairPrinciplesCollapse = ({ onSubmit?.(); }; + const openQuestionLabel = principlesAccordion.openQuestionBadge ?? 'Open question'; + const collapsibleSections: FairPrinciplesCollapsibleSection[] = useMemo(() => { return FAIR_PRINCIPLE_SECTION_KEYS.map((key: FairPrincipleSectionKey) => { const section = principlesAccordion[key] as FairPrincipleSection; @@ -58,11 +64,13 @@ export const FairPrinciplesCollapse = ({ ), }; }); - }, [ principlesAccordion, recommendationHeading]); + }, [principlesAccordion, recommendationHeading, openQuestionLabel, repositoryStatus]); return (
{ + const byId: Record = { + '': 'indexedContent', + r11: 'oaiPmh', + r12: 'applicationProfile', + r21: 'accessFullTexts', + r22: 'embargoedDocuments', + r23: 'licensing', + r31: 'supportSignpostingFAIR', + r32: 'metadataCOAR', + r35: 'DOI', + r36: 'ORCID', + r37: 'ROR', + r41: 'webAccessibility', + r42: 'sourceCode', + }; + return byId[fairId] ?? null; +}; + +const capitalizeStatus = (status: 'yes' | 'no'): 'Yes' | 'No' => + status === 'yes' ? 'Yes' : 'No'; + +export type ResolveFairQuestionStatusLabelParams = { + item: FairQuestionItem; + repositoryStatus: FairRepositoryStatusParams | null | undefined; + openQuestionLabel: string; +}; + +/** + * Resolves the status chip for a FAIR principle row: open-question badge, Yes/No from + * `getCardStatusConfig` when the question maps to a USRN card, or `null` when there is no API match + * (caller should show an em dash). + */ +export const resolveFairQuestionStatusLabel = ({ + item, + repositoryStatus, + openQuestionLabel, +}: ResolveFairQuestionStatusLabelParams): string | null => { + if (item.openQuestion) { + return openQuestionLabel; + } + + if (!repositoryStatus) { + return null; + } + + const cardId = mapFairQuestionIdToUsrnCardId(item.id); + if (!cardId) { + return null; + } + + const { status } = getCardStatusConfig({ + cardId, + rioxx: repositoryStatus.rioxx, + statistics: repositoryStatus.statistics, + internalStatistics: repositoryStatus.internalStatistics, + usrn: repositoryStatus.usrn, + irus: repositoryStatus.irus, + rorId: repositoryStatus.rorId, + }); + + if (status === 'yes' || status === 'no') { + return capitalizeStatus(status); + } + + return null; +}; diff --git a/src/features/Usrn/utils/getCardStatusConfig.ts b/src/features/Usrn/utils/getCardStatusConfig.ts index 538cf3ef..26d531b5 100644 --- a/src/features/Usrn/utils/getCardStatusConfig.ts +++ b/src/features/Usrn/utils/getCardStatusConfig.ts @@ -42,6 +42,7 @@ export const getCardStatusConfig = ({ const hasValue = (n: number | null): SupportStatusVariant => (n ? 'yes' : 'no'); + // TODO check for 1.3 switch (cardId) { case 'oaiPmh': return { ...empty, status: 'yes' }; @@ -54,6 +55,7 @@ export const getCardStatusConfig = ({ countValue, }; } + // TODO check for 2.1 case 'accessFullTexts': { const countCovered = toNumber(internalStatistics?.fullTextCount); const countTotal = toNumber(internalStatistics?.metadataCount); @@ -93,6 +95,7 @@ export const getCardStatusConfig = ({ } case 'IRUS': return { ...empty, status: irus ? 'yes' : 'no' }; + // TODO check for 3.5 case 'DOI': { const countCovered = toNumber(internalStatistics?.doiCount); const countTotal = toNumber(statistics?.countMetadata); @@ -103,6 +106,7 @@ export const getCardStatusConfig = ({ countValue: null, }; } + // TODO check for 3.6 if we have ror id case 'ROR': return { ...empty, status: rorId ? 'yes' : 'no' }; default: diff --git a/src/features/Validator/hooks/useRioxxStats.ts b/src/features/Validator/hooks/useRioxxStats.ts index 79cd88b3..69afd12a 100644 --- a/src/features/Validator/hooks/useRioxxStats.ts +++ b/src/features/Validator/hooks/useRioxxStats.ts @@ -14,7 +14,7 @@ interface RioxxAggregationResponse { export const useRioxxStats = (dataProviderId?: number) => { const { selectedDataProvider, isLoaded } = useDataProviderStore(); const effectiveDataProviderId = dataProviderId || selectedDataProvider?.id; - + // TODO check for 1.2 const key = (isLoaded && effectiveDataProviderId) ? `/internal/data-providers/${effectiveDataProviderId}/rioxx/aggregation` : null; @@ -37,7 +37,7 @@ export const useRioxxStats = (dataProviderId?: number) => { totalCount: data.totalRecords || 0, missingTermsBasic: data.missingTermsBasic || [], ...Object.fromEntries( - Object.entries(data).filter(([key]) => + Object.entries(data).filter(([key]) => !['compliantRecordBasic', 'compliantRecordFull', 'totalRecords', 'missingTermsBasic'].includes(key) ) ), From f67bda27ddcb0c8d641ca73fc9c126f4a07518b0 Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Wed, 29 Apr 2026 15:24:23 +0400 Subject: [PATCH 08/66] CORE: add api call implementation --- .../Fair/components/ApprovedFairView.tsx | 26 +- .../components/FairPrincipleQuestionBlock.tsx | 85 +++- src/features/Fair/texts/fair.json | 10 +- .../Fair/types/fairPrinciples.types.ts | 6 +- .../Fair/utils/resolveFairQuestionStatus.ts | 42 +- src/features/Usrn/UsrnView.tsx | 6 + .../Usrn/components/UsrnCard/UsrnCard.tsx | 34 +- src/features/Usrn/texts/usrn.json | 457 +++++++++--------- src/features/Usrn/types/data.types.ts | 4 + .../Usrn/utils/getCardStatusConfig.ts | 73 ++- 10 files changed, 459 insertions(+), 284 deletions(-) diff --git a/src/features/Fair/components/ApprovedFairView.tsx b/src/features/Fair/components/ApprovedFairView.tsx index 858ddcf0..9cbd2717 100644 --- a/src/features/Fair/components/ApprovedFairView.tsx +++ b/src/features/Fair/components/ApprovedFairView.tsx @@ -1,15 +1,17 @@ -import {CrFeatureLayout, CrPaper} from '@oacore/core-ui'; +import { CrFeatureLayout, CrPaper } from '@oacore/core-ui'; import '../styles.css'; -import {FairDocHeader} from '@features/Fair/components/FairDocHeader.tsx'; -import {FairPrinciplesCollapse} from '@features/Fair/components/FairPrinciplesCollapse.tsx'; -import {FairSubmissionProgress} from '@features/Fair/components/FairSubmissionProgress.tsx'; +import { FairDocHeader } from '@features/Fair/components/FairDocHeader.tsx'; +import { FairPrinciplesCollapse } from '@features/Fair/components/FairPrinciplesCollapse.tsx'; +import { FairSubmissionProgress } from '@features/Fair/components/FairSubmissionProgress.tsx'; import { useDataProviderStore } from '@/store/dataProviderStore'; import { useRioxxStats } from '@features/Validator/hooks/useRioxxStats'; import { useUsrnData } from '@features/Usrn/hooks/useUsrnData'; import { useIrusStats } from '@/hooks/useIrusStats'; import { useDataProviderStatistics } from '@/hooks/useDataProviderStatistics'; import { useDoiStatistics } from '@features/Doi/hooks/useDoiStatistics'; +import { useOrcidStats } from '@features/Orcid/hooks/useOrcidData.ts'; +import { useDasData } from '@features/Das/hooks/useDasData'; import type { FairRepositoryStatusParams } from '@features/Fair/utils/resolveFairQuestionStatus'; export const ApprovedFairView = () => { @@ -18,6 +20,8 @@ export const ApprovedFairView = () => { const { rioxx } = useRioxxStats(selectedDataProvider?.id); const { usrnData } = useUsrnData(selectedDataProvider?.id); const { irus } = useIrusStats(selectedDataProvider?.id); + const { stats: orcidStats } = useOrcidStats(selectedDataProvider?.id ?? 0); + const { data: dasData } = useDasData(selectedDataProvider?.id ?? 0); useDataProviderStatistics(selectedDataProvider?.id ?? null, selectedSetSpec); useDoiStatistics(selectedDataProvider?.id ?? null, selectedSetSpec); @@ -28,22 +32,24 @@ export const ApprovedFairView = () => { internalStatistics: statistics != null || doiStatistics != null ? { - fullTextCount: statistics?.countFulltext, - metadataCount: statistics?.countMetadata, - doiCount: doiStatistics?.dataProviderDoiCount, - } + fullTextCount: statistics?.countFulltext, + metadataCount: statistics?.countMetadata, + doiCount: doiStatistics?.dataProviderDoiCount, + } : undefined, usrn: usrnData ?? null, irus, rorId, + orcidStats, + dasData, }; return ( - + - + ); diff --git a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx index ea9168f4..966cfa0f 100644 --- a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx +++ b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx @@ -1,11 +1,12 @@ import {InfoOutlined, FileTextOutlined} from '@ant-design/icons'; -import {Markdown} from '@oacore/core-ui'; +import {Markdown, PercentBar} from '@oacore/core-ui'; import {Form, Input} from 'antd'; +import {formatNumber} from '@utils/helpers.ts'; import type {FairQuestionItem} from '@features/Fair/types/fairPrinciples.types'; import {getFairQuestionStatusClassName} from '@features/Fair/utils/getFairQuestionStatusClassName'; import { - resolveFairQuestionStatusLabel, + resolveFairQuestionDisplay, type FairRepositoryStatusParams, } from '@features/Fair/utils/resolveFairQuestionStatus'; @@ -25,15 +26,33 @@ export const FairPrincipleQuestionBlock = ({ openQuestionLabel, repositoryStatus, }: FairPrincipleQuestionBlockProps) => { - const displayStatus = - resolveFairQuestionStatusLabel({ - item, - openQuestionLabel, - repositoryStatus, - }) ?? '—'; + const { statusLabel, cardConfig } = resolveFairQuestionDisplay({ + item, + openQuestionLabel, + repositoryStatus, + }); + const displayStatus = statusLabel ?? '—'; const statusClass = getFairQuestionStatusClassName(displayStatus); const isOpenQuestion = Boolean(item.openQuestion); + const countCovered = cardConfig?.countCovered ?? null; + const countTotal = cardConfig?.countTotal ?? null; + const countValue = cardConfig?.countValue ?? null; + const percentLabelText = item.percentLabel; + const counterLabelText = item.counterLabel; + + const showPercentBar = + countCovered != null && + countTotal != null && + countCovered > 0 && + countTotal > 0 && + Boolean(percentLabelText); + + const counterRows = cardConfig?.counterRows; + const showCounterRows = Boolean(counterRows?.length); + const showCountValue = + !showCounterRows && countValue != null && Boolean(counterLabelText); + return (
@@ -46,21 +65,47 @@ export const FairPrincipleQuestionBlock = ({ {displayStatus}
- {item.description &&
-
- {item.description} - {item.statusNote ? ( -

- {item.statusNote} -

- ) : null} + {item.description ? ( +
+
+ {item.description} + {item.statusNote ? ( +

+ {item.statusNote} +

+ ) : null} +
+
- -
- } + ) : null} - {/*TODO ADD BAR CHART IF HAVE*/} + {showCounterRows && counterRows + ? counterRows.map((row, index) => ( +
+ {row.label} + + {formatNumber(row.value)} + +
+ )) + : null} + {showCountValue ? ( +
+ {counterLabelText} + {formatNumber(countValue)} +
+ ) : null} + {showPercentBar && percentLabelText ? ( + + ) : null} {isOpenQuestion ? (
diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json index 8f6665ac..96487b7a 100644 --- a/src/features/Fair/texts/fair.json +++ b/src/features/Fair/texts/fair.json @@ -36,6 +36,7 @@ "code": "R(1.2)", "question": "Does your repository metadata support a widely used Application Profile?", "description": "An application profile is a set of policies and metadata guidelines for describing scholarly documents. Examples include Dublin Core, OpenAIRE Guidelines and RIOXX. However, these application profiles are not equally detailed.\n\nEnsuring the usage of a well-known application profile means that the metadata of the publication is described in a less ambiguous and more accurate way. Most used publications profiles are the OpenAIRE Guidelines 3.0+ and the RIOXX metadata application profile.\n\nApart from matching an application profile, it is also important that your content supports at least the minimal set of metadata to give it the rights to be a scholarly record. We define the minimal set as title, abstract, authors, publication year (or year of first visibility). This list is in no way complete but it helps us establish a minimal compliance of your records.", + "percentLabel": "Your repository support RIOXXv2, RIOXXv3, OAIRE.", "recommendation": "CORE metadata indexing requires Dublin Core as a minimum; it supports OpenAIRE Guidelines; and it recommends RIOXX version 3. CORE recommends that repositories use RIOXX v3, which we consider to be the most suitable metadata application profile for describing scholarly research outputs. RIOXX: The Research Outputs Metadata Schema was developed for institutional repositories to share metadata about the scholarly resources they contain.\n\nInformation about how to comply with metadata recommendations are provided in the CORE Data Provider’s Guide." }, { @@ -49,7 +50,8 @@ "id": "", "code": "", "question": "Indexed content", - "description": "The number of records CORE is able to index from your digital repository. If this number doesn't match your records it might mean you have issues in exposing your records in an interoperable way." + "description": "The number of records CORE is able to index from your digital repository. If this number doesn't match your records it might mean you have issues in exposing your records in an interoperable way.", + "counterLabel": "Number of metadata records:" } ] }, @@ -62,6 +64,7 @@ "code": "R(2.1)", "question": "Do you provide access to full texts of research papers for your content?", "description": "The number of full text CORE is able to discover from your digital repository.", + "percentLabel": "Percentage of documents with access to full text:", "recommendation": "You can check the Indexing tab where we collect issues CORE discovered while indexing your repository. If this number doesn't match your records it might mean you have issues in exposing your records in an interoperable way. The Indexing status tab provides a summary of any issue CORE finds while indexing your repository. CORE requires that full text must be hosted on the same domain or subdomains as the OAI-PMH endpoint, unless we are informed by the institution about other owned domains containing full text.\n\nAlso, we highly recommend checking your repository for compliance with the CORE Data Provider’s Guide requirements, where you will find detailed information about how to configure your repository so it can be indexed to the maximum effect to get more accurate results in the CORE dashboard and to enhance visibility and interoperability beyond CORE." }, { @@ -76,6 +79,7 @@ "code": "R(2.3)", "question": "Do you support machine-readable licensing?", "description": "The percentage of metadata records with licence URLs CORE is able to recognise from the metadata.", + "percentLabel": "Percent of metadata records with a licence specified:", "recommendation": "Licensing and copyright information may be displayed in a machine-readable way or by a text description. However, text descriptions such as “all rights reserved” may not be specific enough for other users to interpret. Much better is to provide a URL to the relevant Creative Commons licence, for example, for the non-commercial CC-BY licence, https://creativecommons.org/licenses/by-nc/4.0/ By clicking on this link, a human can see the precise meaning of this licence. Any machine can identify immediately and unambiguously what kind of licence is stated.\n\nCORE promotes and advocates for the use of machine-readable licencing descriptions. In particular, we advise the use of the NISO Access License and Indicators (ALI) Schema.\n\nInstitutions may have a blanket policy that covers all content with a specific publisher, which usually means that individual documents contain no licensing information. However, a specific CC-BY licence enables exceptions to be made for individual documents and reduces ambiguity.\n\nThe CORE Rights Retention Statement tracker identifies with reasonable accuracy any specific licence conditions that are stated within an article or text file." }, { @@ -128,6 +132,7 @@ "code": "R(3.5)", "question": "DOI", "description": "The DOI provides a machine-actionable persistent identifier that enables systems to consistently locate and reliably link research objects across different platforms, thereby guaranteeing Interoperability (I) and enhancing Findability (F).", + "percentLabel": "Percentage of metadata with DOI:", "recommendation": "To enrich your metadata with more DOIs, you can go to the DOI tab. It provides a checklist of the number of content objects in your repository which have a DOI. It also identifies (and counts) the number of DOIs that it finds from articles held in other repositories relating to research in your institution." }, { @@ -155,6 +160,7 @@ "code": "R(4.1)", "question": "Do you provide data availability statements?", "description": "A data availability statement, also sometimes called a ‘data access statement’, is a short, mandatory section in a paper that tells the reader where the research data associated with a paper is available and under what conditions the data can be accessed.", + "counterLabel": "Papers with Data availability statement:", "recommendation": "Here are some recommendations on how to increase the number of DAS in your repository:\n\n- Include links where applicable to the data set. For example, the paper may state “The data that support the findings of this study are openly available in [repository name] at https://doi.org/[doi]”.\n- Provide a dedicated metadata field for DAS, not just full text in the description.\n- Provide researchers with approved, compliant DAS text for all scenarios.\n- CORE provides several tools under the [DAS identification tab]():\n\n monitoring statistics about DAS in your repository, and a Data Availability Statement checker." }, { @@ -321,4 +327,4 @@ ] } } -} +} \ No newline at end of file diff --git a/src/features/Fair/types/fairPrinciples.types.ts b/src/features/Fair/types/fairPrinciples.types.ts index 336d29ae..e76ca64f 100644 --- a/src/features/Fair/types/fairPrinciples.types.ts +++ b/src/features/Fair/types/fairPrinciples.types.ts @@ -7,13 +7,17 @@ export type FairQuestionItem = { id: string; code: string; question: string; - description: string; + description?: string; recommendation?: string; openQuestion?: boolean; answerPlaceholder?: string; statusLabel?: string; statusNote?: string; metrics?: FairMetricLine[]; + /** Shown above `PercentBar` when counts are available (same role as USRN sub-items). */ + percentLabel?: string; + /** Label for `countValue` (e.g. indexed metadata total). */ + counterLabel?: string; }; export type FairPrincipleSection = { diff --git a/src/features/Fair/utils/resolveFairQuestionStatus.ts b/src/features/Fair/utils/resolveFairQuestionStatus.ts index 635e2843..0e14f816 100644 --- a/src/features/Fair/utils/resolveFairQuestionStatus.ts +++ b/src/features/Fair/utils/resolveFairQuestionStatus.ts @@ -1,3 +1,5 @@ +import type { DasData } from '@features/Das/types/data.types'; +import type { OrcidStats } from '@features/Orcid/types/data.types'; import type { UsrnData } from '@features/Usrn/types/data.types'; import { getCardStatusConfig } from '@features/Usrn/utils/getCardStatusConfig'; import type { FairQuestionItem } from '@features/Fair/types/fairPrinciples.types'; @@ -14,6 +16,8 @@ export type FairRepositoryStatusParams = { usrn: UsrnData | null; irus: unknown; rorId: string | null; + orcidStats?: OrcidStats | null; + dasData?: DasData[] | null; }; /** Maps FAIR question `id` (from `fair.json`) to USRN card ids used by `getCardStatusConfig`. */ @@ -45,30 +49,33 @@ export type ResolveFairQuestionStatusLabelParams = { openQuestionLabel: string; }; +export type FairQuestionResolvedDisplay = { + statusLabel: string | null; + cardConfig: ReturnType | null; +}; + /** - * Resolves the status chip for a FAIR principle row: open-question badge, Yes/No from - * `getCardStatusConfig` when the question maps to a USRN card, or `null` when there is no API match - * (caller should show an em dash). + * Resolves status label (chip) and USRN card metrics for a FAIR row in one `getCardStatusConfig` call. */ -export const resolveFairQuestionStatusLabel = ({ +export const resolveFairQuestionDisplay = ({ item, repositoryStatus, openQuestionLabel, -}: ResolveFairQuestionStatusLabelParams): string | null => { +}: ResolveFairQuestionStatusLabelParams): FairQuestionResolvedDisplay => { if (item.openQuestion) { - return openQuestionLabel; + return { statusLabel: openQuestionLabel, cardConfig: null }; } if (!repositoryStatus) { - return null; + return { statusLabel: null, cardConfig: null }; } const cardId = mapFairQuestionIdToUsrnCardId(item.id); if (!cardId) { - return null; + return { statusLabel: null, cardConfig: null }; } - const { status } = getCardStatusConfig({ + const cardConfig = getCardStatusConfig({ cardId, rioxx: repositoryStatus.rioxx, statistics: repositoryStatus.statistics, @@ -76,11 +83,22 @@ export const resolveFairQuestionStatusLabel = ({ usrn: repositoryStatus.usrn, irus: repositoryStatus.irus, rorId: repositoryStatus.rorId, + orcidStats: repositoryStatus.orcidStats ?? null, + dasData: repositoryStatus.dasData ?? null, }); - if (status === 'yes' || status === 'no') { - return capitalizeStatus(status); + if (cardConfig.status === 'yes' || cardConfig.status === 'no') { + return { statusLabel: capitalizeStatus(cardConfig.status), cardConfig }; } - return null; + return { statusLabel: null, cardConfig }; }; + +/** + * Resolves the status chip for a FAIR principle row: open-question badge, Yes/No from + * `getCardStatusConfig` when the question maps to a USRN card, or `null` when there is no API match + * (caller should show an em dash). + */ +export const resolveFairQuestionStatusLabel = ( + params: ResolveFairQuestionStatusLabelParams, +): string | null => resolveFairQuestionDisplay(params).statusLabel; diff --git a/src/features/Usrn/UsrnView.tsx b/src/features/Usrn/UsrnView.tsx index e7d0dbfa..293ec91f 100644 --- a/src/features/Usrn/UsrnView.tsx +++ b/src/features/Usrn/UsrnView.tsx @@ -8,6 +8,8 @@ import { useDataProviderStatistics } from '@/hooks/useDataProviderStatistics'; import { useUsrnData } from './hooks/useUsrnData'; import { useDoiStatistics } from '@features/Doi/hooks/useDoiStatistics'; import { useIrusStats } from '@/hooks/useIrusStats'; +import { useOrcidStats } from '@features/Orcid/hooks/useOrcidData.ts'; +import { useDasData } from '@features/Das/hooks/useDasData'; import { formatReportDate } from './utils/formatReportDate'; import './style.css'; import { CrHeader, CrPaper, CrFeatureLayout, CrShowMore } from '@oacore/core-ui'; @@ -35,6 +37,8 @@ export const UsrnViewPage = () => { const { error: statisticsError } = useDataProviderStatistics(selectedDataProvider?.id ?? null, selectedSetSpec); const { error: doiError } = useDoiStatistics(selectedDataProvider?.id ?? null, selectedSetSpec); + const { stats: orcidStats } = useOrcidStats(selectedDataProvider?.id ?? 0); + const { data: dasData } = useDasData(selectedDataProvider?.id ?? 0); const failedSections: string[] = []; if (usrnError) failedSections.push(ERROR_SECTION_LABELS.usrn); @@ -76,6 +80,8 @@ export const UsrnViewPage = () => { usrn: usrnData ?? null, irus, rorId, + orcidStats, + dasData, }; const handleDownloadPDF = () => { diff --git a/src/features/Usrn/components/UsrnCard/UsrnCard.tsx b/src/features/Usrn/components/UsrnCard/UsrnCard.tsx index 25cd5e6d..9dce94db 100644 --- a/src/features/Usrn/components/UsrnCard/UsrnCard.tsx +++ b/src/features/Usrn/components/UsrnCard/UsrnCard.tsx @@ -2,6 +2,8 @@ import React from 'react'; import { FileTextFilled, InfoOutlined } from '@ant-design/icons'; import { Markdown, PercentBar } from '@oacore/core-ui'; import { formatNumber } from '@utils/helpers.ts'; +import type { DasData } from '@features/Das/types/data.types'; +import type { OrcidStats } from '@features/Orcid/types/data.types'; import type { SupportStatusVariant, UsrnData } from '@features/Usrn/types/data.types'; import { getCardStatusConfig } from '@features/Usrn/utils/getCardStatusConfig'; import '../../style.css'; @@ -40,6 +42,8 @@ type UsrnCardConfigParams = { usrn: UsrnData | null; irus: unknown; rorId: string | null; + orcidStats?: OrcidStats | null; + dasData?: DasData[] | null; }; interface UsrnCardProps { @@ -55,6 +59,8 @@ export const UsrnCard: React.FC = ({ item, configParams }) => { usrn, irus, rorId, + orcidStats, + dasData, } = configParams; const displayCtaText = (item.linkText && item.linkText.length > 0) @@ -101,6 +107,8 @@ export const UsrnCard: React.FC = ({ item, configParams }) => { usrn: usrn ?? null, irus, rorId, + orcidStats: orcidStats ?? null, + dasData: dasData ?? null, }); const statusVariant: SupportStatusVariant = config.status || ''; @@ -151,16 +159,32 @@ export const UsrnCard: React.FC = ({ item, configParams }) => { />
- {config.countValue != null && subItem.counterLabel && ( -
+ {config.counterRows?.map((row, index) => ( +
- {subItem.counterLabel} + {row.label} - {formatNumber(config.countValue)} + {formatNumber(row.value)}
- )} + ))} + + {!config.counterRows?.length && + config.countValue != null && + subItem.counterLabel && ( +
+ + {subItem.counterLabel} + + + {formatNumber(config.countValue)} + +
+ )} {showPercentBar && subItem.percentLabel && ( element to describe the type of content resource, for example, \"author\", or \"collection\"." - }, - { - "id": "metadataCOAR", - "question": "Use of COAR vocabularies", - "description": "[COAR (Confederation of Open Access Repositories)](https://vocabularies.coar-repositories.org/) vocabularies are structured sets of terms organized to provide a common framework for the sharing and integration of data across different systems. They comprise three types: Access rights, resource types, and version types.", - "linkText": "", - "linkUrl": "", - "percentLabel": "", - "counterLabel": "", - "recomendation": "A metadata profile such as RIOXX can provide an easy way to share and use COAR vocabularies in your metadata.\n\n- **Access rights**: describe the access status of a resource, such as \"embargoed access\", or \"restricted access\".\n\n- **Resource type**: describe the type of resource, such as \"patent\", \"bibliography\", or \"conference presentation\".\n\n- **Version type**: distinguish the various states of an academic article, including the version of record (VOR), the Author's Original (AO), and the Author's Accepted Manuscript (AM)." - }, - { - "id": "embargoedDocuments", - "question": "Do you label embargoed documents?", - "description": "Embargoed documents are articles that can only be made available open access after a stated interval, such as 90 days or six months. These delays are best managed when the embargo is tagged in a machine-readable way to ensure that machines can access and understand the policy.", - "linkText": "", - "linkUrl": "", - "percentLabel": "Percentage of embargoed documents:", - "counterLabel": "", - "recomendation": "There are various supported ways of tagging an embargo document, CORE recognises only a few of them. The [Rioxx specification](https://rioxx.net/profiles/v3-0-final/) recommend that they are tagged using \"HTTP status 451 Unavailable For Legal Reasons\". If this number doesn't match the one in your records please review your system." - } - ] - }, + "id": "indexedContent", + "question": "Indexed content", + "description": "The number of records CORE is able to index from your digital repository. If this number doesn't match your records it might mean you have issues in exposing your records in an interoperable way. The [Indexing status tab](/data-providers/harvesting) provides a summary of any issue CORE finds while indexing your repository. A common issue is when institutions have more than one repository. CORE identifies some but not all of the repositories, which means the total number it identifies is less than the total in the repositories.", + "linkText": "", + "linkUrl": "", + "percentLabel": "", + "counterLabel": "Number of metadata records:" + }, { - "id": "broadMeasuredReuse", - "title": "Broad and Measured Reuse", - "description": "The repository provides a landing page for each digital object that includes metadata required for citation of the object and provides this metadata in a machine-readable format. The repository collects and shares usage information using a standard protocol. The repository offers an API to facilitate machine action and bulk sharing.", - "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Broad-and-Measured-Reuse", - "linkText": "Go to the toolkit", - "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Broad-and-Measured-Reuse", - "items": [ - { - "id": "IRUS", - "question": "Download statistic (IRUS)", - "description": "IRUS (Institutional Repository Usage Statistics) allows you to demonstrate the value and impact of your institutional repository (IR). It does this by enabling institutional repositories to share and compare usage statistics based on the COUNTER standard.", - "linkText": "", - "linkUrl": "", - "percentLabel": "", - "counterLabel": "", - "recomendation": "These statistics are available using the IRUS statistics aggregation service (IRUS-US for the USA, and IRUS-UK for the UK). By using a standard set of statistics, it becomes possible to provide meaningful stats to compare downloads across several repositories. IRUS uses the [COUNTER](https://www.countermetrics.org/) standard to measure usage statistics, such as \"journal request\", or \"book usage\"." - } - ] - }, + "id": "accessFullTexts", + "question": "Access to full texts of research papers for your content", + "description": "The number of full text CORE is able to discover from your digital repository. You can check the [Harvesting tab](/data-providers/harvesting) where we collect issues CORE discovered while indexing your repository. If this number doesn't match your records it might mean you have issues in exposing your records in an interoperable way. The [Indexing status tab](/data-providers/harvesting) provides a summary of any issue CORE finds while indexing your repository. CORE requires that full text must be hosted on the same domain or subdomains as the OAI-PMH endpoint, unless we are informed by the institution about other owned domains containing full text.", + "linkText": "", + "linkUrl": "", + "percentLabel": "Percentage of documents with access to full text:", + "counterLabel": "" + } + ] + }, + { + "id": "guidance", + "title": "Clear Use Guidance", + "description": "The repository ensures metadata records for resources include licensing information stipulating reuse conditions. Ideally, machine-readable information on the OA status with the license is embedded in the resource.", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Clear-Use-Guidance", + "linkText": "Go to the toolkit", + "items": [ { - "id": "commonFormat", - "title": "Common Format", - "description": "The repository allows resources and metadata to be accessed, downloaded, or exported from the repository in widely used, preferably non-proprietary, formats.", - "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Common-Format", - "linkText": "Go to the toolkit", - "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Common-Format", - "items": [ - { - "id": "applicationProfile", - "question": "Your repository metadata supports a widely used Application Profile", - "description": "Ensuring the usage of a well-known application profile means that the metadata of the publication is described in a less ambiguous and more accurate way. Most used publications profiles are the OpenAIRE Guidelines 3.0+ and the RIOXX metadata application profile.", - "linkText": "", - "linkUrl": "", - "percentLabel": "Your repository support RIOXXv2, RIOXXv3, OAIRE.", - "counterLabel": "", - "recomendation": "CORE metadata indexing requires Dublin Core as a minimum; it supports OpenAIRE Guidelines; and it recommends RIOXX version 3. CORE recommends that repositories use [RIOXX v3](https://rioxx.net/profiles/), which we consider to be the most suitable metadata application profile for describing scholarly research outputs. Rioxx: The Research Outputs Metadata Schema was developed for institutional repositories to share metadata about the scholarly resources they contain. Information about how to comply with metadata recommendations are provided in the [CORE Data Provider's Guide](https://core.ac.uk/documentation/data-providers-guide#meta-configuration)." - } - ] - }, + "id": "licensing", + "question": "Licensing", + "description": "The percentage of metadata records with licence URLs CORE is able to recognise from the metadata.", + "linkText": "", + "linkUrl": "", + "percentLabel": "Percent of metadata records with a licence specified:", + "counterLabel": "", + "recomendation": "Licensing and copyright information may be displayed in a machine-readable way, or by a text description. However, text descriptions such as \"all rights reserved\" may not be specific enough for other users to interpret. Much better is to provide a URL to the relevant Creative Commons licence, for example, for the non-commercial CC-BY licence, [https://creativecommons.org/licenses/by-nc/4.0/](https://creativecommons.org/licenses/by-nc/4.0/). By clicking on this link, a human can see the precise meaning of this licence. Any machine can identify immediately, and unambiguously, what kind of licence is stated.\n\n CORE promotes and advocates for the use of machine-readable licencing descriptions. In particular, we advise the use of the NISO Access License and Indicators (ALI) Schema.\n\n Institutions may have a blanket policy that covers all content with a specific publisher, which usually means that individual documents contain no licensing information. However, a specific CC-BY licence enables exceptions to be made for individual documents and reduces ambiguity.\n\n The CORE Rights Retention Statement tracker identifies with reasonable accuracy any specific licence conditions that are stated within an article or text file." + } + ] + }, + { + "id": "uniquePersistentIdentifiers", + "title": "Unique Persistent Identifiers", + "description": "The repository assigns each resource a unique persistent identifier (PID), such as a digital object identifier (DOI), to support discovery, citation, reporting (e.g., of research progress), and research assessment (e.g., identifying the outputs of Federally funded research). The unique PID points to a persistent location that remains accessible even if the content is de-accessioned or no longer available. The repository supports PIDs for entities related to the resources, such as authors (e.g., ORCID), funders (e.g., Funder Registry), and organizations (e.g., ROR).", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Unique-Persistent-Identifiers", + "linkText": "Go to the toolkit", + "items": [ { - "id": "preservation", - "title": "Preservation", - "description": "The repository has a plan in place to ensure the long-term management and preservation of publications and provides documentation of that plan. The management plan should include provisions for maintaining integrity, authenticity, and availability. The repository records basic preservation metadata including provenance, date of upload, and file format.", - "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Preservation", - "linkText": "Go to the toolkit", - "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Preservation" + "id": "webAccessibility", + "question": "Do you provide data accessibility statements", + "description": "A data availability statement, also sometimes called a 'data access statement', tells the reader where the research data associated with a paper is available, and under what conditions the data can be accessed. They also include links where applicable to the data set. For example, the paper may state \"The data that support the findings of this study are openly available in [repository name] at https://doi.org/[doi]\". This is in development right now.", + "linkText": "", + "linkUrl": "", + "percentLabel": "", + "counterLabel": "Papers with Data availability statement:" }, { - "id": "organizationalSustainability", - "title": "Long-term Organizational Sustainability", - "description": "The repository has publicly available policies that outline the plan for its long-term management and funding, naming the organization responsible for its governance and continued management, and containing provisions for cessation of service. The host organization funds staff to support the repository and its users and publicly provides contact information for at least one individual charged with assisting users and the explicit responsibility of managing the repository services.", - "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Long-term-Organizational-Sustainability", - "linkText": "Go to the toolkit", - "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Long-term-Organizational-Sustainability" + "id": "sourceCode", + "question": "Do you provide links to source code", + "description": "This feature is currently under development.", + "linkText": "", + "linkUrl": "", + "percentLabel": "", + "counterLabel": "", + "recomendation": "" }, { - "id": "curationQualityAssurance", - "title": "Curation and Quality Assurance", - "description": "The repository provides or facilitates expert curation and quality assurance to improve the accuracy and integrity of digital objects and their metadata. This may include metadata enhancement, file integrity checks, and/or conversion to machine-readable formats. The repository provides documentation of what curation and quality assurance processes are applied to repository contents.", - "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Toolkit", - "linkText": "Go to the toolkit", - "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Toolkit" + "id": "DOI", + "question": "DOI", + "description": "The [DOI tab](/data-providers/doi) provides a checklist of the number of content objects in your repository which have a DOI. It also identifies (and counts) the number of DOIs that it finds from articles held other repositories relating to research in your institution.", + "linkText": "", + "linkUrl": "", + "percentLabel": "Percentage of metadata with DOI:", + "counterLabel": "" }, { - "id": "provenance", - "title": "Provenance", - "description": "The repository has mechanisms in place to record the origin, provenance, version control, and any other modifications to submitted digital objects and their associated metadata.", - "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Provenance", - "linkText": "Go to the toolkit", - "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Provenance" + "id": "ORCID", + "question": "ORCID", + "description": "Public Investigator and Contributor Identifier is a non-proprietary alphanumeric code that uniquely identifies scientific authors. ORCID is an essential metadata tool that enables researchers to claim credit for their own research, without ambiguity. For example, there are many researchers with the common name \"John Smith\" or \"Zhang Wei\". ORCID enables these names to be disambiguated. CORE is currently developing an ORCID dashboard, which will enable repository managers to both identify missing ORCID IDs, and to enrich your data with ORCID IDs found from elsewhere.", + "linkText": "Link to documentation", + "linkUrl": "https://orcid.org", + "percentLabel": "", + "counterLabel": "" }, { - "id": "authentication", - "title": "Authentication", - "description": "The repository supports authentication of its contributors. The repository has technical capabilities that facilitate associating contributor PIDs with those assigned to their deposited digital objects.", - "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Authentication", - "linkText": "Go to the toolkit", - "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Authentication" + "id": "ROR", + "question": "ROR", + "description": "The CORE Dashboard enables you to specify your ROR ID. You should check the details to make sure your institution is identified correctly.", + "linkText": "", + "linkUrl": "", + "percentLabel": "", + "counterLabel": "", + "recomendation": "If the ROR ID is missing or incorrect, you can correct it on the dashboard. The ROR ID can be obtained from the [Research Organisation Registry](https://ror.org/) website." + } + ] + }, + { + "id": "metadata", + "title": "Metadata", + "description": "The repository ensures publications are accompanied by metadata to enable discovery, reuse, and citation, using schema that are appropriate to, and ideally widely used across, the communities that the repository serves. Metadata for the resource is recorded in a standard, interoperable, non-proprietary format. The repository assigns a CC0 public domain license to all metadata for resources in the repository.", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Metadata", + "linkText": "Go to the toolkit", + "items": [ + { + "id": "supportSignpostingFAIR", + "question": "FAIR Signposting", + "description": "FAIR Signposting is a technical approach designed to enhance the discoverability and interoperability of scholarly resources on the web. It involves using standardized HTTP link relations to expose key information about web resources, particularly in the context of scholarly communication. This approach aligns with the broader FAIR principles, which aim for data to be Findable, Accessible, Interoperable, and Reusable.", + "linkText": "Link to documentation", + "linkUrl": "https://core.ac.uk/documentation/data-providers-guide#fair-signposting", + "percentLabel": "", + "counterLabel": "", + "recomendation": "FAIR signposting can be done adding information to HTTP headers. Specifically, the HTTP header can contain a element to describe the type of content resource, for example, \"author\", or \"collection\"." }, { - "id": "longTermSustainability", - "title": "Long-term Technical Sustainability", - "description": "The repository has a plan for long-term management and funding of its technical infrastructure.", - "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Long-term-Technical-Sustainability", - "linkText": "Go to the toolkit", - "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Long-term-Technical-Sustainability" + "id": "metadataCOAR", + "question": "Use of COAR vocabularies", + "description": "[COAR (Confederation of Open Access Repositories)](https://vocabularies.coar-repositories.org/) vocabularies are structured sets of terms organized to provide a common framework for the sharing and integration of data across different systems. They comprise three types: Access rights, resource types, and version types.", + "linkText": "", + "linkUrl": "", + "percentLabel": "", + "counterLabel": "", + "recomendation": "A metadata profile such as RIOXX can provide an easy way to share and use COAR vocabularies in your metadata.\n\n- **Access rights**: describe the access status of a resource, such as \"embargoed access\", or \"restricted access\".\n\n- **Resource type**: describe the type of resource, such as \"patent\", \"bibliography\", or \"conference presentation\".\n\n- **Version type**: distinguish the various states of an academic article, including the version of record (VOR), the Author's Original (AO), and the Author's Accepted Manuscript (AM)." }, { - "id": "securityIntegrity", - "title": "Security and Integrity", - "description": "The repository has documented measures in place to meet well established cybersecurity criteria for preventing unauthorized access to or manipulation of its content and regularly monitors the integrity of its content. The repository has an emergency response plan in case of natural disaster, cyber attacks, etc.", - "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Security-and-Integrity", - "linkText": "Go to the toolkit", - "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Security-and-Integrity" + "id": "embargoedDocuments", + "question": "Do you label embargoed documents?", + "description": "Embargoed documents are articles that can only be made available open access after a stated interval, such as 90 days or six months. These delays are best managed when the embargo is tagged in a machine-readable way to ensure that machines can access and understand the policy.", + "linkText": "", + "linkUrl": "", + "percentLabel": "Percentage of embargoed documents:", + "counterLabel": "", + "recomendation": "There are various supported ways of tagging an embargo document, CORE recognises only a few of them. The [Rioxx specification](https://rioxx.net/profiles/v3-0-final/) recommend that they are tagged using \"HTTP status 451 Unavailable For Legal Reasons\". If this number doesn't match the one in your records please review your system." + } + ] + }, + { + "id": "broadMeasuredReuse", + "title": "Broad and Measured Reuse", + "description": "The repository provides a landing page for each digital object that includes metadata required for citation of the object and provides this metadata in a machine-readable format. The repository collects and shares usage information using a standard protocol. The repository offers an API to facilitate machine action and bulk sharing.", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Broad-and-Measured-Reuse", + "linkText": "Go to the toolkit", + "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Broad-and-Measured-Reuse", + "items": [ + { + "id": "IRUS", + "question": "Download statistic (IRUS)", + "description": "IRUS (Institutional Repository Usage Statistics) allows you to demonstrate the value and impact of your institutional repository (IR). It does this by enabling institutional repositories to share and compare usage statistics based on the COUNTER standard.", + "linkText": "", + "linkUrl": "", + "percentLabel": "", + "counterLabel": "", + "recomendation": "These statistics are available using the IRUS statistics aggregation service (IRUS-US for the USA, and IRUS-UK for the UK). By using a standard set of statistics, it becomes possible to provide meaningful stats to compare downloads across several repositories. IRUS uses the [COUNTER](https://www.countermetrics.org/) standard to measure usage statistics, such as \"journal request\", or \"book usage\"." + } + ] + }, + { + "id": "commonFormat", + "title": "Common Format", + "description": "The repository allows resources and metadata to be accessed, downloaded, or exported from the repository in widely used, preferably non-proprietary, formats.", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Common-Format", + "linkText": "Go to the toolkit", + "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Common-Format", + "items": [ + { + "id": "applicationProfile", + "question": "Your repository metadata supports a widely used Application Profile", + "description": "Ensuring the usage of a well-known application profile means that the metadata of the publication is described in a less ambiguous and more accurate way. Most used publications profiles are the OpenAIRE Guidelines 3.0+ and the RIOXX metadata application profile.", + "linkText": "", + "linkUrl": "", + "percentLabel": "Your repository support RIOXXv2, RIOXXv3, OAIRE.", + "counterLabel": "", + "recomendation": "CORE metadata indexing requires Dublin Core as a minimum; it supports OpenAIRE Guidelines; and it recommends RIOXX version 3. CORE recommends that repositories use [RIOXX v3](https://rioxx.net/profiles/), which we consider to be the most suitable metadata application profile for describing scholarly research outputs. Rioxx: The Research Outputs Metadata Schema was developed for institutional repositories to share metadata about the scholarly resources they contain. Information about how to comply with metadata recommendations are provided in the [CORE Data Provider's Guide](https://core.ac.uk/documentation/data-providers-guide#meta-configuration)." } - ] -} + ] + }, + { + "id": "preservation", + "title": "Preservation", + "description": "The repository has a plan in place to ensure the long-term management and preservation of publications and provides documentation of that plan. The management plan should include provisions for maintaining integrity, authenticity, and availability. The repository records basic preservation metadata including provenance, date of upload, and file format.", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Preservation", + "linkText": "Go to the toolkit", + "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Preservation" + }, + { + "id": "organizationalSustainability", + "title": "Long-term Organizational Sustainability", + "description": "The repository has publicly available policies that outline the plan for its long-term management and funding, naming the organization responsible for its governance and continued management, and containing provisions for cessation of service. The host organization funds staff to support the repository and its users and publicly provides contact information for at least one individual charged with assisting users and the explicit responsibility of managing the repository services.", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Long-term-Organizational-Sustainability", + "linkText": "Go to the toolkit", + "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Long-term-Organizational-Sustainability" + }, + { + "id": "curationQualityAssurance", + "title": "Curation and Quality Assurance", + "description": "The repository provides or facilitates expert curation and quality assurance to improve the accuracy and integrity of digital objects and their metadata. This may include metadata enhancement, file integrity checks, and/or conversion to machine-readable formats. The repository provides documentation of what curation and quality assurance processes are applied to repository contents.", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Toolkit", + "linkText": "Go to the toolkit", + "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Toolkit" + }, + { + "id": "provenance", + "title": "Provenance", + "description": "The repository has mechanisms in place to record the origin, provenance, version control, and any other modifications to submitted digital objects and their associated metadata.", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Provenance", + "linkText": "Go to the toolkit", + "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Provenance" + }, + { + "id": "authentication", + "title": "Authentication", + "description": "The repository supports authentication of its contributors. The repository has technical capabilities that facilitate associating contributor PIDs with those assigned to their deposited digital objects.", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Authentication", + "linkText": "Go to the toolkit", + "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Authentication" + }, + { + "id": "longTermSustainability", + "title": "Long-term Technical Sustainability", + "description": "The repository has a plan for long-term management and funding of its technical infrastructure.", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Long-term-Technical-Sustainability", + "linkText": "Go to the toolkit", + "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Long-term-Technical-Sustainability" + }, + { + "id": "securityIntegrity", + "title": "Security and Integrity", + "description": "The repository has documented measures in place to meet well established cybersecurity criteria for preventing unauthorized access to or manipulation of its content and regularly monitors the integrity of its content. The repository has an emergency response plan in case of natural disaster, cyber attacks, etc.", + "linkUrl": "https://github.com/antleaf/USRN-Discovery/wiki/Security-and-Integrity", + "linkText": "Go to the toolkit", + "titleLink": "https://github.com/antleaf/USRN-Discovery/wiki/Security-and-Integrity" + } + ] +} \ No newline at end of file diff --git a/src/features/Usrn/types/data.types.ts b/src/features/Usrn/types/data.types.ts index aa0159b2..d05149cc 100644 --- a/src/features/Usrn/types/data.types.ts +++ b/src/features/Usrn/types/data.types.ts @@ -3,5 +3,9 @@ export type SupportStatusVariant = 'yes' | 'no' | ''; export interface UsrnData { dateReportUpdate?: unknown; license?: unknown; + /** Records with FAIR Signposting signals (USRN `/usrn` API). */ + supportSignposting?: unknown; + /** Records using COAR vocabularies in metadata (USRN `/usrn` API). */ + vocabulariesCOAR?: unknown; [key: string]: unknown; } diff --git a/src/features/Usrn/utils/getCardStatusConfig.ts b/src/features/Usrn/utils/getCardStatusConfig.ts index 26d531b5..8e8adb14 100644 --- a/src/features/Usrn/utils/getCardStatusConfig.ts +++ b/src/features/Usrn/utils/getCardStatusConfig.ts @@ -1,10 +1,19 @@ +import type { DasData } from '@features/Das/types/data.types'; +import type { OrcidStats } from '@features/Orcid/types/data.types'; import type { SupportStatusVariant, UsrnData } from '@features/Usrn/types/data.types'; +export type CardCounterRow = { + label: string; + value: number; +}; + type CardStatusConfig = { status: SupportStatusVariant; countCovered: number | null; countTotal: number | null; countValue: number | null; + /** Multiple labelled counts (e.g. ORCID in repository vs in CORE). */ + counterRows?: CardCounterRow[]; }; const toNumber = (value: unknown): number | null => { @@ -22,8 +31,16 @@ type GetCardStatusConfigParams = { usrn: UsrnData | null; irus: unknown; rorId: string | null; + /** From `GET /internal/data-providers/:id/orcid/stats` (`useOrcidStats`). */ + orcidStats?: OrcidStats | null; + /** From `GET .../data-access` (`useDasData`). Used for `webAccessibility` (DAS). */ + dasData?: DasData[] | null; }; +const ORCID_COUNTER_REPOSITORY_LABEL = + 'Papers with at least one ORCID in your repository:'; +const ORCID_COUNTER_CORE_LABEL = 'Papers with at least one ORCID in CORE:'; + export const getCardStatusConfig = ({ cardId, rioxx, @@ -32,6 +49,8 @@ export const getCardStatusConfig = ({ usrn, irus, rorId, + orcidStats, + dasData, }: GetCardStatusConfigParams): CardStatusConfig => { const empty: CardStatusConfig = { status: 'no', @@ -78,11 +97,56 @@ export const getCardStatusConfig = ({ } case 'embargoedDocuments': case 'sourceCode': - case 'webAccessibility': - case 'supportSignpostingFAIR': - case 'metadataCOAR': - case 'ORCID': return empty; + case 'webAccessibility': { + const list = dasData ?? []; + const countValue = list.length; + return { + ...empty, + status: hasValue(countValue), + countCovered: null, + countTotal: null, + countValue, + }; + } + case 'ORCID': { + const basic = toNumber(orcidStats?.basic); + const fromOther = toNumber(orcidStats?.fromOtherRepositories); + const basicNum = basic ?? 0; + const fromOtherNum = fromOther ?? 0; + const totalInCore = basicNum + fromOtherNum; + + return { + status: hasValue(basic), + countCovered: null, + countTotal: null, + countValue: basic, + counterRows: [ + { label: ORCID_COUNTER_REPOSITORY_LABEL, value: basicNum }, + { label: ORCID_COUNTER_CORE_LABEL, value: totalInCore }, + ], + }; + } + case 'supportSignpostingFAIR': { + const countCovered = toNumber(usrn?.supportSignposting); + const countTotal = toNumber(statistics?.countMetadata); + return { + status: hasValue(countCovered), + countCovered, + countTotal, + countValue: null, + }; + } + case 'metadataCOAR': { + const countCovered = toNumber(usrn?.vocabulariesCOAR); + const countTotal = toNumber(statistics?.countMetadata); + return { + status: hasValue(countCovered), + countCovered, + countTotal, + countValue: null, + }; + } case 'licensing': { const countCovered = toNumber(usrn?.license); const countTotal = toNumber(statistics?.countMetadata); @@ -106,7 +170,6 @@ export const getCardStatusConfig = ({ countValue: null, }; } - // TODO check for 3.6 if we have ror id case 'ROR': return { ...empty, status: rorId ? 'yes' : 'no' }; default: From 8ddfad9d3161be9beebb4c7a0631f1f91e835390 Mon Sep 17 00:00:00 2001 From: fropa Date: Fri, 8 May 2026 13:45:17 +0400 Subject: [PATCH 09/66] try deploy to aks --- .github/workflows/aks_deploy.yml | 79 ++++++++++ .github/workflows/prod-stg-deploy.yml | 213 -------------------------- kube/deployment.yaml | 40 +++++ kube/httproute.yaml | 19 +++ kube/service.yaml | 16 ++ 5 files changed, 154 insertions(+), 213 deletions(-) create mode 100644 .github/workflows/aks_deploy.yml delete mode 100644 .github/workflows/prod-stg-deploy.yml create mode 100644 kube/deployment.yaml create mode 100644 kube/httproute.yaml create mode 100644 kube/service.yaml diff --git a/.github/workflows/aks_deploy.yml b/.github/workflows/aks_deploy.yml new file mode 100644 index 00000000..944a042f --- /dev/null +++ b/.github/workflows/aks_deploy.yml @@ -0,0 +1,79 @@ +name: AKS deployment - core-frontend-dashboard + +on: + push: + branches: [aks] + workflow_dispatch: + +env: + ACR_NAME: corecontainers + IMAGE_NAME: core-frontend-dashboard + RESOURCE_GROUP: core-frontend + AKS_CLUSTER_NAME: core-frontend-kubernetes-cluster + NAMESPACE: core-frontend + DEPLOYMENT_NAME: core-frontend-dashboard + +jobs: + build-and-deploy: + runs-on: ubuntu-24.04-arm + environment: azure + permissions: + id-token: write + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Azure Login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Set image reference + id: image + run: | + TAG=$(echo ${{ github.sha }} | cut -c1-7) + echo "ref=${{ env.ACR_NAME }}.azurecr.io/aks-${{ env.IMAGE_NAME }}:$TAG" >> $GITHUB_OUTPUT + + - name: Docker ACR Login + run: az acr login --name ${{ env.ACR_NAME }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker build and push + run: | + docker buildx build \ + --platform linux/arm64 \ + --push \ + --build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \ + --build-arg GA_CODE=${{ secrets.GA_CODE }} \ + --build-arg API_KEY=${{ secrets.API_KEY }} \ + -t ${{ steps.image.outputs.ref }} . + + - name: Get AKS credentials + run: | + az aks get-credentials \ + --resource-group ${{ env.RESOURCE_GROUP }} \ + --name ${{ env.AKS_CLUSTER_NAME }} \ + --admin \ + --overwrite-existing + + - name: Apply Service and HTTPRoute + run: | + kubectl apply -f kube/service.yaml + kubectl apply -f kube/httproute.yaml + + - name: Deploy to AKS + run: | + export IMAGE="${{ steps.image.outputs.ref }}" + envsubst < kube/deployment.yaml | kubectl apply -f - + + - name: Wait for rollout + run: | + kubectl rollout status deployment/${{ env.DEPLOYMENT_NAME }} \ + --namespace ${{ env.NAMESPACE }} \ + --timeout=300s \ No newline at end of file diff --git a/.github/workflows/prod-stg-deploy.yml b/.github/workflows/prod-stg-deploy.yml deleted file mode 100644 index 4a16671d..00000000 --- a/.github/workflows/prod-stg-deploy.yml +++ /dev/null @@ -1,213 +0,0 @@ -name: Container App Prod/Staging Deploy - -on: - push: - branches: - - main - - schedule: - - cron: "0 1 * * *" - - workflow_dispatch: - inputs: - target_env: - description: "Where to deploy" - required: true - type: choice - options: - - staging - - prod - default: staging - branch: - description: "Branch to build (default = main)" - required: true - default: main - -env: - ACR_NAME: corecontainers - IMAGE_NAME: core-frontend-dashboard - - RESOURCE_GROUP: core-frontend - PROD_APP_NAME: core-frontend-dashboard - - STAGING_RESOURCE_GROUP: core-frontend-stage - STAGING_APP_NAME: core-frontend-stage-dashboard - - SENTRY_DSN: https://d116b39bd427449799cdd1516f6f97a1@sentry.io/2091955 - PORT: 8080 - - ALLOWED_PROD_BRANCH: main - -permissions: - id-token: write - contents: read - -jobs: - build-and-push: - runs-on: ubuntu-latest - environment: azure - outputs: - tag: ${{ steps.vars.outputs.tag }} - deploy_env: ${{ steps.vars.outputs.deploy_env }} - app_name: ${{ steps.vars.outputs.app_name }} - ref_to_build: ${{ steps.vars.outputs.ref_to_build }} - branch_tag: ${{ steps.vars.outputs.branch_tag }} - image_tag: ${{ steps.vars.outputs.image_tag }} - - steps: - - name: Compute variables - id: vars - shell: bash - run: | - SHA7="$(echo "${GITHUB_SHA}" | cut -c1-7)" - - # ✅ ADDED: schedule always deploys PROD from the frozen branch - if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then - DEPLOY_ENV="prod" - REF_TO_BUILD="${{ env.ALLOWED_PROD_BRANCH }}" - - elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - DEPLOY_ENV="${{ inputs.target_env }}" - REF_TO_BUILD="${{ inputs.branch }}" - - # existing: Freeze PROD to only one branch; fail if any other branch is selected for PROD - if [[ "${DEPLOY_ENV}" == "prod" && "${REF_TO_BUILD}" != "${{ env.ALLOWED_PROD_BRANCH }}" ]]; then - echo "::error title=Blocked prod deployment::Prod deployments are only allowed from '${{ env.ALLOWED_PROD_BRANCH }}'. You selected '${REF_TO_BUILD}'." - exit 1 - fi - else - DEPLOY_ENV="staging" - REF_TO_BUILD="${GITHUB_REF_NAME}" - fi - - BRANCH_TAG="$(echo "${REF_TO_BUILD}" | tr '[:upper:]' '[:lower:]' \ - | sed -E 's#[^a-z0-9_.-]+#-#g' | sed -E 's#^-+##; s#-+$##' | cut -c1-40)" - - if [[ "${DEPLOY_ENV}" == "prod" ]]; then - APP_NAME="${{ env.PROD_APP_NAME }}" - IMAGE_TAG="prod" - else - APP_NAME="${{ env.STAGING_APP_NAME }}" - IMAGE_TAG="stg-${BRANCH_TAG}" - fi - - echo "tag=${SHA7}" >> "$GITHUB_OUTPUT" - echo "deploy_env=${DEPLOY_ENV}" >> "$GITHUB_OUTPUT" - echo "app_name=${APP_NAME}" >> "$GITHUB_OUTPUT" - echo "ref_to_build=${REF_TO_BUILD}" >> "$GITHUB_OUTPUT" - echo "branch_tag=${BRANCH_TAG}" >> "$GITHUB_OUTPUT" - echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" - - - - name: Summary - run: | - { - echo "## Deploy plan" - echo "- **Trigger:** ${{ github.event_name }}" - echo "- **Env:** ${{ steps.vars.outputs.deploy_env }}" - echo "- **Branch:** ${{ steps.vars.outputs.ref_to_build }}" - echo "- **App:** ${{ steps.vars.outputs.app_name }}" - echo "- **ACR:** ${{ env.ACR_NAME }}" - echo "- **Image:** ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}" - echo "- **Tag (sha7):** ${{ steps.vars.outputs.tag }}" - echo "- **Tag (env):** ${{ steps.vars.outputs.image_tag }}" - } >> "$GITHUB_STEP_SUMMARY" - - - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ steps.vars.outputs.ref_to_build }} - - - name: Azure Login - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Docker ACR Login - run: az acr login --name ${{ env.ACR_NAME }} - - - name: Docker Build - shell: bash - run: | - if [[ "${{ steps.vars.outputs.deploy_env }}" == "staging" ]]; then - NODE_ENV=development - else - NODE_ENV=production - fi - - docker build \ - --build-arg NODE_ENV=$NODE_ENV \ - --build-arg SENTRY_DSN=${{env.SENTRY_DSN}} \ - --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ - --build-arg GA_TRACKING_CODE=${{secrets.GA_TRACKING_CODE}} \ - --build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \ - --build-arg API_KEY=${{ secrets.API_KEY }} \ - -t ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }} \ - -t ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.image_tag }} . - - - name: Docker Push - run: | - docker push ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }} - docker push ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.image_tag }} - - deploy: - needs: build-and-push - runs-on: ubuntu-latest - environment: azure - env: - TAG: ${{ needs.build-and-push.outputs.tag }} - IMAGE_TAG: ${{ needs.build-and-push.outputs.image_tag }} - - steps: - - name: Azure Login - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Resolve deploy target - id: target - shell: bash - run: | - if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then - ENV="prod" - elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - ENV="${{ inputs.target_env }}" - else - ENV="staging" - fi - - if [[ "$ENV" == "prod" ]]; then - echo "APP=${{ env.PROD_APP_NAME }}" >> $GITHUB_OUTPUT - echo "RG=${{ env.RESOURCE_GROUP }}" >> $GITHUB_OUTPUT - else - echo "APP=${{ env.STAGING_APP_NAME }}" >> $GITHUB_OUTPUT - echo "RG=${{ env.STAGING_RESOURCE_GROUP }}" >> $GITHUB_OUTPUT - fi - - - name: Enforce prod branch freeze - shell: bash - run: | - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - if [[ "${{ inputs.target_env }}" == "prod" && "${{ needs.build-and-push.outputs.ref_to_build }}" != "${{ env.ALLOWED_PROD_BRANCH }}" ]]; then - echo "::error title=Blocked prod deployment::Prod deployments are only allowed from '${{ env.ALLOWED_PROD_BRANCH }}'." - exit 1 - fi - fi - - - name: Deploy to Azure Container App - shell: bash - run: | - REDEPLOY_AT="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-$(date -u +%Y%m%d%H%M%S)" - - az containerapp update \ - --name ${{ steps.target.outputs.APP }} \ - --resource-group ${{ steps.target.outputs.RG }} \ - --image ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ env.TAG }} \ - --container-name ${{ steps.target.outputs.APP }} \ - --set configuration.ingress.targetPort=${{ env.PORT }} \ - --set-env-vars REDEPLOY_AT="$REDEPLOY_AT" diff --git a/kube/deployment.yaml b/kube/deployment.yaml new file mode 100644 index 00000000..069be633 --- /dev/null +++ b/kube/deployment.yaml @@ -0,0 +1,40 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: core-frontend-dashboard + namespace: core-frontend + labels: + app: core-frontend-dashboard +spec: + replicas: 1 + revisionHistoryLimit: 5 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: core-frontend-dashboard + template: + metadata: + labels: + app: core-frontend-dashboard + spec: + nodeSelector: + agentpool: userpool + containers: + - name: core-frontend-dashboard + image: ${IMAGE} + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + protocol: TCP + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 1000m + memory: 1000Mi \ No newline at end of file diff --git a/kube/httproute.yaml b/kube/httproute.yaml new file mode 100644 index 00000000..6695d123 --- /dev/null +++ b/kube/httproute.yaml @@ -0,0 +1,19 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: core-frontend-dashboard + namespace: core-frontend +spec: + parentRefs: + - name: public-gateway + namespace: gateway-system + hostnames: + - dashboard.preferans.uk + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: core-frontend-dashboard + port: 80 \ No newline at end of file diff --git a/kube/service.yaml b/kube/service.yaml new file mode 100644 index 00000000..8c4daca5 --- /dev/null +++ b/kube/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: core-frontend-dashboard + namespace: core-frontend + labels: + app: core-frontend-dashboard +spec: + type: ClusterIP + selector: + app: core-frontend-dashboard + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP \ No newline at end of file From 406e5e603462c5b68aac156961b97ae324fe0c69 Mon Sep 17 00:00:00 2001 From: fropa Date: Fri, 8 May 2026 13:50:59 +0400 Subject: [PATCH 10/66] add husky 0 --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8e455d28..7581b9e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,8 @@ ENV NODE_ENV=${NODE_ENV} \ NPM_TOKEN=${NPM_TOKEN} \ SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} \ VITE_SENTRY_DSN=${SENTRY_DSN} \ - VITE_GA_TRACKING_CODE=${GA_TRACKING_CODE} + VITE_GA_TRACKING_CODE=${GA_TRACKING_CODE} \ + HUSKY=0 WORKDIR /app From a38e5085352a6f1f5159b83e9310dfda67fdf259 Mon Sep 17 00:00:00 2001 From: fropa Date: Fri, 8 May 2026 13:55:37 +0400 Subject: [PATCH 11/66] add dependencies to package.json --- Dockerfile | 3 +-- package.json | 4 +++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Dockerfile b/Dockerfile index 7581b9e4..6d5f0f3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,7 @@ ENV NODE_ENV=${NODE_ENV} \ NPM_TOKEN=${NPM_TOKEN} \ SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} \ VITE_SENTRY_DSN=${SENTRY_DSN} \ - VITE_GA_TRACKING_CODE=${GA_TRACKING_CODE} \ - HUSKY=0 + VITE_GA_TRACKING_CODE=${GA_TRACKING_CODE} WORKDIR /app diff --git a/package.json b/package.json index 68650034..d46213f8 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,9 @@ "private": true, "pnpm": { "onlyBuiltDependencies": [ - "@oacore/core-ui" + "@oacore/core-ui", + "@sentry/cli", + "esbuild" ] }, "version": "0.0.0", From 19ebf39e650f4e7c8cd4cd4fcd10934619511bd7 Mon Sep 17 00:00:00 2001 From: fropa Date: Fri, 8 May 2026 13:57:48 +0400 Subject: [PATCH 12/66] add husky --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 6d5f0f3e..7581b9e4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,7 +14,8 @@ ENV NODE_ENV=${NODE_ENV} \ NPM_TOKEN=${NPM_TOKEN} \ SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} \ VITE_SENTRY_DSN=${SENTRY_DSN} \ - VITE_GA_TRACKING_CODE=${GA_TRACKING_CODE} + VITE_GA_TRACKING_CODE=${GA_TRACKING_CODE} \ + HUSKY=0 WORKDIR /app From 2cf1c9a36fc369241c97658b76f30ca5ad33ece6 Mon Sep 17 00:00:00 2001 From: fropa Date: Fri, 8 May 2026 14:00:06 +0400 Subject: [PATCH 13/66] change pnpm install --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 7581b9e4..78617cc3 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,7 +26,7 @@ RUN set -eux; \ test -n "$TOKEN"; \ printf "@oacore:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=%s\n" "$TOKEN" > .npmrc; \ corepack enable; \ - pnpm install --frozen-lockfile; \ + pnpm install --frozen-lockfile --config.confirmModulesPurge=false; \ rm -f .npmrc COPY . . From 47c0ebad3f5ff1aeeb038b65022b1baca638b555 Mon Sep 17 00:00:00 2001 From: fropa Date: Fri, 8 May 2026 14:03:02 +0400 Subject: [PATCH 14/66] revert all changes --- Dockerfile | 5 ++--- package.json | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/Dockerfile b/Dockerfile index 78617cc3..6d5f0f3e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -14,8 +14,7 @@ ENV NODE_ENV=${NODE_ENV} \ NPM_TOKEN=${NPM_TOKEN} \ SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} \ VITE_SENTRY_DSN=${SENTRY_DSN} \ - VITE_GA_TRACKING_CODE=${GA_TRACKING_CODE} \ - HUSKY=0 + VITE_GA_TRACKING_CODE=${GA_TRACKING_CODE} WORKDIR /app @@ -26,7 +25,7 @@ RUN set -eux; \ test -n "$TOKEN"; \ printf "@oacore:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=%s\n" "$TOKEN" > .npmrc; \ corepack enable; \ - pnpm install --frozen-lockfile --config.confirmModulesPurge=false; \ + pnpm install --frozen-lockfile; \ rm -f .npmrc COPY . . diff --git a/package.json b/package.json index d46213f8..68650034 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,7 @@ "private": true, "pnpm": { "onlyBuiltDependencies": [ - "@oacore/core-ui", - "@sentry/cli", - "esbuild" + "@oacore/core-ui" ] }, "version": "0.0.0", From 8924d0ac6abc67ca6dd07c788bd837e3e5cc8ae9 Mon Sep 17 00:00:00 2001 From: fropa Date: Fri, 8 May 2026 14:05:53 +0400 Subject: [PATCH 15/66] add node env and envs during building --- .github/workflows/aks_deploy.yml | 4 +++- kube/deployment.yaml | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/.github/workflows/aks_deploy.yml b/.github/workflows/aks_deploy.yml index 944a042f..80092fa6 100644 --- a/.github/workflows/aks_deploy.yml +++ b/.github/workflows/aks_deploy.yml @@ -49,8 +49,10 @@ jobs: docker buildx build \ --platform linux/arm64 \ --push \ + --build-arg SENTRY_DSN=${{env.SENTRY_DSN}} \ + --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ + --build-arg GA_TRACKING_CODE=${{secrets.GA_TRACKING_CODE}} \ --build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \ - --build-arg GA_CODE=${{ secrets.GA_CODE }} \ --build-arg API_KEY=${{ secrets.API_KEY }} \ -t ${{ steps.image.outputs.ref }} . diff --git a/kube/deployment.yaml b/kube/deployment.yaml index 069be633..b7efcc50 100644 --- a/kube/deployment.yaml +++ b/kube/deployment.yaml @@ -26,6 +26,9 @@ spec: containers: - name: core-frontend-dashboard image: ${IMAGE} + env: + - name: NODE_ENV + value: production imagePullPolicy: IfNotPresent ports: - name: http From d54233f0aa46de8c5a3f057a1e1daab24571ebe9 Mon Sep 17 00:00:00 2001 From: fropa Date: Fri, 8 May 2026 14:10:36 +0400 Subject: [PATCH 16/66] add sentry dns --- .github/workflows/aks_deploy.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/aks_deploy.yml b/.github/workflows/aks_deploy.yml index 80092fa6..33a83ae5 100644 --- a/.github/workflows/aks_deploy.yml +++ b/.github/workflows/aks_deploy.yml @@ -12,6 +12,7 @@ env: AKS_CLUSTER_NAME: core-frontend-kubernetes-cluster NAMESPACE: core-frontend DEPLOYMENT_NAME: core-frontend-dashboard + SENTRY_DSN: https://d116b39bd427449799cdd1516f6f97a1@sentry.io/2091955 jobs: build-and-deploy: From cf02d4a1e872ef1f11da5577f203da4ed3c11074 Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Fri, 8 May 2026 18:11:17 +0400 Subject: [PATCH 17/66] CORE: update docs file --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 86aa4284..32b7c9dd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -29,7 +29,7 @@ RUN set -eux; \ printf '%s\n' '@oacore:registry=https://npm.pkg.github.com' "//npm.pkg.github.com/:_authToken=${TOKEN}" > .npmrc; \ corepack enable; \ corepack prepare pnpm@9 --activate; \ - pnpm install --frozen-lockfile; \ + NODE_ENV=development pnpm install --frozen-lockfile; \ rm -f .npmrc COPY . . From 1e237833a5c9ff5b4544d82db4354510267ab20b Mon Sep 17 00:00:00 2001 From: fropa Date: Thu, 14 May 2026 15:14:54 +0400 Subject: [PATCH 18/66] add core.ac.uk test domain --- kube/httproute.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/kube/httproute.yaml b/kube/httproute.yaml index 6695d123..43542cb7 100644 --- a/kube/httproute.yaml +++ b/kube/httproute.yaml @@ -9,6 +9,7 @@ spec: namespace: gateway-system hostnames: - dashboard.preferans.uk + - dashboard-aks.core.ac.uk rules: - matches: - path: @@ -16,4 +17,4 @@ spec: value: / backendRefs: - name: core-frontend-dashboard - port: 80 \ No newline at end of file + port: 80 From eba9fa8f7c5670fedfc83773ce0398302e822457 Mon Sep 17 00:00:00 2001 From: fropa Date: Thu, 14 May 2026 15:34:35 +0400 Subject: [PATCH 19/66] Add new hostname to httproute configuration --- kube/httproute.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/kube/httproute.yaml b/kube/httproute.yaml index 43542cb7..e8976fd4 100644 --- a/kube/httproute.yaml +++ b/kube/httproute.yaml @@ -10,6 +10,7 @@ spec: hostnames: - dashboard.preferans.uk - dashboard-aks.core.ac.uk + - dashboard.core.ac.uk rules: - matches: - path: From 6750d00867565e15b42abbba6b14ec629274c745 Mon Sep 17 00:00:00 2001 From: fropa Date: Mon, 18 May 2026 13:22:41 +0400 Subject: [PATCH 20/66] merge main to aks branch (#782) * CORE: add development (#779) * CORE: update img src (#780) * CORE: update linking (#781) --------- Co-authored-by: ekachxaidze98 <65679299+ekachxaidze98@users.noreply.github.com> --- src/features/DashboardGuide/config/stepsConfig.ts | 10 +++++----- src/features/DepositCompliance/texts/compliance.json | 2 +- src/features/Overview/texts/depositing.json | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/features/DashboardGuide/config/stepsConfig.ts b/src/features/DashboardGuide/config/stepsConfig.ts index 6d524236..4dcea533 100644 --- a/src/features/DashboardGuide/config/stepsConfig.ts +++ b/src/features/DashboardGuide/config/stepsConfig.ts @@ -1,9 +1,9 @@ -import texts from '../texts'; +import firstStep from '@/assets/icons/stepFirst.svg'; +import secondStep from '@/assets/icons/stepTwo.svg'; +import thirdStep from '@/assets/icons/stepThree.svg'; +import forthStep from '@/assets/icons/stepFour.svg'; -const firstStep = '/src/assets/icons/stepFirst.svg'; -const secondStep = '/src/assets/icons/stepTwo.svg'; -const thirdStep = '/src/assets/icons/stepThree.svg'; -const forthStep = '/src/assets/icons/stepFour.svg'; +import texts from '../texts'; export interface StepConfig { title: string; diff --git a/src/features/DepositCompliance/texts/compliance.json b/src/features/DepositCompliance/texts/compliance.json index 47470a08..47ddc969 100644 --- a/src/features/DepositCompliance/texts/compliance.json +++ b/src/features/DepositCompliance/texts/compliance.json @@ -40,7 +40,7 @@ "chart": { "title": "Deposit time lag", "body": "The chart displays outputs' distribution according to deposit time lag.\n\n- **Time lag** is the difference between deposit and publication date\n in the repository.\n- **Publication date** is taken from [Crossref](https://www.crossref.org).\n When this is not available, the repository's one used.\n- **Deposit date** is calculated using specific techniques.\n On EPrints repositories it is extracted from the *Date Deposited* field,\n while on DSpace the *dc.date.available* field is used.", - "warning": "Check our [guidelines](https://core.ac.uk/ref-audit) and enable the\n required support and then notify us once ready, so that we can initiate\n collecting this information from your repository." + "warning": "Check our [guidelines](https://core.ac.uk/ref) and enable the\n required support and then notify us once ready, so that we can initiate\n collecting this information from your repository." }, "noData": { "body": "No deposit time lag data available for the selected time period." diff --git a/src/features/Overview/texts/depositing.json b/src/features/Overview/texts/depositing.json index 59ab03a9..715bd338 100644 --- a/src/features/Overview/texts/depositing.json +++ b/src/features/Overview/texts/depositing.json @@ -1,9 +1,9 @@ { "title": "OA compliance (REF)", - "tooltip": " Check compliance of content with REF 2021 Open Access policy. Access the data overview count, view your outputs’\n time lag, see the matching of your outputs’ publication dates and get an insight of your outputs’ deposit dates.", + "tooltip": " Check compliance of content with REF 2029 Open Access policy. Access the data overview count, view your outputs’\n time lag, see the matching of your outputs’ publication dates and get an insight of your outputs’ deposit dates.", "action": "Details", "description": { - "missingData": "Your repository is not configured to expose information on dates of\n deposit in a machine-readable format. For more information check our\n [guidelines](https://core.ac.uk/ref-audit) and contact us at\n [theteam@core.ac.uk](mailto:t%68%65t%65am%40core%2e%61c%2eu%6b)", + "missingData": "Your repository is not configured to expose information on dates of\n deposit in a machine-readable format. For more information check our\n [guidelines](https://core.ac.uk/ref) and contact us at\n [theteam@core.ac.uk](mailto:t%68%65t%65am%40core%2e%61c%2eu%6b)", "regionWarning": "Specific to UK repositories.", "complianceLevel": { "render": "{{amount}}% of outputs are non-compliant." From 715f750d9bc96be8915eab35a77d958877e5bc7c Mon Sep 17 00:00:00 2001 From: anwiev <50668058+anwiev@users.noreply.github.com> Date: Mon, 18 May 2026 16:42:46 +0300 Subject: [PATCH 21/66] Add GA tracking code environment variable --- kube/deployment.yaml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/kube/deployment.yaml b/kube/deployment.yaml index b7efcc50..29de2a6d 100644 --- a/kube/deployment.yaml +++ b/kube/deployment.yaml @@ -29,6 +29,8 @@ spec: env: - name: NODE_ENV value: production + - name: GA_TRACKING_CODE + value: ${{secrets.GA_TRACKING_CODE}} imagePullPolicy: IfNotPresent ports: - name: http @@ -40,4 +42,4 @@ spec: memory: 256Mi limits: cpu: 1000m - memory: 1000Mi \ No newline at end of file + memory: 1000Mi From a0d2680f237e94d2e6e0a0c483452b0f54d3fa4e Mon Sep 17 00:00:00 2001 From: anwiev <50668058+anwiev@users.noreply.github.com> Date: Mon, 18 May 2026 16:55:44 +0300 Subject: [PATCH 22/66] Fix syntax for accessing GA_TRACKING_CODE secret --- kube/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kube/deployment.yaml b/kube/deployment.yaml index 29de2a6d..e5709b38 100644 --- a/kube/deployment.yaml +++ b/kube/deployment.yaml @@ -30,7 +30,7 @@ spec: - name: NODE_ENV value: production - name: GA_TRACKING_CODE - value: ${{secrets.GA_TRACKING_CODE}} + value: ${secrets.GA_TRACKING_CODE} imagePullPolicy: IfNotPresent ports: - name: http From 69e67b8b2d7eb19645044dc53a3b4a0e3336088e Mon Sep 17 00:00:00 2001 From: fropa Date: Tue, 19 May 2026 11:27:29 +0400 Subject: [PATCH 23/66] add secrets --- .github/workflows/aks_deploy.yml | 12 ++++++++++++ kube/deployment.yaml | 5 +++-- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/.github/workflows/aks_deploy.yml b/.github/workflows/aks_deploy.yml index 33a83ae5..29be2925 100644 --- a/.github/workflows/aks_deploy.yml +++ b/.github/workflows/aks_deploy.yml @@ -65,6 +65,18 @@ jobs: --admin \ --overwrite-existing + - name: Sync secrets to AKS + env: + API_KEY: ${{ secrets.API_KEY }} + GA_TRACKING_CODE: ${{ secrets.GA_TRACKING_CODE }} + run: | + kubectl create secret generic core-frontend-dashboard-secrets \ + --namespace core-frontend \ + --build-arg SENTRY_DSN=${{env.SENTRY_DSN}} \ + --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ + --build-arg GA_TRACKING_CODE=${{secrets.GA_TRACKING_CODE}} \ + --dry-run=client -o yaml | kubectl apply -f - + - name: Apply Service and HTTPRoute run: | kubectl apply -f kube/service.yaml diff --git a/kube/deployment.yaml b/kube/deployment.yaml index e5709b38..9775c785 100644 --- a/kube/deployment.yaml +++ b/kube/deployment.yaml @@ -29,9 +29,10 @@ spec: env: - name: NODE_ENV value: production - - name: GA_TRACKING_CODE - value: ${secrets.GA_TRACKING_CODE} imagePullPolicy: IfNotPresent + envFrom: + - secretRef: + name: core-frontend-dashboard-secrets ports: - name: http containerPort: 8080 From a6e9d2d99533ffadf6b8d2e585087695da560224 Mon Sep 17 00:00:00 2001 From: fropa Date: Tue, 19 May 2026 11:32:13 +0400 Subject: [PATCH 24/66] fix secret naming --- .github/workflows/aks_deploy.yml | 7 ++++--- kube/deployment.yaml | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/aks_deploy.yml b/.github/workflows/aks_deploy.yml index 29be2925..e3d6bd04 100644 --- a/.github/workflows/aks_deploy.yml +++ b/.github/workflows/aks_deploy.yml @@ -72,9 +72,10 @@ jobs: run: | kubectl create secret generic core-frontend-dashboard-secrets \ --namespace core-frontend \ - --build-arg SENTRY_DSN=${{env.SENTRY_DSN}} \ - --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ - --build-arg GA_TRACKING_CODE=${{secrets.GA_TRACKING_CODE}} \ + --from-literal=SENTRY_DSN="${{ env.SENTRY_DSN }}" \ + --from-literal=SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}" \ + --from-literal=GA_TRACKING_CODE="${{ secrets.GA_TRACKING_CODE }}" \ + --from-literal=API_KEY="${{ secrets.API_KEY }}" \ --dry-run=client -o yaml | kubectl apply -f - - name: Apply Service and HTTPRoute diff --git a/kube/deployment.yaml b/kube/deployment.yaml index 9775c785..d2bbf56b 100644 --- a/kube/deployment.yaml +++ b/kube/deployment.yaml @@ -6,7 +6,7 @@ metadata: labels: app: core-frontend-dashboard spec: - replicas: 1 + replicas: 3 revisionHistoryLimit: 5 strategy: type: RollingUpdate From a542662c8844fc67b74551e5f60565b025153e5a Mon Sep 17 00:00:00 2001 From: fropa Date: Thu, 21 May 2026 15:22:00 +0400 Subject: [PATCH 25/66] merge main to aks (#784) * CORE: add development (#779) * CORE: update img src (#780) * CORE: update linking (#781) --------- Co-authored-by: ekachxaidze98 <65679299+ekachxaidze98@users.noreply.github.com> From 9a09da44af0bdd8097d388bef793258a4f72e0ec Mon Sep 17 00:00:00 2001 From: fropa Date: Thu, 21 May 2026 15:27:33 +0400 Subject: [PATCH 26/66] adding previews --- .github/workflows/preview-deploy.yml | 134 +++++++++++++++++++++++++ .github/workflows/preview-teardown.yml | 95 ++++++++++++++++++ kube/preview.yml | 75 ++++++++++++++ 3 files changed, 304 insertions(+) create mode 100644 .github/workflows/preview-deploy.yml create mode 100644 .github/workflows/preview-teardown.yml create mode 100644 kube/preview.yml diff --git a/.github/workflows/preview-deploy.yml b/.github/workflows/preview-deploy.yml new file mode 100644 index 00000000..2c8e82b0 --- /dev/null +++ b/.github/workflows/preview-deploy.yml @@ -0,0 +1,134 @@ +name: Preview deploy + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch to deploy as preview' + required: true + type: string + +env: + ACR_NAME: corecontainers + IMAGE_NAME: preview-hello + RESOURCE_GROUP: core-frontend + AKS_CLUSTER_NAME: core-frontend-kubernetes-cluster + PREVIEW_NAMESPACE: core-frontend-preview + ORIGIN_IP: 74.177.197.180 + +jobs: + preview: + runs-on: ubuntu-24.04-arm + environment: azure + permissions: + id-token: write + contents: read + + steps: + - name: Checkout selected branch + uses: actions/checkout@v4 + with: + ref: ${{ inputs.branch }} + + - name: Compute names + id: vars + run: | + RAW="${{ inputs.branch }}" + SLUG=$(echo "$RAW" \ + | tr '[:upper:]' '[:lower:]' \ + | sed 's/[^a-z0-9]/-/g' \ + | sed 's/^-*//;s/-*$//' \ + | cut -c1-40 \ + | sed 's/-*$//') + TAG=$(echo ${{ github.sha }} | cut -c1-7) + DNS_NAME="preview-${SLUG}" + echo "slug=$SLUG" >> $GITHUB_OUTPUT + echo "name=preview-$SLUG" >> $GITHUB_OUTPUT + echo "dns_name=$DNS_NAME" >> $GITHUB_OUTPUT + echo "host=$DNS_NAME.core.ac.uk" >> $GITHUB_OUTPUT + echo "image=${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:$SLUG-$TAG" >> $GITHUB_OUTPUT + + - name: Ensure Cloudflare DNS record + env: + CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} + CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }} + DNS_NAME: ${{ steps.vars.outputs.dns_name }} + HOST: ${{ steps.vars.outputs.host }} + run: | + set -e + + LOOKUP=$(curl -s -X GET \ + "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=A&name=${HOST}" \ + -H "Authorization: Bearer ${CF_API_TOKEN}" \ + -H "Content-Type: application/json") + + EXISTING=$(echo "$LOOKUP" | jq -r '.result[0].id // empty') + + if [ -n "$EXISTING" ]; then + echo "::notice::DNS record for $HOST already exists (id=$EXISTING)" + exit 0 + fi + + echo "Creating DNS record for $HOST -> ${ORIGIN_IP}" + RESPONSE=$(curl -s -X POST \ + "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records" \ + -H "Authorization: Bearer ${CF_API_TOKEN}" \ + -H "Content-Type: application/json" \ + --data "{\"type\":\"A\",\"name\":\"${DNS_NAME}\",\"content\":\"${ORIGIN_IP}\",\"proxied\":true,\"ttl\":1}") + + SUCCESS=$(echo "$RESPONSE" | jq -r '.success') + if [ "$SUCCESS" != "true" ]; then + echo "::error::DNS record creation failed" + echo "$RESPONSE" | jq + exit 1 + fi + + echo "::notice::DNS record created for $HOST" + + - name: Azure Login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Docker ACR Login + run: az acr login --name ${{ env.ACR_NAME }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker build and push + run: | + docker buildx build \ + --platform linux/arm64 \ + --push \ + -t ${{ steps.vars.outputs.image }} . + + - name: Get AKS credentials + run: | + az aks get-credentials \ + --resource-group ${{ env.RESOURCE_GROUP }} \ + --name ${{ env.AKS_CLUSTER_NAME }} \ + --admin --overwrite-existing + + - name: Deploy preview + run: | + export NAME="${{ steps.vars.outputs.name }}" + export IMAGE="${{ steps.vars.outputs.image }}" + export HOST="${{ steps.vars.outputs.host }}" + export BRANCH="${{ inputs.branch }}" + envsubst < kube/preview.yml | kubectl apply -f - + + - name: Wait for rollout + run: | + kubectl rollout status deployment/${{ steps.vars.outputs.name }} \ + -n ${{ env.PREVIEW_NAMESPACE }} \ + --timeout=300s + + - name: Output preview URL + run: | + echo "### 🚀 Preview deployed" >> $GITHUB_STEP_SUMMARY + echo "URL: https://${{ steps.vars.outputs.host }}" >> $GITHUB_STEP_SUMMARY + echo "Branch: \`${{ inputs.branch }}\`" >> $GITHUB_STEP_SUMMARY + echo "Image: \`${{ steps.vars.outputs.image }}\`" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/.github/workflows/preview-teardown.yml b/.github/workflows/preview-teardown.yml new file mode 100644 index 00000000..da043ae6 --- /dev/null +++ b/.github/workflows/preview-teardown.yml @@ -0,0 +1,95 @@ +name: Preview teardown + +on: + workflow_dispatch: + inputs: + branch: + description: 'Branch whose preview to remove' + required: true + type: string + +env: + PREVIEW_NAMESPACE: core-frontend-preview + +jobs: + teardown: + runs-on: ubuntu-24.04-arm + environment: azure + permissions: + id-token: write + contents: read + + steps: + - name: Compute names + id: vars + run: | + RAW="${{ inputs.branch }}" + SLUG=$(echo "$RAW" \ + | tr '[:upper:]' '[:lower:]' \ + | sed 's/[^a-z0-9]/-/g' \ + | sed 's/^-*//;s/-*$//' \ + | cut -c1-40 \ + | sed 's/-*$//') + DNS_NAME="preview-${SLUG}" + echo "name=preview-$SLUG" >> $GITHUB_OUTPUT + echo "dns_name=$DNS_NAME" >> $GITHUB_OUTPUT + echo "host=$DNS_NAME.core.ac.uk" >> $GITHUB_OUTPUT + + - name: Azure Login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Get AKS credentials + run: | + az aks get-credentials \ + --resource-group core-frontend \ + --name core-frontend-kubernetes-cluster \ + --admin --overwrite-existing + + - name: Delete Kubernetes resources + run: | + NAME="${{ steps.vars.outputs.name }}" + NS="${{ env.PREVIEW_NAMESPACE }}" + echo "Deleting all resources for $NAME in namespace $NS" + kubectl delete deployment,service,httproute "$NAME" \ + -n "$NS" --ignore-not-found + echo "::notice::Kubernetes resources removed for $NAME" + + - name: Delete Cloudflare DNS record + env: + CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }} + CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }} + HOST: ${{ steps.vars.outputs.host }} + run: | + RECORD_ID=$(curl -s -X GET \ + "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records?type=A&name=${HOST}" \ + -H "Authorization: Bearer ${CF_API_TOKEN}" \ + | jq -r '.result[0].id // empty') + + if [ -z "$RECORD_ID" ]; then + echo "::notice::No DNS record found for $HOST (already deleted?)" + exit 0 + fi + + echo "Deleting DNS record $HOST (id=$RECORD_ID)" + RESPONSE=$(curl -s -X DELETE \ + "https://api.cloudflare.com/client/v4/zones/${CF_ZONE_ID}/dns_records/${RECORD_ID}" \ + -H "Authorization: Bearer ${CF_API_TOKEN}") + + SUCCESS=$(echo "$RESPONSE" | jq -r '.success') + if [ "$SUCCESS" != "true" ]; then + echo "::error::DNS record deletion failed" + echo "$RESPONSE" | jq + exit 1 + fi + + echo "::notice::DNS record removed for $HOST" + + - name: Summary + run: | + echo "### 🗑️ Preview removed" >> $GITHUB_STEP_SUMMARY + echo "Branch: \`${{ inputs.branch }}\`" >> $GITHUB_STEP_SUMMARY + echo "Was at: https://${{ steps.vars.outputs.host }}" >> $GITHUB_STEP_SUMMARY \ No newline at end of file diff --git a/kube/preview.yml b/kube/preview.yml new file mode 100644 index 00000000..d5604deb --- /dev/null +++ b/kube/preview.yml @@ -0,0 +1,75 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: ${NAME} + namespace: core-frontend-preview + labels: + app: ${NAME} + preview: "true" +spec: + replicas: 1 + selector: + matchLabels: + app: ${NAME} + template: + metadata: + labels: + app: ${NAME} + preview: "true" + spec: + nodeSelector: + agentpool: userpool + containers: + - name: app + image: ${IMAGE} + ports: + - name: http + containerPort: 8080 + env: + - name: BRANCH_NAME + value: ${BRANCH} + resources: + requests: + cpu: 50m + memory: 64Mi + limits: + cpu: 250m + memory: 256Mi +--- +apiVersion: v1 +kind: Service +metadata: + name: ${NAME} + namespace: core-frontend-preview +spec: + type: ClusterIP + selector: + app: ${NAME} + ports: + - name: http + port: 80 + targetPort: http +--- +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: ${NAME} + namespace: core-frontend-preview + labels: + preview: "true" +spec: + parentRefs: + - group: gateway.networking.k8s.io + kind: Gateway + name: public-gateway + namespace: gateway-system + hostnames: + - ${HOST} + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: ${NAME} + port: 80 From dcb1006b7e03fea64538843ad1567f09e2bbc4c5 Mon Sep 17 00:00:00 2001 From: Valerii Budko Date: Wed, 3 Jun 2026 16:34:02 +0100 Subject: [PATCH 27/66] Set gitignore config npm (#785) --- .gitignore | 1 + .npmrc | 1 - .npmrc.dist | 2 ++ 3 files changed, 3 insertions(+), 1 deletion(-) delete mode 100644 .npmrc create mode 100644 .npmrc.dist diff --git a/.gitignore b/.gitignore index effcf7aa..a356ed59 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* .pnpm-store +.npmrc node_modules dist diff --git a/.npmrc b/.npmrc deleted file mode 100644 index 06d813ea..00000000 --- a/.npmrc +++ /dev/null @@ -1 +0,0 @@ -@oacore:registry=https://npm.pkg.github.com diff --git a/.npmrc.dist b/.npmrc.dist new file mode 100644 index 00000000..c7a20ca3 --- /dev/null +++ b/.npmrc.dist @@ -0,0 +1,2 @@ +@oacore:registry=https://npm.pkg.github.com +//npm.pkg.github.com/:_authToken=${NPM_TOKEN} \ No newline at end of file From 9ab602bb9416a1b5ee5570205ad1df10af8c0bbd Mon Sep 17 00:00:00 2001 From: Valerii Budko Date: Mon, 8 Jun 2026 08:27:03 +0100 Subject: [PATCH 28/66] Set gitignore config npm (#787) From 5adabbe5b426aea24a8286d56960e482e05b619c Mon Sep 17 00:00:00 2001 From: fropa Date: Fri, 12 Jun 2026 13:47:59 +0400 Subject: [PATCH 29/66] Refactor AKS deployment workflow and add sourcemaps upload --- .github/workflows/aks_deploy.yml | 85 +++++++++++++++++++++++++++----- 1 file changed, 72 insertions(+), 13 deletions(-) diff --git a/.github/workflows/aks_deploy.yml b/.github/workflows/aks_deploy.yml index e3d6bd04..55638f32 100644 --- a/.github/workflows/aks_deploy.yml +++ b/.github/workflows/aks_deploy.yml @@ -21,11 +21,21 @@ jobs: permissions: id-token: write contents: read + outputs: + release: ${{ steps.image.outputs.release }} + sha: ${{ github.sha }} steps: - name: Checkout uses: actions/checkout@v4 + - name: Set image reference + id: image + run: | + TAG=$(echo ${{ github.sha }} | cut -c1-7) + echo "ref=${{ env.ACR_NAME }}.azurecr.io/aks-${{ env.IMAGE_NAME }}:$TAG" >> $GITHUB_OUTPUT + echo "release=$TAG" >> $GITHUB_OUTPUT + - name: Azure Login uses: azure/login@v2 with: @@ -33,12 +43,6 @@ jobs: tenant-id: ${{ secrets.AZURE_TENANT_ID }} subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - name: Set image reference - id: image - run: | - TAG=$(echo ${{ github.sha }} | cut -c1-7) - echo "ref=${{ env.ACR_NAME }}.azurecr.io/aks-${{ env.IMAGE_NAME }}:$TAG" >> $GITHUB_OUTPUT - - name: Docker ACR Login run: az acr login --name ${{ env.ACR_NAME }} @@ -50,11 +54,12 @@ jobs: docker buildx build \ --platform linux/arm64 \ --push \ - --build-arg SENTRY_DSN=${{env.SENTRY_DSN}} \ + --build-arg SENTRY_DSN=${{ env.SENTRY_DSN }} \ --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ - --build-arg GA_TRACKING_CODE=${{secrets.GA_TRACKING_CODE}} \ + --build-arg GA_TRACKING_CODE=${{ secrets.GA_TRACKING_CODE }} \ --build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \ --build-arg API_KEY=${{ secrets.API_KEY }} \ + --build-arg SENTRY_RELEASE=${{ steps.image.outputs.release }} \ -t ${{ steps.image.outputs.ref }} . - name: Get AKS credentials @@ -66,12 +71,9 @@ jobs: --overwrite-existing - name: Sync secrets to AKS - env: - API_KEY: ${{ secrets.API_KEY }} - GA_TRACKING_CODE: ${{ secrets.GA_TRACKING_CODE }} run: | kubectl create secret generic core-frontend-dashboard-secrets \ - --namespace core-frontend \ + --namespace ${{ env.NAMESPACE }} \ --from-literal=SENTRY_DSN="${{ env.SENTRY_DSN }}" \ --from-literal=SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}" \ --from-literal=GA_TRACKING_CODE="${{ secrets.GA_TRACKING_CODE }}" \ @@ -92,4 +94,61 @@ jobs: run: | kubectl rollout status deployment/${{ env.DEPLOYMENT_NAME }} \ --namespace ${{ env.NAMESPACE }} \ - --timeout=300s \ No newline at end of file + --timeout=300s + + upload-sentry-sourcemaps: + name: Upload Sentry sourcemaps (prod) + needs: [build-and-deploy] + runs-on: ubuntu-latest + environment: azure + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ needs.build-and-deploy.outputs.sha }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Configure npm auth for @oacore + run: | + printf "@oacore:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=%s\n" "${{ secrets.NPM_TOKEN }}" > .npmrc + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Remove npm auth + if: always() + run: rm -f .npmrc + + - name: Build web app + run: pnpm run build + env: + NODE_OPTIONS: --max-old-space-size=4096 + SENTRY_DSN: ${{ env.SENTRY_DSN }} + SENTRY_RELEASE: ${{ needs.build-and-deploy.outputs.release }} + GA_TRACKING_CODE: ${{ secrets.GA_TRACKING_CODE }} + API_KEY: ${{ secrets.API_KEY }} + + - name: Create Sentry release + uses: getsentry/action-release@v3 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: oacore + SENTRY_PROJECT: dashboard-pk + with: + environment: production + release: ${{ needs.build-and-deploy.outputs.release }} + sourcemaps: ./dist/assets + url_prefix: '~/assets' From 3b0ef93dd00b25edd83227298af09c519db901ae Mon Sep 17 00:00:00 2001 From: beso tsiklauri Date: Tue, 16 Jun 2026 13:32:31 +0400 Subject: [PATCH 30/66] add staging --- .../{aks_deploy.yml => aks_deploy_prod.yml} | 8 +- .github/workflows/aks_deploy_stage.yml | 154 ++++++++++ .github/workflows/prod-stg-deploy.yml | 267 ------------------ kube/{ => preview}/preview.yml | 0 kube/{ => prod}/deployment.yaml | 0 kube/{ => prod}/httproute.yaml | 0 kube/{ => prod}/service.yaml | 0 kube/stage/deployment.yaml | 46 +++ kube/stage/httproute.yaml | 19 ++ kube/stage/service.yaml | 16 ++ 10 files changed, 239 insertions(+), 271 deletions(-) rename .github/workflows/{aks_deploy.yml => aks_deploy_prod.yml} (95%) create mode 100644 .github/workflows/aks_deploy_stage.yml delete mode 100644 .github/workflows/prod-stg-deploy.yml rename kube/{ => preview}/preview.yml (100%) rename kube/{ => prod}/deployment.yaml (100%) rename kube/{ => prod}/httproute.yaml (100%) rename kube/{ => prod}/service.yaml (100%) create mode 100644 kube/stage/deployment.yaml create mode 100644 kube/stage/httproute.yaml create mode 100644 kube/stage/service.yaml diff --git a/.github/workflows/aks_deploy.yml b/.github/workflows/aks_deploy_prod.yml similarity index 95% rename from .github/workflows/aks_deploy.yml rename to .github/workflows/aks_deploy_prod.yml index 55638f32..f7e2d02a 100644 --- a/.github/workflows/aks_deploy.yml +++ b/.github/workflows/aks_deploy_prod.yml @@ -1,4 +1,4 @@ -name: AKS deployment - core-frontend-dashboard +name: AKS Prod on: push: @@ -82,13 +82,13 @@ jobs: - name: Apply Service and HTTPRoute run: | - kubectl apply -f kube/service.yaml - kubectl apply -f kube/httproute.yaml + kubectl apply -f kube/prod/service.yaml + kubectl apply -f kube/prod/httproute.yaml - name: Deploy to AKS run: | export IMAGE="${{ steps.image.outputs.ref }}" - envsubst < kube/deployment.yaml | kubectl apply -f - + envsubst < kube/prod/deployment.yaml | kubectl apply -f - - name: Wait for rollout run: | diff --git a/.github/workflows/aks_deploy_stage.yml b/.github/workflows/aks_deploy_stage.yml new file mode 100644 index 00000000..249854ca --- /dev/null +++ b/.github/workflows/aks_deploy_stage.yml @@ -0,0 +1,154 @@ +name: AKS Stage + +on: + push: + branches: [aks] + workflow_dispatch: + +env: + ACR_NAME: corecontainers + IMAGE_NAME: core-frontend-dashboard-stage + RESOURCE_GROUP: core-frontend + AKS_CLUSTER_NAME: core-frontend-kubernetes-cluster + NAMESPACE: core-frontend-stage + DEPLOYMENT_NAME: core-frontend-dashboard + SENTRY_DSN: https://d116b39bd427449799cdd1516f6f97a1@sentry.io/2091955 + +jobs: + build-and-deploy: + runs-on: ubuntu-24.04-arm + environment: azure + permissions: + id-token: write + contents: read + outputs: + release: ${{ steps.image.outputs.release }} + sha: ${{ github.sha }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set image reference + id: image + run: | + TAG=$(echo ${{ github.sha }} | cut -c1-7) + echo "ref=${{ env.ACR_NAME }}.azurecr.io/aks-${{ env.IMAGE_NAME }}:$TAG" >> $GITHUB_OUTPUT + echo "release=$TAG" >> $GITHUB_OUTPUT + + - name: Azure Login + uses: azure/login@v2 + with: + client-id: ${{ secrets.AZURE_CLIENT_ID }} + tenant-id: ${{ secrets.AZURE_TENANT_ID }} + subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} + + - name: Docker ACR Login + run: az acr login --name ${{ env.ACR_NAME }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Docker build and push + run: | + docker buildx build \ + --platform linux/arm64 \ + --push \ + --build-arg SENTRY_DSN=${{ env.SENTRY_DSN }} \ + --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ + --build-arg GA_TRACKING_CODE=${{ secrets.GA_TRACKING_CODE }} \ + --build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \ + --build-arg API_KEY=${{ secrets.API_KEY }} \ + --build-arg SENTRY_RELEASE=${{ steps.image.outputs.release }} \ + -t ${{ steps.image.outputs.ref }} . + + - name: Get AKS credentials + run: | + az aks get-credentials \ + --resource-group ${{ env.RESOURCE_GROUP }} \ + --name ${{ env.AKS_CLUSTER_NAME }} \ + --admin \ + --overwrite-existing + + - name: Sync secrets to AKS + run: | + kubectl create secret generic core-frontend-dashboard-secrets \ + --namespace ${{ env.NAMESPACE }} \ + --from-literal=SENTRY_DSN="${{ env.SENTRY_DSN }}" \ + --from-literal=SENTRY_AUTH_TOKEN="${{ secrets.SENTRY_AUTH_TOKEN }}" \ + --from-literal=GA_TRACKING_CODE="${{ secrets.GA_TRACKING_CODE }}" \ + --from-literal=API_KEY="${{ secrets.API_KEY }}" \ + --dry-run=client -o yaml | kubectl apply -f - + + - name: Apply Service and HTTPRoute + run: | + kubectl apply -f kube/stage/service.yaml + kubectl apply -f kube/stage/httproute.yaml + + - name: Deploy to AKS + run: | + export IMAGE="${{ steps.image.outputs.ref }}" + envsubst < kube/stage/deployment.yaml | kubectl apply -f - + + - name: Wait for rollout + run: | + kubectl rollout status deployment/${{ env.DEPLOYMENT_NAME }} \ + --namespace ${{ env.NAMESPACE }} \ + --timeout=300s + + upload-sentry-sourcemaps: + name: Upload Sentry sourcemaps (stage) + needs: [build-and-deploy] + runs-on: ubuntu-latest + environment: azure + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + ref: ${{ needs.build-and-deploy.outputs.sha }} + + - name: Setup pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 24 + cache: pnpm + + - name: Configure npm auth for @oacore + run: | + printf "@oacore:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=%s\n" "${{ secrets.NPM_TOKEN }}" > .npmrc + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Remove npm auth + if: always() + run: rm -f .npmrc + + - name: Build web app + run: pnpm run build + env: + NODE_OPTIONS: --max-old-space-size=4096 + SENTRY_DSN: ${{ env.SENTRY_DSN }} + SENTRY_RELEASE: ${{ needs.build-and-deploy.outputs.release }} + GA_TRACKING_CODE: ${{ secrets.GA_TRACKING_CODE }} + API_KEY: ${{ secrets.API_KEY }} + + - name: Create Sentry release + uses: getsentry/action-release@v3 + env: + SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} + SENTRY_ORG: oacore + SENTRY_PROJECT: dashboard-pk + with: + environment: production + release: ${{ needs.build-and-deploy.outputs.release }} + sourcemaps: ./dist/assets + url_prefix: '~/assets' diff --git a/.github/workflows/prod-stg-deploy.yml b/.github/workflows/prod-stg-deploy.yml deleted file mode 100644 index 55baf3f7..00000000 --- a/.github/workflows/prod-stg-deploy.yml +++ /dev/null @@ -1,267 +0,0 @@ -name: Container App Prod/Staging Deploy - -on: - push: - branches: - - main - - schedule: - - cron: "0 1 * * *" - - workflow_dispatch: - inputs: - target_env: - description: "Where to deploy" - required: true - type: choice - options: - - staging - - prod - default: staging - branch: - description: "Branch to build (default = main)" - required: true - default: main - -env: - ACR_NAME: corecontainers - IMAGE_NAME: core-frontend-dashboard - - RESOURCE_GROUP: core-frontend - PROD_APP_NAME: core-frontend-dashboard - - STAGING_RESOURCE_GROUP: core-frontend-stage - STAGING_APP_NAME: core-frontend-stage-dashboard - - SENTRY_DSN: https://d116b39bd427449799cdd1516f6f97a1@sentry.io/2091955 - PORT: 8080 - - ALLOWED_PROD_BRANCH: main - -permissions: - id-token: write - contents: read - -jobs: - build-and-push: - runs-on: ubuntu-latest - environment: azure - outputs: - tag: ${{ steps.vars.outputs.tag }} - deploy_env: ${{ steps.vars.outputs.deploy_env }} - app_name: ${{ steps.vars.outputs.app_name }} - ref_to_build: ${{ steps.vars.outputs.ref_to_build }} - branch_tag: ${{ steps.vars.outputs.branch_tag }} - image_tag: ${{ steps.vars.outputs.image_tag }} - - steps: - - name: Compute variables - id: vars - shell: bash - run: | - SHA7="$(echo "${GITHUB_SHA}" | cut -c1-7)" - - # ✅ ADDED: schedule always deploys PROD from the frozen branch - if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then - DEPLOY_ENV="prod" - REF_TO_BUILD="${{ env.ALLOWED_PROD_BRANCH }}" - - elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - DEPLOY_ENV="${{ inputs.target_env }}" - REF_TO_BUILD="${{ inputs.branch }}" - - # existing: Freeze PROD to only one branch; fail if any other branch is selected for PROD - if [[ "${DEPLOY_ENV}" == "prod" && "${REF_TO_BUILD}" != "${{ env.ALLOWED_PROD_BRANCH }}" ]]; then - echo "::error title=Blocked prod deployment::Prod deployments are only allowed from '${{ env.ALLOWED_PROD_BRANCH }}'. You selected '${REF_TO_BUILD}'." - exit 1 - fi - else - DEPLOY_ENV="staging" - REF_TO_BUILD="${GITHUB_REF_NAME}" - fi - - BRANCH_TAG="$(echo "${REF_TO_BUILD}" | tr '[:upper:]' '[:lower:]' \ - | sed -E 's#[^a-z0-9_.-]+#-#g' | sed -E 's#^-+##; s#-+$##' | cut -c1-40)" - - if [[ "${DEPLOY_ENV}" == "prod" ]]; then - APP_NAME="${{ env.PROD_APP_NAME }}" - IMAGE_TAG="prod" - else - APP_NAME="${{ env.STAGING_APP_NAME }}" - IMAGE_TAG="stg-${BRANCH_TAG}" - fi - - echo "tag=${SHA7}" >> "$GITHUB_OUTPUT" - echo "deploy_env=${DEPLOY_ENV}" >> "$GITHUB_OUTPUT" - echo "app_name=${APP_NAME}" >> "$GITHUB_OUTPUT" - echo "ref_to_build=${REF_TO_BUILD}" >> "$GITHUB_OUTPUT" - echo "branch_tag=${BRANCH_TAG}" >> "$GITHUB_OUTPUT" - echo "image_tag=${IMAGE_TAG}" >> "$GITHUB_OUTPUT" - - - - name: Summary - run: | - { - echo "## Deploy plan" - echo "- **Trigger:** ${{ github.event_name }}" - echo "- **Env:** ${{ steps.vars.outputs.deploy_env }}" - echo "- **Branch:** ${{ steps.vars.outputs.ref_to_build }}" - echo "- **App:** ${{ steps.vars.outputs.app_name }}" - echo "- **ACR:** ${{ env.ACR_NAME }}" - echo "- **Image:** ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}" - echo "- **Tag (sha7):** ${{ steps.vars.outputs.tag }}" - echo "- **Tag (env):** ${{ steps.vars.outputs.image_tag }}" - } >> "$GITHUB_STEP_SUMMARY" - - - - name: Checkout code - uses: actions/checkout@v4 - with: - ref: ${{ steps.vars.outputs.ref_to_build }} - - - name: Azure Login - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Docker ACR Login - run: az acr login --name ${{ env.ACR_NAME }} - - - name: Docker Build - shell: bash - run: | - if [[ "${{ steps.vars.outputs.deploy_env }}" == "staging" ]]; then - NODE_ENV=development - else - NODE_ENV=production - fi - - docker build \ - --build-arg NODE_ENV=$NODE_ENV \ - --build-arg SENTRY_DSN=${{env.SENTRY_DSN}} \ - --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ - --build-arg GA_TRACKING_CODE=${{secrets.GA_TRACKING_CODE}} \ - --build-arg NPM_TOKEN=${{ secrets.NPM_TOKEN }} \ - --build-arg API_KEY=${{ secrets.API_KEY }} \ - -t ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }} \ - -t ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.image_tag }} . - - - name: Docker Push - run: | - docker push ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.tag }} - docker push ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ steps.vars.outputs.image_tag }} - - deploy: - needs: build-and-push - runs-on: ubuntu-latest - environment: azure - env: - TAG: ${{ needs.build-and-push.outputs.tag }} - IMAGE_TAG: ${{ needs.build-and-push.outputs.image_tag }} - - steps: - - name: Azure Login - uses: azure/login@v2 - with: - client-id: ${{ secrets.AZURE_CLIENT_ID }} - tenant-id: ${{ secrets.AZURE_TENANT_ID }} - subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }} - - - name: Resolve deploy target - id: target - shell: bash - run: | - if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then - ENV="prod" - elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - ENV="${{ inputs.target_env }}" - else - ENV="staging" - fi - - if [[ "$ENV" == "prod" ]]; then - echo "APP=${{ env.PROD_APP_NAME }}" >> $GITHUB_OUTPUT - echo "RG=${{ env.RESOURCE_GROUP }}" >> $GITHUB_OUTPUT - else - echo "APP=${{ env.STAGING_APP_NAME }}" >> $GITHUB_OUTPUT - echo "RG=${{ env.STAGING_RESOURCE_GROUP }}" >> $GITHUB_OUTPUT - fi - - - name: Enforce prod branch freeze - shell: bash - run: | - if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then - if [[ "${{ inputs.target_env }}" == "prod" && "${{ needs.build-and-push.outputs.ref_to_build }}" != "${{ env.ALLOWED_PROD_BRANCH }}" ]]; then - echo "::error title=Blocked prod deployment::Prod deployments are only allowed from '${{ env.ALLOWED_PROD_BRANCH }}'." - exit 1 - fi - fi - - - name: Deploy to Azure Container App - shell: bash - run: | - REDEPLOY_AT="${GITHUB_RUN_ID}-${GITHUB_RUN_ATTEMPT}-$(date -u +%Y%m%d%H%M%S)" - - az containerapp update \ - --name ${{ steps.target.outputs.APP }} \ - --resource-group ${{ steps.target.outputs.RG }} \ - --image ${{ env.ACR_NAME }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ env.TAG }} \ - --container-name ${{ steps.target.outputs.APP }} \ - --set configuration.ingress.targetPort=${{ env.PORT }} \ - --set-env-vars REDEPLOY_AT="$REDEPLOY_AT" - - upload-sentry-sourcemaps: - name: Upload Sentry sourcemaps (prod) - needs: [deploy] - runs-on: ubuntu-latest - environment: azure - permissions: - id-token: write - contents: read - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - ref: ${{ needs.build-and-push.outputs.ref_to_build }} - - - name: Setup pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 24 - cache: pnpm - - - name: Configure npm auth for @oacore - run: | - printf "@oacore:registry=https://npm.pkg.github.com\n//npm.pkg.github.com/:_authToken=%s\n" "${{ secrets.NPM_TOKEN }}" > .npmrc - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Remove npm auth - if: always() - run: rm -f .npmrc - - - name: Build web app - run: pnpm run build - env: - NODE_OPTIONS: --max-old-space-size=4096 - - - name: Create Sentry release - uses: getsentry/action-release@v3 - env: - SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }} - SENTRY_ORG: oacore - SENTRY_PROJECT: dashboard-pk - with: - environment: production - sourcemaps: ./dist/assets - url_prefix: '~/assets' diff --git a/kube/preview.yml b/kube/preview/preview.yml similarity index 100% rename from kube/preview.yml rename to kube/preview/preview.yml diff --git a/kube/deployment.yaml b/kube/prod/deployment.yaml similarity index 100% rename from kube/deployment.yaml rename to kube/prod/deployment.yaml diff --git a/kube/httproute.yaml b/kube/prod/httproute.yaml similarity index 100% rename from kube/httproute.yaml rename to kube/prod/httproute.yaml diff --git a/kube/service.yaml b/kube/prod/service.yaml similarity index 100% rename from kube/service.yaml rename to kube/prod/service.yaml diff --git a/kube/stage/deployment.yaml b/kube/stage/deployment.yaml new file mode 100644 index 00000000..23d94399 --- /dev/null +++ b/kube/stage/deployment.yaml @@ -0,0 +1,46 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: core-frontend-dashboard + namespace: core-frontend-stage + labels: + app: core-frontend-dashboard +spec: + replicas: 3 + revisionHistoryLimit: 5 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + maxUnavailable: 0 + selector: + matchLabels: + app: core-frontend-dashboard + template: + metadata: + labels: + app: core-frontend-dashboard + spec: + nodeSelector: + agentpool: userpool + containers: + - name: core-frontend-dashboard + image: ${IMAGE} + env: + - name: NODE_ENV + value: production + imagePullPolicy: IfNotPresent + envFrom: + - secretRef: + name: core-frontend-dashboard-secrets + ports: + - name: http + containerPort: 8080 + protocol: TCP + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 1000m + memory: 1000Mi diff --git a/kube/stage/httproute.yaml b/kube/stage/httproute.yaml new file mode 100644 index 00000000..28f6a948 --- /dev/null +++ b/kube/stage/httproute.yaml @@ -0,0 +1,19 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: core-frontend-dashboard + namespace: core-frontend-stage +spec: + parentRefs: + - name: public-gateway + namespace: gateway-system + hostnames: + - dashboard-staging.core.ac.uk + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: core-frontend-dashboard + port: 80 diff --git a/kube/stage/service.yaml b/kube/stage/service.yaml new file mode 100644 index 00000000..92b3da72 --- /dev/null +++ b/kube/stage/service.yaml @@ -0,0 +1,16 @@ +apiVersion: v1 +kind: Service +metadata: + name: core-frontend-dashboard + namespace: core-frontend-stage + labels: + app: core-frontend-dashboard +spec: + type: ClusterIP + selector: + app: core-frontend-dashboard + ports: + - name: http + port: 80 + targetPort: http + protocol: TCP \ No newline at end of file From 15a7b1e9ad9bceca0479e21278a3941f1566463f Mon Sep 17 00:00:00 2001 From: fropa Date: Tue, 16 Jun 2026 13:33:10 +0400 Subject: [PATCH 31/66] Change branch trigger from 'aks' to 'stage' --- .github/workflows/aks_deploy_stage.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/aks_deploy_stage.yml b/.github/workflows/aks_deploy_stage.yml index 249854ca..112f80c7 100644 --- a/.github/workflows/aks_deploy_stage.yml +++ b/.github/workflows/aks_deploy_stage.yml @@ -2,7 +2,7 @@ name: AKS Stage on: push: - branches: [aks] + branches: [stage] workflow_dispatch: env: From 29f484fa24d56112eb51432c8cd518194960ddfe Mon Sep 17 00:00:00 2001 From: beso tsiklauri Date: Wed, 17 Jun 2026 01:30:18 +0400 Subject: [PATCH 32/66] set stg api on stg --- .env.staging | 7 +++++++ package.json | 2 ++ 2 files changed, 9 insertions(+) create mode 100644 .env.staging diff --git a/.env.staging b/.env.staging new file mode 100644 index 00000000..f3a8cac1 --- /dev/null +++ b/.env.staging @@ -0,0 +1,7 @@ +# Staging – uses staging API (api-stg.core.ac.uk) +VITE_APP_NAME=CORE Dashboard (staging) +VITE_APP_API_BASE_URL=https://api-stg.core.ac.uk +VITE_API_URL=https://api-stg.core.ac.uk +VITE_IDP_URL=https://api-stg.core.ac.uk +VITE_SENTRY_DSN=https://4d292e22294f991799b49f3ffa238ff7@o318858.ingest.us.sentry.io/4511235416981504 +VITE_GA_TRACKING_CODE= diff --git a/package.json b/package.json index bc5ee066..9661afe2 100644 --- a/package.json +++ b/package.json @@ -16,8 +16,10 @@ "build:core-ui": "cd ../core-ui && pnpm run build:lib", "dev": "vite --mode development", "dev:local": "vite --mode local", + "dev:staging": "vite --mode staging", "build": "tsc -b && vite build --mode production", "build:dev": "tsc -b && vite build --mode development", + "build:staging": "tsc -b && vite build --mode staging", "docker:build": "sh scripts/docker-build.sh", "lint": "eslint . --format compact", "preview": "vite preview", From 1895bf1cb8aaa4ca877381fb49322c50ad7c4952 Mon Sep 17 00:00:00 2001 From: beso tsiklauri Date: Wed, 17 Jun 2026 16:01:25 +0400 Subject: [PATCH 33/66] add stg api for stage env --- .github/workflows/aks_deploy_prod.yml | 11 +++++++---- .github/workflows/aks_deploy_stage.yml | 11 +++++++---- Dockerfile | 9 ++++++++- package.json | 1 + 4 files changed, 23 insertions(+), 9 deletions(-) diff --git a/.github/workflows/aks_deploy_prod.yml b/.github/workflows/aks_deploy_prod.yml index f7e2d02a..9d48ed1d 100644 --- a/.github/workflows/aks_deploy_prod.yml +++ b/.github/workflows/aks_deploy_prod.yml @@ -12,6 +12,8 @@ env: AKS_CLUSTER_NAME: core-frontend-kubernetes-cluster NAMESPACE: core-frontend DEPLOYMENT_NAME: core-frontend-dashboard + APP_ENV: production + KUBE_DIR: kube/prod SENTRY_DSN: https://d116b39bd427449799cdd1516f6f97a1@sentry.io/2091955 jobs: @@ -54,6 +56,7 @@ jobs: docker buildx build \ --platform linux/arm64 \ --push \ + --build-arg APP_ENV=${{ env.APP_ENV }} \ --build-arg SENTRY_DSN=${{ env.SENTRY_DSN }} \ --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ --build-arg GA_TRACKING_CODE=${{ secrets.GA_TRACKING_CODE }} \ @@ -82,13 +85,13 @@ jobs: - name: Apply Service and HTTPRoute run: | - kubectl apply -f kube/prod/service.yaml - kubectl apply -f kube/prod/httproute.yaml + kubectl apply -f ${{ env.KUBE_DIR }}/service.yaml + kubectl apply -f ${{ env.KUBE_DIR }}/httproute.yaml - name: Deploy to AKS run: | export IMAGE="${{ steps.image.outputs.ref }}" - envsubst < kube/prod/deployment.yaml | kubectl apply -f - + envsubst < ${{ env.KUBE_DIR }}/deployment.yaml | kubectl apply -f - - name: Wait for rollout run: | @@ -133,7 +136,7 @@ jobs: run: rm -f .npmrc - name: Build web app - run: pnpm run build + run: pnpm run build:${{ env.APP_ENV }} env: NODE_OPTIONS: --max-old-space-size=4096 SENTRY_DSN: ${{ env.SENTRY_DSN }} diff --git a/.github/workflows/aks_deploy_stage.yml b/.github/workflows/aks_deploy_stage.yml index 112f80c7..0e9f7c47 100644 --- a/.github/workflows/aks_deploy_stage.yml +++ b/.github/workflows/aks_deploy_stage.yml @@ -12,6 +12,8 @@ env: AKS_CLUSTER_NAME: core-frontend-kubernetes-cluster NAMESPACE: core-frontend-stage DEPLOYMENT_NAME: core-frontend-dashboard + APP_ENV: staging + KUBE_DIR: kube/stage SENTRY_DSN: https://d116b39bd427449799cdd1516f6f97a1@sentry.io/2091955 jobs: @@ -54,6 +56,7 @@ jobs: docker buildx build \ --platform linux/arm64 \ --push \ + --build-arg APP_ENV=${{ env.APP_ENV }} \ --build-arg SENTRY_DSN=${{ env.SENTRY_DSN }} \ --build-arg SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }} \ --build-arg GA_TRACKING_CODE=${{ secrets.GA_TRACKING_CODE }} \ @@ -82,13 +85,13 @@ jobs: - name: Apply Service and HTTPRoute run: | - kubectl apply -f kube/stage/service.yaml - kubectl apply -f kube/stage/httproute.yaml + kubectl apply -f ${{ env.KUBE_DIR }}/service.yaml + kubectl apply -f ${{ env.KUBE_DIR }}/httproute.yaml - name: Deploy to AKS run: | export IMAGE="${{ steps.image.outputs.ref }}" - envsubst < kube/stage/deployment.yaml | kubectl apply -f - + envsubst < ${{ env.KUBE_DIR }}/deployment.yaml | kubectl apply -f - - name: Wait for rollout run: | @@ -133,7 +136,7 @@ jobs: run: rm -f .npmrc - name: Build web app - run: pnpm run build + run: pnpm run build:${{ env.APP_ENV }} env: NODE_OPTIONS: --max-old-space-size=4096 SENTRY_DSN: ${{ env.SENTRY_DSN }} diff --git a/Dockerfile b/Dockerfile index 32b7c9dd..1a559cd7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,7 @@ FROM node:24-alpine AS builder ARG NODE_ENV=production +ARG APP_ENV=production ARG BUILD_TARGET=azure ARG NPM_TOKEN ARG SENTRY_DSN="" @@ -10,6 +11,7 @@ ARG GA_TRACKING_CODE="" # VITE_API_URL, VITE_IDP_URL come from committed .env.development / .env.production # SENTRY_AUTH_TOKEN: build-only; enables Vite sourcemaps + Sentry upload (see vite.config.ts) ENV NODE_ENV=${NODE_ENV} \ + APP_ENV=${APP_ENV} \ BUILD_TARGET=${BUILD_TARGET} \ NPM_TOKEN=${NPM_TOKEN} \ SENTRY_AUTH_TOKEN=${SENTRY_AUTH_TOKEN} \ @@ -34,7 +36,12 @@ RUN set -eux; \ COPY . . -RUN if [ "$NODE_ENV" = "production" ]; then pnpm run build; else pnpm run build:dev; fi +RUN case "$APP_ENV" in \ + staging) pnpm run build:staging ;; \ + development) pnpm run build:dev ;; \ + production) pnpm run build ;; \ + *) pnpm run build ;; \ + esac FROM nginx:1.27-alpine AS runtime diff --git a/package.json b/package.json index 9661afe2..443e334e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dev:local": "vite --mode local", "dev:staging": "vite --mode staging", "build": "tsc -b && vite build --mode production", + "build:production": "tsc -b && vite build --mode production", "build:dev": "tsc -b && vite build --mode development", "build:staging": "tsc -b && vite build --mode staging", "docker:build": "sh scripts/docker-build.sh", From 8eb46c3f2c2bca5bddf6e95c6895eb083d86921d Mon Sep 17 00:00:00 2001 From: fropa Date: Wed, 17 Jun 2026 17:10:42 +0400 Subject: [PATCH 34/66] Update hostname for staging environment --- kube/stage/httproute.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kube/stage/httproute.yaml b/kube/stage/httproute.yaml index 28f6a948..7f54f6d1 100644 --- a/kube/stage/httproute.yaml +++ b/kube/stage/httproute.yaml @@ -8,7 +8,7 @@ spec: - name: public-gateway namespace: gateway-system hostnames: - - dashboard-staging.core.ac.uk + - dashboard-stg.core.ac.uk rules: - matches: - path: From a06bcb652524cedbe1bb07010509a0e7486191d1 Mon Sep 17 00:00:00 2001 From: fropa Date: Wed, 17 Jun 2026 18:18:26 +0400 Subject: [PATCH 35/66] Change replica count from 3 to 1 in deployment.yaml --- kube/stage/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kube/stage/deployment.yaml b/kube/stage/deployment.yaml index 23d94399..892381b6 100644 --- a/kube/stage/deployment.yaml +++ b/kube/stage/deployment.yaml @@ -6,7 +6,7 @@ metadata: labels: app: core-frontend-dashboard spec: - replicas: 3 + replicas: 1 revisionHistoryLimit: 5 strategy: type: RollingUpdate From f76d71ec187ac731d311c46f9b9ab1879318ca61 Mon Sep 17 00:00:00 2001 From: beso tsiklauri Date: Thu, 18 Jun 2026 10:44:55 +0400 Subject: [PATCH 36/66] stage: 1 replica + forward APP_ENV in docker-build script --- scripts/docker-build.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/docker-build.sh b/scripts/docker-build.sh index d8d2882e..68dabd86 100755 --- a/scripts/docker-build.sh +++ b/scripts/docker-build.sh @@ -13,6 +13,7 @@ fi # URLs come from committed .env.development / .env.production exec docker build \ --build-arg NODE_ENV="${NODE_ENV:-production}" \ + --build-arg APP_ENV="${APP_ENV:-${NODE_ENV:-production}}" \ --build-arg BUILD_TARGET="${BUILD_TARGET:-azure}" \ --build-arg SENTRY_DSN="$SENTRY_DSN" \ --build-arg SENTRY_AUTH_TOKEN="$SENTRY_AUTH_TOKEN" \ From 6758e81e0a3f09a524caf3ea0ec2146464c02272 Mon Sep 17 00:00:00 2001 From: Valerii Budko Date: Tue, 30 Jun 2026 20:22:34 +0100 Subject: [PATCH 37/66] Set gitignore config npm (#789) From a04b1673e70a87f3da7d1a8b2258385727641092 Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Mon, 13 Jul 2026 17:38:10 +0400 Subject: [PATCH 38/66] CORE: certificate view img --- src/assets/img/fairCertificateCoreLogo.svg | 8 + src/assets/img/fairCertificateSeal.svg | 43 +++ .../Fair/components/FairCertificateView.tsx | 106 ++++++ .../Fair/components/FairDocHeader.tsx | 6 +- src/features/Fair/styles.css | 331 +++++++++++++++++- src/features/Fair/texts/fair.json | 2 +- 6 files changed, 488 insertions(+), 8 deletions(-) create mode 100644 src/assets/img/fairCertificateCoreLogo.svg create mode 100644 src/assets/img/fairCertificateSeal.svg create mode 100644 src/features/Fair/components/FairCertificateView.tsx diff --git a/src/assets/img/fairCertificateCoreLogo.svg b/src/assets/img/fairCertificateCoreLogo.svg new file mode 100644 index 00000000..f997c125 --- /dev/null +++ b/src/assets/img/fairCertificateCoreLogo.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/assets/img/fairCertificateSeal.svg b/src/assets/img/fairCertificateSeal.svg new file mode 100644 index 00000000..ddfe1c6f --- /dev/null +++ b/src/assets/img/fairCertificateSeal.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/features/Fair/components/FairCertificateView.tsx b/src/features/Fair/components/FairCertificateView.tsx new file mode 100644 index 00000000..9f1711ea --- /dev/null +++ b/src/features/Fair/components/FairCertificateView.tsx @@ -0,0 +1,106 @@ +import coreLogo from '@/assets/img/fairCertificateCoreLogo.svg'; +import certificateSeal from '@/assets/img/fairCertificateSeal.svg'; + +export type FairCertificateData = { + title: string; + level: string; + presentedLabel: string; + repositoryName: string; + description: string; + signatoryName: string; + signatoryTitle: string; + infoUrl: string; + issueDateLabel: string; + issueDate: string; + validUntilLabel: string; + validUntil: string; +}; + +export type FairCertificateViewProps = { + data?: FairCertificateData; +}; + +const DUMMY_CERTIFICATE_DATA: FairCertificateData = { + title: 'CERTIFICATE', + level: 'BRONZE', + presentedLabel: 'PROUDLY PRESENTED TO :', + repositoryName: 'Open Research Online', + description: + "In recognition of the repository's demonstrated commitment to best practice data stewardship, attested by its compliance with the CORE FAIR Certification requirements at the Bronze level. This certificate signifies that the records managed herein meet the globally recognized standards for Findability, Accessibility, Interoperability, and Reusability (FAIR), ensuring long-term utility and transparency for the Open Access community.", + signatoryName: 'Petr Knoth', + signatoryTitle: 'CEO of CORE', + infoUrl: 'For more information visit https://core.ac.uk/', + issueDateLabel: 'Issue Date:', + issueDate: '12/03/2023', + validUntilLabel: 'Valid until:', + validUntil: '12/03/2024', +}; + +export const FairCertificateView = ({ + data = DUMMY_CERTIFICATE_DATA, +}: FairCertificateViewProps) => { + return ( +
+
+
+
+ ); +}; diff --git a/src/features/Fair/components/FairDocHeader.tsx b/src/features/Fair/components/FairDocHeader.tsx index 02d58380..8fcfe184 100644 --- a/src/features/Fair/components/FairDocHeader.tsx +++ b/src/features/Fair/components/FairDocHeader.tsx @@ -1,7 +1,7 @@ import fairTexts from '@features/Fair/texts/fair.json'; import {Markdown} from '@oacore/core-ui'; import {Button} from 'antd'; -import placeholder from '@/assets/img/certificatePlaceholder.svg'; +import { FairCertificateView } from '@features/Fair/components/FairCertificateView.tsx'; import '../styles.css'; export const FairDocHeader = () => { @@ -36,9 +36,7 @@ export const FairDocHeader = () => { {approvedView.submissionsLine}
-
- -
+
); diff --git a/src/features/Fair/styles.css b/src/features/Fair/styles.css index 19e11d5b..8da0d4d7 100644 --- a/src/features/Fair/styles.css +++ b/src/features/Fair/styles.css @@ -485,10 +485,9 @@ } .fair-certification-header-wrapper { - display: flex; - justify-content: space-between; - align-items: center; gap: 50px; + display: grid; + grid-template-columns: 2fr 1fr; } .fair-certification-header-inner-wrapper { @@ -511,6 +510,332 @@ color: #666; } +/* FAIR certification certificate card */ +.fair-certificate { + flex-shrink: 0; + width: 100%; + max-width: 520px; +} + +.fair-certificate__card { + position: relative; + box-sizing: border-box; + width: 100%; + padding: 36px 24px 24px 24px; + background: #FAFAFA; + box-shadow: 0 2.043px 3.404px 0 rgba(0, 0, 0, 0.14), 0 0.34px 6.128px 0 rgba(0, 0, 0, 0.12), 0 1.021px 1.702px 0 rgba(0, 0, 0, 0.20); +} + +.fair-certificate__content { + position: relative; + z-index: 1; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; +} + +.fair-certificate__header { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + margin-bottom: 6px; +} + +.fair-certificate__title { + margin: 0; + color: var(--color-primary); + font-size: 14px; + font-style: normal; + font-weight: 700; + line-height: 1.1; + letter-spacing: 0.08em; + text-transform: uppercase; +} + +.fair-certificate__level { + margin: 0; + font-style: normal; + text-transform: uppercase; + color: #000; + text-align: center; + font-size: 6px; + font-weight: 700; + line-height: 140%; /* 7.512px */ + letter-spacing: 0.537px; + border-bottom: 1px solid var(--color-primary); + padding: 4px 0; +} + +.fair-certificate__presented-label { + margin: 4px 0 0; + color: var(--color-primary); + font-size: 7px; + font-style: normal; + font-weight: 300; + line-height: 1.3; + letter-spacing: 0.1em; + text-transform: uppercase; +} + +.fair-certificate__recipient { + margin: 0 0 8px; + font-style: normal; + letter-spacing: 0.01em; + color: #000; + text-align: center; + font-size: 16px; + font-weight: 600; + line-height: 130%; +} + +.fair-certificate__description { + margin: 0 0 12px; + width: 200px; + letter-spacing: 0.01em; + color: #000; + text-align: center; + font-size: 4px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 5.05px */ +} + +.fair-certificate__footer { + display: grid; + grid-template-columns: 1fr 1fr 1fr; + align-items: end; + gap: 8px; + width: 100%; +} + +.fair-certificate__signatory { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 1px; + text-align: left; +} + +.fair-certificate__footer-name { + margin: 0; + color: var(--color-primary); + font-size: 6px; + font-style: normal; + font-weight: 700; + line-height: 140%; /* 7.512px */ + letter-spacing: 0.268px; + padding: 2px 0; + border-bottom: 1px solid var(--color-primary); +} + +.fair-certificate__footer-title { + font-size: 4px; + font-style: normal; + font-weight: 400; + line-height: 140%; /* 5.302px */ + margin: 0 0 4px; + color: var(--color-primary); + margin-top: 2px; +} + +.fair-certificate__core-logo { + display: block; + width: 32px; + height: auto; +} + +.fair-certificate__seal-block { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; +} + +.fair-certificate__seal { + display: block; + width: 52px; + height: auto; +} + +.fair-certificate__info-url { + margin: 0; + color: #666; + text-align: center; + font-size: 4px; + font-style: normal; + font-weight: 400; + line-height: 160%; /* 5.05px */ + margin-top: 10px; +} + +.fair-certificate__dates { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + text-align: right; +} + +.fair-certificate__issue-date { + display: flex; + flex-direction: column; + text-align: right; + margin: 0; + font-size: 6.5px; + font-style: normal; + line-height: 1.3; +} + + +.fair-certificate__valid-until { + color: #212121; + text-align: right; + font-size: 3.787px; + font-style: normal; + font-weight: 400; + line-height: 140%; /* 5.302px */ +} + +/* Corner ornaments */ +.fair-certificate__frame { + position: absolute; + width: 14px; + height: 14px; + pointer-events: none; +} + +.fair-certificate__frame::before, +.fair-certificate__frame::after { + content: ''; + position: absolute; + background: #212121; +} + +.fair-certificate__frame--top-left { + top: 12px; + left: 12px; +} + +.fair-certificate__frame--top-left::before { + top: 0; + left: 0; + width: 14px; + height: 1px; +} + +.fair-certificate__frame--top-left::after { + top: 0; + left: 0; + width: 1px; + height: 14px; +} + +.fair-certificate__frame--top-right { + top: 12px; + right: 12px; +} + +.fair-certificate__frame--top-right::before { + top: 0; + right: 0; + width: 14px; + height: 1px; +} + +.fair-certificate__frame--top-right::after { + top: 0; + right: 0; + width: 1px; + height: 14px; +} + +.fair-certificate__frame--bottom-left { + bottom: 12px; + left: 12px; +} + +.fair-certificate__frame--bottom-left::before { + bottom: 0; + left: 0; + width: 14px; + height: 1px; +} + +.fair-certificate__frame--bottom-left::after { + bottom: 0; + left: 0; + width: 1px; + height: 14px; +} + +.fair-certificate__frame--bottom-right { + bottom: 12px; + right: 12px; +} + +.fair-certificate__frame--bottom-right::before { + bottom: 0; + right: 0; + width: 14px; + height: 1px; +} + +.fair-certificate__frame--bottom-right::after { + bottom: 0; + right: 0; + width: 1px; + height: 14px; +} + +/* Partial border edges */ +.fair-certificate__edge { + position: absolute; + background: #212121; + pointer-events: none; +} + +.fair-certificate__edge--top { + top: 12px; + left: 42px; + right: 42px; + height: 1px; +} + +.fair-certificate__edge--bottom { + bottom: 12px; + left: 42px; + right: 42px; + height: 1px; +} + +.fair-certificate__edge--left { + top: 42px; + bottom: 42px; + left: 12px; + width: 1px; +} + +.fair-certificate__edge--right { + top: 42px; + bottom: 42px; + right: 12px; + width: 1px; +} + +@media (max-width: 768px) { + .fair-certification-header-wrapper { + flex-direction: column; + align-items: flex-start; + } + + .fair-certificate { + align-self: center; + max-width: 100%; + } +} + /* Approved view — FAIR principles accordion (Ant Design Collapse) */ .fair-principles-accordion-section { box-sizing: border-box; diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json index 96487b7a..38cb9e33 100644 --- a/src/features/Fair/texts/fair.json +++ b/src/features/Fair/texts/fair.json @@ -327,4 +327,4 @@ ] } } -} \ No newline at end of file +} From bb5c6402a1db04268608f4fcf82c7e1baf2df46a Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Tue, 14 Jul 2026 14:02:00 +0400 Subject: [PATCH 39/66] CORE: certificate border fix --- src/assets/img/certificatePlaceholder.svg | 58 +++++------ .../Fair/components/FairCertificateView.tsx | 6 ++ .../Fair/components/FairDocHeader.tsx | 2 + src/features/Fair/styles.css | 95 +++++++++++++++---- 4 files changed, 116 insertions(+), 45 deletions(-) diff --git a/src/assets/img/certificatePlaceholder.svg b/src/assets/img/certificatePlaceholder.svg index 2c535a86..0510d80a 100644 --- a/src/assets/img/certificatePlaceholder.svg +++ b/src/assets/img/certificatePlaceholder.svg @@ -1,52 +1,52 @@ - - - - - - - - - + + + + + + + + + - - - - - - - - - - - - - + + + + + + + + + + + + + - + - + - + - - + + - - + + diff --git a/src/features/Fair/components/FairCertificateView.tsx b/src/features/Fair/components/FairCertificateView.tsx index 9f1711ea..1e9e928b 100644 --- a/src/features/Fair/components/FairCertificateView.tsx +++ b/src/features/Fair/components/FairCertificateView.tsx @@ -54,6 +54,12 @@ export const FairCertificateView = ({
diff --git a/src/features/Fair/styles.css b/src/features/Fair/styles.css index 8da0d4d7..503cab0b 100644 --- a/src/features/Fair/styles.css +++ b/src/features/Fair/styles.css @@ -510,6 +510,10 @@ color: #666; } +.fair-certification-placeholder { + width: 100%; +} + /* FAIR certification certificate card */ .fair-certificate { flex-shrink: 0; @@ -721,7 +725,7 @@ .fair-certificate__frame--top-left::before { top: 0; left: 0; - width: 14px; + width: 20px; height: 1px; } @@ -729,7 +733,7 @@ top: 0; left: 0; width: 1px; - height: 14px; + height: 20px; } .fair-certificate__frame--top-right { @@ -740,7 +744,7 @@ .fair-certificate__frame--top-right::before { top: 0; right: 0; - width: 14px; + width: 20px; height: 1px; } @@ -748,7 +752,7 @@ top: 0; right: 0; width: 1px; - height: 14px; + height: 20px; } .fair-certificate__frame--bottom-left { @@ -759,7 +763,7 @@ .fair-certificate__frame--bottom-left::before { bottom: 0; left: 0; - width: 14px; + width: 20px; height: 1px; } @@ -767,7 +771,7 @@ bottom: 0; left: 0; width: 1px; - height: 14px; + height: 20px; } .fair-certificate__frame--bottom-right { @@ -778,7 +782,7 @@ .fair-certificate__frame--bottom-right::before { bottom: 0; right: 0; - width: 14px; + width: 20px; height: 1px; } @@ -786,7 +790,7 @@ bottom: 0; right: 0; width: 1px; - height: 14px; + height: 20px; } /* Partial border edges */ @@ -798,32 +802,91 @@ .fair-certificate__edge--top { top: 12px; - left: 42px; - right: 42px; + left: 65px; + right: 65px; height: 1px; } .fair-certificate__edge--bottom { bottom: 12px; - left: 42px; - right: 42px; + left: 65px; + right: 65px; height: 1px; } .fair-certificate__edge--left { - top: 42px; - bottom: 42px; + top: 65px; + bottom: 65px; left: 12px; width: 1px; } .fair-certificate__edge--right { - top: 42px; - bottom: 42px; + top: 65px; + bottom: 65px; right: 12px; width: 1px; } +/* Inner corner ornaments + continuous border */ + +.fair-certificate__frame--inner { + width: 8px; + height: 8px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--top-left { + top: 17px; + left: 17px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--top-left::before { + width: 10px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--top-left::after { + height: 10px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--top-right { + top: 17px; + right: 17px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--top-right::before { + width: 10px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--top-right::after { + height: 10px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--bottom-left { + bottom: 17px; + left: 17px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--bottom-left::before { + width: 10px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--bottom-left::after { + height: 10px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--bottom-right { + bottom: 17px; + right: 17px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--bottom-right::before { + width: 10px; +} + +.fair-certificate__frame--inner.fair-certificate__frame--bottom-right::after { + height: 10px; +} + @media (max-width: 768px) { .fair-certification-header-wrapper { flex-direction: column; From ddae2f7c661d7708540cd10d833e323a16c19e0f Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Thu, 16 Jul 2026 15:07:38 +0400 Subject: [PATCH 40/66] CORE: V2 add api get request --- .../Fair/components/ApprovedFairView.tsx | 73 ++++++------ .../components/FairPrincipleQuestionBlock.tsx | 81 +++++--------- .../FairPrincipleSectionContent.tsx | 8 +- .../components/FairPrinciplesCollapse.tsx | 36 +++++- .../Fair/hooks/useFairCertification.ts | 27 +++++ src/features/Fair/styles.css | 14 +-- src/features/Fair/texts/fair.json | 96 ++++++++++------ .../Fair/types/fairCertification.types.ts | 57 ++++++++++ .../Fair/types/fairPrinciples.types.ts | 5 + .../Fair/utils/resolveFairQuestionStatus.ts | 104 ------------------ .../utils/resolveFairQuestionStatusLabel.ts | 26 +++++ src/pages/FAIRCertification.tsx | 14 ++- 12 files changed, 295 insertions(+), 246 deletions(-) create mode 100644 src/features/Fair/hooks/useFairCertification.ts create mode 100644 src/features/Fair/types/fairCertification.types.ts delete mode 100644 src/features/Fair/utils/resolveFairQuestionStatus.ts create mode 100644 src/features/Fair/utils/resolveFairQuestionStatusLabel.ts diff --git a/src/features/Fair/components/ApprovedFairView.tsx b/src/features/Fair/components/ApprovedFairView.tsx index 9cbd2717..2a66d7d5 100644 --- a/src/features/Fair/components/ApprovedFairView.tsx +++ b/src/features/Fair/components/ApprovedFairView.tsx @@ -4,51 +4,50 @@ import '../styles.css'; import { FairDocHeader } from '@features/Fair/components/FairDocHeader.tsx'; import { FairPrinciplesCollapse } from '@features/Fair/components/FairPrinciplesCollapse.tsx'; import { FairSubmissionProgress } from '@features/Fair/components/FairSubmissionProgress.tsx'; -import { useDataProviderStore } from '@/store/dataProviderStore'; -import { useRioxxStats } from '@features/Validator/hooks/useRioxxStats'; -import { useUsrnData } from '@features/Usrn/hooks/useUsrnData'; -import { useIrusStats } from '@/hooks/useIrusStats'; -import { useDataProviderStatistics } from '@/hooks/useDataProviderStatistics'; -import { useDoiStatistics } from '@features/Doi/hooks/useDoiStatistics'; -import { useOrcidStats } from '@features/Orcid/hooks/useOrcidData.ts'; -import { useDasData } from '@features/Das/hooks/useDasData'; -import type { FairRepositoryStatusParams } from '@features/Fair/utils/resolveFairQuestionStatus'; +import type { FairCertificationQuestion } from '@features/Fair/types/fairCertification.types'; -export const ApprovedFairView = () => { - const { selectedDataProvider, selectedSetSpec, statistics, doiStatistics } = useDataProviderStore(); - const rorId = selectedDataProvider?.rorData?.rorId ?? null; - const { rioxx } = useRioxxStats(selectedDataProvider?.id); - const { usrnData } = useUsrnData(selectedDataProvider?.id); - const { irus } = useIrusStats(selectedDataProvider?.id); - const { stats: orcidStats } = useOrcidStats(selectedDataProvider?.id ?? 0); - const { data: dasData } = useDasData(selectedDataProvider?.id ?? 0); +export type ApprovedFairViewProps = { + certificationQuestions?: FairCertificationQuestion[]; +}; + +export const ApprovedFairView = ({ certificationQuestions }: ApprovedFairViewProps) => { + // const { selectedDataProvider, selectedSetSpec, statistics, doiStatistics } = useDataProviderStore(); + // const rorId = selectedDataProvider?.rorData?.rorId ?? null; + // const { rioxx } = useRioxxStats(selectedDataProvider?.id); + // const { usrnData } = useUsrnData(selectedDataProvider?.id); + // const { irus } = useIrusStats(selectedDataProvider?.id); + // const { stats: orcidStats } = useOrcidStats(selectedDataProvider?.id ?? 0); + // const { data: dasData } = useDasData(selectedDataProvider?.id ?? 0); - useDataProviderStatistics(selectedDataProvider?.id ?? null, selectedSetSpec); - useDoiStatistics(selectedDataProvider?.id ?? null, selectedSetSpec); + // useDataProviderStatistics(selectedDataProvider?.id ?? null, selectedSetSpec); + // useDoiStatistics(selectedDataProvider?.id ?? null, selectedSetSpec); - const repositoryStatus: FairRepositoryStatusParams = { - rioxx: rioxx ?? undefined, - statistics: statistics ?? undefined, - internalStatistics: - statistics != null || doiStatistics != null - ? { - fullTextCount: statistics?.countFulltext, - metadataCount: statistics?.countMetadata, - doiCount: doiStatistics?.dataProviderDoiCount, - } - : undefined, - usrn: usrnData ?? null, - irus, - rorId, - orcidStats, - dasData, - }; + // const repositoryStatus: FairRepositoryStatusParams = { + // rioxx: rioxx ?? undefined, + // statistics: statistics ?? undefined, + // internalStatistics: + // statistics != null || doiStatistics != null + // ? { + // fullTextCount: statistics?.countFulltext, + // metadataCount: statistics?.countMetadata, + // doiCount: doiStatistics?.dataProviderDoiCount, + // } + // : undefined, + // usrn: usrnData ?? null, + // irus, + // rorId, + // orcidStats, + // dasData, + // }; return ( - + diff --git a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx index 966cfa0f..46868f5f 100644 --- a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx +++ b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx @@ -4,55 +4,32 @@ import {Form, Input} from 'antd'; import {formatNumber} from '@utils/helpers.ts'; import type {FairQuestionItem} from '@features/Fair/types/fairPrinciples.types'; -import {getFairQuestionStatusClassName} from '@features/Fair/utils/getFairQuestionStatusClassName'; -import { - resolveFairQuestionDisplay, - type FairRepositoryStatusParams, -} from '@features/Fair/utils/resolveFairQuestionStatus'; import '../../Usrn/style.css'; import '../styles.css'; +import { getFairQuestionStatusClassName } from '@features/Fair/utils/getFairQuestionStatusClassName'; +import { resolveFairQuestionStatusLabel } from '@features/Fair/utils/resolveFairQuestionStatusLabel'; export type FairPrincipleQuestionBlockProps = { item: FairQuestionItem; recommendationHeading: string; openQuestionLabel: string; - repositoryStatus?: FairRepositoryStatusParams | null; + // repositoryStatus?: FairRepositoryStatusParams | null; }; export const FairPrincipleQuestionBlock = ({ item, recommendationHeading, openQuestionLabel, - repositoryStatus, }: FairPrincipleQuestionBlockProps) => { - const { statusLabel, cardConfig } = resolveFairQuestionDisplay({ - item, - openQuestionLabel, - repositoryStatus, - }); - const displayStatus = statusLabel ?? '—'; - const statusClass = getFairQuestionStatusClassName(displayStatus); const isOpenQuestion = Boolean(item.openQuestion); - const countCovered = cardConfig?.countCovered ?? null; - const countTotal = cardConfig?.countTotal ?? null; - const countValue = cardConfig?.countValue ?? null; const percentLabelText = item.percentLabel; const counterLabelText = item.counterLabel; + const statusLabel = resolveFairQuestionStatusLabel(item.certificationQuestion, openQuestionLabel); + const statusClassName = getFairQuestionStatusClassName(statusLabel); - const showPercentBar = - countCovered != null && - countTotal != null && - countCovered > 0 && - countTotal > 0 && - Boolean(percentLabelText); - - const counterRows = cardConfig?.counterRows; - const showCounterRows = Boolean(counterRows?.length); - const showCountValue = - !showCounterRows && countValue != null && Boolean(counterLabelText); - + // TODO CHANGE TO REAL NUM return (
@@ -63,7 +40,7 @@ export const FairPrincipleQuestionBlock = ({ - {displayStatus} + {statusLabel}
{item.description ? (
@@ -79,34 +56,30 @@ export const FairPrincipleQuestionBlock = ({
) : null} - {showCounterRows && counterRows - ? counterRows.map((row, index) => ( -
- {row.label} - - {formatNumber(row.value)} - -
- )) - : null} - {showCountValue ? ( -
- {counterLabelText} - {formatNumber(countValue)} -
- ) : null} + {/*{showCounterRows && counterRows*/} + {/* ? counterRows.map((row, index) => (*/} + {/* */} + {/* {row.label}*/} + {/* */} + {/* {formatNumber(row.value)}*/} + {/* */} + {/*
*/} + {/* ))*/} + {/* : null}*/} + +
+ {counterLabelText} + {formatNumber(32)} +
- {showPercentBar && percentLabelText ? ( - ) : null} - {isOpenQuestion ? (
diff --git a/src/features/Fair/components/FairPrincipleSectionContent.tsx b/src/features/Fair/components/FairPrincipleSectionContent.tsx index 1e3d012d..f3ab860c 100644 --- a/src/features/Fair/components/FairPrincipleSectionContent.tsx +++ b/src/features/Fair/components/FairPrincipleSectionContent.tsx @@ -1,6 +1,6 @@ import {FairPrincipleQuestionBlock} from '@features/Fair/components/FairPrincipleQuestionBlock'; import type {FairPrincipleSection} from '@features/Fair/types/fairPrinciples.types'; -import type { FairRepositoryStatusParams } from '@features/Fair/utils/resolveFairQuestionStatus'; +// import type { FairRepositoryStatusParams } from '@features/Fair/utils/resolveFairQuestionStatus'; import '../styles.css'; @@ -8,14 +8,14 @@ export type FairPrincipleSectionContentProps = { section: FairPrincipleSection; recommendationHeading: string; openQuestionLabel: string; - repositoryStatus?: FairRepositoryStatusParams | null; + // repositoryStatus?: FairRepositoryStatusParams | null; }; export const FairPrincipleSectionContent = ({ section, recommendationHeading, openQuestionLabel, - repositoryStatus, + // repositoryStatus, }: FairPrincipleSectionContentProps) => { if (!section.items?.length) { return null; @@ -29,7 +29,7 @@ export const FairPrincipleSectionContent = ({ key={item.id ? item.id : `${item.code || 'row'}-${index}`} recommendationHeading={recommendationHeading} openQuestionLabel={openQuestionLabel} - repositoryStatus={repositoryStatus} + // repositoryStatus={repositoryStatus} /> ))}
diff --git a/src/features/Fair/components/FairPrinciplesCollapse.tsx b/src/features/Fair/components/FairPrinciplesCollapse.tsx index 38b0ce46..239b8b7f 100644 --- a/src/features/Fair/components/FairPrinciplesCollapse.tsx +++ b/src/features/Fair/components/FairPrinciplesCollapse.tsx @@ -4,12 +4,14 @@ import { FairPrinciplesCollapsible, type FairPrinciplesCollapsibleSection, } from '@features/Fair/components/FairPrinciplesCollapsible'; +import type { FairCertificationQuestion } from '@features/Fair/types/fairCertification.types'; import { FAIR_PRINCIPLE_SECTION_KEYS, type FairPrincipleSection, type FairPrincipleSectionKey, + type FairQuestionItem, } from '@features/Fair/types/fairPrinciples.types'; -import type { FairRepositoryStatusParams } from '@features/Fair/utils/resolveFairQuestionStatus'; +// import type { FairRepositoryStatusParams } from '@features/Fair/utils/resolveFairQuestionStatus'; import {Button, Form, Typography} from 'antd'; import {useMemo} from 'react'; @@ -20,8 +22,9 @@ export type FairPrinciplesCollapseProps = { onSubmit?: () => void; /** Collapsible presentation: `default` (full FAIR styling) or `compact` (tighter panels). */ collapsibleVariant?: 'default' | 'compact'; + certificationQuestions?: FairCertificationQuestion[]; /** When set, principle rows with a USRN mapping show Yes/No from the same APIs as USRN. */ - repositoryStatus?: FairRepositoryStatusParams | null; + // repositoryStatus?: FairRepositoryStatusParams | null; }; const {Title, Paragraph} = Typography; @@ -30,7 +33,8 @@ export const FairPrinciplesCollapse = ({ onSave, onSubmit, collapsibleVariant = 'default', - repositoryStatus, + certificationQuestions, + // repositoryStatus, }: FairPrinciplesCollapseProps) => { const {principlesAccordion} = fairTexts; const [openAnswersForm] = Form.useForm(); @@ -48,8 +52,28 @@ export const FairPrinciplesCollapse = ({ const openQuestionLabel = principlesAccordion.openQuestionBadge ?? 'Open question'; const collapsibleSections: FairPrinciplesCollapsibleSection[] = useMemo(() => { + const questionsByNumber = certificationQuestions?.length + ? new Map( + certificationQuestions + .filter((question) => question.number) + .map((question) => [question.number as string, question]), + ) + : undefined; + + const attachCertificationQuestion = (item: FairQuestionItem): FairQuestionItem => { + const certificationQuestion = + item.number && questionsByNumber ? questionsByNumber.get(item.number) : undefined; + + return certificationQuestion ? { ...item, certificationQuestion } : item; + }; + return FAIR_PRINCIPLE_SECTION_KEYS.map((key: FairPrincipleSectionKey) => { const section = principlesAccordion[key] as FairPrincipleSection; + const enrichedSection: FairPrincipleSection = { + ...section, + items: section.items.map(attachCertificationQuestion), + }; + return { key, label: ( @@ -63,14 +87,14 @@ export const FairPrinciplesCollapse = ({ children: ( ), }; }); - }, [principlesAccordion, recommendationHeading, openQuestionLabel, repositoryStatus]); + }, [principlesAccordion, recommendationHeading, openQuestionLabel, certificationQuestions]); return (
{ + const { selectedDataProvider, isLoaded } = useDataProviderStore(); + const effectiveDataProviderId = dataProviderId ?? selectedDataProvider?.id; + + const key = + isLoaded && effectiveDataProviderId + ? `/internal/data-providers/${effectiveDataProviderId}/fair-certification` + : null; + + const { data, error, isLoading, mutate } = useSWR( + key, + key ? () => fetcher(key).then((res) => res as FairCertificationApiResponse) : null, + swrDefaultConfig, + ); + + return { + fairCertification: data ?? null, + error, + isLoading: isLoading || !isLoaded, + mutate, + }; +}; diff --git a/src/features/Fair/styles.css b/src/features/Fair/styles.css index 503cab0b..46259a80 100644 --- a/src/features/Fair/styles.css +++ b/src/features/Fair/styles.css @@ -603,7 +603,7 @@ font-size: 4px; font-style: normal; font-weight: 400; - line-height: 160%; /* 5.05px */ + line-height: 160%; } .fair-certificate__footer { @@ -628,7 +628,7 @@ font-size: 6px; font-style: normal; font-weight: 700; - line-height: 140%; /* 7.512px */ + line-height: 140%; letter-spacing: 0.268px; padding: 2px 0; border-bottom: 1px solid var(--color-primary); @@ -638,7 +638,7 @@ font-size: 4px; font-style: normal; font-weight: 400; - line-height: 140%; /* 5.302px */ + line-height: 140%; margin: 0 0 4px; color: var(--color-primary); margin-top: 2px; @@ -670,7 +670,7 @@ font-size: 4px; font-style: normal; font-weight: 400; - line-height: 160%; /* 5.05px */ + line-height: 160%; margin-top: 10px; } @@ -699,7 +699,7 @@ font-size: 3.787px; font-style: normal; font-weight: 400; - line-height: 140%; /* 5.302px */ + line-height: 140%; } /* Corner ornaments */ @@ -946,7 +946,7 @@ margin-bottom: 0; } -.fair-principles-collapsible-root .ant-collapse-borderless >.ant-collapse-item:last-child { +.fair-principles-collapsible-root .ant-collapse-borderless>.ant-collapse-item:last-child { border: 1px solid var(--color-border); } @@ -1241,7 +1241,7 @@ font-size: 16px; font-style: normal; font-weight: 500; - line-height: 130%; /* 20.8px */ + line-height: 130%; letter-spacing: 0.01px; } diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json index 38cb9e33..c52cd0f1 100644 --- a/src/features/Fair/texts/fair.json +++ b/src/features/Fair/texts/fair.json @@ -29,7 +29,8 @@ "code": "R(1.1)", "question": "Does your repository support OAI-PMH?", "description": "The Open Archives Initiative Protocol for Metadata Harvesting (referred to as the OAI-PMH in the remainder of this document) provides an application-independent interoperability framework based on metadata harvesting. This provides the basic building block to provide an interoperable digital publications repository.", - "recommendation": "OAI-PMH is the most widely used protocol for exchanging information between repositories. It is the standard used by CERN, by arXiv, by Wikipedia, by repository software tools such as DSpace and ePrints, and by CORE. It reduces network traffic because it can do incremental harvesting.\n\nRepository managers can check their OAI-PMH endpoint using the [OAI-PMH Validator tool](https://www.openarchives.org/Register/ValidateSite)." + "recommendation": "OAI-PMH is the most widely used protocol for exchanging information between repositories. It is the standard used by CERN, by arXiv, by Wikipedia, by repository software tools such as DSpace and ePrints, and by CORE. It reduces network traffic because it can do incremental harvesting.\n\nRepository managers can check their OAI-PMH endpoint using the [OAI-PMH Validator tool](https://www.openarchives.org/Register/ValidateSite).", + "number": "1.1" }, { "id": "r12", @@ -37,14 +38,16 @@ "question": "Does your repository metadata support a widely used Application Profile?", "description": "An application profile is a set of policies and metadata guidelines for describing scholarly documents. Examples include Dublin Core, OpenAIRE Guidelines and RIOXX. However, these application profiles are not equally detailed.\n\nEnsuring the usage of a well-known application profile means that the metadata of the publication is described in a less ambiguous and more accurate way. Most used publications profiles are the OpenAIRE Guidelines 3.0+ and the RIOXX metadata application profile.\n\nApart from matching an application profile, it is also important that your content supports at least the minimal set of metadata to give it the rights to be a scholarly record. We define the minimal set as title, abstract, authors, publication year (or year of first visibility). This list is in no way complete but it helps us establish a minimal compliance of your records.", "percentLabel": "Your repository support RIOXXv2, RIOXXv3, OAIRE.", - "recommendation": "CORE metadata indexing requires Dublin Core as a minimum; it supports OpenAIRE Guidelines; and it recommends RIOXX version 3. CORE recommends that repositories use RIOXX v3, which we consider to be the most suitable metadata application profile for describing scholarly research outputs. RIOXX: The Research Outputs Metadata Schema was developed for institutional repositories to share metadata about the scholarly resources they contain.\n\nInformation about how to comply with metadata recommendations are provided in the CORE Data Provider’s Guide." + "recommendation": "CORE metadata indexing requires Dublin Core as a minimum; it supports OpenAIRE Guidelines; and it recommends RIOXX version 3. CORE recommends that repositories use RIOXX v3, which we consider to be the most suitable metadata application profile for describing scholarly research outputs. RIOXX: The Research Outputs Metadata Schema was developed for institutional repositories to share metadata about the scholarly resources they contain.\n\nInformation about how to comply with metadata recommendations are provided in the CORE Data Provider’s Guide.", + "number": "1.2" }, { "id": "r13", "code": "R(1.3)", "openQuestion": true, "answerPlaceholder": "Write your answer here …", - "question": "How many metadata records are there in your repository?" + "question": "How many metadata records are there in your repository?", + "number": "1.3" }, { "id": "", @@ -65,14 +68,16 @@ "question": "Do you provide access to full texts of research papers for your content?", "description": "The number of full text CORE is able to discover from your digital repository.", "percentLabel": "Percentage of documents with access to full text:", - "recommendation": "You can check the Indexing tab where we collect issues CORE discovered while indexing your repository. If this number doesn't match your records it might mean you have issues in exposing your records in an interoperable way. The Indexing status tab provides a summary of any issue CORE finds while indexing your repository. CORE requires that full text must be hosted on the same domain or subdomains as the OAI-PMH endpoint, unless we are informed by the institution about other owned domains containing full text.\n\nAlso, we highly recommend checking your repository for compliance with the CORE Data Provider’s Guide requirements, where you will find detailed information about how to configure your repository so it can be indexed to the maximum effect to get more accurate results in the CORE dashboard and to enhance visibility and interoperability beyond CORE." + "recommendation": "You can check the Indexing tab where we collect issues CORE discovered while indexing your repository. If this number doesn't match your records it might mean you have issues in exposing your records in an interoperable way. The Indexing status tab provides a summary of any issue CORE finds while indexing your repository. CORE requires that full text must be hosted on the same domain or subdomains as the OAI-PMH endpoint, unless we are informed by the institution about other owned domains containing full text.\n\nAlso, we highly recommend checking your repository for compliance with the CORE Data Provider’s Guide requirements, where you will find detailed information about how to configure your repository so it can be indexed to the maximum effect to get more accurate results in the CORE dashboard and to enhance visibility and interoperability beyond CORE.", + "number": "2.1" }, { "id": "r22", "code": "R(2.2)", "question": "Do you label embargoed documents in a machine-readable way?", "description": "Embargoed documents are articles that can only be made available open access after a stated interval, such as 90 days or six months. These delays are best managed when the embargo is tagged in a machine-readable way to ensure that machines can access and understand the policy.", - "recommendation": "There are various supported ways of tagging an embargo document; CORE recognises only a few of them. The RIOXX specification recommends that they are tagged using “HTTP status 451 Unavailable For Legal Reasons”. If this number doesn't match the one in your records please review your system.\n\nAnother common case is reporting 401 when a record is not available. We recognise both of these cases to be valid.\n\nMany platforms report embargoed documents through a redirect to a login page or external links. These solutions although effective for humans, are extremely difficult to catch for a machine, and we recommend against it." + "recommendation": "There are various supported ways of tagging an embargo document; CORE recognises only a few of them. The RIOXX specification recommends that they are tagged using “HTTP status 451 Unavailable For Legal Reasons”. If this number doesn't match the one in your records please review your system.\n\nAnother common case is reporting 401 when a record is not available. We recognise both of these cases to be valid.\n\nMany platforms report embargoed documents through a redirect to a login page or external links. These solutions although effective for humans, are extremely difficult to catch for a machine, and we recommend against it.", + "number": "2.2" }, { "id": "r23", @@ -80,7 +85,8 @@ "question": "Do you support machine-readable licensing?", "description": "The percentage of metadata records with licence URLs CORE is able to recognise from the metadata.", "percentLabel": "Percent of metadata records with a licence specified:", - "recommendation": "Licensing and copyright information may be displayed in a machine-readable way or by a text description. However, text descriptions such as “all rights reserved” may not be specific enough for other users to interpret. Much better is to provide a URL to the relevant Creative Commons licence, for example, for the non-commercial CC-BY licence, https://creativecommons.org/licenses/by-nc/4.0/ By clicking on this link, a human can see the precise meaning of this licence. Any machine can identify immediately and unambiguously what kind of licence is stated.\n\nCORE promotes and advocates for the use of machine-readable licencing descriptions. In particular, we advise the use of the NISO Access License and Indicators (ALI) Schema.\n\nInstitutions may have a blanket policy that covers all content with a specific publisher, which usually means that individual documents contain no licensing information. However, a specific CC-BY licence enables exceptions to be made for individual documents and reduces ambiguity.\n\nThe CORE Rights Retention Statement tracker identifies with reasonable accuracy any specific licence conditions that are stated within an article or text file." + "recommendation": "Licensing and copyright information may be displayed in a machine-readable way or by a text description. However, text descriptions such as “all rights reserved” may not be specific enough for other users to interpret. Much better is to provide a URL to the relevant Creative Commons licence, for example, for the non-commercial CC-BY licence, https://creativecommons.org/licenses/by-nc/4.0/ By clicking on this link, a human can see the precise meaning of this licence. Any machine can identify immediately and unambiguously what kind of licence is stated.\n\nCORE promotes and advocates for the use of machine-readable licencing descriptions. In particular, we advise the use of the NISO Access License and Indicators (ALI) Schema.\n\nInstitutions may have a blanket policy that covers all content with a specific publisher, which usually means that individual documents contain no licensing information. However, a specific CC-BY licence enables exceptions to be made for individual documents and reduces ambiguity.\n\nThe CORE Rights Retention Statement tracker identifies with reasonable accuracy any specific licence conditions that are stated within an article or text file.", + "number": "2.3" }, { "id": "r24", @@ -89,7 +95,8 @@ "description": "Tools for tracking COUNTER-compliant downloads allow you to demonstrate the value and impact of your institutional repository (IR) by enabling institutional repositories to share and compare usage statistics based on the COUNTER standard.", "recommendation": "By using a standard set of statistics, it becomes possible to provide meaningful stats to compare downloads across several repositories.", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "2.4" } ] }, @@ -102,14 +109,16 @@ "code": "R(3.1)", "question": "Do you support FAIR Signposting?", "description": "FAIR Signposting is a technical approach designed to enhance the discoverability and interoperability of scholarly resources on the web. It involves using standardized HTTP link relations to expose key information about web resources, particularly in the context of scholarly communication. This approach aligns with the broader FAIR principles, which aim for data to be Findable, Accessible, Interoperable and Reusable.", - "recommendation": "FAIR signposting can be done by adding information to HTTP headers. Specifically, the HTTP header can contain an element to describe the type of content resource, for example, “author”, or “collection”. Find more information in [CORE Data Provider’s Guide.](https://core.ac.uk/documentation/data-providers-guide#fair-signposting)" + "recommendation": "FAIR signposting can be done by adding information to HTTP headers. Specifically, the HTTP header can contain an element to describe the type of content resource, for example, “author”, or “collection”. Find more information in [CORE Data Provider’s Guide.](https://core.ac.uk/documentation/data-providers-guide#fair-signposting)", + "number": "3.1" }, { "id": "r32", "code": "R(3.2)", "question": "Do your metadata use COAR vocabularies?", "description": "COAR (Confederation of Open Access Repositories) vocabularies are structured sets of terms organized to provide a common framework for the sharing and integration of data across different systems. They comprise three types: Access rights, resource types and version types.", - "recommendation": "A metadata profile such as RIOXX can provide an easy way to share and use COAR vocabularies in your metadata.\n\nAccess rights: describe the access status of a resource, such as “embargoed access” or “restricted access”.\n\nResource type: describe the type of resource, such as “patent”, “bibliography” or “conference presentation”.\n\nVersion type: distinguish the various states of an academic article, including the version of record (VOR), the Author’s Original (AO) and the Author’s Accepted Manuscript (AM)." + "recommendation": "A metadata profile such as RIOXX can provide an easy way to share and use COAR vocabularies in your metadata.\n\nAccess rights: describe the access status of a resource, such as “embargoed access” or “restricted access”.\n\nResource type: describe the type of resource, such as “patent”, “bibliography” or “conference presentation”.\n\nVersion type: distinguish the various states of an academic article, including the version of record (VOR), the Author’s Original (AO) and the Author’s Accepted Manuscript (AM).", + "number": "3.2" }, { "id": "r33", @@ -117,7 +126,8 @@ "question": "Does your repository support persistent management of records?", "description": "A repository with persistent management ensures that metadata and content are not lost, that links remain valid and that researchers can reliably cite and access resources over time.", "recommendation": "It is recommended that the repository implements a recognized system for persistent identifiers like DOI or ROR. Clear policies should be in place for record versioning, updates and withdrawal, ensuring transparency and trustworthiness. Documentation of these policies should be openly available to users.", - "openQuestion": true + "openQuestion": true, + "number": "3.3" }, { "id": "r34", @@ -125,7 +135,8 @@ "question": "Is your repository configured for the OAI resolver and does it work?", "description": "An OAI (Open Archives Initiative) identifier is a unique identifier of a metadata record. CORE can resolve any OAI identifier directly to your repository page.", "statusNote": "Error — We are not able to resolve OAI identifiers from your repository to the repository landing pages.", - "recommendation": "To register your OAI mapping, navigate to the **Settings** tab in the dashboard for your repository. To route directly to the repository, it is necessary to provide mapping in the CORE Repository page between the OAI prefix of a repository and the currently used URL for the repository metadata record display page or splash page. The redirection will change instantly. More information about OAI resolver and its configuration can be found in the \n- [CORE Data Provider’s Guide: Repository configuration, OAI identifiers](https://core.ac.uk/documentation/data-providers-guide#oai-identifier).\n- [Membership documentation: Get your OAI identifiers resolved to your repository](https://core.ac.uk/documentation/membership-documentation#OAI-identifiers) \n- [OAI Identifier documentation page](https://core.ac.uk/documentation/oai-resolver)." + "recommendation": "To register your OAI mapping, navigate to the **Settings** tab in the dashboard for your repository. To route directly to the repository, it is necessary to provide mapping in the CORE Repository page between the OAI prefix of a repository and the currently used URL for the repository metadata record display page or splash page. The redirection will change instantly. More information about OAI resolver and its configuration can be found in the \n- [CORE Data Provider’s Guide: Repository configuration, OAI identifiers](https://core.ac.uk/documentation/data-providers-guide#oai-identifier).\n- [Membership documentation: Get your OAI identifiers resolved to your repository](https://core.ac.uk/documentation/membership-documentation#OAI-identifiers) \n- [OAI Identifier documentation page](https://core.ac.uk/documentation/oai-resolver).", + "number": "3.4" }, { "id": "r35", @@ -133,21 +144,24 @@ "question": "DOI", "description": "The DOI provides a machine-actionable persistent identifier that enables systems to consistently locate and reliably link research objects across different platforms, thereby guaranteeing Interoperability (I) and enhancing Findability (F).", "percentLabel": "Percentage of metadata with DOI:", - "recommendation": "To enrich your metadata with more DOIs, you can go to the DOI tab. It provides a checklist of the number of content objects in your repository which have a DOI. It also identifies (and counts) the number of DOIs that it finds from articles held in other repositories relating to research in your institution." + "recommendation": "To enrich your metadata with more DOIs, you can go to the DOI tab. It provides a checklist of the number of content objects in your repository which have a DOI. It also identifies (and counts) the number of DOIs that it finds from articles held in other repositories relating to research in your institution.", + "number": "3.5" }, { "id": "r36", "code": "R(3.6)", "question": "ORCID", "description": "Public Investigator and Contributor Identifier is a non-proprietary alphanumeric code that uniquely identifies scientific authors. ORCID is an essential metadata tool that enables researchers to claim credit for their own research, without ambiguity. For example, there are many researchers with the common name “John Smith” or “Zhang Wei”. ORCID enables these names to be disambiguated.", - "recommendation": "The repository should support integration with ORCID IDs in its metadata schema. Metadata records should expose ORCID identifiers in a standardised way using DataCite or Dublin Core fields. If possible, the repository should support ORCID authentication during deposit, allowing researchers to connect their ORCID profile directly and authorise the repository to update their ORCID record with new works.\n\nThe CORE ORCID dashboard tab enables repository managers both to identify missing ORCID IDs and to enrich their data with ORCID IDs found from elsewhere. Find more information in the ORCID Membership documentation." + "recommendation": "The repository should support integration with ORCID IDs in its metadata schema. Metadata records should expose ORCID identifiers in a standardised way using DataCite or Dublin Core fields. If possible, the repository should support ORCID authentication during deposit, allowing researchers to connect their ORCID profile directly and authorise the repository to update their ORCID record with new works.\n\nThe CORE ORCID dashboard tab enables repository managers both to identify missing ORCID IDs and to enrich their data with ORCID IDs found from elsewhere. Find more information in the ORCID Membership documentation.", + "number": "3.6" }, { "id": "r37", "code": "R(3.7)", "question": "ROR", "description": "ROR — Research Organisation Registry. Identifies institutions and organisations: universities, research centres, funders, etc.", - "recommendation": "The CORE Dashboard enables you to specify your ROR ID. You should check the details to make sure your institution is identified correctly. If the ROR ID is missing or incorrect, you can correct it on the dashboard. The ROR ID can be obtained from the Research Organisation Registry website." + "recommendation": "The CORE Dashboard enables you to specify your ROR ID. You should check the details to make sure your institution is identified correctly. If the ROR ID is missing or incorrect, you can correct it on the dashboard. The ROR ID can be obtained from the Research Organisation Registry website.", + "number": "3.7" } ] }, @@ -161,14 +175,16 @@ "question": "Do you provide data availability statements?", "description": "A data availability statement, also sometimes called a ‘data access statement’, is a short, mandatory section in a paper that tells the reader where the research data associated with a paper is available and under what conditions the data can be accessed.", "counterLabel": "Papers with Data availability statement:", - "recommendation": "Here are some recommendations on how to increase the number of DAS in your repository:\n\n- Include links where applicable to the data set. For example, the paper may state “The data that support the findings of this study are openly available in [repository name] at https://doi.org/[doi]”.\n- Provide a dedicated metadata field for DAS, not just full text in the description.\n- Provide researchers with approved, compliant DAS text for all scenarios.\n- CORE provides several tools under the [DAS identification tab]():\n\n monitoring statistics about DAS in your repository, and a Data Availability Statement checker." + "recommendation": "Here are some recommendations on how to increase the number of DAS in your repository:\n\n- Include links where applicable to the data set. For example, the paper may state “The data that support the findings of this study are openly available in [repository name] at https://doi.org/[doi]”.\n- Provide a dedicated metadata field for DAS, not just full text in the description.\n- Provide researchers with approved, compliant DAS text for all scenarios.\n- CORE provides several tools under the [DAS identification tab]():\n\n monitoring statistics about DAS in your repository, and a Data Availability Statement checker.", + "number": "4.1" }, { "id": "r42", "code": "R(4.2)", "question": "Do you provide links to software mentions?", "description": "Providing links to source means that the repository includes references to the articles that were used in the research.", - "recommendation": "We are working on making software mentions a first-class citation object. The most common place to store this information in the metadata is the `dc:relation` field. CORE also supports a machine learning tool to recognise software mentions directly inside the paper; using this feedback loop the repository can track more citations and integrate them in the global scholarly graph." + "recommendation": "We are working on making software mentions a first-class citation object. The most common place to store this information in the metadata is the `dc:relation` field. CORE also supports a machine learning tool to recognise software mentions directly inside the paper; using this feedback loop the repository can track more citations and integrate them in the global scholarly graph.", + "number": "4.2" }, { "id": "r43", @@ -177,7 +193,8 @@ "description": "Example from assessment: papers with RRS section — **13.61K**.", "openQuestion": true, "answerPlaceholder": "Write your answer here …", - "recommendation": "Follow your institutional policy on rights retention and ensure RRS sections are visible where applicable." + "recommendation": "Follow your institutional policy on rights retention and ensure RRS sections are visible where applicable.", + "number": "4.3" } ] }, @@ -190,28 +207,32 @@ "code": "R(5.1)", "question": "Is your research identifiable by funding bodies?", "description": "Applies only to UK repositories. Only a recommendation for everybody else. The repository records and exposes details about the organizations or agencies that financially supported the research.", - "recommendation": "This typically includes the funder name, grant number or project ID and ideally a persistent identifier for the funder — a ROR ID or Crossref Funder Registry ID." + "recommendation": "This typically includes the funder name, grant number or project ID and ideally a persistent identifier for the funder — a ROR ID or Crossref Funder Registry ID.", + "number": "5.1" }, { "id": "r52", "code": "R(5.2)", "question": "Do you expose metadata about dates of deposit?", "description": "Exposing metadata about dates of deposit means that the repository clearly records and makes available the date an item was first deposited into the system.", - "recommendation": "This provides provenance information and helps users and aggregators know when the record entered the repository. It is also useful for version control, compliance checking and tracking the research lifecycle.\n\n- Configure the repository to automatically capture deposit dates during submission.\n- Ensure the deposit date is included in all exposed metadata formats (OAI-PMH, DataCite, Dublin Core).\n- Regularly audit metadata records to check completeness and identify missing dates." + "recommendation": "This provides provenance information and helps users and aggregators know when the record entered the repository. It is also useful for version control, compliance checking and tracking the research lifecycle.\n\n- Configure the repository to automatically capture deposit dates during submission.\n- Ensure the deposit date is included in all exposed metadata formats (OAI-PMH, DataCite, Dublin Core).\n- Regularly audit metadata records to check completeness and identify missing dates.", + "number": "5.2" }, { "id": "r53", "code": "R(5.3)", "question": "Do you provide information about funding bodies for the research outputs?", "description": "The repository records and exposes details about the organizations or agencies that financially supported the research.", - "recommendation": "This typically includes the funder name, grant number or project ID and ideally a persistent identifier for the funder — a ROR ID or Crossref Funder Registry ID." + "recommendation": "This typically includes the funder name, grant number or project ID and ideally a persistent identifier for the funder — a ROR ID or Crossref Funder Registry ID.", + "number": "5.3" }, { "id": "r54", "code": "R(5.4)", "question": "Do you provide in the metadata information about project IDs?", "description": "The repository records and exposes identifiers of funded projects associated with research outputs.", - "recommendation": "- Implement a dedicated metadata field for project IDs alongside funding body details.\n- Support standard formats and schemas like OpenAIRE Guidelines or DataCite metadata that include fields for project identifiers.\n- Encourage depositors to enter official grant or project IDs at the time of submission.\n- Where possible, use persistent identifiers for projects: OpenAIRE project IDs, DOIs.\n- Ensure the information is machine-readable and harvestable through OAI-PMH." + "recommendation": "- Implement a dedicated metadata field for project IDs alongside funding body details.\n- Support standard formats and schemas like OpenAIRE Guidelines or DataCite metadata that include fields for project identifiers.\n- Encourage depositors to enter official grant or project IDs at the time of submission.\n- Where possible, use persistent identifiers for projects: OpenAIRE project IDs, DOIs.\n- Ensure the information is machine-readable and harvestable through OAI-PMH.", + "number": "5.4" } ] }, @@ -226,7 +247,8 @@ "description": "ResourceSync allows repositories to expose changes to their content and metadata in a structured, machine-readable way. With ResourceSync, external services (such as aggregators, harvesters and preservation systems) can keep their records in sync with the repository by automatically detecting new, updated or deleted items. Unlike OAI-PMH, which focuses on metadata harvesting, ResourceSync provides a more efficient and scalable way to track repository content changes in real time.", "recommendation": "- Assess whether your repository platform supports ResourceSync (e.g. DSpace, EPrints, Invenio).\n- Provide a Resource List (all available items) and Change Lists (new, updated or deleted items) to support synchronisation.\n- Ensure ResourceSync feeds include persistent identifiers (DOIs, Handles) and standard metadata formats.\n- Document the repository’s ResourceSync base URL in public documentation for service providers.", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "6.1" }, { "id": "r62", @@ -235,7 +257,8 @@ "description": "COAR Notify is a protocol developed by the Confederation of Open Access Repositories (COAR) to enable interoperable, event-based communication between repositories, publishers and other scholarly services. It is built on the Linked Data Notifications (LDN) standard and allows repositories to send and receive notifications about research objects — for example when a preprint has been deposited or when a peer review has been linked to a record.", "recommendation": "- Ensure that your repository can send notifications about deposits, updates or withdrawals, and receive notifications about reviews, endorsements or citations.\n- Align metadata and identifiers used in notifications with persistent identifiers (DOIs, ORCIDs, RORs) to maximize interoperability.\n- Document your repository COAR Notify endpoints and policies for external partners.", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "6.2" }, { "id": "r63", @@ -244,7 +267,8 @@ "description": "FAIRiCat (FAIR Implementation Profile Catalog) is an international initiative led by GO FAIR that provides a registry of FAIR Implementation Profiles (FIPs). Read more: https://signposting.org/FAIRiCat/", "recommendation": "- Prepare a FAIR Implementation Profile (FIP) for your repository, describing identifiers (DOI, ORCID, ROR), metadata schemas (DataCite, Dublin Core), protocols (OAI-PMH, ResourceSync) and vocabularies in use.\n- Submit your FIP to the FAIRiCat registry so it is visible and reusable by the wider community.\n- Regularly update the FIP as your repository adopts new standards or technologies.\n- Reference your published FIP in the repository documentation to demonstrate compliance and transparency.", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "6.3" } ] }, @@ -259,7 +283,8 @@ "description": "Deposit management refers to the process the repository uses to handle new submissions of research outputs.", "recommendation": "Define a clear deposit workflow: self-deposit, mediated deposit or curated. Assign repository staff (curators or managers) to review submissions for metadata quality, copyright, licensing and compliance with institutional and funder policies. Provide support for depositors: templates, checklists, FAQs.\n\n- Self-deposited — by researchers, authors or administrative staff.\n- Mediated — by librarians or repository managers on behalf of authors.\n- Curated — before publication to ensure metadata quality, copyright compliance and adherence to repository policies.", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "7.1" }, { "id": "r72", @@ -268,7 +293,8 @@ "description": "Define a clear deposit workflow: self-deposit, mediated deposit or curated. Assign repository staff to review submissions for metadata quality, copyright, licensing and compliance with institutional and funder policies. Provide support for depositors: templates, checklists, FAQs.", "recommendation": "Ensure a documented review step exists before content becomes publicly available, with clear accountability.", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "7.2" }, { "id": "r73", @@ -277,7 +303,8 @@ "description": "Handling take-down notices refers to the repository’s process for managing requests to remove or restrict access to deposited content.", "recommendation": "- Establish a publicly available take-down policy, outlining when and how content may be removed.\n- Designate a contact point (repository manager or legal office) for receiving and handling take-down requests.\n- Acknowledge requests promptly and investigate validity in consultation with legal or policy experts if needed.\n- If the request is justified, withdraw or restrict access to the item, but preserve the metadata record with a clear note explaining the reason for removal.", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "7.3" }, { "id": "r74", @@ -286,7 +313,8 @@ "description": "Preservation of content in a repository refers to the set of policies, workflows and technical measures that ensure digital objects remain\n- available (can be found and opened),\n- authentic (guaranteed to be original)\n- usable (can be realistically reused).", "recommendation": "- Define and publish a preservation policy that outlines the repository’s commitment to long-term access and specifies responsibilities.\n- Implement redundant storage and backup strategies: multiple servers, offsite copies, cloud storage.\n- Use fixity checks (checksums, hashes) to regularly verify file integrity and detect corruption.\n- Prefer open and widely used file formats (PDF, TXT, CSV) for better long-term sustainability.\n- Record and expose technical metadata (file format, size, checksum) to support digital preservation workflows.", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "7.4" }, { "id": "r75", @@ -295,7 +323,8 @@ "description": "Encouraging researchers to obtain an ORCID (Open Researcher and Contributor ID) ensures that their work is uniquely identifiable and correctly attributed, regardless of name variations, institutional changes or publication platforms. ORCID adoption strengthens the quality of repository metadata, improves interoperability with publishers and funders and helps researchers gain recognition for their contributions.", "recommendation": "- Promote ORCID awareness through training sessions, repository guides and onboarding materials.\n- Provide step-by-step instructions and links for registering an ORCID ID.\n- Enable ORCID authentication and login in the repository so researchers can connect their profile during deposit.\n- Encourage depositors to include their ORCID in the metadata of every submission.\n- Where possible set ORCID as a mandatory or strongly recommended field in deposit workflows.\n- Integrate with the ORCID API to push publications and datasets from the repository directly to a researcher’s ORCID profile.", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "7.5" }, { "id": "r76", @@ -304,7 +333,8 @@ "description": "The repository actively implements policies, technologies and community practices to make research outputs more discoverable, usable and sustainable. ", "recommendation": "- Clear policies — Publish policies that explicitly reference FAIR principles.\n- Metadata standards — Require rich, standardized metadata (DataCite, Dublin Core, OpenAIRE Guidelines).\n- Persistent identifiers — Support DOIs, ORCIDs, RORs and project IDs.\n- Infrastructure — Provide OAI-PMH, ResourceSync and APIs.\n- Licensing and reuse — Encourage open licenses (CC-BY, CC0).\n- Data linking — Promote Data Availability Statements and links to datasets and source code.\n- Quality control — Review deposits for metadata completeness.\n- Awareness and training — Workshops and guidance for researchers.\n- Community engagement — Participate in FAIR-related initiatives (GO FAIR, COAR).", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "7.6" }, { "id": "r77", @@ -313,7 +343,8 @@ "description": "Promoting research reproducibility means ensuring that other researchers can verify, replicate and build upon the results deposited in the repository. This goes beyond simply storing outputs: it requires linking publications with their underlying datasets, methods and documentation, as well as providing transparent metadata and persistent identifiers.", "recommendation": "- Require or encourage Data Availability Statements linking publications to underlying datasets.\n- Enable persistent identifiers (DOIs for publications and datasets, ORCID for authors, ROR for institutions).\n- Support versioning so users can access previous iterations of datasets or papers.\n- Provide metadata fields for methods and protocols.\n- Encourage open licenses so that others can reuse data and code legally.\n- Facilitate interoperability using standard metadata schemas.\n- Promote best practices through guidance, training and templates.\n- Integrate with external registries where relevant.\n- Provide transparency in curation.", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "7.7" }, { "id": "r78", @@ -322,7 +353,8 @@ "description": "Implementing policies, infrastructure and cultural practices that make research outputs more transparent, accessible and reusable. This includes promoting open access to publications, sharing research data, supporting reproducibility and enabling collaboration across disciplines and communities.", "recommendation": "- Adopt an institutional Open Science policy aligned with national and European guidelines (EOSC, Plan S).\n- Provide infrastructure such as institutional repositories, data archives and integration with ORCID, DOI and ROR identifiers.\n- Promote FAIR data practices by training researchers in data management, metadata standards and reproducibility.\n- Support Open Access publishing through funding or agreements with publishers or institutional repositories (Green/Gold OA).\n- Encourage Data and Code Availability Statements.\n- Offer training on open research practices, licensing and copyright.\n- Recognize and reward Open Science practices in research evaluation where appropriate.\n- Collaborate with external infrastructures (OpenAIRE, EOSC, COAR).\n- Provide long-term preservation services.\n- Engage the community through workshops and awareness campaigns.", "openQuestion": true, - "answerPlaceholder": "Write your answer here …" + "answerPlaceholder": "Write your answer here …", + "number": "7.8" } ] } diff --git a/src/features/Fair/types/fairCertification.types.ts b/src/features/Fair/types/fairCertification.types.ts new file mode 100644 index 00000000..a9fc8a48 --- /dev/null +++ b/src/features/Fair/types/fairCertification.types.ts @@ -0,0 +1,57 @@ +export type FairCertificationStatus = + | 'not_certified' + | 'pending' + | 'certified' + | 'expired' + | string; + +export type FairCertificationLevel = + | 'BRONZE' + | 'SILVER' + | 'GOLD' + | 'PLATINUM' + | string; + +export type FairCertificationQuestionResultStatus = 'pass' | 'fail' | 'unknown' | string; + +export type FairCertificationQuestionCounts = Record; + +export type FairCertificationQuestionMetrics = Record; + +export type FairCertificationQuestionResult = { + status?: FairCertificationQuestionResultStatus; + metrics?: FairCertificationQuestionMetrics; + counts?: FairCertificationQuestionCounts; + evidence?: Record; + threshold?: { + operator?: string; + value?: number; + unit?: string; + }; + source?: string; + checkedAt?: string; +}; + +export type FairCertificationQuestion = { + id: string; + number?: string; + section?: string; + type?: string; + required?: boolean; + question?: string; + description?: string; + recommendation?: string; + result?: FairCertificationQuestionResult; +}; + +export type FairCertificationApiResponse = { + status?: FairCertificationStatus; + level?: FairCertificationLevel | null; + repositoryName?: string; + issueDate?: string | null; + validUntil?: string | null; + lastReportUpdate?: string | null; + submissionCount?: number | null; + questions?: FairCertificationQuestion[]; + [key: string]: unknown; +}; diff --git a/src/features/Fair/types/fairPrinciples.types.ts b/src/features/Fair/types/fairPrinciples.types.ts index e76ca64f..a6222503 100644 --- a/src/features/Fair/types/fairPrinciples.types.ts +++ b/src/features/Fair/types/fairPrinciples.types.ts @@ -1,3 +1,5 @@ +import type { FairCertificationQuestion } from '@features/Fair/types/fairCertification.types'; + export type FairMetricLine = { label: string; value: string; @@ -6,6 +8,8 @@ export type FairMetricLine = { export type FairQuestionItem = { id: string; code: string; + /** Matches API question `number` (e.g. "1.1", "2.4"). */ + number?: string; question: string; description?: string; recommendation?: string; @@ -18,6 +22,7 @@ export type FairQuestionItem = { percentLabel?: string; /** Label for `countValue` (e.g. indexed metadata total). */ counterLabel?: string; + certificationQuestion?: FairCertificationQuestion; }; export type FairPrincipleSection = { diff --git a/src/features/Fair/utils/resolveFairQuestionStatus.ts b/src/features/Fair/utils/resolveFairQuestionStatus.ts deleted file mode 100644 index 0e14f816..00000000 --- a/src/features/Fair/utils/resolveFairQuestionStatus.ts +++ /dev/null @@ -1,104 +0,0 @@ -import type { DasData } from '@features/Das/types/data.types'; -import type { OrcidStats } from '@features/Orcid/types/data.types'; -import type { UsrnData } from '@features/Usrn/types/data.types'; -import { getCardStatusConfig } from '@features/Usrn/utils/getCardStatusConfig'; -import type { FairQuestionItem } from '@features/Fair/types/fairPrinciples.types'; - -/** Same shape as data passed to USRN `getCardStatusConfig` / `UsrnView` `configParams`. */ -export type FairRepositoryStatusParams = { - rioxx: { partiallyCompliantCount?: number; totalCount?: number } | undefined; - statistics: { countMetadata?: number } | undefined; - internalStatistics: { - fullTextCount?: number; - metadataCount?: number; - doiCount?: number; - } | undefined; - usrn: UsrnData | null; - irus: unknown; - rorId: string | null; - orcidStats?: OrcidStats | null; - dasData?: DasData[] | null; -}; - -/** Maps FAIR question `id` (from `fair.json`) to USRN card ids used by `getCardStatusConfig`. */ -const mapFairQuestionIdToUsrnCardId = (fairId: string): string | null => { - const byId: Record = { - '': 'indexedContent', - r11: 'oaiPmh', - r12: 'applicationProfile', - r21: 'accessFullTexts', - r22: 'embargoedDocuments', - r23: 'licensing', - r31: 'supportSignpostingFAIR', - r32: 'metadataCOAR', - r35: 'DOI', - r36: 'ORCID', - r37: 'ROR', - r41: 'webAccessibility', - r42: 'sourceCode', - }; - return byId[fairId] ?? null; -}; - -const capitalizeStatus = (status: 'yes' | 'no'): 'Yes' | 'No' => - status === 'yes' ? 'Yes' : 'No'; - -export type ResolveFairQuestionStatusLabelParams = { - item: FairQuestionItem; - repositoryStatus: FairRepositoryStatusParams | null | undefined; - openQuestionLabel: string; -}; - -export type FairQuestionResolvedDisplay = { - statusLabel: string | null; - cardConfig: ReturnType | null; -}; - -/** - * Resolves status label (chip) and USRN card metrics for a FAIR row in one `getCardStatusConfig` call. - */ -export const resolveFairQuestionDisplay = ({ - item, - repositoryStatus, - openQuestionLabel, -}: ResolveFairQuestionStatusLabelParams): FairQuestionResolvedDisplay => { - if (item.openQuestion) { - return { statusLabel: openQuestionLabel, cardConfig: null }; - } - - if (!repositoryStatus) { - return { statusLabel: null, cardConfig: null }; - } - - const cardId = mapFairQuestionIdToUsrnCardId(item.id); - if (!cardId) { - return { statusLabel: null, cardConfig: null }; - } - - const cardConfig = getCardStatusConfig({ - cardId, - rioxx: repositoryStatus.rioxx, - statistics: repositoryStatus.statistics, - internalStatistics: repositoryStatus.internalStatistics, - usrn: repositoryStatus.usrn, - irus: repositoryStatus.irus, - rorId: repositoryStatus.rorId, - orcidStats: repositoryStatus.orcidStats ?? null, - dasData: repositoryStatus.dasData ?? null, - }); - - if (cardConfig.status === 'yes' || cardConfig.status === 'no') { - return { statusLabel: capitalizeStatus(cardConfig.status), cardConfig }; - } - - return { statusLabel: null, cardConfig }; -}; - -/** - * Resolves the status chip for a FAIR principle row: open-question badge, Yes/No from - * `getCardStatusConfig` when the question maps to a USRN card, or `null` when there is no API match - * (caller should show an em dash). - */ -export const resolveFairQuestionStatusLabel = ( - params: ResolveFairQuestionStatusLabelParams, -): string | null => resolveFairQuestionDisplay(params).statusLabel; diff --git a/src/features/Fair/utils/resolveFairQuestionStatusLabel.ts b/src/features/Fair/utils/resolveFairQuestionStatusLabel.ts new file mode 100644 index 00000000..11c74b16 --- /dev/null +++ b/src/features/Fair/utils/resolveFairQuestionStatusLabel.ts @@ -0,0 +1,26 @@ +import type { FairCertificationQuestion } from '@features/Fair/types/fairCertification.types'; + +export const resolveFairQuestionStatusLabel = ( + certificationQuestion?: FairCertificationQuestion, + openQuestionLabel = 'Open question', +): string => { + if (!certificationQuestion || !('result' in certificationQuestion)) { + return openQuestionLabel; + } + + const status = certificationQuestion.result?.status?.toLowerCase(); + + if (status === 'pass') { + return 'YES'; + } + + if (status === 'fail') { + return 'No'; + } + + if (status === 'unknown') { + return '_'; + } + + return openQuestionLabel; +}; diff --git a/src/pages/FAIRCertification.tsx b/src/pages/FAIRCertification.tsx index 0a3d007b..4a8c4a28 100644 --- a/src/pages/FAIRCertification.tsx +++ b/src/pages/FAIRCertification.tsx @@ -2,7 +2,8 @@ import { useDocumentTitle } from '@hooks/useDocumentTitle.ts'; import { useState, useEffect, useRef } from 'react'; import retrieveContent from '@/utils/retrieveContent'; import { FairFeature, type FairCertificationData } from '@features/Fair/Fair.tsx'; -// import {ApprovedFairView} from '@features/Fair/components/ApprovedFairView.tsx'; +// import { ApprovedFairView } from '@features/Fair/components/ApprovedFairView.tsx'; +import { useFairCertification } from '@features/Fair/hooks/useFairCertification'; const loadFairCertification = async (ref?: string): Promise => { return (await retrieveContent('fair-certification', { @@ -13,8 +14,17 @@ const loadFairCertification = async (ref?: string): Promise(null); const hasLoadedRef = useRef(false); + // TODO test + useEffect(() => { + if (isFairCertificationLoading) { + return; + } + + console.log('fairCertification', fairCertification?.questions); + }, [fairCertification, isFairCertificationLoading]); useEffect(() => { if (hasLoadedRef.current) { @@ -34,7 +44,7 @@ export function FAIRCertificationPage() { // TODO RENDER VIEW BASED ON STATUS return ( - // true ? : + // true ? : ); } From 626a3fafba16a01f8368b25136f07f085b2bce82 Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Thu, 16 Jul 2026 16:34:40 +0400 Subject: [PATCH 41/66] CORE: V2 input auto save on blur --- .../components/FairPrincipleQuestionBlock.tsx | 25 +++++++++++++++++-- .../Fair/hooks/useFairCertification.ts | 12 ++++++++- .../Fair/types/fairCertification.types.ts | 1 + 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx index 46868f5f..9948c4a2 100644 --- a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx +++ b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx @@ -1,9 +1,12 @@ import {InfoOutlined, FileTextOutlined} from '@ant-design/icons'; import {Markdown, PercentBar} from '@oacore/core-ui'; -import {Form, Input} from 'antd'; +import {Form, Input, message} from 'antd'; import {formatNumber} from '@utils/helpers.ts'; +import type {FocusEvent} from 'react'; import type {FairQuestionItem} from '@features/Fair/types/fairPrinciples.types'; +import { updateFairCertificationAnswer } from '@features/Fair/hooks/useFairCertification'; +import { useDataProviderStore } from '@/store/dataProviderStore'; import '../../Usrn/style.css'; import '../styles.css'; @@ -22,7 +25,20 @@ export const FairPrincipleQuestionBlock = ({ recommendationHeading, openQuestionLabel, }: FairPrincipleQuestionBlockProps) => { + const { selectedDataProvider } = useDataProviderStore(); + const dataProviderId = selectedDataProvider?.id; const isOpenQuestion = Boolean(item.openQuestion); + const questionId = item.certificationQuestion?.id; + + const handleAnswerBlur = (event: FocusEvent) => { + if (!dataProviderId || !questionId) { + return; + } + + updateFairCertificationAnswer(dataProviderId, questionId, event.target.value).catch(() => { + message.error('Failed to save your answer. Please try again.'); + }); + }; const percentLabelText = item.percentLabel; const counterLabelText = item.counterLabel; @@ -82,9 +98,14 @@ export const FairPrincipleQuestionBlock = ({ /> {isOpenQuestion ? (
- + diff --git a/src/features/Fair/hooks/useFairCertification.ts b/src/features/Fair/hooks/useFairCertification.ts index c94bdab7..bac1bdf0 100644 --- a/src/features/Fair/hooks/useFairCertification.ts +++ b/src/features/Fair/hooks/useFairCertification.ts @@ -1,5 +1,5 @@ import useSWR from 'swr'; -import { fetcher, swrDefaultConfig } from '@/config/swr'; +import { fetcher, putRequest, swrDefaultConfig } from '@/config/swr'; import { useDataProviderStore } from '@/store/dataProviderStore'; import type { FairCertificationApiResponse } from '@features/Fair/types/fairCertification.types'; @@ -25,3 +25,13 @@ export const useFairCertification = (dataProviderId?: number) => { mutate, }; }; + +export const updateFairCertificationAnswer = ( + dataProviderId: number, + questionId: string, + answer: string, +) => + putRequest( + `/internal/data-providers/${dataProviderId}/fair-certification/answers/${questionId}`, + { answer }, + ); diff --git a/src/features/Fair/types/fairCertification.types.ts b/src/features/Fair/types/fairCertification.types.ts index a9fc8a48..0a569c04 100644 --- a/src/features/Fair/types/fairCertification.types.ts +++ b/src/features/Fair/types/fairCertification.types.ts @@ -41,6 +41,7 @@ export type FairCertificationQuestion = { question?: string; description?: string; recommendation?: string; + answer?: string; result?: FairCertificationQuestionResult; }; From ac39d46580cbb10a224d0fa06347e13715b6bcfc Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Fri, 17 Jul 2026 10:22:56 +0400 Subject: [PATCH 42/66] CORE: V2 input add submit api --- .../Fair/components/ApprovedFairView.tsx | 34 +++++++++++++++++++ .../components/FairPrinciplesCollapse.tsx | 8 +++-- .../Fair/hooks/useFairCertification.ts | 9 ++++- src/features/Fair/texts/fair.json | 2 ++ .../Fair/types/fairCertification.types.ts | 14 ++++++++ 5 files changed, 64 insertions(+), 3 deletions(-) diff --git a/src/features/Fair/components/ApprovedFairView.tsx b/src/features/Fair/components/ApprovedFairView.tsx index 2a66d7d5..55f0d17b 100644 --- a/src/features/Fair/components/ApprovedFairView.tsx +++ b/src/features/Fair/components/ApprovedFairView.tsx @@ -1,16 +1,48 @@ import { CrFeatureLayout, CrPaper } from '@oacore/core-ui'; +import fairTexts from '@features/Fair/texts/fair.json'; +import { message } from 'antd'; +import { useCallback, useState } from 'react'; import '../styles.css'; import { FairDocHeader } from '@features/Fair/components/FairDocHeader.tsx'; import { FairPrinciplesCollapse } from '@features/Fair/components/FairPrinciplesCollapse.tsx'; import { FairSubmissionProgress } from '@features/Fair/components/FairSubmissionProgress.tsx'; +import { + submitFairCertification, + useFairCertification, +} from '@features/Fair/hooks/useFairCertification'; import type { FairCertificationQuestion } from '@features/Fair/types/fairCertification.types'; +import { useDataProviderStore } from '@/store/dataProviderStore'; export type ApprovedFairViewProps = { certificationQuestions?: FairCertificationQuestion[]; }; export const ApprovedFairView = ({ certificationQuestions }: ApprovedFairViewProps) => { + const { selectedDataProvider } = useDataProviderStore(); + const dataProviderId = selectedDataProvider?.id; + const { mutate } = useFairCertification(); + const [isSubmitting, setIsSubmitting] = useState(false); + const { submitSuccessMessage, submitErrorMessage } = fairTexts.principlesAccordion; + + const handleSubmit = useCallback(async () => { + if (!dataProviderId) { + return; + } + + setIsSubmitting(true); + + try { + await submitFairCertification(dataProviderId); + await mutate(); + message.success(submitSuccessMessage); + } catch { + message.error(submitErrorMessage); + } finally { + setIsSubmitting(false); + } + }, [dataProviderId, mutate, submitErrorMessage, submitSuccessMessage]); + // const { selectedDataProvider, selectedSetSpec, statistics, doiStatistics } = useDataProviderStore(); // const rorId = selectedDataProvider?.rorData?.rorId ?? null; // const { rioxx } = useRioxxStats(selectedDataProvider?.id); @@ -46,6 +78,8 @@ export const ApprovedFairView = ({ certificationQuestions }: ApprovedFairViewPro diff --git a/src/features/Fair/components/FairPrinciplesCollapse.tsx b/src/features/Fair/components/FairPrinciplesCollapse.tsx index 239b8b7f..e4413b13 100644 --- a/src/features/Fair/components/FairPrinciplesCollapse.tsx +++ b/src/features/Fair/components/FairPrinciplesCollapse.tsx @@ -19,7 +19,8 @@ import '../styles.css'; export type FairPrinciplesCollapseProps = { onSave?: () => void; - onSubmit?: () => void; + onSubmit?: () => void | Promise; + isSubmitting?: boolean; /** Collapsible presentation: `default` (full FAIR styling) or `compact` (tighter panels). */ collapsibleVariant?: 'default' | 'compact'; certificationQuestions?: FairCertificationQuestion[]; @@ -32,6 +33,7 @@ const {Title, Paragraph} = Typography; export const FairPrinciplesCollapse = ({ onSave, onSubmit, + isSubmitting = false, collapsibleVariant = 'default', certificationQuestions, // repositoryStatus, @@ -46,7 +48,7 @@ export const FairPrinciplesCollapse = ({ }; const handleSubmit = () => { - onSubmit?.(); + void onSubmit?.(); }; const openQuestionLabel = principlesAccordion.openQuestionBadge ?? 'Open question'; @@ -120,7 +122,9 @@ export const FairPrinciplesCollapse = ({
{/* TODO enable based on condition */} diff --git a/src/features/Fair/components/FairPrinciplesCollapse.tsx b/src/features/Fair/components/FairPrinciplesCollapse.tsx index e4413b13..e889b13f 100644 --- a/src/features/Fair/components/FairPrinciplesCollapse.tsx +++ b/src/features/Fair/components/FairPrinciplesCollapse.tsx @@ -134,3 +134,4 @@ export const FairPrinciplesCollapse = ({
); }; + diff --git a/src/features/Fair/store/fairCertificationStore.ts b/src/features/Fair/store/fairCertificationStore.ts new file mode 100644 index 00000000..d49c0fee --- /dev/null +++ b/src/features/Fair/store/fairCertificationStore.ts @@ -0,0 +1,29 @@ +import { create } from 'zustand'; +import { persist } from 'zustand/middleware'; + +interface FairCertificationStore { + registeredInterestByProvider: Record; + grantApprovedAccess: (dataProviderId: number) => void; + hasRegisteredInterest: (dataProviderId: number) => boolean; +} + +export const useFairCertificationStore = create()( + persist( + (set, get) => ({ + registeredInterestByProvider: {}, + + grantApprovedAccess: (dataProviderId) => { + set((state) => ({ + registeredInterestByProvider: { + ...state.registeredInterestByProvider, + [dataProviderId]: true, + }, + })); + }, + + hasRegisteredInterest: (dataProviderId) => + Boolean(get().registeredInterestByProvider[dataProviderId]), + }), + { name: 'fair-certification-store' }, + ), +); diff --git a/src/pages/FAIRCertification.tsx b/src/pages/FAIRCertification.tsx index 4a8c4a28..80ac91b4 100644 --- a/src/pages/FAIRCertification.tsx +++ b/src/pages/FAIRCertification.tsx @@ -1,9 +1,18 @@ import { useDocumentTitle } from '@hooks/useDocumentTitle.ts'; -import { useState, useEffect, useRef } from 'react'; +import { useState, useEffect, useRef, useCallback } from 'react'; import retrieveContent from '@/utils/retrieveContent'; import { FairFeature, type FairCertificationData } from '@features/Fair/Fair.tsx'; -// import { ApprovedFairView } from '@features/Fair/components/ApprovedFairView.tsx'; +import { ApprovedFairView } from '@features/Fair/components/ApprovedFairView.tsx'; import { useFairCertification } from '@features/Fair/hooks/useFairCertification'; +import { useOrganisation } from '@features/Settings/OrganisationalSettings/hooks/useOrganisation'; +import { useStartingOrSupportingBillingPlanData } from '@features/Orcid/hooks/useStartingOrSupportingBillingPlanData'; +import { useFairCertificationStore } from '@features/Fair/store/fairCertificationStore'; +import { useDataProviderStore } from '@/store/dataProviderStore'; + +const FAIR_REGISTER_INTEREST_FORM_URL = + 'https://docs.google.com/forms/d/e/1FAIpQLScVAzXyEoPNBno9qorv2pQU9QmUalagtcoRn9Tze4V5TQZ1Pw/viewform?usp=dialog'; + +const SUCCESS_MESSAGE_DURATION_MS = 5000; const loadFairCertification = async (ref?: string): Promise => { return (await retrieveContent('fair-certification', { @@ -14,17 +23,43 @@ const loadFairCertification = async (ref?: string): Promise state.grantApprovedAccess); + const hasRegisteredInterest = useFairCertificationStore((state) => + selectedDataProvider?.id ? state.hasRegisteredInterest(selectedDataProvider.id) : false, + ); const [stateData, setStateData] = useState(null); + const [showSuccessMessage, setShowSuccessMessage] = useState(false); const hasLoadedRef = useRef(false); - // TODO test + + const dataProviderId = selectedDataProvider?.id; + + const hasApprovedAccess = + hasRegisteredInterest || + (fairCertification?.status != null && fairCertification.status !== 'not_certified'); + + const handleRegisterInterest = useCallback(() => { + if (dataProviderId) { + grantApprovedAccess(dataProviderId); + } + + setShowSuccessMessage(true); + }, [dataProviderId, grantApprovedAccess]); + useEffect(() => { - if (isFairCertificationLoading) { + if (!showSuccessMessage) { return; } - console.log('fairCertification', fairCertification?.questions); - }, [fairCertification, isFairCertificationLoading]); + const timer = setTimeout(() => { + setShowSuccessMessage(false); + }, SUCCESS_MESSAGE_DURATION_MS); + + return () => clearTimeout(timer); + }, [showSuccessMessage]); useEffect(() => { if (hasLoadedRef.current) { @@ -41,10 +76,19 @@ export function FAIRCertificationPage() { if (!stateData) { return null; } - // TODO RENDER VIEW BASED ON STATUS + + if (hasApprovedAccess && !showSuccessMessage) { + return ; + } + + console.log(isStartingOrSupportingPlan, "isStartingOrSupportingPlan") return ( - // true ? : - + ); } From 6abda5a5935e8b556ed5fa9496383e360798e3db Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Fri, 17 Jul 2026 17:03:15 +0400 Subject: [PATCH 44/66] CORE: V2 input auto save fix --- .../components/FairPrincipleQuestionBlock.tsx | 56 +++++++------------ .../components/FairPrinciplesCollapse.tsx | 15 ++--- 2 files changed, 25 insertions(+), 46 deletions(-) diff --git a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx index 9948c4a2..bb8189f6 100644 --- a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx +++ b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx @@ -1,17 +1,17 @@ import {InfoOutlined, FileTextOutlined} from '@ant-design/icons'; import {Markdown, PercentBar} from '@oacore/core-ui'; -import {Form, Input, message} from 'antd'; +import {Input, message} from 'antd'; import {formatNumber} from '@utils/helpers.ts'; import type {FocusEvent} from 'react'; import type {FairQuestionItem} from '@features/Fair/types/fairPrinciples.types'; -import { updateFairCertificationAnswer } from '@features/Fair/hooks/useFairCertification'; -import { useDataProviderStore } from '@/store/dataProviderStore'; +import {updateFairCertificationAnswer} from '@features/Fair/hooks/useFairCertification'; +import {useDataProviderStore} from '@/store/dataProviderStore'; import '../../Usrn/style.css'; import '../styles.css'; -import { getFairQuestionStatusClassName } from '@features/Fair/utils/getFairQuestionStatusClassName'; -import { resolveFairQuestionStatusLabel } from '@features/Fair/utils/resolveFairQuestionStatusLabel'; +import {getFairQuestionStatusClassName} from '@features/Fair/utils/getFairQuestionStatusClassName'; +import {resolveFairQuestionStatusLabel} from '@features/Fair/utils/resolveFairQuestionStatusLabel'; export type FairPrincipleQuestionBlockProps = { item: FairQuestionItem; @@ -25,11 +25,13 @@ export const FairPrincipleQuestionBlock = ({ recommendationHeading, openQuestionLabel, }: FairPrincipleQuestionBlockProps) => { - const { selectedDataProvider } = useDataProviderStore(); + const {selectedDataProvider} = useDataProviderStore(); const dataProviderId = selectedDataProvider?.id; const isOpenQuestion = Boolean(item.openQuestion); const questionId = item.certificationQuestion?.id; + console.log(item.certificationQuestion?.id, "item.certificationQuestion?.id;") + const handleAnswerBlur = (event: FocusEvent) => { if (!dataProviderId || !questionId) { return; @@ -54,7 +56,7 @@ export const FairPrincipleQuestionBlock = ({

{item.question}

- + {statusLabel} @@ -72,44 +74,24 @@ export const FairPrincipleQuestionBlock = ({ ) : null} - {/*{showCounterRows && counterRows*/} - {/* ? counterRows.map((row, index) => (*/} - {/* */} - {/* {row.label}*/} - {/* */} - {/* {formatNumber(row.value)}*/} - {/* */} - {/* */} - {/* ))*/} - {/* : null}*/} -
{counterLabelText} {formatNumber(32)}
- + {isOpenQuestion ? (
- - - + defaultValue={item.certificationQuestion?.answer ?? ''} + disabled={!questionId} + onBlur={handleAnswerBlur} + placeholder={item.answerPlaceholder ?? 'Write your answer here …'} + rows={4} + />
) : null} diff --git a/src/features/Fair/components/FairPrinciplesCollapse.tsx b/src/features/Fair/components/FairPrinciplesCollapse.tsx index e889b13f..c5024ca7 100644 --- a/src/features/Fair/components/FairPrinciplesCollapse.tsx +++ b/src/features/Fair/components/FairPrinciplesCollapse.tsx @@ -12,7 +12,7 @@ import { type FairQuestionItem, } from '@features/Fair/types/fairPrinciples.types'; // import type { FairRepositoryStatusParams } from '@features/Fair/utils/resolveFairQuestionStatus'; -import {Button, Form, Typography} from 'antd'; +import {Button, Typography} from 'antd'; import {useMemo} from 'react'; import '../styles.css'; @@ -39,7 +39,6 @@ export const FairPrinciplesCollapse = ({ // repositoryStatus, }: FairPrinciplesCollapseProps) => { const {principlesAccordion} = fairTexts; - const [openAnswersForm] = Form.useForm(); const recommendationHeading = principlesAccordion.recommendationHeading ?? 'Recommendation'; @@ -103,13 +102,11 @@ export const FairPrinciplesCollapse = ({ aria-label={principlesAccordion.sectionAriaLabel} className="fair-principles-accordion-section" > -
- - +
- -
-
-
-

{approvedView.title}

- - {approvedView.certificationDescription} - - - {approvedView.lastReportUpdateLine} - - - {approvedView.submissionsLine} - -
- {/**/} - -
- + <> +
+ + +
+
+
+

{approvedView.title}

+ + {approvedView.certificationDescription} + + + {`${approvedView.issued} ${formatIsoDate(certificate?.issueDate)}`} + + + {`${approvedView.valid} ${formatIsoDate(certificate?.validUntil)}`} + + + {`${approvedView.lastReportUpdateLine} ${formatIsoDate(certificate?.reviewedAt)}`} + + + {`${approvedView.submissionsLine}${certificationQuestions?.numberOfSubmissions ?? ''}`} + +
+ {/**/} + +
+ ); }; diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json index 67c6f7d1..28e6e81c 100644 --- a/src/features/Fair/texts/fair.json +++ b/src/features/Fair/texts/fair.json @@ -9,8 +9,10 @@ "approvedView": { "title": "FAIR certification: Not certified", "certificationDescription": "*Your repository is not certified yet. In the report below you can review the estimation to what extent your repository correspond to the FAIR principles.*", - "lastReportUpdateLine": "**Date of the last report update:** 26.05.2025", - "submissionsLine": "**Number of submissions:** 0", + "issued": "**Certificate issued:**", + "valid": "**Certificate valid until:**", + "lastReportUpdateLine": "**Date of the last report update:**", + "submissionsLine": "**Number of submissions:** ", "aboutButtonLabel": "About CORE FAIR Certification", "downloadReportButtonLabel": "Download report" }, diff --git a/src/features/Fair/types/fairCertification.types.ts b/src/features/Fair/types/fairCertification.types.ts index 1a1defd1..39e993c5 100644 --- a/src/features/Fair/types/fairCertification.types.ts +++ b/src/features/Fair/types/fairCertification.types.ts @@ -1,15 +1,16 @@ -export type FairCertificationStatus = - | 'not_certified' - | 'pending' - | 'certified' +export type FairCertificationWorkflowStatus = string; + +export type FairCertificationCertificateStatus = + | 'valid' | 'expired' + | 'not_certified' | string; export type FairCertificationLevel = - | 'BRONZE' - | 'SILVER' - | 'GOLD' - | 'PLATINUM' + | 'bronze' + | 'silver' + | 'gold' + | 'platinum' | string; export type FairCertificationQuestionResultStatus = 'pass' | 'fail' | 'unknown' | string; @@ -19,7 +20,10 @@ export type FairCertificationQuestionCounts = Record export type FairCertificationQuestionMetrics = Record; export type FairCertificationQuestionResult = { + value?: boolean | string | number; status?: FairCertificationQuestionResultStatus; + source?: string; + checkedAt?: string; metrics?: FairCertificationQuestionMetrics; counts?: FairCertificationQuestionCounts; evidence?: Record; @@ -28,26 +32,31 @@ export type FairCertificationQuestionResult = { value?: number; unit?: string; }; - source?: string; - checkedAt?: string; }; export type FairCertificationQuestion = { id: string; - number?: string; - section?: string; - type?: string; - required?: boolean; - question?: string; - description?: string; - recommendation?: string; + number: string; + section: string; + type: string; + required: boolean; + question: string; + description: string; + recommendation: string; answer?: string; result?: FairCertificationQuestionResult; }; +export type FairCertificationRepository = { + repositoryId: number | string; + repositoryName: string; + organisationName: string; + countryCode: string; +}; + export type FairCertificationSubmissionReviewStatus = 'pending' | string; -export type FairCertificationSubmissionResponse = { +export type FairCertificationSubmission = { id: number; submissionNumber: number; submittedBy: string; @@ -56,17 +65,36 @@ export type FairCertificationSubmissionResponse = { pdfGeneratedAt: string; reportUrl: string; reviewStatus: FairCertificationSubmissionReviewStatus; - snapshot: Record; + reviewedBy: string | null; + reviewedAt: string | null; + reviewNotes: string | null; +}; + +export type FairCertificationApiCertificate = { + repositoryId: number; + repositoryName: string; + organisationName: string; + countryCode: string; + certificateId: string; + level: FairCertificationLevel; + issueDate: string; + validUntil: string; + status: FairCertificationCertificateStatus; + certificateUrl: string; + reportUrl: string; + reviewedBy: string; + reviewedAt: string; }; export type FairCertificationApiResponse = { - status?: FairCertificationStatus; - level?: FairCertificationLevel | null; - repositoryName?: string; - issueDate?: string | null; - validUntil?: string | null; - lastReportUpdate?: string | null; - submissionCount?: number | null; - questions?: FairCertificationQuestion[]; - [key: string]: unknown; + repositoryId: number; + repository: FairCertificationRepository; + workflowStatus: FairCertificationWorkflowStatus; + certificationLevel: FairCertificationLevel; + questionSetVersion: string; + lastAssessmentAt: string; + numberOfSubmissions: number; + questions: FairCertificationQuestion[]; + submissions: FairCertificationSubmission[]; + certificate: FairCertificationApiCertificate | null; }; diff --git a/src/pages/FAIRCertification.tsx b/src/pages/FAIRCertification.tsx index 80ac91b4..bc8d0aa0 100644 --- a/src/pages/FAIRCertification.tsx +++ b/src/pages/FAIRCertification.tsx @@ -37,9 +37,12 @@ export function FAIRCertificationPage() { const dataProviderId = selectedDataProvider?.id; + const certificationStatus = + fairCertification?.certificate?.status ?? fairCertification?.workflowStatus; + const hasApprovedAccess = hasRegisteredInterest || - (fairCertification?.status != null && fairCertification.status !== 'not_certified'); + (certificationStatus != null && certificationStatus !== 'not_certified'); const handleRegisterInterest = useCallback(() => { if (dataProviderId) { @@ -77,12 +80,11 @@ export function FAIRCertificationPage() { return null; } + // TODO might need to redo for demo if (hasApprovedAccess && !showSuccessMessage) { - return ; + return ; } - console.log(isStartingOrSupportingPlan, "isStartingOrSupportingPlan") - return ( { if (!dateString) return 'N/A'; @@ -10,3 +12,12 @@ export const formatDate = (dateString: string): string => { year: 'numeric', }); }; + +export const formatIsoDate = (dateString?: string | null): string => { + if (!dateString) return ''; + + const parsed = dayjs(dateString); + if (!parsed.isValid()) return ''; + + return parsed.format('YYYY-MM-DD'); +}; From e2e32fb8717ecd5c03a70c0b350979e17051401e Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Mon, 20 Jul 2026 17:36:55 +0400 Subject: [PATCH 46/66] CORE: V2 add certificate view data --- .../Fair/components/FairCertificateView.tsx | 60 ++++++------------- .../Fair/components/FairDocHeader.tsx | 9 ++- src/features/Fair/texts/fair.json | 11 ++++ 3 files changed, 34 insertions(+), 46 deletions(-) diff --git a/src/features/Fair/components/FairCertificateView.tsx b/src/features/Fair/components/FairCertificateView.tsx index 1e9e928b..93fdf892 100644 --- a/src/features/Fair/components/FairCertificateView.tsx +++ b/src/features/Fair/components/FairCertificateView.tsx @@ -1,44 +1,18 @@ import coreLogo from '@/assets/img/fairCertificateCoreLogo.svg'; import certificateSeal from '@/assets/img/fairCertificateSeal.svg'; - -export type FairCertificateData = { - title: string; - level: string; - presentedLabel: string; - repositoryName: string; - description: string; - signatoryName: string; - signatoryTitle: string; - infoUrl: string; - issueDateLabel: string; - issueDate: string; - validUntilLabel: string; - validUntil: string; -}; +import fairTexts from '@features/Fair/texts/fair.json'; +import type { FairCertificationApiCertificate } from '@features/Fair/types/fairCertification.types'; export type FairCertificateViewProps = { - data?: FairCertificateData; + certificationData?: FairCertificationApiCertificate | null; }; -const DUMMY_CERTIFICATE_DATA: FairCertificateData = { - title: 'CERTIFICATE', - level: 'BRONZE', - presentedLabel: 'PROUDLY PRESENTED TO :', - repositoryName: 'Open Research Online', - description: - "In recognition of the repository's demonstrated commitment to best practice data stewardship, attested by its compliance with the CORE FAIR Certification requirements at the Bronze level. This certificate signifies that the records managed herein meet the globally recognized standards for Findability, Accessibility, Interoperability, and Reusability (FAIR), ensuring long-term utility and transparency for the Open Access community.", - signatoryName: 'Petr Knoth', - signatoryTitle: 'CEO of CORE', - infoUrl: 'For more information visit https://core.ac.uk/', - issueDateLabel: 'Issue Date:', - issueDate: '12/03/2023', - validUntilLabel: 'Valid until:', - validUntil: '12/03/2024', -}; export const FairCertificateView = ({ - data = DUMMY_CERTIFICATE_DATA, + certificationData, }: FairCertificateViewProps) => { + const { certificate } = fairTexts; + return (
-

{data.title}

-

{data.level}

-

{data.presentedLabel}

+

{certificate.title}

+

{certificationData?.level}

+

{certificate.presentedLabel}

-

{data.repositoryName}

+

{certificationData?.repositoryName}

-

{data.description}

+

{certificate.description}

-

{data.signatoryName}

-

{data.signatoryTitle}

+

{certificate.signatoryName}

+

{certificate.signatoryTitle}

- {data.issueDateLabel} - {data.issueDate} + {certificate.issueDateLabel} + {certificationData?.issueDate}

- {data.validUntilLabel}{data.validUntil} + {certificate.validUntilLabel}{certificationData?.validUntil}

-

{data.infoUrl}

+

{certificate.infoUrl}

diff --git a/src/features/Fair/components/FairDocHeader.tsx b/src/features/Fair/components/FairDocHeader.tsx index c332beed..f8446526 100644 --- a/src/features/Fair/components/FairDocHeader.tsx +++ b/src/features/Fair/components/FairDocHeader.tsx @@ -1,7 +1,7 @@ import fairTexts from '@features/Fair/texts/fair.json'; import { Markdown } from '@oacore/core-ui'; import { Button } from 'antd'; -// import placeholder from '@/assets/img/certificatePlaceholder.svg'; +import placeholder from '@/assets/img/certificatePlaceholder.svg'; import { FairCertificateView } from '@features/Fair/components/FairCertificateView.tsx'; import type { FairCertificationApiResponse } from '@features/Fair/types/fairCertification.types'; import { formatIsoDate } from '@/utils/dateUtils'; @@ -50,8 +50,11 @@ export const FairDocHeader = ({ certificationQuestions }: FairDocHeaderProps) => {`${approvedView.submissionsLine}${certificationQuestions?.numberOfSubmissions ?? ''}`} - {/**/} - + {certificationQuestions?.certificate ? + + : + + } ); diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json index 28e6e81c..592c72fc 100644 --- a/src/features/Fair/texts/fair.json +++ b/src/features/Fair/texts/fair.json @@ -362,5 +362,16 @@ } ] } + }, + "certificate": { + "title": "CERTIFICATE", + "presentedLabel": "PROUDLY PRESENTED TO :", + "description": + "In recognition of the repository's demonstrated commitment to best practice data stewardship, attested by its compliance with the CORE FAIR Certification requirements at the Bronze level. This certificate signifies that the records managed herein meet the globally recognized standards for Findability, Accessibility, Interoperability, and Reusability (FAIR), ensuring long-term utility and transparency for the Open Access community.", + "signatoryName": "Petr Knoth", + "signatoryTitle": "CEO of CORE", + "infoUrl": "For more information visit https://core.ac.uk/", + "issueDateLabel": "Issue Date:", + "validUntilLabel": "Issue Date:" } } From 43da12b1225ce7a36bdda582f7c4b060410ee332 Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Mon, 20 Jul 2026 18:09:54 +0400 Subject: [PATCH 47/66] CORE: V2 download certificate png --- package.json | 3 +- pnpm-lock.yaml | 21 +++++++ .../Fair/components/FairCertificateView.tsx | 27 +++++---- .../Fair/components/FairDocHeader.tsx | 60 +++++++++++++++++-- src/features/Fair/texts/fair.json | 6 +- .../Fair/utils/downloadFairCertificatePng.ts | 28 +++++++++ 6 files changed, 125 insertions(+), 20 deletions(-) create mode 100644 src/features/Fair/utils/downloadFairCertificatePng.ts diff --git a/package.json b/package.json index 09765f34..42486b74 100644 --- a/package.json +++ b/package.json @@ -23,14 +23,15 @@ "prepare": "husky" }, "dependencies": { - "@oacore/core-ui": "^2.0.16", "@ant-design/icons": "^5.5.1", + "@oacore/core-ui": "^2.0.16", "@octokit/rest": "^22.0.1", "antd": "^6.3.1", "axios": "^1.13.6", "classnames": "^2.5.1", "dayjs": "^1.11.19", "front-matter": "^4.0.2", + "html-to-image": "^1.11.13", "js-cookie": "^3.0.5", "js-yaml": "^4.1.1", "react": "^19.2.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 2aae3c63..38fec89b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: front-matter: specifier: ^4.0.2 version: 4.0.2 + html-to-image: + specifier: ^1.11.13 + version: 1.11.13 js-cookie: specifier: ^3.0.5 version: 3.0.5 @@ -881,66 +884,79 @@ packages: resolution: {integrity: sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==} cpu: [arm] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm-musleabihf@4.59.0': resolution: {integrity: sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==} cpu: [arm] os: [linux] + libc: [musl] '@rollup/rollup-linux-arm64-gnu@4.59.0': resolution: {integrity: sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==} cpu: [arm64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-arm64-musl@4.59.0': resolution: {integrity: sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==} cpu: [arm64] os: [linux] + libc: [musl] '@rollup/rollup-linux-loong64-gnu@4.59.0': resolution: {integrity: sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==} cpu: [loong64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-loong64-musl@4.59.0': resolution: {integrity: sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==} cpu: [loong64] os: [linux] + libc: [musl] '@rollup/rollup-linux-ppc64-gnu@4.59.0': resolution: {integrity: sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-ppc64-musl@4.59.0': resolution: {integrity: sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==} cpu: [ppc64] os: [linux] + libc: [musl] '@rollup/rollup-linux-riscv64-gnu@4.59.0': resolution: {integrity: sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==} cpu: [riscv64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-riscv64-musl@4.59.0': resolution: {integrity: sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==} cpu: [riscv64] os: [linux] + libc: [musl] '@rollup/rollup-linux-s390x-gnu@4.59.0': resolution: {integrity: sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==} cpu: [s390x] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-gnu@4.59.0': resolution: {integrity: sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==} cpu: [x64] os: [linux] + libc: [glibc] '@rollup/rollup-linux-x64-musl@4.59.0': resolution: {integrity: sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==} cpu: [x64] os: [linux] + libc: [musl] '@rollup/rollup-openbsd-x64@4.59.0': resolution: {integrity: sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==} @@ -1602,6 +1618,9 @@ packages: hermes-parser@0.25.1: resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} + html-to-image@1.11.13: + resolution: {integrity: sha512-cuOPoI7WApyhBElTTb9oqsawRvZ0rHhaHwghRLlTuffoD1B2aDemlCruLeZrUIIdvG7gs9xeELEPm6PhuASqrg==} + html-url-attributes@3.0.1: resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} @@ -3903,6 +3922,8 @@ snapshots: dependencies: hermes-estree: 0.25.1 + html-to-image@1.11.13: {} + html-url-attributes@3.0.1: {} html-void-elements@3.0.0: {} diff --git a/src/features/Fair/components/FairCertificateView.tsx b/src/features/Fair/components/FairCertificateView.tsx index 93fdf892..f8429ec9 100644 --- a/src/features/Fair/components/FairCertificateView.tsx +++ b/src/features/Fair/components/FairCertificateView.tsx @@ -1,3 +1,5 @@ +import { forwardRef } from 'react'; + import coreLogo from '@/assets/img/fairCertificateCoreLogo.svg'; import certificateSeal from '@/assets/img/fairCertificateSeal.svg'; import fairTexts from '@features/Fair/texts/fair.json'; @@ -7,16 +9,15 @@ export type FairCertificateViewProps = { certificationData?: FairCertificationApiCertificate | null; }; +export const FairCertificateView = forwardRef( + ({ certificationData }, ref) => { + const { certificate } = fairTexts; -export const FairCertificateView = ({ - certificationData, -}: FairCertificateViewProps) => { - const { certificate } = fairTexts; - - return ( -
+ return ( +
-
- ); -}; + + ); + }); + +FairCertificateView.displayName = 'FairCertificateView'; diff --git a/src/features/Fair/components/FairDocHeader.tsx b/src/features/Fair/components/FairDocHeader.tsx index f8446526..34809587 100644 --- a/src/features/Fair/components/FairDocHeader.tsx +++ b/src/features/Fair/components/FairDocHeader.tsx @@ -1,9 +1,15 @@ +import { useCallback, useRef, useState } from 'react'; + import fairTexts from '@features/Fair/texts/fair.json'; import { Markdown } from '@oacore/core-ui'; -import { Button } from 'antd'; +import { Button, message } from 'antd'; import placeholder from '@/assets/img/certificatePlaceholder.svg'; import { FairCertificateView } from '@features/Fair/components/FairCertificateView.tsx'; import type { FairCertificationApiResponse } from '@features/Fair/types/fairCertification.types'; +import { + buildFairCertificatePngFilename, + downloadFairCertificatePng, +} from '@features/Fair/utils/downloadFairCertificatePng'; import { formatIsoDate } from '@/utils/dateUtils'; import '../styles.css'; @@ -14,22 +20,63 @@ export type FairDocHeaderProps = { export const FairDocHeader = ({ certificationQuestions }: FairDocHeaderProps) => { const { approvedView } = fairTexts; const certificate = certificationQuestions?.certificate; + const certificateRef = useRef(null); + const [isDownloadingPng, setIsDownloadingPng] = useState(false); + + const handleDownloadPng = useCallback(async () => { + if (!certificateRef.current) { + return; + } + + setIsDownloadingPng(true); + + try { + const filename = buildFairCertificatePngFilename(certificate?.repositoryName); + await downloadFairCertificatePng(certificateRef.current, filename); + } catch { + message.error(approvedView.downloadPngErrorMessage); + } finally { + setIsDownloadingPng(false); + } + }, [approvedView.downloadPngErrorMessage, certificate?.repositoryName]); return ( <>
+ {/*TODO*/} + {certificationQuestions?.certificate && + <> + {/*TODO*/} + + {/*TODO*/} + + + }
@@ -51,9 +98,12 @@ export const FairDocHeader = ({ certificationQuestions }: FairDocHeaderProps) =>
{certificationQuestions?.certificate ? - + : - + }
diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json index 592c72fc..5747f73a 100644 --- a/src/features/Fair/texts/fair.json +++ b/src/features/Fair/texts/fair.json @@ -14,6 +14,9 @@ "lastReportUpdateLine": "**Date of the last report update:**", "submissionsLine": "**Number of submissions:** ", "aboutButtonLabel": "About CORE FAIR Certification", + "downloadPNG": "Download badge in PNG", + "downloadPngErrorMessage": "Failed to download the certificate badge. Please try again.", + "downloadPdf": "Download certificate in PDF", "downloadReportButtonLabel": "Download report" }, "principlesAccordion": { @@ -366,8 +369,7 @@ "certificate": { "title": "CERTIFICATE", "presentedLabel": "PROUDLY PRESENTED TO :", - "description": - "In recognition of the repository's demonstrated commitment to best practice data stewardship, attested by its compliance with the CORE FAIR Certification requirements at the Bronze level. This certificate signifies that the records managed herein meet the globally recognized standards for Findability, Accessibility, Interoperability, and Reusability (FAIR), ensuring long-term utility and transparency for the Open Access community.", + "description": "In recognition of the repository's demonstrated commitment to best practice data stewardship, attested by its compliance with the CORE FAIR Certification requirements at the Bronze level. This certificate signifies that the records managed herein meet the globally recognized standards for Findability, Accessibility, Interoperability, and Reusability (FAIR), ensuring long-term utility and transparency for the Open Access community.", "signatoryName": "Petr Knoth", "signatoryTitle": "CEO of CORE", "infoUrl": "For more information visit https://core.ac.uk/", diff --git a/src/features/Fair/utils/downloadFairCertificatePng.ts b/src/features/Fair/utils/downloadFairCertificatePng.ts new file mode 100644 index 00000000..e124e610 --- /dev/null +++ b/src/features/Fair/utils/downloadFairCertificatePng.ts @@ -0,0 +1,28 @@ +import { toPng } from 'html-to-image'; + +import { downloadFile } from '@/utils/downloadUtils'; + +const sanitizeFilenamePart = (value: string): string => + value + .trim() + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-+|-+$/g, ''); + +export const buildFairCertificatePngFilename = (repositoryName?: string | null): string => { + const repositoryPart = repositoryName ? sanitizeFilenamePart(repositoryName) : 'repository'; + + return `fair-certification-badge-${repositoryPart}.png`; +}; + +export const downloadFairCertificatePng = async ( + element: HTMLElement, + filename: string, +): Promise => { + const dataUrl = await toPng(element, { + cacheBust: true, + pixelRatio: 2, + }); + + downloadFile(dataUrl, filename); +}; From c8ca12dc26d51be43e68985cc279e3162892ff08 Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Tue, 21 Jul 2026 10:56:38 +0400 Subject: [PATCH 48/66] CORE: V2 header responsive --- .../Fair/components/FairDocHeader.tsx | 10 +- src/features/Fair/styles.css | 95 +++++++++++++++---- 2 files changed, 85 insertions(+), 20 deletions(-) diff --git a/src/features/Fair/components/FairDocHeader.tsx b/src/features/Fair/components/FairDocHeader.tsx index 34809587..0862ad5e 100644 --- a/src/features/Fair/components/FairDocHeader.tsx +++ b/src/features/Fair/components/FairDocHeader.tsx @@ -52,12 +52,12 @@ export const FairDocHeader = ({ certificationQuestions }: FairDocHeaderProps) => {/*TODO*/} - {certificationQuestions?.certificate && + {certificationQuestions?.certificate && <> {/*TODO*/} diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json index 55adecc2..70dc781d 100644 --- a/src/features/Fair/texts/fair.json +++ b/src/features/Fair/texts/fair.json @@ -16,6 +16,7 @@ "downloadPNG": "Download badge in PNG", "downloadPngErrorMessage": "Failed to download the certificate badge. Please try again.", "downloadPdf": "Download certificate in PDF", + "downloadPdfErrorMessage": "Failed to download the certificate in PDF. Please try again.", "downloadReportButtonLabel": "Download report" }, "principlesAccordion": { diff --git a/src/features/Fair/utils/downloadFairCertificatePdf.ts b/src/features/Fair/utils/downloadFairCertificatePdf.ts new file mode 100644 index 00000000..d087d2c8 --- /dev/null +++ b/src/features/Fair/utils/downloadFairCertificatePdf.ts @@ -0,0 +1,29 @@ +import { toPng } from 'html-to-image'; +import { jsPDF } from 'jspdf'; + +import { buildFairCertificatePngFilename } from '@features/Fair/utils/downloadFairCertificatePng'; + +export const buildFairCertificatePdfFilename = (repositoryName?: string | null): string => + buildFairCertificatePngFilename(repositoryName).replace(/\.png$/, '.pdf'); + +export const downloadFairCertificatePdf = async ( + element: HTMLElement, + filename: string, +): Promise => { + const dataUrl = await toPng(element, { + cacheBust: true, + pixelRatio: 2, + }); + + const width = element.offsetWidth; + const height = element.offsetHeight; + + const pdf = new jsPDF({ + orientation: width > height ? 'landscape' : 'portrait', + unit: 'px', + format: [width, height], + }); + + pdf.addImage(dataUrl, 'PNG', 0, 0, width, height); + pdf.save(filename); +}; From e1f8fb0830a8b2dcf0ffeb873a3ac0f219b3afe4 Mon Sep 17 00:00:00 2001 From: ekachxaidze98 Date: Wed, 22 Jul 2026 12:16:09 +0400 Subject: [PATCH 58/66] CORE: add report url --- src/features/Fair/components/FairDocHeader.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/features/Fair/components/FairDocHeader.tsx b/src/features/Fair/components/FairDocHeader.tsx index d478e538..fb2428f4 100644 --- a/src/features/Fair/components/FairDocHeader.tsx +++ b/src/features/Fair/components/FairDocHeader.tsx @@ -16,6 +16,7 @@ import { } from '@features/Fair/utils/downloadFairCertificatePdf'; import { formatIsoDate } from '@/utils/dateUtils'; import '../styles.css'; +import {useDataProviderStore} from '@/store/dataProviderStore.ts'; export type FairDocHeaderProps = { certificationQuestions?: FairCertificationApiResponse | null; @@ -23,6 +24,7 @@ export type FairDocHeaderProps = { export const FairDocHeader = ({ certificationQuestions }: FairDocHeaderProps) => { const { approvedView } = fairTexts; + const { selectedDataProvider } = useDataProviderStore(); const certificate = certificationQuestions?.certificate; const certificateRef = useRef(null); const [isDownloadingPng, setIsDownloadingPng] = useState(false); @@ -73,15 +75,15 @@ export const FairDocHeader = ({ certificationQuestions }: FairDocHeaderProps) => {approvedView.aboutButtonLabel} {/*TODO*/} + {/*fair-certification/FAIR-2026-4A717572CF12/report*/} {certificationQuestions?.certificate && <> - {/*TODO*/} - {/*TODO*/}