initial commit

This commit is contained in:
2026-09-11 02:46:56 +02:00
commit 20b384039c
288 changed files with 46557 additions and 0 deletions
+37
View File
@@ -0,0 +1,37 @@
import type { StrapiApp } from '@strapi/strapi/admin';
export default {
config: {
locales: [
// 'ar',
// 'fr',
// 'cs',
// 'de',
// 'da',
// 'es',
// 'he',
// 'id',
// 'it',
// 'ja',
// 'ko',
// 'ms',
// 'nl',
// 'no',
// 'pl',
// 'pt-BR',
// 'pt',
// 'ru',
// 'sk',
// 'sv',
// 'th',
// 'tr',
// 'uk',
// 'vi',
// 'zh-Hans',
// 'zh',
],
},
bootstrap(app: StrapiApp) {
console.log(app);
},
};
+20
View File
@@ -0,0 +1,20 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "Bundler",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["../plugins/**/admin/src/**/*", "./"],
"exclude": ["node_modules/", "build/", "dist/", "**/*.test.ts"]
}
+12
View File
@@ -0,0 +1,12 @@
import { mergeConfig, type UserConfig } from 'vite';
export default (config: UserConfig) => {
// Important: always return the modified config
return mergeConfig(config, {
resolve: {
alias: {
'@': '/src',
},
},
});
};
View File
@@ -0,0 +1,113 @@
{
"kind": "singleType",
"collectionName": "about_pages",
"info": {
"singularName": "about-page",
"pluralName": "about-pages",
"displayName": "About Page",
"description": "About and Strategy: who SHARP-ER is, the consortium and the SHARP legacy."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"hero": {
"type": "component",
"repeatable": false,
"component": "shared.hero",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"body": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"blocks": {
"type": "dynamiczone",
"components": [
"blocks.rich-text",
"blocks.image-gallery",
"blocks.stats-grid",
"blocks.cta-banner",
"blocks.card-grid",
"blocks.quote",
"blocks.accordion",
"blocks.video-embed",
"blocks.partner-logos"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"consortium": {
"type": "component",
"repeatable": true,
"component": "page.consortium-member",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"pillars": {
"type": "component",
"repeatable": true,
"component": "shared.titled-text",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"legacyHeadline": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"legacyBody": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"legacyLink": {
"type": "component",
"repeatable": false,
"component": "shared.link",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"seo": {
"type": "component",
"repeatable": false,
"component": "shared.seo",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* about-page controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::about-page.about-page');
@@ -0,0 +1,7 @@
/**
* about-page router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::about-page.about-page');
@@ -0,0 +1,7 @@
/**
* about-page service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::about-page.about-page');
@@ -0,0 +1,159 @@
{
"kind": "collectionType",
"collectionName": "activities",
"info": {
"singularName": "activity",
"pluralName": "activities",
"displayName": "Activity",
"description": "Activities and initiatives delivered under the SHARP-ER workstreams."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"title": {
"type": "string",
"required": true,
"maxLength": 250,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"summary": {
"type": "text",
"maxLength": 500,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"body": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"blocks": {
"type": "dynamiczone",
"components": [
"blocks.rich-text",
"blocks.image-gallery",
"blocks.stats-grid",
"blocks.cta-banner",
"blocks.card-grid",
"blocks.quote",
"blocks.accordion",
"blocks.video-embed",
"blocks.partner-logos"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"activityStream": {
"type": "enumeration",
"enum": [
"sharp-network",
"cso-subprojects-p2r",
"suq-dialogues",
"human-security",
"research"
],
"required": true,
"default": "sharp-network",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"implementingPartners": {
"type": "relation",
"relation": "oneToMany",
"target": "api::partner.partner"
},
"regions": {
"type": "relation",
"relation": "oneToMany",
"target": "api::region.region"
},
"topics": {
"type": "relation",
"relation": "oneToMany",
"target": "api::topic.topic"
},
"activityStatus": {
"type": "enumeration",
"enum": [
"planned",
"ongoing",
"completed"
],
"required": true,
"default": "ongoing",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"coverImage": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"gallery": {
"type": "media",
"multiple": true,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"seo": {
"type": "component",
"repeatable": false,
"component": "shared.seo",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* activity controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::activity.activity');
@@ -0,0 +1,7 @@
/**
* activity router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::activity.activity');
@@ -0,0 +1,7 @@
/**
* activity service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::activity.activity');
@@ -0,0 +1,24 @@
/**
* application lifecycles
*
* `submittedAt` is never required in the schema, because the public API sets it
* in the sanitize-submission middleware rather than trusting the caller. This
* backfills it for entries created any other way (the admin panel, a seed
* script) so the column is never null.
*/
interface CreateEvent {
params: {
data: Record<string, unknown>;
};
}
export default {
beforeCreate(event: CreateEvent) {
const { data } = event.params;
if (!data.submittedAt) {
data.submittedAt = new Date().toISOString();
}
},
};
@@ -0,0 +1,70 @@
{
"kind": "collectionType",
"collectionName": "applications",
"info": {
"singularName": "application",
"pluralName": "applications",
"displayName": "Application",
"description": "A submitted application against an opportunity. Not translated; managed by staff."
},
"options": {
"draftAndPublish": false
},
"attributes": {
"opportunity": {
"type": "relation",
"relation": "oneToOne",
"target": "api::opportunity.opportunity"
},
"applicantName": {
"type": "string",
"required": true,
"maxLength": 200
},
"email": {
"type": "email",
"required": true
},
"phone": {
"type": "string"
},
"organisation": {
"type": "string",
"maxLength": 200
},
"organisationType": {
"type": "string",
"maxLength": 200
},
"answers": {
"type": "json"
},
"attachments": {
"type": "media",
"multiple": true,
"required": false,
"allowedTypes": [
"images",
"files"
]
},
"applicationStatus": {
"type": "enumeration",
"enum": [
"submitted",
"under-review",
"shortlisted",
"accepted",
"rejected"
],
"required": true,
"default": "submitted"
},
"submittedAt": {
"type": "datetime"
},
"acknowledgementSentAt": {
"type": "datetime"
}
}
}
@@ -0,0 +1,7 @@
/**
* application controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::application.application');
@@ -0,0 +1,56 @@
/**
* application sanitize-submission middleware
*
* The public role may create applications, so the request body is
* attacker-controlled. Only the fields an applicant is allowed to supply are
* copied out of it; the review state and the timestamps are set here, on the
* server, and anything else the caller sent is dropped.
*
* `attachments` is deliberately NOT on the whitelist. It is a media relation,
* so its value is a list of ids of files that are ALREADY in the Media
* Library - accepting it from an unauthenticated caller lets anyone bind any
* uploaded file, including staff-only documents, onto an application row that
* reviewers then open. The Public role cannot upload either, so there is no
* legitimate id a public applicant could send. Attachments are added by staff
* in the admin panel; a public upload flow would need its own authenticated
* endpoint that returns ids scoped to the submission being created.
*/
import type { Core } from '@strapi/strapi';
const CLIENT_FIELDS = [
'opportunity',
'applicantName',
'email',
'phone',
'organisation',
'organisationType',
'answers',
] as const;
const sanitizeSubmission: Core.MiddlewareFactory = () => async (ctx, next) => {
const request = ctx.request as typeof ctx.request & { body?: unknown };
const body = (request.body ?? {}) as { data?: unknown };
const input = (typeof body.data === 'object' && body.data !== null ? body.data : {}) as Record<
string,
unknown
>;
const data: Record<string, unknown> = {};
for (const field of CLIENT_FIELDS) {
if (field in input) {
data[field] = input[field];
}
}
data.applicationStatus = 'submitted';
data.submittedAt = new Date().toISOString();
data.acknowledgementSentAt = null;
request.body = { data };
await next();
};
export default sanitizeSubmission;
@@ -0,0 +1,31 @@
/**
* application router
*
* `create` is the one action the Public role holds here, so the POST route
* carries two middlewares and the ORDER matters:
*
* 1. `global::rate-limit` caps how often one IP may post, before any
* work is done on the body.
* 2. `api::application.sanitize-submission`
* drops every field that is not on the client
* whitelist and sets the server-owned state.
*
* See `src/middlewares/rate-limit.ts` for the limiter's scope and its
* single-instance caveat.
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::application.application', {
config: {
create: {
middlewares: [
{
name: 'global::rate-limit',
config: { name: 'application-create', interval: 60_000, max: 5 },
},
'api::application.sanitize-submission',
],
},
},
});
@@ -0,0 +1,7 @@
/**
* application service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::application.application');
@@ -0,0 +1,84 @@
{
"kind": "singleType",
"collectionName": "contact_pages",
"info": {
"singularName": "contact-page",
"pluralName": "contact-pages",
"displayName": "Contact Page",
"description": "Contact details for SHARP-ER and each consortium member."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"hero": {
"type": "component",
"repeatable": false,
"component": "shared.hero",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"intro": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"generalEmail": {
"type": "email",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"websiteLabel": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"socialLinks": {
"type": "component",
"repeatable": true,
"component": "shared.link",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"consortiumContacts": {
"type": "component",
"repeatable": true,
"component": "shared.address",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"seo": {
"type": "component",
"repeatable": false,
"component": "shared.seo",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* contact-page controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::contact-page.contact-page');
@@ -0,0 +1,7 @@
/**
* contact-page router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::contact-page.contact-page');
@@ -0,0 +1,7 @@
/**
* contact-page service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::contact-page.contact-page');
@@ -0,0 +1,24 @@
/**
* contact-submission lifecycles
*
* `submittedAt` is never required in the schema, because the public API sets it
* in the sanitize-submission middleware rather than trusting the caller. This
* backfills it for entries created any other way (the admin panel, a seed
* script) so the column is never null.
*/
interface CreateEvent {
params: {
data: Record<string, unknown>;
};
}
export default {
beforeCreate(event: CreateEvent) {
const { data } = event.params;
if (!data.submittedAt) {
data.submittedAt = new Date().toISOString();
}
},
};
@@ -0,0 +1,50 @@
{
"kind": "collectionType",
"collectionName": "contact_submissions",
"info": {
"singularName": "contact-submission",
"pluralName": "contact-submissions",
"displayName": "Contact Submission",
"description": "A message sent through the contact form. Not translated; managed by staff."
},
"options": {
"draftAndPublish": false
},
"attributes": {
"name": {
"type": "string",
"required": true,
"maxLength": 200
},
"organisation": {
"type": "string",
"maxLength": 200
},
"email": {
"type": "email",
"required": true
},
"subject": {
"type": "string",
"required": true,
"maxLength": 250
},
"message": {
"type": "text",
"required": true
},
"submissionStatus": {
"type": "enumeration",
"enum": [
"new",
"in-progress",
"resolved"
],
"required": true,
"default": "new"
},
"submittedAt": {
"type": "datetime"
}
}
}
@@ -0,0 +1,7 @@
/**
* contact-submission controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::contact-submission.contact-submission');
@@ -0,0 +1,38 @@
/**
* contact-submission sanitize-submission middleware
*
* The public role may create contact submissions, so the request body is
* attacker-controlled. Only the fields a visitor is allowed to supply are
* copied out of it; the triage state and the timestamp are set here, on the
* server, and anything else the caller sent is dropped.
*/
import type { Core } from '@strapi/strapi';
const CLIENT_FIELDS = ['name', 'organisation', 'email', 'subject', 'message'] as const;
const sanitizeSubmission: Core.MiddlewareFactory = () => async (ctx, next) => {
const request = ctx.request as typeof ctx.request & { body?: unknown };
const body = (request.body ?? {}) as { data?: unknown };
const input = (typeof body.data === 'object' && body.data !== null ? body.data : {}) as Record<
string,
unknown
>;
const data: Record<string, unknown> = {};
for (const field of CLIENT_FIELDS) {
if (field in input) {
data[field] = input[field];
}
}
data.submissionStatus = 'new';
data.submittedAt = new Date().toISOString();
request.body = { data };
await next();
};
export default sanitizeSubmission;
@@ -0,0 +1,31 @@
/**
* contact-submission router
*
* `create` is the one action the Public role holds here, so the POST route
* carries two middlewares and the ORDER matters:
*
* 1. `global::rate-limit` caps how often one IP may post, before any
* work is done on the body.
* 2. `api::contact-submission.sanitize-submission`
* drops every field that is not on the client
* whitelist and sets the server-owned state.
*
* See `src/middlewares/rate-limit.ts` for the limiter's scope and its
* single-instance caveat.
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::contact-submission.contact-submission', {
config: {
create: {
middlewares: [
{
name: 'global::rate-limit',
config: { name: 'contact-submission-create', interval: 60_000, max: 5 },
},
'api::contact-submission.sanitize-submission',
],
},
},
});
@@ -0,0 +1,7 @@
/**
* contact-submission service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::contact-submission.contact-submission');
@@ -0,0 +1,24 @@
/**
* event-registration lifecycles
*
* `submittedAt` is never required in the schema, because the public API sets it
* in the sanitize-submission middleware rather than trusting the caller. This
* backfills it for entries created any other way (the admin panel, a seed
* script) so the column is never null.
*/
interface CreateEvent {
params: {
data: Record<string, unknown>;
};
}
export default {
beforeCreate(event: CreateEvent) {
const { data } = event.params;
if (!data.submittedAt) {
data.submittedAt = new Date().toISOString();
}
},
};
@@ -0,0 +1,64 @@
{
"kind": "collectionType",
"collectionName": "event_registrations",
"info": {
"singularName": "event-registration",
"pluralName": "event-registrations",
"displayName": "Event Registration",
"description": "A submitted registration for an event. Not translated; managed by staff."
},
"options": {
"draftAndPublish": false
},
"attributes": {
"event": {
"type": "relation",
"relation": "oneToOne",
"target": "api::event.event"
},
"fullName": {
"type": "string",
"required": true,
"maxLength": 200
},
"email": {
"type": "email",
"required": true
},
"phone": {
"type": "string"
},
"organisation": {
"type": "string",
"maxLength": 200
},
"role": {
"type": "string",
"maxLength": 200
},
"dietaryOrAccess": {
"type": "text"
},
"registrationState": {
"type": "enumeration",
"enum": [
"confirmed",
"waitlisted",
"cancelled",
"attended"
],
"required": true,
"default": "confirmed"
},
"consentGdpr": {
"type": "boolean",
"required": true
},
"submittedAt": {
"type": "datetime"
},
"reminderSentAt": {
"type": "datetime"
}
}
}
@@ -0,0 +1,7 @@
/**
* event-registration controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::event-registration.event-registration');
@@ -0,0 +1,180 @@
/**
* event-registration sanitize-submission middleware
*
* The public role may create event registrations, so the request body is
* attacker-controlled. Only the fields a registrant is allowed to supply are
* copied out of it; the workflow state and the timestamps are set here, on the
* server, and anything else the caller sent is dropped.
*
* REGISTRATION STATE IS DERIVED, NEVER SENT
* `registrationState` used to be hard-set to `confirmed`, which meant a FULL
* event silently confirmed everyone and staff had to unpick it by hand. It is
* now worked out here, from the event itself:
*
* capacity reached (or the editors marked the event `full`)
* + a waitlist -> `waitlisted`
* + no waitlist -> the request is REFUSED with a 400
* otherwise -> `confirmed`
*
* This is the only place that decision can safely be made. Putting it in the
* browser would let anyone confirm a place on a sold-out event with one
* `curl`, and the count it depends on - how many places are already taken - is
* not something a client can be trusted to report.
*
* The count is read and the row is written without a transaction, so two
* registrations arriving in the same instant can both see the last free place.
* Overbooking by the number of concurrent requests is the accepted trade-off
* for a public form; staff see the overflow in the admin panel.
*/
import type { Core } from '@strapi/strapi';
import { errors } from '@strapi/utils';
const CLIENT_FIELDS = [
'event',
'fullName',
'email',
'phone',
'organisation',
'role',
'dietaryOrAccess',
'consentGdpr',
] as const;
const EVENT_UID = 'api::event.event';
const REGISTRATION_UID = 'api::event-registration.event-registration';
/** The subset of the event that decides whether there is a place left. */
type EventCapacity = {
documentId: string;
capacity: number | null;
waitlistEnabled: boolean | null;
registrationStatus: string | null;
};
/**
* The `event` value a client may send is a documentId (what the REST API uses
* for relations), and older clients may send a numeric id. Both are accepted;
* anything else is treated as "no event given".
*/
const readEventReference = (value: unknown): { documentId?: string; id?: number } => {
if (typeof value === 'string' && value.length > 0) {
return { documentId: value };
}
if (typeof value === 'number' && Number.isInteger(value)) {
return { id: value };
}
// `{ connect: [...] }` / `{ set: [...] }` and arrays are relation-write
// shapes a public form has no business sending, so they resolve to nothing
// and the registration is treated as having no known event.
return {};
};
/**
* `capacity`, `waitlistEnabled` and `registrationStatus` are not localised, so
* any row sharing the documentId carries the same values and the first match
* is enough.
*/
const findEvent = async (
strapi: Core.Strapi,
reference: { documentId?: string; id?: number }
): Promise<EventCapacity | null> => {
if (!reference.documentId && reference.id === undefined) {
return null;
}
const where = reference.documentId
? { documentId: reference.documentId }
: { id: reference.id };
return (await strapi.db.query(EVENT_UID).findOne({
where,
select: ['documentId', 'capacity', 'waitlistEnabled', 'registrationStatus'],
})) as EventCapacity | null;
};
/** How many places on this event are already taken. */
const countConfirmed = async (strapi: Core.Strapi, documentId: string): Promise<number> =>
strapi.db.query(REGISTRATION_UID).count({
where: {
event: { documentId },
registrationState: 'confirmed',
},
});
type RegistrationState = 'confirmed' | 'waitlisted';
/**
* Work out the state this registration is entitled to.
*
* Throws `ApplicationError` (HTTP 400) when the event has no place and no
* waitlist to offer - the honest answer to "may I register?" is no, and
* storing a row that says `confirmed` would be a lie to both the registrant
* and the staff reading the list.
*/
const resolveRegistrationState = async (
strapi: Core.Strapi,
eventValue: unknown
): Promise<RegistrationState> => {
const event = await findEvent(strapi, readEventReference(eventValue));
if (!event) {
// Either no event was given or the id does not resolve. There is no
// capacity to weigh, and nothing to confirm a place on.
return 'waitlisted';
}
if (event.registrationStatus === 'closed') {
throw new errors.ApplicationError('Registration for this event is closed.');
}
const capacity =
typeof event.capacity === 'number' && event.capacity > 0 ? event.capacity : null;
const taken = capacity === null ? 0 : await countConfirmed(strapi, event.documentId);
const isFull =
event.registrationStatus === 'full' || (capacity !== null && taken >= capacity);
if (!isFull) {
return 'confirmed';
}
const waitlistOpen = event.waitlistEnabled === true || event.registrationStatus === 'waitlist';
if (!waitlistOpen) {
throw new errors.ApplicationError('This event is full and has no waitlist.');
}
return 'waitlisted';
};
const sanitizeSubmission: Core.MiddlewareFactory =
(_config, { strapi }) =>
async (ctx, next) => {
const request = ctx.request as typeof ctx.request & { body?: unknown };
const body = (request.body ?? {}) as { data?: unknown };
const input = (typeof body.data === 'object' && body.data !== null ? body.data : {}) as Record<
string,
unknown
>;
const data: Record<string, unknown> = {};
for (const field of CLIENT_FIELDS) {
if (field in input) {
data[field] = input[field];
}
}
data.registrationState = await resolveRegistrationState(strapi, data.event);
data.submittedAt = new Date().toISOString();
data.reminderSentAt = null;
request.body = { data };
await next();
};
export default sanitizeSubmission;
@@ -0,0 +1,31 @@
/**
* event-registration router
*
* `create` is the one action the Public role holds here, so the POST route
* carries two middlewares and the ORDER matters:
*
* 1. `global::rate-limit` caps how often one IP may post, before any
* work is done on the body.
* 2. `api::event-registration.sanitize-submission`
* drops every field that is not on the client
* whitelist and sets the server-owned state.
*
* See `src/middlewares/rate-limit.ts` for the limiter's scope and its
* single-instance caveat.
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::event-registration.event-registration', {
config: {
create: {
middlewares: [
{
name: 'global::rate-limit',
config: { name: 'event-registration-create', interval: 60_000, max: 5 },
},
'api::event-registration.sanitize-submission',
],
},
},
});
@@ -0,0 +1,7 @@
/**
* event-registration service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::event-registration.event-registration');
@@ -0,0 +1,239 @@
{
"kind": "collectionType",
"collectionName": "events",
"info": {
"singularName": "event",
"pluralName": "events",
"displayName": "Event",
"description": "Entries in the events calendar: Suq dialogues, trainings, law sessions, network meetings and launches."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"title": {
"type": "string",
"required": true,
"maxLength": 250,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"description": {
"type": "text",
"maxLength": 500,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"body": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"eventType": {
"type": "enumeration",
"enum": [
"suq-dialogue",
"training",
"law-session",
"network-meeting",
"launch",
"other"
],
"required": true,
"default": "other",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"startDateTime": {
"type": "datetime",
"required": true,
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"endDateTime": {
"type": "datetime",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"timezone": {
"type": "string",
"default": "Africa/Tripoli",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"format": {
"type": "enumeration",
"enum": [
"in-person",
"online",
"hybrid"
],
"required": true,
"default": "in-person",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"venue": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"onlineUrl": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"region": {
"type": "relation",
"relation": "oneToOne",
"target": "api::region.region"
},
"topics": {
"type": "relation",
"relation": "oneToMany",
"target": "api::topic.topic"
},
"implementingPartners": {
"type": "relation",
"relation": "oneToMany",
"target": "api::partner.partner"
},
"registrationStatus": {
"type": "enumeration",
"enum": [
"open",
"closed",
"full",
"waitlist",
"not-required"
],
"required": true,
"default": "not-required",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"capacity": {
"type": "integer",
"min": 0,
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"waitlistEnabled": {
"type": "boolean",
"default": false,
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"registrationDeadline": {
"type": "datetime",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"coverImage": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"gallery": {
"type": "media",
"multiple": true,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"recapUrl": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"relatedResources": {
"type": "relation",
"relation": "oneToMany",
"target": "api::resource.resource"
},
"seo": {
"type": "component",
"repeatable": false,
"component": "shared.seo",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* event controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::event.event');
+7
View File
@@ -0,0 +1,7 @@
/**
* event router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::event.event');
+7
View File
@@ -0,0 +1,7 @@
/**
* event service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::event.event');
@@ -0,0 +1,92 @@
{
"kind": "singleType",
"collectionName": "home_pages",
"info": {
"singularName": "home-page",
"pluralName": "home-pages",
"displayName": "Home Page",
"description": "Content of the SHARP-ER home page."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"hero": {
"type": "component",
"repeatable": false,
"component": "shared.hero",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"stats": {
"type": "component",
"repeatable": true,
"component": "shared.stat",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"blocks": {
"type": "dynamiczone",
"components": [
"blocks.rich-text",
"blocks.image-gallery",
"blocks.stats-grid",
"blocks.cta-banner",
"blocks.card-grid",
"blocks.quote",
"blocks.accordion",
"blocks.video-embed",
"blocks.partner-logos"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"featuredNews": {
"type": "relation",
"relation": "oneToMany",
"target": "api::news-article.news-article"
},
"featuredEvents": {
"type": "relation",
"relation": "oneToMany",
"target": "api::event.event"
},
"partnersHeadline": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"partners": {
"type": "relation",
"relation": "oneToMany",
"target": "api::partner.partner"
},
"seo": {
"type": "component",
"repeatable": false,
"component": "shared.seo",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* home-page controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::home-page.home-page');
@@ -0,0 +1,7 @@
/**
* home-page router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::home-page.home-page');
@@ -0,0 +1,7 @@
/**
* home-page service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::home-page.home-page');
@@ -0,0 +1,166 @@
{
"kind": "collectionType",
"collectionName": "knowledge_items",
"info": {
"singularName": "knowledge-item",
"pluralName": "knowledge-items",
"displayName": "Knowledge Item",
"description": "Long-term Knowledge Hub repository: training modules, toolkits, law session guides, case studies and FAQs."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"title": {
"type": "string",
"required": true,
"maxLength": 250,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"summary": {
"type": "text",
"maxLength": 1000,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"body": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"blocks": {
"type": "dynamiczone",
"components": [
"blocks.rich-text",
"blocks.image-gallery",
"blocks.stats-grid",
"blocks.cta-banner",
"blocks.card-grid",
"blocks.quote",
"blocks.accordion",
"blocks.video-embed",
"blocks.partner-logos"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"itemType": {
"type": "enumeration",
"enum": [
"training-module",
"toolkit",
"law-session-guide",
"case-study",
"faq"
],
"required": true,
"default": "toolkit",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"attachments": {
"type": "media",
"multiple": true,
"required": false,
"allowedTypes": [
"images",
"files"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"topics": {
"type": "relation",
"relation": "oneToMany",
"target": "api::topic.topic"
},
"relatedItems": {
"type": "relation",
"relation": "oneToMany",
"target": "api::knowledge-item.knowledge-item"
},
"relatedResources": {
"type": "relation",
"relation": "oneToMany",
"target": "api::resource.resource"
},
"keywords": {
"type": "json",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"sourceActivity": {
"type": "relation",
"relation": "oneToOne",
"target": "api::activity.activity"
},
"publicationDate": {
"type": "date",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"thumbnail": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"seo": {
"type": "component",
"repeatable": false,
"component": "shared.seo",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* knowledge-item controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::knowledge-item.knowledge-item');
@@ -0,0 +1,7 @@
/**
* knowledge-item router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::knowledge-item.knowledge-item');
@@ -0,0 +1,7 @@
/**
* knowledge-item service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::knowledge-item.knowledge-item');
@@ -0,0 +1,159 @@
{
"kind": "collectionType",
"collectionName": "news_articles",
"info": {
"singularName": "news-article",
"pluralName": "news-articles",
"displayName": "News Article",
"description": "News and updates, split between SHARP-ER news and stories from the network."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"title": {
"type": "string",
"required": true,
"maxLength": 250,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"excerpt": {
"type": "text",
"maxLength": 500,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"body": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"blocks": {
"type": "dynamiczone",
"components": [
"blocks.rich-text",
"blocks.image-gallery",
"blocks.stats-grid",
"blocks.cta-banner",
"blocks.card-grid",
"blocks.quote",
"blocks.accordion",
"blocks.video-embed",
"blocks.partner-logos"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"category": {
"type": "enumeration",
"enum": [
"sharper-news",
"from-the-network"
],
"required": true,
"default": "sharper-news",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"authorOrganisation": {
"type": "relation",
"relation": "oneToOne",
"target": "api::partner.partner"
},
"publishDate": {
"type": "date",
"required": true,
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"coverImage": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"gallery": {
"type": "media",
"multiple": true,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"topics": {
"type": "relation",
"relation": "oneToMany",
"target": "api::topic.topic"
},
"regions": {
"type": "relation",
"relation": "oneToMany",
"target": "api::region.region"
},
"featured": {
"type": "boolean",
"default": false,
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"seo": {
"type": "component",
"repeatable": false,
"component": "shared.seo",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* news-article controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::news-article.news-article');
@@ -0,0 +1,7 @@
/**
* news-article router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::news-article.news-article');
@@ -0,0 +1,7 @@
/**
* news-article service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::news-article.news-article');
@@ -0,0 +1,176 @@
{
"kind": "collectionType",
"collectionName": "opportunities",
"info": {
"singularName": "opportunity",
"pluralName": "opportunities",
"displayName": "Opportunity",
"description": "Calls for expression of interest, network membership, trainings and grants open for application."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"title": {
"type": "string",
"required": true,
"maxLength": 250,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"summary": {
"type": "text",
"maxLength": 500,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"body": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"opportunityType": {
"type": "enumeration",
"enum": [
"call-eoi",
"network-membership",
"training",
"grant",
"other"
],
"required": true,
"default": "call-eoi",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"opportunityStatus": {
"type": "enumeration",
"enum": [
"upcoming",
"open",
"closed"
],
"required": true,
"default": "upcoming",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"openDate": {
"type": "date",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"deadline": {
"type": "datetime",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"eligibility": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"howToApply": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"externalApplicationUrl": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"attachments": {
"type": "media",
"multiple": true,
"required": false,
"allowedTypes": [
"images",
"files"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"topics": {
"type": "relation",
"relation": "oneToMany",
"target": "api::topic.topic"
},
"implementingPartners": {
"type": "relation",
"relation": "oneToMany",
"target": "api::partner.partner"
},
"coverImage": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"seo": {
"type": "component",
"repeatable": false,
"component": "shared.seo",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* opportunity controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::opportunity.opportunity');
@@ -0,0 +1,7 @@
/**
* opportunity router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::opportunity.opportunity');
@@ -0,0 +1,7 @@
/**
* opportunity service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::opportunity.opportunity');
@@ -0,0 +1,106 @@
{
"kind": "collectionType",
"collectionName": "partners",
"info": {
"singularName": "partner",
"pluralName": "partners",
"displayName": "Partner",
"description": "Consortium members, CSO partners and institutions SHARP-ER works with."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"name": {
"type": "string",
"required": true,
"maxLength": 200,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"slug": {
"type": "uid",
"targetField": "name",
"required": true,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"logo": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"logoDark": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"description": {
"type": "text",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"website": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"partnerRole": {
"type": "enumeration",
"enum": [
"consortium-lead",
"consortium-partner",
"cso-partner",
"institution"
],
"required": true,
"default": "cso-partner",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"country": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* partner controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::partner.partner');
@@ -0,0 +1,7 @@
/**
* partner router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::partner.partner');
@@ -0,0 +1,7 @@
/**
* partner service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::partner.partner');
@@ -0,0 +1,40 @@
{
"kind": "collectionType",
"collectionName": "regions",
"info": {
"singularName": "region",
"pluralName": "regions",
"displayName": "Region",
"description": "Libyan region or city used to filter events and activities geographically."
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"name": {
"type": "string",
"required": true,
"maxLength": 120,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"slug": {
"type": "uid",
"targetField": "name",
"required": true,
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* region controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::region.region');
+7
View File
@@ -0,0 +1,7 @@
/**
* region router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::region.region');
@@ -0,0 +1,7 @@
/**
* region service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::region.region');
@@ -0,0 +1,214 @@
{
"kind": "collectionType",
"collectionName": "resources",
"info": {
"singularName": "resource",
"pluralName": "resources",
"displayName": "Resource",
"description": "Publications: perception research, policy articles, project reports and visibility factsheets."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"title": {
"type": "string",
"required": true,
"maxLength": 250,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"slug": {
"type": "uid",
"targetField": "title",
"required": true,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"description": {
"type": "text",
"maxLength": 1000,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"resourceType": {
"type": "enumeration",
"enum": [
"research-perception",
"policy-article",
"project-report",
"visibility-factsheet"
],
"required": true,
"default": "project-report",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"file": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"files",
"images",
"videos",
"audios"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"externalUrl": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"fileSizeLabel": {
"type": "string",
"maxLength": 40,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"pageCount": {
"type": "integer",
"min": 0,
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"resourceLanguage": {
"type": "enumeration",
"enum": [
"en",
"ar",
"bilingual"
],
"required": true,
"default": "en",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"publicationDate": {
"type": "date",
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"authors": {
"type": "string",
"maxLength": 500,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"partnerOrganisations": {
"type": "relation",
"relation": "oneToMany",
"target": "api::partner.partner"
},
"topics": {
"type": "relation",
"relation": "oneToMany",
"target": "api::topic.topic"
},
"doi": {
"type": "string",
"maxLength": 200,
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"handle": {
"type": "string",
"maxLength": 200,
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"version": {
"type": "string",
"default": "1.0",
"maxLength": 20,
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"supersedes": {
"type": "relation",
"relation": "oneToOne",
"target": "api::resource.resource"
},
"thumbnail": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"featured": {
"type": "boolean",
"default": false,
"pluginOptions": {
"i18n": {
"localized": false
}
}
},
"seo": {
"type": "component",
"repeatable": false,
"component": "shared.seo",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* resource controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::resource.resource');
@@ -0,0 +1,7 @@
/**
* resource router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::resource.resource');
@@ -0,0 +1,7 @@
/**
* resource service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::resource.resource');
@@ -0,0 +1,151 @@
{
"kind": "singleType",
"collectionName": "site_settings",
"info": {
"singularName": "site-settings",
"pluralName": "site-settings-list",
"displayName": "Site Settings",
"description": "Global, per-locale site chrome: navigation, footer, EU visibility statement and default SEO."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"siteName": {
"type": "string",
"required": true,
"default": "SHARP-ER",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"tagline": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"logo": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"logoDark": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"primaryNav": {
"type": "component",
"repeatable": true,
"component": "navigation.nav-group",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"footerColumns": {
"type": "component",
"repeatable": true,
"component": "navigation.footer-column",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"socialLinks": {
"type": "component",
"repeatable": true,
"component": "shared.link",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"euFundingStatement": {
"type": "text",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"euBadge": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"euBadgeDark": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"implementedByLabel": {
"type": "string",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"consortiumLogos": {
"type": "relation",
"relation": "oneToMany",
"target": "api::partner.partner"
},
"defaultSeo": {
"type": "component",
"repeatable": false,
"component": "shared.seo",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* site-settings controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::site-settings.site-settings');
@@ -0,0 +1,7 @@
/**
* site-settings router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::site-settings.site-settings');
@@ -0,0 +1,7 @@
/**
* site-settings service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::site-settings.site-settings');
@@ -0,0 +1,48 @@
{
"kind": "collectionType",
"collectionName": "topics",
"info": {
"singularName": "topic",
"pluralName": "topics",
"displayName": "Topic",
"description": "Thematic tag used to filter activities, events, news, resources and knowledge items."
},
"options": {
"draftAndPublish": false
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"name": {
"type": "string",
"required": true,
"maxLength": 120,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"slug": {
"type": "uid",
"targetField": "name",
"required": true,
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"description": {
"type": "text",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* topic controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::topic.topic');
+7
View File
@@ -0,0 +1,7 @@
/**
* topic router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::topic.topic');
+7
View File
@@ -0,0 +1,7 @@
/**
* topic service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::topic.topic');
@@ -0,0 +1,95 @@
{
"kind": "singleType",
"collectionName": "what_we_do_pages",
"info": {
"singularName": "what-we-do-page",
"pluralName": "what-we-do-pages",
"displayName": "What We Do Page",
"description": "Workstreams, principles and guiding values of SHARP-ER."
},
"options": {
"draftAndPublish": true
},
"pluginOptions": {
"i18n": {
"localized": true
}
},
"attributes": {
"hero": {
"type": "component",
"repeatable": false,
"component": "shared.hero",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"intro": {
"type": "richtext",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"workstreams": {
"type": "component",
"repeatable": true,
"component": "page.workstream",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"principles": {
"type": "component",
"repeatable": true,
"component": "shared.titled-text",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"guidingValues": {
"type": "json",
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"blocks": {
"type": "dynamiczone",
"components": [
"blocks.rich-text",
"blocks.image-gallery",
"blocks.stats-grid",
"blocks.cta-banner",
"blocks.card-grid",
"blocks.quote",
"blocks.accordion",
"blocks.video-embed",
"blocks.partner-logos"
],
"pluginOptions": {
"i18n": {
"localized": true
}
}
},
"seo": {
"type": "component",
"repeatable": false,
"component": "shared.seo",
"pluginOptions": {
"i18n": {
"localized": true
}
}
}
}
}
@@ -0,0 +1,7 @@
/**
* what-we-do-page controller
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreController('api::what-we-do-page.what-we-do-page');
@@ -0,0 +1,7 @@
/**
* what-we-do-page router
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreRouter('api::what-we-do-page.what-we-do-page');
@@ -0,0 +1,7 @@
/**
* what-we-do-page service
*/
import { factories } from '@strapi/strapi';
export default factories.createCoreService('api::what-we-do-page.what-we-do-page');
@@ -0,0 +1,91 @@
import type { Core } from '@strapi/strapi';
import {
AUTH_PERMISSION_PREFIX,
DISABLED_PUBLIC_AUTH_ACTIONS,
LOG_PREFIX,
PUBLIC_ROLE_TYPE,
readDeliberatePublicAuthActions,
} from './constants';
const PERMISSION_UID = 'plugin::users-permissions.permission';
const ROLE_UID = 'plugin::users-permissions.role';
/**
* Revoke the users-permissions AUTH endpoints from the Public role.
*
* WHY
* Strapi enables `auth.register` (and, depending on the version, `callback`
* and the password/e-mail-confirmation flows) for the Public role on a fresh
* database. SHARP-ER has no end-user accounts at all: the site is read-only
* plus three write-only submission forms, and staff sign in to the ADMIN
* panel, which is a different permission system entirely. Left as Strapi
* ships it, `POST /api/auth/local/register` lets anyone create an
* authenticated API account on a site that was never meant to have one, and
* every account created that way inherits whatever the Authenticated role is
* later given.
*
* A permission row in `up_permissions` IS the grant - the table has no
* `enabled` column - so revoking means deleting the row. Deleting a row that
* is not there is a no-op, which is what keeps this safe on every boot.
*
* IF THE SITE EVER NEEDS PUBLIC ACCOUNTS
* Do not delete this step. List the actions that are deliberately public in
* `PUBLIC_AUTH_ACTIONS` (comma-separated, e.g. `callback,forgotPassword`) and
* they are left alone; everything else stays revoked. That keeps the decision
* visible in the environment instead of hidden in a database row someone
* clicked once.
*/
export const revokePublicAuthActions = async (strapi: Core.Strapi): Promise<void> => {
if (!strapi.plugins['users-permissions']) {
strapi.log.warn(`${LOG_PREFIX} users-permissions plugin is disabled - skipping auth lockdown.`);
return;
}
const publicRole = await strapi.db.query(ROLE_UID).findOne({
where: { type: PUBLIC_ROLE_TYPE },
select: ['id'],
});
if (!publicRole) {
strapi.log.warn(`${LOG_PREFIX} no "${PUBLIC_ROLE_TYPE}" role found - skipping auth lockdown.`);
return;
}
const deliberate = readDeliberatePublicAuthActions();
const revokable = DISABLED_PUBLIC_AUTH_ACTIONS.filter((action) => !deliberate.has(action));
if (deliberate.size > 0) {
strapi.log.info(
`${LOG_PREFIX} PUBLIC_AUTH_ACTIONS keeps these public auth action(s): ` +
`${[...deliberate].sort().join(', ')}.`
);
}
if (revokable.length === 0) {
return;
}
const actions = revokable.map((action) => `${AUTH_PERMISSION_PREFIX}.${action}`);
const granted = (await strapi.db.query(PERMISSION_UID).findMany({
where: { role: { id: publicRole.id }, action: { $in: actions } },
select: ['id', 'action'],
})) as { id: number; action: string }[];
if (granted.length === 0) {
return;
}
await strapi.db.query(PERMISSION_UID).deleteMany({
where: { id: { $in: granted.map(({ id }) => id) } },
});
strapi.log.info(
`${LOG_PREFIX} revoked ${granted.length} public auth permission(s): ` +
`${granted
.map(({ action }) => action)
.sort()
.join(', ')}.`
);
};
+104
View File
@@ -0,0 +1,104 @@
/**
* Shared constants for the boot-time reconciliation steps.
*
* Everything here describes the state the CMS must be in after a boot. The
* steps in this folder only ever add what is missing, so a second boot is a
* no-op.
*/
/** Prefix used for every log line written by the bootstrap steps. */
export const LOG_PREFIX = '[sharp-er]';
export type LocaleDefinition = {
code: string;
name: string;
/** The locale content editors get by default, and the one `/` falls back to. */
isDefault?: boolean;
};
/**
* Used only if `config/plugins.ts` does not declare `plugin::i18n.locales`
* (for example when the config file is being edited). The real source of truth
* is that config file.
*/
export const FALLBACK_LOCALES: LocaleDefinition[] = [
{ code: 'en', name: 'English (en)', isDefault: true },
{ code: 'ar', name: 'Arabic (ar)' },
];
/** `type` of the users-permissions role that unauthenticated requests use. */
export const PUBLIC_ROLE_TYPE = 'public';
/**
* Read actions granted to the Public role on every API content type.
*
* Single types only expose `find`, collection types expose both; the
* intersection with the actions a controller actually has is taken at runtime,
* so listing both here is safe.
*/
export const PUBLIC_READ_ACTIONS = ['find', 'findOne'];
/** Write action granted to the Public role on the submission content types. */
export const PUBLIC_SUBMIT_ACTIONS = ['create'];
/**
* Content types the public site is allowed to POST to: event sign-ups, calls
* for applications and the contact form.
*
* These hold personal data, so they get `create` and nothing else - no `find`,
* no `findOne`. Submissions are read in the admin panel only.
*
* Names are the API/controller names of the content types (kebab-case
* singular), owned by the content-type agent under `src/api/**`.
*/
export const PUBLIC_SUBMISSION_CONTENT_TYPES = [
'event-registration',
'application',
'contact-submission',
];
/* ---------------------------------------------------------------------------
PUBLIC AUTH LOCKDOWN
------------------------------------------------------------------------ */
/** Permission namespace of the users-permissions auth controller. */
export const AUTH_PERMISSION_PREFIX = 'plugin::users-permissions.auth';
/**
* Auth actions the Public role must NOT hold.
*
* This site has no end-user accounts: readers read, three forms post, and
* staff sign in to the admin panel, which is a separate permission system. So
* every public-facing account flow is dead weight and pure attack surface -
* `register` most of all, because Strapi grants it by default and it creates
* real API accounts on a site that has none.
*
* `logout`, `refresh`, `getSessions` and `revokeSession` are deliberately NOT
* listed: they act on the caller's own session, they are useless without an
* account, and revoking them would break session handling the moment accounts
* are ever introduced.
*/
export const DISABLED_PUBLIC_AUTH_ACTIONS = [
'register',
'callback',
'connect',
'forgotPassword',
'resetPassword',
'changePassword',
'emailConfirmation',
'sendEmailConfirmation',
];
/**
* Escape hatch for the day the site does grow public accounts:
* `PUBLIC_AUTH_ACTIONS=callback,forgotPassword` leaves those two alone. The
* decision then lives in the environment, where it is reviewable, rather than
* in a checkbox someone once ticked in the admin panel.
*/
export const readDeliberatePublicAuthActions = (): ReadonlySet<string> =>
new Set(
(process.env.PUBLIC_AUTH_ACTIONS ?? '')
.split(',')
.map((action) => action.trim())
.filter(Boolean)
);
+31
View File
@@ -0,0 +1,31 @@
import type { Core } from '@strapi/strapi';
import { revokePublicAuthActions } from './auth-permissions';
import { LOG_PREFIX } from './constants';
import { ensureLocales } from './locales';
import { ensurePublicPermissions } from './public-permissions';
export { revokePublicAuthActions } from './auth-permissions';
export { ensureLocales } from './locales';
export { ensurePublicPermissions } from './public-permissions';
/**
* Boot-time reconciliation, run from `src/index.ts`.
*
* Every step is idempotent: the first two only add what is missing, and the
* third only removes grants this site has decided it never wants. Running the
* lot on each boot - including on every hot reload in development - leaves an
* already-reconciled instance untouched.
*
* Runs after the plugin bootstraps, so the `en` locale and the Public role that
* Strapi creates on a fresh database already exist here - including the auth
* permissions users-permissions grants that role by default, which is exactly
* what the third step takes back.
*/
export const runBootstrap = async ({ strapi }: { strapi: Core.Strapi }): Promise<void> => {
await ensureLocales(strapi);
await ensurePublicPermissions(strapi);
await revokePublicAuthActions(strapi);
strapi.log.debug(`${LOG_PREFIX} bootstrap complete.`);
};
+97
View File
@@ -0,0 +1,97 @@
import type { Core } from '@strapi/strapi';
import { FALLBACK_LOCALES, LOG_PREFIX, type LocaleDefinition } from './constants';
type Locale = { id: number; code: string; name: string };
/** The subset of `plugin::i18n.locales` service methods used here. */
type LocaleService = {
findByCode(code: string): Promise<Locale | null>;
create(locale: { code: string; name: string }): Promise<Locale>;
getDefaultLocale(): Promise<string | null>;
setDefaultLocale(params: { code: string }): Promise<void>;
};
const isLocaleDefinition = (value: unknown): value is LocaleDefinition => {
if (typeof value !== 'object' || value === null) {
return false;
}
const { code, name } = value as Partial<LocaleDefinition>;
return typeof code === 'string' && code.length > 0 && typeof name === 'string' && name.length > 0;
};
/** Locales declared in `config/plugins.ts`, falling back to the built-in pair. */
const readLocaleDefinitions = (strapi: Core.Strapi): LocaleDefinition[] => {
const configured = strapi.plugin('i18n').config<unknown>('locales');
const definitions = Array.isArray(configured) ? configured.filter(isLocaleDefinition) : [];
return definitions.length > 0 ? definitions : FALLBACK_LOCALES;
};
/** The code the `defaultLocale` config key asks for, or the flagged/first one. */
const resolveDefaultLocaleCode = (
strapi: Core.Strapi,
definitions: LocaleDefinition[]
): string | undefined => {
const configured = strapi.plugin('i18n').config<unknown>('defaultLocale');
if (typeof configured === 'string' && definitions.some(({ code }) => code === configured)) {
return configured;
}
return (definitions.find(({ isDefault }) => isDefault) ?? definitions[0])?.code;
};
/**
* Make sure every configured locale (`en`, `ar`) exists and that the default
* one is the configured default.
*
* Idempotent: existing locales are left untouched, and the default is only
* written when it differs from what is stored.
*/
export const ensureLocales = async (strapi: Core.Strapi): Promise<void> => {
if (!strapi.plugins.i18n) {
strapi.log.warn(`${LOG_PREFIX} i18n plugin is disabled - skipping locale bootstrap.`);
return;
}
const localeService = strapi.plugin('i18n').service<LocaleService>('locales');
const definitions = readLocaleDefinitions(strapi);
const created: string[] = [];
for (const definition of definitions) {
if (await localeService.findByCode(definition.code)) {
continue;
}
try {
await localeService.create({ code: definition.code, name: definition.name });
created.push(definition.code);
} catch (error) {
// Another instance booting at the same time may have inserted it between
// the lookup and the insert; only a still-missing locale is a real error.
if (!(await localeService.findByCode(definition.code))) {
throw error;
}
}
}
if (created.length > 0) {
strapi.log.info(`${LOG_PREFIX} created locale(s): ${created.join(', ')}.`);
}
const defaultLocaleCode = resolveDefaultLocaleCode(strapi, definitions);
if (!defaultLocaleCode) {
return;
}
const currentDefault = await localeService.getDefaultLocale();
if (currentDefault !== defaultLocaleCode) {
await localeService.setDefaultLocale({ code: defaultLocaleCode });
strapi.log.info(`${LOG_PREFIX} default locale set to "${defaultLocaleCode}".`);
}
};
@@ -0,0 +1,123 @@
import type { Core } from '@strapi/strapi';
import {
LOG_PREFIX,
PUBLIC_READ_ACTIONS,
PUBLIC_ROLE_TYPE,
PUBLIC_SUBMISSION_CONTENT_TYPES,
PUBLIC_SUBMIT_ACTIONS,
} from './constants';
/**
* Shape returned by the users-permissions `getActions()` service:
* `{ 'api::event': { controllers: { event: { find: {...}, findOne: {...} } } } }`
*/
type ActionMap = Record<string, { controllers?: Record<string, Record<string, unknown>> }>;
type UsersPermissionsService = {
getActions(options?: { defaultEnable?: boolean }): ActionMap;
};
const API_NAMESPACE_PREFIX = 'api::';
/**
* Actions the Public role should hold on a given content type.
*
* Submission types are write-only for the public: granting `find`/`findOne`
* there would expose the personal data people send through the site.
*/
const desiredActionsFor = (controllerName: string): string[] =>
PUBLIC_SUBMISSION_CONTENT_TYPES.includes(controllerName)
? [...PUBLIC_SUBMIT_ACTIONS]
: [...PUBLIC_READ_ACTIONS];
/**
* Grant the Public role read access to the public content (collection types and
* single types) and create access to the three submission content types.
*
* Only missing permissions are inserted, and permissions granted by hand in the
* admin panel are never removed - so this is safe to run on every boot. A read
* grant on a submission type is reported as a warning rather than deleted, so
* that an unexpected grant is visible without the boot silently undoing an
* administrator's decision.
*/
export const ensurePublicPermissions = async (strapi: Core.Strapi): Promise<void> => {
if (!strapi.plugins['users-permissions']) {
strapi.log.warn(`${LOG_PREFIX} users-permissions plugin is disabled - skipping permissions.`);
return;
}
const publicRole = await strapi.db.query('plugin::users-permissions.role').findOne({
where: { type: PUBLIC_ROLE_TYPE },
select: ['id'],
});
if (!publicRole) {
strapi.log.warn(`${LOG_PREFIX} no "${PUBLIC_ROLE_TYPE}" role found - skipping permissions.`);
return;
}
const actionMap = strapi
.plugin('users-permissions')
.service<UsersPermissionsService>('users-permissions')
.getActions();
const desiredActions = new Set<string>();
const forbiddenReadActions = new Set<string>();
for (const [namespace, { controllers }] of Object.entries(actionMap)) {
// Only the project's own APIs; plugin permissions are managed by Strapi.
if (!namespace.startsWith(API_NAMESPACE_PREFIX)) {
continue;
}
for (const [controllerName, actions] of Object.entries(controllers ?? {})) {
const availableActions = Object.keys(actions ?? {});
const wantedActions = desiredActionsFor(controllerName);
for (const action of availableActions) {
const permission = `${namespace}.${controllerName}.${action}`;
if (wantedActions.includes(action)) {
desiredActions.add(permission);
} else if (
PUBLIC_SUBMISSION_CONTENT_TYPES.includes(controllerName) &&
PUBLIC_READ_ACTIONS.includes(action)
) {
forbiddenReadActions.add(permission);
}
}
}
}
const existingPermissions = await strapi.db
.query('plugin::users-permissions.permission')
.findMany({
where: { role: { id: publicRole.id } },
select: ['action'],
});
const existingActions = new Set(existingPermissions.map(({ action }) => action));
const missingActions = [...desiredActions].filter((action) => !existingActions.has(action)).sort();
for (const action of missingActions) {
await strapi.db.query('plugin::users-permissions.permission').create({
data: { action, role: publicRole.id },
});
}
if (missingActions.length > 0) {
strapi.log.info(
`${LOG_PREFIX} granted ${missingActions.length} public permission(s): ${missingActions.join(', ')}.`
);
}
const leakedActions = [...forbiddenReadActions].filter((action) => existingActions.has(action));
if (leakedActions.length > 0) {
strapi.log.warn(
`${LOG_PREFIX} the Public role can read submission data: ${leakedActions.join(', ')}. ` +
'Revoke these in Settings > Roles > Public unless this is intentional.'
);
}
};
@@ -0,0 +1,19 @@
{
"collectionName": "components_blocks_accordions",
"info": {
"displayName": "Accordion",
"icon": "bulletList",
"description": "A collapsible list of questions and answers (FAQs)."
},
"options": {},
"attributes": {
"headline": {
"type": "string"
},
"items": {
"type": "component",
"repeatable": true,
"component": "shared.accordion-item"
}
}
}
@@ -0,0 +1,22 @@
{
"collectionName": "components_blocks_card_grids",
"info": {
"displayName": "Card Grid",
"icon": "grid",
"description": "A headed grid of cards."
},
"options": {},
"attributes": {
"headline": {
"type": "string"
},
"intro": {
"type": "text"
},
"cards": {
"type": "component",
"repeatable": true,
"component": "shared.card"
}
}
}
@@ -0,0 +1,33 @@
{
"collectionName": "components_blocks_cta_banners",
"info": {
"displayName": "CTA Banner",
"icon": "cursor",
"description": "A full-width banner with a single call to action."
},
"options": {},
"attributes": {
"headline": {
"type": "string",
"required": true
},
"body": {
"type": "text"
},
"cta": {
"type": "component",
"repeatable": false,
"component": "shared.cta"
},
"surface": {
"type": "enumeration",
"enum": [
"dark",
"light",
"accent"
],
"default": "dark",
"required": true
}
}
}
@@ -0,0 +1,22 @@
{
"collectionName": "components_blocks_image_galleries",
"info": {
"displayName": "Image Gallery",
"icon": "picture",
"description": "A gallery of images with an optional caption."
},
"options": {},
"attributes": {
"images": {
"type": "media",
"multiple": true,
"required": true,
"allowedTypes": [
"images"
]
},
"caption": {
"type": "string"
}
}
}
@@ -0,0 +1,19 @@
{
"collectionName": "components_blocks_partner_logos",
"info": {
"displayName": "Partner Logos",
"icon": "apps",
"description": "A row of partner logos pulled from the Partner collection."
},
"options": {},
"attributes": {
"headline": {
"type": "string"
},
"partners": {
"type": "relation",
"relation": "oneToMany",
"target": "api::partner.partner"
}
}
}
+21
View File
@@ -0,0 +1,21 @@
{
"collectionName": "components_blocks_quotes",
"info": {
"displayName": "Quote",
"icon": "quote",
"description": "A pull quote with attribution."
},
"options": {},
"attributes": {
"quote": {
"type": "text",
"required": true
},
"attribution": {
"type": "string"
},
"role": {
"type": "string"
}
}
}
@@ -0,0 +1,15 @@
{
"collectionName": "components_blocks_rich_texts",
"info": {
"displayName": "Rich Text",
"icon": "feather",
"description": "A block of formatted Markdown text."
},
"options": {},
"attributes": {
"body": {
"type": "richtext",
"required": true
}
}
}
@@ -0,0 +1,19 @@
{
"collectionName": "components_blocks_stats_grids",
"info": {
"displayName": "Stats Grid",
"icon": "chartBubble",
"description": "A grid of headline figures."
},
"options": {},
"attributes": {
"stats": {
"type": "component",
"repeatable": true,
"component": "shared.stat"
},
"note": {
"type": "text"
}
}
}
@@ -0,0 +1,26 @@
{
"collectionName": "components_blocks_video_embeds",
"info": {
"displayName": "Video Embed",
"icon": "play",
"description": "An embedded video with an optional poster image."
},
"options": {},
"attributes": {
"url": {
"type": "string",
"required": true
},
"caption": {
"type": "string"
},
"poster": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
]
}
}
}
@@ -0,0 +1,20 @@
{
"collectionName": "components_navigation_footer_columns",
"info": {
"displayName": "Footer Column",
"icon": "layout",
"description": "A heading plus a list of links in the footer."
},
"options": {},
"attributes": {
"heading": {
"type": "string",
"required": true
},
"links": {
"type": "component",
"repeatable": true,
"component": "shared.link"
}
}
}
@@ -0,0 +1,23 @@
{
"collectionName": "components_navigation_nav_groups",
"info": {
"displayName": "Nav Group",
"icon": "bulletList",
"description": "A top-level navigation entry with optional child links."
},
"options": {},
"attributes": {
"label": {
"type": "string",
"required": true
},
"href": {
"type": "string"
},
"children": {
"type": "component",
"repeatable": true,
"component": "shared.link"
}
}
}
@@ -0,0 +1,19 @@
{
"collectionName": "components_page_consortium_members",
"info": {
"displayName": "Consortium Member",
"icon": "user",
"description": "A consortium partner plus its role in SHARP-ER."
},
"options": {},
"attributes": {
"partner": {
"type": "relation",
"relation": "oneToOne",
"target": "api::partner.partner"
},
"roleDescription": {
"type": "text"
}
}
}
@@ -0,0 +1,26 @@
{
"collectionName": "components_page_workstreams",
"info": {
"displayName": "Workstream",
"icon": "puzzle",
"description": "A strand of work with a title, description and icon."
},
"options": {},
"attributes": {
"title": {
"type": "string",
"required": true
},
"description": {
"type": "text"
},
"icon": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
]
}
}
}
@@ -0,0 +1,19 @@
{
"collectionName": "components_shared_accordion_items",
"info": {
"displayName": "Accordion Item",
"icon": "bulletList",
"description": "A question / answer pair."
},
"options": {},
"attributes": {
"question": {
"type": "string",
"required": true
},
"answer": {
"type": "richtext",
"required": true
}
}
}
@@ -0,0 +1,30 @@
{
"collectionName": "components_shared_addresses",
"info": {
"displayName": "Address",
"icon": "pinMap",
"description": "A consortium office / contact address block."
},
"options": {},
"attributes": {
"organisation": {
"type": "string",
"required": true
},
"addressLine": {
"type": "text"
},
"city": {
"type": "string"
},
"country": {
"type": "string"
},
"email": {
"type": "email"
},
"phone": {
"type": "string"
}
}
}
+31
View File
@@ -0,0 +1,31 @@
{
"collectionName": "components_shared_cards",
"info": {
"displayName": "Card",
"icon": "layer",
"description": "A card inside a card grid."
},
"options": {},
"attributes": {
"title": {
"type": "string",
"required": true
},
"body": {
"type": "text"
},
"media": {
"type": "media",
"multiple": false,
"required": false,
"allowedTypes": [
"images"
]
},
"link": {
"type": "component",
"repeatable": false,
"component": "shared.link"
}
}
}
+31
View File
@@ -0,0 +1,31 @@
{
"collectionName": "components_shared_ctas",
"info": {
"displayName": "CTA",
"icon": "cursor",
"description": "A call-to-action button."
},
"options": {},
"attributes": {
"label": {
"type": "string",
"required": true
},
"href": {
"type": "string",
"required": true
},
"variant": {
"type": "enumeration",
"enum": [
"primary",
"secondary",
"dark",
"danger",
"ghost"
],
"default": "primary",
"required": true
}
}
}

Some files were not shown because too many files have changed in this diff Show More