-
-
+
+
+
+
+
+
+ {/* TODO enable based on condition */}
+ {(feesTable || membersTable) && (
+
+
+ {feesTable ? : null}
+ {membersTable ? : null}
+
+
+ )}
+
+
-
-
-
- How to Get Started
-
-
- {applicationProcess.steps.map((step, index) => (
-
-
-
- {step.step ?? index + 1}
-
-
-
-
-
{step.title}
-
- {step.description}
-
+
+ {applicationProcess.steps.map((step, index) => (
+
+
+
+ {step.step ?? index + 1}
+
+
+
+
+
{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 000000000..7554251ff
--- /dev/null
+++ b/src/features/Fair/components/ApprovedFairView.tsx
@@ -0,0 +1,59 @@
+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 { FairCertificationLoadingView } from '@features/Fair/components/FairCertificationLoadingView.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 {
+ submitFairCertification,
+ useFairCertification,
+} from '@features/Fair/hooks/useFairCertification';
+import { useDataProviderStore } from '@/store/dataProviderStore';
+
+export const ApprovedFairView = () => {
+ const { selectedDataProvider } = useDataProviderStore();
+ const dataProviderId = selectedDataProvider?.id;
+ const { fairCertification, mutate, isLoading } = 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]);
+
+ if (isLoading) {
+ return
;
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+};
diff --git a/src/features/Fair/components/FairCertificateView.tsx b/src/features/Fair/components/FairCertificateView.tsx
new file mode 100644
index 000000000..0452a7ffd
--- /dev/null
+++ b/src/features/Fair/components/FairCertificateView.tsx
@@ -0,0 +1,114 @@
+import { forwardRef } from 'react';
+
+import coreLogo from '@/assets/img/fairCertificateCoreLogo.svg';
+import certificateSealBronze from '@/assets/img/fairCertificateSealBrone.svg';
+import certificateSealSilver from '@/assets/img/fairCertificateSealSilver.svg';
+import certificateSealGold from '@/assets/img/fairCertificateSealGold.svg';
+import fairTexts from '@features/Fair/texts/fair.json';
+import type {
+ FairCertificationApiCertificate,
+ FairCertificationLevel,
+} from '@features/Fair/types/fairCertification.types';
+import { processTemplate } from '@/utils/helpers';
+
+const CERTIFICATE_SEAL_BY_LEVEL: Record
= {
+ bronze: certificateSealBronze,
+ silver: certificateSealSilver,
+ gold: certificateSealGold,
+ platinum: certificateSealGold,
+};
+
+const getCertificateSeal = (level?: FairCertificationLevel | null): string => {
+ if (!level) {
+ return certificateSealBronze;
+ }
+
+ return CERTIFICATE_SEAL_BY_LEVEL[level.toLowerCase() as FairCertificationLevel] ?? certificateSealBronze;
+};
+
+export type FairCertificateViewProps = {
+ certificationData?: FairCertificationApiCertificate | null;
+};
+
+export const FairCertificateView = forwardRef(
+ ({ certificationData }, ref) => {
+ const { certificate } = fairTexts;
+ const certificateDescription = processTemplate(certificate.description, {
+ level: certificationData?.level ?? '',
+ });
+ const certificateSeal = getCertificateSeal(certificationData?.level);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{certificationData?.repositoryName}
+
+
{certificateDescription}
+
+
+
{certificate.infoUrl}{certificationData?.certificateUrl}
+
+
+
+ );
+ });
+
+FairCertificateView.displayName = 'FairCertificateView';
diff --git a/src/features/Fair/components/FairCertificationFeesTable.tsx b/src/features/Fair/components/FairCertificationFeesTable.tsx
new file mode 100644
index 000000000..450acd490
--- /dev/null
+++ b/src/features/Fair/components/FairCertificationFeesTable.tsx
@@ -0,0 +1,71 @@
+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 '../types/fairPricingTypes.ts';
+import { findPrice, formatGbp } from '../hooks/fairPricingUtils.ts';
+
+export type FairCertificationFeesTableProps = {
+ config: FairPricingTable;
+};
+
+type FeesRecord = {
+ key: string;
+ band: ReactNode;
+} & Partial>;
+
+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/components/FairCertificationLoadingView.tsx b/src/features/Fair/components/FairCertificationLoadingView.tsx
new file mode 100644
index 000000000..4b8fd0525
--- /dev/null
+++ b/src/features/Fair/components/FairCertificationLoadingView.tsx
@@ -0,0 +1,21 @@
+import { CrFeatureLayout, CrPaper } from '@oacore/core-ui';
+import { Spin } from 'antd';
+import { LoadingOutlined } from '@ant-design/icons';
+import '../styles.css';
+
+export const FairCertificationLoadingView = () => (
+
+
+
+
} size="large" />
+
Loading...
+
+
+
+);
diff --git a/src/features/Fair/components/FairCertificationMembersFeesTable.tsx b/src/features/Fair/components/FairCertificationMembersFeesTable.tsx
new file mode 100644
index 000000000..37da0e0b8
--- /dev/null
+++ b/src/features/Fair/components/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 '../types/fairPricingTypes.ts';
+import { findPrice, formatGbp } from '../hooks/fairPricingUtils.ts';
+
+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/components/FairDocHeader.tsx b/src/features/Fair/components/FairDocHeader.tsx
new file mode 100644
index 000000000..50e914991
--- /dev/null
+++ b/src/features/Fair/components/FairDocHeader.tsx
@@ -0,0 +1,147 @@
+import { useCallback, useRef, useState } from 'react';
+
+import fairTexts from '@features/Fair/texts/fair.json';
+import { Markdown } from '@oacore/core-ui';
+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 {
+ buildFairCertificatePdfFilename,
+ downloadFairCertificatePdf,
+} 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;
+};
+
+export const FairDocHeader = ({ certificationQuestions }: FairDocHeaderProps) => {
+ const { approvedView } = fairTexts;
+ const { selectedDataProvider } = useDataProviderStore();
+ const certificate = certificationQuestions?.certificate;
+ const certificateRef = useRef(null);
+ const [isDownloadingPng, setIsDownloadingPng] = useState(false);
+ const [isDownloadingPdf, setIsDownloadingPdf] = 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]);
+
+ const handleDownloadPdf = useCallback(async () => {
+ if (!certificateRef.current) {
+ return;
+ }
+
+ setIsDownloadingPdf(true);
+
+ try {
+ const filename = buildFairCertificatePdfFilename(certificate?.repositoryName);
+ await downloadFairCertificatePdf(certificateRef.current, filename);
+ } catch {
+ message.error(approvedView.downloadPdfErrorMessage);
+ } finally {
+ setIsDownloadingPdf(false);
+ }
+ }, [approvedView.downloadPdfErrorMessage, certificate?.repositoryName]);
+
+ return (
+ <>
+
+
+ {approvedView.aboutButtonLabel}
+
+ {/*TODO*/}
+ {/*fair-certification/FAIR-2026-4A717572CF12/report*/}
+
+ {approvedView.downloadReportButtonLabel}
+
+ {certificationQuestions?.certificate &&
+ <>
+
+ {approvedView.downloadPNG}
+
+
+ {approvedView.downloadPdf}
+
+ >
+ }
+
+
+
+
{approvedView.title}{(certificate?.level)}
+
+ {approvedView.certificationDescription}
+
+ {certificate?.issueDate && (
+
+ {`${approvedView.issued} ${formatIsoDate(certificate?.issueDate)}`}
+
+ ) }
+ {certificate?.validUntil && (
+
+ {`${approvedView.valid} ${formatIsoDate(certificate?.validUntil)}`}
+
+ )}
+ {certificate?.reviewedAt && (
+
+ {`${approvedView.lastReportUpdateLine} ${formatIsoDate(certificate?.reviewedAt)}`}
+
+ )}
+
+ {`${approvedView.submissionsLine}${certificationQuestions?.numberOfSubmissions ?? ''}`}
+
+
+ {certificationQuestions?.certificate ?
+
+ :
+
+ }
+
+ >
+ );
+};
diff --git a/src/features/Fair/components/FairPrincipleQuestionBlock.tsx b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx
new file mode 100644
index 000000000..1aed6d5c7
--- /dev/null
+++ b/src/features/Fair/components/FairPrincipleQuestionBlock.tsx
@@ -0,0 +1,148 @@
+import { InfoOutlined, FileTextOutlined } from '@ant-design/icons';
+import { Markdown, PercentBar } from '@oacore/core-ui';
+import { Input, message, notification } 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 '../styles.css';
+import { formatFairResultName } from '@features/Fair/utils/formatFairResultName';
+import { getFairQuestionStatusClassName } from '@features/Fair/utils/getFairQuestionStatusClassName';
+import { resolveFairQuestionCountValues } from '@features/Fair/utils/resolveFairQuestionCountValues';
+import { resolveFairQuestionMetricValues } from '@features/Fair/utils/resolveFairQuestionMetricValues';
+import { isFairOpenQuestion } from '@features/Fair/utils/isFairOpenQuestion';
+import { resolveFairQuestionStatusLabel } from '@features/Fair/utils/resolveFairQuestionStatusLabel';
+
+export type FairPrincipleQuestionBlockProps = {
+ item: FairQuestionItem;
+ showResultCounts?: boolean;
+ recommendationHeading: string;
+ openQuestionLabel: string;
+ answerSavedMessage: string;
+ answerSaveErrorMessage: string;
+ // repositoryStatus?: FairRepositoryStatusParams | null;
+};
+
+export const FairPrincipleQuestionBlock = ({
+ item,
+ showResultCounts = false,
+ recommendationHeading,
+ openQuestionLabel,
+ answerSavedMessage,
+ answerSaveErrorMessage,
+}: FairPrincipleQuestionBlockProps) => {
+ const [notificationApi, notificationContextHolder] = notification.useNotification();
+ const { selectedDataProvider } = useDataProviderStore();
+ const dataProviderId = selectedDataProvider?.id;
+ const isOpenQuestion = Boolean(item.number) && isFairOpenQuestion(item.certificationQuestion);
+ const questionId = item.certificationQuestion?.id;
+
+ const handleAnswerBlur = async (event: FocusEvent) => {
+ if (!dataProviderId || !questionId) {
+ return;
+ }
+
+ const answer = event.target.value;
+ const savedAnswer = item.certificationQuestion?.answer?.answer ?? '';
+
+ if (answer === savedAnswer) {
+ return;
+ }
+
+ try {
+ await updateFairCertificationAnswer(dataProviderId, questionId, answer);
+ notificationApi.success({
+ title: answerSavedMessage,
+ placement: 'bottomRight',
+ duration: 2,
+ });
+ } catch {
+ message.error(answerSaveErrorMessage);
+ }
+ };
+
+ const countValues = showResultCounts
+ ? resolveFairQuestionCountValues(item.certificationQuestion?.result?.counts)
+ : [];
+ const metricValues = showResultCounts
+ ? resolveFairQuestionMetricValues(item.certificationQuestion?.result?.metrics)
+ : [];
+ const statusLabel = isOpenQuestion
+ ? openQuestionLabel
+ : resolveFairQuestionStatusLabel(item.certificationQuestion);
+ const statusClassName = getFairQuestionStatusClassName(statusLabel);
+
+ return (
+ <>
+ {notificationContextHolder}
+
+
+
+
{item.code}
+
{item.question}
+
+
+
+
+
{statusLabel}
+
+ {item.description ? (
+
+
+ {item.description}
+
+
+
+ ) : null}
+
+ {countValues.map((count) => (
+
+ {formatFairResultName(count.name)}
+ {formatNumber(count.value)}
+
+ ))}
+
+ {metricValues.map((metric) => (
+
+ ))}
+ {isOpenQuestion ? (
+ <>
+
+
+
+ {item.certificationQuestion?.answer?.answer &&
Last time edited {item.certificationQuestion?.answer?.editedDate} by {item.certificationQuestion?.answer?.editedBy}
}
+ >
+ ) : null}
+
+ {item.recommendation ? (
+
+
+
+
{recommendationHeading}
+
+ {item.recommendation}
+
+
+
+ ) : null}
+
+ >
+ );
+};
diff --git a/src/features/Fair/components/FairPrincipleSectionContent.tsx b/src/features/Fair/components/FairPrincipleSectionContent.tsx
new file mode 100644
index 000000000..841d3b1be
--- /dev/null
+++ b/src/features/Fair/components/FairPrincipleSectionContent.tsx
@@ -0,0 +1,45 @@
+import {FairPrincipleQuestionBlock} from '@features/Fair/components/FairPrincipleQuestionBlock';
+import type {FairPrincipleSection} from '@features/Fair/types/fairPrinciples.types';
+import { shouldShowFairQuestionResults } from '@features/Fair/utils/shouldShowFairQuestionResults';
+// import type { FairRepositoryStatusParams } from '@features/Fair/utils/resolveFairQuestionStatus';
+
+import '../styles.css';
+
+export type FairPrincipleSectionContentProps = {
+ section: FairPrincipleSection;
+ recommendationHeading: string;
+ openQuestionLabel: string;
+ answerSavedMessage: string;
+ answerSaveErrorMessage: string;
+ // repositoryStatus?: FairRepositoryStatusParams | null;
+};
+
+export const FairPrincipleSectionContent = ({
+ section,
+ recommendationHeading,
+ openQuestionLabel,
+ answerSavedMessage,
+ answerSaveErrorMessage,
+ // repositoryStatus,
+}: FairPrincipleSectionContentProps) => {
+ if (!section.items?.length) {
+ return null;
+ }
+
+ return (
+
+ {section.items.map((item, index) => (
+
+ ))}
+
+ );
+};
diff --git a/src/features/Fair/components/FairPrinciplesCollapse.tsx b/src/features/Fair/components/FairPrinciplesCollapse.tsx
new file mode 100644
index 000000000..e7e73d838
--- /dev/null
+++ b/src/features/Fair/components/FairPrinciplesCollapse.tsx
@@ -0,0 +1,128 @@
+import fairTexts from '@features/Fair/texts/fair.json';
+import { FairPrincipleSectionContent } from '@features/Fair/components/FairPrincipleSectionContent';
+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 { Button, Typography } from 'antd';
+import { useMemo } from 'react';
+
+import '../styles.css';
+
+export type FairPrinciplesCollapseProps = {
+ onSave?: () => void;
+ onSubmit?: () => void | Promise;
+ isSubmitting?: boolean;
+ /** 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;
+};
+
+const { Title, Paragraph } = Typography;
+
+export const FairPrinciplesCollapse = ({
+ onSubmit,
+ isSubmitting = false,
+ collapsibleVariant = 'default',
+ certificationQuestions,
+ // repositoryStatus,
+}: FairPrinciplesCollapseProps) => {
+ const { principlesAccordion } = fairTexts;
+
+ const recommendationHeading = principlesAccordion.recommendationHeading ?? 'Recommendation';
+
+
+ const handleSubmit = () => {
+ void onSubmit?.();
+ };
+
+ 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 => {
+ if (!questionsByNumber) {
+ return item;
+ }
+
+ const questionNumber = item.number ?? item.linkedQuestionNumber;
+ const certificationQuestion = questionNumber
+ ? questionsByNumber.get(questionNumber)
+ : 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: (
+
+
+ {section.title}
+
+
{section.description}
+
+ ),
+ children: (
+
+ ),
+ };
+ });
+ }, [principlesAccordion, recommendationHeading, certificationQuestions]);
+
+
+ return (
+
+
+
+
+ {principlesAccordion.submitButtonLabel}
+
+
+
+ );
+};
+
diff --git a/src/features/Fair/components/FairPrinciplesCollapsible.tsx b/src/features/Fair/components/FairPrinciplesCollapsible.tsx
new file mode 100644
index 000000000..134b0719e
--- /dev/null
+++ b/src/features/Fair/components/FairPrinciplesCollapsible.tsx
@@ -0,0 +1,100 @@
+import {DownOutlined} from '@ant-design/icons';
+import {Collapse} from 'antd';
+import type {CollapseProps} from 'antd';
+import type {ReactNode} from 'react';
+import {useCallback} from 'react';
+
+import '../styles.css';
+
+export type FairPrinciplesCollapsibleVariant = 'default' | 'compact';
+
+export type FairPrinciplesCollapsibleSection = {
+ key: string;
+ /** Panel header (e.g. title + summary). */
+ label: ReactNode;
+ /** Expanded panel body — any view: questions list, placeholder, or custom markup. */
+ children: ReactNode;
+};
+
+export type FairPrinciplesCollapsibleProps = {
+ /** Visual density: `default` matches the FAIR certification design; `compact` uses tighter spacing. */
+ variant?: FairPrinciplesCollapsibleVariant;
+ /** Accessible name for the accordion region. */
+ ariaLabel?: string;
+ /** One entry per collapsible panel. */
+ sections: FairPrinciplesCollapsibleSection[];
+ /** Initially expanded panel key(s). */
+ defaultActiveKey?: string | string[];
+ /** Controlled active keys (optional; when set, use with `onChange`). */
+ activeKey?: string | string[];
+ onChange?: (key: string | string[]) => void;
+ /** Extra class on the outer `` wrapper. */
+ className?: string;
+ /** Extra class on the Ant Design `Collapse` root. */
+ collapseClassName?: string;
+ expandIconPlacement?: CollapseProps['expandIconPlacement'];
+ bordered?: boolean;
+ ghost?: boolean;
+};
+
+const variantClassName: Record = {
+ default: 'fair-principles-collapse--variant-default',
+ compact: 'fair-principles-collapse--variant-compact',
+};
+
+export const FairPrinciplesCollapsible = ({
+ variant = 'default',
+ ariaLabel,
+ sections,
+ defaultActiveKey,
+ activeKey,
+ onChange,
+ className = '',
+ collapseClassName = '',
+ expandIconPlacement = 'end',
+ bordered = false,
+ ghost = true,
+}: FairPrinciplesCollapsibleProps) => {
+ const renderExpandIcon: NonNullable = useCallback((panelProps) => {
+ const isActive = Boolean(panelProps.isActive);
+ return (
+
+
+
+ );
+ }, []);
+
+ const items: CollapseProps['items'] = sections.map(({key, label, children}) => ({
+ key,
+ label,
+ children,
+ }));
+
+ const collapseClassNames = [
+ 'fair-principles-collapse',
+ variantClassName[variant],
+ collapseClassName,
+ ]
+ .filter(Boolean)
+ .join(' ');
+
+ return (
+
+
+
+ );
+};
diff --git a/src/features/Fair/components/FairSubmissionProgress.tsx b/src/features/Fair/components/FairSubmissionProgress.tsx
new file mode 100644
index 000000000..7617c145f
--- /dev/null
+++ b/src/features/Fair/components/FairSubmissionProgress.tsx
@@ -0,0 +1,108 @@
+import { CheckOutlined } from '@ant-design/icons';
+import { useFairCertificationSubmissions } from '@features/Fair/hooks/useFairCertification';
+import fairTexts from '@features/Fair/texts/fair.json';
+import type { FairCertificationSubmission } from '@features/Fair/types/fairCertification.types';
+import { formatFairSubmissionLabel } from '@features/Fair/utils/formatFairSubmissionLabel';
+import { formatDate } from '@/utils/dateUtils';
+import { Steps } from 'antd';
+import type { StepsProps } from 'antd';
+import { useMemo } from 'react';
+import '../styles.css';
+
+const CheckIcon = () => (
+
+
+
+);
+
+const EllipsisIcon = () => (
+
+
+
+
+
+);
+
+type SubmissionProgressCopy = typeof fairTexts.submissionProgress;
+
+const buildSubmissionStepItems = (
+ submissions: FairCertificationSubmission[],
+ copy: SubmissionProgressCopy,
+): NonNullable => {
+ const sortedSubmissions = [...submissions].sort(
+ (left, right) => left.submissionNumber - right.submissionNumber,
+ );
+
+ const submissionItems: NonNullable = sortedSubmissions.map(
+ (submission) => ({
+ status: 'finish' as const,
+ icon: ,
+ title: (
+
+
+ {formatDate(submission.pdfGeneratedAt)}
+
+
+ {formatFairSubmissionLabel(submission.submissionNumber)}
+
+
+ ),
+ description: submission.reportUrl ? (
+
+ {copy.viewSubmittedReport}
+
+ ) : undefined,
+ }),
+ );
+
+ return [
+ ...submissionItems,
+ {
+ status: 'finish' as const,
+ icon: ,
+ title: (
+
+ {copy.unlimitedSubmissions}
+
+ ),
+ },
+ {
+ status: 'finish' as const,
+ icon: ,
+ title: (
+
+ {copy.gettingCertificate}
+
+ ),
+ },
+ ];
+};
+
+export const FairSubmissionProgress = () => {
+ const { submissionProgress } = fairTexts;
+ const { submissions } = useFairCertificationSubmissions();
+
+ const items = useMemo(
+ () => buildSubmissionStepItems(submissions, submissionProgress),
+ [submissions, submissionProgress],
+ );
+
+ return (
+
+
+ {submissionProgress.title}
+
+
+
+ );
+};
diff --git a/src/features/Fair/hooks/fairPricingUtils.ts b/src/features/Fair/hooks/fairPricingUtils.ts
new file mode 100644
index 000000000..ac1a9a858
--- /dev/null
+++ b/src/features/Fair/hooks/fairPricingUtils.ts
@@ -0,0 +1,24 @@
+import type {
+ FairPricingPrice,
+ FairPricingPriceType,
+ FairPricingTable,
+} from '../types/fairPricingTypes.ts';
+
+/** 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/hooks/useFairCertification.ts b/src/features/Fair/hooks/useFairCertification.ts
new file mode 100644
index 000000000..b95648bfc
--- /dev/null
+++ b/src/features/Fair/hooks/useFairCertification.ts
@@ -0,0 +1,111 @@
+import useSWR from 'swr';
+import { fetcher, postRequestFetcher, swrDefaultConfig } from '@/config/swr';
+import { useDataProviderStore } from '@/store/dataProviderStore';
+import type {
+ FairCertificationApiResponse,
+ FairCertificationSubmissionsApiResponse,
+} from '@features/Fair/types/fairCertification.types';
+
+// TODO CHECK refresh
+// export const refreshFairCertificationAutomaticChecks = (dataProviderId: number) =>
+// postRequestFetcher(
+// `/internal/data-providers/${dataProviderId}/fair-certification/refresh`,
+// undefined,
+// true,
+// );
+
+// const fetchFairCertification = async (
+// url: string,
+// dataProviderId: number,
+// ): Promise => {
+// const response = (await fetcher(url)) as FairCertificationApiResponse;
+// await refreshFairCertificationAutomaticChecks(dataProviderId);
+// return response;
+// };
+
+// const fetchFairCertification = async (
+// url: string,
+// _dataProviderId: number,
+// ): Promise => {
+// const response = (await fetcher(url)) as FairCertificationApiResponse;
+// // TODO: re-enable when refresh endpoint is ready
+// // await refreshFairCertificationAutomaticChecks(dataProviderId);
+// return response;
+// };
+
+
+export const useFairCertification = (dataProviderId?: number) => {
+ 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,
+ );
+
+ // const { data, error, isLoading, mutate } = useSWR(
+ // key,
+ // key && effectiveDataProviderId
+ // ? () => fetchFairCertification(key, effectiveDataProviderId)
+ // : null,
+ // swrDefaultConfig,
+ // );
+
+ return {
+ fairCertification: data ?? null,
+ error,
+ isLoading: isLoading || !isLoaded,
+ mutate,
+ };
+};
+
+export const updateFairCertificationAnswer = (
+ dataProviderId: number,
+ questionId: string,
+ answer: string,
+) =>
+ postRequestFetcher(
+ `/internal/data-providers/${dataProviderId}/fair-certification/answers/${questionId}`,
+ { answer },
+ );
+
+export const submitFairCertification = (dataProviderId: number) =>
+ postRequestFetcher(
+ `/internal/data-providers/${dataProviderId}/fair-certification/submissions`,
+ undefined,
+ true,
+ );
+
+export const useFairCertificationSubmissions = (dataProviderId?: number) => {
+ const { selectedDataProvider, isLoaded } = useDataProviderStore();
+ const effectiveDataProviderId = dataProviderId ?? selectedDataProvider?.id;
+
+ const key =
+ isLoaded && effectiveDataProviderId
+ ? `/internal/data-providers/${effectiveDataProviderId}/fair-certification/submissions`
+ : null;
+
+ const { data, error, isLoading, mutate } = useSWR(
+ key,
+ key
+ ? () =>
+ fetcher(key).then(
+ (response) => response as FairCertificationSubmissionsApiResponse,
+ )
+ : null,
+ swrDefaultConfig,
+ );
+
+ return {
+ submissions: data?.submissions ?? [],
+ error,
+ isLoading: isLoading || !isLoaded,
+ mutate,
+ };
+};
diff --git a/src/features/Fair/store/fairCertificationStore.ts b/src/features/Fair/store/fairCertificationStore.ts
new file mode 100644
index 000000000..d49c0fee4
--- /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/features/Fair/styles.css b/src/features/Fair/styles.css
index a2d9def07..0095608fa 100644
--- a/src/features/Fair/styles.css
+++ b/src/features/Fair/styles.css
@@ -1,3 +1,18 @@
+.fair-certification-loading {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+ gap: 1rem;
+ height: calc(100vh - 130px);
+ padding: 2rem;
+}
+
+.fair-certification-loading-text {
+ margin: 0;
+ color: var(--color-text-secondary);
+}
+
/* FAIR certification page layout */
.fair-certification {
padding: 0 0 24px;
@@ -8,8 +23,8 @@
box-sizing: border-box;
width: 100%;
margin: 0 0 40px;
- padding: 40px 24px 48px;
- background: #f5f5f5;
+ padding: 40px 100px 48px;
+ background: var(--color-bg-layout);
border-radius: 2px;
}
@@ -53,11 +68,11 @@
align-items: center;
gap: 20px;
width: 100%;
- margin: 28px 0 0;
+ margin: 28px 0 60px 0;
}
.fair-certification-intro-read-more {
- color: #a65e17;
+ color: var(--color-primary);
font-size: 16px;
font-weight: 400;
line-height: 1.4;
@@ -66,7 +81,7 @@
}
.fair-certification-intro-read-more:hover {
- color: #8a4d12;
+ color: var(--color-primary-dark);
}
.fair-certification-intro-register {
@@ -76,8 +91,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 +102,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 +125,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 +136,7 @@
}
.fair-certification-markdown {
- color: #212121;
+ color: var(--color-text-secondary);
font-size: 16px;
font-weight: 400;
line-height: 1.4;
@@ -167,8 +182,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 +195,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,21 +215,23 @@
.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%; /* 31.2px */
+ line-height: 130%;
+ /* 31.2px */
letter-spacing: 0.036px;
}
.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;
- line-height: 24px; /* 150% */
+ line-height: 24px;
+ /* 150% */
letter-spacing: 0.026px;
}
@@ -223,7 +240,7 @@
}
.certification-section-title {
- color: #212121;
+ color: var(--color-text-secondary);
text-align: center;
font-size: 32px;
font-style: normal;
@@ -245,7 +262,7 @@
align-items: center;
gap: 16px;
border-radius: 6px;
- background: #f5f5f5;
+ background: var(--color-bg-layout);
}
.cert-image {
@@ -254,7 +271,7 @@
}
.certification-title {
- color: #212121;
+ color: var(--color-text-secondary);
text-align: center;
font-size: 24px;
font-style: normal;
@@ -265,7 +282,7 @@
}
.certification-markdown {
- color: #000;
+ color: var(--color-dark);
font-size: 16px;
font-style: normal;
font-weight: 400;
@@ -274,6 +291,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 +308,1142 @@
.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: var(--color-text-secondary);
+ font-size: 15px;
+ line-height: 1.35;
+ background: var(--color-text-white);
+}
+
+.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: #B9B9B9;
+ padding: 12px 14px;
+ text-align: center;
+ vertical-align: middle;
+}
+
+.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 {
+ font-variant-numeric: tabular-nums;
+}
+
+.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 {
+ 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: var(--color-primary);
+ font-weight: 600;
+}
+
+.fair-certification-price-struck {
+ color: var(--color-text-secondary);
+ text-decoration: line-through;
+ font-weight: 400;
+}
+
+.fair-certification-pricing-td--sustaining {
+ vertical-align: middle;
+}
+
+.fair-certification-price-free {
+ color: var(--color-primary);
+ font-weight: 700;
+}
+
+/*header cer*/
+.fair-button-wrapper {
+ display: flex;
+ flex-wrap: wrap;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 48px;
+}
+
+.fair-certification-header-wrapper {
+ gap: 50px;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 455px;
+ align-items: start;
+}
+
+.fair-certification-header-inner-wrapper {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ min-width: 0;
+}
+
+.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;
+}
+
+.fair-certification-placeholder {
+ flex-shrink: 0;
+ width: 455px;
+ height: 255px;
+ object-fit: contain;
+}
+
+/* FAIR certification certificate card */
+.fair-certificate {
+ flex-shrink: 0;
+ width: 455px;
+ height: 255px;
+}
+
+.fair-certificate__card {
+ position: relative;
+ box-sizing: border-box;
+ width: 100%;
+ height: 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);
+}
+
+@media (max-width: 1200px) {
+ .fair-button-wrapper {
+ justify-content: flex-start;
+ }
+
+ .fair-button-wrapper .ant-btn {
+ flex: 1 1 calc(50% - 4px);
+ min-width: 0;
+ padding: 8px 16px;
+ }
+
+ .fair-certification-header-wrapper {
+ grid-template-columns: 1fr;
+ gap: 32px;
+ }
+
+ .fair-certification-header-inner-wrapper {
+ width: 100%;
+ }
+
+ .fair-certification-title {
+ font-size: 24px;
+ }
+
+ .fair-certification-description {
+ margin: 8px 0 16px;
+ }
+
+ .fair-certificate,
+ .fair-certification-placeholder {
+ justify-self: center;
+ }
+}
+
+@media (max-width: 992px) {
+ .fair-button-wrapper .ant-btn {
+ font-size: 12px;
+ }
+}
+
+@media (max-width: 576px) {
+ .fair-button-wrapper {
+ flex-direction: column;
+ align-items: stretch;
+ margin-bottom: 32px;
+ }
+
+ .fair-button-wrapper .ant-btn {
+ flex: none;
+ width: 100%;
+ }
+
+ .fair-certification-placeholder {
+ width: 100%;
+ height: 100%;
+ object-fit: contain;
+ }
+
+ /* FAIR certification certificate card */
+ .fair-certificate {
+ width: 100%;
+ height: 100%;
+ }
+}
+
+.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%;
+}
+
+.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%;
+ 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%;
+ 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%;
+ 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%;
+}
+
+/* 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: 20px;
+ height: 1px;
+}
+
+.fair-certificate__frame--top-left::after {
+ top: 0;
+ left: 0;
+ width: 1px;
+ height: 20px;
+}
+
+.fair-certificate__frame--top-right {
+ top: 12px;
+ right: 12px;
+}
+
+.fair-certificate__frame--top-right::before {
+ top: 0;
+ right: 0;
+ width: 20px;
+ height: 1px;
+}
+
+.fair-certificate__frame--top-right::after {
+ top: 0;
+ right: 0;
+ width: 1px;
+ height: 20px;
+}
+
+.fair-certificate__frame--bottom-left {
+ bottom: 12px;
+ left: 12px;
+}
+
+.fair-certificate__frame--bottom-left::before {
+ bottom: 0;
+ left: 0;
+ width: 20px;
+ height: 1px;
+}
+
+.fair-certificate__frame--bottom-left::after {
+ bottom: 0;
+ left: 0;
+ width: 1px;
+ height: 20px;
+}
+
+.fair-certificate__frame--bottom-right {
+ bottom: 12px;
+ right: 12px;
+}
+
+.fair-certificate__frame--bottom-right::before {
+ bottom: 0;
+ right: 0;
+ width: 20px;
+ height: 1px;
+}
+
+.fair-certificate__frame--bottom-right::after {
+ bottom: 0;
+ right: 0;
+ width: 1px;
+ height: 20px;
+}
+
+/* Partial border edges */
+.fair-certificate__edge {
+ position: absolute;
+ background: #212121;
+ pointer-events: none;
+}
+
+.fair-certificate__edge--top {
+ top: 12px;
+ left: 65px;
+ right: 65px;
+ height: 1px;
+}
+
+.fair-certificate__edge--bottom {
+ bottom: 12px;
+ left: 65px;
+ right: 65px;
+ height: 1px;
+}
+
+.fair-certificate__edge--left {
+ top: 65px;
+ bottom: 65px;
+ left: 12px;
+ width: 1px;
+}
+
+.fair-certificate__edge--right {
+ 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;
+}
+
+/* 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;
+}
+
+/* Variants — `FairPrinciplesCollapsible` (`compact` tightens spacing; `default` uses base rules only) */
+.fair-principles-collapse--variant-compact .ant-collapse-item {
+ margin-bottom: 16px;
+}
+
+.fair-principles-collapse--variant-compact .ant-collapse-item .ant-collapse-header {
+ padding: 16px 18px;
+}
+
+.fair-principles-collapse--variant-compact .ant-collapse-content-box {
+ padding: 16px 18px;
+}
+
+.fair-principles-collapse--variant-compact .fair-principles-collapse-expand-icon {
+ height: 48px;
+ width: 52px;
+}
+
+.fair-principles-collapse--variant-compact .fair-principles-collapse-label .fair-principles-collapse-title {
+ font-size: 20px;
+}
+
+.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-collapsible-root .ant-collapse-borderless>.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;
+}
+
+.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-open-form {
+ width: 100%;
+}
+
+.fair-principles__panel-body {
+ display: flex;
+ flex-direction: column;
+ gap: 24px;
+}
+
+.fair-principles__question {
+ border-radius: 10px;
+ border: 1px solid var(--gray-300);
+ background: var(--color-bg-container);
+ padding: 24px;
+}
+
+.fair-principles__status-note {
+ margin: 12px 0 0;
+ padding: 10px 12px;
+ border-radius: 4px;
+ background: var(--danger-light-background);
+ color: var(--danger);
+ font-size: 14px;
+ font-weight: 500;
+}
+
+.fair-principles__metrics {
+ margin-top: 8px;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.fair-principles__metric-row {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+}
+
+.fair-principles__metric-row .support-status__counter-value {
+ margin: 4px 0 12px;
+ font-size: 36px;
+ line-height: 1.1;
+}
+
+.fair-principles__open-block {
+ margin-top: 16px;
+}
+
+.fair-principles__open-field textarea {
+ height: 108px;
+ margin-top: 24px;
+}
+
+.fair-principles__open-badge {
+ display: inline-block;
+ margin-bottom: 10px;
+ padding: 2px 8px;
+ border-radius: 2px;
+ background: var(--color-bg-layout);
+ color: var(--color-text-secondary);
+ font-size: 13px;
+ font-weight: 600;
+ text-transform: capitalize;
+}
+
+.fair-principles__open-field {
+ margin-bottom: 0;
+}
+
+.fair-principles-accordion-actions {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ gap: 14px;
+ 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-heading {
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.fair-submission-progress__step-date {
+ color: var(--color-text-secondary);
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 130%;
+ letter-spacing: 0.026px;
+ text-align: left;
+}
+
+.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 .ant-steps-item-content {
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 130%;
+ letter-spacing: 0.026px;
+ margin-top: 20px !important;
+}
+
+.fair-submission-progress__report-link {
+ color: var(--color-primary, #1890ff);
+ text-align: left;
+}
+
+.fair-submission-progress__report-link:hover {
+ color: var(--color-primary-hover, #40a9ff);
+}
+
+.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);
+ margin-left: 2px;
+}
+
+.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;
+}
+
+.fair-submission-progress-steps .ant-steps-item-description {
+ margin-top: 8px;
+ padding-bottom: 8px;
+}
+
+.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;
+ min-width: 0;
+ padding: 12px 16px;
+ color: var(--color-text-secondary);
+ font-size: 16px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 130%;
+ letter-spacing: 0.01px;
+}
+
+@media (max-width: 767px) {
+ .support-status__row {
+ grid-template-columns: 1fr 1fr;
+ grid-template-areas:
+ 'question question'
+ 'secondary status';
+ gap: 8px;
+ }
+
+ .support-status__question-wrap {
+ grid-area: question;
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 4px;
+ padding: 10px 12px;
+ font-size: 14px;
+ }
+
+ .support-status__row > .required-link {
+ grid-area: secondary;
+ justify-self: start;
+ min-width: 72px;
+ padding: 6px 10px;
+ font-size: 14px;
+ }
+
+ .support-status__row > .support-status__status:not(.support-status__status--hidden) {
+ grid-area: status;
+ justify-self: end;
+ min-width: 72px;
+ padding: 6px 10px;
+ font-size: 14px;
+ }
+
+ .support-status__row > .support-status__description {
+ grid-column: 1 / -1;
+ padding: 12px 0;
+ }
+
+ .support-status__status--hidden {
+ display: none;
+ }
+
+ .fair-principles__question {
+ padding: 16px;
+ }
+}
+
+.file-icon {
+ font-size: 20px;
+}
+
+/*section item*/
+.support-status__counter {
+ margin-bottom: 24px;
+}
+
+.support-status__counter-label {
+ padding: 0;
+}
+
+.support-status__counter-value {
+ margin: 0;
+ color: var(--color-text-secondary, #212121);
+ font-size: 32px;
+ font-style: normal;
+ font-weight: 500;
+ line-height: 130%;
+}
+
+.fair-principles-input-identifier {
+ text-align: left;
+ color: #666;
+ font-size: 14px;
+ font-style: normal;
+ font-weight: 400;
+ line-height: 130%; /* 18.2px */
+ letter-spacing: 0.042px;
+ margin-top: 8px;
+}
+
+.fair-submission-progress-steps .ant-steps-item-content {
+ text-align: left;
+}
+
+.fair-submission-progress .ant-steps-vertical >.ant-steps-item {
+ padding-bottom: 40px;
}
diff --git a/src/features/Fair/texts/fair.json b/src/features/Fair/texts/fair.json
new file mode 100644
index 000000000..0370e5fdb
--- /dev/null
+++ b/src/features/Fair/texts/fair.json
@@ -0,0 +1,373 @@
+{
+ "submissionProgress": {
+ "title": "Progress of the submission",
+ "unlimitedSubmissions": "Unlimited number of submission",
+ "gettingCertificate": "Getting certificate",
+ "viewSubmittedReport": "View submitted report (PDF)"
+ },
+ "approvedView": {
+ "title": "FAIR certification:",
+ "certificationDescription": "Your repository is not certified yet. In the report below you can review how your repository performs in terms of fair principles, once you’re satisfied with the report and you reply to some more detailed questions, you can submit the report for reviews and start the awarding process.",
+ "issued": "**Certificate issued:**",
+ "valid": "**Certificate valid until:**",
+ "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",
+ "downloadPdfErrorMessage": "Failed to download the certificate in PDF. Please try again.",
+ "downloadReportButtonLabel": "Download report"
+ },
+ "principlesAccordion": {
+ "sectionAriaLabel": "FAIR principles and certification questions",
+ "saveButtonLabel": "Save",
+ "submitButtonLabel": "Submit",
+ "submitSuccessMessage": "Your FAIR certification has been submitted successfully.",
+ "submitErrorMessage": "Failed to submit your FAIR certification. Please try again.",
+ "recommendationHeading": "Recommendation",
+ "openQuestionBadge": "Open question",
+ "answerSavedMessage": "Your progress has been saved.",
+ "answerSaveErrorMessage": "Failed to save your answer. Please try again.",
+ "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.\n\nMachine-readable metadata are essential for automatic discovery of datasets and services, so this is an essential component of the FAIRification process.",
+ "items": [
+ {
+ "id": "r11",
+ "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).",
+ "number": "1.1"
+ },
+ {
+ "id": "r12",
+ "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](https://rioxx.net/profiles/v3-0-final/), 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](https://core.ac.uk/documentation/data-providers-guide#meta-configuration).",
+ "number": "1.2"
+ },
+ {
+ "id": "r13",
+ "code": "R(1.3)",
+ "answerPlaceholder": "Write your answer here …",
+ "question": "How many metadata records are there in your repository?",
+ "description": "it is not always easy to extract this information from your repository platform, so some estimation is good to to enable us to compare what we see in the public to your expectations.\n",
+ "number": "1.3"
+ },
+ {
+ "id": "",
+ "code": "",
+ "question": "Indexed content",
+ "description": " The number of records CORE is able to index from your repository. If this number doesn't match your records, it might mean you have issues in exposing your records in an interoperable way, you might want to check the [Indexing Status tab](indexing)",
+ "counterLabel": "Number of metadata records:",
+ "linkedQuestionNumber": "1.3"
+ }
+ ]
+ },
+ "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.",
+ "items": [
+ {
+ "id": "r21",
+ "code": "R(2.1)",
+ "answerPlaceholder": "Write your answer here …",
+ "question": "How many full texts are there in your repository?",
+ "description": "it is not always easy to extract this information from your repository platform, so some estimation is good to to enable us to compare what we see in the public to your expectations.\n",
+ "number": "2.1"
+ },
+ {
+ "id": "",
+ "code": "",
+ "question": "Indexed full texts",
+ "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](/data-providers/indexing) 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/indexing) 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](https://core.ac.uk/documentation/data-providers-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.",
+ "linkedQuestionNumber": "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](https://rioxx.net/profiles/v3-0-final/) 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",
+ "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/](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",
+ "code": "R(2.4)",
+ "question": "Do you support any tool for tracking COUNTER-compliant downloads in your system?",
+ "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.",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "2.4"
+ }
+ ]
+ },
+ "interoperable": {
+ "title": "Interoperable",
+ "description": "This section assesses how well the data within the repository can be integrated with other datasets and how effectively it can be used with various applications or workflows for analysis, storage, and processing.",
+ "items": [
+ {
+ "id": "r31",
+ "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)",
+ "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).",
+ "number": "3.2"
+ },
+ {
+ "id": "r33",
+ "code": "R(3.3)",
+ "question": "Does your repository support persistent management of records?",
+ "description": "A repository with persistent record 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.",
+ "number": "3.3"
+ },
+ {
+ "id": "r34",
+ "code": "R(3.4)",
+ "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.",
+ "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",
+ "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](doi). 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](orcid) 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](documentation#orcid).",
+ "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.",
+ "number": "3.7"
+ }
+ ]
+ },
+ "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.",
+ "items": [
+ {
+ "id": "r41",
+ "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](das):\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.",
+ "number": "4.2"
+ },
+ {
+ "id": "r43",
+ "code": "R(4.3)",
+ "question": "Do you have an Institutional RRS or do your papers contain an RRS section?",
+ "answerPlaceholder": "Write your answer here …",
+ "recommendation": "Follow your institutional policy on rights retention and ensure RRS sections are visible where applicable.",
+ "number": "4.3"
+ }
+ ]
+ },
+ "fitForFunders": {
+ "title": "Fit for funders",
+ "description": "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": "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.",
+ "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.",
+ "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.",
+ "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.",
+ "number": "5.4"
+ }
+ ]
+ },
+ "fitForFuture": {
+ "title": "Fit for future",
+ "description": "Standards and protocols in this section help your repository align with emerging infrastructure for discovery, synchronisation and notifications.",
+ "items": [
+ {
+ "id": "r61",
+ "code": "R(6.1)",
+ "question": "Do you support ResourceSync?",
+ "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.",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "6.1"
+ },
+ {
+ "id": "r62",
+ "code": "R(6.2)",
+ "question": "Do you support COAR Notify?",
+ "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.",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "6.2"
+ },
+ {
+ "id": "r63",
+ "code": "R(6.3)",
+ "question": "Do you support FAIRiCat?",
+ "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.",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "6.3"
+ }
+ ]
+ },
+ "operational": {
+ "title": "Operational / Environmental considerations",
+ "description": "We have run an automatic check of your repository. To get certification please answer the following questions:",
+ "items": [
+ {
+ "id": "r71",
+ "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.",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "7.1"
+ },
+ {
+ "id": "r72",
+ "code": "R(7.2)",
+ "question": "Deposits are checked before made available.",
+ "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.",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "7.2"
+ },
+ {
+ "id": "r73",
+ "code": "R(7.3)",
+ "question": "How do you handle take-down notices?",
+ "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.",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "7.3"
+ },
+ {
+ "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\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.",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "7.4"
+ },
+ {
+ "id": "r75",
+ "code": "R(7.5)",
+ "question": "How do you encourage researchers to get their ORCID?",
+ "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.",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "7.5"
+ },
+ {
+ "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).",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "7.6"
+ },
+ {
+ "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. 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.",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "7.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. 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.",
+ "answerPlaceholder": "Write your answer here …",
+ "number": "7.8"
+ }
+ ]
+ }
+ },
+ "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 {{ level }} 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.",
+ "signatoryTitle": "Certificate ID",
+ "infoUrl": "For more information visit https://core.ac.uk",
+ "issueDateLabel": "Issue Date:",
+ "validUntilLabel": "Valid until:"
+ }
+}
diff --git a/src/features/Fair/texts/fairCertificationLanding.json b/src/features/Fair/texts/fairCertificationLanding.json
new file mode 100644
index 000000000..c2346eb23
--- /dev/null
+++ b/src/features/Fair/texts/fairCertificationLanding.json
@@ -0,0 +1,204 @@
+{
+ "howItWorks": {
+ "howItWorks": {
+ "title": "Why it's important?",
+ "image": "/images/workCertificate.svg",
+ "description": "Aligning with FAIR is crucial for ensuring that repository content is easily discoverable, accessible, interoperable and reusable within scholarly infrastructures.\nOur FAIR Certification programme consists of a set of objective data-driven automated checks of your platform directly assessing FAIRness.\nOur easy to navigate certification process is fully technology-supported, objective and provides specific guidance and recommendations leading to tangible improvements, ensuring that your platform aligns with best technology and content management practices."
+ }
+ },
+ "applicationProcess": {
+ "applicationProcess": {
+ "title": "How to get your repository FAIR certified?",
+ "steps": [
+ {
+ "step": 1,
+ "title": "Apply for FAIR certification",
+ "description": "Register your interest below or apply for FAIR Certification via your CORE Dashboard account (FAIR certification tab) and arrange payment (free for Sustaining Members)."
+ },
+ {
+ "step": 2,
+ "title": "CORE team assesses the submission",
+ "description": "We will evaluate your repository and inform you if you qualify for the certificate. In case you don't qualify yet, we will offer technical advice on how to satisfy the requirements. You are free to resubmit your application as many times as needed during a 365 days period."
+ },
+ {
+ "step": 3,
+ "title": "Submit the form",
+ "description": "CORE will run automated checks for your repository and produce a report containing results and recommendations against the certification criteria. Review the recommendations, complete the report and submit when ready."
+ },
+ {
+ "step": 4,
+ "title": "Get your FAIR certificate",
+ "description": "Once your repository gets FAIR certification you will get your certificate via email and your organisation will appear in the FAIR certificated repositories list. You can now display your certificate in your repository."
+ }
+ ]
+ }
+ },
+ "certificates": {
+ "certificates": [
+ {
+ "title": "Bronze",
+ "picture": "/images/bronze.svg",
+ "description": "Guarantees a foundational level of compliance. Repository metadata and content is indexable, records are assigned identifiers and metadata have been populated to at least a minimum level."
+ },
+ {
+ "title": "Silver",
+ "picture": "/images/silver.svg",
+ "description": "Demonstrates strong compliance with FAIR. Metadata are exposed using industry-leading application profiles and vocabularies, contain machine-readable licence information and have been sufficiently populated."
+ },
+ {
+ "title": "Gold",
+ "picture": "/images/gold.svg",
+ "description": "Achieves full machine-actionability. Metadata have been richly populated and support linking of research papers and datasets. Repository supports industry-leading protocols and interoperability standards."
+ }
+ ]
+ },
+ "table": {
+ "table": {
+ "title": "FAIR Certification fees",
+ "headers": [
+ {
+ "name": "Data provider size***"
+ },
+ {
+ "name": "[Low-income countries](https://datahelpdesk.worldbank.org/knowledgebase/articles/906519-world-bank-country-and-lending-groups)",
+ "type": "low"
+ },
+ {
+ "name": "[Middle-income countries](https://datahelpdesk.worldbank.org/knowledgebase/articles/906519-world-bank-country-and-lending-groups)",
+ "type": "middle"
+ },
+ {
+ "name": "[High-income countries](https://datahelpdesk.worldbank.org/knowledgebase/articles/906519-world-bank-country-and-lending-groups)",
+ "type": "high"
+ }
+ ],
+ "certification": {
+ "subRows": [
+ {
+ "title": "Band 1",
+ "caption": "up to 5K articles",
+ "prices": [
+ { "type": "low", "original": 1000 },
+ { "type": "middle", "original": 2000 },
+ { "type": "high", "original": 3000 }
+ ]
+ },
+ {
+ "title": "Band 2",
+ "caption": "5K - 10K articles",
+ "prices": [
+ { "type": "low", "original": 1250 },
+ { "type": "middle", "original": 2250 },
+ { "type": "high", "original": 3250 }
+ ]
+ },
+ {
+ "title": "Band 3",
+ "caption": "10K - 20K articles",
+ "prices": [
+ { "type": "low", "original": 1500 },
+ { "type": "middle", "original": 2500 },
+ { "type": "high", "original": 3500 }
+ ]
+ },
+ {
+ "title": "Band 4",
+ "caption": "20K - 40K articles",
+ "prices": [
+ { "type": "low", "original": 1750 },
+ { "type": "middle", "original": 2750 },
+ { "type": "high", "original": 3750 }
+ ]
+ },
+ {
+ "title": "Band 5",
+ "caption": "40K+ articles",
+ "prices": [
+ { "type": "low", "original": 2000 },
+ { "type": "middle", "original": 3000 },
+ { "type": "high", "original": 4000 }
+ ]
+ }
+ ]
+ }
+ },
+ "tableMembers": {
+ "title": "FAIR Certification fees for Supporting and Sustaining members",
+ "headers": [
+ {
+ "name": "Data provider size**"
+ },
+ {
+ "name": "[Low-income countries](https://datahelpdesk.worldbank.org/knowledgebase/articles/906519-world-bank-country-and-lending-groups)",
+ "type": "low"
+ },
+ {
+ "name": "[Middle-income countries](https://datahelpdesk.worldbank.org/knowledgebase/articles/906519-world-bank-country-and-lending-groups)",
+ "type": "middle"
+ },
+ {
+ "name": "[High-income countries](https://datahelpdesk.worldbank.org/knowledgebase/articles/906519-world-bank-country-and-lending-groups)",
+ "type": "high"
+ },
+ {
+ "name": "Sustaining",
+ "type": "sustaining"
+ }
+ ],
+ "certification": {
+ "subRows": [
+ {
+ "title": "Band 1",
+ "caption": "up to 5K articles",
+ "prices": [
+ { "type": "low", "discounted": 500, "original": 1000 },
+ { "type": "middle", "discounted": 1000, "original": 2000 },
+ { "type": "high", "discounted": 1500, "original": 3000 },
+ { "type": "sustaining", "free": true }
+ ]
+ },
+ {
+ "title": "Band 2",
+ "caption": "5K - 10K articles",
+ "prices": [
+ { "type": "low", "discounted": 625, "original": 1250 },
+ { "type": "middle", "discounted": 1125, "original": 2250 },
+ { "type": "high", "discounted": 1625, "original": 3250 },
+ { "type": "sustaining", "free": true }
+ ]
+ },
+ {
+ "title": "Band 3",
+ "caption": "10K - 20K articles",
+ "prices": [
+ { "type": "low", "discounted": 750, "original": 1500 },
+ { "type": "middle", "discounted": 1250, "original": 2500 },
+ { "type": "high", "discounted": 1750, "original": 3500 },
+ { "type": "sustaining", "free": true }
+ ]
+ },
+ {
+ "title": "Band 4",
+ "caption": "20K - 40K articles",
+ "prices": [
+ { "type": "low", "discounted": 875, "original": 1750 },
+ { "type": "middle", "discounted": 1375, "original": 2750 },
+ { "type": "high", "discounted": 1875, "original": 3750 },
+ { "type": "sustaining", "free": true }
+ ]
+ },
+ {
+ "title": "Band 5",
+ "caption": "40K+ articles",
+ "prices": [
+ { "type": "low", "discounted": 1000, "original": 2000 },
+ { "type": "middle", "discounted": 1500, "original": 3000 },
+ { "type": "high", "discounted": 2000, "original": 4000 },
+ { "type": "sustaining", "free": true }
+ ]
+ }
+ ]
+ }
+ }
+ }
+}
diff --git a/src/features/Fair/types/fairCertification.types.ts b/src/features/Fair/types/fairCertification.types.ts
new file mode 100644
index 000000000..b4c594b2a
--- /dev/null
+++ b/src/features/Fair/types/fairCertification.types.ts
@@ -0,0 +1,128 @@
+export type FairCertificationWorkflowStatus = string;
+
+export type FairCertificationCertificateStatus =
+ | 'valid'
+ | 'expired'
+ | 'not_certified'
+ | string;
+
+export type FairCertificationLevel =
+ | 'bronze'
+ | 'silver'
+ | 'gold'
+ | 'platinum'
+ | string;
+
+export type FairCertificationQuestionResultStatus = 'pass' | 'fail' | 'unknown' | string;
+
+export type FairCertificationQuestionType = 'open' | 'automatic' | string;
+
+export type FairCertificationQuestionCountEntry = {
+ name: string;
+ value: number | null;
+};
+
+export type FairCertificationQuestionCounts =
+ | Record
+ | FairCertificationQuestionCountEntry[];
+
+export type FairCertificationQuestionMetricEntry = {
+ name: string;
+ value: number | null;
+ unit?: string;
+};
+
+export type FairCertificationQuestionMetrics =
+ | Record
+ | FairCertificationQuestionMetricEntry[];
+
+export type FairCertificationQuestionResult = {
+ value?: boolean | string | number;
+ status?: FairCertificationQuestionResultStatus;
+ source?: string;
+ checkedAt?: string;
+ metrics?: FairCertificationQuestionMetrics;
+ counts?: FairCertificationQuestionCounts;
+ evidence?: Record;
+ threshold?: {
+ operator?: string;
+ value?: number;
+ unit?: string;
+ };
+};
+
+export type FairCertificationQuestionAnswer = {
+ answer?: string;
+ editedAt?: string;
+ editedBy?: string;
+ editedDate?: string;
+};
+
+export type FairCertificationQuestion = {
+ id: string;
+ number: string;
+ section: string;
+ type: FairCertificationQuestionType;
+ required: boolean;
+ question: string;
+ description: string;
+ recommendation: string;
+ answer?: FairCertificationQuestionAnswer;
+ result?: FairCertificationQuestionResult;
+};
+
+export type FairCertificationRepository = {
+ repositoryId: number | string;
+ repositoryName: string;
+ organisationName: string;
+ countryCode: string;
+};
+
+export type FairCertificationSubmissionReviewStatus = 'pending' | 'approved' | string;
+
+export type FairCertificationSubmission = {
+ id: number;
+ submissionNumber: number;
+ submittedBy: string;
+ submittedAt: string;
+ submissionDate: string;
+ pdfGeneratedAt: string;
+ reportUrl: string;
+ reviewStatus: FairCertificationSubmissionReviewStatus;
+ reviewedBy: string | null;
+ reviewedAt: string | null;
+ reviewNotes: string | null;
+};
+
+export type FairCertificationSubmissionsApiResponse = {
+ submissions: FairCertificationSubmission[];
+};
+
+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 = {
+ 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/features/Fair/types/fairPricingTypes.ts b/src/features/Fair/types/fairPricingTypes.ts
new file mode 100644
index 000000000..2124f1589
--- /dev/null
+++ b/src/features/Fair/types/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/types/fairPrinciples.types.ts b/src/features/Fair/types/fairPrinciples.types.ts
new file mode 100644
index 000000000..d6ce7d5fa
--- /dev/null
+++ b/src/features/Fair/types/fairPrinciples.types.ts
@@ -0,0 +1,44 @@
+import type { FairCertificationQuestion } from '@features/Fair/types/fairCertification.types';
+
+export type FairMetricLine = {
+ label: string;
+ value: string;
+};
+
+export type FairQuestionItem = {
+ id: string;
+ code: string;
+ /** Matches API question `number` (e.g. "1.1", "2.4"). */
+ number?: string;
+ /** Links a display-only sub-row to an open question's API result (e.g. "1.3", "2.1"). */
+ linkedQuestionNumber?: string;
+ question: string;
+ description?: string;
+ recommendation?: string;
+ answerPlaceholder?: string;
+ statusLabel?: 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;
+ certificationQuestion?: FairCertificationQuestion;
+};
+
+export type FairPrincipleSection = {
+ title: string;
+ description: string;
+ items: FairQuestionItem[];
+};
+
+export const FAIR_PRINCIPLE_SECTION_KEYS = [
+ 'findable',
+ 'accessible',
+ 'interoperable',
+ 'reusable',
+ 'fitForFunders',
+ 'fitForFuture',
+ 'operational',
+] as const;
+
+export type FairPrincipleSectionKey = (typeof FAIR_PRINCIPLE_SECTION_KEYS)[number];
diff --git a/src/features/Fair/utils/downloadFairCertificatePdf.ts b/src/features/Fair/utils/downloadFairCertificatePdf.ts
new file mode 100644
index 000000000..d087d2c8f
--- /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);
+};
diff --git a/src/features/Fair/utils/downloadFairCertificatePng.ts b/src/features/Fair/utils/downloadFairCertificatePng.ts
new file mode 100644
index 000000000..e124e610d
--- /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);
+};
diff --git a/src/features/Fair/utils/formatFairResultName.ts b/src/features/Fair/utils/formatFairResultName.ts
new file mode 100644
index 000000000..711233a76
--- /dev/null
+++ b/src/features/Fair/utils/formatFairResultName.ts
@@ -0,0 +1,11 @@
+export const formatFairResultName = (name: string): string => {
+ const formattedName = name
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2')
+ .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2');
+
+ if (!formattedName) {
+ return formattedName;
+ }
+
+ return formattedName.charAt(0).toUpperCase() + formattedName.slice(1);
+};
diff --git a/src/features/Fair/utils/formatFairSubmissionLabel.ts b/src/features/Fair/utils/formatFairSubmissionLabel.ts
new file mode 100644
index 000000000..767c9acf3
--- /dev/null
+++ b/src/features/Fair/utils/formatFairSubmissionLabel.ts
@@ -0,0 +1,21 @@
+const getOrdinalSuffix = (value: number): string => {
+ const remainder = value % 100;
+
+ if (remainder >= 11 && remainder <= 13) {
+ return 'th';
+ }
+
+ switch (value % 10) {
+ case 1:
+ return 'st';
+ case 2:
+ return 'nd';
+ case 3:
+ return 'rd';
+ default:
+ return 'th';
+ }
+};
+
+export const formatFairSubmissionLabel = (submissionNumber: number): string =>
+ `${submissionNumber}${getOrdinalSuffix(submissionNumber)} submission`;
diff --git a/src/features/Fair/utils/getFairQuestionStatusClassName.ts b/src/features/Fair/utils/getFairQuestionStatusClassName.ts
new file mode 100644
index 000000000..310cdbd09
--- /dev/null
+++ b/src/features/Fair/utils/getFairQuestionStatusClassName.ts
@@ -0,0 +1,16 @@
+export const getFairQuestionStatusClassName = (statusLabel?: string): string => {
+ if (!statusLabel) {
+ return 'support-status__status--neutral';
+ }
+ const normalized = statusLabel.trim().toLowerCase();
+ if (normalized === 'yes') {
+ return 'support-status__status--yes';
+ }
+ if (normalized === 'no') {
+ return 'support-status__status--no';
+ }
+ if (normalized.includes('error')) {
+ return 'support-status__status--no';
+ }
+ return 'support-status__status--neutral';
+};
diff --git a/src/features/Fair/utils/isFairOpenQuestion.ts b/src/features/Fair/utils/isFairOpenQuestion.ts
new file mode 100644
index 000000000..cf0d65244
--- /dev/null
+++ b/src/features/Fair/utils/isFairOpenQuestion.ts
@@ -0,0 +1,4 @@
+import type { FairCertificationQuestion } from '@features/Fair/types/fairCertification.types';
+
+export const isFairOpenQuestion = (certificationQuestion?: FairCertificationQuestion): boolean =>
+ certificationQuestion?.type === 'open';
diff --git a/src/features/Fair/utils/resolveFairQuestionCountValues.ts b/src/features/Fair/utils/resolveFairQuestionCountValues.ts
new file mode 100644
index 000000000..e9817f2e7
--- /dev/null
+++ b/src/features/Fair/utils/resolveFairQuestionCountValues.ts
@@ -0,0 +1,26 @@
+import type {FairCertificationQuestionCounts} from '@features/Fair/types/fairCertification.types';
+
+export type FairQuestionResultNamedValue = {
+ name: string;
+ value: number;
+};
+
+const hasCountValue = (value: number | null | undefined): value is number => value != null;
+
+export const resolveFairQuestionCountValues = (
+ counts?: FairCertificationQuestionCounts,
+): FairQuestionResultNamedValue[] => {
+ if (!counts) {
+ return [];
+ }
+
+ if (Array.isArray(counts)) {
+ return counts
+ .filter((entry): entry is {name: string; value: number} => hasCountValue(entry.value))
+ .map((entry) => ({name: entry.name, value: entry.value}));
+ }
+
+ return Object.entries(counts)
+ .filter((entry): entry is [string, number] => hasCountValue(entry[1]))
+ .map(([name, value]) => ({name, value}));
+};
diff --git a/src/features/Fair/utils/resolveFairQuestionMetricValues.ts b/src/features/Fair/utils/resolveFairQuestionMetricValues.ts
new file mode 100644
index 000000000..33532ec57
--- /dev/null
+++ b/src/features/Fair/utils/resolveFairQuestionMetricValues.ts
@@ -0,0 +1,24 @@
+import type {FairCertificationQuestionMetrics} from '@features/Fair/types/fairCertification.types';
+
+import type {FairQuestionResultNamedValue} from '@features/Fair/utils/resolveFairQuestionCountValues';
+
+const hasMetricValue = (value: number | null | undefined): value is number =>
+ value != null && value !== 0;
+
+export const resolveFairQuestionMetricValues = (
+ metrics?: FairCertificationQuestionMetrics,
+): FairQuestionResultNamedValue[] => {
+ if (!metrics) {
+ return [];
+ }
+
+ if (Array.isArray(metrics)) {
+ return metrics
+ .filter((entry): entry is {name: string; value: number} => hasMetricValue(entry.value))
+ .map((entry) => ({name: entry.name, value: entry.value}));
+ }
+
+ return Object.entries(metrics)
+ .filter((entry): entry is [string, number] => hasMetricValue(entry[1]))
+ .map(([name, value]) => ({name, value}));
+};
diff --git a/src/features/Fair/utils/resolveFairQuestionStatusLabel.ts b/src/features/Fair/utils/resolveFairQuestionStatusLabel.ts
new file mode 100644
index 000000000..af7642ee0
--- /dev/null
+++ b/src/features/Fair/utils/resolveFairQuestionStatusLabel.ts
@@ -0,0 +1,21 @@
+import type { FairCertificationQuestion } from '@features/Fair/types/fairCertification.types';
+
+export const resolveFairQuestionStatusLabel = (
+ certificationQuestion?: FairCertificationQuestion,
+): string => {
+ if (!certificationQuestion?.result) {
+ return '_';
+ }
+
+ const status = certificationQuestion.result.status?.toLowerCase();
+
+ if (status === 'pass') {
+ return 'YES';
+ }
+
+ if (status === 'fail') {
+ return 'No';
+ }
+
+ return '_';
+};
diff --git a/src/features/Fair/utils/shouldShowFairQuestionResults.ts b/src/features/Fair/utils/shouldShowFairQuestionResults.ts
new file mode 100644
index 000000000..5c2aa28d4
--- /dev/null
+++ b/src/features/Fair/utils/shouldShowFairQuestionResults.ts
@@ -0,0 +1,28 @@
+import type { FairQuestionItem } from '@features/Fair/types/fairPrinciples.types';
+import { isFairOpenQuestion } from '@features/Fair/utils/isFairOpenQuestion';
+
+export const shouldShowFairQuestionResults = (
+ item: FairQuestionItem,
+ sectionItems: FairQuestionItem[],
+): boolean => {
+ if (!item.certificationQuestion) {
+ return false;
+ }
+
+ if (item.linkedQuestionNumber) {
+ return true;
+ }
+
+ const isOpenQuestion =
+ Boolean(item.number) && isFairOpenQuestion(item.certificationQuestion);
+
+ if (!isOpenQuestion) {
+ return true;
+ }
+
+ const defersResultsToLinkedRow = sectionItems.some(
+ (sectionItem) => sectionItem.linkedQuestionNumber === item.number,
+ );
+
+ return !defersResultsToLinkedRow;
+};
diff --git a/src/features/Usrn/UsrnView.tsx b/src/features/Usrn/UsrnView.tsx
index e7d0dbfa3..293ec91f5 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 25cd5e6dd..9dce94db5 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/indexing) 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/indexing) 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"
+ }
+ ]
}
diff --git a/src/features/Usrn/types/data.types.ts b/src/features/Usrn/types/data.types.ts
index aa0159b24..d05149cc7 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 538cf3ef8..8e8adb14d 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',
@@ -42,6 +61,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 +74,7 @@ export const getCardStatusConfig = ({
countValue,
};
}
+ // TODO check for 2.1
case 'accessFullTexts': {
const countCovered = toNumber(internalStatistics?.fullTextCount);
const countTotal = toNumber(internalStatistics?.metadataCount);
@@ -76,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);
@@ -93,6 +159,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);
diff --git a/src/features/Validator/hooks/useRioxxStats.ts b/src/features/Validator/hooks/useRioxxStats.ts
index 79cd88b3e..69afd12ab 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)
)
),
diff --git a/src/pages/FAIRCertification.tsx b/src/pages/FAIRCertification.tsx
index 9297a789f..ca3fc1e74 100644
--- a/src/pages/FAIRCertification.tsx
+++ b/src/pages/FAIRCertification.tsx
@@ -1,35 +1,60 @@
import { useDocumentTitle } from '@hooks/useDocumentTitle.ts';
-import { useState, useEffect, useRef } from 'react';
-import retrieveContent from '@/utils/retrieveContent';
+import { useState, useEffect } from 'react';
import { FairFeature, type FairCertificationData } from '@features/Fair/Fair.tsx';
+import { ApprovedFairView } from '@features/Fair/components/ApprovedFairView.tsx';
+// import { useOrganisation } from '@features/Settings/OrganisationalSettings/hooks/useOrganisation';
+// import { useStartingOrSupportingBillingPlanData } from '@features/Orcid/hooks/useStartingOrSupportingBillingPlanData';
+import { useDataProviderStore } from '@/store/dataProviderStore';
+import fairCertificationLanding from '@features/Fair/texts/fairCertificationLanding.json';
-const loadFairCertification = async (ref?: string): Promise => {
- return (await retrieveContent('fair-certification', {
- ref,
- transform: 'object',
- })) as FairCertificationData;
-};
+const FAIR_REGISTER_INTEREST_FORM_URL =
+ 'https://docs.google.com/forms/d/e/1FAIpQLScVAzXyEoPNBno9qorv2pQU9QmUalagtcoRn9Tze4V5TQZ1Pw/viewform?usp=dialog';
+
+const SUCCESS_MESSAGE_DURATION_MS = 5000;
+// TODO temp show
+const APPROVED_FAIR_VIEW_DATA_PROVIDER_IDS = [1, 86] as const;
+
+const fairCertificationData = fairCertificationLanding as FairCertificationData;
+
+const canAccessApprovedFairView = (dataProviderId?: number): boolean =>
+ dataProviderId != null &&
+ (APPROVED_FAIR_VIEW_DATA_PROVIDER_IDS as readonly number[]).includes(dataProviderId);
export function FAIRCertificationPage() {
useDocumentTitle('FAIR Certification');
- const [stateData, setStateData] = useState(null);
- const hasLoadedRef = useRef(false);
+ const { selectedDataProvider } = useDataProviderStore();
+ // const { organisation } = useOrganisation();
+ // const { isStartingOrSupportingPlan } = useStartingOrSupportingBillingPlanData([], organisation);
+ const [showSuccessMessage, setShowSuccessMessage] = useState(false);
+
+ const dataProviderId = selectedDataProvider?.id;
+
+ // const handleRegisterInterest = useCallback(() => {
+ // setShowSuccessMessage(true);
+ // }, []);
useEffect(() => {
- if (hasLoadedRef.current) {
+ if (!showSuccessMessage) {
return;
}
- hasLoadedRef.current = true;
+ const timer = setTimeout(() => {
+ setShowSuccessMessage(false);
+ }, SUCCESS_MESSAGE_DURATION_MS);
- loadFairCertification().then((content) => {
- setStateData(content);
- });
- }, []);
+ return () => clearTimeout(timer);
+ }, [showSuccessMessage]);
- if (!stateData) {
- return null;
+ if (canAccessApprovedFairView(dataProviderId) && !showSuccessMessage) {
+ return ;
}
- return ;
+ return (
+
+ );
}
diff --git a/src/utils/dateUtils.ts b/src/utils/dateUtils.ts
index ce3d7f12f..10f1ec001 100644
--- a/src/utils/dateUtils.ts
+++ b/src/utils/dateUtils.ts
@@ -1,3 +1,5 @@
+import dayjs from 'dayjs';
+
export const formatDate = (dateString: string): string => {
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');
+};