added seeder for legacy apge and added the legacy page
This commit is contained in:
@@ -62,7 +62,7 @@ sharp-er/
|
|||||||
│ ├── web/ React Router 8 front end (SSR)
|
│ ├── web/ React Router 8 front end (SSR)
|
||||||
│ │ ├── app/
|
│ │ ├── app/
|
||||||
│ │ │ ├── root.tsx HTML shell: <html lang/dir>, canonical + hreflang, nav, footer
|
│ │ │ ├── root.tsx HTML shell: <html lang/dir>, canonical + hreflang, nav, footer
|
||||||
│ │ │ ├── routes.ts Route config: "/" redirect, ":lang" layout, the ten sections
|
│ │ │ ├── routes.ts Route config: "/" redirect, ":lang" layout, the eleven sections
|
||||||
│ │ │ ├── routes/ One module per page (+ shared _page-kit / _shared helpers)
|
│ │ │ ├── routes/ One module per page (+ shared _page-kit / _shared helpers)
|
||||||
│ │ │ ├── components/ui/ Button, Input, Card, Navigation, Footer, LanguageSwitcher
|
│ │ │ ├── components/ui/ Button, Input, Card, Navigation, Footer, LanguageSwitcher
|
||||||
│ │ │ ├── styles/ Design tokens — primitives.css, semantic.css, typography.css
|
│ │ │ ├── styles/ Design tokens — primitives.css, semantic.css, typography.css
|
||||||
@@ -470,6 +470,7 @@ docker run --rm -v sharper_cms-uploads:/data -v "$PWD":/backup alpine \
|
|||||||
| `pnpm build` | builds the CMS, then the web app (that order: the web build has no CMS dependency, but a broken CMS build should fail first) |
|
| `pnpm build` | builds the CMS, then the web app (that order: the web build has no CMS dependency, but a broken CMS build should fail first) |
|
||||||
| `pnpm start` | runs both production builds |
|
| `pnpm start` | runs both production builds |
|
||||||
| `pnpm seed` | writes the approved content into the CMS — see [Seeding content](#seeding-content) |
|
| `pnpm seed` | writes the approved content into the CMS — see [Seeding content](#seeding-content) |
|
||||||
|
| `pnpm seed -- --legacy-only` | writes SHARP I, SHARP II, and the home section below the hero through the bilingual Legacy page; preserves its uploaded media |
|
||||||
| `pnpm typecheck` | `tsc` in both applications, preceded in the web app by `react-router typegen` |
|
| `pnpm typecheck` | `tsc` in both applications, preceded in the web app by `react-router typegen` |
|
||||||
|
|
||||||
**`apps/web`** — run with `pnpm --filter web <script>`:
|
**`apps/web`** — run with `pnpm --filter web <script>`:
|
||||||
@@ -512,6 +513,7 @@ and never opens a port of its own.
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm seed # the approved content
|
pnpm seed # the approved content
|
||||||
|
pnpm seed -- --legacy-only # SHARP I/II and home Legacy section only
|
||||||
pnpm seed -- --demo # the same, plus fixtures to look at
|
pnpm seed -- --demo # the same, plus fixtures to look at
|
||||||
pnpm seed -- --demo --clean # remove those fixtures again
|
pnpm seed -- --demo --clean # remove those fixtures again
|
||||||
```
|
```
|
||||||
@@ -532,8 +534,16 @@ settings, the three consortium members, the topic taxonomy, the five activity
|
|||||||
streams, the launch article, and the About, What We Do, Contact and Home
|
streams, the launch article, and the About, What We Do, Contact and Home
|
||||||
pages.
|
pages.
|
||||||
|
|
||||||
|
The Legacy single type also holds the SHARP I and SHARP II records and the
|
||||||
|
Legacy section shown below the home hero. Its programme facts and figures come
|
||||||
|
from `SHARP I Programme SUMMARY.docx` and `SHARP II Programme SUMMARY.docx`;
|
||||||
|
the English text is a concise paraphrase and the Arabic text is a translation.
|
||||||
|
Run `pnpm seed -- --legacy-only` to update only that single type. The seed
|
||||||
|
preserves the hero and up to five gallery images selected in each locale.
|
||||||
|
|
||||||
What `docs/` does not supply is not written. There are no statistics anywhere
|
What `docs/` does not supply is not written. There are no statistics anywhere
|
||||||
(the source marks every number "TBD — must not be published until confirmed"),
|
(outside the sourced Legacy page; the `docs/` source marks other numbers
|
||||||
|
"TBD — must not be published until confirmed"),
|
||||||
no project email, no Facebook URL and no consortium addresses, because the
|
no project email, no Facebook URL and no consortium addresses, because the
|
||||||
source writes all of those as bracketed placeholders. Two fields are judgement
|
source writes all of those as bracketed placeholders. Two fields are judgement
|
||||||
calls and say so at the line: every activity's `activityStatus`, which the
|
calls and say so at the line: every activity's `activityStatus`, which the
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
{
|
||||||
|
"kind": "singleType",
|
||||||
|
"collectionName": "legacy_pages",
|
||||||
|
"info": {
|
||||||
|
"singularName": "legacy-page",
|
||||||
|
"pluralName": "legacy-pages",
|
||||||
|
"displayName": "Legacy Page",
|
||||||
|
"description": "SHARP I and SHARP II programme history, achievements, metrics, and featured images."
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"draftAndPublish": true
|
||||||
|
},
|
||||||
|
"pluginOptions": {
|
||||||
|
"i18n": {
|
||||||
|
"localized": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"attributes": {
|
||||||
|
"homeEyebrow": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } },
|
||||||
|
"homeHeading": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } },
|
||||||
|
"homeLead": { "type": "text", "pluginOptions": { "i18n": { "localized": true } } },
|
||||||
|
"hero": {
|
||||||
|
"type": "component",
|
||||||
|
"repeatable": false,
|
||||||
|
"component": "shared.hero",
|
||||||
|
"pluginOptions": { "i18n": { "localized": true } }
|
||||||
|
},
|
||||||
|
"stageOne": {
|
||||||
|
"type": "component",
|
||||||
|
"repeatable": false,
|
||||||
|
"component": "page.legacy-stage",
|
||||||
|
"pluginOptions": { "i18n": { "localized": true } }
|
||||||
|
},
|
||||||
|
"stageTwo": {
|
||||||
|
"type": "component",
|
||||||
|
"repeatable": false,
|
||||||
|
"component": "page.legacy-stage",
|
||||||
|
"pluginOptions": { "i18n": { "localized": true } }
|
||||||
|
},
|
||||||
|
"closingHeading": { "type": "string", "pluginOptions": { "i18n": { "localized": true } } },
|
||||||
|
"closingBody": { "type": "richtext", "pluginOptions": { "i18n": { "localized": true } } },
|
||||||
|
"seo": {
|
||||||
|
"type": "component",
|
||||||
|
"repeatable": false,
|
||||||
|
"component": "shared.seo",
|
||||||
|
"pluginOptions": { "i18n": { "localized": true } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* legacy-page controller
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreController('api::legacy-page.legacy-page');
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* legacy-page router
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreRouter('api::legacy-page.legacy-page');
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
/**
|
||||||
|
* legacy-page service
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { factories } from '@strapi/strapi';
|
||||||
|
|
||||||
|
export default factories.createCoreService('api::legacy-page.legacy-page');
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"collectionName": "components_page_legacy_focus_areas",
|
||||||
|
"info": {
|
||||||
|
"displayName": "Legacy Focus Area",
|
||||||
|
"icon": "bulletList"
|
||||||
|
},
|
||||||
|
"options": {},
|
||||||
|
"attributes": {
|
||||||
|
"name": { "type": "string", "required": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"collectionName": "components_page_legacy_metrics",
|
||||||
|
"info": {
|
||||||
|
"displayName": "Legacy Metric",
|
||||||
|
"icon": "chartBubble"
|
||||||
|
},
|
||||||
|
"options": {},
|
||||||
|
"attributes": {
|
||||||
|
"value": { "type": "string", "required": true },
|
||||||
|
"label": { "type": "string", "required": true }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
{
|
||||||
|
"collectionName": "components_page_legacy_stages",
|
||||||
|
"info": {
|
||||||
|
"displayName": "Legacy Phase",
|
||||||
|
"icon": "landmark",
|
||||||
|
"description": "One SHARP programme phase with its copy, figures, areas of work, and image gallery. The website displays up to five selected images."
|
||||||
|
},
|
||||||
|
"options": {},
|
||||||
|
"attributes": {
|
||||||
|
"name": { "type": "string", "required": true },
|
||||||
|
"years": { "type": "string" },
|
||||||
|
"summary": { "type": "text" },
|
||||||
|
"description": { "type": "richtext" },
|
||||||
|
"metrics": { "type": "component", "repeatable": true, "component": "page.legacy-metric" },
|
||||||
|
"focusHeading": { "type": "string" },
|
||||||
|
"focusAreas": { "type": "component", "repeatable": true, "component": "page.legacy-focus-area" },
|
||||||
|
"galleryHeading": { "type": "string" },
|
||||||
|
"images": {
|
||||||
|
"type": "media",
|
||||||
|
"multiple": true,
|
||||||
|
"required": false,
|
||||||
|
"description": "Select up to five images for this programme. The gallery shows the first five in this order.",
|
||||||
|
"allowedTypes": ["images"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
SEED - APPROVED CONTENT
|
SEED - APPROVED CONTENT
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
Everything in this file is transcribed from docs/, in both languages, and
|
Everything in this file is transcribed from docs/ or the supplied project
|
||||||
nothing in it is invented. That is the whole point of keeping it apart from
|
content suggestions, in both languages, and nothing in it is invented. That
|
||||||
demo.ts: this content is safe to publish.
|
is the whole point of keeping it apart from demo.ts: this content is safe to
|
||||||
|
publish.
|
||||||
|
|
||||||
SOURCES
|
SOURCES
|
||||||
docs/SHARPER_Website_Draft_Section_EN_AR.docx sections 1-9, EN and AR
|
docs/SHARPER_Website_Draft_Section_EN_AR.docx sections 1-9, EN and AR
|
||||||
@@ -13,6 +14,9 @@
|
|||||||
docs/SHARPER Home Page .docx the launch article, EN/AR
|
docs/SHARPER Home Page .docx the launch article, EN/AR
|
||||||
docs/UEC_SHARPER_Website_TOR V3.docx the consortium and the
|
docs/UEC_SHARPER_Website_TOR V3.docx the consortium and the
|
||||||
project's purpose
|
project's purpose
|
||||||
|
SHARP-ER Website Content suggestions submitted by Isam Saidi, supplied by
|
||||||
|
the project team: SHARP I/II
|
||||||
|
legacy text and historical figures
|
||||||
|
|
||||||
WHAT docs/ DOES NOT SUPPLY, AND IS THEREFORE NOT HERE
|
WHAT docs/ DOES NOT SUPPLY, AND IS THEREFORE NOT HERE
|
||||||
* Figures. Every number in the draft is bracketed and marked "TBD ... must
|
* Figures. Every number in the draft is bracketed and marked "TBD ... must
|
||||||
@@ -424,10 +428,6 @@ export const LAUNCH_ARTICLE = {
|
|||||||
export const HOME_PAGE: Localised<SeedData> = {
|
export const HOME_PAGE: Localised<SeedData> = {
|
||||||
en: {
|
en: {
|
||||||
hero: {
|
hero: {
|
||||||
/* No `eyebrow`. The design draws a status pill reading "Now open -
|
|
||||||
Funding calls for Libyan organisations"; whether a call is open on any
|
|
||||||
given day is a fact about the project, not about the design, so the
|
|
||||||
pill stays unwritten until an editor writes it. */
|
|
||||||
headline: 'Advancing Justice and Accountability, Led by Libyan Communities',
|
headline: 'Advancing Justice and Accountability, Led by Libyan Communities',
|
||||||
subheading:
|
subheading:
|
||||||
'SHARP-ER — Shared Action on Rule of Law Progress, Exit Route — works with civil society organisations, municipalities, and community leaders across Libya to strengthen access to justice, accountability, and local governance. Funded by the European Union, running to December 2027.',
|
'SHARP-ER — Shared Action on Rule of Law Progress, Exit Route — works with civil society organisations, municipalities, and community leaders across Libya to strengthen access to justice, accountability, and local governance. Funded by the European Union, running to December 2027.',
|
||||||
@@ -547,6 +547,188 @@ export const ABOUT_PAGE: Localised<SeedData> = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Facts and figures below come from "SHARP I Programme SUMMARY.docx" and
|
||||||
|
* "SHARP II Programme SUMMARY.docx". English page copy paraphrases the source;
|
||||||
|
* Arabic page copy translates it. UI headings are editorial labels, not quotes.
|
||||||
|
* The home Legacy section is stored here with the two programme records.
|
||||||
|
*/
|
||||||
|
export const LEGACY_PAGE: Localised<SeedData> = {
|
||||||
|
en: {
|
||||||
|
homeEyebrow: 'SHARP I and SHARP II',
|
||||||
|
homeHeading: 'Dialogue and local action in Libya, 2019–2024',
|
||||||
|
homeLead:
|
||||||
|
'Two EU-funded programmes supported rule of law dialogue, civil society capacity, and local action across Libya.',
|
||||||
|
hero: {
|
||||||
|
eyebrow: 'SHARP I and SHARP II',
|
||||||
|
headline: 'Dialogue and local action across Libya',
|
||||||
|
subheading:
|
||||||
|
'From November 2019 to August 2024, two EU-funded programmes supported rule of law dialogue, civil society capacity, and locally led action.',
|
||||||
|
},
|
||||||
|
stageOne: {
|
||||||
|
name: 'SHARP I',
|
||||||
|
years: 'November 2019 – August 2022',
|
||||||
|
summary:
|
||||||
|
'Built dialogue and local capacity through research, learning, and community-led action.',
|
||||||
|
description:
|
||||||
|
`The EU-funded SHARP I programme was implemented by Institute Clingendael and Bureau Ensemble with Libyan civil society organisations, local authorities, and national institutions. From November 2019 to August 2022, it worked to strengthen Libya’s rule of law through dialogue, research, and local action. Budget: €1.996 million.\n\n### Objectives\n- Strengthen Libyan institutions’ ability to respond to rule of law challenges.\n- Build the organisational capabilities of Libyan change agents.\n- Foster dialogue and information-sharing between stakeholders.\n\n### Dialogue and learning\nThe programme held 102 Search, Understand, and Question (SUQ) dialogues with 1,583 participants; 48% of participants were women. Topics included human rights, transitional justice, and local governance. A further 46 Training & Learning sessions covered facilitation, project management, and monitoring.\n\n### Community action\nTwenty-one sub-contract actions addressed conflict mediation, access to justice, women’s rights, labour rights, and civil-society and media engagement. They produced 62 workshops, 6 research papers, 24 training sessions, and 1,555 media outreach activities.\n\n### Results and lasting value\n87% of SUQ participants considered the discussions relevant, and 75% believed SHARP could contribute to rule of law change. Projects strengthened CSO reporting skills and partnerships with local authorities. Libyan-led initiatives built community trust; the programme adapted to pandemic and security constraints with virtual activities and local dialogue, helping CSOs develop lasting networks.\n\n### What followed\nThe EU approved SHARP II to build on these lessons: deepen dialogue, focus on four to six rule of law themes while retaining flexibility, strengthen CSO project design and evaluation, and support durable government–CSO networks.`,
|
||||||
|
metrics: [
|
||||||
|
{ value: '102', label: 'SUQ dialogues' },
|
||||||
|
{ value: '1,583', label: 'Dialogue participants' },
|
||||||
|
{ value: '48%', label: 'Women participants' },
|
||||||
|
{ value: '46', label: 'Learning sessions' },
|
||||||
|
{ value: '21', label: 'Sub-contract actions' },
|
||||||
|
{ value: '62', label: 'Workshops delivered' },
|
||||||
|
{ value: '6', label: 'Research papers' },
|
||||||
|
{ value: '24', label: 'Training sessions' },
|
||||||
|
{ value: '1,555', label: 'Media outreach activities' },
|
||||||
|
{ value: '87%', label: 'Found dialogues relevant' },
|
||||||
|
{ value: '75%', label: 'Saw potential for change' },
|
||||||
|
],
|
||||||
|
focusHeading: 'Areas of work',
|
||||||
|
focusAreas: [
|
||||||
|
{ name: 'Human rights' },
|
||||||
|
{ name: 'Transitional justice' },
|
||||||
|
{ name: 'Local governance' },
|
||||||
|
{ name: 'Conflict mediation' },
|
||||||
|
{ name: 'Access to justice' },
|
||||||
|
{ name: 'Women’s rights' },
|
||||||
|
{ name: 'Labour rights' },
|
||||||
|
{ name: 'Civil society and media' },
|
||||||
|
],
|
||||||
|
galleryHeading: 'SHARP I in the field',
|
||||||
|
},
|
||||||
|
stageTwo: {
|
||||||
|
name: 'SHARP II',
|
||||||
|
years: 'September 2022 – August 2024',
|
||||||
|
summary:
|
||||||
|
'Expanded a nationwide civil-society network and turned inclusive dialogue into community action.',
|
||||||
|
description:
|
||||||
|
`Funded by the European Union and implemented by Bureau Ensemble with Libyan partners, SHARP II built on SHARP I to strengthen the rule of law, stability, and participatory governance. From September 2022 to August 2024, the programme had a budget of €1.426 million.\n\n### Priorities\nSHARP II aimed to grow a national network of rule of law dialogue groups; build social cohesion and consensus locally, regionally, and nationally; organise inclusive dialogues on rule of law and positive citizenship; and support small community actions.\n\n### Dialogue and network\n157 SUQ dialogues engaged 2,554 participants, 44% of them women. Discussions covered access to justice, women’s empowerment, local governance, and the media’s role in the rule of law. The network grew to 64 active CSOs from 27, reaching every region, including marginalised areas in the south.\n\n### Skills and community action\nSix Training & Learning sessions covered facilitation, project management, and monitoring. Advanced training reached 100 participants, with 24 certified as trainers. All 34 approved sub-contracts were implemented, addressing legal awareness, reconciliation, women’s rights, gender-based violence, municipal capacity, ethical journalism, and misinformation. Activities reached 1,487 trainees and 1,697 workshop participants; awareness campaigns benefited 2,896 people.\n\n### Policy, communication, and impact\nRegional and national dialogues informed recommendations on faster court processes and legal aid, municipal empowerment under Local Governance Law 59, and media transparency and hate speech. Libyan-led monitoring supported adaptive delivery, while 1,555 media outreach activities amplified the work. CSOs led 100% of activities; ministries and municipalities partnered with them, and young people, displaced people, and persons with disabilities took part.\n\n### A sustainable exit\nThe March–August 2024 exit phase shaped three pillars for continued impact: Access to Justice (legal aid, mediation, and policy reform), TAZIZ (municipal capacity and citizen engagement), and ADWA (ethical journalism and countering misinformation). United Efforts was established and registered as a Libyan legal entity to help sustain the work.`,
|
||||||
|
metrics: [
|
||||||
|
{ value: '157', label: 'SUQ dialogues' },
|
||||||
|
{ value: '2,554', label: 'Dialogue participants' },
|
||||||
|
{ value: '64', label: 'Active CSOs' },
|
||||||
|
{ value: '44%', label: 'Women participants' },
|
||||||
|
{ value: '34', label: 'Sub-contracts' },
|
||||||
|
{ value: '100', label: 'Advanced training participants' },
|
||||||
|
{ value: '24', label: 'Certified trainers' },
|
||||||
|
{ value: '1,487', label: 'Trainees' },
|
||||||
|
{ value: '1,697', label: 'Workshop participants' },
|
||||||
|
{ value: '2,896', label: 'Awareness campaign beneficiaries' },
|
||||||
|
{ value: '1,555', label: 'Media outreach activities' },
|
||||||
|
],
|
||||||
|
focusHeading: 'Areas of work',
|
||||||
|
focusAreas: [
|
||||||
|
{ name: 'Access to justice' },
|
||||||
|
{ name: 'Women’s empowerment' },
|
||||||
|
{ name: 'Local governance' },
|
||||||
|
{ name: 'Media and rule of law' },
|
||||||
|
{ name: 'Social cohesion' },
|
||||||
|
{ name: 'Rule of law' },
|
||||||
|
{ name: 'Positive citizenship' },
|
||||||
|
],
|
||||||
|
galleryHeading: 'SHARP II in the field',
|
||||||
|
},
|
||||||
|
closingHeading: 'SHARP II’s exit phase',
|
||||||
|
closingBody: [
|
||||||
|
'From March to August 2024, the SHARP II exit phase focused on access to justice, local governance through TAZIZ, and media through ADWA.',
|
||||||
|
'United Efforts was established and registered as a Libyan legal entity. The SHARP II summary identifies a stable nationwide civil society network and a roadmap for future EU–Libya collaboration through SHARP-ER.',
|
||||||
|
].join('\n\n'),
|
||||||
|
seo: {
|
||||||
|
metaTitle: 'SHARP Legacy',
|
||||||
|
metaDescription:
|
||||||
|
'Discover the experience, achievements, and networks built through SHARP I and SHARP II in Libya.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ar: {
|
||||||
|
homeEyebrow: 'شارب الأول وشارب الثاني',
|
||||||
|
homeHeading: 'الحوار والعمل المحلي في ليبيا، 2019–2024',
|
||||||
|
homeLead:
|
||||||
|
'دعم برنامجان ممولان من الاتحاد الأوروبي الحوار حول سيادة القانون، وقدرات المجتمع المدني، والعمل المحلي في مختلف أنحاء ليبيا.',
|
||||||
|
hero: {
|
||||||
|
eyebrow: 'شارب الأول وشارب الثاني',
|
||||||
|
headline: 'الحوار والعمل المحلي في مختلف أنحاء ليبيا',
|
||||||
|
subheading:
|
||||||
|
'من نوفمبر 2019 إلى أغسطس 2024، دعم برنامجان ممولان من الاتحاد الأوروبي الحوار حول سيادة القانون، وقدرات المجتمع المدني، والعمل بقيادة محلية.',
|
||||||
|
},
|
||||||
|
stageOne: {
|
||||||
|
name: 'شارب الأول',
|
||||||
|
years: 'نوفمبر 2019 – أغسطس 2022',
|
||||||
|
summary: 'عزّز الحوار والقدرات المحلية من خلال البحث والتعلّم والعمل المجتمعي.',
|
||||||
|
description:
|
||||||
|
`نفّذ معهد كلينغندايل ومؤسسة Bureau Ensemble برنامج شارب الأول بتمويل من الاتحاد الأوروبي، وبالتعاون مع منظمات المجتمع المدني والسلطات المحلية والمؤسسات الوطنية الليبية. عمل البرنامج من نوفمبر 2019 إلى أغسطس 2022 على دعم سيادة القانون في ليبيا عبر الحوار والبحث والعمل المحلي، بميزانية بلغت 1.996 مليون يورو.\n\n### الأهداف\n- تعزيز قدرة المؤسسات الليبية على الاستجابة لتحديات سيادة القانون.\n- بناء القدرات التنظيمية للفاعلين الليبيين الساعين إلى التغيير.\n- تشجيع الحوار وتبادل المعلومات بين أصحاب المصلحة.\n\n### الحوار والتعلّم\nعُقدت 102 جلسة «ابحث، افهم، واسأل» (SUQ)، بمشاركة 1,583 شخصاً، وكانت نسبة النساء 48%. وتناولت الجلسات حقوق الإنسان والعدالة الانتقالية والحوكمة المحلية. كما عُقدت 46 جلسة للتدريب والتعلّم حول التيسير وإدارة المشاريع والرصد.\n\n### العمل المجتمعي\nنُفّذ 21 مشروعاً فرعياً تناولت الوساطة في النزاعات والوصول إلى العدالة وحقوق النساء وحقوق العمال ومشاركة المجتمع المدني والإعلام. وأسفرت عن 62 ورشة عمل و6 أوراق بحثية و24 جلسة تدريبية و1,555 نشاطاً للتواصل الإعلامي.\n\n### النتائج والأثر المستدام\nرأى 87% من المشاركين أن جلسات الحوار ذات صلة، واعتقد 75% أن شارب قادر على الإسهام في تغيير واقع سيادة القانون. وعززت المشاريع مهارات إعداد التقارير لدى منظمات المجتمع المدني وشراكاتها مع السلطات المحلية. كما دعمت المبادرات بقيادة ليبية الثقة المجتمعية، وتكيّف البرنامج مع قيود الجائحة والأوضاع الأمنية عبر الأنشطة الافتراضية والحوار المحلي، بما ساعد المنظمات على بناء شبكات طويلة الأمد.\n\n### ما تلا ذلك\nوافق الاتحاد الأوروبي على شارب الثاني للاستفادة من هذه الخبرات: تعميق الحوار، والتركيز على أربعة إلى ستة موضوعات مع الحفاظ على المرونة، وتعزيز مهارات تصميم المشاريع وتقييمها لدى منظمات المجتمع المدني، ودعم شبكات مستدامة بين الحكومة والمجتمع المدني.`,
|
||||||
|
metrics: [
|
||||||
|
{ value: '102', label: 'جلسة حوار SUQ' },
|
||||||
|
{ value: '1,583', label: 'مشاركاً في جلسات الحوار' },
|
||||||
|
{ value: '48%', label: 'مشاركة النساء' },
|
||||||
|
{ value: '46', label: 'جلسة للتعلّم' },
|
||||||
|
{ value: '21', label: 'عملاً بعقد فرعي' },
|
||||||
|
{ value: '62', label: 'ورشة عمل' },
|
||||||
|
{ value: '6', label: 'أوراق بحثية' },
|
||||||
|
{ value: '24', label: 'جلسة تدريبية' },
|
||||||
|
{ value: '1,555', label: 'نشاطاً للتواصل الإعلامي' },
|
||||||
|
{ value: '87%', label: 'وجدوا الحوارات ذات صلة' },
|
||||||
|
{ value: '75%', label: 'رأوا إمكانية إحداث تغيير' },
|
||||||
|
],
|
||||||
|
focusHeading: 'مجالات العمل',
|
||||||
|
focusAreas: [
|
||||||
|
{ name: 'حقوق الإنسان' },
|
||||||
|
{ name: 'العدالة الانتقالية' },
|
||||||
|
{ name: 'الحوكمة المحلية' },
|
||||||
|
{ name: 'الوساطة في النزاعات' },
|
||||||
|
{ name: 'الوصول إلى العدالة' },
|
||||||
|
{ name: 'حقوق النساء' },
|
||||||
|
{ name: 'حقوق العمال' },
|
||||||
|
{ name: 'المجتمع المدني والإعلام' },
|
||||||
|
],
|
||||||
|
galleryHeading: 'شارب الأول ميدانياً',
|
||||||
|
},
|
||||||
|
stageTwo: {
|
||||||
|
name: 'شارب الثاني',
|
||||||
|
years: 'سبتمبر 2022 – أغسطس 2024',
|
||||||
|
summary:
|
||||||
|
'وسّع شبكة المجتمع المدني الوطنية وحوّل الحوار الشامل إلى عمل مجتمعي.',
|
||||||
|
description:
|
||||||
|
`بتمويل من الاتحاد الأوروبي وتنفيذ Bureau Ensemble بالشراكة مع فاعلين ليبيين، بنى شارب الثاني على إنجازات شارب الأول لتعزيز سيادة القانون والاستقرار والحوكمة التشاركية. امتد البرنامج من سبتمبر 2022 إلى أغسطس 2024 بميزانية 1.426 مليون يورو.\n\n### الأولويات\nعمل شارب الثاني على توسيع شبكة وطنية لمجموعات الحوار حول سيادة القانون، وتعزيز التماسك الاجتماعي والتوافق محلياً وإقليمياً ووطنياً، وتنظيم حوارات شاملة حول سيادة القانون والمواطنة الإيجابية، ودعم مبادرات مجتمعية صغيرة.\n\n### الحوار وتوسيع الشبكة\nعُقدت 157 جلسة حوار SUQ بمشاركة 2,554 شخصاً، وكانت نسبة النساء 44%. وتناولت الحوارات الوصول إلى العدالة وتمكين المرأة والحوكمة المحلية ودور الإعلام في سيادة القانون. ونمت الشبكة إلى 64 منظمة مجتمع مدني ناشطة بعد أن كانت 27، وامتدت إلى جميع المناطق بما فيها المناطق المهمشة في الجنوب.\n\n### بناء القدرات والعمل المجتمعي\nعُقدت 6 جلسات للتدريب والتعلّم حول التيسير وإدارة المشاريع والرصد. وشمل التدريب المتقدم 100 مشارك، حصل 24 منهم على اعتماد مدربين. ونُفّذت العقود الفرعية الـ34 المعتمدة، وتناولت التوعية القانونية والمصالحة وحقوق المرأة ومكافحة العنف القائم على النوع الاجتماعي وبناء قدرات البلديات والصحافة الأخلاقية ومواجهة المعلومات المضللة. ووصلت الأنشطة إلى 1,487 متدرباً و1,697 مشاركاً في الورش؛ واستفاد 2,896 شخصاً من حملات التوعية.\n\n### السياسات والتواصل والأثر\nأسهمت الحوارات الإقليمية والوطنية في صياغة توصيات لتسريع إجراءات المحاكم وتوفير مكاتب للمساعدة القانونية، وتمكين البلديات بموجب قانون الحكم المحلي رقم 59، ودعم الشفافية الإعلامية ومكافحة خطاب الكراهية. وأسهم الرصد بقيادة ليبية في تكييف التنفيذ، كما عززت 1,555 نشاطاً إعلامياً أثر البرنامج. وقادت منظمات المجتمع المدني 100% من الأنشطة، وتعاونت معها الوزارات والبلديات، وشارك الشباب والنازحون والأشخاص ذوو الإعاقة.\n\n### خطة خروج مستدامة\nصممت مرحلة الخروج، من مارس إلى أغسطس 2024، ثلاثة مشاريع لضمان الأثر على المدى الطويل: الوصول إلى العدالة عبر المساعدة القانونية والتدريب على الوساطة والإصلاحات؛ و«تعزيز» (TAZIZ) لبناء قدرات البلديات ومشاركة المواطنين؛ و«أضواء» (ADWA) للصحافة الأخلاقية ومواجهة المعلومات المضللة. كما أُسست وسُجلت «United Efforts» ككيان قانوني ليبي لضمان استمرارية العمل.`,
|
||||||
|
metrics: [
|
||||||
|
{ value: '157', label: 'جلسة حوار SUQ' },
|
||||||
|
{ value: '2,554', label: 'مشاركاً في الحوار' },
|
||||||
|
{ value: '64', label: 'منظمة مجتمع مدني ناشطة' },
|
||||||
|
{ value: '44%', label: 'مشاركة النساء' },
|
||||||
|
{ value: '34', label: 'عقداً فرعياً' },
|
||||||
|
{ value: '100', label: 'مشاركاً في التدريب المتقدم' },
|
||||||
|
{ value: '24', label: 'مدرباً معتمداً' },
|
||||||
|
{ value: '1,487', label: 'متدرباً' },
|
||||||
|
{ value: '1,697', label: 'مشاركاً في الورش' },
|
||||||
|
{ value: '2,896', label: 'مستفيداً من حملات التوعية' },
|
||||||
|
{ value: '1,555', label: 'نشاطاً للتواصل الإعلامي' },
|
||||||
|
],
|
||||||
|
focusHeading: 'مجالات العمل',
|
||||||
|
focusAreas: [
|
||||||
|
{ name: 'الوصول إلى العدالة' },
|
||||||
|
{ name: 'تمكين المرأة' },
|
||||||
|
{ name: 'الحوكمة المحلية' },
|
||||||
|
{ name: 'الإعلام وسيادة القانون' },
|
||||||
|
{ name: 'التماسك الاجتماعي' },
|
||||||
|
{ name: 'سيادة القانون' },
|
||||||
|
{ name: 'المواطنة الإيجابية' },
|
||||||
|
],
|
||||||
|
galleryHeading: 'شارب الثاني ميدانياً',
|
||||||
|
},
|
||||||
|
closingHeading: 'مرحلة خروج شارب الثاني',
|
||||||
|
closingBody: [
|
||||||
|
'من مارس إلى أغسطس 2024، ركزت مرحلة خروج شارب الثاني على الوصول إلى العدالة، والحوكمة المحلية عبر «تعزيز» (TAZIZ)، والإعلام عبر «أضواء» (ADWA).',
|
||||||
|
'أُسست «United Efforts» وسُجلت ككيان قانوني ليبي. ويشير ملخص شارب الثاني إلى شبكة وطنية مستقرة من منظمات المجتمع المدني وخارطة طريق لتعاون الاتحاد الأوروبي وليبيا مستقبلاً عبر شارب-إي آر.',
|
||||||
|
].join('\n\n'),
|
||||||
|
seo: {
|
||||||
|
metaTitle: 'إرث شارب',
|
||||||
|
metaDescription:
|
||||||
|
'اكتشفوا الخبرات والإنجازات والشبكات التي بُنيت من خلال مشروعي شارب الأول والثاني في ليبيا.',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
/** The role chip and its line on the About page's consortium cards. */
|
/** The role chip and its line on the About page's consortium cards. */
|
||||||
export const CONSORTIUM_ROLES: Readonly<
|
export const CONSORTIUM_ROLES: Readonly<
|
||||||
Record<string, Localised<{ role: string; roleDescription: string }>>
|
Record<string, Localised<{ role: string; roleDescription: string }>>
|
||||||
|
|||||||
@@ -911,15 +911,3 @@ export const demoKnowledge = (now: Date): readonly DemoEntry[] => {
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* The home page fields the demo set overrides.
|
|
||||||
*
|
|
||||||
* The status pill is the one piece of approved-looking copy the demo writes,
|
|
||||||
* and it is true of the demo data: `demo-call-for-expression-of-interest-
|
|
||||||
* round-two` really is open on a demo-seeded site.
|
|
||||||
*/
|
|
||||||
export const DEMO_HOME_PAGE: Localised<SeedData> = {
|
|
||||||
en: { hero: { eyebrow: 'Now open · Funding calls for Libyan organisations' } },
|
|
||||||
ar: { hero: { eyebrow: 'مفتوح الآن · دعوات تمويل للمنظمات الليبية' } },
|
|
||||||
};
|
|
||||||
|
|||||||
+74
-12
@@ -4,6 +4,7 @@
|
|||||||
Run it with `pnpm seed` from the repository root, or from apps/cms:
|
Run it with `pnpm seed` from the repository root, or from apps/cms:
|
||||||
|
|
||||||
pnpm seed the approved content only (approved.ts)
|
pnpm seed the approved content only (approved.ts)
|
||||||
|
pnpm seed -- --legacy-only only create/update the Legacy single type
|
||||||
pnpm seed --demo the same, plus the invented fixtures (demo.ts)
|
pnpm seed --demo the same, plus the invented fixtures (demo.ts)
|
||||||
pnpm seed --demo --clean remove the invented fixtures, leave the rest
|
pnpm seed --demo --clean remove the invented fixtures, leave the rest
|
||||||
|
|
||||||
@@ -17,10 +18,9 @@
|
|||||||
documentId, and a relation can only be set once its target exists.
|
documentId, and a relation can only be set once its target exists.
|
||||||
|
|
||||||
WHAT IT DOES NOT TOUCH
|
WHAT IT DOES NOT TOUCH
|
||||||
* Media. The seed uploads nothing. Where a page already has a picture -
|
* Media. The seed uploads nothing. Existing home and Legacy hero/gallery
|
||||||
the home hero, most of all - the existing file is read back and written
|
media are read back and written out again with the copy, because a
|
||||||
out again with the new copy, because a component is replaced wholesale
|
component is replaced wholesale on write and omitted media is removed.
|
||||||
on write and anything left out of the payload is gone.
|
|
||||||
* Submissions. `application`, `event-registration` and `contact-submission`
|
* Submissions. `application`, `event-registration` and `contact-submission`
|
||||||
hold personal data. Nothing writes to them here, not even in demo mode.
|
hold personal data. Nothing writes to them here, not even in demo mode.
|
||||||
* Locales and permissions. Those are reconciled at boot by src/bootstrap.
|
* Locales and permissions. Those are reconciled at boot by src/bootstrap.
|
||||||
@@ -35,6 +35,7 @@ import {
|
|||||||
CONSORTIUM_ROLES,
|
CONSORTIUM_ROLES,
|
||||||
CONTACT_PAGE,
|
CONTACT_PAGE,
|
||||||
HOME_PAGE,
|
HOME_PAGE,
|
||||||
|
LEGACY_PAGE,
|
||||||
LAUNCH_ARTICLE,
|
LAUNCH_ARTICLE,
|
||||||
SITE_SETTINGS,
|
SITE_SETTINGS,
|
||||||
TOPICS,
|
TOPICS,
|
||||||
@@ -43,7 +44,6 @@ import {
|
|||||||
} from './approved';
|
} from './approved';
|
||||||
import {
|
import {
|
||||||
DEMO_FAQS,
|
DEMO_FAQS,
|
||||||
DEMO_HOME_PAGE,
|
|
||||||
DEMO_PARTNERS,
|
DEMO_PARTNERS,
|
||||||
DEMO_PARTNER_SLUGS,
|
DEMO_PARTNER_SLUGS,
|
||||||
DEMO_PREFIX,
|
DEMO_PREFIX,
|
||||||
@@ -59,6 +59,7 @@ import {
|
|||||||
deleteBySlugPrefix,
|
deleteBySlugPrefix,
|
||||||
deleteWhere,
|
deleteWhere,
|
||||||
mediaId,
|
mediaId,
|
||||||
|
mediaIds,
|
||||||
readExisting,
|
readExisting,
|
||||||
upsert,
|
upsert,
|
||||||
type Localised,
|
type Localised,
|
||||||
@@ -71,12 +72,15 @@ export interface SeedOptions {
|
|||||||
readonly demo: boolean;
|
readonly demo: boolean;
|
||||||
/** With `demo`, remove those fixtures instead of writing them. */
|
/** With `demo`, remove those fixtures instead of writing them. */
|
||||||
readonly clean: boolean;
|
readonly clean: boolean;
|
||||||
|
/** Write only the bilingual Legacy single type, preserving its media. */
|
||||||
|
readonly legacyOnly: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const UID = {
|
const UID = {
|
||||||
siteSettings: 'api::site-settings.site-settings',
|
siteSettings: 'api::site-settings.site-settings',
|
||||||
homePage: 'api::home-page.home-page',
|
homePage: 'api::home-page.home-page',
|
||||||
aboutPage: 'api::about-page.about-page',
|
aboutPage: 'api::about-page.about-page',
|
||||||
|
legacyPage: 'api::legacy-page.legacy-page',
|
||||||
whatWeDoPage: 'api::what-we-do-page.what-we-do-page',
|
whatWeDoPage: 'api::what-we-do-page.what-we-do-page',
|
||||||
contactPage: 'api::contact-page.contact-page',
|
contactPage: 'api::contact-page.contact-page',
|
||||||
partner: 'api::partner.partner',
|
partner: 'api::partner.partner',
|
||||||
@@ -218,6 +222,8 @@ async function seedApproved(strapi: Core.Strapi): Promise<{
|
|||||||
});
|
});
|
||||||
log(strapi, 'about page');
|
log(strapi, 'about page');
|
||||||
|
|
||||||
|
await seedLegacyPage(strapi);
|
||||||
|
|
||||||
await upsert(strapi, UID.whatWeDoPage, WHAT_WE_DO_PAGE);
|
await upsert(strapi, UID.whatWeDoPage, WHAT_WE_DO_PAGE);
|
||||||
log(strapi, 'what we do page');
|
log(strapi, 'what we do page');
|
||||||
|
|
||||||
@@ -227,6 +233,57 @@ async function seedApproved(strapi: Core.Strapi): Promise<{
|
|||||||
return { partners, topics, launchArticleId: launch.documentId };
|
return { partners, topics, launchArticleId: launch.documentId };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Seed only the Legacy single type, leaving every other CMS record untouched. */
|
||||||
|
async function seedLegacyPage(strapi: Core.Strapi): Promise<void> {
|
||||||
|
/* A component is replaced wholesale on update. Carry editor-selected hero
|
||||||
|
and phase-gallery media across reseeds so the approved copy never drops it. */
|
||||||
|
const populate = [
|
||||||
|
'hero.media',
|
||||||
|
'stageOne.images',
|
||||||
|
'stageTwo.images',
|
||||||
|
];
|
||||||
|
const existingArabic = await readExisting(strapi, UID.legacyPage, populate);
|
||||||
|
const existingEnglish = await readExisting(strapi, UID.legacyPage, populate, undefined, 'en');
|
||||||
|
const existingFor = (locale: 'en' | 'ar') =>
|
||||||
|
locale === 'en' ? existingEnglish ?? existingArabic : existingArabic ?? existingEnglish;
|
||||||
|
const mediaFor = (locale: 'en' | 'ar') => {
|
||||||
|
const existing = existingFor(locale);
|
||||||
|
return {
|
||||||
|
hero: mediaId(existing?.hero, 'media'),
|
||||||
|
one: mediaIds(existing?.stageOne, 'images'),
|
||||||
|
two: mediaIds(existing?.stageTwo, 'images'),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
const enMedia = mediaFor('en');
|
||||||
|
const arMedia = mediaFor('ar');
|
||||||
|
const withGallery = (stage: unknown, images: number[]): SeedData => ({
|
||||||
|
...(stage as SeedData),
|
||||||
|
...(images.length > 0 ? { images } : {}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await upsert(strapi, UID.legacyPage, {
|
||||||
|
en: {
|
||||||
|
...LEGACY_PAGE.en,
|
||||||
|
hero: {
|
||||||
|
...(LEGACY_PAGE.en.hero as SeedData),
|
||||||
|
...(enMedia.hero ? { media: enMedia.hero } : {}),
|
||||||
|
},
|
||||||
|
stageOne: withGallery(LEGACY_PAGE.en.stageOne, enMedia.one),
|
||||||
|
stageTwo: withGallery(LEGACY_PAGE.en.stageTwo, enMedia.two),
|
||||||
|
},
|
||||||
|
ar: {
|
||||||
|
...LEGACY_PAGE.ar,
|
||||||
|
hero: {
|
||||||
|
...(LEGACY_PAGE.ar.hero as SeedData),
|
||||||
|
...(arMedia.hero ? { media: arMedia.hero } : {}),
|
||||||
|
},
|
||||||
|
stageOne: withGallery(LEGACY_PAGE.ar.stageOne, arMedia.one),
|
||||||
|
stageTwo: withGallery(LEGACY_PAGE.ar.stageTwo, arMedia.two),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
log(strapi, 'legacy page');
|
||||||
|
}
|
||||||
|
|
||||||
/* ---------------------------------------------------------------------------
|
/* ---------------------------------------------------------------------------
|
||||||
THE DEMO SET
|
THE DEMO SET
|
||||||
------------------------------------------------------------------------ */
|
------------------------------------------------------------------------ */
|
||||||
@@ -326,11 +383,10 @@ async function seedDemo(
|
|||||||
}
|
}
|
||||||
log(strapi, `${knowledge.length} demo knowledge items`);
|
log(strapi, `${knowledge.length} demo knowledge items`);
|
||||||
|
|
||||||
/* Finally the home page, which the demo set only adds to: the status pill,
|
/* Finally the home page: the demo set adds twelve organisations to the
|
||||||
the network logo wall (twelve organisations, which is what turns the wall
|
network logo wall and two network stories beside the real launch article.
|
||||||
into the design's marquee), and two network stories beside the real launch
|
The hero is rewritten in full, media included, because writing a component
|
||||||
article. The hero is rewritten in full, media included, because writing a
|
replaces it. */
|
||||||
component replaces it. */
|
|
||||||
const existingHome = await readExisting(strapi, UID.homePage, ['hero.media']);
|
const existingHome = await readExisting(strapi, UID.homePage, ['hero.media']);
|
||||||
const heroMedia = mediaId(existingHome?.hero, 'media');
|
const heroMedia = mediaId(existingHome?.hero, 'media');
|
||||||
|
|
||||||
@@ -344,7 +400,6 @@ async function seedDemo(
|
|||||||
...HOME_PAGE[locale],
|
...HOME_PAGE[locale],
|
||||||
hero: {
|
hero: {
|
||||||
...(HOME_PAGE[locale].hero as SeedData),
|
...(HOME_PAGE[locale].hero as SeedData),
|
||||||
...(DEMO_HOME_PAGE[locale].hero as SeedData),
|
|
||||||
...(heroMedia ? { media: heroMedia } : {}),
|
...(heroMedia ? { media: heroMedia } : {}),
|
||||||
},
|
},
|
||||||
featuredNews: featured,
|
featuredNews: featured,
|
||||||
@@ -376,7 +431,7 @@ async function cleanDemo(strapi: Core.Strapi): Promise<void> {
|
|||||||
|
|
||||||
/* The home page is approved content with demo fields written onto it, so it
|
/* The home page is approved content with demo fields written onto it, so it
|
||||||
is put back rather than deleted. Deleting the partners above already
|
is put back rather than deleted. Deleting the partners above already
|
||||||
emptied the wall; this takes the status pill off again. */
|
emptied the wall. */
|
||||||
const existingHome = await readExisting(strapi, UID.homePage, ['hero.media']);
|
const existingHome = await readExisting(strapi, UID.homePage, ['hero.media']);
|
||||||
const heroMedia = mediaId(existingHome?.hero, 'media');
|
const heroMedia = mediaId(existingHome?.hero, 'media');
|
||||||
|
|
||||||
@@ -420,6 +475,12 @@ export async function runSeed(strapi: Core.Strapi, options: SeedOptions): Promis
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options.legacyOnly) {
|
||||||
|
if (options.demo) throw new Error('[seed] --legacy-only cannot be combined with --demo.');
|
||||||
|
await seedLegacyPage(strapi);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const context = await seedApproved(strapi);
|
const context = await seedApproved(strapi);
|
||||||
|
|
||||||
if (options.demo) {
|
if (options.demo) {
|
||||||
@@ -435,6 +496,7 @@ export function parseSeedOptions(argv: readonly string[]): SeedOptions {
|
|||||||
return {
|
return {
|
||||||
demo: argv.includes('--demo'),
|
demo: argv.includes('--demo'),
|
||||||
clean: argv.includes('--clean'),
|
clean: argv.includes('--clean'),
|
||||||
|
legacyOnly: argv.includes('--legacy-only'),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -218,11 +218,12 @@ export async function readExisting(
|
|||||||
strapi: Core.Strapi,
|
strapi: Core.Strapi,
|
||||||
uid: SeedUid,
|
uid: SeedUid,
|
||||||
populate: string[],
|
populate: string[],
|
||||||
find?: SeedData
|
find?: SeedData,
|
||||||
|
locale: SeedLocale = 'ar'
|
||||||
): Promise<SeedDocument | null> {
|
): Promise<SeedDocument | null> {
|
||||||
return documents(strapi, uid).findFirst({
|
return documents(strapi, uid).findFirst({
|
||||||
...(find ? { filters: find } : {}),
|
...(find ? { filters: find } : {}),
|
||||||
locale: 'ar',
|
locale,
|
||||||
...draftFilter(strapi, uid),
|
...draftFilter(strapi, uid),
|
||||||
populate,
|
populate,
|
||||||
});
|
});
|
||||||
@@ -236,3 +237,15 @@ export function mediaId(component: unknown, field: string): number | undefined {
|
|||||||
const id = (media as Record<string, unknown>).id;
|
const id = (media as Record<string, unknown>).id;
|
||||||
return typeof id === 'number' ? id : undefined;
|
return typeof id === 'number' ? id : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The ids of populated media entries nested in a component. */
|
||||||
|
export function mediaIds(component: unknown, field: string): number[] {
|
||||||
|
if (!component || typeof component !== 'object') return [];
|
||||||
|
const media = (component as Record<string, unknown>)[field];
|
||||||
|
if (!Array.isArray(media)) return [];
|
||||||
|
return media.flatMap((item: unknown) => {
|
||||||
|
if (!item || typeof item !== 'object') return [];
|
||||||
|
const id = (item as Record<string, unknown>).id;
|
||||||
|
return typeof id === 'number' ? [id] : [];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
+46
@@ -196,6 +196,49 @@ export interface PageConsortiumMember extends Struct.ComponentSchema {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface PageLegacyFocusArea extends Struct.ComponentSchema {
|
||||||
|
collectionName: 'components_page_legacy_focus_areas';
|
||||||
|
info: {
|
||||||
|
displayName: 'Legacy Focus Area';
|
||||||
|
icon: 'bulletList';
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
name: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageLegacyMetric extends Struct.ComponentSchema {
|
||||||
|
collectionName: 'components_page_legacy_metrics';
|
||||||
|
info: {
|
||||||
|
displayName: 'Legacy Metric';
|
||||||
|
icon: 'chartBubble';
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
label: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
value: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PageLegacyStage extends Struct.ComponentSchema {
|
||||||
|
collectionName: 'components_page_legacy_stages';
|
||||||
|
info: {
|
||||||
|
description: 'One SHARP programme phase with copy, figures, areas of work, and its own image gallery.';
|
||||||
|
displayName: 'Legacy Phase';
|
||||||
|
icon: 'landmark';
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
description: Schema.Attribute.RichText;
|
||||||
|
focusAreas: Schema.Attribute.Component<'page.legacy-focus-area', true>;
|
||||||
|
focusHeading: Schema.Attribute.String;
|
||||||
|
galleryHeading: Schema.Attribute.String;
|
||||||
|
images: Schema.Attribute.Media<'images', true>;
|
||||||
|
metrics: Schema.Attribute.Component<'page.legacy-metric', true>;
|
||||||
|
name: Schema.Attribute.String & Schema.Attribute.Required;
|
||||||
|
summary: Schema.Attribute.Text;
|
||||||
|
years: Schema.Attribute.String;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface PageWorkstream extends Struct.ComponentSchema {
|
export interface PageWorkstream extends Struct.ComponentSchema {
|
||||||
collectionName: 'components_page_workstreams';
|
collectionName: 'components_page_workstreams';
|
||||||
info: {
|
info: {
|
||||||
@@ -355,6 +398,9 @@ declare module '@strapi/strapi' {
|
|||||||
'navigation.nav-group': NavigationNavGroup;
|
'navigation.nav-group': NavigationNavGroup;
|
||||||
'page.band-intro': PageBandIntro;
|
'page.band-intro': PageBandIntro;
|
||||||
'page.consortium-member': PageConsortiumMember;
|
'page.consortium-member': PageConsortiumMember;
|
||||||
|
'page.legacy-focus-area': PageLegacyFocusArea;
|
||||||
|
'page.legacy-metric': PageLegacyMetric;
|
||||||
|
'page.legacy-stage': PageLegacyStage;
|
||||||
'page.workstream': PageWorkstream;
|
'page.workstream': PageWorkstream;
|
||||||
'shared.accordion-item': SharedAccordionItem;
|
'shared.accordion-item': SharedAccordionItem;
|
||||||
'shared.address': SharedAddress;
|
'shared.address': SharedAddress;
|
||||||
|
|||||||
+87
@@ -1393,6 +1393,92 @@ export interface ApiKnowledgeItemKnowledgeItem
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ApiLegacyPageLegacyPage extends Struct.SingleTypeSchema {
|
||||||
|
collectionName: 'legacy_pages';
|
||||||
|
info: {
|
||||||
|
description: 'SHARP I and SHARP II legacy content, hero media, and phase galleries.';
|
||||||
|
displayName: 'Legacy Page';
|
||||||
|
pluralName: 'legacy-pages';
|
||||||
|
singularName: 'legacy-page';
|
||||||
|
};
|
||||||
|
options: {
|
||||||
|
draftAndPublish: true;
|
||||||
|
};
|
||||||
|
pluginOptions: {
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
attributes: {
|
||||||
|
closingBody: Schema.Attribute.RichText &
|
||||||
|
Schema.Attribute.SetPluginOptions<{
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
closingHeading: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.SetPluginOptions<{
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
createdAt: Schema.Attribute.DateTime;
|
||||||
|
createdBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
hero: Schema.Attribute.Component<'shared.hero', false> &
|
||||||
|
Schema.Attribute.SetPluginOptions<{
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
homeEyebrow: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.SetPluginOptions<{
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
homeHeading: Schema.Attribute.String &
|
||||||
|
Schema.Attribute.SetPluginOptions<{
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
homeLead: Schema.Attribute.Text &
|
||||||
|
Schema.Attribute.SetPluginOptions<{
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
locale: Schema.Attribute.String;
|
||||||
|
localizations: Schema.Attribute.Relation<
|
||||||
|
'oneToMany',
|
||||||
|
'api::legacy-page.legacy-page'
|
||||||
|
>;
|
||||||
|
publishedAt: Schema.Attribute.DateTime;
|
||||||
|
seo: Schema.Attribute.Component<'shared.seo', false> &
|
||||||
|
Schema.Attribute.SetPluginOptions<{
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
stageOne: Schema.Attribute.Component<'page.legacy-stage', false> &
|
||||||
|
Schema.Attribute.SetPluginOptions<{
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
stageTwo: Schema.Attribute.Component<'page.legacy-stage', false> &
|
||||||
|
Schema.Attribute.SetPluginOptions<{
|
||||||
|
i18n: {
|
||||||
|
localized: true;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
updatedAt: Schema.Attribute.DateTime;
|
||||||
|
updatedBy: Schema.Attribute.Relation<'oneToOne', 'admin::user'> &
|
||||||
|
Schema.Attribute.Private;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export interface ApiNewsArticleNewsArticle extends Struct.CollectionTypeSchema {
|
export interface ApiNewsArticleNewsArticle extends Struct.CollectionTypeSchema {
|
||||||
collectionName: 'news_articles';
|
collectionName: 'news_articles';
|
||||||
info: {
|
info: {
|
||||||
@@ -2789,6 +2875,7 @@ declare module '@strapi/strapi' {
|
|||||||
'api::faq.faq': ApiFaqFaq;
|
'api::faq.faq': ApiFaqFaq;
|
||||||
'api::home-page.home-page': ApiHomePageHomePage;
|
'api::home-page.home-page': ApiHomePageHomePage;
|
||||||
'api::knowledge-item.knowledge-item': ApiKnowledgeItemKnowledgeItem;
|
'api::knowledge-item.knowledge-item': ApiKnowledgeItemKnowledgeItem;
|
||||||
|
'api::legacy-page.legacy-page': ApiLegacyPageLegacyPage;
|
||||||
'api::news-article.news-article': ApiNewsArticleNewsArticle;
|
'api::news-article.news-article': ApiNewsArticleNewsArticle;
|
||||||
'api::opportunity.opportunity': ApiOpportunityOpportunity;
|
'api::opportunity.opportunity': ApiOpportunityOpportunity;
|
||||||
'api::partner.partner': ApiPartnerPartner;
|
'api::partner.partner': ApiPartnerPartner;
|
||||||
|
|||||||
+1
-1
@@ -28,7 +28,7 @@ the canonical URL is derived from the request).
|
|||||||
```
|
```
|
||||||
app/
|
app/
|
||||||
├── root.tsx HTML shell: <html lang/dir>, canonical + hreflang, <Navigation>, <Footer>
|
├── root.tsx HTML shell: <html lang/dir>, canonical + hreflang, <Navigation>, <Footer>
|
||||||
├── routes.ts Route config: "/" redirect, the ":lang" layout, the ten sections
|
├── routes.ts Route config: "/" redirect, the ":lang" layout, the eleven sections
|
||||||
├── routes/
|
├── routes/
|
||||||
│ ├── *.tsx One module per page, serving BOTH locales
|
│ ├── *.tsx One module per page, serving BOTH locales
|
||||||
│ ├── _page-kit/ Section/hero/prose primitives, markdown renderer, CMS helpers
|
│ ├── _page-kit/ Section/hero/prose primitives, markdown renderer, CMS helpers
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
DEFAULT INFORMATION ARCHITECTURE
|
DEFAULT INFORMATION ARCHITECTURE
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
The ten top-level destinations from the Figma navigation (frame 20:236) and
|
The top-level destinations from the Figma navigation (frame 20:236) and
|
||||||
ToR 3.2. The app shell normally feeds `items` to <Navigation /> from the
|
ToR 3.2. The app shell normally feeds `items` to <Navigation /> from the
|
||||||
Strapi site-settings single type; this module is the typed fallback that
|
Strapi site-settings single type; this module is the typed fallback that
|
||||||
renders when Strapi has not been wired yet.
|
renders when Strapi has not been wired yet.
|
||||||
@@ -30,6 +30,7 @@ import type { NavItem, NavigationStrings, NavigationStringsByLocale } from "./ty
|
|||||||
export const NAV_PATHS = {
|
export const NAV_PATHS = {
|
||||||
home: "/",
|
home: "/",
|
||||||
about: "/about",
|
about: "/about",
|
||||||
|
legacy: "/legacy",
|
||||||
whatWeDo: "/what-we-do",
|
whatWeDo: "/what-we-do",
|
||||||
activities: "/activities-and-initiatives",
|
activities: "/activities-and-initiatives",
|
||||||
events: "/events-calendar",
|
events: "/events-calendar",
|
||||||
@@ -53,6 +54,7 @@ const LABELS: Record<Locale, Record<LabelKey, string>> = {
|
|||||||
home: "Home",
|
home: "Home",
|
||||||
aboutAndStrategy: "About and Strategy",
|
aboutAndStrategy: "About and Strategy",
|
||||||
about: "About",
|
about: "About",
|
||||||
|
legacy: "SHARP Legacy",
|
||||||
whatWeDo: "What We Do",
|
whatWeDo: "What We Do",
|
||||||
activities: "Activities and Initiatives",
|
activities: "Activities and Initiatives",
|
||||||
engagementAndOpportunities: "Engagement and Opportunities",
|
engagementAndOpportunities: "Engagement and Opportunities",
|
||||||
@@ -68,6 +70,7 @@ const LABELS: Record<Locale, Record<LabelKey, string>> = {
|
|||||||
home: "الرئيسية",
|
home: "الرئيسية",
|
||||||
aboutAndStrategy: "عن المشروع والاستراتيجية",
|
aboutAndStrategy: "عن المشروع والاستراتيجية",
|
||||||
about: "عن المشروع",
|
about: "عن المشروع",
|
||||||
|
legacy: "إرث شارب",
|
||||||
whatWeDo: "ماذا نفعل",
|
whatWeDo: "ماذا نفعل",
|
||||||
activities: "الأنشطة والمبادرات",
|
activities: "الأنشطة والمبادرات",
|
||||||
engagementAndOpportunities: "المشاركة والفرص",
|
engagementAndOpportunities: "المشاركة والفرص",
|
||||||
@@ -96,6 +99,7 @@ export function getDefaultNavItems(locale: Locale): NavItem[] {
|
|||||||
label: label.aboutAndStrategy,
|
label: label.aboutAndStrategy,
|
||||||
children: [
|
children: [
|
||||||
{ id: "about", label: label.about, href: href("about") },
|
{ id: "about", label: label.about, href: href("about") },
|
||||||
|
{ id: "legacy", label: label.legacy, href: href("legacy") },
|
||||||
{ id: "what-we-do", label: label.whatWeDo, href: href("whatWeDo") },
|
{ id: "what-we-do", label: label.whatWeDo, href: href("whatWeDo") },
|
||||||
{ id: "activities", label: label.activities, href: href("activities") },
|
{ id: "activities", label: label.activities, href: href("activities") },
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ export interface Translation {
|
|||||||
readonly messages: Messages;
|
readonly messages: Messages;
|
||||||
/** `/{locale}` prefixed URL for an unprefixed path. */
|
/** `/{locale}` prefixed URL for an unprefixed path. */
|
||||||
readonly localizedPath: (path: string) => string;
|
readonly localizedPath: (path: string) => string;
|
||||||
/** The locale-prefixed URL of one of the ten sections. */
|
/** The locale-prefixed URL of one of the eleven sections. */
|
||||||
readonly sectionPath: (key: RouteSlugKey) => string;
|
readonly sectionPath: (key: RouteSlugKey) => string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"home": "الرئيسية",
|
"home": "الرئيسية",
|
||||||
"aboutAndStrategy": "عن المشروع والاستراتيجية",
|
"aboutAndStrategy": "عن المشروع والاستراتيجية",
|
||||||
"about": "عن المشروع",
|
"about": "عن المشروع",
|
||||||
|
"legacy": "إرث شارب",
|
||||||
"whatWeDo": "ماذا نفعل",
|
"whatWeDo": "ماذا نفعل",
|
||||||
"activities": "الأنشطة والمبادرات",
|
"activities": "الأنشطة والمبادرات",
|
||||||
"engagementAndOpportunities": "المشاركة والفرص",
|
"engagementAndOpportunities": "المشاركة والفرص",
|
||||||
@@ -42,6 +43,12 @@
|
|||||||
"en": "English",
|
"en": "English",
|
||||||
"ar": "العربية"
|
"ar": "العربية"
|
||||||
},
|
},
|
||||||
|
"legacy": {
|
||||||
|
"fullRecord": "اقرأ التفاصيل الكاملة لبرنامج {programme}",
|
||||||
|
"previousImage": "الصورة السابقة من {programme}",
|
||||||
|
"nextImage": "الصورة التالية من {programme}",
|
||||||
|
"imageCount": "الصورة {current} من {total}"
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"readMore": "اقرأ المزيد",
|
"readMore": "اقرأ المزيد",
|
||||||
"learnMore": "اعرف المزيد",
|
"learnMore": "اعرف المزيد",
|
||||||
|
|||||||
@@ -24,6 +24,7 @@
|
|||||||
"home": "Home",
|
"home": "Home",
|
||||||
"aboutAndStrategy": "About and Strategy",
|
"aboutAndStrategy": "About and Strategy",
|
||||||
"about": "About",
|
"about": "About",
|
||||||
|
"legacy": "SHARP Legacy",
|
||||||
"whatWeDo": "What We Do",
|
"whatWeDo": "What We Do",
|
||||||
"activities": "Activities and Initiatives",
|
"activities": "Activities and Initiatives",
|
||||||
"engagementAndOpportunities": "Engagement and Opportunities",
|
"engagementAndOpportunities": "Engagement and Opportunities",
|
||||||
@@ -42,6 +43,12 @@
|
|||||||
"en": "English",
|
"en": "English",
|
||||||
"ar": "العربية"
|
"ar": "العربية"
|
||||||
},
|
},
|
||||||
|
"legacy": {
|
||||||
|
"fullRecord": "Read the full {programme} record",
|
||||||
|
"previousImage": "Previous {programme} image",
|
||||||
|
"nextImage": "Next {programme} image",
|
||||||
|
"imageCount": "Image {current} of {total}"
|
||||||
|
},
|
||||||
"actions": {
|
"actions": {
|
||||||
"readMore": "Read more",
|
"readMore": "Read more",
|
||||||
"learnMore": "Learn more",
|
"learnMore": "Learn more",
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export function alternatePath(
|
|||||||
return buildLocalizedHref(pathname, targetLocale, options);
|
return buildLocalizedHref(pathname, targetLocale, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The locale-prefixed URL of one of the ten sections. */
|
/** The locale-prefixed URL of one of the eleven sections. */
|
||||||
export function sectionPath(key: RouteSlugKey, locale: Locale): string {
|
export function sectionPath(key: RouteSlugKey, locale: Locale): string {
|
||||||
return localizePath(sectionSlugPath(key), locale);
|
return localizePath(sectionSlugPath(key), locale);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
/* ============================================================================
|
/* ============================================================================
|
||||||
ROUTE SLUGS - the single source of truth for the site's ten sections
|
ROUTE SLUGS - the single source of truth for the site's eleven sections
|
||||||
----------------------------------------------------------------------------
|
----------------------------------------------------------------------------
|
||||||
This module has ZERO imports on purpose: it is loaded by `app/routes.ts`,
|
This module has ZERO imports on purpose: it is loaded by `app/routes.ts`,
|
||||||
which the React Router dev plugin evaluates in Node while building the route
|
which the React Router dev plugin evaluates in Node while building the route
|
||||||
@@ -23,6 +23,7 @@
|
|||||||
export const ROUTE_SLUGS = {
|
export const ROUTE_SLUGS = {
|
||||||
home: "",
|
home: "",
|
||||||
about: "about",
|
about: "about",
|
||||||
|
legacy: "legacy",
|
||||||
whatWeDo: "what-we-do",
|
whatWeDo: "what-we-do",
|
||||||
activities: "activities-and-initiatives",
|
activities: "activities-and-initiatives",
|
||||||
events: "events-calendar",
|
events: "events-calendar",
|
||||||
@@ -41,7 +42,7 @@ export type RouteSlugKey = keyof typeof ROUTE_SLUGS;
|
|||||||
*/
|
*/
|
||||||
export type RouteSlugs = typeof ROUTE_SLUGS;
|
export type RouteSlugs = typeof ROUTE_SLUGS;
|
||||||
|
|
||||||
/** The ten section keys in navigation order. */
|
/** The section keys in navigation order. */
|
||||||
export const ROUTE_SLUG_KEYS = Object.keys(ROUTE_SLUGS) as readonly RouteSlugKey[];
|
export const ROUTE_SLUG_KEYS = Object.keys(ROUTE_SLUGS) as readonly RouteSlugKey[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -57,7 +58,7 @@ export function sectionSlugPath(key: RouteSlugKey): string {
|
|||||||
export const ROUTE_SECTION_GROUPS = [
|
export const ROUTE_SECTION_GROUPS = [
|
||||||
{
|
{
|
||||||
id: "about-and-strategy",
|
id: "about-and-strategy",
|
||||||
children: ["about", "whatWeDo", "activities"],
|
children: ["about", "legacy", "whatWeDo", "activities"],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "engagement-and-opportunities",
|
id: "engagement-and-opportunities",
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
|
|
||||||
/ -> 302 to /en or /ar (Accept-Language)
|
/ -> 302 to /en or /ar (Accept-Language)
|
||||||
/:lang -> home
|
/:lang -> home
|
||||||
/:lang/<section> -> one of the ten top-level sections
|
/:lang/<section> -> one of the eleven top-level sections
|
||||||
/:lang/<section>/<slug> -> one entry of a CMS-backed section
|
/:lang/<section>/<slug> -> one entry of a CMS-backed section
|
||||||
/:lang/events-calendar/<slug>.ics -> that event as an RFC 5545 file
|
/:lang/events-calendar/<slug>.ics -> that event as an RFC 5545 file
|
||||||
anything else -> 404
|
anything else -> 404
|
||||||
@@ -43,7 +43,7 @@
|
|||||||
ROUTE IDS
|
ROUTE IDS
|
||||||
---------
|
---------
|
||||||
"root" | "routes/locale-redirect" | "locale" | "routes/not-found", one per
|
"root" | "routes/locale-redirect" | "locale" | "routes/not-found", one per
|
||||||
section named after its key: "home", "about", "whatWeDo", "activities",
|
section named after its key: "home", "about", "legacy", "whatWeDo", "activities",
|
||||||
"events", "applications", "news", "contact", "resources", "knowledgeHub" -
|
"events", "applications", "news", "contact", "resources", "knowledgeHub" -
|
||||||
and one per detail route, the section key plus "Detail" ("newsDetail",
|
and one per detail route, the section key plus "Detail" ("newsDetail",
|
||||||
"eventsDetail", ...), plus "eventsIcs". So `useRouteLoaderData("locale")`
|
"eventsDetail", ...), plus "eventsIcs". So `useRouteLoaderData("locale")`
|
||||||
@@ -76,6 +76,7 @@ import { ROUTE_SLUGS, type RouteSlugKey } from "./lib/route-slugs";
|
|||||||
const SECTION_MODULES = {
|
const SECTION_MODULES = {
|
||||||
home: "routes/home.tsx",
|
home: "routes/home.tsx",
|
||||||
about: "routes/about.tsx",
|
about: "routes/about.tsx",
|
||||||
|
legacy: "routes/legacy.tsx",
|
||||||
whatWeDo: "routes/what-we-do.tsx",
|
whatWeDo: "routes/what-we-do.tsx",
|
||||||
activities: "routes/activities-and-initiatives.tsx",
|
activities: "routes/activities-and-initiatives.tsx",
|
||||||
events: "routes/events-calendar.tsx",
|
events: "routes/events-calendar.tsx",
|
||||||
@@ -89,7 +90,7 @@ const SECTION_MODULES = {
|
|||||||
/**
|
/**
|
||||||
* Section -> the module that renders ONE entry of it, at
|
* Section -> the module that renders ONE entry of it, at
|
||||||
* `<section slug>/:slug`. Only the six CMS-backed sections have one; "about",
|
* `<section slug>/:slug`. Only the six CMS-backed sections have one; "about",
|
||||||
* "what-we-do", "contact" and the home page are single pages with nothing to
|
* "legacy", "what-we-do", "contact" and the home page are single pages with nothing to
|
||||||
* drill into.
|
* drill into.
|
||||||
*/
|
*/
|
||||||
const DETAIL_MODULES = {
|
const DETAIL_MODULES = {
|
||||||
@@ -117,7 +118,7 @@ const RESOURCE_ROUTES = [
|
|||||||
const detailKeys = Object.keys(DETAIL_MODULES) as (keyof typeof DETAIL_MODULES)[];
|
const detailKeys = Object.keys(DETAIL_MODULES) as (keyof typeof DETAIL_MODULES)[];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Every page under ":lang": the ten sections, then the six detail routes, then
|
* Every page under ":lang": the eleven sections, then the six detail routes, then
|
||||||
* the .ics resource route. Flat on purpose - see "LISTING AND DETAIL ARE
|
* the .ics resource route. Flat on purpose - see "LISTING AND DETAIL ARE
|
||||||
* SIBLINGS" above.
|
* SIBLINGS" above.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -206,6 +206,46 @@ export interface CmsAboutPage extends StrapiEntity {
|
|||||||
readonly seo?: CmsSeo | null;
|
readonly seo?: CmsSeo | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** page.legacy-metric */
|
||||||
|
export interface CmsLegacyMetric {
|
||||||
|
readonly id?: number;
|
||||||
|
readonly value: string;
|
||||||
|
readonly label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** page.legacy-focus-area */
|
||||||
|
export interface CmsLegacyFocusArea {
|
||||||
|
readonly id?: number;
|
||||||
|
readonly name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** page.legacy-stage */
|
||||||
|
export interface CmsLegacyStage {
|
||||||
|
readonly id?: number;
|
||||||
|
readonly name?: string | null;
|
||||||
|
readonly years?: string | null;
|
||||||
|
readonly summary?: string | null;
|
||||||
|
readonly description?: string | null;
|
||||||
|
readonly metrics?: readonly CmsLegacyMetric[] | null;
|
||||||
|
readonly focusHeading?: string | null;
|
||||||
|
readonly focusAreas?: readonly CmsLegacyFocusArea[] | null;
|
||||||
|
readonly galleryHeading?: string | null;
|
||||||
|
readonly images?: readonly StrapiMedia[] | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** api::legacy-page.legacy-page */
|
||||||
|
export interface CmsLegacyPage extends StrapiEntity {
|
||||||
|
readonly homeEyebrow?: string | null;
|
||||||
|
readonly homeHeading?: string | null;
|
||||||
|
readonly homeLead?: string | null;
|
||||||
|
readonly hero?: CmsHero | null;
|
||||||
|
readonly stageOne?: CmsLegacyStage | null;
|
||||||
|
readonly stageTwo?: CmsLegacyStage | null;
|
||||||
|
readonly closingHeading?: string | null;
|
||||||
|
readonly closingBody?: string | null;
|
||||||
|
readonly seo?: CmsSeo | null;
|
||||||
|
}
|
||||||
|
|
||||||
/** api::what-we-do-page.what-we-do-page */
|
/** api::what-we-do-page.what-we-do-page */
|
||||||
export interface CmsWhatWeDoPage extends StrapiEntity {
|
export interface CmsWhatWeDoPage extends StrapiEntity {
|
||||||
readonly hero?: CmsHero | null;
|
readonly hero?: CmsHero | null;
|
||||||
|
|||||||
@@ -31,13 +31,10 @@
|
|||||||
overridden by the matching Strapi field the moment an editor fills it in,
|
overridden by the matching Strapi field the moment an editor fills it in,
|
||||||
and the pages read `cms ?? draft`.
|
and the pages read `cms ?? draft`.
|
||||||
|
|
||||||
WHAT IS DELIBERATELY ABSENT
|
HISTORICAL FIGURES
|
||||||
Figures. The draft marks every number "TBD" and says they must not be
|
The general SHARP-ER home draft marks current-programme figures "TBD";
|
||||||
published until confirmed, so there is no statistic anywhere in this file.
|
this file adds only the SHARP I and SHARP II historical figures from the
|
||||||
Figma hides the home page's figures band outright (frame 253:4835 is
|
supplied legacy content.
|
||||||
hidden="true") and the third design drop deletes it, so the band is gone
|
|
||||||
from the page and the `stats` component is gone from the CMS. There is
|
|
||||||
nowhere left for a figure to be typed in, which is the point.
|
|
||||||
========================================================================= */
|
========================================================================= */
|
||||||
|
|
||||||
import type { Locale } from "~/i18n";
|
import type { Locale } from "~/i18n";
|
||||||
@@ -104,6 +101,44 @@ export interface HomeCopy {
|
|||||||
readonly partnersEmpty: string;
|
readonly partnersEmpty: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface LegacyMetric {
|
||||||
|
readonly value: string;
|
||||||
|
readonly label: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyGalleryImage {
|
||||||
|
readonly src: string;
|
||||||
|
readonly alt: string;
|
||||||
|
readonly caption?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyStageCopy {
|
||||||
|
readonly name: string;
|
||||||
|
readonly years: string;
|
||||||
|
readonly summary: string;
|
||||||
|
readonly description: string;
|
||||||
|
readonly metrics: readonly LegacyMetric[];
|
||||||
|
readonly focusHeading: string;
|
||||||
|
readonly focusAreas: readonly string[];
|
||||||
|
readonly imageHeading: string;
|
||||||
|
readonly imageEmpty: string;
|
||||||
|
readonly images: readonly LegacyGalleryImage[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface LegacyCopy {
|
||||||
|
readonly homeEyebrow: string;
|
||||||
|
readonly homeHeading: string;
|
||||||
|
readonly homeLead: string;
|
||||||
|
readonly homeLinkLabel: string;
|
||||||
|
readonly heroEyebrow: string;
|
||||||
|
readonly heroTitle: string;
|
||||||
|
readonly heroLead: string;
|
||||||
|
readonly stageOne: LegacyStageCopy;
|
||||||
|
readonly stageTwo: LegacyStageCopy;
|
||||||
|
readonly closingHeading: string;
|
||||||
|
readonly closingParagraphs: readonly string[];
|
||||||
|
}
|
||||||
|
|
||||||
export interface AboutCopy {
|
export interface AboutCopy {
|
||||||
/** Draft eyebrow over the hero headline - "About the project". */
|
/** Draft eyebrow over the hero headline - "About the project". */
|
||||||
readonly heroEyebrow: string;
|
readonly heroEyebrow: string;
|
||||||
@@ -170,8 +205,6 @@ export interface ContactCopy {
|
|||||||
readonly websiteHref: string;
|
readonly websiteHref: string;
|
||||||
readonly websiteText: string;
|
readonly websiteText: string;
|
||||||
readonly contactPending: string;
|
readonly contactPending: string;
|
||||||
readonly consortiumHeading: string;
|
|
||||||
readonly consortiumEmpty: string;
|
|
||||||
readonly sendAnother: string;
|
readonly sendAnother: string;
|
||||||
readonly errorSummaryTitle: string;
|
readonly errorSummaryTitle: string;
|
||||||
readonly errorTooLong: string;
|
readonly errorTooLong: string;
|
||||||
@@ -184,6 +217,7 @@ export interface ContactCopy {
|
|||||||
export interface PageCopy {
|
export interface PageCopy {
|
||||||
readonly home: HomeCopy;
|
readonly home: HomeCopy;
|
||||||
readonly about: AboutCopy;
|
readonly about: AboutCopy;
|
||||||
|
readonly legacy: LegacyCopy;
|
||||||
readonly whatWeDo: WhatWeDoCopy;
|
readonly whatWeDo: WhatWeDoCopy;
|
||||||
readonly contact: ContactCopy;
|
readonly contact: ContactCopy;
|
||||||
}
|
}
|
||||||
@@ -241,6 +275,121 @@ const EN: PageCopy = {
|
|||||||
"Partner organisations will be listed here as they join the SHARP-ER network.",
|
"Partner organisations will be listed here as they join the SHARP-ER network.",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
legacy: {
|
||||||
|
homeEyebrow: "SHARP I and SHARP II",
|
||||||
|
homeHeading: "Dialogue and local action in Libya, 2019–2024",
|
||||||
|
homeLead:
|
||||||
|
"Two EU-funded programmes supported rule of law dialogue, civil society capacity, and local action across Libya.",
|
||||||
|
homeLinkLabel: "Explore this phase",
|
||||||
|
heroEyebrow: "SHARP I and SHARP II",
|
||||||
|
heroTitle: "Dialogue and local action across Libya",
|
||||||
|
heroLead:
|
||||||
|
"From November 2019 to August 2024, two EU-funded programmes supported rule of law dialogue, civil society capacity, and locally led action.",
|
||||||
|
stageOne: {
|
||||||
|
name: "SHARP I",
|
||||||
|
years: "November 2019 – August 2022",
|
||||||
|
summary:
|
||||||
|
"Built dialogue and local capacity through research, learning, and community-led action.",
|
||||||
|
description: `The EU-funded SHARP I programme was implemented by Institute Clingendael and Bureau Ensemble with Libyan civil society organisations, local authorities, and national institutions. From November 2019 to August 2022, it worked to strengthen Libya’s rule of law through dialogue, research, and local action. Budget: €1.996 million.
|
||||||
|
|
||||||
|
### Objectives
|
||||||
|
- Strengthen Libyan institutions’ ability to respond to rule of law challenges.
|
||||||
|
- Build the organisational capabilities of Libyan change agents.
|
||||||
|
- Foster dialogue and information-sharing between stakeholders.
|
||||||
|
|
||||||
|
### Dialogue and learning
|
||||||
|
The programme held 102 Search, Understand, and Question (SUQ) dialogues with 1,583 participants; 48% of participants were women. Topics included human rights, transitional justice, and local governance. A further 46 Training & Learning sessions covered facilitation, project management, and monitoring.
|
||||||
|
|
||||||
|
### Community action
|
||||||
|
Twenty-one sub-contract actions addressed conflict mediation, access to justice, women’s rights, labour rights, and civil-society and media engagement. They produced 62 workshops, 6 research papers, 24 training sessions, and 1,555 media outreach activities.
|
||||||
|
|
||||||
|
### Results and lasting value
|
||||||
|
87% of SUQ participants considered the discussions relevant, and 75% believed SHARP could contribute to rule of law change. Projects strengthened CSO reporting skills and partnerships with local authorities. Libyan-led initiatives built community trust; the programme adapted to pandemic and security constraints with virtual activities and local dialogue, helping CSOs develop lasting networks.
|
||||||
|
|
||||||
|
### What followed
|
||||||
|
The EU approved SHARP II to build on these lessons: deepen dialogue, focus on four to six rule of law themes while retaining flexibility, strengthen CSO project design and evaluation, and support durable government–CSO networks.`,
|
||||||
|
metrics: [
|
||||||
|
{ value: "102", label: "SUQ dialogues" },
|
||||||
|
{ value: "1,583", label: "Dialogue participants" },
|
||||||
|
{ value: "48%", label: "Women participants" },
|
||||||
|
{ value: "46", label: "Learning sessions" },
|
||||||
|
{ value: "21", label: "Sub-contract actions" },
|
||||||
|
{ value: "62", label: "Workshops delivered" },
|
||||||
|
{ value: "6", label: "Research papers" },
|
||||||
|
{ value: "24", label: "Training sessions" },
|
||||||
|
{ value: "1,555", label: "Media outreach activities" },
|
||||||
|
{ value: "87%", label: "Found dialogues relevant" },
|
||||||
|
{ value: "75%", label: "Saw potential for change" },
|
||||||
|
],
|
||||||
|
focusHeading: "Areas of work",
|
||||||
|
focusAreas: [
|
||||||
|
"Human rights",
|
||||||
|
"Transitional justice",
|
||||||
|
"Local governance",
|
||||||
|
"Conflict mediation",
|
||||||
|
"Access to justice",
|
||||||
|
"Women’s rights",
|
||||||
|
"Labour rights",
|
||||||
|
"Civil society and media",
|
||||||
|
],
|
||||||
|
imageHeading: "SHARP I in the field",
|
||||||
|
imageEmpty: "Dialogue and local action across Libya.",
|
||||||
|
images: [],
|
||||||
|
},
|
||||||
|
stageTwo: {
|
||||||
|
name: "SHARP II",
|
||||||
|
years: "September 2022 – August 2024",
|
||||||
|
summary:
|
||||||
|
"Expanded a nationwide civil-society network and turned inclusive dialogue into community action.",
|
||||||
|
description: `Funded by the European Union and implemented by Bureau Ensemble with Libyan partners, SHARP II built on SHARP I to strengthen the rule of law, stability, and participatory governance. From September 2022 to August 2024, the programme had a budget of €1.426 million.
|
||||||
|
|
||||||
|
### Priorities
|
||||||
|
SHARP II aimed to grow a national network of rule of law dialogue groups; build social cohesion and consensus locally, regionally, and nationally; organise inclusive dialogues on rule of law and positive citizenship; and support small community actions.
|
||||||
|
|
||||||
|
### Dialogue and network
|
||||||
|
157 SUQ dialogues engaged 2,554 participants, 44% of them women. Discussions covered access to justice, women’s empowerment, local governance, and the media’s role in the rule of law. The network grew to 64 active CSOs from 27, reaching every region, including marginalised areas in the south.
|
||||||
|
|
||||||
|
### Skills and community action
|
||||||
|
Six Training & Learning sessions covered facilitation, project management, and monitoring. Advanced training reached 100 participants, with 24 certified as trainers. All 34 approved sub-contracts were implemented, addressing legal awareness, reconciliation, women’s rights, gender-based violence, municipal capacity, ethical journalism, and misinformation. Activities reached 1,487 trainees and 1,697 workshop participants; awareness campaigns benefited 2,896 people.
|
||||||
|
|
||||||
|
### Policy, communication, and impact
|
||||||
|
Regional and national dialogues informed recommendations on faster court processes and legal aid, municipal empowerment under Local Governance Law 59, and media transparency and hate speech. Libyan-led monitoring supported adaptive delivery, while 1,555 media outreach activities amplified the work. CSOs led 100% of activities; ministries and municipalities partnered with them, and young people, displaced people, and persons with disabilities took part.
|
||||||
|
|
||||||
|
### A sustainable exit
|
||||||
|
The March–August 2024 exit phase shaped three pillars for continued impact: Access to Justice (legal aid, mediation, and policy reform), TAZIZ (municipal capacity and citizen engagement), and ADWA (ethical journalism and countering misinformation). United Efforts was established and registered as a Libyan legal entity to help sustain the work.`,
|
||||||
|
metrics: [
|
||||||
|
{ value: "157", label: "SUQ dialogues" },
|
||||||
|
{ value: "2,554", label: "Dialogue participants" },
|
||||||
|
{ value: "64", label: "Active CSOs" },
|
||||||
|
{ value: "44%", label: "Women participants" },
|
||||||
|
{ value: "34", label: "Sub-contracts" },
|
||||||
|
{ value: "100", label: "Advanced training participants" },
|
||||||
|
{ value: "24", label: "Certified trainers" },
|
||||||
|
{ value: "1,487", label: "Trainees" },
|
||||||
|
{ value: "1,697", label: "Workshop participants" },
|
||||||
|
{ value: "2,896", label: "Awareness campaign beneficiaries" },
|
||||||
|
{ value: "1,555", label: "Media outreach activities" },
|
||||||
|
],
|
||||||
|
focusHeading: "Areas of work",
|
||||||
|
focusAreas: [
|
||||||
|
"Access to justice",
|
||||||
|
"Women’s empowerment",
|
||||||
|
"Local governance",
|
||||||
|
"Media and rule of law",
|
||||||
|
"Social cohesion",
|
||||||
|
"Positive citizenship",
|
||||||
|
],
|
||||||
|
imageHeading: "SHARP II in the field",
|
||||||
|
imageEmpty: "A nationwide civil-society network in action.",
|
||||||
|
images: [],
|
||||||
|
},
|
||||||
|
closingHeading: "SHARP II’s exit phase",
|
||||||
|
closingParagraphs: [
|
||||||
|
"From March to August 2024, the SHARP II exit phase focused on access to justice, local governance through TAZIZ, and media through ADWA.",
|
||||||
|
"United Efforts was established and registered as a Libyan legal entity. The SHARP II summary identifies a stable nationwide civil society network and a roadmap for future EU–Libya collaboration through SHARP-ER.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
about: {
|
about: {
|
||||||
heroEyebrow: "About the project",
|
heroEyebrow: "About the project",
|
||||||
heroHeadline: "Strengthening the Rule of Law, Together",
|
heroHeadline: "Strengthening the Rule of Law, Together",
|
||||||
@@ -357,9 +506,6 @@ const EN: PageCopy = {
|
|||||||
websiteText: "sharper.ly",
|
websiteText: "sharper.ly",
|
||||||
contactPending:
|
contactPending:
|
||||||
"The project’s general enquiries address will be published here once it is confirmed. In the meantime, use the form and your message will reach the team.",
|
"The project’s general enquiries address will be published here once it is confirmed. In the meantime, use the form and your message will reach the team.",
|
||||||
consortiumHeading: "Consortium contacts",
|
|
||||||
consortiumEmpty:
|
|
||||||
"Contact details for Acted Libya, United Efforts Consultancy and Stichting Bureau Ensemble will be published here once they are confirmed.",
|
|
||||||
sendAnother: "Send another message",
|
sendAnother: "Send another message",
|
||||||
errorSummaryTitle: "Please check the form",
|
errorSummaryTitle: "Please check the form",
|
||||||
errorTooLong: "This answer is too long.",
|
errorTooLong: "This answer is too long.",
|
||||||
@@ -423,6 +569,122 @@ const AR: PageCopy = {
|
|||||||
"ستُدرج المنظمات الشريكة هنا فور انضمامها إلى شبكة SHARP-ER.",
|
"ستُدرج المنظمات الشريكة هنا فور انضمامها إلى شبكة SHARP-ER.",
|
||||||
},
|
},
|
||||||
|
|
||||||
|
legacy: {
|
||||||
|
homeEyebrow: "شارب الأول وشارب الثاني",
|
||||||
|
homeHeading: "الحوار والعمل المحلي في ليبيا، 2019–2024",
|
||||||
|
homeLead:
|
||||||
|
"دعم برنامجان ممولان من الاتحاد الأوروبي الحوار حول سيادة القانون، وقدرات المجتمع المدني، والعمل المحلي في مختلف أنحاء ليبيا.",
|
||||||
|
homeLinkLabel: "اكتشفوا هذه المرحلة",
|
||||||
|
heroEyebrow: "شارب الأول وشارب الثاني",
|
||||||
|
heroTitle: "الحوار والعمل المحلي في مختلف أنحاء ليبيا",
|
||||||
|
heroLead:
|
||||||
|
"من نوفمبر 2019 إلى أغسطس 2024، دعم برنامجان ممولان من الاتحاد الأوروبي الحوار حول سيادة القانون، وقدرات المجتمع المدني، والعمل بقيادة محلية.",
|
||||||
|
stageOne: {
|
||||||
|
name: "شارب الأول",
|
||||||
|
years: "نوفمبر 2019 – أغسطس 2022",
|
||||||
|
summary:
|
||||||
|
"عزّز الحوار والقدرات المحلية من خلال البحث والتعلّم والعمل المجتمعي.",
|
||||||
|
description: `نفّذ معهد كلينغندايل ومؤسسة Bureau Ensemble برنامج شارب الأول بتمويل من الاتحاد الأوروبي، وبالتعاون مع منظمات المجتمع المدني والسلطات المحلية والمؤسسات الوطنية الليبية. عمل البرنامج من نوفمبر 2019 إلى أغسطس 2022 على دعم سيادة القانون في ليبيا عبر الحوار والبحث والعمل المحلي، بميزانية بلغت 1.996 مليون يورو.
|
||||||
|
|
||||||
|
### الأهداف
|
||||||
|
- تعزيز قدرة المؤسسات الليبية على الاستجابة لتحديات سيادة القانون.
|
||||||
|
- بناء القدرات التنظيمية للفاعلين الليبيين الساعين إلى التغيير.
|
||||||
|
- تشجيع الحوار وتبادل المعلومات بين أصحاب المصلحة.
|
||||||
|
|
||||||
|
### الحوار والتعلّم
|
||||||
|
عُقدت 102 جلسة «ابحث، افهم، واسأل» (SUQ)، بمشاركة 1,583 شخصاً، وكانت نسبة النساء 48%. وتناولت الجلسات حقوق الإنسان والعدالة الانتقالية والحوكمة المحلية. كما عُقدت 46 جلسة للتدريب والتعلّم حول التيسير وإدارة المشاريع والرصد.
|
||||||
|
|
||||||
|
### العمل المجتمعي
|
||||||
|
نُفّذ 21 مشروعاً فرعياً تناولت الوساطة في النزاعات والوصول إلى العدالة وحقوق النساء وحقوق العمال ومشاركة المجتمع المدني والإعلام. وأسفرت عن 62 ورشة عمل و6 أوراق بحثية و24 جلسة تدريبية و1,555 نشاطاً للتواصل الإعلامي.
|
||||||
|
|
||||||
|
### النتائج والأثر المستدام
|
||||||
|
رأى 87% من المشاركين أن جلسات الحوار ذات صلة، واعتقد 75% أن شارب قادر على الإسهام في تغيير واقع سيادة القانون. وعززت المشاريع مهارات إعداد التقارير لدى منظمات المجتمع المدني وشراكاتها مع السلطات المحلية. كما دعمت المبادرات بقيادة ليبية الثقة المجتمعية، وتكيّف البرنامج مع قيود الجائحة والأوضاع الأمنية عبر الأنشطة الافتراضية والحوار المحلي، بما ساعد المنظمات على بناء شبكات طويلة الأمد.
|
||||||
|
|
||||||
|
### ما تلا ذلك
|
||||||
|
وافق الاتحاد الأوروبي على شارب الثاني للاستفادة من هذه الخبرات: تعميق الحوار، والتركيز على أربعة إلى ستة موضوعات مع الحفاظ على المرونة، وتعزيز مهارات تصميم المشاريع وتقييمها لدى منظمات المجتمع المدني، ودعم شبكات مستدامة بين الحكومة والمجتمع المدني.`,
|
||||||
|
metrics: [
|
||||||
|
{ value: "102", label: "جلسة حوار SUQ" },
|
||||||
|
{ value: "1,583", label: "مشاركاً في الحوار" },
|
||||||
|
{ value: "48%", label: "مشاركة النساء" },
|
||||||
|
{ value: "46", label: "جلسة للتعلّم" },
|
||||||
|
{ value: "21", label: "عملاً بعقد فرعي" },
|
||||||
|
{ value: "62", label: "ورشة عمل" },
|
||||||
|
{ value: "6", label: "أوراق بحثية" },
|
||||||
|
{ value: "24", label: "جلسة تدريبية" },
|
||||||
|
{ value: "1,555", label: "نشاطاً للتواصل الإعلامي" },
|
||||||
|
{ value: "87%", label: "وجدوا الحوارات ذات صلة" },
|
||||||
|
{ value: "75%", label: "رأوا إمكانية إحداث تغيير" },
|
||||||
|
],
|
||||||
|
focusHeading: "مجالات العمل",
|
||||||
|
focusAreas: [
|
||||||
|
"حقوق الإنسان",
|
||||||
|
"العدالة الانتقالية",
|
||||||
|
"الحوكمة المحلية",
|
||||||
|
"الوساطة في النزاعات",
|
||||||
|
"الوصول إلى العدالة",
|
||||||
|
"حقوق النساء",
|
||||||
|
"حقوق العمال",
|
||||||
|
"المجتمع المدني والإعلام",
|
||||||
|
],
|
||||||
|
imageHeading: "شارب الأول ميدانياً",
|
||||||
|
imageEmpty: "حوار وعمل محلي في مختلف أنحاء ليبيا.",
|
||||||
|
images: [],
|
||||||
|
},
|
||||||
|
stageTwo: {
|
||||||
|
name: "شارب الثاني",
|
||||||
|
years: "سبتمبر 2022 – أغسطس 2024",
|
||||||
|
summary:
|
||||||
|
"وسّع شبكة المجتمع المدني الوطنية وحوّل الحوار الشامل إلى عمل مجتمعي.",
|
||||||
|
description: `بتمويل من الاتحاد الأوروبي وتنفيذ Bureau Ensemble بالشراكة مع فاعلين ليبيين، بنى شارب الثاني على إنجازات شارب الأول لتعزيز سيادة القانون والاستقرار والحوكمة التشاركية. امتد البرنامج من سبتمبر 2022 إلى أغسطس 2024 بميزانية 1.426 مليون يورو.
|
||||||
|
|
||||||
|
### الأولويات
|
||||||
|
عمل شارب الثاني على توسيع شبكة وطنية لمجموعات الحوار حول سيادة القانون، وتعزيز التماسك الاجتماعي والتوافق محلياً وإقليمياً ووطنياً، وتنظيم حوارات شاملة حول سيادة القانون والمواطنة الإيجابية، ودعم مبادرات مجتمعية صغيرة.
|
||||||
|
|
||||||
|
### الحوار وتوسيع الشبكة
|
||||||
|
عُقدت 157 جلسة حوار SUQ بمشاركة 2,554 شخصاً، وكانت نسبة النساء 44%. وتناولت الحوارات الوصول إلى العدالة وتمكين المرأة والحوكمة المحلية ودور الإعلام في سيادة القانون. ونمت الشبكة إلى 64 منظمة مجتمع مدني ناشطة بعد أن كانت 27، وامتدت إلى جميع المناطق بما فيها المناطق المهمشة في الجنوب.
|
||||||
|
|
||||||
|
### بناء القدرات والعمل المجتمعي
|
||||||
|
عُقدت 6 جلسات للتدريب والتعلّم حول التيسير وإدارة المشاريع والرصد. وشمل التدريب المتقدم 100 مشارك، حصل 24 منهم على اعتماد مدربين. ونُفّذت العقود الفرعية الـ34 المعتمدة، وتناولت التوعية القانونية والمصالحة وحقوق المرأة ومكافحة العنف القائم على النوع الاجتماعي وبناء قدرات البلديات والصحافة الأخلاقية ومواجهة المعلومات المضللة. ووصلت الأنشطة إلى 1,487 متدرباً و1,697 مشاركاً في الورش؛ واستفاد 2,896 شخصاً من حملات التوعية.
|
||||||
|
|
||||||
|
### السياسات والتواصل والأثر
|
||||||
|
أسهمت الحوارات الإقليمية والوطنية في صياغة توصيات لتسريع إجراءات المحاكم وتوفير مكاتب للمساعدة القانونية، وتمكين البلديات بموجب قانون الحكم المحلي رقم 59، ودعم الشفافية الإعلامية ومكافحة خطاب الكراهية. وأسهم الرصد بقيادة ليبية في تكييف التنفيذ، كما عززت 1,555 نشاطاً إعلامياً أثر البرنامج. وقادت منظمات المجتمع المدني 100% من الأنشطة، وتعاونت معها الوزارات والبلديات، وشارك الشباب والنازحون والأشخاص ذوو الإعاقة.
|
||||||
|
|
||||||
|
### خطة خروج مستدامة
|
||||||
|
صممت مرحلة الخروج، من مارس إلى أغسطس 2024، ثلاثة مشاريع لضمان الأثر على المدى الطويل: الوصول إلى العدالة عبر المساعدة القانونية والتدريب على الوساطة والإصلاحات؛ و«تعزيز» (TAZIZ) لبناء قدرات البلديات ومشاركة المواطنين؛ و«أضواء» (ADWA) للصحافة الأخلاقية ومواجهة المعلومات المضللة. كما أُسست وسُجلت «United Efforts» ككيان قانوني ليبي لضمان استمرارية العمل.`,
|
||||||
|
metrics: [
|
||||||
|
{ value: "157", label: "جلسة حوار SUQ" },
|
||||||
|
{ value: "2,554", label: "مشاركاً في الحوار" },
|
||||||
|
{ value: "64", label: "منظمة مجتمع مدني ناشطة" },
|
||||||
|
{ value: "44%", label: "مشاركة النساء" },
|
||||||
|
{ value: "34", label: "عقداً فرعياً" },
|
||||||
|
{ value: "100", label: "مشاركاً في التدريب المتقدم" },
|
||||||
|
{ value: "24", label: "مدرباً معتمداً" },
|
||||||
|
{ value: "1,487", label: "متدرباً" },
|
||||||
|
{ value: "1,697", label: "مشاركاً في الورش" },
|
||||||
|
{ value: "2,896", label: "مستفيداً من حملات التوعية" },
|
||||||
|
{ value: "1,555", label: "نشاطاً للتواصل الإعلامي" },
|
||||||
|
],
|
||||||
|
focusHeading: "مجالات العمل",
|
||||||
|
focusAreas: [
|
||||||
|
"الوصول إلى العدالة",
|
||||||
|
"تمكين المرأة",
|
||||||
|
"الحوكمة المحلية",
|
||||||
|
"الإعلام وسيادة القانون",
|
||||||
|
"التماسك الاجتماعي",
|
||||||
|
"سيادة القانون",
|
||||||
|
"المواطنة الإيجابية",
|
||||||
|
],
|
||||||
|
imageHeading: "شارب الثاني ميدانياً",
|
||||||
|
imageEmpty: "شبكة وطنية من منظمات المجتمع المدني تتحرك على الأرض.",
|
||||||
|
images: [],
|
||||||
|
},
|
||||||
|
closingHeading: "مرحلة خروج شارب الثاني",
|
||||||
|
closingParagraphs: [
|
||||||
|
"من مارس إلى أغسطس 2024، ركزت مرحلة خروج شارب الثاني على الوصول إلى العدالة، والحوكمة المحلية عبر «تعزيز» (TAZIZ)، والإعلام عبر «أضواء» (ADWA).",
|
||||||
|
"أُسست «United Efforts» وسُجلت ككيان قانوني ليبي. ويشير ملخص شارب الثاني إلى شبكة وطنية مستقرة من منظمات المجتمع المدني وخارطة طريق لتعاون الاتحاد الأوروبي وليبيا مستقبلاً عبر شارب-إي آر.",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
about: {
|
about: {
|
||||||
heroEyebrow: "عن المشروع",
|
heroEyebrow: "عن المشروع",
|
||||||
heroHeadline: "نحو تعزيز سيادة القانون، معًا",
|
heroHeadline: "نحو تعزيز سيادة القانون، معًا",
|
||||||
@@ -544,9 +806,6 @@ const AR: PageCopy = {
|
|||||||
websiteText: "sharper.ly",
|
websiteText: "sharper.ly",
|
||||||
contactPending:
|
contactPending:
|
||||||
"سيُنشر هنا البريد الإلكتروني العام للمشروع فور تأكيده. وفي غضون ذلك، استخدم النموذج وستصل رسالتك إلى الفريق.",
|
"سيُنشر هنا البريد الإلكتروني العام للمشروع فور تأكيده. وفي غضون ذلك، استخدم النموذج وستصل رسالتك إلى الفريق.",
|
||||||
consortiumHeading: "جهات اتصال الائتلاف",
|
|
||||||
consortiumEmpty:
|
|
||||||
"ستُنشر هنا بيانات التواصل الخاصة بـ Acted ليبيا، وUnited Efforts Consultancy، وStichting Bureau Ensemble فور تأكيدها.",
|
|
||||||
sendAnother: "إرسال رسالة أخرى",
|
sendAnother: "إرسال رسالة أخرى",
|
||||||
errorSummaryTitle: "يرجى مراجعة النموذج",
|
errorSummaryTitle: "يرجى مراجعة النموذج",
|
||||||
errorTooLong: "هذه الإجابة طويلة جدًا.",
|
errorTooLong: "هذه الإجابة طويلة جدًا.",
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
/* Server-side mapping from the editable Strapi phase component to page copy. */
|
||||||
|
|
||||||
|
import type { CmsLegacyStage } from "./cms-types";
|
||||||
|
import { list, toImage } from "./cms.server";
|
||||||
|
import type { LegacyGalleryImage, LegacyStageCopy } from "./copy";
|
||||||
|
|
||||||
|
const textOr = (value: string | null | undefined, fallback: string): string =>
|
||||||
|
value?.trim() ? value : fallback;
|
||||||
|
|
||||||
|
export function resolveLegacyStage(
|
||||||
|
source: CmsLegacyStage | null | undefined,
|
||||||
|
fallback: LegacyStageCopy,
|
||||||
|
): LegacyStageCopy {
|
||||||
|
const metrics = list(source?.metrics)
|
||||||
|
.filter((metric) => metric.value.trim() !== "" && metric.label.trim() !== "")
|
||||||
|
.map(({ value, label }) => ({ value, label }));
|
||||||
|
const focusAreas = list(source?.focusAreas)
|
||||||
|
.map((area) => area.name.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
const images: LegacyGalleryImage[] = list(source?.images)
|
||||||
|
.slice(0, 5)
|
||||||
|
.flatMap((media) => {
|
||||||
|
const image = toImage(media, "large");
|
||||||
|
return image
|
||||||
|
? [{
|
||||||
|
src: image.src,
|
||||||
|
alt: image.alt,
|
||||||
|
...(media.caption ? { caption: media.caption } : {}),
|
||||||
|
}]
|
||||||
|
: [];
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
...fallback,
|
||||||
|
name: textOr(source?.name, fallback.name),
|
||||||
|
years: textOr(source?.years, fallback.years),
|
||||||
|
summary: textOr(source?.summary, fallback.summary),
|
||||||
|
description: textOr(source?.description, fallback.description),
|
||||||
|
metrics: metrics.length > 0 ? metrics : fallback.metrics,
|
||||||
|
focusHeading: textOr(source?.focusHeading, fallback.focusHeading),
|
||||||
|
focusAreas: focusAreas.length > 0 ? focusAreas : fallback.focusAreas,
|
||||||
|
imageHeading: textOr(source?.galleryHeading, fallback.imageHeading),
|
||||||
|
images,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -256,12 +256,6 @@ export interface PageHeroProps {
|
|||||||
* footer for EU visibility.
|
* footer for EU visibility.
|
||||||
*/
|
*/
|
||||||
funderBadge?: ReactNode;
|
funderBadge?: ReactNode;
|
||||||
/**
|
|
||||||
* A short status line drawn as a pill above the headline - the redesign's
|
|
||||||
* "Now live" chip. Only rendered when the caller passes one, so it can never
|
|
||||||
* announce a state the CMS has not authored.
|
|
||||||
*/
|
|
||||||
statusPill?: ReactNode;
|
|
||||||
/** Buttons, rendered inline-end of the headline on wide viewports. */
|
/** Buttons, rendered inline-end of the headline on wide viewports. */
|
||||||
actions?: ReactNode;
|
actions?: ReactNode;
|
||||||
/**
|
/**
|
||||||
@@ -378,7 +372,6 @@ export function PageHero({
|
|||||||
lead,
|
lead,
|
||||||
wordmark,
|
wordmark,
|
||||||
funderBadge,
|
funderBadge,
|
||||||
statusPill,
|
|
||||||
actions,
|
actions,
|
||||||
leadWithActions = false,
|
leadWithActions = false,
|
||||||
ambient,
|
ambient,
|
||||||
@@ -454,8 +447,6 @@ export function PageHero({
|
|||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{statusPill ? <div className={rise(2)}>{statusPill}</div> : null}
|
|
||||||
|
|
||||||
{leadWithActions ? (
|
{leadWithActions ? (
|
||||||
<>
|
<>
|
||||||
{/* Headline across the top ... */}
|
{/* Headline across the top ... */}
|
||||||
|
|||||||
@@ -361,43 +361,13 @@ export default function About({ loaderData }: Route.ComponentProps) {
|
|||||||
<div className="flex flex-wrap items-center gap-md">
|
<div className="flex flex-wrap items-center gap-md">
|
||||||
<Button
|
<Button
|
||||||
as={Link}
|
as={Link}
|
||||||
to={sectionPath("whatWeDo", locale)}
|
to={sectionPath("legacy", locale)}
|
||||||
prefetch="intent"
|
prefetch="intent"
|
||||||
size="lg"
|
size="lg"
|
||||||
iconEnd={<ArrowIcon />}
|
iconEnd={<ArrowIcon />}
|
||||||
>
|
>
|
||||||
{t("actions.learnWhatWeDo")}
|
{copy.legacyLinkLabel}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
{/* The draft marks the legacy page as "[link: …]" with no target
|
|
||||||
yet, so this second button appears only once the CMS gives it
|
|
||||||
a real one. */}
|
|
||||||
{legacy.link ? (
|
|
||||||
legacy.link.isExternal ? (
|
|
||||||
<Button
|
|
||||||
as="a"
|
|
||||||
href={legacy.link.href}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
iconEnd={<ArrowIcon />}
|
|
||||||
>
|
|
||||||
{legacy.link.label}
|
|
||||||
</Button>
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
as={Link}
|
|
||||||
to={legacy.link.href}
|
|
||||||
prefetch="intent"
|
|
||||||
variant="secondary"
|
|
||||||
size="lg"
|
|
||||||
iconEnd={<ArrowIcon />}
|
|
||||||
>
|
|
||||||
{legacy.link.label}
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</PageSection>
|
</PageSection>
|
||||||
|
|||||||
@@ -46,14 +46,12 @@ import { getServerEnv } from "~/lib/env.server";
|
|||||||
import { buildBreadcrumbJsonLd, buildMeta } from "~/lib/seo";
|
import { buildBreadcrumbJsonLd, buildMeta } from "~/lib/seo";
|
||||||
|
|
||||||
import type { Route } from "./+types/contact";
|
import type { Route } from "./+types/contact";
|
||||||
import { Accordion, type AccordionItem } from "./_page-kit/accordion";
|
|
||||||
import type { CmsContactPage } from "./_page-kit/cms-types";
|
import type { CmsContactPage } from "./_page-kit/cms-types";
|
||||||
import { list, loadSingle, toImage } from "./_page-kit/cms.server";
|
import { list, loadSingle, toImage } from "./_page-kit/cms.server";
|
||||||
import { pageCopy } from "./_page-kit/copy";
|
import { pageCopy } from "./_page-kit/copy";
|
||||||
import { routeLocale } from "./_page-kit/locale";
|
import { routeLocale } from "./_page-kit/locale";
|
||||||
import { RichText } from "./_page-kit/rich-text";
|
import { RichText } from "./_page-kit/rich-text";
|
||||||
import {
|
import {
|
||||||
EmptyState,
|
|
||||||
PageHero,
|
PageHero,
|
||||||
PageSection,
|
PageSection,
|
||||||
Prose,
|
Prose,
|
||||||
@@ -74,7 +72,6 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
populate: {
|
populate: {
|
||||||
hero: { populate: { media: true, ctas: true } },
|
hero: { populate: { media: true, ctas: true } },
|
||||||
socialLinks: true,
|
socialLinks: true,
|
||||||
consortiumContacts: true,
|
|
||||||
seo: { populate: { ogImage: true } },
|
seo: { populate: { ogImage: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -95,15 +92,6 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
href: link.href,
|
href: link.href,
|
||||||
isExternal: link.isExternal ?? true,
|
isExternal: link.isExternal ?? true,
|
||||||
})),
|
})),
|
||||||
consortiumContacts: list(page?.consortiumContacts).map((address) => ({
|
|
||||||
id: String(address.id ?? address.organisation),
|
|
||||||
organisation: address.organisation,
|
|
||||||
addressLine: address.addressLine ?? null,
|
|
||||||
city: address.city ?? null,
|
|
||||||
country: address.country ?? null,
|
|
||||||
email: address.email ?? null,
|
|
||||||
phone: address.phone ?? null,
|
|
||||||
})),
|
|
||||||
seo: {
|
seo: {
|
||||||
title: page?.seo?.metaTitle ?? null,
|
title: page?.seo?.metaTitle ?? null,
|
||||||
description: page?.seo?.metaDescription ?? null,
|
description: page?.seo?.metaDescription ?? null,
|
||||||
@@ -362,8 +350,7 @@ export default function Contact({
|
|||||||
loaderData,
|
loaderData,
|
||||||
actionData,
|
actionData,
|
||||||
}: Route.ComponentProps) {
|
}: Route.ComponentProps) {
|
||||||
const { locale, hero, intro, generalEmail, socialLinks, consortiumContacts } =
|
const { locale, hero, intro, generalEmail, socialLinks } = loaderData;
|
||||||
loaderData;
|
|
||||||
const t = createTranslator(locale);
|
const t = createTranslator(locale);
|
||||||
const copy = pageCopy(locale).contact;
|
const copy = pageCopy(locale).contact;
|
||||||
|
|
||||||
@@ -390,32 +377,6 @@ export default function Contact({
|
|||||||
(message): message is string => typeof message === "string",
|
(message): message is string => typeof message === "string",
|
||||||
);
|
);
|
||||||
|
|
||||||
/* The draft draws the consortium addresses as a disclosure list, which the
|
|
||||||
approved draft also calls for ("optional accordion"). The answers are
|
|
||||||
Markdown because that is what <Accordion /> renders; every value in them
|
|
||||||
comes from the CMS, and the whole list is replaced by the localised empty
|
|
||||||
state while the addresses are still "TBD". */
|
|
||||||
const consortiumItems: readonly AccordionItem[] = consortiumContacts.map(
|
|
||||||
(contact) => {
|
|
||||||
const place = [contact.city, contact.country].filter(Boolean).join(", ");
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: contact.id,
|
|
||||||
question: contact.organisation,
|
|
||||||
answer: [
|
|
||||||
contact.addressLine,
|
|
||||||
place === "" ? null : place,
|
|
||||||
contact.email ? `[${contact.email}](mailto:${contact.email})` : null,
|
|
||||||
contact.phone
|
|
||||||
? `[${contact.phone}](tel:${contact.phone.replace(/\s+/g, "")})`
|
|
||||||
: null,
|
|
||||||
]
|
|
||||||
.filter((line): line is string => typeof line === "string")
|
|
||||||
.join("\n\n"),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
/* A failed submit re-renders the page with the summary at the top of the
|
/* A failed submit re-renders the page with the summary at the top of the
|
||||||
form. Screen-reader users are left wherever they were, and keyboard users
|
form. Screen-reader users are left wherever they were, and keyboard users
|
||||||
below the fold, so focus is moved onto the summary itself - it is
|
below the fold, so focus is moved onto the summary itself - it is
|
||||||
@@ -664,24 +625,6 @@ export default function Contact({
|
|||||||
</dl>
|
</dl>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* CONSORTIUM CONTACTS - "TBD" in the draft, so the CMS owns them.
|
|
||||||
The draft labels the list with an accent eyebrow rather than a
|
|
||||||
section heading, so the heading takes the label role. */}
|
|
||||||
<div className="sh-reveal flex flex-col gap-sm">
|
|
||||||
<h2
|
|
||||||
id="contact-consortium-heading"
|
|
||||||
className="label-md uppercase text-[var(--text-accent)]"
|
|
||||||
>
|
|
||||||
{copy.consortiumHeading}
|
|
||||||
</h2>
|
|
||||||
|
|
||||||
{consortiumItems.length > 0 ? (
|
|
||||||
<Accordion items={consortiumItems} defaultOpenIndex={null} />
|
|
||||||
) : (
|
|
||||||
<EmptyState message={copy.consortiumEmpty} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* EU VISIBILITY - the funding note the draft prints at the foot of
|
{/* EU VISIBILITY - the funding note the draft prints at the foot of
|
||||||
this column. The emblem and the statement come from the shared
|
this column. The emblem and the statement come from the shared
|
||||||
badge, so the wording and the artwork stay contractual. */}
|
badge, so the wording and the artwork stay contractual. */}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
alternating the two dark surfaces the way the draft alternates #0B0B0A and
|
alternating the two dark surfaces the way the draft alternates #0B0B0A and
|
||||||
#1D1D1B:
|
#1D1D1B:
|
||||||
|
|
||||||
Hero EU badge, an optional status pill, the headline, the lead,
|
Hero EU badge, the headline, the lead,
|
||||||
the CTA pair, the wide picture and the brand stripe. Two
|
the CTA pair, the wide picture and the brand stripe. Two
|
||||||
slow washes drift behind it.
|
slow washes drift behind it.
|
||||||
Approach "A nationwide project, delivered locally": what the project
|
Approach "A nationwide project, delivered locally": what the project
|
||||||
@@ -73,6 +73,7 @@ import {
|
|||||||
isImageGalleryBlock,
|
isImageGalleryBlock,
|
||||||
type CmsFaq,
|
type CmsFaq,
|
||||||
type CmsHomePage,
|
type CmsHomePage,
|
||||||
|
type CmsLegacyPage,
|
||||||
type CmsNewsArticle,
|
type CmsNewsArticle,
|
||||||
type CmsPartner,
|
type CmsPartner,
|
||||||
} from "./_page-kit/cms-types";
|
} from "./_page-kit/cms-types";
|
||||||
@@ -86,6 +87,7 @@ import {
|
|||||||
type ResolvedPartner,
|
type ResolvedPartner,
|
||||||
} from "./_page-kit/cms.server";
|
} from "./_page-kit/cms.server";
|
||||||
import { pageCopy } from "./_page-kit/copy";
|
import { pageCopy } from "./_page-kit/copy";
|
||||||
|
import { resolveLegacyStage } from "./_page-kit/legacy-cms.server";
|
||||||
import { routeLocale } from "./_page-kit/locale";
|
import { routeLocale } from "./_page-kit/locale";
|
||||||
import {
|
import {
|
||||||
cx,
|
cx,
|
||||||
@@ -181,6 +183,13 @@ function toNewsCard(article: CmsNewsArticle, locale: Locale): NewsCard {
|
|||||||
|
|
||||||
export async function loader({ params }: Route.LoaderArgs) {
|
export async function loader({ params }: Route.LoaderArgs) {
|
||||||
const locale = routeLocale(params);
|
const locale = routeLocale(params);
|
||||||
|
const legacyPagePromise = loadSingle<CmsLegacyPage>("legacy-page", {
|
||||||
|
locale,
|
||||||
|
populate: {
|
||||||
|
stageOne: { populate: { metrics: true, focusAreas: true } },
|
||||||
|
stageTwo: { populate: { metrics: true, focusAreas: true } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const page = await loadSingle<CmsHomePage>("home-page", {
|
const page = await loadSingle<CmsHomePage>("home-page", {
|
||||||
locale,
|
locale,
|
||||||
@@ -194,6 +203,8 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
seo: { populate: { ogImage: true } },
|
seo: { populate: { ogImage: true } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const legacyPage = await legacyPagePromise;
|
||||||
|
const legacyFallback = pageCopy(locale).legacy;
|
||||||
|
|
||||||
/* The curated relation wins; otherwise show every partner the CMS knows, so
|
/* The curated relation wins; otherwise show every partner the CMS knows, so
|
||||||
the wall fills in as soon as partners exist - before anyone remembers to
|
the wall fills in as soon as partners exist - before anyone remembers to
|
||||||
@@ -243,8 +254,15 @@ export async function loader({ params }: Route.LoaderArgs) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
locale,
|
locale,
|
||||||
|
legacy: {
|
||||||
|
homeEyebrow: legacyPage?.homeEyebrow ?? legacyFallback.homeEyebrow,
|
||||||
|
homeHeading: legacyPage?.homeHeading ?? legacyFallback.homeHeading,
|
||||||
|
homeLead: legacyPage?.homeLead ?? legacyFallback.homeLead,
|
||||||
|
homeLinkLabel: legacyFallback.homeLinkLabel,
|
||||||
|
stageOne: resolveLegacyStage(legacyPage?.stageOne, legacyFallback.stageOne),
|
||||||
|
stageTwo: resolveLegacyStage(legacyPage?.stageTwo, legacyFallback.stageTwo),
|
||||||
|
},
|
||||||
hero: {
|
hero: {
|
||||||
eyebrow: page?.hero?.eyebrow ?? null,
|
|
||||||
headline: page?.hero?.headline ?? null,
|
headline: page?.hero?.headline ?? null,
|
||||||
subheading: page?.hero?.subheading ?? null,
|
subheading: page?.hero?.subheading ?? null,
|
||||||
media: toImage(page?.hero?.media, "large") ?? null,
|
media: toImage(page?.hero?.media, "large") ?? null,
|
||||||
@@ -334,24 +352,6 @@ function HeroCta({ href, label, variant }: HeroCtaProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* The hero's status line. The draft hard-codes "Now live · Expression of
|
|
||||||
* Interest open"; here the words are the CMS hero eyebrow, so the pill only
|
|
||||||
* ever announces a state an editor has actually written - and the hero simply
|
|
||||||
* has no pill until then. The dot pulses; the text does not move.
|
|
||||||
*/
|
|
||||||
function StatusPill({ label }: { label: string }) {
|
|
||||||
return (
|
|
||||||
<span className="inline-flex items-center gap-xs rounded-pill border border-[var(--border-subtle)] bg-[var(--surface-raised)] py-2xs ps-sm pe-md">
|
|
||||||
<span
|
|
||||||
aria-hidden="true"
|
|
||||||
className="sh-pulse size-[7px] shrink-0 rounded-pill bg-[var(--surface-accent)]"
|
|
||||||
/>
|
|
||||||
<span className="label-md uppercase text-[var(--text-secondary)]">{label}</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The approach band's pictures. The CMS alt text doubles as the visible
|
* The approach band's pictures. The CMS alt text doubles as the visible
|
||||||
* caption - so the picture is described once, in the caption, and the <img>
|
* caption - so the picture is described once, in the caption, and the <img>
|
||||||
@@ -546,6 +546,7 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
const { locale, hero, approach, gallery, partners, news, faqs } = loaderData;
|
const { locale, hero, approach, gallery, partners, news, faqs } = loaderData;
|
||||||
const t = createTranslator(locale);
|
const t = createTranslator(locale);
|
||||||
const copy = pageCopy(locale).home;
|
const copy = pageCopy(locale).home;
|
||||||
|
const legacyCopy = loaderData.legacy;
|
||||||
/* The hero badge sits on the page's own surface, so it follows the theme
|
/* The hero badge sits on the page's own surface, so it follows the theme
|
||||||
exactly as the footer's badge does: same root cookie, same emblem. */
|
exactly as the footer's badge does: same root cookie, same emblem. */
|
||||||
const root = useRouteLoaderData<{ theme?: AppTheme }>("root");
|
const root = useRouteLoaderData<{ theme?: AppTheme }>("root");
|
||||||
@@ -603,7 +604,6 @@ export default function Home({ loaderData }: Route.ComponentProps) {
|
|||||||
title={headline}
|
title={headline}
|
||||||
lead={""}
|
lead={""}
|
||||||
leadWithActions
|
leadWithActions
|
||||||
{...(hero.eyebrow ? { statusPill: <StatusPill label={hero.eyebrow} /> } : {})}
|
|
||||||
ambient={[
|
ambient={[
|
||||||
{ top: -220, from: "start", offset: "8%", width: 780, height: 620, durationSeconds: 18 },
|
{ top: -220, from: "start", offset: "8%", width: 780, height: 620, durationSeconds: 18 },
|
||||||
{
|
{
|
||||||
@@ -641,6 +641,80 @@ reveal
|
|||||||
))}
|
))}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* THE TWO EARLIER SHARP PHASES */}
|
||||||
|
<PageSection
|
||||||
|
aria-labelledby="home-legacy-heading"
|
||||||
|
className="bg-[var(--surface-sunken)]"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2xl">
|
||||||
|
<div className="sh-reveal flex flex-col gap-md border-b border-[var(--border-default)] pb-lg md:max-w-[52rem]">
|
||||||
|
<p className="label-md uppercase text-[var(--text-accent)]">
|
||||||
|
{legacyCopy.homeEyebrow}
|
||||||
|
</p>
|
||||||
|
<SectionHeader
|
||||||
|
id="home-legacy-heading"
|
||||||
|
title={legacyCopy.homeHeading}
|
||||||
|
lead={legacyCopy.homeLead}
|
||||||
|
size="display"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-lg lg:grid-cols-2">
|
||||||
|
{[
|
||||||
|
{
|
||||||
|
stage: legacyCopy.stageOne,
|
||||||
|
anchor: "sharp-i",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
stage: legacyCopy.stageTwo,
|
||||||
|
anchor: "sharp-ii",
|
||||||
|
},
|
||||||
|
].map(({ stage, anchor }) => (
|
||||||
|
<article
|
||||||
|
key={stage.name}
|
||||||
|
className="sh-lift flex h-full flex-col overflow-hidden rounded-card border border-[var(--border-subtle)] bg-[var(--surface-card)]"
|
||||||
|
>
|
||||||
|
<div className="flex flex-1 flex-col gap-lg p-lg md:p-xl">
|
||||||
|
<div className="flex flex-wrap items-baseline justify-between gap-sm">
|
||||||
|
<h3 className="heading-h2 text-[var(--text-primary)]">
|
||||||
|
{stage.name}
|
||||||
|
</h3>
|
||||||
|
<span className="label-md text-[var(--text-accent)]">
|
||||||
|
{stage.years}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="body-md text-[var(--text-secondary)]">
|
||||||
|
{stage.summary}
|
||||||
|
</p>
|
||||||
|
<dl className="grid grid-cols-3 gap-md border-y border-[var(--border-subtle)] py-md">
|
||||||
|
{stage.metrics.slice(0, 3).map((metric) => (
|
||||||
|
<div key={metric.label} className="flex flex-col gap-2xs">
|
||||||
|
<dt className="label-md text-[var(--text-secondary)]">
|
||||||
|
{metric.label}
|
||||||
|
</dt>
|
||||||
|
<dd className="heading-h2 text-[var(--text-accent)]">
|
||||||
|
{metric.value}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
to={`${sectionPath("legacy", locale)}#${anchor}`}
|
||||||
|
prefetch="intent"
|
||||||
|
variant="ghost"
|
||||||
|
className="self-start"
|
||||||
|
iconEnd={<ArrowIcon />}
|
||||||
|
>
|
||||||
|
{legacyCopy.homeLinkLabel}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</PageSection>
|
||||||
|
|
||||||
{/* WHAT THE PROJECT IS, AND WHO DELIVERS IT */}
|
{/* WHAT THE PROJECT IS, AND WHO DELIVERS IT */}
|
||||||
{/* `bleed` drops the 80rem Container so the band's content - the three
|
{/* `bleed` drops the 80rem Container so the band's content - the three
|
||||||
pictures especially - runs the full width of the viewport, the way
|
pictures especially - runs the full width of the viewport, the way
|
||||||
|
|||||||
@@ -0,0 +1,458 @@
|
|||||||
|
/* ==========================================================================
|
||||||
|
SHARP LEGACY - /en/legacy and /ar/legacy
|
||||||
|
--------------------------------------------------------------------------
|
||||||
|
The full programme record stays available from the CMS. The page introduces
|
||||||
|
each phase with a short story, its own gallery, and three highlights.
|
||||||
|
========================================================================== */
|
||||||
|
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { Link } from "react-router";
|
||||||
|
|
||||||
|
import { ArrowIcon, Button } from "~/components/ui/Button";
|
||||||
|
import { createTranslator, sectionPath, type Translator } from "~/i18n";
|
||||||
|
import { buildBreadcrumbJsonLd, buildMeta } from "~/lib/seo";
|
||||||
|
|
||||||
|
import type { Route } from "./+types/legacy";
|
||||||
|
import type { CmsLegacyPage } from "./_page-kit/cms-types";
|
||||||
|
import { loadSingle, toImage } from "./_page-kit/cms.server";
|
||||||
|
import type { LegacyGalleryImage, LegacyStageCopy } from "./_page-kit/copy";
|
||||||
|
import { pageCopy } from "./_page-kit/copy";
|
||||||
|
import { resolveLegacyStage } from "./_page-kit/legacy-cms.server";
|
||||||
|
import { routeLocale } from "./_page-kit/locale";
|
||||||
|
import { cx, PageHero, PageSection, SectionHeader } from "./_page-kit/sections";
|
||||||
|
import { RichText } from "./_page-kit/rich-text";
|
||||||
|
|
||||||
|
import "./_page-kit/motion.css";
|
||||||
|
|
||||||
|
const STAGES = [
|
||||||
|
{
|
||||||
|
id: "sharp-i",
|
||||||
|
band: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "sharp-ii",
|
||||||
|
band: false,
|
||||||
|
},
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
/** Keep the short page narrative in step with the editor's CMS description. */
|
||||||
|
function storyPreview(markdown: string) {
|
||||||
|
const [introduction = "", ...rawChapters] = markdown
|
||||||
|
.trim()
|
||||||
|
.replace(/\r\n?/g, "\n")
|
||||||
|
.split(/\n(?=###\s)/);
|
||||||
|
const chapters = rawChapters.flatMap((raw) => {
|
||||||
|
const match = /^###\s+([^\n]+)\n([\s\S]*)$/.exec(raw.trim());
|
||||||
|
return match
|
||||||
|
? [{ heading: match[1]!, body: match[2]!.trim() }]
|
||||||
|
: [];
|
||||||
|
});
|
||||||
|
const featuredIndexes = new Set(
|
||||||
|
chapters.length > 3 ? [1, 2, chapters.length - 1] : chapters.map((_, index) => index),
|
||||||
|
);
|
||||||
|
const firstSentences = (value: string, count: number) =>
|
||||||
|
value.trim().split(/(?<=[.!?؟])\s+/u).slice(0, count).join(" ");
|
||||||
|
|
||||||
|
return {
|
||||||
|
introduction: firstSentences(introduction, 2),
|
||||||
|
highlights: chapters
|
||||||
|
.filter((_, index) => featuredIndexes.has(index))
|
||||||
|
.map(({ heading, body }) => ({
|
||||||
|
heading,
|
||||||
|
preview: firstSentences(body.split(/\n\s*\n/)[0] ?? body, 2),
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loader({ params }: Route.LoaderArgs) {
|
||||||
|
const locale = routeLocale(params);
|
||||||
|
|
||||||
|
return loadSingle<CmsLegacyPage>("legacy-page", {
|
||||||
|
locale,
|
||||||
|
populate: {
|
||||||
|
hero: { populate: { media: true, ctas: true } },
|
||||||
|
stageOne: { populate: { metrics: true, focusAreas: true, images: true } },
|
||||||
|
stageTwo: { populate: { metrics: true, focusAreas: true, images: true } },
|
||||||
|
seo: { populate: { ogImage: true } },
|
||||||
|
},
|
||||||
|
}).then((page) => {
|
||||||
|
const fallback = pageCopy(locale).legacy;
|
||||||
|
return {
|
||||||
|
locale,
|
||||||
|
hero: {
|
||||||
|
eyebrow: page?.hero?.eyebrow ?? null,
|
||||||
|
title: page?.hero?.headline ?? null,
|
||||||
|
lead: page?.hero?.subheading ?? null,
|
||||||
|
media: toImage(page?.hero?.media, "large") ?? null,
|
||||||
|
},
|
||||||
|
stageOne: resolveLegacyStage(page?.stageOne, fallback.stageOne),
|
||||||
|
stageTwo: resolveLegacyStage(page?.stageTwo, fallback.stageTwo),
|
||||||
|
closingHeading: page?.closingHeading ?? null,
|
||||||
|
closingBody: page?.closingBody ?? null,
|
||||||
|
seo: {
|
||||||
|
title: page?.seo?.metaTitle ?? null,
|
||||||
|
description: page?.seo?.metaDescription ?? null,
|
||||||
|
image: toImage(page?.seo?.ogImage, "large") ?? null,
|
||||||
|
noIndex: page?.seo?.noIndex ?? false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function meta({ loaderData, matches }: Route.MetaArgs) {
|
||||||
|
const root = matches[0].loaderData;
|
||||||
|
const locale = loaderData?.locale ?? root.locale;
|
||||||
|
const t = createTranslator(locale);
|
||||||
|
const copy = pageCopy(locale).legacy;
|
||||||
|
const seo = loaderData?.seo;
|
||||||
|
|
||||||
|
return buildMeta({
|
||||||
|
locale,
|
||||||
|
pathname: root.pathname,
|
||||||
|
siteUrl: root.siteUrl,
|
||||||
|
title: seo?.title ?? t("nav.sections.legacy"),
|
||||||
|
description: seo?.description ?? loaderData?.hero.lead ?? copy.heroLead,
|
||||||
|
...(seo?.image
|
||||||
|
? { image: { url: seo.image.src, alt: seo.image.alt } }
|
||||||
|
: {}),
|
||||||
|
noIndex: seo?.noIndex ?? false,
|
||||||
|
jsonLd: buildBreadcrumbJsonLd(root.siteUrl, [
|
||||||
|
{ name: t("nav.sections.home"), pathname: sectionPath("home", locale) },
|
||||||
|
{ name: t("nav.sections.about"), pathname: sectionPath("about", locale) },
|
||||||
|
{ name: t("nav.sections.legacy"), pathname: sectionPath("legacy", locale) },
|
||||||
|
]),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function LegacyImageGallery({
|
||||||
|
images,
|
||||||
|
stageName,
|
||||||
|
heading,
|
||||||
|
empty,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
images: readonly LegacyGalleryImage[];
|
||||||
|
stageName: string;
|
||||||
|
heading: string;
|
||||||
|
empty: string;
|
||||||
|
t: Translator;
|
||||||
|
}) {
|
||||||
|
const trackRef = useRef<HTMLUListElement>(null);
|
||||||
|
const [activeIndex, setActiveIndex] = useState(0);
|
||||||
|
|
||||||
|
const scrollToImage = (index: number) => {
|
||||||
|
trackRef.current?.children.item(index)?.scrollIntoView({
|
||||||
|
behavior: "auto",
|
||||||
|
block: "nearest",
|
||||||
|
inline: "start",
|
||||||
|
});
|
||||||
|
setActiveIndex(index);
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateActiveImage = () => {
|
||||||
|
const track = trackRef.current;
|
||||||
|
if (!track) return;
|
||||||
|
|
||||||
|
const bounds = track.getBoundingClientRect();
|
||||||
|
const center = (bounds.left + bounds.right) / 2;
|
||||||
|
let closestIndex = 0;
|
||||||
|
let closestDistance = Number.POSITIVE_INFINITY;
|
||||||
|
Array.from(track.children).forEach((slide, index) => {
|
||||||
|
const rect = slide.getBoundingClientRect();
|
||||||
|
const distance = Math.abs((rect.left + rect.right) / 2 - center);
|
||||||
|
if (distance < closestDistance) {
|
||||||
|
closestIndex = index;
|
||||||
|
closestDistance = distance;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setActiveIndex((current) => current === closestIndex ? current : closestIndex);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (images.length === 0) {
|
||||||
|
return (
|
||||||
|
<div className="relative flex aspect-[4/3] flex-col justify-end overflow-hidden rounded-card border border-[var(--border-subtle)] bg-[var(--surface-card)] p-xl">
|
||||||
|
<span aria-hidden="true" className="absolute -end-md -top-xl display-sm text-[var(--text-accent)] opacity-10">
|
||||||
|
{stageName}
|
||||||
|
</span>
|
||||||
|
<p className="relative max-w-[20rem] body-md text-[var(--text-secondary)]">
|
||||||
|
{empty}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (images.length === 1) {
|
||||||
|
const image = images[0]!;
|
||||||
|
return (
|
||||||
|
<figure className="flex flex-col gap-sm">
|
||||||
|
<img
|
||||||
|
src={image.src}
|
||||||
|
alt={image.alt}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
className="aspect-[4/3] w-full rounded-card border border-[var(--border-subtle)] object-cover shadow-[var(--elevation-card)]"
|
||||||
|
/>
|
||||||
|
{image.caption ? (
|
||||||
|
<figcaption className="body-sm text-[var(--text-secondary)]">
|
||||||
|
{image.caption}
|
||||||
|
</figcaption>
|
||||||
|
) : null}
|
||||||
|
</figure>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div role="region" aria-label={heading} className="flex min-w-0 flex-col gap-sm">
|
||||||
|
<ul
|
||||||
|
ref={trackRef}
|
||||||
|
tabIndex={0}
|
||||||
|
onScroll={updateActiveImage}
|
||||||
|
className="flex snap-x snap-mandatory overflow-x-auto overscroll-x-contain rounded-card motion-safe:scroll-smooth focus-visible:outline-[color:var(--focus-ring-color)] focus-visible:outline-[length:var(--focus-ring-width)] focus-visible:outline-offset-[var(--focus-ring-offset)]"
|
||||||
|
>
|
||||||
|
{images.map((image, index) => (
|
||||||
|
<li key={`${image.src}-${index}`} className="w-full shrink-0 snap-start">
|
||||||
|
<figure className="flex flex-col gap-sm">
|
||||||
|
<img
|
||||||
|
src={image.src}
|
||||||
|
alt={image.alt}
|
||||||
|
loading="lazy"
|
||||||
|
decoding="async"
|
||||||
|
className="aspect-[4/3] w-full rounded-card border border-[var(--border-subtle)] object-cover shadow-[var(--elevation-card)]"
|
||||||
|
/>
|
||||||
|
{image.caption ? (
|
||||||
|
<figcaption className="body-sm text-[var(--text-secondary)]">
|
||||||
|
{image.caption}
|
||||||
|
</figcaption>
|
||||||
|
) : null}
|
||||||
|
</figure>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex items-center justify-between gap-sm">
|
||||||
|
<p aria-live="polite" className="label-md text-[var(--text-secondary)]">
|
||||||
|
{t("legacy.imageCount", { current: activeIndex + 1, total: images.length })}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center gap-xs">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t("legacy.previousImage", { programme: stageName })}
|
||||||
|
disabled={activeIndex === 0}
|
||||||
|
onClick={() => scrollToImage(activeIndex - 1)}
|
||||||
|
className="flex size-11 items-center justify-center rounded-control border border-[var(--border-default)] text-[var(--text-primary)] transition-colors hover:border-[var(--border-accent)] hover:text-[var(--text-accent)] disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-[color:var(--focus-ring-color)] focus-visible:outline-[length:var(--focus-ring-width)] focus-visible:outline-offset-[var(--focus-ring-offset)]"
|
||||||
|
>
|
||||||
|
<ArrowIcon direction="backward" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label={t("legacy.nextImage", { programme: stageName })}
|
||||||
|
disabled={activeIndex === images.length - 1}
|
||||||
|
onClick={() => scrollToImage(activeIndex + 1)}
|
||||||
|
className="flex size-11 items-center justify-center rounded-control border border-[var(--border-default)] text-[var(--text-primary)] transition-colors hover:border-[var(--border-accent)] hover:text-[var(--text-accent)] disabled:cursor-not-allowed disabled:opacity-40 focus-visible:outline-[color:var(--focus-ring-color)] focus-visible:outline-[length:var(--focus-ring-width)] focus-visible:outline-offset-[var(--focus-ring-offset)]"
|
||||||
|
>
|
||||||
|
<ArrowIcon direction="forward" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div aria-hidden="true" className="flex gap-xs">
|
||||||
|
{images.map((image, index) => (
|
||||||
|
<span
|
||||||
|
key={`${image.src}-${index}`}
|
||||||
|
className={cx(
|
||||||
|
"h-1 flex-1 rounded-chip transition-colors",
|
||||||
|
index === activeIndex ? "bg-[var(--border-accent)]" : "bg-[var(--border-subtle)]",
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StageSection({
|
||||||
|
stage,
|
||||||
|
id,
|
||||||
|
band,
|
||||||
|
t,
|
||||||
|
}: {
|
||||||
|
stage: LegacyStageCopy;
|
||||||
|
id: string;
|
||||||
|
band: boolean;
|
||||||
|
t: Translator;
|
||||||
|
}) {
|
||||||
|
const story = storyPreview(stage.description);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageSection
|
||||||
|
id={id}
|
||||||
|
aria-labelledby={`${id}-heading`}
|
||||||
|
{...(band ? { band: true } : {})}
|
||||||
|
>
|
||||||
|
<div className="flex flex-col gap-2xl">
|
||||||
|
<div className="grid items-center gap-xl lg:grid-cols-[minmax(0,0.88fr)_minmax(0,1.12fr)] lg:gap-2xl">
|
||||||
|
<div className="flex min-w-0 flex-col gap-lg">
|
||||||
|
<SectionHeader
|
||||||
|
id={`${id}-heading`}
|
||||||
|
eyebrow={stage.years}
|
||||||
|
title={stage.name}
|
||||||
|
lead={stage.summary}
|
||||||
|
size="display"
|
||||||
|
/>
|
||||||
|
{story.introduction ? (
|
||||||
|
<p className="body-lg text-[var(--text-secondary)]">
|
||||||
|
{story.introduction}
|
||||||
|
</p>
|
||||||
|
) : null}
|
||||||
|
<dl className="grid grid-cols-3 gap-sm border-t border-[var(--border-default)] pt-lg">
|
||||||
|
{stage.metrics.slice(0, 3).map((metric) => (
|
||||||
|
<div key={metric.label} className="flex min-w-0 flex-col gap-xs">
|
||||||
|
<dt className="order-2 label-md text-[var(--text-secondary)]">
|
||||||
|
{metric.label}
|
||||||
|
</dt>
|
||||||
|
<dd className="order-1 heading-h2 text-[var(--text-accent)]">
|
||||||
|
{metric.value}
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className={cx("flex min-w-0 flex-col gap-sm", id === "sharp-ii" && "lg:order-first")}>
|
||||||
|
<h3 className="label-md uppercase text-[var(--text-accent)]">
|
||||||
|
{stage.imageHeading}
|
||||||
|
</h3>
|
||||||
|
<LegacyImageGallery
|
||||||
|
images={stage.images}
|
||||||
|
stageName={stage.name}
|
||||||
|
heading={stage.imageHeading}
|
||||||
|
empty={stage.imageEmpty}
|
||||||
|
t={t}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{story.highlights.length > 0 ? (
|
||||||
|
<div className="grid gap-md md:grid-cols-3">
|
||||||
|
{story.highlights.map((highlight) => (
|
||||||
|
<article
|
||||||
|
key={highlight.heading}
|
||||||
|
className="flex flex-col gap-sm border-s-2 border-[var(--border-accent)] bg-[var(--surface-card)] p-lg"
|
||||||
|
>
|
||||||
|
<h3 className="heading-h3 text-[var(--text-primary)]">
|
||||||
|
{highlight.heading}
|
||||||
|
</h3>
|
||||||
|
<p className="body-md text-[var(--text-secondary)]">
|
||||||
|
{highlight.preview}
|
||||||
|
</p>
|
||||||
|
</article>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
|
||||||
|
<details className="group rounded-card border border-[var(--border-subtle)] bg-[var(--surface-card)]">
|
||||||
|
<summary className="flex cursor-pointer list-none items-center justify-between gap-md p-lg body-md-bold text-[var(--text-primary)] focus-visible:outline-[color:var(--focus-ring-color)] focus-visible:outline-[length:var(--focus-ring-width)] focus-visible:outline-offset-[var(--focus-ring-offset)] [&::-webkit-details-marker]:hidden">
|
||||||
|
{t("legacy.fullRecord", { programme: stage.name })}
|
||||||
|
<span aria-hidden="true" className="heading-h3 text-[var(--text-accent)] group-open:rotate-45">
|
||||||
|
+
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
|
<div className="flex flex-col gap-xl border-t border-[var(--border-subtle)] px-lg py-xl">
|
||||||
|
<RichText markdown={stage.description} className="w-full" />
|
||||||
|
{stage.focusAreas.length > 0 ? (
|
||||||
|
<div className="flex flex-col gap-sm">
|
||||||
|
<h3 className="heading-h3 text-[var(--text-primary)]">
|
||||||
|
{stage.focusHeading}
|
||||||
|
</h3>
|
||||||
|
<ul className="flex flex-wrap gap-sm">
|
||||||
|
{stage.focusAreas.map((area) => (
|
||||||
|
<li
|
||||||
|
key={area}
|
||||||
|
className="rounded-chip border border-[var(--border-subtle)] px-md py-xs body-sm text-[var(--text-secondary)]"
|
||||||
|
>
|
||||||
|
{area}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</PageSection>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Legacy({ loaderData }: Route.ComponentProps) {
|
||||||
|
const locale = loaderData.locale;
|
||||||
|
const t = createTranslator(locale);
|
||||||
|
const copy = pageCopy(locale).legacy;
|
||||||
|
const stageEntries = [
|
||||||
|
{ ...STAGES[0], stage: loaderData.stageOne },
|
||||||
|
{ ...STAGES[1], stage: loaderData.stageTwo },
|
||||||
|
] as const;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<PageHero
|
||||||
|
eyebrow={loaderData.hero.eyebrow ?? copy.heroEyebrow}
|
||||||
|
title={loaderData.hero.title ?? copy.heroTitle}
|
||||||
|
lead={loaderData.hero.lead ?? copy.heroLead}
|
||||||
|
{...(loaderData.hero.media ? { media: loaderData.hero.media } : {})}
|
||||||
|
ambient={[
|
||||||
|
{
|
||||||
|
top: -150,
|
||||||
|
from: "start",
|
||||||
|
offset: "5%",
|
||||||
|
width: 560,
|
||||||
|
height: 470,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
top: 100,
|
||||||
|
from: "end",
|
||||||
|
offset: "-80px",
|
||||||
|
width: 460,
|
||||||
|
height: 420,
|
||||||
|
tone: "danger",
|
||||||
|
reverse: true,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{stageEntries.map(({ stage, ...config }) => (
|
||||||
|
<StageSection
|
||||||
|
key={`${config.id}-${locale}`}
|
||||||
|
stage={stage}
|
||||||
|
t={t}
|
||||||
|
{...config}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<PageSection
|
||||||
|
aria-labelledby="legacy-closing-heading"
|
||||||
|
className="bg-[var(--surface-sunken)]"
|
||||||
|
>
|
||||||
|
<div className="mx-auto flex max-w-[var(--layout-prose-max)] flex-col items-start gap-lg">
|
||||||
|
<SectionHeader
|
||||||
|
id="legacy-closing-heading"
|
||||||
|
title={loaderData.closingHeading ?? copy.closingHeading}
|
||||||
|
/>
|
||||||
|
<p className="body-lg text-[var(--text-secondary)]">
|
||||||
|
{(loaderData.closingBody ?? copy.closingParagraphs.join(" "))
|
||||||
|
.trim()
|
||||||
|
.split(/(?<=[.!?؟])\s+/u)
|
||||||
|
.slice(0, 2)
|
||||||
|
.join(" ")}
|
||||||
|
</p>
|
||||||
|
<Button
|
||||||
|
as={Link}
|
||||||
|
to={sectionPath("about", locale)}
|
||||||
|
prefetch="intent"
|
||||||
|
variant="primary"
|
||||||
|
iconEnd={<ArrowIcon />}
|
||||||
|
>
|
||||||
|
{t("actions.discoverSharper")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</PageSection>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user