From 20b384039cbe884178fec99aa7f6ca4bf532d81b Mon Sep 17 00:00:00 2001 From: Hisham Aburaqibah Date: Fri, 11 Sep 2026 02:46:56 +0200 Subject: [PATCH] initial commit --- .dockerignore | 42 + .editorconfig | 35 + .env.example | 114 + .gitignore | 81 + .npmrc | 3 + .nvmrc | 1 + CLAUDE.md | 130 + README.md | 906 + apps/cms/.env.example | 156 + apps/cms/.gitignore | 131 + apps/cms/Dockerfile | 86 + apps/cms/README.md | 61 + apps/cms/config/admin.ts | 34 + apps/cms/config/api.ts | 16 + apps/cms/config/database.ts | 72 + apps/cms/config/middlewares.ts | 63 + apps/cms/config/plugins.ts | 143 + apps/cms/config/server.ts | 30 + apps/cms/database/migrations/.gitkeep | 0 apps/cms/favicon.png | Bin 0 -> 497 bytes apps/cms/package.json | 43 + apps/cms/public/robots.txt | 3 + apps/cms/public/uploads/.gitkeep | 0 apps/cms/src/admin/app.example.tsx | 37 + apps/cms/src/admin/tsconfig.json | 20 + apps/cms/src/admin/vite.config.example.ts | 12 + apps/cms/src/api/.gitkeep | 0 .../content-types/about-page/schema.json | 113 + .../api/about-page/controllers/about-page.ts | 7 + .../src/api/about-page/routes/about-page.ts | 7 + .../src/api/about-page/services/about-page.ts | 7 + .../content-types/activity/schema.json | 159 + .../src/api/activity/controllers/activity.ts | 7 + apps/cms/src/api/activity/routes/activity.ts | 7 + .../cms/src/api/activity/services/activity.ts | 7 + .../content-types/application/lifecycles.ts | 24 + .../content-types/application/schema.json | 70 + .../application/controllers/application.ts | 7 + .../middlewares/sanitize-submission.ts | 56 + .../src/api/application/routes/application.ts | 31 + .../api/application/services/application.ts | 7 + .../content-types/contact-page/schema.json | 84 + .../contact-page/controllers/contact-page.ts | 7 + .../api/contact-page/routes/contact-page.ts | 7 + .../api/contact-page/services/contact-page.ts | 7 + .../contact-submission/lifecycles.ts | 24 + .../contact-submission/schema.json | 50 + .../controllers/contact-submission.ts | 7 + .../middlewares/sanitize-submission.ts | 38 + .../routes/contact-submission.ts | 31 + .../services/contact-submission.ts | 7 + .../event-registration/lifecycles.ts | 24 + .../event-registration/schema.json | 64 + .../controllers/event-registration.ts | 7 + .../middlewares/sanitize-submission.ts | 180 + .../routes/event-registration.ts | 31 + .../services/event-registration.ts | 7 + .../api/event/content-types/event/schema.json | 239 + apps/cms/src/api/event/controllers/event.ts | 7 + apps/cms/src/api/event/routes/event.ts | 7 + apps/cms/src/api/event/services/event.ts | 7 + .../content-types/home-page/schema.json | 92 + .../api/home-page/controllers/home-page.ts | 7 + .../cms/src/api/home-page/routes/home-page.ts | 7 + .../src/api/home-page/services/home-page.ts | 7 + .../content-types/knowledge-item/schema.json | 166 + .../controllers/knowledge-item.ts | 7 + .../knowledge-item/routes/knowledge-item.ts | 7 + .../knowledge-item/services/knowledge-item.ts | 7 + .../content-types/news-article/schema.json | 159 + .../news-article/controllers/news-article.ts | 7 + .../api/news-article/routes/news-article.ts | 7 + .../api/news-article/services/news-article.ts | 7 + .../content-types/opportunity/schema.json | 176 + .../opportunity/controllers/opportunity.ts | 7 + .../src/api/opportunity/routes/opportunity.ts | 7 + .../api/opportunity/services/opportunity.ts | 7 + .../partner/content-types/partner/schema.json | 106 + .../src/api/partner/controllers/partner.ts | 7 + apps/cms/src/api/partner/routes/partner.ts | 7 + apps/cms/src/api/partner/services/partner.ts | 7 + .../region/content-types/region/schema.json | 40 + apps/cms/src/api/region/controllers/region.ts | 7 + apps/cms/src/api/region/routes/region.ts | 7 + apps/cms/src/api/region/services/region.ts | 7 + .../content-types/resource/schema.json | 214 + .../src/api/resource/controllers/resource.ts | 7 + apps/cms/src/api/resource/routes/resource.ts | 7 + .../cms/src/api/resource/services/resource.ts | 7 + .../content-types/site-settings/schema.json | 151 + .../controllers/site-settings.ts | 7 + .../api/site-settings/routes/site-settings.ts | 7 + .../site-settings/services/site-settings.ts | 7 + .../api/topic/content-types/topic/schema.json | 48 + apps/cms/src/api/topic/controllers/topic.ts | 7 + apps/cms/src/api/topic/routes/topic.ts | 7 + apps/cms/src/api/topic/services/topic.ts | 7 + .../content-types/what-we-do-page/schema.json | 95 + .../controllers/what-we-do-page.ts | 7 + .../what-we-do-page/routes/what-we-do-page.ts | 7 + .../services/what-we-do-page.ts | 7 + apps/cms/src/bootstrap/auth-permissions.ts | 91 + apps/cms/src/bootstrap/constants.ts | 104 + apps/cms/src/bootstrap/index.ts | 31 + apps/cms/src/bootstrap/locales.ts | 97 + apps/cms/src/bootstrap/public-permissions.ts | 123 + apps/cms/src/components/blocks/accordion.json | 19 + apps/cms/src/components/blocks/card-grid.json | 22 + .../cms/src/components/blocks/cta-banner.json | 33 + .../src/components/blocks/image-gallery.json | 22 + .../src/components/blocks/partner-logos.json | 19 + apps/cms/src/components/blocks/quote.json | 21 + apps/cms/src/components/blocks/rich-text.json | 15 + .../cms/src/components/blocks/stats-grid.json | 19 + .../src/components/blocks/video-embed.json | 26 + .../components/navigation/footer-column.json | 20 + .../src/components/navigation/nav-group.json | 23 + .../components/page/consortium-member.json | 19 + apps/cms/src/components/page/workstream.json | 26 + .../src/components/shared/accordion-item.json | 19 + apps/cms/src/components/shared/address.json | 30 + apps/cms/src/components/shared/card.json | 31 + apps/cms/src/components/shared/cta.json | 31 + apps/cms/src/components/shared/hero.json | 35 + apps/cms/src/components/shared/link.json | 23 + apps/cms/src/components/shared/seo.json | 35 + apps/cms/src/components/shared/stat.json | 19 + .../src/components/shared/titled-text.json | 18 + apps/cms/src/extensions/.gitkeep | 0 apps/cms/src/index.ts | 25 + apps/cms/src/middlewares/rate-limit.ts | 139 + apps/cms/tsconfig.json | 44 + apps/cms/types/generated/components.d.ts | 349 + apps/cms/types/generated/contentTypes.d.ts | 2689 +++ apps/web/.agents/skills/react-router/SKILL.md | 122 + .../react-router/references/data-mode.md | 165 + .../references/declarative-mode.md | 123 + .../react-router/references/framework-mode.md | 213 + .../skills/react-router/references/rsc.md | 90 + apps/web/.dockerignore | 4 + apps/web/.gitignore | 7 + apps/web/Dockerfile | 69 + apps/web/README.md | 122 + apps/web/app/app.css | 88 + .../app/components/ui/Button/ArrowIcon.css | 62 + .../app/components/ui/Button/ArrowIcon.tsx | 32 + apps/web/app/components/ui/Button/Button.css | 247 + apps/web/app/components/ui/Button/Button.tsx | 151 + apps/web/app/components/ui/Button/index.ts | 4 + apps/web/app/components/ui/Card/CardCTA.tsx | 97 + .../app/components/ui/Card/CardContent.tsx | 114 + .../app/components/ui/Card/CardHorizontal.tsx | 104 + apps/web/app/components/ui/Card/CardShell.tsx | 238 + apps/web/app/components/ui/Card/CardStat.tsx | 106 + apps/web/app/components/ui/Card/CardTeam.tsx | 140 + apps/web/app/components/ui/Card/card.css | 129 + apps/web/app/components/ui/Card/index.ts | 71 + apps/web/app/components/ui/Card/types.ts | 42 + apps/web/app/components/ui/Footer/Footer.tsx | 340 + .../components/ui/Footer/FooterSocialIcon.tsx | 43 + apps/web/app/components/ui/Footer/content.ts | 202 + apps/web/app/components/ui/Footer/index.ts | 31 + apps/web/app/components/ui/Footer/types.ts | 116 + apps/web/app/components/ui/Input/Input.css | 141 + apps/web/app/components/ui/Input/Input.tsx | 246 + apps/web/app/components/ui/Input/index.ts | 2 + .../ui/LanguageSwitcher/LanguageSwitcher.tsx | 121 + .../components/ui/LanguageSwitcher/index.ts | 26 + .../ui/LanguageSwitcher/locale-path.ts | 140 + .../app/components/ui/Navigation/Brand.tsx | 50 + .../components/ui/Navigation/DesktopNav.tsx | 441 + .../app/components/ui/Navigation/EuBadge.tsx | 73 + .../app/components/ui/Navigation/MenuBars.tsx | 33 + .../components/ui/Navigation/MobileNav.tsx | 344 + .../components/ui/Navigation/Navigation.tsx | 211 + .../web/app/components/ui/Navigation/index.ts | 31 + .../app/components/ui/Navigation/nav-items.ts | 153 + .../app/components/ui/Navigation/styles.ts | 19 + .../web/app/components/ui/Navigation/types.ts | 53 + apps/web/app/entry.server.tsx | 122 + apps/web/app/i18n/config.ts | 136 + apps/web/app/i18n/format.ts | 103 + apps/web/app/i18n/hooks.ts | 111 + apps/web/app/i18n/index.ts | 78 + apps/web/app/i18n/messages/ar.json | 169 + apps/web/app/i18n/messages/en.json | 169 + apps/web/app/i18n/paths.ts | 80 + apps/web/app/i18n/t.ts | 118 + apps/web/app/lib/env.server.ts | 135 + apps/web/app/lib/route-handle.ts | 59 + apps/web/app/lib/route-slugs.ts | 75 + apps/web/app/lib/safe-href.ts | 71 + apps/web/app/lib/seo.ts | 566 + apps/web/app/lib/site-navigation.ts | 79 + apps/web/app/lib/strapi.ts | 700 + apps/web/app/root.tsx | 266 + apps/web/app/routes.ts | 157 + .../app/routes/_events-opportunities/copy.ts | 247 + .../_events-opportunities/events.server.ts | 231 + .../routes/_events-opportunities/events.ts | 408 + .../app/routes/_events-opportunities/forms.ts | 152 + .../app/routes/_events-opportunities/ics.ts | 219 + .../opportunities.server.ts | 212 + .../_events-opportunities/opportunities.ts | 227 + .../routes/_events-opportunities/root-seo.ts | 71 + .../strapi-write.server.ts | 112 + .../submissions.server.ts | 106 + .../app/routes/_events-opportunities/ui.tsx | 432 + apps/web/app/routes/_page-kit/cms-types.ts | 196 + apps/web/app/routes/_page-kit/cms.server.ts | 131 + apps/web/app/routes/_page-kit/copy.ts | 386 + apps/web/app/routes/_page-kit/locale.ts | 13 + apps/web/app/routes/_page-kit/rich-text.tsx | 272 + apps/web/app/routes/_page-kit/sections.tsx | 362 + apps/web/app/routes/_shared/RichText.tsx | 236 + apps/web/app/routes/_shared/alternates.ts | 190 + apps/web/app/routes/_shared/cms.ts | 480 + apps/web/app/routes/_shared/listing-ui.tsx | 526 + apps/web/app/routes/_shared/listing.ts | 321 + apps/web/app/routes/_shared/page-copy.ts | 229 + apps/web/app/routes/about.tsx | 266 + .../activities-and-initiatives.$slug.tsx | 666 + .../app/routes/activities-and-initiatives.tsx | 347 + apps/web/app/routes/activities.content.ts | 422 + .../applications-and-opportunities.$slug.tsx | 556 + .../routes/applications-and-opportunities.tsx | 271 + apps/web/app/routes/contact.tsx | 601 + .../app/routes/events-calendar.$slug.ics.ts | 100 + apps/web/app/routes/events-calendar.$slug.tsx | 643 + apps/web/app/routes/events-calendar.tsx | 253 + apps/web/app/routes/home.tsx | 341 + apps/web/app/routes/knowledge-hub.$slug.tsx | 292 + apps/web/app/routes/knowledge-hub.tsx | 309 + apps/web/app/routes/locale-layout.tsx | 47 + apps/web/app/routes/locale-redirect.tsx | 33 + .../web/app/routes/news-and-updates.$slug.tsx | 264 + apps/web/app/routes/news-and-updates.tsx | 253 + apps/web/app/routes/not-found.tsx | 21 + .../resources-and-publications.$slug.tsx | 297 + .../app/routes/resources-and-publications.tsx | 457 + apps/web/app/routes/what-we-do.tsx | 212 + apps/web/app/styles/README.md | 285 + apps/web/app/styles/primitives.css | 249 + apps/web/app/styles/semantic.css | 423 + apps/web/app/styles/typography.css | 224 + apps/web/package.json | 29 + apps/web/public/assets/eu-emblem-inverse.png | Bin 0 -> 4507 bytes apps/web/public/assets/eu-emblem.png | Bin 0 -> 6699 bytes apps/web/public/assets/eu-flag-dark.png | Bin 0 -> 5072 bytes apps/web/public/assets/eu-flag.png | Bin 0 -> 6699 bytes apps/web/public/assets/icons/arrow-right.svg | 5 + .../public/assets/icons/arrow-small-right.svg | 5 + apps/web/public/assets/icons/arrow-up.svg | 5 + apps/web/public/assets/icons/globe.svg | 10 + apps/web/public/assets/icons/linkedin.svg | 9 + apps/web/public/assets/icons/mail.svg | 5 + .../public/assets/partner-acted-inverse.png | Bin 0 -> 11076 bytes apps/web/public/assets/partner-acted.png | Bin 0 -> 17529 bytes .../web/public/assets/partner-sbe-inverse.png | Bin 0 -> 7365 bytes apps/web/public/assets/partner-sbe.png | Bin 0 -> 11221 bytes .../web/public/assets/partner-uec-inverse.png | Bin 0 -> 2675 bytes apps/web/public/assets/partner-uec.png | Bin 0 -> 9423 bytes apps/web/public/assets/sharp-er-logo-dark.png | Bin 0 -> 3409 bytes apps/web/public/assets/sharp-er-logo.png | Bin 0 -> 5272 bytes .../assets/sharp-er-wordmark-inverse.png | Bin 0 -> 3255 bytes apps/web/public/assets/sharp-er-wordmark.png | Bin 0 -> 5229 bytes apps/web/public/favicon.ico | Bin 0 -> 15086 bytes apps/web/public/fonts/.gitkeep | 0 .../public/fonts/Montserrat-Arabic-Bold.woff2 | Bin 0 -> 81320 bytes .../fonts/Montserrat-Arabic-Medium.woff2 | Bin 0 -> 80576 bytes .../fonts/Montserrat-Arabic-Regular.woff2 | Bin 0 -> 79236 bytes apps/web/public/fonts/Panton-Bold.woff2 | Bin 0 -> 32352 bytes apps/web/public/fonts/Panton-Regular.woff2 | Bin 0 -> 32084 bytes apps/web/react-router.config.ts | 7 + apps/web/tsconfig.json | 26 + apps/web/vite.config.ts | 24 + docker-compose.yml | 140 + docs/SHARP-ER_Website Recommendations.docx | Bin 0 -> 21492 bytes docs/SHARPER Home Page .docx | Bin 0 -> 15673 bytes docs/SHARPER_Website_Draft_Section_EN_AR.docx | Bin 0 -> 26784 bytes ...rp Updated communication guidline_ (2).pdf | Bin 0 -> 2596251 bytes docs/UEC_SHARPER_Website_TOR V3.docx | Bin 0 -> 34514 bytes package.json | 26 + pnpm-lock.yaml | 14376 ++++++++++++++++ pnpm-workspace.yaml | 3 + scripts/check-prereqs.sh | 160 + scripts/generate-secrets.sh | 181 + scripts/setup-db.sh | 168 + 288 files changed, 46557 insertions(+) create mode 100644 .dockerignore create mode 100644 .editorconfig create mode 100644 .env.example create mode 100644 .gitignore create mode 100644 .npmrc create mode 100644 .nvmrc create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 apps/cms/.env.example create mode 100644 apps/cms/.gitignore create mode 100644 apps/cms/Dockerfile create mode 100644 apps/cms/README.md create mode 100644 apps/cms/config/admin.ts create mode 100644 apps/cms/config/api.ts create mode 100644 apps/cms/config/database.ts create mode 100644 apps/cms/config/middlewares.ts create mode 100644 apps/cms/config/plugins.ts create mode 100644 apps/cms/config/server.ts create mode 100644 apps/cms/database/migrations/.gitkeep create mode 100644 apps/cms/favicon.png create mode 100644 apps/cms/package.json create mode 100644 apps/cms/public/robots.txt create mode 100644 apps/cms/public/uploads/.gitkeep create mode 100644 apps/cms/src/admin/app.example.tsx create mode 100644 apps/cms/src/admin/tsconfig.json create mode 100644 apps/cms/src/admin/vite.config.example.ts create mode 100644 apps/cms/src/api/.gitkeep create mode 100644 apps/cms/src/api/about-page/content-types/about-page/schema.json create mode 100644 apps/cms/src/api/about-page/controllers/about-page.ts create mode 100644 apps/cms/src/api/about-page/routes/about-page.ts create mode 100644 apps/cms/src/api/about-page/services/about-page.ts create mode 100644 apps/cms/src/api/activity/content-types/activity/schema.json create mode 100644 apps/cms/src/api/activity/controllers/activity.ts create mode 100644 apps/cms/src/api/activity/routes/activity.ts create mode 100644 apps/cms/src/api/activity/services/activity.ts create mode 100644 apps/cms/src/api/application/content-types/application/lifecycles.ts create mode 100644 apps/cms/src/api/application/content-types/application/schema.json create mode 100644 apps/cms/src/api/application/controllers/application.ts create mode 100644 apps/cms/src/api/application/middlewares/sanitize-submission.ts create mode 100644 apps/cms/src/api/application/routes/application.ts create mode 100644 apps/cms/src/api/application/services/application.ts create mode 100644 apps/cms/src/api/contact-page/content-types/contact-page/schema.json create mode 100644 apps/cms/src/api/contact-page/controllers/contact-page.ts create mode 100644 apps/cms/src/api/contact-page/routes/contact-page.ts create mode 100644 apps/cms/src/api/contact-page/services/contact-page.ts create mode 100644 apps/cms/src/api/contact-submission/content-types/contact-submission/lifecycles.ts create mode 100644 apps/cms/src/api/contact-submission/content-types/contact-submission/schema.json create mode 100644 apps/cms/src/api/contact-submission/controllers/contact-submission.ts create mode 100644 apps/cms/src/api/contact-submission/middlewares/sanitize-submission.ts create mode 100644 apps/cms/src/api/contact-submission/routes/contact-submission.ts create mode 100644 apps/cms/src/api/contact-submission/services/contact-submission.ts create mode 100644 apps/cms/src/api/event-registration/content-types/event-registration/lifecycles.ts create mode 100644 apps/cms/src/api/event-registration/content-types/event-registration/schema.json create mode 100644 apps/cms/src/api/event-registration/controllers/event-registration.ts create mode 100644 apps/cms/src/api/event-registration/middlewares/sanitize-submission.ts create mode 100644 apps/cms/src/api/event-registration/routes/event-registration.ts create mode 100644 apps/cms/src/api/event-registration/services/event-registration.ts create mode 100644 apps/cms/src/api/event/content-types/event/schema.json create mode 100644 apps/cms/src/api/event/controllers/event.ts create mode 100644 apps/cms/src/api/event/routes/event.ts create mode 100644 apps/cms/src/api/event/services/event.ts create mode 100644 apps/cms/src/api/home-page/content-types/home-page/schema.json create mode 100644 apps/cms/src/api/home-page/controllers/home-page.ts create mode 100644 apps/cms/src/api/home-page/routes/home-page.ts create mode 100644 apps/cms/src/api/home-page/services/home-page.ts create mode 100644 apps/cms/src/api/knowledge-item/content-types/knowledge-item/schema.json create mode 100644 apps/cms/src/api/knowledge-item/controllers/knowledge-item.ts create mode 100644 apps/cms/src/api/knowledge-item/routes/knowledge-item.ts create mode 100644 apps/cms/src/api/knowledge-item/services/knowledge-item.ts create mode 100644 apps/cms/src/api/news-article/content-types/news-article/schema.json create mode 100644 apps/cms/src/api/news-article/controllers/news-article.ts create mode 100644 apps/cms/src/api/news-article/routes/news-article.ts create mode 100644 apps/cms/src/api/news-article/services/news-article.ts create mode 100644 apps/cms/src/api/opportunity/content-types/opportunity/schema.json create mode 100644 apps/cms/src/api/opportunity/controllers/opportunity.ts create mode 100644 apps/cms/src/api/opportunity/routes/opportunity.ts create mode 100644 apps/cms/src/api/opportunity/services/opportunity.ts create mode 100644 apps/cms/src/api/partner/content-types/partner/schema.json create mode 100644 apps/cms/src/api/partner/controllers/partner.ts create mode 100644 apps/cms/src/api/partner/routes/partner.ts create mode 100644 apps/cms/src/api/partner/services/partner.ts create mode 100644 apps/cms/src/api/region/content-types/region/schema.json create mode 100644 apps/cms/src/api/region/controllers/region.ts create mode 100644 apps/cms/src/api/region/routes/region.ts create mode 100644 apps/cms/src/api/region/services/region.ts create mode 100644 apps/cms/src/api/resource/content-types/resource/schema.json create mode 100644 apps/cms/src/api/resource/controllers/resource.ts create mode 100644 apps/cms/src/api/resource/routes/resource.ts create mode 100644 apps/cms/src/api/resource/services/resource.ts create mode 100644 apps/cms/src/api/site-settings/content-types/site-settings/schema.json create mode 100644 apps/cms/src/api/site-settings/controllers/site-settings.ts create mode 100644 apps/cms/src/api/site-settings/routes/site-settings.ts create mode 100644 apps/cms/src/api/site-settings/services/site-settings.ts create mode 100644 apps/cms/src/api/topic/content-types/topic/schema.json create mode 100644 apps/cms/src/api/topic/controllers/topic.ts create mode 100644 apps/cms/src/api/topic/routes/topic.ts create mode 100644 apps/cms/src/api/topic/services/topic.ts create mode 100644 apps/cms/src/api/what-we-do-page/content-types/what-we-do-page/schema.json create mode 100644 apps/cms/src/api/what-we-do-page/controllers/what-we-do-page.ts create mode 100644 apps/cms/src/api/what-we-do-page/routes/what-we-do-page.ts create mode 100644 apps/cms/src/api/what-we-do-page/services/what-we-do-page.ts create mode 100644 apps/cms/src/bootstrap/auth-permissions.ts create mode 100644 apps/cms/src/bootstrap/constants.ts create mode 100644 apps/cms/src/bootstrap/index.ts create mode 100644 apps/cms/src/bootstrap/locales.ts create mode 100644 apps/cms/src/bootstrap/public-permissions.ts create mode 100644 apps/cms/src/components/blocks/accordion.json create mode 100644 apps/cms/src/components/blocks/card-grid.json create mode 100644 apps/cms/src/components/blocks/cta-banner.json create mode 100644 apps/cms/src/components/blocks/image-gallery.json create mode 100644 apps/cms/src/components/blocks/partner-logos.json create mode 100644 apps/cms/src/components/blocks/quote.json create mode 100644 apps/cms/src/components/blocks/rich-text.json create mode 100644 apps/cms/src/components/blocks/stats-grid.json create mode 100644 apps/cms/src/components/blocks/video-embed.json create mode 100644 apps/cms/src/components/navigation/footer-column.json create mode 100644 apps/cms/src/components/navigation/nav-group.json create mode 100644 apps/cms/src/components/page/consortium-member.json create mode 100644 apps/cms/src/components/page/workstream.json create mode 100644 apps/cms/src/components/shared/accordion-item.json create mode 100644 apps/cms/src/components/shared/address.json create mode 100644 apps/cms/src/components/shared/card.json create mode 100644 apps/cms/src/components/shared/cta.json create mode 100644 apps/cms/src/components/shared/hero.json create mode 100644 apps/cms/src/components/shared/link.json create mode 100644 apps/cms/src/components/shared/seo.json create mode 100644 apps/cms/src/components/shared/stat.json create mode 100644 apps/cms/src/components/shared/titled-text.json create mode 100644 apps/cms/src/extensions/.gitkeep create mode 100644 apps/cms/src/index.ts create mode 100644 apps/cms/src/middlewares/rate-limit.ts create mode 100644 apps/cms/tsconfig.json create mode 100644 apps/cms/types/generated/components.d.ts create mode 100644 apps/cms/types/generated/contentTypes.d.ts create mode 100644 apps/web/.agents/skills/react-router/SKILL.md create mode 100644 apps/web/.agents/skills/react-router/references/data-mode.md create mode 100644 apps/web/.agents/skills/react-router/references/declarative-mode.md create mode 100644 apps/web/.agents/skills/react-router/references/framework-mode.md create mode 100644 apps/web/.agents/skills/react-router/references/rsc.md create mode 100644 apps/web/.dockerignore create mode 100644 apps/web/.gitignore create mode 100644 apps/web/Dockerfile create mode 100644 apps/web/README.md create mode 100644 apps/web/app/app.css create mode 100644 apps/web/app/components/ui/Button/ArrowIcon.css create mode 100644 apps/web/app/components/ui/Button/ArrowIcon.tsx create mode 100644 apps/web/app/components/ui/Button/Button.css create mode 100644 apps/web/app/components/ui/Button/Button.tsx create mode 100644 apps/web/app/components/ui/Button/index.ts create mode 100644 apps/web/app/components/ui/Card/CardCTA.tsx create mode 100644 apps/web/app/components/ui/Card/CardContent.tsx create mode 100644 apps/web/app/components/ui/Card/CardHorizontal.tsx create mode 100644 apps/web/app/components/ui/Card/CardShell.tsx create mode 100644 apps/web/app/components/ui/Card/CardStat.tsx create mode 100644 apps/web/app/components/ui/Card/CardTeam.tsx create mode 100644 apps/web/app/components/ui/Card/card.css create mode 100644 apps/web/app/components/ui/Card/index.ts create mode 100644 apps/web/app/components/ui/Card/types.ts create mode 100644 apps/web/app/components/ui/Footer/Footer.tsx create mode 100644 apps/web/app/components/ui/Footer/FooterSocialIcon.tsx create mode 100644 apps/web/app/components/ui/Footer/content.ts create mode 100644 apps/web/app/components/ui/Footer/index.ts create mode 100644 apps/web/app/components/ui/Footer/types.ts create mode 100644 apps/web/app/components/ui/Input/Input.css create mode 100644 apps/web/app/components/ui/Input/Input.tsx create mode 100644 apps/web/app/components/ui/Input/index.ts create mode 100644 apps/web/app/components/ui/LanguageSwitcher/LanguageSwitcher.tsx create mode 100644 apps/web/app/components/ui/LanguageSwitcher/index.ts create mode 100644 apps/web/app/components/ui/LanguageSwitcher/locale-path.ts create mode 100644 apps/web/app/components/ui/Navigation/Brand.tsx create mode 100644 apps/web/app/components/ui/Navigation/DesktopNav.tsx create mode 100644 apps/web/app/components/ui/Navigation/EuBadge.tsx create mode 100644 apps/web/app/components/ui/Navigation/MenuBars.tsx create mode 100644 apps/web/app/components/ui/Navigation/MobileNav.tsx create mode 100644 apps/web/app/components/ui/Navigation/Navigation.tsx create mode 100644 apps/web/app/components/ui/Navigation/index.ts create mode 100644 apps/web/app/components/ui/Navigation/nav-items.ts create mode 100644 apps/web/app/components/ui/Navigation/styles.ts create mode 100644 apps/web/app/components/ui/Navigation/types.ts create mode 100644 apps/web/app/entry.server.tsx create mode 100644 apps/web/app/i18n/config.ts create mode 100644 apps/web/app/i18n/format.ts create mode 100644 apps/web/app/i18n/hooks.ts create mode 100644 apps/web/app/i18n/index.ts create mode 100644 apps/web/app/i18n/messages/ar.json create mode 100644 apps/web/app/i18n/messages/en.json create mode 100644 apps/web/app/i18n/paths.ts create mode 100644 apps/web/app/i18n/t.ts create mode 100644 apps/web/app/lib/env.server.ts create mode 100644 apps/web/app/lib/route-handle.ts create mode 100644 apps/web/app/lib/route-slugs.ts create mode 100644 apps/web/app/lib/safe-href.ts create mode 100644 apps/web/app/lib/seo.ts create mode 100644 apps/web/app/lib/site-navigation.ts create mode 100644 apps/web/app/lib/strapi.ts create mode 100644 apps/web/app/root.tsx create mode 100644 apps/web/app/routes.ts create mode 100644 apps/web/app/routes/_events-opportunities/copy.ts create mode 100644 apps/web/app/routes/_events-opportunities/events.server.ts create mode 100644 apps/web/app/routes/_events-opportunities/events.ts create mode 100644 apps/web/app/routes/_events-opportunities/forms.ts create mode 100644 apps/web/app/routes/_events-opportunities/ics.ts create mode 100644 apps/web/app/routes/_events-opportunities/opportunities.server.ts create mode 100644 apps/web/app/routes/_events-opportunities/opportunities.ts create mode 100644 apps/web/app/routes/_events-opportunities/root-seo.ts create mode 100644 apps/web/app/routes/_events-opportunities/strapi-write.server.ts create mode 100644 apps/web/app/routes/_events-opportunities/submissions.server.ts create mode 100644 apps/web/app/routes/_events-opportunities/ui.tsx create mode 100644 apps/web/app/routes/_page-kit/cms-types.ts create mode 100644 apps/web/app/routes/_page-kit/cms.server.ts create mode 100644 apps/web/app/routes/_page-kit/copy.ts create mode 100644 apps/web/app/routes/_page-kit/locale.ts create mode 100644 apps/web/app/routes/_page-kit/rich-text.tsx create mode 100644 apps/web/app/routes/_page-kit/sections.tsx create mode 100644 apps/web/app/routes/_shared/RichText.tsx create mode 100644 apps/web/app/routes/_shared/alternates.ts create mode 100644 apps/web/app/routes/_shared/cms.ts create mode 100644 apps/web/app/routes/_shared/listing-ui.tsx create mode 100644 apps/web/app/routes/_shared/listing.ts create mode 100644 apps/web/app/routes/_shared/page-copy.ts create mode 100644 apps/web/app/routes/about.tsx create mode 100644 apps/web/app/routes/activities-and-initiatives.$slug.tsx create mode 100644 apps/web/app/routes/activities-and-initiatives.tsx create mode 100644 apps/web/app/routes/activities.content.ts create mode 100644 apps/web/app/routes/applications-and-opportunities.$slug.tsx create mode 100644 apps/web/app/routes/applications-and-opportunities.tsx create mode 100644 apps/web/app/routes/contact.tsx create mode 100644 apps/web/app/routes/events-calendar.$slug.ics.ts create mode 100644 apps/web/app/routes/events-calendar.$slug.tsx create mode 100644 apps/web/app/routes/events-calendar.tsx create mode 100644 apps/web/app/routes/home.tsx create mode 100644 apps/web/app/routes/knowledge-hub.$slug.tsx create mode 100644 apps/web/app/routes/knowledge-hub.tsx create mode 100644 apps/web/app/routes/locale-layout.tsx create mode 100644 apps/web/app/routes/locale-redirect.tsx create mode 100644 apps/web/app/routes/news-and-updates.$slug.tsx create mode 100644 apps/web/app/routes/news-and-updates.tsx create mode 100644 apps/web/app/routes/not-found.tsx create mode 100644 apps/web/app/routes/resources-and-publications.$slug.tsx create mode 100644 apps/web/app/routes/resources-and-publications.tsx create mode 100644 apps/web/app/routes/what-we-do.tsx create mode 100644 apps/web/app/styles/README.md create mode 100644 apps/web/app/styles/primitives.css create mode 100644 apps/web/app/styles/semantic.css create mode 100644 apps/web/app/styles/typography.css create mode 100644 apps/web/package.json create mode 100644 apps/web/public/assets/eu-emblem-inverse.png create mode 100644 apps/web/public/assets/eu-emblem.png create mode 100644 apps/web/public/assets/eu-flag-dark.png create mode 100644 apps/web/public/assets/eu-flag.png create mode 100644 apps/web/public/assets/icons/arrow-right.svg create mode 100644 apps/web/public/assets/icons/arrow-small-right.svg create mode 100644 apps/web/public/assets/icons/arrow-up.svg create mode 100644 apps/web/public/assets/icons/globe.svg create mode 100644 apps/web/public/assets/icons/linkedin.svg create mode 100644 apps/web/public/assets/icons/mail.svg create mode 100644 apps/web/public/assets/partner-acted-inverse.png create mode 100644 apps/web/public/assets/partner-acted.png create mode 100644 apps/web/public/assets/partner-sbe-inverse.png create mode 100644 apps/web/public/assets/partner-sbe.png create mode 100644 apps/web/public/assets/partner-uec-inverse.png create mode 100644 apps/web/public/assets/partner-uec.png create mode 100644 apps/web/public/assets/sharp-er-logo-dark.png create mode 100644 apps/web/public/assets/sharp-er-logo.png create mode 100644 apps/web/public/assets/sharp-er-wordmark-inverse.png create mode 100644 apps/web/public/assets/sharp-er-wordmark.png create mode 100644 apps/web/public/favicon.ico create mode 100644 apps/web/public/fonts/.gitkeep create mode 100644 apps/web/public/fonts/Montserrat-Arabic-Bold.woff2 create mode 100644 apps/web/public/fonts/Montserrat-Arabic-Medium.woff2 create mode 100644 apps/web/public/fonts/Montserrat-Arabic-Regular.woff2 create mode 100644 apps/web/public/fonts/Panton-Bold.woff2 create mode 100644 apps/web/public/fonts/Panton-Regular.woff2 create mode 100644 apps/web/react-router.config.ts create mode 100644 apps/web/tsconfig.json create mode 100644 apps/web/vite.config.ts create mode 100644 docker-compose.yml create mode 100644 docs/SHARP-ER_Website Recommendations.docx create mode 100644 docs/SHARPER Home Page .docx create mode 100644 docs/SHARPER_Website_Draft_Section_EN_AR.docx create mode 100644 docs/Sharp Updated communication guidline_ (2).pdf create mode 100644 docs/UEC_SHARPER_Website_TOR V3.docx create mode 100644 package.json create mode 100644 pnpm-lock.yaml create mode 100644 pnpm-workspace.yaml create mode 100755 scripts/check-prereqs.sh create mode 100755 scripts/generate-secrets.sh create mode 100755 scripts/setup-db.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..14dc2d3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,42 @@ +# ============================================================================ +# Build context for BOTH images is the repository root (see docker-compose.yml). +# Everything below is either reinstalled/rebuilt inside the image or must never +# enter one. +# ============================================================================ + +# Dependencies and build output - recreated by pnpm install / pnpm build. +**/node_modules +apps/web/build +apps/web/.react-router +apps/cms/dist +apps/cms/build +apps/cms/.strapi +apps/cms/.tmp +apps/cms/.cache + +# Secrets. The containers receive their environment from docker compose. +.env +.env.* +!.env.example + +# Licensed fonts are mounted or copied in at deploy time, never baked into a +# published image. See the FONTS section of README.md. +apps/web/public/fonts/*.woff2 +apps/web/public/fonts/*.woff +apps/web/public/fonts/*.ttf +apps/web/public/fonts/*.otf + +# Locally uploaded media lives in the cms-uploads volume, not in the image. +apps/cms/public/uploads/* +!apps/cms/public/uploads/.gitkeep + +# Nothing here changes the built artefact; excluding it keeps the layer cache warm. +.git +.github +.vscode +.idea +docs +**/*.log +**/.DS_Store +**/coverage +**/*.tsbuildinfo diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..de7d26f --- /dev/null +++ b/.editorconfig @@ -0,0 +1,35 @@ +# https://editorconfig.org +# One formatting contract for every editor working on SHARP-ER. +root = true + +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 100 + +# Markdown keeps trailing whitespace: two spaces are a hard line break. +[*.md] +trim_trailing_whitespace = false +max_line_length = off + +# Makefiles are tab-indented by definition. +[Makefile] +indent_style = tab + +[*.{yml,yaml}] +indent_size = 2 + +[*.{json,jsonc}] +indent_size = 2 + +[*.sh] +indent_size = 2 + +# Lockfiles and generated files are written by tooling - leave them alone. +[{pnpm-lock.yaml,package-lock.json}] +insert_final_newline = unset +trim_trailing_whitespace = unset diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..63e5537 --- /dev/null +++ b/.env.example @@ -0,0 +1,114 @@ +# ============================================================================ +# SHARP-ER - root environment template +# ---------------------------------------------------------------------------- +# Copy once, then fill in: cp .env.example .env +# Generate the CMS secrets: ./scripts/generate-secrets.sh --write .env +# +# WHO READS THIS FILE +# * docker compose - every ${VARIABLE} in docker-compose.yml is read from +# the .env file sitting next to it (this one). +# * apps/web - the React Router server reads STRAPI_URL / STRAPI_TOKEN +# / SITE_URL from its process environment. In manual mode +# nothing loads this file for you, so export it first: +# set -a; . ./.env; set +a +# The defaults below are also the code defaults, so a +# plain `pnpm dev` works without exporting anything. +# * apps/cms - Strapi does NOT read this file. In manual mode it reads +# apps/cms/.env (cp apps/cms/.env.example apps/cms/.env). +# Under Docker the values below are injected into the +# container instead, so apps/cms/.env is not used there. +# +# Never commit .env. Never paste a real secret back into this template. +# ============================================================================ + + +# --------------------------------------------------------------------------- +# DATABASE +# Manual mode: your local PostgreSQL 16 instance (scripts/setup-db.sh uses +# these values). Docker mode: the postgres service is created with them. +# --------------------------------------------------------------------------- + +DATABASE_NAME=sharper_cms +DATABASE_USERNAME=postgres +DATABASE_PASSWORD=postgres + +# Manual mode only - where the local server listens. Inside Docker the CMS +# always reaches the database as postgres:5432 on the compose network. +DATABASE_HOST=127.0.0.1 +DATABASE_PORT=5432 + +# Docker mode only - host port the containerised database is published on. +# 5433 by default so it cannot collide with a PostgreSQL you already run on +# 5432. Connect with: psql -h 127.0.0.1 -p 5433 -U postgres sharper_cms +POSTGRES_PUBLISHED_PORT=5433 + + +# --------------------------------------------------------------------------- +# CMS (Strapi 5) SECRETS +# Required. Every value must be a fresh random string - generate them with +# ./scripts/generate-secrets.sh --write .env +# or by hand with `openssl rand -base64 32`. +# APP_KEYS is a comma-separated list (at least two, so keys can be rotated). +# Rotating APP_KEYS logs everyone out; rotating ENCRYPTION_KEY makes values +# already encrypted in the database unreadable. +# --------------------------------------------------------------------------- + +APP_KEYS= +API_TOKEN_SALT= +ADMIN_JWT_SECRET= +JWT_SECRET= +TRANSFER_TOKEN_SALT= +ENCRYPTION_KEY= + + +# --------------------------------------------------------------------------- +# PORTS +# Host ports the two applications are published on in Docker mode, and the +# ports they listen on in manual mode. +# --------------------------------------------------------------------------- + +# React Router server (production build / Docker). `pnpm dev:web` uses 5173. +WEB_PORT=3000 +# Strapi API + admin panel. +CMS_PORT=1337 + +# Docker mode only - the absolute URL the admin panel and Strapi's generated +# links use. Change it together with CMS_PORT, and set it to the public origin +# in production (e.g. https://cms.sharper.ly). +CMS_PUBLIC_URL=http://localhost:1337 + + +# --------------------------------------------------------------------------- +# WEB APP +# --------------------------------------------------------------------------- + +# Public origin of the site. Used for canonical URLs, hreflang alternates and +# JSON-LD, so it must match how visitors actually reach the site. +# Leave it unset in local development and the request's own host is used. +SITE_URL=http://localhost:3000 + +# Where the server-side loaders fetch content from, MANUAL MODE. +STRAPI_URL=http://localhost:1337 + +# Where they fetch from inside Docker, where "cms" is the compose service name. +# NOTE: this origin is also prefixed onto uploaded media URLs, which the +# browser has to be able to resolve too - see "Images from the CMS" in the +# Docker section of README.md. +DOCKER_STRAPI_URL=http://cms:1337 + +# Optional Strapi API token. Only needed to read draft or otherwise protected +# content; published content is public. Create it in the admin panel under +# Settings -> API Tokens, then paste the value here (it is shown once). +STRAPI_TOKEN= + + +# --------------------------------------------------------------------------- +# CMS RUNTIME +# --------------------------------------------------------------------------- + +# Browser origins allowed to call the API, comma separated. localhost:5173 and +# 127.0.0.1:5173 are always allowed outside production and need not be listed. +CORS_ORIGINS=http://localhost:3000 + +# Log verbosity: fatal | error | warn | info | http | debug | trace +LOG_LEVEL=info diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8a7fb5b --- /dev/null +++ b/.gitignore @@ -0,0 +1,81 @@ +# ============================================================================ +# SHARP-ER - repository ignore rules +# Anything listed here is either generated, machine-local, or licensed +# material that must not be redistributed through this repository. +# ============================================================================ + +# --- Dependencies ---------------------------------------------------------- +node_modules/ +.pnpm-store/ +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/releases + +# --- Build output ---------------------------------------------------------- +# apps/web (React Router framework mode) +apps/web/build/ +apps/web/.react-router/ +# apps/cms (Strapi 5: dist/ is the compiled server, build/ the admin panel) +apps/cms/dist/ +apps/cms/build/ +apps/cms/.strapi/ +apps/cms/.tmp/ +apps/cms/.cache/ +apps/cms/exports/ +apps/cms/.strapi-updater.json +apps/cms/.strapi-cloud.json +apps/cms/license.txt +# Uploaded media lives in the database-backed media library, not in git. +apps/cms/public/uploads/* +!apps/cms/public/uploads/.gitkeep + +# --- Environment ----------------------------------------------------------- +# Real secrets never enter git; the *.example templates always do. +.env +.env.* +!.env.example +!.env.*.example + +# --- Logs and diagnostics -------------------------------------------------- +*.log +npm-debug.log* +yarn-error.log +pnpm-debug.log* +logs/ +pids/ +*.pid +coverage/ +.tsbuildinfo +*.tsbuildinfo +.eslintcache + +# --- Local databases / dumps ---------------------------------------------- +*.sqlite +*.sqlite3 +*.db +*.sql +!scripts/**/*.sql + +# --- Editors and OS -------------------------------------------------------- +.DS_Store +.AppleDouble +.LSOverride +._* +.Spotlight-V100 +.Trashes +Thumbs.db +ehthumbs.db +Desktop.ini +$RECYCLE.BIN/ +.idea/ +.vscode/* +!.vscode/extensions.json +!.vscode/settings.example.json +*.swp +*.swo +*~ +.nbproject/ + +# --- Agent / tooling scratch ---------------------------------------------- +.agents/**/tmp/ diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..45f7272 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +# Strapi and React Router both expect a flat-ish tree for their plugin resolution. +node-linker=hoisted +engine-strict=false diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..c947119 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22.23.2 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d406544 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,130 @@ +# CLAUDE.md — working brief for agent sessions + +Read this before touching anything. Human setup instructions live in [README.md](README.md); +this file is the short version plus the rules that are easy to break. + +## What the project is + +**SHARP-ER** — *Shared Action on Rule of Law Progress – Exit Route*. EU-funded, nationwide +Libya, December 2025 – December 2027. Implemented by **Acted** (lead), **United Efforts +Consultancy (UEC)** and **Stichting Bureau Ensemble (SBE)**. Supports Libyan civil society, +local institutions and communities on justice, accountability and governance. Successor to +SHARP I and II (2019–2024). + +Tone in all copy: professional, neutral, factual, inclusive, non-confrontational. +**Not** political, **not** advocacy, **not** campaign-style. + +## Architecture in one screen + +``` +apps/web React Router 8 (framework mode, ssr: true) · React 19 · Vite 8 · Tailwind 4 + SSR only reads the CMS from loaders. No client-side fetch on first paint. + / 302 (Vary: Accept-Language) -> /en or /ar + /:lang locale layout, validates params.lang, provides { locale, dir } + /:lang/slug ten top-level sections + detail routes + Route ids: root · locale · home · about · whatWeDo · activities · events · + applications · news · contact · resources · knowledgeHub (+ *Detail) + +apps/cms Strapi 5.53 (TypeScript) · PostgreSQL 16 · i18n (en default, ar) · draft & publish + src/api/** content types (schema.json is the contract, commit it) + src/bootstrap/** runs on every boot: creates locales, grants Public permissions + (find/findOne on content; create-only on submission types) + config/ server · admin · database · middlewares (CORS) · plugins + +docs/ Source material: ToR, approved EN/AR draft copy, brand guideline. READ ONLY. +``` + +Data flow: browser → React Router SSR loader → Strapi REST (`~/lib/strapi`, server only) → +PostgreSQL. `STRAPI_TOKEN` never reaches the browser (`~/lib/env.server.ts` is `.server`, and +the bundler enforces it). Every page falls back to bundled draft copy when the CMS is +unreachable — the site must never 500 because Strapi is down. + +## Hard rules + +1. **Design tokens are two-tier. Components use tier 2 only.** + Tier 1 (`app/styles/primitives.css`) holds raw values; tier 2 (`semantic.css`, + `typography.css`) holds meanings. Never a raw hex, never a tier-1 primitive, never a raw + `font-size` / `line-height` — use the type-role utilities (`display-sm`, `heading-h2`, + `body-md`, `label-md`, …). + Check: `grep -rn "#[0-9a-fA-F]\{6\}\|--color-green\|--color-neutral\|--color-red" apps/web/app/components apps/web/app/routes` + → must return nothing. +2. **Logical properties only. Never `left` / `right`.** + `ps-` `pe-` `ms-` `me-` `start-` `end-`, `margin-inline-start`, `text-align: start`. + The site ships RTL Arabic; physical directions silently break it. + Check: `grep -rnE "\b(pl|pr|ml|mr|left|right)-[0-9]" apps/web/app` → nothing meaningful. +3. **Surfaces, not a theme.** `:root` *is* the dark surface; `data-surface="light"` flips a + subtree and repaints itself. Do not add a global theme toggle or a `dark:` variant. +4. **Figma is READ ONLY.** Only `get_design_context`, `get_screenshot`, `get_metadata`, + `get_variable_defs`. Never call anything that writes (`use_figma`, `generate_figma_design`, + `create_new_file`, `upload_assets`, shaders, plugins, …). Before the first + `get_design_context`, read `skill://figma/figma-design-to-code/SKILL.md` and pass + `skillNames: "resource:figma-design-to-code"`. File key `yvKHp3Ha2rQmv9kt9tCC7j`. +5. **Never invent statistics, dates, names or quotes.** If a number is not in `docs/` or in + the CMS, it does not go on the page. Leave the slot empty or omit the component. +6. **`docs/` is read-only source material.** Never edit, move or "clean up" those files. +7. **Both locales, always.** New UI text goes into `app/i18n/messages/en.json` *and* + `ar.json` with identical keys (the `Messages` type enforces it). A component that hard-codes + an English string is a bug — that is why `Card.Content`'s `actionLabel` is required. +8. **EU visibility is contractual.** "Funded by the European Union" badge top-right, project + logo top-left, the three partner logos always shown together, no standalone partner + branding. Do not restyle or omit them. +9. **No long-running servers or installs without being asked.** Do not run `pnpm install`, + `pnpm dev`, `strapi develop` or `docker compose up` speculatively. If you need a new + dependency, report it instead of installing it. +10. **No `?lang=` parameter, ever.** The locale is a URL prefix on both languages. + +## Where things live + +| You need | It is in | +| --- | --- | +| Tokens, surfaces, type roles | `apps/web/app/styles/` (+ its own `README.md`) | +| UI components | `apps/web/app/components/ui/{Button,Input,Card,Navigation,Footer,LanguageSwitcher}` | +| Page modules | `apps/web/app/routes/*.tsx`; shared kits in `routes/_page-kit/`, `routes/_shared/`, `routes/_events-opportunities/` | +| Route config, slugs | `apps/web/app/routes.ts`, `apps/web/app/lib/route-slugs.ts` | +| i18n | `apps/web/app/i18n/` (`config.ts`, `paths.ts`, `t.ts`, `format.ts`, `messages/*.json`) | +| Strapi client, SEO, env | `apps/web/app/lib/{strapi.ts,seo.ts,env.server.ts,route-handle.ts,site-navigation.ts}` | +| HTML shell, canonical, hreflang | `apps/web/app/root.tsx` | +| Content types | `apps/cms/src/api//content-types//schema.json` | +| Boot-time locale + permission setup | `apps/cms/src/bootstrap/` | +| CORS, upload rules, i18n locales | `apps/cms/config/{middlewares.ts,plugins.ts}` | + +## Conventions that bite + +- **`meta` reads `loaderData`, not `data`.** In React Router 8, `matches[0].data` is + `undefined`; use `readRootSeo(matches)` from `~/routes/_shared/listing`. +- **Do not pass `alternates: true` to `buildMeta`.** `root.tsx` emits the canonical and the + `hreflang` alternates for every route; a leaf that repeats them duplicates the tags. Use + `handle.canonicalPath` to override the canonical. +- **`app/routes.ts` existence-checks section modules** and Vite does not re-evaluate it when a + file appears — restart the dev server after adding a route module. +- **Strapi 5 addresses entries by `documentId`**, not by a numeric `id`, and localisation is a + `locale` query parameter on the same entry. +- **Submission content types are write-only for the public** (`create`, never `find`). They + hold personal data. The field is `language`, not `locale` — `locale` is reserved by the i18n + plugin. +- **`.server.ts` files never reach the browser.** Anything touching `STRAPI_TOKEN`, the + database or `process.env` belongs in one. +- **Licensed fonts are not in the repository** (`.gitignore` excludes `public/fonts/*.woff2`). + Expect fallback typefaces locally; never commit a font file. + +## Verifying a change + +```bash +pnpm typecheck # react-router typegen + tsc (web; the CMS is checked by its build) +pnpm --filter web build # the SSR build must succeed +bash -n scripts/.sh # any shell script you touched +docker compose config # any docker-compose.yml change (needs the docker CLI) +``` + +Then, in a browser or with `curl`, on **both** locales: + +```bash +curl -sI http://localhost:5173/ # 302 + Vary: Accept-Language +curl -s http://localhost:5173/en | head -c 200 +curl -s http://localhost:5173/ar | head -c 200 # lang="ar" dir="rtl" in the first bytes +``` + +Checklist before calling a UI change done: renders in **both** locales; Arabic mirrors +correctly; no raw hex or tier-1 token; no physical-direction utility; keyboard focus is +visible; text comes from the message catalogue or the CMS, not from a literal in the +component. diff --git a/README.md b/README.md new file mode 100644 index 0000000..c8a45c2 --- /dev/null +++ b/README.md @@ -0,0 +1,906 @@ +# SHARP-ER website + +Bilingual (English / Arabic) website for **SHARP-ER** — *Shared Action on Rule of Law +Progress – Exit Route* — a European Union funded programme running nationwide in Libya +from December 2025 to December 2027. + +SHARP-ER supports Libyan civil society organisations, local institutions and communities +working on justice, accountability and governance. It is implemented by a consortium of +**Acted** (lead), **United Efforts Consultancy (UEC)** and **Stichting Bureau Ensemble (SBE)**, +and continues the work of SHARP I and SHARP II (2019–2024). + +The site publishes project information, events and registrations, calls and applications, +news from the project and its partner network, and a knowledge hub of resources and +publications — all of it in English and Arabic, with the Arabic side laid out right to left. + +> EU visibility is a contractual requirement: the *Funded by the European Union* emblem is +> present on every page, and the three implementing partners are always shown together. + +--- + +## Contents + +- [The stack, and why](#the-stack-and-why) +- [Repository layout](#repository-layout) +- [Choosing a setup path](#choosing-a-setup-path) +- [Path A — manual setup](#path-a--manual-setup) +- [Path B — Docker](#path-b--docker) +- [Available scripts](#available-scripts) +- [Design tokens](#design-tokens) +- [Bilingual EN / AR](#bilingual-en--ar) +- [Adding a content type, end to end](#adding-a-content-type-end-to-end) +- [Fonts (licensed, not in this repository)](#fonts-licensed-not-in-this-repository) +- [Deployment and operations](#deployment-and-operations) +- [Troubleshooting](#troubleshooting) +- [Source documents](#source-documents) + +--- + +## The stack, and why + +| Layer | Choice | Why | +| --- | --- | --- | +| Front end | **React Router 8**, framework mode, `ssr: true` | The terms of reference require an SEO-friendly structure. Server-side rendering means every page — including the Arabic one — is delivered as complete HTML with the correct `` and `` in the first byte, plus canonical and `hreflang` tags a crawler can read without executing JavaScript. | +| UI | **React 19**, **TypeScript**, **Vite 8** | The framework's own toolchain. Typed loaders and route modules, so a content shape change is a compile error rather than a blank page. | +| Styling | **Tailwind CSS 4** over a **two-tier design-token** layer | Tier 1 holds raw values, tier 2 holds meanings; components only ever spend tier‑2 tokens. Re-branding is a one-line edit in one file. See [Design tokens](#design-tokens). | +| CMS | **Strapi 5.53** (TypeScript) | Self-hosted and handed over to UEC at the end of the assignment, as the ToR requires. Its i18n plugin models EN/AR as first-class locales of the same entry, and draft & publish gives editors a review step. | +| Database | **PostgreSQL 16** | Strapi's recommended production database; proper UTF‑8 and JSON support for bilingual content. | +| Runtime | **Node 22**, **pnpm 9** workspace | One lockfile, two applications, one `pnpm install`. | + +The web application never talks to the database. It reads the Strapi REST API from its +**loaders on the server**, so the API token never reaches the browser, and every page falls +back to bundled draft copy if the CMS is unreachable — the site stays up when Strapi is down. + +--- + +## Repository layout + +``` +sharp-er/ +├── apps/ +│ ├── web/ React Router 8 front end (SSR) +│ │ ├── app/ +│ │ │ ├── root.tsx HTML shell: , canonical + hreflang, nav, footer +│ │ │ ├── routes.ts Route config: "/" redirect, ":lang" layout, the ten sections +│ │ │ ├── routes/ One module per page (+ shared _page-kit / _shared helpers) +│ │ │ ├── components/ui/ Button, Input, Card, Navigation, Footer, LanguageSwitcher +│ │ │ ├── styles/ Design tokens — primitives.css, semantic.css, typography.css +│ │ │ ├── i18n/ Locale config, message catalogues (en.json / ar.json), formatters +│ │ │ └── lib/ strapi.ts (API client), seo.ts, env.server.ts, route-slugs.ts +│ │ ├── public/ +│ │ │ ├── assets/ Logos, EU emblem, partner logos, icons +│ │ │ └── fonts/ Licensed woff2 files — NOT committed, see FONTS below +│ │ └── Dockerfile +│ │ +│ └── cms/ Strapi 5 CMS +│ ├── config/ server, admin, database, middlewares (CORS), plugins (i18n, upload) +│ ├── src/ +│ │ ├── api/ One folder per content type (schema, controller, routes, service) +│ │ ├── components/ Reusable field groups (hero, cta, seo, blocks/*) +│ │ └── bootstrap/ Boot-time reconciliation: locales + public permissions +│ ├── .env.example Every Strapi variable, documented +│ └── Dockerfile +│ +├── docs/ SOURCE MATERIAL — read only, never edited +├── scripts/ setup-db.sh, generate-secrets.sh, check-prereqs.sh +├── docker-compose.yml postgres + cms + web +├── .env.example Root template: docker compose and the web server read this +├── CLAUDE.md Working brief for AI agent sessions +└── pnpm-workspace.yaml +``` + +--- + +## Choosing a setup path + +Both paths give you the same three processes. Pick one; you do not need both. + +| | **Path A — manual** | **Path B — Docker** | +| --- | --- | --- | +| You install | Node 22, pnpm 9, PostgreSQL 16 | Docker Engine + Compose v2 | +| Best for | day-to-day development (hot reload, fast rebuilds) | trying the project out, review builds, servers | +| Web | `http://localhost:5173` (dev server) | `http://localhost:3000` (production build) | +| CMS | `http://localhost:1337/admin` | `http://localhost:1337/admin` | +| Database | your local PostgreSQL, port 5432 | container, published on port **5433** | +| Time to first run | ~5 minutes | ~10 minutes (image build) | + +Check your machine before you start: + +```bash +./scripts/check-prereqs.sh # both paths +./scripts/check-prereqs.sh manual # or just one +./scripts/check-prereqs.sh docker +``` + +--- + +## Path A — manual setup + +### 1. Prerequisites + +| Tool | Version | Notes | +| --- | --- | --- | +| **Node.js** | **>= 22.22.0** | React Router 8 will not run on anything older — this is a hard floor, not a recommendation. `.nvmrc` pins **22.23.2**, the version this repository is developed on. Strapi 5 accepts Node 20 – 26, so 22 satisfies both. | +| **pnpm** | **9.x** (repo pins `pnpm@9.12.3`) | npm and yarn will not work: the workspace, the lockfile and the hoisted `node_modules` layout in `.npmrc` are all pnpm's. | +| **PostgreSQL** | **16** (server + client tools) | Client tools give you `psql`, `createdb` and `pg_isready`. | +| **Git** | any recent version | | + +```bash +# Node — with nvm, the pinned version is one command +nvm install "$(cat .nvmrc)" && nvm use + +# pnpm — corepack ships with Node +corepack enable && corepack prepare pnpm@9.12.3 --activate + +# PostgreSQL 16 on Debian / Ubuntu +sudo apt install postgresql-16 postgresql-client-16 +sudo systemctl enable --now postgresql + +# macOS with Homebrew +brew install node@22 pnpm postgresql@16 && brew services start postgresql@16 +``` + +Verify: `node -v` → `v22.22.0` or newer, `pnpm -v` → `9.x`, `psql --version` → `16.x`. + +### 2. Install dependencies + +```bash +git clone sharp-er +cd sharp-er +pnpm install # installs BOTH apps from the single root lockfile +``` + +Always run `pnpm install` from the repository root. `.npmrc` sets `node-linker=hoisted` +because Strapi's plugin resolution expects a flat `node_modules`; installing inside +`apps/cms` or `apps/web` on its own produces a tree neither application can boot from. + +### 3. Create the database + +The database is called `sharper_cms` and is owned by the role Strapi connects as +(`postgres` by default). + +```bash +./scripts/setup-db.sh +``` + +The script is idempotent — it creates the database only if it is missing, never drops +anything, and finishes by proving it can connect and that the role may create tables. +It reads its defaults from `.env` and `apps/cms/.env`, and takes +`--name`, `--user`, `--password`, `--host`, `--port` overrides (`--help` for the details). + +Doing it by hand is two commands: + +```bash +createdb -h 127.0.0.1 -U postgres sharper_cms +psql -h 127.0.0.1 -U postgres -d sharper_cms -c "SELECT current_database(), version();" +``` + +
+If psql asks for a password you do not have (fresh Ubuntu install) + +A newly installed PostgreSQL has a `postgres` role with no password set, reachable only +over the local socket. Set one, then use it as `DATABASE_PASSWORD` in `apps/cms/.env`: + +```bash +sudo -u postgres psql -c "ALTER USER postgres PASSWORD 'postgres';" +``` + +For a shared or production machine, create a dedicated role instead: + +```bash +sudo -u postgres psql -c "CREATE ROLE sharper WITH LOGIN PASSWORD 'a-strong-password' CREATEDB;" +./scripts/setup-db.sh --user sharper --password 'a-strong-password' +``` +
+ +### 4. Environment files + +Two files, with different jobs: + +| File | Read by | Required in manual mode | +| --- | --- | --- | +| `apps/cms/.env` | Strapi (it loads its own `.env`) | **yes** | +| `.env` (root) | `docker compose`, and optionally the web server | no — the code defaults already point at `localhost` | + +```bash +# CMS: copy the fully documented template, then fill in the six secrets +cp apps/cms/.env.example apps/cms/.env +./scripts/generate-secrets.sh --write apps/cms/.env + +# Set the database credentials you used in step 3 +# DATABASE_CLIENT=postgres +# DATABASE_HOST=127.0.0.1 +# DATABASE_PORT=5432 +# DATABASE_NAME=sharper_cms +# DATABASE_USERNAME=postgres +# DATABASE_PASSWORD=... +``` + +`scripts/generate-secrets.sh` fills `APP_KEYS`, `API_TOKEN_SALT`, `ADMIN_JWT_SECRET`, +`JWT_SECRET`, `TRANSFER_TOKEN_SALT` and `ENCRYPTION_KEY` with 32 random bytes each. It never +overwrites a value that is already set unless you pass `--force`, so it is safe to re-run. +Without `--write` it just prints a block you can paste. By hand: +`openssl rand -base64 32`, once per key (`APP_KEYS` takes at least two, comma separated). + +The web application reads `STRAPI_URL`, `STRAPI_TOKEN` and `SITE_URL` from its **process +environment**. Nothing loads the root `.env` for you in manual mode, and you do not need it: +`STRAPI_URL` already defaults to `http://localhost:1337`, and with `SITE_URL` unset the +canonical URL is derived from the incoming request. To override them anyway: + +```bash +cp .env.example .env +set -a; . ./.env; set +a # export it into the current shell +pnpm dev +``` + +### 5. Run both applications + +```bash +pnpm dev # web + cms together, output interleaved and prefixed +``` + +or, in two terminals: + +```bash +pnpm dev:cms # Strapi -> http://localhost:1337 +pnpm dev:web # React Router -> http://localhost:5173 +``` + +The first `pnpm dev:cms` takes a minute: Strapi creates its tables, then the boot-time +reconciliation in `apps/cms/src/bootstrap/` runs. That step is idempotent and executes on +every boot; it + +- creates the `en` (default) and `ar` locales if they are missing, and +- grants the **Public** role `find` / `findOne` on the content types the site reads, and + `create` — and only `create` — on the three submission types (event registrations, + applications, contact messages), so nobody can read other people's personal data back out. + +You never have to click through the Users & Permissions screens after adding a content type; +restart the CMS and the grant is there. + +### 6. Create the first admin user + +Strapi has no seeded administrator. On first run, open + +**http://localhost:1337/admin** + +and complete the registration form — the first account created becomes the super admin. +Or do it from the command line: + +```bash +pnpm --filter cms exec strapi admin:create-user \ + --firstname="Site" --lastname="Admin" \ + --email="admin@example.org" --password="SomeStrongPassw0rd" +``` + +Passwords must be at least 8 characters and contain a number and an uppercase letter. + +### 7. Confirm it works + +| URL | Expected | +| --- | --- | +| `http://localhost:5173/` | 302 to `/en` or `/ar` depending on your `Accept-Language` | +| `http://localhost:5173/en` | English home page, `` | +| `http://localhost:5173/ar` | Arabic home page, ``, mirrored layout | +| `http://localhost:1337/admin` | Strapi admin panel | +| `http://localhost:1337/api/home-page?locale=en` | JSON (or `404` until an entry is published) | +| `http://localhost:1337/_health` | `204 No Content` | + +```bash +# The one check worth automating: is the SSR HTML right in the first byte? +curl -s http://localhost:5173/ar | head -c 200 +``` + +Then run the type checker once, so you know the baseline is clean: + +```bash +pnpm typecheck +``` + +--- + +## Path B — Docker + +Three containers: **postgres** (database), **cms** (Strapi), **web** (React Router). +Nothing else is installed on your machine — no Node, no pnpm, no PostgreSQL. + +### 1. Prerequisites + +- **Docker Engine 24+** (or Docker Desktop) with the **Compose v2** plugin. + Check with `docker compose version`; the standalone `docker-compose` binary is *not* + supported — this file uses `depends_on.condition`, which is Compose v2 syntax. +- Roughly 3 GB of disk for the images and 2 GB of RAM while they build. + +### 2. Configure + +```bash +cp .env.example .env +./scripts/generate-secrets.sh --write .env +``` + +`docker compose` reads the `.env` file sitting next to `docker-compose.yml` and substitutes +every `${VARIABLE}` in it. The six Strapi secrets are declared as `${VAR:?...}`, so a missing +value stops the stack straight away with a readable message instead of a Strapi crash loop. + +In Docker mode `apps/cms/.env` is **not used** — the container receives its environment from +`docker-compose.yml`. The root `.env` is the only file you edit. + +### 3. Start + +```bash +docker compose up -d --build # first run: builds both images, ~5-10 minutes +docker compose ps # STATUS column shows (healthy) when each is up +``` + +Start-up is ordered by health, not by luck: + +``` +postgres healthy -> cms healthy -> web + pg_isready GET /_health GET /en +``` + +`cms` waits for a database that is actually accepting connections (`pg_isready`), and `web` +waits for a CMS that answers `/_health`, so the very first page render already has content. +The health checks themselves are declared in `apps/cms/Dockerfile` and `apps/web/Dockerfile`, +which means `docker run` and any orchestrator inherit them too. + +Give the CMS up to 90 seconds on its first boot: it creates its tables, then runs the +locale and permission reconciliation described in Path A, step 5. + +```bash +docker compose logs -f # everything, following +docker compose logs -f cms # one service +docker compose logs --tail=50 web +``` + +### 4. Create the first admin user + +Open **http://localhost:1337/admin** and register — the first account is the super admin. +From the command line: + +```bash +docker compose exec cms pnpm exec strapi admin:create-user \ + --firstname="Site" --lastname="Admin" \ + --email="admin@example.org" --password="SomeStrongPassw0rd" +``` + +### 5. Confirm it works + +| URL | Expected | +| --- | --- | +| `http://localhost:3000/` | 302 to `/en` or `/ar` | +| `http://localhost:3000/en` and `/ar` | English and Arabic home pages | +| `http://localhost:1337/admin` | Strapi admin panel | +| `psql -h 127.0.0.1 -p 5433 -U postgres sharper_cms` | a database prompt | + +### What the compose file sets up + +| Service | Image | Host port | Persists in | Health check | +| --- | --- | --- | --- | --- | +| `postgres` | `postgres:16-alpine` | **5433** → 5432 | volume `sharper_postgres-data` | `pg_isready` | +| `cms` | built from `apps/cms/Dockerfile` | 1337 | volume `sharper_cms-uploads` (Media Library) | `GET /_health` | +| `web` | built from `apps/web/Dockerfile` | 3000 | nothing — stateless | `GET /en` | + +The database is published on **5433** on purpose, so it cannot collide with a PostgreSQL +already listening on your host's 5432. Inside the compose network the CMS still reaches it as +`postgres:5432`. Change the published port with `POSTGRES_PUBLISHED_PORT` in `.env`. + +Both images build from the **repository root** as their context, because the pnpm lockfile +lives there. They are not size-optimised: the CMS image keeps its dev dependencies, because +Strapi's CLI resolves its TypeScript toolchain when it starts. + +### Images from the CMS + +Uploaded media is served by Strapi, and the web server turns a relative `/uploads/x.jpg` +into an absolute URL by prefixing `STRAPI_URL`. Inside Docker that value is `http://cms:1337`, +a hostname Docker resolves but your browser does not — so pictures uploaded through the Media +Library will not load until it can. One line in your hosts file fixes it, permanently: + +```bash +echo "127.0.0.1 cms" | sudo tee -a /etc/hosts # Windows: C:\Windows\System32\drivers\etc\hosts +``` + +`cms` then resolves to `127.0.0.1`, where compose publishes the CMS on port 1337, so the one +URL works from the browser and from inside the compose network alike. Everything else — +pages, text, layout, and the site's own images in `apps/web/public/assets/` — works without +this step. + +In production this problem disappears: the CMS has a real hostname +(`https://cms.sharper.ly`), and `STRAPI_URL` is set to it. + +### Everyday commands + +```bash +docker compose ps # status and health +docker compose restart cms # restart one service +docker compose exec cms sh # shell inside the CMS container +docker compose exec postgres psql -U postgres sharper_cms + +docker compose up -d --build web # rebuild after changing app code +docker compose build --no-cache cms # rebuild ignoring the layer cache +``` + +Source code is **not** bind-mounted: the images contain a production build, so a code change +needs a rebuild. For day-to-day development use Path A, or run only the database in Docker +(`docker compose up -d postgres`) and the two applications with `pnpm dev`. + +### Stopping and tearing down + +| Command | Containers | Database + uploads | +| --- | --- | --- | +| `docker compose stop` | stopped, kept | kept | +| `docker compose down` | removed | **kept** (named volumes survive) | +| `docker compose down -v` | removed | **deleted — irreversible** | +| `docker compose down --rmi local` | removed, images too | kept | + +`down -v` is how you start from an empty database — every entry, every uploaded file and +the admin account are gone. Take a dump first if you are unsure: + +```bash +# Back up +docker compose exec -T postgres pg_dump -U postgres sharper_cms > backup-$(date +%F).sql + +# Restore into a fresh stack +docker compose exec -T postgres psql -U postgres sharper_cms < backup-2026-01-31.sql + +# Uploaded media +docker run --rm -v sharper_cms-uploads:/data -v "$PWD":/backup alpine \ + tar czf /backup/uploads-$(date +%F).tar.gz -C /data . +``` + +--- + +## Available scripts + +**Repository root** (`package.json`) — these fan out across the workspace: + +| Command | Does | +| --- | --- | +| `pnpm dev` | runs `web` and `cms` in parallel, streaming both logs | +| `pnpm dev:web` | React Router dev server with HMR → `http://localhost:5173` | +| `pnpm dev:cms` | `strapi develop` (auto-reloads on schema changes) → `http://localhost:1337` | +| `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 typecheck` | the web app's `react-router typegen && tsc` (the CMS has no `typecheck` script — its TypeScript is checked by `strapi build`) | + +**`apps/web`** — run with `pnpm --filter web