diff --git a/.github/ISSUE_TEMPLATE/learning-unit.md b/.github/ISSUE_TEMPLATE/learning-unit.md new file mode 100644 index 000000000..d4629a206 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/learning-unit.md @@ -0,0 +1,97 @@ +--- +name: Learning Unit +about: Standard workflow for producing a new learning unit +title: LU XX +labels: e-learning, Learning Unit, content +--- + +- [ ] Autorenvertrag + - [ ] Vertrag an die Autor:innen schicken + - [ ] Unterschriebenen Vertrag erhalten + - [ ] An Ute Möntnich schicken, fuer Vorstand zum unterschreiben + - [ ] Unterschriebenen Vertrag zurück an die Autor:innen schicken +- [ ] Bezahlung (1. Hälfte) + - [ ] Rechnung 1 erhalten + - [ ] Rechnung 1 bezahlt +- [ ] Onboarding-Anruf mit Autor:innen +- [ ] Onboarding-Email an Autor:innen (mit Dropbox-Links) +- [ ] Email an Autor:innen: Schickt uns CV, Profilbild, Affiliation +- [ ] 1. Draft von Autor:innen erhalten (Word oder Dropbox) +- [ ] 1. Inhaltliches Review (Niklas + nn) (Kommentare im Dokument + Email) +- [ ] 2. Draft von Autor:innen erhalten +- [ ] 2. Inhaltiches Review (Niklas + nn) +- [ ] 2. Draft erhalten, alle sind zufrieden +- [ ] Draft an Language Editing schicken (mit Frist: TBD) +- [ ] Final Draft kommt zurueck +- [ ] Animated Podcasts vorbereiten + - [ ] Email an Autor:innen: Nehmt eure Audios auf (mit Frist: TBD) + - [ ] Audiofile Rohversion da + - [ ] Storyboards erstellen + - [ ] Storyboards abnehmen + - [ ] Icon-Auftrag erteilen (Icons, die in dem Storyboard gebraucht werden) + - [ ] Zusätzliche Assets sammeln (Bilder, Videos, etc; Rechte beachten) + - [ ] Zusätzliche Assets bearbeiten falls nötig / erlaubt + - [ ] Icons kommen zurueck +- [ ] Animated Podcasts erstellen + - [ ] Dropbox-Order erstellen (Storyboard, Audio-Dateien, Icons, Assets, Autoren-Namen, Autoren-Bilder, Script fuer Kontext) + - [ ] Ordner an Form-Art schicken / Auftrag erteilen mit Frist + - [ ] Video + Cover-Bild kommt zurück (in editierbarem Format für weiter unten) + - [ ] Abnahme durch uns, wenn nicht: Monita + - [ ] Untertitel erstellen (Premiere) + - [ ] Finale Video-Dateien exportieren + - [ ] Video + Cover-Bild + Untertitel hochladen auf VideoStream Server +- [ ] Assets im Text + - [ ] Fotos + - [ ] Nutzungs- und Bearbeitungsrechte klären + - [ ] Bild nachbearbeiten falls nötig && erlaubt + - [ ] Fertiges Bild in Dropbox Ablegen + - [ ] Korrekte Lizenzinformationen in die Permissions-Tabelle schreiben + - [ ] Unsere Grafiken (PRIF) (Landkarten, Diagramme) + - [ ] Datengrundlagen für Grafiken einholen + - [ ] Nutzungsrechte der Datengrundlage klären + - [ ] Externe Grafiken + - [ ] Skizzen prüfen + - [ ] Dropbox-Paket mit allen Skizzen/Notitzen vorberreiten + - [ ] Email an Agentur: Hier ist das Paket, macht Grafiken, Frist + - [ ] Grafiken kommen zurueck + - [ ] Email an Autoren: Gebt die Grafiken frei! + - [ ] Grafiken sind fertig und bereit + - [ ] Fertige Grafiken in Dropbox speichern fuer spaeter +- MDX Create + - [ ] Branch: Feat/LuXX + - [ ] Neuer Order für LU + - [ ] `index.mdx`: + - [ ] Titel + - [ ] Autor:innen (Namen, Fotos, CV, Institution) + - [ ] Summary + - [ ] Learning Objectives + - [ ] Cover-Bild (+ credits) + - [ ] Kapitel-MDX + - [ ] Titel + - [ ] Intro + - [ ] Inhalte + - [ ] Assets einbauen: (Karten, Charts, Bilder, Interaktive Komponenten, Timelines, Zitate, Externe Grafiken) + - [ ] Videos einbauen + - [ ] ggf. Glossar-Begriffe einbetten und Definitionen hinterlegen (`terms.json`): ggf. Review durch Niklas + - Einheit fertig, branch deploy erzeugen + - Email an Autor: Hier ist dein Branch Deploy, bitte abnehmen (Frist) + - [ ] Freigabe liegt vor? +- Certificate Section aktualisieren + - [ ] Neues Quiz anlegen (versteckt), mit neuen Fraugen fuer die neuen Inhalte + - [ ] Scores vom alten aufs neue Quiz übertragen + - [ ] Certifikat anpassen (auf das neue Quiz zeigen, aber alte Quizzes weiterhin gelten lassen) +- [ ] Veröffentlichung + - [ ] Text an PrÖA für Twitter und Email: Wir gehen dann und dann live! + - Email an Autor:innen: Wir gehen dann und dann live! + - `git merge feat/luXX` + - [ ] Moodle-Upload + - [ ] Autorin Bild + Text einfügen + - [ ] Inhaltsverzeichnis in Lernbar aktualisieren (siehe Dokumentation, ist glaubt Max, eine HTML-Datei) + - [ ] Startseite: Anzahl Experts + Videos aktualisieren + - [ ] In Moodle: LU in "new" Kategorie einfuegen + - [ ] Beschreibungstext der LU + - [ ] Redirect in .htaccess aktualisieren +- Post-Launch + - [ ] Email an Autor:in: Ist live, schick Rechnung 2! + - [ ] Rechnung 2 erhalten + - [ ] Rechnung 2 bezahlt diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a7415ebc1..ca6bb4ff2 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -21,3 +21,6 @@ updates: retext: patterns: - 'retext*' + testing-library: + patterns: + - '@testing-library*' diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7f43993f1..db08478c4 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -2,9 +2,9 @@ name: "CodeQL" on: push: - branches: [ "main" ] + branches: [ "main", "feature/*"] pull_request: - branches: [ "main" ] + branches: [ "main", "feature/*" ] workflow_dispatch: jobs: diff --git a/.github/workflows/elearning-test.yml b/.github/workflows/elearning-test.yml new file mode 100644 index 000000000..993b538b8 --- /dev/null +++ b/.github/workflows/elearning-test.yml @@ -0,0 +1,22 @@ +name: '[eLearning] Run tests' + +on: + pull_request: + paths: + - './e-learning/**' + workflow_dispatch: +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - name: Install dependencies + working-directory: ./e-learning/ + run: npm install + - name: Run tests + working-directory: ./e-learning/ + run: npm run test diff --git a/.github/workflows/elearning-update-treaties.yml b/.github/workflows/elearning-update-treaties.yml index 783cfd8ac..16da18192 100644 --- a/.github/workflows/elearning-update-treaties.yml +++ b/.github/workflows/elearning-update-treaties.yml @@ -14,20 +14,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - with: - path: main - - uses: actions/checkout@v3 - with: - path: updates - uses: actions/setup-node@v3 with: node-version: 18 cache: npm - cache-dependency-path: | - main/package-lock.json - updates/package-lock.json - name: Run update script - run: ${GITHUB_WORKSPACE}/main/scripts/updateTreaties.sh + run: ${GITHUB_WORKSPACE}/scripts/updateTreaties.sh env: BRANCH_NAME: update-treaty-participants GITHUB_ACTOR: ${{ github.actor }} diff --git a/.github/workflows/shared-test.yml b/.github/workflows/shared-test.yml new file mode 100644 index 000000000..d81651bec --- /dev/null +++ b/.github/workflows/shared-test.yml @@ -0,0 +1,20 @@ +name: '[shared] Run tests' + +on: + pull_request: + workflow_dispatch: +permissions: + contents: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-node@v3 + - name: Install dependencies + working-directory: ./shared/ + run: npm install + - name: Run tests + working-directory: ./shared/ + run: npm run test diff --git a/.gitignore b/.gitignore index 197e8326a..3c74bacf2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ -node_modules -.netlify -tmp +node_modules +.netlify +tmp +.DS_Store diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..66eb93e0d --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,4 @@ +{ + "prisma-smart-formatter.typescript.defaultFormatter": "esbenp.prettier-vscode", + "prisma-smart-formatter.typescriptreact.defaultFormatter": "esbenp.prettier-vscode" +} diff --git a/README.md b/README.md index 5bb4c1ff2..edf0f66cc 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ This is a monorepo for all our React-based editorial products. ## Deployment Status -| Site | URL | Status | -| ----------- | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| eLearning | [eunpdc-elearning.netlify.app](https://eunpdc-elearning.netlify.app/) | [![Netlify Status](https://api.netlify.com/api/v1/badges/be127c78-15e4-457f-8880-078ca5f1128c/deploy-status)](https://app.netlify.com/sites/eunpdc-elearning/deploys) [![[eLearning] Build and Deploy](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-deploy.yml/badge.svg)](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-deploy.yml) [![[eLearning] Update Treaties](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-update-treaties.yml/badge.svg)](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-update-treaties.yml) [![[eLearning] Check Markdown Links](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-check-links.yml/badge.svg)](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-check-links.yml) | -| New Work | [leibniz-nw.netlify.app](https://leibniz-nw.netlify.app/) | [![Netlify Status](https://api.netlify.com/api/v1/badges/a9e50b5c-a39d-4bd7-9324-bf20958b2ecf/deploy-status)](https://app.netlify.com/sites/leibniz-nw/deploys) [![[New Work] Check Markdown Links](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/new-work-check-links.yml/badge.svg)](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/new-work-check-links.yml) | -| PRIF Review | [review.prif.org](https://review.prif.org) | [![Netlify Status](https://api.netlify.com/api/v1/badges/61f8dfa0-3e12-4cb2-95ee-128c928efd25/deploy-status)](https://app.netlify.com/sites/prif-review/deploys) [![[PRIF Review] Check Markdown Links](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/prif-review-check-links.yml/badge.svg?branch=main)](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/prif-review-check-links.yml) | -| CNTR | TBD | TBD | +| Site | URL | Status | +| ------------ | --------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| eLearning | [eunpdc-elearning.netlify.app](https://eunpdc-elearning.netlify.app/) | [![Netlify Status](https://api.netlify.com/api/v1/badges/be127c78-15e4-457f-8880-078ca5f1128c/deploy-status)](https://app.netlify.com/sites/eunpdc-elearning/deploys) [![[eLearning] Build and Deploy](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-deploy.yml/badge.svg)](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-deploy.yml) [![[eLearning] Update Treaties](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-update-treaties.yml/badge.svg)](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-update-treaties.yml) [![[eLearning] Check Markdown Links](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-check-links.yml/badge.svg)](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/elearning-check-links.yml) | +| New Work | [leibniz-nw.netlify.app](https://leibniz-nw.netlify.app/) | [![Netlify Status](https://api.netlify.com/api/v1/badges/a9e50b5c-a39d-4bd7-9324-bf20958b2ecf/deploy-status)](https://app.netlify.com/sites/leibniz-nw/deploys) [![[New Work] Check Markdown Links](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/new-work-check-links.yml/badge.svg)](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/new-work-check-links.yml) | +| PRIF Review | [review.prif.org](https://review.prif.org) | [![Netlify Status](https://api.netlify.com/api/v1/badges/61f8dfa0-3e12-4cb2-95ee-128c928efd25/deploy-status)](https://app.netlify.com/sites/prif-review/deploys) [![[PRIF Review] Check Markdown Links](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/prif-review-check-links.yml/badge.svg?branch=main)](https://github.com/Peace-Research-Institute-Frankfurt/websites/actions/workflows/prif-review-check-links.yml) | +| CNTR Monitor | [monitor.cntrarmscontrol.org](https://monitor.cntrarmscontrol.org/) | [![Netlify Status](https://api.netlify.com/api/v1/badges/e20bc7bc-3b8f-42e0-bcfc-d36721be2bf0/deploy-status)](https://app.netlify.com/sites/cntr-monitor/deploys) | diff --git a/cntr-monitor/.gitignore b/cntr-monitor/.gitignore new file mode 100644 index 000000000..6137cae05 --- /dev/null +++ b/cntr-monitor/.gitignore @@ -0,0 +1,6 @@ +.cache/ +public +fonts/ +documentation/ +.env +.netlify diff --git a/cntr-monitor/content/de/issues/2024/authors/test-author.mdx b/cntr-monitor/content/de/issues/2024/authors/test-author.mdx new file mode 100644 index 000000000..7090ac7af --- /dev/null +++ b/cntr-monitor/content/de/issues/2024/authors/test-author.mdx @@ -0,0 +1,6 @@ +--- +name: Test Author +author_id: test-author +--- + +This is the author's bio diff --git a/cntr-monitor/content/de/issues/2024/index.mdx b/cntr-monitor/content/de/issues/2024/index.mdx new file mode 100644 index 000000000..0519daa1b --- /dev/null +++ b/cntr-monitor/content/de/issues/2024/index.mdx @@ -0,0 +1,8 @@ +--- +title: CNTR Monitor 2024 +year: 2024 +order: 0 +color: '#ef7d23' +--- + +Issue intro copy (DE) diff --git a/cntr-monitor/content/de/issues/2024/posts/1_test.mdx b/cntr-monitor/content/de/issues/2024/posts/1_test.mdx new file mode 100644 index 000000000..1a6a33c58 --- /dev/null +++ b/cntr-monitor/content/de/issues/2024/posts/1_test.mdx @@ -0,0 +1,7 @@ +--- +order: 1 +title: Test Post +color: rgb(0, 131, 181) +--- + +This is the post copy (DE) diff --git a/cntr-monitor/content/de/pages/accessibility.mdx b/cntr-monitor/content/de/pages/accessibility.mdx new file mode 100644 index 000000000..0d72b4e9f --- /dev/null +++ b/cntr-monitor/content/de/pages/accessibility.mdx @@ -0,0 +1,6 @@ +--- +title: Barrierefreiheit +order: 2 +--- + +TODO A11y statement diff --git a/cntr-monitor/content/de/pages/legal.mdx b/cntr-monitor/content/de/pages/legal.mdx new file mode 100644 index 000000000..56e60df3a --- /dev/null +++ b/cntr-monitor/content/de/pages/legal.mdx @@ -0,0 +1,6 @@ +--- +order: 4 +title: Impressum +--- + +TODO Legal (DE) diff --git a/cntr-monitor/content/de/pages/privacy.mdx b/cntr-monitor/content/de/pages/privacy.mdx new file mode 100644 index 000000000..d1f470c68 --- /dev/null +++ b/cntr-monitor/content/de/pages/privacy.mdx @@ -0,0 +1,6 @@ +--- +order: 1 +title: Datenschutz +--- + +TODO Privacy diff --git a/cntr-monitor/content/en/issues/2024/authors/test-author.mdx b/cntr-monitor/content/en/issues/2024/authors/test-author.mdx new file mode 100644 index 000000000..7090ac7af --- /dev/null +++ b/cntr-monitor/content/en/issues/2024/authors/test-author.mdx @@ -0,0 +1,6 @@ +--- +name: Test Author +author_id: test-author +--- + +This is the author's bio diff --git a/cntr-monitor/content/en/issues/2024/index.mdx b/cntr-monitor/content/en/issues/2024/index.mdx new file mode 100644 index 000000000..dbfdb6115 --- /dev/null +++ b/cntr-monitor/content/en/issues/2024/index.mdx @@ -0,0 +1,7 @@ +--- +title: CNTR Monitor 2024 +year: 2024 +order: 0 +--- + +Issue intro copy (EN) diff --git a/cntr-monitor/content/en/issues/2024/posts/1_test.mdx b/cntr-monitor/content/en/issues/2024/posts/1_test.mdx new file mode 100644 index 000000000..6d832ec74 --- /dev/null +++ b/cntr-monitor/content/en/issues/2024/posts/1_test.mdx @@ -0,0 +1,7 @@ +--- +order: 0 +title: Test Post +color: rgb(0, 131, 181) +--- + +This is the post copy (EN) diff --git a/cntr-monitor/content/en/pages/accessibility.mdx b/cntr-monitor/content/en/pages/accessibility.mdx new file mode 100644 index 000000000..da1afea32 --- /dev/null +++ b/cntr-monitor/content/en/pages/accessibility.mdx @@ -0,0 +1,6 @@ +--- +title: Accessibility +order: 2 +--- + +TODO A11y Statement (DE) diff --git a/cntr-monitor/content/en/pages/legal.mdx b/cntr-monitor/content/en/pages/legal.mdx new file mode 100644 index 000000000..1f0807c4c --- /dev/null +++ b/cntr-monitor/content/en/pages/legal.mdx @@ -0,0 +1,6 @@ +--- +order: 4 +title: Legal +--- + +TODO Legal (EN) diff --git a/cntr-monitor/content/en/pages/privacy.mdx b/cntr-monitor/content/en/pages/privacy.mdx new file mode 100644 index 000000000..2f6be45e2 --- /dev/null +++ b/cntr-monitor/content/en/pages/privacy.mdx @@ -0,0 +1,6 @@ +--- +order: 1 +title: Privacy +--- + +TODO Privacy (DE) diff --git a/cntr-monitor/gatsby-browser.js b/cntr-monitor/gatsby-browser.js new file mode 100644 index 000000000..cca874e58 --- /dev/null +++ b/cntr-monitor/gatsby-browser.js @@ -0,0 +1,6 @@ +import React from 'react' +import { EmbedChoicesProvider } from './src/context/EmbedChoicesContext' + +export const wrapRootElement = ({ element }) => { + return {element} +} diff --git a/cntr-monitor/gatsby-config.mjs b/cntr-monitor/gatsby-config.mjs new file mode 100644 index 000000000..cbe697c10 --- /dev/null +++ b/cntr-monitor/gatsby-config.mjs @@ -0,0 +1,90 @@ +import path from 'path' +import { fileURLToPath } from 'url' +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) +import remarkGfm from 'remark-gfm' +import adapter from 'gatsby-adapter-netlify' + +const config = { + siteMetadata: { + siteUrl: 'https://monitor.cntrarmscontrol.org/', + title: 'CNTR Monitor', + description: '', + siteTwitter: '@PRIF_org', + authorTwitter: '@PRIF_org', + image: { + src: '/social-image.png', + alt: 'image alt', + }, + }, + adapter: adapter.default(), + plugins: [ + 'gatsby-plugin-image', + 'gatsby-transformer-json', + 'gatsby-transformer-sharp', + 'gatsby-plugin-sass', + 'gatsby-transformer-csv', + 'gatsby-plugin-react-svg', + { + resolve: 'gatsby-plugin-sharp', + options: { + defaults: { + formats: ['auto', 'webp', 'avif'], + }, + }, + }, + { + resolve: 'gatsby-plugin-manifest', + options: { + icon: 'src/images/favicon.svg', + }, + }, + { + resolve: 'gatsby-source-filesystem', + options: { + name: 'content', + path: `${__dirname}/content/`, + }, + }, + { + resolve: 'gatsby-plugin-mdx', + options: { + mdxOptions: { + remarkPlugins: [remarkGfm], + }, + }, + }, + { + resolve: 'gatsby-source-filesystem', + options: { + path: `${__dirname}/locales`, + name: 'locale', + }, + }, + { + resolve: 'gatsby-plugin-react-i18next', + options: { + localeJsonSourceName: 'locale', + languages: ['en', 'de'], + defaultLanguage: 'de', + fallbackLanguage: 'de', + redirect: false, + i18nextOptions: { + interpolation: { + escapeValue: false, + }, + keySeparator: false, + nsSeparator: false, + }, + pages: [ + { + matchPath: '/:lang?/:slug+', // Match all paths except the top-level index + getLanguageFromPath: true, + }, + ], + }, + }, + ], +} + +export default config diff --git a/cntr-monitor/gatsby-node.js b/cntr-monitor/gatsby-node.js new file mode 100644 index 000000000..84f9d5a4d --- /dev/null +++ b/cntr-monitor/gatsby-node.js @@ -0,0 +1,197 @@ +const { createFilePath } = require(`gatsby-source-filesystem`) +const slug = require('slug') +slug.extend({ '—': '-', '–': '-' }) +const path = require('path') + +const locales = ['en', 'de'] +const defaultLocale = 'de' + +function findTranslationNodes(n, nodes) { + const locale = n.childMdx.fields.locale + const nodeDir = n.relativeDirectory.replace(`${locale}/`, '') + const translationTargets = locales.filter((el) => el !== locale) + + const translationCandidates = nodes.filter((el) => { + const dir = el.relativeDirectory.replace(`${el.childMdx.fields.locale}/`, '') + return dir === nodeDir + }) + + const rootFileName = n.base + + const translations = translationCandidates.filter((el) => { + const foundIndex = translationTargets.findIndex((t) => { + return rootFileName === el.base && el.childMdx.fields.locale !== locale + }) + return foundIndex !== -1 + }) + + return translations +} + +exports.createPages = async function ({ actions, graphql }) { + const { data } = await graphql(` + query { + posts: allFile(filter: { sourceInstanceName: { eq: "content" }, extension: { eq: "mdx" } }) { + nodes { + id + base + relativeDirectory + childMdx { + frontmatter { + title + } + fields { + slug + locale + } + frontmatter { + title + } + internal { + contentFilePath + } + } + } + } + } + `) + + const posts = data.posts.nodes.filter((el) => { + return el.relativeDirectory.includes('/posts') + }) + const pages = data.posts.nodes.filter((el) => { + return el.relativeDirectory.includes('/pages') + }) + const issues = data.posts.nodes.filter((el) => { + return el.base === 'index.mdx' + }) + + console.log(`${issues.length} issues found`) + console.log(`${posts.length} posts found`) + console.log(`${pages.length} pages found`) + + // Create issue pages + issues.forEach((node) => { + const locale = node.childMdx.fields.locale + const postTemplate = require.resolve(`./src/components/Issue.js`) + const translations = findTranslationNodes(node, issues) + const translationIds = translations.map((t) => t.id) + const year = node.relativeDirectory.replace(/(.{2})\/(issues)\//g, '') + let path = `${locale && locale !== defaultLocale ? locale : ''}/${year}/` + + actions.createPage({ + path: path, + component: `${postTemplate}?__contentFilePath=${node.childMdx.internal.contentFilePath}`, + context: { id: node.id, postsDirectory: `${node.relativeDirectory}/posts`, translations: translationIds }, + }) + }) + + // Create post pages + posts.forEach((post) => { + const locale = post.childMdx.fields.locale + const postTemplate = require.resolve(`./src/components/Post.js`) + const translations = findTranslationNodes(post, posts) + const translationIds = translations.map((t) => t.id) + const year = post.relativeDirectory.replace(/(.{2})\/(issues)\//g, '').replace('/posts', '') + let path = `${locale && locale !== defaultLocale ? locale : ''}/${year}/${post.childMdx.fields.slug}` + + const issue = issues.find((node) => { + return node.relativeDirectory === post.relativeDirectory.replace('/posts', '') + }) + + actions.createPage({ + path: path, + component: `${postTemplate}?__contentFilePath=${post.childMdx.internal.contentFilePath}`, + context: { id: post.id, translations: translationIds, issueId: issue.id }, + }) + }) + + pages.forEach((node) => { + const locale = node.childMdx.fields.locale + const postTemplate = require.resolve(`./src/components/Page.js`) + const translations = findTranslationNodes(node, pages) + const translationIds = translations.map((t) => t.id) + let path = `${locale && locale !== defaultLocale ? locale : ''}/${node.childMdx.fields.slug}` + actions.createPage({ + path: path, + component: `${postTemplate}?__contentFilePath=${node.childMdx.internal.contentFilePath}`, + context: { id: node.id, translations: translationIds }, + }) + }) +} + +exports.onCreateNode = ({ node, actions, createNodeId, getNode }) => { + // Create auuthor nodes + if (node.internal.type === 'Mdx' && node.internal.contentFilePath.indexOf('authors') !== -1) { + actions.createNode({ + id: createNodeId(`author-${node.id}`), + parent: node.id, + author_id: node.frontmatter.author_id, + frontmatter: node.frontmatter, + internal: { + type: `Author`, + contentDigest: node.internal.contentDigest, + }, + }) + } + if (node.internal.type === 'Mdx') { + let nodeLocale = '' + locales.forEach((locale) => { + if (node.internal.contentFilePath.indexOf(`/${locale}/`) !== -1) { + nodeLocale = locale + } + }) + actions.createNodeField({ node, name: 'locale', value: nodeLocale }) + + let path = createFilePath({ node, getNode }) + if (node.frontmatter.title) path = slug(node.frontmatter.title) + actions.createNodeField({ node, name: 'slug', value: path }) + } +} + +exports.createSchemaCustomization = async ({ getNode, getNodesByType, pathPrefix, reporter, cache, actions, schema }) => { + const { createTypes } = actions + + const typeDefs = ` + type Author implements Node { + name: String + image: String + image_alt: String + bio: String + } + type PostFrontmatter { + authors: [Author] + intro: String + teaser: String + eyebrow: String + title: String + color: String + color_secondary: String + } + type Mdx { + frontmatter: PostFrontmatter + } + ` + createTypes(typeDefs) +} + +exports.onCreateWebpackConfig = ({ actions, getConfig }) => { + const cfg = getConfig() + cfg.resolve.alias = { + ...cfg.resolve.alias, + '@shared': path.resolve(__dirname, '../shared'), + } + cfg.module.rules = [ + ...cfg.module.rules, + { + test: /\.csv$/, + loader: 'csv-loader', + options: { + dynamicTyping: true, + header: true, + skipEmptyLines: true, + }, + }, + ] + actions.replaceWebpackConfig(cfg) +} diff --git a/cntr-monitor/gatsby-ssr.js b/cntr-monitor/gatsby-ssr.js new file mode 100644 index 000000000..cca874e58 --- /dev/null +++ b/cntr-monitor/gatsby-ssr.js @@ -0,0 +1,6 @@ +import React from 'react' +import { EmbedChoicesProvider } from './src/context/EmbedChoicesContext' + +export const wrapRootElement = ({ element }) => { + return {element} +} diff --git a/cntr-monitor/latin-ext.txt b/cntr-monitor/latin-ext.txt new file mode 100644 index 000000000..6cd1003e3 --- /dev/null +++ b/cntr-monitor/latin-ext.txt @@ -0,0 +1,5 @@ +# Unicode ranges for Latin Extended +U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD +# German +U+20-5F,U+61-7D,U+A0,U+A7,U+A9,U+AB,U+BB,U+C4,U+D6,U+DC,U+DF,U+E4,U+F6,U+FC,U+2010-2011,U+2013-2014,U+2018,U+201A,U+201C,U+201E,U+2026,U+2030,U+20AC,U+20-5F,U+61-7D,U+A0,U+A7,U+A9,U+AB,U+BB,U+C4,U+D6,U+DC,U+DF,U+E4,U+F6,U+FC,U+2010-2011,U+2013-2014,U+2018,U+201A,U+201C,U+201E,U+2026,U+2030,U+20AC + diff --git a/cntr-monitor/locales/de/index.json b/cntr-monitor/locales/de/index.json new file mode 100644 index 000000000..327d8e8dc --- /dev/null +++ b/cntr-monitor/locales/de/index.json @@ -0,0 +1,13 @@ +{ + "Home": "Startseite", + "© PRIF and the authors": "© PRIF und die Autor*innen", + "Skip to content": "Zum Inhalt Springen", + "Previous": "Zurück", + "Next": "Weiter", + "Contents": "Inhalt", + "Editorial": "Editorial", + "Read more": "Weiterlesen", + "Less": "Schließen", + "Published on": "Veröffentlicht am", + "Site intro copy": "Site intro (DE)" +} diff --git a/cntr-monitor/locales/en/index.json b/cntr-monitor/locales/en/index.json new file mode 100644 index 000000000..c3e704146 --- /dev/null +++ b/cntr-monitor/locales/en/index.json @@ -0,0 +1,4 @@ +{ + "Previous": "Previous", + "Next": "Next" +} diff --git a/cntr-monitor/netlify.toml b/cntr-monitor/netlify.toml new file mode 100644 index 000000000..37ee6285d --- /dev/null +++ b/cntr-monitor/netlify.toml @@ -0,0 +1,2 @@ +[[plugins]] + package = "@netlify/plugin-gatsby" \ No newline at end of file diff --git a/cntr-monitor/package.json b/cntr-monitor/package.json new file mode 100644 index 000000000..86619a01e --- /dev/null +++ b/cntr-monitor/package.json @@ -0,0 +1,47 @@ +{ + "name": "cntr-monitor", + "version": "0.0.1", + "private": true, + "description": "Gatsby source for CNTR Monitor", + "license": "UNLICENSED", + "author": "Max Kohler", + "scripts": { + "build": "gatsby build", + "clean": "gatsby clean", + "develop": "gatsby develop", + "serve": "gatsby serve", + "start": "gatsby develop" + }, + "dependencies": { + "@mdx-js/react": "^2.3.0", + "@prif/shared": "0.0.1", + "colorjs.io": "^0.4.5", + "dotenv": "^16.4.2", + "gatsby": "^5.13.3", + "gatsby-adapter-netlify": "^1.1.3", + "gatsby-plugin-image": "^3.13.1", + "gatsby-plugin-manifest": "^5.13.1", + "gatsby-plugin-mdx": "^5.13.1", + "gatsby-plugin-react-i18next": "^3.0.1", + "gatsby-plugin-react-svg": "^3.3.0", + "gatsby-plugin-sass": "^6.13.1", + "gatsby-plugin-sharp": "^5.13.1", + "gatsby-plugin-sitemap": "^6.13.1", + "gatsby-source-filesystem": "^5.13.1", + "gatsby-transformer-sharp": "^5.13.1", + "i18next": "^22.0.6", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-to-string": "^4.0.0", + "react": "18.2.0", + "react-dom": "^18.2.0", + "react-i18next": "^14.0.5", + "react-markdown-renderer": "^1.4.0", + "remark-gfm": "^3.0.1", + "sass": "^1.70.0", + "slug": "^8.2.3", + "unist-util-visit": "^5.0.0" + }, + "devDependencies": { + "eslint-import-resolver-alias": "^1.1.2" + } +} diff --git a/cntr-monitor/src/components/App.js b/cntr-monitor/src/components/App.js new file mode 100644 index 000000000..c44c53312 --- /dev/null +++ b/cntr-monitor/src/components/App.js @@ -0,0 +1,19 @@ +import React from 'react' +import { graphql, useStaticQuery } from 'gatsby' +import LanguageSwitcher from './LanguageSwitcher' +import Footer from './Footer' +import SkipToContent from './SkipToContent' +import SiteHeader from './SiteHeader' +import './global.scss' +import useTranslations from '../hooks/useTranslations' + +function App({ translationData, pages, issue, post, pagination, children }) { + return ( + <> + + {children} + + ) +} + +export default App diff --git a/cntr-monitor/src/components/Footer.js b/cntr-monitor/src/components/Footer.js new file mode 100644 index 000000000..02a28b232 --- /dev/null +++ b/cntr-monitor/src/components/Footer.js @@ -0,0 +1,30 @@ +import { Link, useTranslation } from 'gatsby-plugin-react-i18next' +import React from 'react' +import * as styles from './Footer.module.scss' + +export default function Footer({ pages }) { + const { t } = useTranslation() + + if (!pages) pages = [] + return ( + + ) +} diff --git a/cntr-monitor/src/components/Footer.module.scss b/cntr-monitor/src/components/Footer.module.scss new file mode 100644 index 000000000..cb185d8a2 --- /dev/null +++ b/cntr-monitor/src/components/Footer.module.scss @@ -0,0 +1,13 @@ +@import 'vars'; + +.container { + padding: 0 var(--padding); +} + +.menu { + @extend %caption; + ul { + display: flex; + gap: 1em; + } +} diff --git a/cntr-monitor/src/components/Issue.js b/cntr-monitor/src/components/Issue.js new file mode 100644 index 000000000..85046270b --- /dev/null +++ b/cntr-monitor/src/components/Issue.js @@ -0,0 +1,153 @@ +import React from 'react' +import { graphql } from 'gatsby' +import MarkdownRenderer from 'react-markdown-renderer' +import App from './App.js' +import Meta from './Meta.js' +import SkipToContent from './SkipToContent.js' +import { useTranslation } from 'gatsby-plugin-react-i18next' +import { Link } from 'gatsby-plugin-react-i18next' +import * as styles from './Issue.module.scss' + +export const query = graphql` + query ($id: String!, $language: String!, $postsDirectory: String!) { + locales: allLocale(filter: { language: { eq: $language } }) { + edges { + node { + ns + data + language + } + } + } + + site { + siteMetadata { + title + siteUrl + } + } + + post: file(id: { eq: $id }) { + relativeDirectory + childMdx { + fields { + slug + } + frontmatter { + title + intro + order + authors { + name + } + } + } + } + + posts: allFile( + sort: { childMdx: { frontmatter: { order: ASC } } } + filter: { + extension: { eq: "mdx" } + relativeDirectory: { eq: $postsDirectory } + sourceInstanceName: { eq: "content" } + childMdx: { fields: { locale: { eq: $language } } } + } + ) { + nodes { + relativePath + relativeDirectory + id + childMdx { + fields { + slug + locale + } + frontmatter { + title + order + } + } + } + } + + pages: allFile( + filter: { + relativeDirectory: { glob: "**/pages/**" } + extension: { eq: "mdx" } + sourceInstanceName: { eq: "content" } + childMdx: { fields: { locale: { eq: $language } } } + } + ) { + nodes { + id + childMdx { + fields { + slug + } + frontmatter { + title + order + } + } + } + } + } +` + +const Index = ({ data, pageContext, children, location }) => { + const { t } = useTranslation() + + const posts = data.posts.nodes.map((p) => { + let postStyles = {} + const year = data.post.relativeDirectory.replace(/(.{2})\/(issues)\//g, '') + const frontmatter = p.childMdx.frontmatter + const maxWords = 45 + let intro = '' + if (frontmatter.teaser) { + intro = frontmatter.teaser + } else { + intro = + frontmatter.intro && frontmatter.intro.split(' ').length > maxWords + ? frontmatter.intro.split(' ').slice(0, maxWords).join(' ') + '...' + : frontmatter.intro + } + + return ( +
  • + +

    {frontmatter.title}

    + {frontmatter.intro && } + +
  • + ) + }) + return ( + + +
    +
    +

    {data.post.childMdx.frontmatter.title}

    +
    +
    +

    {t('Contents')}

    +
      {posts}
    +
    +
    +
    + ) +} + +export default Index + +export const Head = ({ data, pageContext, location }) => { + const year = data.post.relativeDirectory.replace(/(.{2})\/(issues)\//g, '') + const translationData = { currentLanguage: pageContext.language, currentSlug: location.pathname } + + const bodyStyles = {} + return ( + <> + + + + ) +} diff --git a/cntr-monitor/src/components/Issue.module.scss b/cntr-monitor/src/components/Issue.module.scss new file mode 100644 index 000000000..4213140f3 --- /dev/null +++ b/cntr-monitor/src/components/Issue.module.scss @@ -0,0 +1,5 @@ +@import 'vars'; + +.title { + @extend %h1; +} diff --git a/cntr-monitor/src/components/LanguageSwitcher.js b/cntr-monitor/src/components/LanguageSwitcher.js new file mode 100644 index 000000000..986902fdc --- /dev/null +++ b/cntr-monitor/src/components/LanguageSwitcher.js @@ -0,0 +1,28 @@ +import React from 'react' +import { Link } from 'gatsby' +import { useI18next } from 'gatsby-plugin-react-i18next' +import * as styles from './LanguageSwitcher.module.scss' + +export default function LanguageSwitcher({ translationData, translations }) { + const { languages } = useI18next() + const currentLanguage = translationData.currentLanguage + + const languageLinks = languages.map((l, i) => { + const t = translations.find((el) => el.language === l) + let inner = {l} + if (t) { + inner = ( + + {l} + + ) + } + return ( +
  • + {inner} +
  • + ) + }) + + return +} diff --git a/cntr-monitor/src/components/LanguageSwitcher.module.scss b/cntr-monitor/src/components/LanguageSwitcher.module.scss new file mode 100644 index 000000000..689ca18a0 --- /dev/null +++ b/cntr-monitor/src/components/LanguageSwitcher.module.scss @@ -0,0 +1,44 @@ +@import 'vars'; + +.container { + display: flex; + font-size: var(--ms--1); +} + +.item { + display: block; + list-style: none; + &:first-child { + border-left: 1px solid var(--gray); + } + &:last-child { + border-right: 1px solid var(--gray); + } +} + +.item-current { + background: black; + color: white; + border-right: 0 !important; + border-left: 0 !important; + .inner { + &:hover, + &:focus-visible { + text-decoration: none; + } + } +} + +.inner { + display: flex; + text-transform: uppercase; + padding: 0 0.65em; + height: 100%; + align-items: center; + letter-spacing: 0.05em; + font-size: var(--ms-0); + &:hover, + &:focus-visible { + text-decoration: underline; + } +} diff --git a/cntr-monitor/src/components/Meta.js b/cntr-monitor/src/components/Meta.js new file mode 100644 index 000000000..b29dab634 --- /dev/null +++ b/cntr-monitor/src/components/Meta.js @@ -0,0 +1,78 @@ +import { graphql, useStaticQuery } from 'gatsby' +import React from 'react' +import useTranslations from '../hooks/useTranslations' + +export default function Meta({ title, description, image, url, translationData, children }) { + const defaultData = useStaticQuery(graphql` + query { + site { + siteMetadata { + title + description + siteTwitter + authorTwitter + siteUrl + image { + src + alt + } + } + } + allSitePage { + nodes { + path + pageContext + } + } + } + `) + const fallback = defaultData.site.siteMetadata + + let data = { + url: url || fallback.siteUrl, + title: title || fallback.title, + description: description || fallback.description, + image: { + src: `${fallback.siteUrl}${image?.src || fallback.image.src}`, + alt: image?.alt || fallback.image.alt, + }, + siteTwitter: fallback.siteTwitter, + } + + let translations = useTranslations(translationData, defaultData.allSitePage.nodes) + + const alternateLinks = translations.map((t, i) => { + const path = `${defaultData.site.siteMetadata.siteUrl}/${t.path}` + return + }) + + return ( + <> + {translationData && } + + {data.title} + + + + + + + + + + + + + + + + + + + + {alternateLinks} + + {children} + + ) +} diff --git a/cntr-monitor/src/components/Page.js b/cntr-monitor/src/components/Page.js new file mode 100644 index 000000000..f2fb6f5a6 --- /dev/null +++ b/cntr-monitor/src/components/Page.js @@ -0,0 +1,99 @@ +import { graphql } from 'gatsby' +import React from 'react' +import App from './App' +import PostBody from './PostBody' +import Meta from './Meta' +import * as styles from './Page.module.scss' + +export const query = graphql` + query ($id: String!, $language: String!, $translations: [String!]) { + site: site { + siteMetadata { + title + } + } + locales: allLocale(filter: { language: { eq: $language } }) { + edges { + node { + ns + data + language + } + } + } + post: file(id: { eq: $id }) { + modifiedTime(locale: "de-DE", formatString: "dddd, D.M.YYYY") + childMdx { + fields { + slug + locale + } + frontmatter { + title + intro + } + } + } + translations: allFile(filter: { id: { in: $translations } }) { + nodes { + relativeDirectory + id + childMdx { + fields { + locale + slug + } + frontmatter { + title + } + } + } + } + pages: allFile( + filter: { + relativeDirectory: { glob: "**/pages/**" } + extension: { eq: "mdx" } + sourceInstanceName: { eq: "content" } + childMdx: { fields: { locale: { eq: $language } } } + } + ) { + nodes { + id + childMdx { + fields { + slug + } + frontmatter { + title + order + } + } + } + } + } +` +const Page = ({ data, children, pageContext }) => { + return ( + +
    + {children} +
    +
    + ) +} + +export function Head({ data, pageContext, location }) { + const frontmatter = data.post.childMdx.frontmatter + const translationData = { + currentPath: location, + currentSlug: data.post.childMdx.fields.slug, + currentLanguage: pageContext.pageLocale, + translations: data.translations.nodes, + } + return +} + +export default Page diff --git a/cntr-monitor/src/components/Page.module.scss b/cntr-monitor/src/components/Page.module.scss new file mode 100644 index 000000000..f8fb449de --- /dev/null +++ b/cntr-monitor/src/components/Page.module.scss @@ -0,0 +1 @@ +@import 'vars'; diff --git a/cntr-monitor/src/components/Post.js b/cntr-monitor/src/components/Post.js new file mode 100644 index 000000000..c6b1dbfa9 --- /dev/null +++ b/cntr-monitor/src/components/Post.js @@ -0,0 +1,218 @@ +import React from 'react' +import { Link, graphql } from 'gatsby' +import App from './App' +import PostBody from './PostBody' +import Footer from './Footer.js' +import Meta from './Meta' +import { useTranslation } from 'gatsby-plugin-react-i18next' +import useTranslations from '../hooks/useTranslations.js' +import SiteHeader from './SiteHeader.js' +import LanguageSwitcher from './LanguageSwitcher.js' +import useColors from '../hooks/useColors.js' +import Arrow from '../images/arrow-right.svg' +import * as styles from './Post.module.scss' + +export const query = graphql` + query ($id: String!, $language: String!, $translations: [String!], $issueId: String!) { + site { + siteMetadata { + title + siteUrl + } + } + locales: allLocale(filter: { language: { eq: $language } }) { + edges { + node { + ns + data + language + } + } + } + + issue: file(id: { eq: $issueId }) { + relativeDirectory + childMdx { + fields { + locale + } + frontmatter { + title + } + } + } + + post: file(id: { eq: $id }) { + relativeDirectory + childMdx { + fields { + slug + } + frontmatter { + title + color + authors { + frontmatter { + author_id + } + } + } + } + } + posts: allFile( + filter: { + relativeDirectory: { glob: "**/posts/**" } + extension: { eq: "mdx" } + sourceInstanceName: { eq: "content" } + childMdx: { fields: { locale: { eq: $language } } } + } + sort: { childMdx: { frontmatter: { order: ASC } } } + ) { + nodes { + id + relativeDirectory + childMdx { + fields { + slug + } + frontmatter { + title + order + } + } + } + } + authors: allFile( + filter: { + relativeDirectory: { glob: "**/authors" } + extension: { eq: "mdx" } + sourceInstanceName: { eq: "content" } + childMdx: { fields: { locale: { eq: $language } } } + } + ) { + nodes { + id + childMdx { + frontmatter { + name + author_id + } + body + } + } + } + pages: allFile( + filter: { + relativeDirectory: { glob: "**/pages/**" } + extension: { eq: "mdx" } + sourceInstanceName: { eq: "content" } + childMdx: { fields: { locale: { eq: $language } } } + } + ) { + nodes { + id + childMdx { + fields { + slug + } + frontmatter { + title + order + } + } + } + } + + translations: allFile(filter: { id: { in: $translations } }) { + nodes { + id + relativeDirectory + childMdx { + fields { + locale + slug + } + frontmatter { + title + } + } + } + } + allSitePage { + nodes { + path + pageContext + } + } + } +` +export default function Post({ data, pageContext, children }) { + const { t } = useTranslation() + + const frontmatter = data.post.childMdx.frontmatter + const posts = data.posts.nodes.filter((node) => { + return node.relativeDirectory.includes(data.issue.relativeDirectory) + }) + const currentIndex = posts.findIndex((el) => { + return el.childMdx.frontmatter.title === frontmatter.title + }) + const next = posts[currentIndex + 1] || null + const previous = posts[currentIndex - 1] || null + + const pagination = ( + + ) + + let translationData = { translations: data.translations.nodes, currentLanguage: pageContext.language, currentSlug: data.post.childMdx.fields.slug } + let translations = useTranslations(translationData, data.allSitePage.nodes) + + return ( + + + {pagination && pagination} + {data.translations.nodes.length > 0 && } + +
    +

    {data.post.childMdx.frontmatter.title}

    + {children} +
    +