e.preventDefault()}
- onDrop={onDrop}
+
-
-
- {file ? (
-
- Selected file: {file.name}
+
+
+ This app evaluates how well multimodal AI models turn emergency maps
+ into meaningful text. Upload your map, let the AI generate a
+ description, then review and rate the result based on your expertise.
- ) : (
-
Drag & Drop a file here
- )}
-
- {/* File-picker button - always visible */}
-
- onFileChange(e.target.files?.[0], "file")}
- />
- (document.querySelector('input[type="file"]') as HTMLInputElement)?.click()}
+
+ {/* "More »" link */}
+
+
+ e.preventDefault()}
+ onDrop={onDrop}
>
- {file ? 'Change File' : 'Upload'}
-
-
+ {file && preview ? (
+
+
+
+
+
+ {file.name}
+
+
+ ) : (
+ <>
+
+
Drag & Drop a file here
+
or
+ >
+ )}
+
+ {/* File-picker button - always visible */}
+
+ onFileChange(e.target.files?.[0], "file")}
+ />
+ (document.querySelector('input[type="file"]') as HTMLInputElement)?.click()}
+ >
+ {file ? 'Change File' : 'Browse Device'}
+
+
+
+
+
+ )}
+
+ {/* Loading state */}
+ {isLoading && (
+
+
+
Generating caption...
)}
{/* Generate button */}
- {step === 1 && (
-
+ {step === 1 && !isLoading && (
+
)}
-{step === 2 && imageUrl && (
-
-
-
-
-
-)}
-
-{step === 2 && (
-
- {/* ────── METADATA FORM ────── */}
-
- o.s_code}
- labelSelector={(o) => o.label}
- required
- />
- o.t_code}
- labelSelector={(o) => o.label}
- required
- />
- o.epsg}
- labelSelector={(o) => `${o.srid} (EPSG:${o.epsg})`}
- required
- />
- o.image_type}
- labelSelector={(o) => o.label}
- required
- />
- o.c_code}
- labelSelector={(o) => o.label}
- placeholder="Select one or more"
- />
-
-
- {/* ────── RATING SLIDERS ────── */}
-
-
How well did the AI perform on the task?
- {(['accuracy', 'context', 'usability'] as const).map((k) => (
-
- {k}
-
- setScores((s) => ({ ...s, [k]: Number(e.target.value) }))
- }
- className="w-full accent-ifrcRed"
- />
- {scores[k]}
-
- ))}
-
-
- {/* ────── AI‑GENERATED CAPTION ────── */}
-
- AI‑Generated Caption
-
-
- {/* ────── SUBMIT BUTTON ────── */}
-
-
- Delete
-
-
- Submit
-
-
-
- )}
-
- {/* Success page */}
- {step === 3 && (
-
-
Saved!
-
Your caption has been successfully saved.
-
-
+
+
+
+
+
+
+ )}
+
+ {step === 2 && (
+
+ {/* ────── METADATA FORM ────── */}
+
- Upload Another
-
+
+
+ setTitle(value || '')}
+ placeholder="Enter a title for this map..."
+ required
+ />
+
+
o.s_code}
+ labelSelector={(o) => o.label}
+ required
+ />
+ o.t_code}
+ labelSelector={(o) => o.label}
+ required
+ />
+ o.epsg}
+ labelSelector={(o) => `${o.srid} (EPSG:${o.epsg})`}
+ required
+ />
+ o.image_type}
+ labelSelector={(o) => o.label}
+ required
+ />
+ o.c_code}
+ labelSelector={(o) => o.label}
+ placeholder="Select one or more"
+ />
+
+
+
+ {/* ────── RATING SLIDERS ────── */}
+
+
+
How well did the AI perform on the task?
+ {(['accuracy', 'context', 'usability'] as const).map((k) => (
+
+ {k}
+
+ setScores((s) => ({ ...s, [k]: Number(e.target.value) }))
+ }
+ className="w-full accent-ifrcRed"
+ />
+ {scores[k]}
+
+ ))}
+
+
+
+ {/* ────── AI‑GENERATED CAPTION ────── */}
+
+
+
+
+
+ {/* ────── SUBMIT BUTTON ────── */}
+
+
+
+
+
+ Submit
+
+
-
- )}
-
-
-
-
-);
+ )}
+
+ {/* Success page */}
+ {step === 3 && (
+
+
Saved!
+
Your caption has been successfully saved.
+
+
+ Upload Another
+
+
+
+ )}
+
+
+ );
}
diff --git a/go-web-app-develop/.changeset/README.md b/go-web-app-develop/.changeset/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..e5b6d8d6a67ad0dca8f20117fbfc72e076882d00
--- /dev/null
+++ b/go-web-app-develop/.changeset/README.md
@@ -0,0 +1,8 @@
+# Changesets
+
+Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
+with multi-package repos, or single-package repos to help you version and publish your code. You can
+find the full documentation for it [in our repository](https://github.com/changesets/changesets)
+
+We have a quick list of common questions to get you started engaging with this project in
+[our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
diff --git a/go-web-app-develop/.changeset/config.json b/go-web-app-develop/.changeset/config.json
new file mode 100644
index 0000000000000000000000000000000000000000..78e2547c0a77bd1108fc97b2ee04fca048e780dc
--- /dev/null
+++ b/go-web-app-develop/.changeset/config.json
@@ -0,0 +1,15 @@
+{
+ "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
+ "changelog": "@changesets/cli/changelog",
+ "commit": false,
+ "fixed": [],
+ "linked": [],
+ "access": "public",
+ "baseBranch": "develop",
+ "updateInternalDependencies": "patch",
+ "ignore": [],
+ "privatePackages": {
+ "version": true,
+ "tag": true
+ }
+}
diff --git a/go-web-app-develop/.changeset/lovely-kids-boil.md b/go-web-app-develop/.changeset/lovely-kids-boil.md
new file mode 100644
index 0000000000000000000000000000000000000000..fbb5e507c03ea4314965d3f8fe7227baaf04131b
--- /dev/null
+++ b/go-web-app-develop/.changeset/lovely-kids-boil.md
@@ -0,0 +1,5 @@
+---
+"go-web-app": patch
+---
+
+Fix use of operational timeframe date in imminent final report form
diff --git a/go-web-app-develop/.changeset/pre.json b/go-web-app-develop/.changeset/pre.json
new file mode 100644
index 0000000000000000000000000000000000000000..c547ab77a110f9927419ee2de404109a6aba307b
--- /dev/null
+++ b/go-web-app-develop/.changeset/pre.json
@@ -0,0 +1,15 @@
+{
+ "mode": "pre",
+ "tag": "beta",
+ "initialVersions": {
+ "go-web-app": "7.20.2",
+ "go-ui-storybook": "1.0.7",
+ "@ifrc-go/ui": "1.5.1"
+ },
+ "changesets": [
+ "lovely-kids-boil",
+ "solid-clubs-care",
+ "sweet-gifts-cheer",
+ "whole-lions-guess"
+ ]
+}
diff --git a/go-web-app-develop/.changeset/solid-clubs-care.md b/go-web-app-develop/.changeset/solid-clubs-care.md
new file mode 100644
index 0000000000000000000000000000000000000000..1ba2c262bebd10d30fd668efb4757d5e5caa1ec2
--- /dev/null
+++ b/go-web-app-develop/.changeset/solid-clubs-care.md
@@ -0,0 +1,8 @@
+---
+"go-web-app": minor
+---
+
+Add Crisis categorization update date
+
+- Add updated date for crisis categorization in emergency page.
+- Add consent checkbox over situational overview in field report form.
diff --git a/go-web-app-develop/.changeset/sweet-gifts-cheer.md b/go-web-app-develop/.changeset/sweet-gifts-cheer.md
new file mode 100644
index 0000000000000000000000000000000000000000..7ead9c065d23406e1b6dfab6b7005ee81597125d
--- /dev/null
+++ b/go-web-app-develop/.changeset/sweet-gifts-cheer.md
@@ -0,0 +1,9 @@
+---
+"go-web-app": minor
+---
+
+Add support for DREF imminent v2 in final report
+
+- Add a separate route for the old dref final report form
+- Update dref final report to accomodate imminent v2 changes
+
diff --git a/go-web-app-develop/.changeset/whole-lions-guess.md b/go-web-app-develop/.changeset/whole-lions-guess.md
new file mode 100644
index 0000000000000000000000000000000000000000..e7f61facce276c868c920e75e99b59bbcfe16efd
--- /dev/null
+++ b/go-web-app-develop/.changeset/whole-lions-guess.md
@@ -0,0 +1,7 @@
+---
+"go-web-app": patch
+---
+
+- Fix calculation of Operation End date in Final report form
+- Fix icon position issue in the implementation table of DREF PDF export
+- Update the label for last update date in the crisis categorization pop-up
diff --git a/go-web-app-develop/.dockerignore b/go-web-app-develop/.dockerignore
new file mode 100644
index 0000000000000000000000000000000000000000..cf4994588663a234b28d9e5016c7aa5468008dce
--- /dev/null
+++ b/go-web-app-develop/.dockerignore
@@ -0,0 +1,148 @@
+# Swap files
+*.swp
+
+# Byte-compiled / optimized / DLL files
+__pycache__
+*.py[cod]
+*$py.class
+
+# C extensions
+*.so
+
+# Distribution / packaging
+.Python
+env
+build
+develop-eggs
+dist
+downloads
+eggs
+.eggs
+lib
+lib64
+parts
+sdist
+var
+*.egg-info
+.installed.cfg
+*.egg
+
+# PyInstaller
+# Usually these files are written by a python script from a template
+# before PyInstaller builds the exe, so as to inject date/other infos into it.
+*.manifest
+*.spec
+
+# Installer logs
+pip-log.txt
+pip-delete-this-directory.txt
+
+# Unit test / coverage reports
+htmlcov
+.tox
+.coverage
+.coverage.*
+.cache
+nosetests.xml
+coverage.xml
+*,cover
+.hypothesis
+
+# Translations
+*.mo
+*.pot
+
+# Django stuff:
+*.log
+
+# Sphinx documentation
+docs/_build
+
+# PyBuilder
+target
+
+#Ipython Notebook
+.ipynb_checkpoints
+
+# SASS cache
+.sass-cache
+media_test
+
+# Rope project settings
+.ropeproject
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (http://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules
+jspm_packages
+
+# Typescript v1 declaration files
+typings
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variables file
+.env
+.env*
+
+# Sensitive Deploy Files
+deploy/eb/
+
+# tox
+./.tox
+
+# Helm
+.helm-charts/
+
+# Docker
+Dockerfile
+.dockerignore
+
+# git
+.gitignore
\ No newline at end of file
diff --git a/go-web-app-develop/.github/ISSUE_TEMPLATE/01_bug_report.yml b/go-web-app-develop/.github/ISSUE_TEMPLATE/01_bug_report.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f4abf01393ec0c3ee9e09833ec08f73ef8ea28a9
--- /dev/null
+++ b/go-web-app-develop/.github/ISSUE_TEMPLATE/01_bug_report.yml
@@ -0,0 +1,92 @@
+name: "Bug Report"
+description: "Report a technical or visual issue."
+labels: ["type: bug"]
+type: "Bug"
+body:
+ - type: markdown
+ attributes:
+ value: |
+ **Bug Report**
+ Please fill out the form below with as much detail as possible.
+ If the issue is visual, screenshots or videos are greatly appreciated.
+ **Please review [our guide on reporting bugs](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#reporting-bugs) before opening a new issue.**
+
+ - type: input
+ attributes:
+ label: "Page URL"
+ description: "The URL of the page where you encountered the issue."
+ placeholder: "https://go.ifrc.org/"
+ validations:
+ required: true
+
+ - type: dropdown
+ attributes:
+ label: "Environment"
+ description: "Please select the environment where the bug occurred."
+ options:
+ - "Alpha"
+ - "Staging"
+ - "Production"
+ validations:
+ required: true
+
+ - type: input
+ attributes:
+ label: "Browser"
+ description: "Which browser are you using? (e.g., Chrome, Firefox, Safari)"
+ placeholder: "Chrome"
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: "Steps to Reproduce the Issue"
+ description: |
+ Please describe the issue in detail, including:
+ 1. What actions led to the issue?
+ 2. If possible, attach screenshots or videos demonstrating the problem.
+ placeholder: |
+ 1. I clicked on...
+ 2. [Attach screenshots/videos if available]
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: "Expected Behavior"
+ description: "Describe what you expected to happen."
+ placeholder: "I expected the page to..."
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: "Actual Behavior"
+ description: "Describe what actually happened, including any error messages."
+ placeholder: "Instead, I saw..."
+ validations:
+ required: true
+
+ - type: dropdown
+ attributes:
+ label: "Priority"
+ description: "How urgent is this issue?"
+ options:
+ - "Low (Minor inconvenience)"
+ - "Medium (Affects functionality, but there is a workaround)"
+ - "High (Major functionality is broken)"
+ - "Critical (Site is unusable)"
+ validations:
+ required: false
+
+ - type: textarea
+ attributes:
+ label: "Additional Context (Optional)"
+ description: |
+ Provide any extra details, such as:
+ - Related links.
+ - Previous occurrences of this issue.
+ - Workarounds you have tried.
+ placeholder: "This issue also happened on [link]."
+ validations:
+ required: false
diff --git a/go-web-app-develop/.github/ISSUE_TEMPLATE/02_feature_request.yml b/go-web-app-develop/.github/ISSUE_TEMPLATE/02_feature_request.yml
new file mode 100644
index 0000000000000000000000000000000000000000..163c573d859af4dafb2e2e4ccc12cfc03e93b571
--- /dev/null
+++ b/go-web-app-develop/.github/ISSUE_TEMPLATE/02_feature_request.yml
@@ -0,0 +1,39 @@
+name: "Feature Request"
+description: "Suggest a new idea or enhancement."
+labels: ["type: feature-request"]
+type: "Feature"
+body:
+ - type: markdown
+ attributes:
+ value: |
+ **Feature Request**
+ Thank you for suggesting a new feature!
+ Please provide as much detail as possible to help us understand and evaluate your idea.
+ **Please review [our guide on suggesting enhancements](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#suggesting-enhancements).**
+
+ - type: textarea
+ attributes:
+ label: "Feature Description"
+ description: |
+ Describe your feature request in detail, including:
+ - What the feature is.
+ - Why it is needed and how it will improve the project.
+ - How it will benefit users (e.g., As a user, I want to [do something] so that [desired outcome].).
+ placeholder: "As a user, I want to filter search results by date so that I can quickly find recent information."
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: "Additional Context"
+ description: |
+ Provide any extra details or supporting information, such as:
+ - Links to references or related resources.
+ - Examples from other projects or systems.
+ - Screenshots, mockups, or diagrams.
+ *Tip: You can attach files by clicking here and dragging them in.*
+ placeholder: |
+ Here's a link to a similar feature in another project: [link].
+ I've also attached a mockup of what this could look like.
+ validations:
+ required: false
diff --git a/go-web-app-develop/.github/ISSUE_TEMPLATE/03_epic_request.yml b/go-web-app-develop/.github/ISSUE_TEMPLATE/03_epic_request.yml
new file mode 100644
index 0000000000000000000000000000000000000000..f15ad133073a3b398d059b692a4e6768a6766b1f
--- /dev/null
+++ b/go-web-app-develop/.github/ISSUE_TEMPLATE/03_epic_request.yml
@@ -0,0 +1,37 @@
+name: "Epic"
+description: "Track a larger initiative with multiple related tasks and deliverables."
+labels: ["type: epic"]
+type: "Feature"
+body:
+ - type: markdown
+ attributes:
+ value: |
+ **Epic**
+ Use this to define a large, overarching initiative.
+ **Please review [our guide on suggesting enhancements](https://github.com/IFRCGo/go-web-app/blob/develop/CONTRIBUTING.md#suggesting-enhancements).**
+
+ - type: textarea
+ attributes:
+ label: "Epic Summary"
+ description: |
+ Provide a clear and concise summary of the epic.
+ - What is this epic about?
+ - What problem does it solve or what goal does it achieve?
+ - How does it align with the project’s objectives?
+ placeholder: |
+ Example:
+ This epic focuses on implementing a new feature.
+ validations:
+ required: true
+
+ - type: textarea
+ attributes:
+ label: "Additional Context or Resources"
+ description: "Provide any additional information, links, or resources that will help the team understand and execute this epic."
+ placeholder: |
+ Examples:
+ - Link to design mockups: [link]
+ - Technical specs document: [link]
+ - Reference to similar features: [link]
+ validations:
+ required: false
diff --git a/go-web-app-develop/.github/ISSUE_TEMPLATE/config.yml b/go-web-app-develop/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000000000000000000000000000000000000..6e0e864658c3567d911593daf3fc122b0bb825a1
--- /dev/null
+++ b/go-web-app-develop/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,5 @@
+blank_issues_enabled: true
+contact_links:
+ - name: Documentation
+ url: https://go-wiki.ifrc.org/en/home
+ about: Please consult the wiki to know more about IFRC GO.
diff --git a/go-web-app-develop/.github/dependabot.yml b/go-web-app-develop/.github/dependabot.yml
new file mode 100644
index 0000000000000000000000000000000000000000..5af73a621cfd1ee6a9d876326a6177a53679c516
--- /dev/null
+++ b/go-web-app-develop/.github/dependabot.yml
@@ -0,0 +1,27 @@
+version: 2
+updates:
+ - package-ecosystem: npm
+ directory: /
+ schedule:
+ interval: weekly
+ groups:
+ eslint:
+ patterns:
+ - "*eslint*"
+ vite:
+ patterns:
+ - "*vite*"
+ postcss:
+ patterns:
+ - "*postcss*"
+ stylelint:
+ patterns:
+ - "*stylelint*"
+ all-other-dependencies:
+ patterns:
+ - "*"
+ exclude-patterns:
+ - "*eslint*"
+ - "*vite*"
+ - "*postcss*"
+ - "*stylelint*"
diff --git a/go-web-app-develop/.github/pull_request_template.md b/go-web-app-develop/.github/pull_request_template.md
new file mode 100644
index 0000000000000000000000000000000000000000..0ff480c6e6965c575f1bae9f32185ed54e80225d
--- /dev/null
+++ b/go-web-app-develop/.github/pull_request_template.md
@@ -0,0 +1,30 @@
+## Summary
+
+Provide a brief description of what this PR addresses and its purpose.
+
+## Addresses
+
+* Issue(s): *List related issues or tickets.*
+
+## Depends On
+
+* Other PRs or Dependencies: *List PRs or dependencies this PR relies on.*
+
+## Changes
+
+* Detailed list or prose of changes
+* Breaking changes
+* Changes to configurations
+
+## This PR Ensures:
+
+* \[ ] No typos or grammatical errors
+* \[ ] No conflict markers left in the code
+* \[ ] No unwanted comments, temporary files, or auto-generated files
+* \[ ] No inclusion of secret keys or sensitive data
+* \[ ] No `console.log` statements meant for debugging
+* \[ ] All CI checks have passed
+
+## Additional Notes
+
+*Optional: Add any other relevant context, screenshots, or details here.*
diff --git a/go-web-app-develop/.github/workflows/add-issue-to-backlog.yml b/go-web-app-develop/.github/workflows/add-issue-to-backlog.yml
new file mode 100644
index 0000000000000000000000000000000000000000..0a7a4ea7fc5c5d253d3baaa0ed03eed88f515013
--- /dev/null
+++ b/go-web-app-develop/.github/workflows/add-issue-to-backlog.yml
@@ -0,0 +1,16 @@
+name: Add issues to Backlog
+
+on:
+ issues:
+ types:
+ - opened
+
+jobs:
+ add-to-project:
+ name: Add issue to project
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/add-to-project@v0.4.0
+ with:
+ project-url: https://github.com/orgs/IFRCGo/projects/12
+ github-token: ${{ secrets.ADD_TO_PROJECT_PAT }}
diff --git a/go-web-app-develop/.github/workflows/chromatic.yml b/go-web-app-develop/.github/workflows/chromatic.yml
new file mode 100644
index 0000000000000000000000000000000000000000..2bc901b6f5ccea680d8ab29375ee52f4e3ab5ad0
--- /dev/null
+++ b/go-web-app-develop/.github/workflows/chromatic.yml
@@ -0,0 +1,127 @@
+name: 'Chromatic'
+
+on:
+ pull_request:
+ push:
+ branches:
+ - develop
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}-chromatic
+ cancel-in-progress: true
+
+permissions:
+ actions: write
+ contents: read
+ pages: write
+ id-token: write
+
+jobs:
+ changed-files:
+ name: Check for changed files
+ runs-on: ubuntu-latest
+ outputs:
+ all_changed_files: ${{ steps.changed-files.outputs.all_changed_files }}
+ any_changed: ${{ steps.changed-files.outputs.any_changed }}
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Get changed files
+ id: changed-files
+ uses: tj-actions/changed-files@v44
+ with:
+ files: |
+ packages/ui/**
+ packages/go-ui-storybook/**
+ ui:
+ name: Build UI Library
+ environment: 'test'
+ runs-on: ubuntu-latest
+ needs: [changed-files]
+ if: ${{ needs.changed-files.outputs.any_changed == 'true' }}
+ defaults:
+ run:
+ working-directory: packages/ui
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - name: Install dependencies
+ run: pnpm install
+ - name: Typecheck
+ run: pnpm typecheck
+ - name: Lint CSS
+ run: pnpm lint:css
+ - name: Lint JS
+ run: pnpm lint:js
+ - name: build UI library
+ run: pnpm build
+ - uses: actions/upload-artifact@v4
+ with:
+ name: ui-build
+ path: packages/ui/dist
+ chromatic:
+ name: Chromatic Deploy
+ runs-on: ubuntu-latest
+ needs: [ui]
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - name: Install dependencies
+ run: pnpm install
+ - uses: actions/download-artifact@v4
+ with:
+ name: ui-build
+ path: packages/ui/dist
+ - name: Run Chromatic
+ uses: chromaui/action@v1
+ with:
+ exitZeroOnChanges: true
+ exitOnceUploaded: true
+ onlyChanged: true
+ skip: "@(renovate/**|dependabot/**)"
+ projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
+ token: ${{ secrets.GITHUB_TOKEN }}
+ autoAcceptChanges: "develop"
+ workingDir: packages/go-ui-storybook
+ github-pages:
+ name: Deploy to Github Pages
+ runs-on: ubuntu-latest
+ needs: [ui]
+ steps:
+ - uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - uses: actions/download-artifact@v4
+ with:
+ name: ui-build
+ path: packages/ui/dist
+ - uses: bitovi/github-actions-storybook-to-github-pages@v1.0.3
+ with:
+ install_command: pnpm install
+ build_command: pnpm build-storybook
+ path: packages/go-ui-storybook/storybook-static
+ checkout: false
diff --git a/go-web-app-develop/.github/workflows/ci.yml b/go-web-app-develop/.github/workflows/ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..ebdd425e6e7243c4be81500e123eeb46c16319ca
--- /dev/null
+++ b/go-web-app-develop/.github/workflows/ci.yml
@@ -0,0 +1,304 @@
+name: CI
+
+on:
+ pull_request:
+ push:
+ branches:
+ - 'develop'
+
+env:
+ APP_ADMIN_URL: ${{ vars.APP_ADMIN_URL }}
+ APP_API_ENDPOINT: ${{ vars.APP_API_ENDPOINT }}
+ APP_ENVIRONMENT: ${{ vars.APP_ENVIRONMENT }}
+ APP_MAPBOX_ACCESS_TOKEN: ${{ vars.APP_MAPBOX_ACCESS_TOKEN }}
+ APP_RISK_ADMIN_URL: ${{ vars.APP_RISK_ADMIN_URL }}
+ APP_RISK_API_ENDPOINT: ${{ vars.APP_RISK_API_ENDPOINT }}
+ APP_SENTRY_DSN: ${{ vars.APP_SENTRY_DSN }}
+ APP_SENTRY_NORMALIZE_DEPTH: ${{ vars.APP_SENTRY_NORMALIZE_DEPTH }}
+ APP_SENTRY_TRACES_SAMPLE_RATE: ${{ vars.APP_SENTRY_TRACES_SAMPLE_RATE }}
+ APP_SHOW_ENV_BANNER: ${{ vars.APP_SHOW_ENV_BANNER }}
+ APP_TINY_API_KEY: ${{ vars.APP_TINY_API_KEY }}
+ APP_TITLE: ${{ vars.APP_TITLE }}
+ GITHUB_WORKFLOW: true
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
+jobs:
+ ui:
+ name: Build UI Library
+ environment: 'test'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: packages/ui
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Typecheck
+ run: pnpm typecheck
+
+ - name: Lint CSS
+ run: pnpm lint:css
+
+ - name: Lint JS
+ run: pnpm lint:js
+
+ - name: Build
+ run: pnpm build
+
+ - uses: actions/upload-artifact@v4
+ with:
+ name: ui-build
+ path: packages/ui/dist
+
+ test:
+ name: Run tests
+ environment: 'test'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: app
+ needs: [ui]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - name: Install dependencies
+ run: pnpm install
+
+ - uses: actions/download-artifact@v4
+ with:
+ name: ui-build
+ path: packages/ui/dist
+
+ - name: Run test
+ run: pnpm test
+
+ translation:
+ continue-on-error: true
+ name: Identify error with translation files
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: app
+ needs: [ui]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - name: Install dependencies
+ run: pnpm install
+
+ - uses: actions/download-artifact@v4
+ with:
+ name: ui-build
+ path: packages/ui/dist
+
+ - name: Identify error with translation files
+ run: pnpm lint:translation
+
+ translation-migrations:
+ if: |
+ (github.event_name == 'pull_request' && github.base_ref == 'develop') ||
+ (github.event_name == 'push' && github.ref == 'refs/heads/develop')
+ continue-on-error: true
+ name: Identify if translation migrations need to be generated
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: app
+ needs: [ui]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - name: Install dependencies
+ run: pnpm install
+
+ - uses: actions/download-artifact@v4
+ with:
+ name: ui-build
+ path: packages/ui/dist
+
+ - name: Identify if translation migrations need to be generated
+ run: |
+ if pnpm translatte:generate; then
+ # The step should fail if generation is possible
+ exit 1
+ fi
+
+ unused:
+ name: Identify unused files
+ runs-on: ubuntu-latest
+ needs: [ui]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - name: Install dependencies
+ run: pnpm install
+
+ - name: Initialize types
+ run: pnpm initialize:type
+ working-directory: app
+
+ - name: Identify unused files
+ run: pnpm lint:unused
+
+ lint:
+ name: Lint JS
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: app
+ needs: [ui]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - name: Install dependencies
+ run: pnpm install
+
+ - uses: actions/download-artifact@v4
+ with:
+ name: ui-build
+ path: packages/ui/dist
+
+ - name: Lint JS
+ run: pnpm lint:js
+
+ lint-css:
+ name: Lint CSS
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: app
+ needs: [ui]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - name: Install dependencies
+ run: pnpm install
+
+ - uses: actions/download-artifact@v4
+ with:
+ name: ui-build
+ path: packages/ui/dist
+
+ - name: Lint CSS
+ run: pnpm lint:css
+
+ # FIXME: Identify a way to generate schema before we run typecheck
+ # typecheck:
+ # name: Typecheck
+ # runs-on: ubuntu-latest
+ # steps:
+ # - uses: actions/checkout@v4
+ # - name: Install pnpm
+ # uses: pnpm/action-setup@v4
+ # - name: Install Node.js
+ # uses: actions/setup-node@v4
+ # with:
+ # node-version: 20
+ # cache: 'pnpm'
+ # - name: Install dependencies
+ # run: pnpm install
+ #
+ # - name: Typecheck
+ # run: pnpm typecheck
+
+ typos:
+ name: Spell Check with Typos
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Actions Repository
+ uses: actions/checkout@v4
+
+ - name: Check spelling
+ uses: crate-ci/typos@v1.29.4
+
+ build:
+ name: Build GO Web App
+ environment: 'test'
+ runs-on: ubuntu-latest
+ defaults:
+ run:
+ working-directory: app
+ needs: [lint, lint-css, test, ui]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Install pnpm
+ uses: pnpm/action-setup@v4
+ - name: Install Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 20
+ cache: 'pnpm'
+ - name: Install dependencies
+ run: pnpm install
+
+ - uses: actions/download-artifact@v4
+ with:
+ name: ui-build
+ path: packages/ui/dist
+
+ - name: Build
+ run: pnpm build
+
+ validate_helm:
+ name: Validate Helm
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@main
+
+ - name: Install Helm
+ uses: azure/setup-helm@v4
+
+ - name: Helm lint
+ run: helm lint ./nginx-serve/helm --values ./nginx-serve/helm/values-test.yaml
+
+ - name: Helm template
+ run: helm template ./nginx-serve/helm --values ./nginx-serve/helm/values-test.yaml
diff --git a/go-web-app-develop/.github/workflows/publish-nginx-serve.yml b/go-web-app-develop/.github/workflows/publish-nginx-serve.yml
new file mode 100644
index 0000000000000000000000000000000000000000..28c91fbc56be483ef9e53bbc4c619fb8297b6e0b
--- /dev/null
+++ b/go-web-app-develop/.github/workflows/publish-nginx-serve.yml
@@ -0,0 +1,147 @@
+name: Publish Helm
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - develop
+ - project/*
+
+permissions:
+ packages: write
+
+
+jobs:
+ publish_image:
+ name: Publish Docker Image
+ runs-on: ubuntu-latest
+
+ outputs:
+ docker_image_name: ${{ steps.prep.outputs.tagged_image_name }}
+ docker_image_tag: ${{ steps.prep.outputs.tag }}
+ docker_image: ${{ steps.prep.outputs.tagged_image }}
+
+ steps:
+ - uses: actions/checkout@main
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: 🐳 Prepare Docker
+ id: prep
+ env:
+ IMAGE_NAME: ghcr.io/${{ github.repository }}
+ run: |
+ BRANCH_NAME=$(echo $GITHUB_REF_NAME | sed 's|[/:]|-|' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' | cut -c1-100 | sed 's/-*$//')
+
+ # XXX: Check if there is a slash in the BRANCH_NAME eg: project/add-docker
+ if [[ "$BRANCH_NAME" == *"/"* ]]; then
+ # XXX: Change the docker image package to -alpha
+ IMAGE_NAME="$IMAGE_NAME-alpha"
+ TAG="$(echo "$BRANCH_NAME" | sed 's|/|-|g').$(echo $GITHUB_SHA | head -c7)"
+ else
+ TAG="$BRANCH_NAME.$(echo $GITHUB_SHA | head -c7)"
+ fi
+
+ IMAGE_NAME=$(echo $IMAGE_NAME | tr '[:upper:]' '[:lower:]')
+ echo "tagged_image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT
+ echo "tag=${TAG}" >> $GITHUB_OUTPUT
+ echo "tagged_image=${IMAGE_NAME}:${TAG}" >> $GITHUB_OUTPUT
+ echo "::notice::Tagged docker image: ${IMAGE_NAME}:${TAG}"
+
+ - name: 🐳 Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: 🐳 Cache Docker layers
+ uses: actions/cache@v4
+ with:
+ path: /tmp/.buildx-cache
+ key: ${{ runner.os }}-buildx-${{ github.ref }}
+ restore-keys: |
+ ${{ runner.os }}-buildx-refs/develop
+ ${{ runner.os }}-buildx-
+
+ - name: 🐳 Docker build
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ builder: ${{ steps.buildx.outputs.name }}
+ file: nginx-serve/Dockerfile
+ target: nginx-serve
+ load: true
+ push: true
+ tags: ${{ steps.prep.outputs.tagged_image }}
+ cache-from: type=local,src=/tmp/.buildx-cache
+ cache-to: type=local,dest=/tmp/.buildx-cache-new
+ build-args: |
+ "APP_SENTRY_TRACES_SAMPLE_RATE=0.8"
+ "APP_SENTRY_REPLAYS_SESSION_SAMPLE_RATE=0.8"
+ "APP_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE=0.8"
+
+ - name: 🐳 Move docker cache
+ run: |
+ rm -rf /tmp/.buildx-cache
+ mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+
+ publish_helm:
+ name: Publish Helm
+ needs: publish_image
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: Install Helm
+ uses: azure/setup-helm@v3
+
+ - name: Tag docker image in Helm Chart values.yaml
+ env:
+ IMAGE_NAME: ${{ needs.publish_image.outputs.docker_image_name }}
+ IMAGE_TAG: ${{ needs.publish_image.outputs.docker_image_tag }}
+ run: |
+ # Update values.yaml with latest docker image
+ sed -i "s|SET-BY-CICD-IMAGE|$IMAGE_NAME|" nginx-serve/helm/values.yaml
+ sed -i "s/SET-BY-CICD-TAG/$IMAGE_TAG/" nginx-serve/helm/values.yaml
+
+ - name: Package Helm Chart
+ id: set-variables
+ run: |
+ # XXX: Check if there is a slash in the BRANCH_NAME eg: project/add-docker
+ if [[ "$GITHUB_REF_NAME" == *"/"* ]]; then
+ # XXX: Change the helm chart to
-alpha
+ sed -i 's/^name: \(.*\)/name: \1-alpha/' nginx-serve/helm/Chart.yaml
+ fi
+
+ SHA_SHORT=$(git rev-parse --short HEAD)
+ sed -i "s/SET-BY-CICD/$SHA_SHORT/g" nginx-serve/helm/Chart.yaml
+ helm package ./nginx-serve/helm -d .helm-charts
+
+ - name: Push Helm Chart
+ env:
+ IMAGE: ${{ needs.publish_image.outputs.docker_image }}
+ OCI_REPO: oci://ghcr.io/${{ github.repository }}
+ run: |
+ OCI_REPO=$(echo $OCI_REPO | tr '[:upper:]' '[:lower:]')
+ PACKAGE_FILE=$(ls .helm-charts/*.tgz | head -n 1)
+ echo "# Helm Chart" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "Tagged Image: **$IMAGE**" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "Helm push output" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo '```bash' >> $GITHUB_STEP_SUMMARY
+ helm push "$PACKAGE_FILE" $OCI_REPO >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
diff --git a/go-web-app-develop/.github/workflows/publish-storybook-nginx-serve.yml b/go-web-app-develop/.github/workflows/publish-storybook-nginx-serve.yml
new file mode 100644
index 0000000000000000000000000000000000000000..430237923bc504148b1a4f372ee398bba191032a
--- /dev/null
+++ b/go-web-app-develop/.github/workflows/publish-storybook-nginx-serve.yml
@@ -0,0 +1,127 @@
+name: Publish Storybook Helm
+
+on:
+ workflow_dispatch:
+ push:
+ branches:
+ - develop
+
+permissions:
+ packages: write
+
+
+jobs:
+ publish_image:
+ name: 🐳 Publish Docker Image
+ runs-on: ubuntu-latest
+
+ outputs:
+ docker_image_name: ${{ steps.prep.outputs.tagged_image_name }}
+ docker_image_tag: ${{ steps.prep.outputs.tag }}
+ docker_image: ${{ steps.prep.outputs.tagged_image }}
+
+ steps:
+ - uses: actions/checkout@main
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: 🐳 Prepare Docker
+ id: prep
+ env:
+ IMAGE_NAME: ghcr.io/${{ github.repository }}/go-ui-storybook
+ run: |
+ BRANCH_NAME=$(echo $GITHUB_REF_NAME | sed 's|[/:]|-|' | tr '[:upper:]' '[:lower:]' | sed 's/_/-/g' | cut -c1-100 | sed 's/-*$//')
+ TAG="$BRANCH_NAME.$(echo $GITHUB_SHA | head -c7)"
+ IMAGE_NAME=$(echo $IMAGE_NAME | tr '[:upper:]' '[:lower:]')
+ echo "tagged_image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT
+ echo "tag=${TAG}" >> $GITHUB_OUTPUT
+ echo "tagged_image=${IMAGE_NAME}:${TAG}" >> $GITHUB_OUTPUT
+ echo "::notice::Tagged docker image: ${IMAGE_NAME}:${TAG}"
+
+ - name: 🐳 Set up Docker Buildx
+ id: buildx
+ uses: docker/setup-buildx-action@v3
+
+ - name: 🐳 Cache Docker layers
+ uses: actions/cache@v4
+ with:
+ path: /tmp/.buildx-cache
+ key: ${{ runner.os }}-buildx-${{ github.ref }}
+ restore-keys: |
+ ${{ runner.os }}-buildx-refs/develop
+ ${{ runner.os }}-buildx-
+
+ - name: 🐳 Docker build
+ uses: docker/build-push-action@v6
+ with:
+ context: .
+ builder: ${{ steps.buildx.outputs.name }}
+ file: packages/go-ui-storybook/nginx-serve/Dockerfile
+ target: nginx-serve
+ load: true
+ push: true
+ tags: ${{ steps.prep.outputs.tagged_image }}
+ cache-from: type=local,src=/tmp/.buildx-cache
+ cache-to: type=local,dest=/tmp/.buildx-cache-new
+
+ - name: 🐳 Move docker cache
+ run: |
+ rm -rf /tmp/.buildx-cache
+ mv /tmp/.buildx-cache-new /tmp/.buildx-cache
+
+ publish_helm:
+ name: ⎈ Publish Helm
+ needs: publish_image
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Login to GitHub Container Registry
+ uses: docker/login-action@v3
+ with:
+ registry: ghcr.io
+ username: ${{ github.actor }}
+ password: ${{ secrets.GITHUB_TOKEN }}
+
+ - name: ⎈ Install Helm
+ uses: azure/setup-helm@v3
+
+ - name: ⎈ Tag docker image in Helm Chart values.yaml
+ env:
+ IMAGE_NAME: ${{ needs.publish_image.outputs.docker_image_name }}
+ IMAGE_TAG: ${{ needs.publish_image.outputs.docker_image_tag }}
+ run: |
+ # Update values.yaml with latest docker image
+ sed -i "s|SET-BY-CICD-IMAGE|$IMAGE_NAME|" packages/go-ui-storybook/nginx-serve/helm/values.yaml
+ sed -i "s/SET-BY-CICD-TAG/$IMAGE_TAG/" packages/go-ui-storybook/nginx-serve/helm/values.yaml
+
+ - name: ⎈ Package Helm Chart
+ id: set-variables
+ run: |
+ SHA_SHORT=$(git rev-parse --short HEAD)
+ sed -i "s/SET-BY-CICD/$SHA_SHORT/g" packages/go-ui-storybook/nginx-serve/helm/Chart.yaml
+ helm package ./packages/go-ui-storybook/nginx-serve/helm -d .helm-charts
+
+ - name: ⎈ Push Helm Chart
+ env:
+ IMAGE: ${{ needs.publish_image.outputs.docker_image }}
+ OCI_REPO: oci://ghcr.io/${{ github.repository }}
+ run: |
+ OCI_REPO=$(echo $OCI_REPO | tr '[:upper:]' '[:lower:]')
+ PACKAGE_FILE=$(ls .helm-charts/*.tgz | head -n 1)
+ echo "## 🚀 IFRC GO UI Helm Chart 🚀" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "🐳 Tagged Image: **$IMAGE**" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo "⎈ Helm push output" >> $GITHUB_STEP_SUMMARY
+ echo "" >> $GITHUB_STEP_SUMMARY
+ echo '```bash' >> $GITHUB_STEP_SUMMARY
+ helm push "$PACKAGE_FILE" $OCI_REPO >> $GITHUB_STEP_SUMMARY
+ echo '```' >> $GITHUB_STEP_SUMMARY
diff --git a/go-web-app-develop/.gitignore b/go-web-app-develop/.gitignore
new file mode 100644
index 0000000000000000000000000000000000000000..6f710ff8f79403d54017754f5d34528087363516
--- /dev/null
+++ b/go-web-app-develop/.gitignore
@@ -0,0 +1,43 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+build
+build-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+.env*
+!.env.example
+.eslintcache
+tsconfig.tsbuildinfo
+
+# Custom ignores
+
+stats.html
+generated/
+coverage/
+
+# storybook build
+storybook-static/
+
+# Helm
+.helm-charts/
diff --git a/go-web-app-develop/.npmrc b/go-web-app-develop/.npmrc
new file mode 100644
index 0000000000000000000000000000000000000000..6c59086d862516d2fffa5da2035e5003100fb5c7
--- /dev/null
+++ b/go-web-app-develop/.npmrc
@@ -0,0 +1 @@
+enable-pre-post-scripts=true
diff --git a/go-web-app-develop/COLLABORATING.md b/go-web-app-develop/COLLABORATING.md
new file mode 100644
index 0000000000000000000000000000000000000000..3596cf283617c189f1f7db3d8027317c5f911376
--- /dev/null
+++ b/go-web-app-develop/COLLABORATING.md
@@ -0,0 +1,18 @@
+# IFRC GO Collaboration Guide
+
+This document offers guidelines for collaborators on codebase maintenance, testing, building and deployment, and issue management.
+
+## Repository
+
+* [Issues and Pull Requests](./collaborating/issues-and-pull-requests.md)
+* [Structure](./collaborating/repository-structure.md)
+* [Linting](./collaborating/linting.md)
+* [Technology Used](./collaborating/technology.md)
+
+## Development
+
+* [Developing](./collaborating/developing.md)
+* [Translation](./collaborating/translation.md)
+* [Building](./collaborating/building.md)
+* [Testing](./collaborating/testing.md)
+* [Release](./collaborating/release.md)
diff --git a/go-web-app-develop/CONTRIBUTING.md b/go-web-app-develop/CONTRIBUTING.md
new file mode 100644
index 0000000000000000000000000000000000000000..503e8a4aad5961a5d579646a75e4e41cea880f16
--- /dev/null
+++ b/go-web-app-develop/CONTRIBUTING.md
@@ -0,0 +1,81 @@
+# IFRC GO Web Application Contributing Guide
+
+First off, thanks for taking the time to contribute! ❤️
+
+All types of contributions are encouraged and valued. See the [Table of Contents](#table-of-contents) for different ways to help and details about how this project handles them. Please make sure to read the relevant section before making your contribution.
+
+## Table of Contents
+
+* [I Have a Question](#i-have-a-question)
+* [I Want To Contribute](#i-want-to-contribute)
+* [What should I know before I get started?](#what-should-i-know-before-i-get-started)
+* [Reporting Bugs](#reporting-bugs)
+* [Suggesting Enhancements](#suggesting-enhancements)
+* [Becoming a Collaborator](#becoming-a-collaborator)
+
+## I Have a Question
+
+> If you want to ask a question, we assume that you have read the available [documentation](https://go-wiki.ifrc.org/en/home).
+
+Before you ask a question, it is best to search for existing [issues](https://github.com/IFRCGo/go-web-app/issues) that might help you. In case you have found a suitable issue and still need clarification, you can write your question in this issue.
+
+If you then still feel the need to ask a question and need clarification, we recommend the following:
+
+* Open a [discussion](https://github.com/IFRCGo/go-web-app/discussions).
+* Open an [issue](https://github.com/IFRCGo/go-web-app/issues/new/choose).
+* Provide as much context as you can about what you're running into.
+
+## I Want To Contribute
+
+Any individual is welcome to contribute to IFRC GO. The repository currently has two kinds of contribution personas:
+
+* A **Contributor** is any individual who creates an issue/PR, comments on an issue/PR, or contributes in some other way.
+* A **Collaborator** is a contributor with write access to the repository.
+
+### What should I know before I get started?
+
+### IFRC GO and Packages
+
+The project is hosted at .
+
+The project comprises several [repositories](https://github.com/orgs/IFRCGo/repositories), with notable ones including:
+
+* [go-web-app](https://github.com/IFRCGo/go-web-app/) - The frontend repository for the IFRC GO project.
+* [go-api](https://github.com/IFRCGo/go-api) - The backed repository for the IFRC GO project.
+
+### Reporting Bugs
+
+#### Before Submitting a Bug Report
+
+Ensure the issue is not a user error by reviewing the documentation. Check the [existing bug reports](https://github.com/IFRCGo/go-web-app/issues?q=is%3Aissue%20state%3Aopen%20type%3ABug) to confirm if the issue has already been reported.
+
+#### Submitting the Bug Report
+
+1. Open a new [Issue](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=01_bug_report.yml).
+2. Provide all relevant details.
+
+#### After Submitting the Issue
+
+* The team will categorize and attempt to reproduce the issue.
+* If reproducible, the team will work on resolving the bug.
+
+### Suggesting Enhancements
+
+#### Before Submitting an Enhancement
+
+* Review the [documentation](https://go-wiki.ifrc.org/en/home) to ensure the functionality isn't already covered.
+* Perform a [search](https://github.com/IFRCGo/go-web-app/issues) to check if the enhancement has been suggested. If so, comment on the existing issue.
+* Confirm that your suggestion aligns with the project’s scope and objectives.
+
+#### How to Submit an Enhancement Suggestion
+
+Enhancements are tracked as [GitHub issues](https://github.com/IFRCGo/go-web-app/issues).
+
+* Open a new [feature request](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=02_feature_request.yml) or [Epic ticket](https://github.com/IFRCGo/go-web-app/issues/new?q=is%3Aissue+state%3Aopen+type%3ABug\&template=03_epic_request.yml) depending on the scale of the enhancement.
+* Provide a clear description and submit the ticket.
+
+## Becoming a Collaborator
+
+Collaborators are key members of the IFRC GO Web Application Team, responsible for its development. Members should have expertise in modern web technologies and standards.
+
+For detailed guidelines, refer to the [Collaboration Guide](./COLLABORATING.md).
diff --git a/go-web-app-develop/LICENSE b/go-web-app-develop/LICENSE
new file mode 100644
index 0000000000000000000000000000000000000000..04eca71b6e81461214eb8ad3277aa5338f4abd05
--- /dev/null
+++ b/go-web-app-develop/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 GO
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/go-web-app-develop/README.md b/go-web-app-develop/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..077e001cbdb03504d43441ab3260aea64fb133a4
--- /dev/null
+++ b/go-web-app-develop/README.md
@@ -0,0 +1,117 @@
+
+
+
+
+
+
+
+
+
+# IFRC GO
+
+[IFRC GO](https://go.ifrc.org/) is the platform of the International Federation of Red Cross and Red Crescent, aimed at connecting crucial information on emergency needs with the appropriate response. This repository houses the frontend source code for the application, developed using [React](https://react.dev/), [Vite](https://vitejs.dev/), and associated technologies.
+
+## Built With
+
+[![React][react-shields]][react-url] [![Vite][vite-shields]][vite-url] [![TypeScript][typescript-shields]][typescript-url] [![pnpm][pnpm-shields]][pnpm-url]
+
+## Getting Started
+
+Below are the steps to guide you through preparing your local environment for IFRC GO Web application development. The repository is set up as a [monorepo](https://monorepo.tools/). The [app](https://github.com/IFRCGo/go-web-app/tree/develop/app) directory houses the application code, while the [packages](https://github.com/IFRCGo/go-web-app/tree/develop/packages) directory contains related packages, including the [IFRC GO UI](https://www.npmjs.com/package/@ifrc-go/ui) components library.
+
+### Prerequisites
+
+To begin, ensure you have network access. Then, you'll need the following:
+
+1. [Git](https://git-scm.com/)
+2. [Node.js](https://nodejs.org/en/) as specified under `engines` section in `package.json` file
+3. [pnpm](https://pnpm.io/) as specified under `engines` section in `package.json` file
+4. Alternatively, you can use [Docker](https://www.docker.com/) to build the application.
+
+> \[!NOTE]\
+> Make sure the correct versions of pnpm and Node.js are installed. They are specified under `engines` section in `package.json` file.
+
+### Local Development
+
+1. Clone the repository using HTTPS, SSH, or GitHub CLI:
+
+ ```bash
+ git clone https://github.com/IFRCGo/go-web-app.git # HTTPS
+ git clone git@github.com:IFRCGo/go-web-app.git # SSH
+ gh repo clone IFRCGo/go-web-app # GitHub CLI
+ ```
+
+2. Install the dependencies:
+
+ ```bash
+ pnpm install
+ ```
+
+3. Create a `.env` file in the `app` directory and add variables from [env.ts](https://github.com/IFRCGo/go-web-app/blob/develop/app/env.ts). Any variables marked with `.optional()` are not mandatory for setup and can be skipped.
+
+ ```bash
+ cd app
+ touch .env
+ ```
+
+ Example `.env` file
+ ```
+ APP_TITLE=IFRC GO
+ APP_ENVIRONMENT=testing
+ ...
+ ```
+
+4. Start the development server:
+
+ ```bash
+ pnpm start:app
+ ```
+
+## Contributing
+
+* Check out existing [Issues](https://github.com/IFRCGo/go-web-app/issues) and [Pull Requests](https://github.com/IFRCGo/go-web-app/pulls) to contribute.
+* To request a feature or report a bug, [create a GitHub Issue](https://github.com/IFRCGo/go-web-app/issues/new/choose).
+* [Contribution Guide →](./CONTRIBUTING.md)
+* [Collaboration Guide →](./COLLABORATING.md)
+
+## Additional Packages
+
+The repository hosts multiple packages under the `packages` directory.
+
+1. [IFRC GO UI](https://github.com/IFRCGo/go-web-app/tree/develop/packages/ui) is a React UI components library tailored to meet the specific requirements of the IFRC GO community and its associated projects.
+2. [IFRC GO UI Storybook](https://github.com/IFRCGo/go-web-app/tree/develop/packages/go-ui-storybook) serves as the comprehensive showcase for the IFRC GO UI components library. It is hosted on [Chromatic](https://66557be6b68dacbf0a96db23-zctxglhsnk.chromatic.com/).
+
+## IFRC GO Backend
+
+The backend that serves the frontend application is maintained in a separate [repository](https://github.com/IFRCGo/go-api/).
+
+## Previous Repository
+
+[Go Frontend](https://github.com/IFRCGo/go-frontend) is the previous version of the project which contains the original codebase and project history.
+
+## Community & Support
+
+* Visit the [IFRC GO Wiki](https://go-wiki.ifrc.org/) for documentation of the IFRC GO platform.
+* Stay informed about the latest project updates on [Medium](https://ifrcgoproject.medium.com/).
+
+## License
+
+[MIT](https://github.com/IFRCGo/go-web-app/blob/develop/LICENSE)
+
+
+
+[react-shields]: https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB
+
+[react-url]: https://reactjs.org/
+
+[vite-shields]: https://img.shields.io/badge/vite-%23646CFF.svg?style=for-the-badge&logo=vite&logoColor=white
+
+[vite-url]: https://vitejs.dev/
+
+[typescript-shields]: https://img.shields.io/badge/typescript-%23007ACC.svg?style=for-the-badge&logo=typescript&logoColor=white
+
+[typescript-url]: https://www.typescriptlang.org/
+
+[pnpm-shields]: https://img.shields.io/badge/pnpm-F69220?style=for-the-badge&logo=pnpm&logoColor=fff
+
+[pnpm-url]: https://pnpm.io/
diff --git a/go-web-app-develop/app/CHANGELOG.md b/go-web-app-develop/app/CHANGELOG.md
new file mode 100644
index 0000000000000000000000000000000000000000..939d0cceaea6368f223c0c080688919333a38b9c
--- /dev/null
+++ b/go-web-app-develop/app/CHANGELOG.md
@@ -0,0 +1,729 @@
+# go-web-app
+
+## 7.21.0-beta.2
+
+### Patch Changes
+
+- b949fcd: Fix use of operational timeframe date in imminent final report form
+
+## 7.21.0-beta.1
+
+### Patch Changes
+
+- 84b4802: - Fix calculation of Operation End date in Final report form
+ - Fix icon position issue in the implementation table of DREF PDF export
+ - Update the label for last update date in the crisis categorization pop-up
+
+## 7.21.0-beta.0
+
+### Minor Changes
+
+- 039c488: Add Crisis categorization update date
+
+ - Add updated date for crisis categorization in emergency page.
+ - Add consent checkbox over situational overview in field report form.
+
+- 3ee9979: Add support for DREF imminent v2 in final report
+
+ - Add a separate route for the old dref final report form
+ - Update dref final report to accomodate imminent v2 changes
+
+## 7.20.2
+
+### Patch Changes
+
+- 8090b9a: Fix other action section visibility condition in DREF export
+
+## 7.20.1
+
+### Patch Changes
+
+- 4418171: Fix DREF form to properly save major coordination mechanism [#1928](https://github.com/IFRCGo/go-web-app/issues/1928)
+
+## 7.20.1-beta.0
+
+### Patch Changes
+
+- 4418171: Fix DREF form to properly save major coordination mechanism [#1928](https://github.com/IFRCGo/go-web-app/issues/1928)
+
+## 7.20.0
+
+### Minor Changes
+
+- 5771a6b: Update DREF application form and export
+
+ - add new field hazard date and location
+ - update hazard date as forcasted day of event
+ - update the section in dref application export
+ - remove Current National Society Actions from the export
+
+## 7.20.0-beta.0
+
+### Minor Changes
+
+- 5771a6b: Update DREF application form and export
+
+ - add new field hazard date and location
+ - update hazard date as forcasted day of event
+ - update the section in dref application export
+ - remove Current National Society Actions from the export
+
+## 7.19.0
+
+### Minor Changes
+
+- 456a145: Fix versioning
+
+### Patch Changes
+
+- 47786f8: Fix the undefined society name issue in surge page [#1899](https://github.com/IFRCGo/go-web-app/issues/1899)
+
+## 7.18.2
+
+### Patch Changes
+
+- e51a80f: Update the action for the DREF Ops update form for imminent.
+ - Remove change to response modal in the ops update form for type imminent.
+ - Fix the order of the field in operational timeframe tab.
+ - Add description text under upload assessment report button in DREF operation update form
+- Fix the error while viewing PER process [#1838](https://github.com/IFRCGo/go-web-app/issues/1838).
+
+## 7.18.1
+
+### Patch Changes
+
+- 75bf525: Fix logic to disable ops update for old imminents
+
+## 7.18.0
+
+### Minor Changes
+
+- bfcaecf: Address [Dref imminent Application](https://github.com/IFRCGo/go-web-app/issues/1455)
+ - Update logic for creation of dref final report for imminent
+ - Update allocatioon form for dref imminent
+ - Add Activity input in proposed action for dref type imminent
+ - Add proposed actions icons
+ - Show proposed actions for existing imminent dref applications
+ - Hide unused sections for dref imminent export and preserve proposed actions order
+ - Prevent selection of past dates for the `hazard_date` in dref imminent
+ - Add auto total population calculation in dref
+ - Add a confirmation popup before creating ops. update from imminent dref
+
+### Patch Changes
+
+- ee1bd60: Add proper redirect for Non-sovereign country in the country ongoing emergencies page
+- 771d085: Community Based Surveillance updates (Surge CoS Health)
+ - Changed page: https://go.ifrc.org/surge/catalogue/health/community-based-surveillance
+ - The changes affect team size and some standard components (e.g. kit content)
+- Updated dependencies [bfcaecf]
+ - @ifrc-go/ui@1.5.1
+
+## 7.17.4
+
+### Patch Changes
+
+- 14a7f2c: Update People assisted field label in the export of Dref final report.
+
+## 7.17.3
+
+### Patch Changes
+
+- fc8b427: Update field label in DrefFinalReport form and export
+
+## 7.17.2
+
+### Patch Changes
+
+- 54df6ff: Update DREF final report form
+
+ - The DREF final report form and export now include a new "Assisted Population" field, replacing the "Targeted Population" field.
+
+## 7.17.1
+
+### Patch Changes
+
+- 215030a: Update DREF forms
+
+ - Move Response strategy description from placeholder to below the input
+ - Add DREF allocation field in event details for the Loan type Ops. update form
+
+## 7.17.0
+
+### Minor Changes
+
+- 0b351d1: Address [DREF Superticket 2 bugs](https://github.com/IFRCGo/go-web-app/issues/1784)
+
+ - Update no of images in for "Description of event" from 2 to 4
+ - Update descriptions of few fields
+ - Replace \* with bullet in description of planned interventions in DREF import
+ - Add some of the missing fields to exports
+ - Remove warnings for previously removed fields
+
+## 7.16.2
+
+### Patch Changes
+
+- c086629: Update Learn > Resources > Montandon page
+ - Update styling of 'API Access' buttons
+ - Reword 'Access API' link to 'Access Montandon API'
+ - Reword 'Explore Radiant Earth API' to 'Explore data in STAC browser'
+- 2ee6a1e: Remove a broken image from Catalogue of Surge Services > Health > ERU Hospital page
+
+## 7.16.1
+
+### Patch Changes
+
+- d561dc4: - Update Montandon landing page - Fix typo in Justin's name and email - Update description
+ - Fix position and deploying organisation in ongoing RR deployment table
+
+## 7.16.0
+
+### Minor Changes
+
+- 9dcdd38: Add Montandon landing page
+
+ - Add a basic landing page for Montandon with links and information
+ - Add link to Montandon landing page to Learn > Resources menu
+
+## 7.15.0
+
+### Minor Changes
+
+- c26bda4: Implement [ERU Readiness](https://github.com/IFRCGo/go-web-app/issues/1710)
+
+ - Restucture surge page to acommodate ERU
+ - Move surge deployment related sections to a new dedicated tab **Active Surge Deployments**
+ - Update active deployments to improve scaling of points in the map
+ - Add **Active Surge Support per Emergency** section
+ - Revamp **Surge Overview** tab
+ - Add **Rapid Response Personnel** sub-tab
+ - Update existings charts and add new related tables/charts
+ - Add **Emergency Response Unit** sub-tab
+ - Add section to visualize ERU capacity and readiness
+ - Add section to view ongoing ERU deployments
+ - Add a form to update ERU Readiness
+ - Add option to export ERU Readiness data
+ - Update **Respond > Surge/Deployments** menu to include **Active Surge Deployments**
+
+- 9ed8181: Address feedbacks in [DREF superticket feedbacks](https://github.com/IFRCGo/go-web-app/issues/1816)
+
+ - Make end date of operation readonly field in all DREF forms
+ - Fix font and spacing issues in the DREF exports (caused by link text overflow)
+ - Update styling of Risk and Security Considerations section to match that of Previous Operations
+ - Update visibility condition of National Society Actions in Final Report export
+
+### Patch Changes
+
+- Updated dependencies [c26bda4]
+ - @ifrc-go/ui@1.5.0
+
+## 7.14.0
+
+### Minor Changes
+
+- 18ccc85:
+ - Update styling of vertical NavigationTab
+ - Hide register URL in the T&C page for logged in user
+ - Update styling of T&C page
+ - Make the page responsive
+ - Make sidebar sticky
+ - Update url for [monty docs](https://github.com/IFRCGo/go-web-app/issues/1418#issuecomment-2422371363)
+- 8d3a7bd: Initiate shutdown for 3W
+ - Remove "Submit 3W Projects" from the menu Prepare > Global 3W projects
+ - Rename "Global 3W Projects" to "Programmatic Partnerships" in Prepare menu
+ - Update global 3W page
+ - Update title and description for Programmatic Partnerships
+ - Remove all the contents related to 3W
+ - Replace contents in various places with project shutdown message
+ - Regional 3W tab
+ - 3W Projects section in Accounts > My Form > 3W
+ - Projects tab in Country > Ongoing Activities
+ - All Projects page
+ - New, edit 3W project form
+ - View 3W project page
+ - Remove NS Activities section in Country > NS overview > NS Activities page
+ - Remove Projects section from search results page
+
+### Patch Changes
+
+- Updated dependencies [18ccc85]
+ - @ifrc-go/ui@1.4.0
+
+## 7.13.0
+
+### Minor Changes
+
+- 69fd74f: - Update page title for Emergency to include the name
+ - Update page title of Flash update to include the name
+ - Fix the user registration link in the Terms & Condition page
+- 680c673: Implement [DREF Superticket 2.0](https://github.com/IFRCGo/go-web-app/issues/1695)
+
+### Patch Changes
+
+- fe4b727: - Upgrade pnpm to v10.6.1
+ - Cleanup Dockerfile
+ - Configure depandabot to track other dependencies updates
+ - Upgrade eslint
+ - Use workspace protocol to reference workspace packages
+- 9f20016: Enable user to edit their position field in [#1647](https://github.com/IFRCGo/go-web-app/issues/1647)
+- ef15af1: Add secondary ordering in tables for rows with same date
+- Updated dependencies [fe4b727]
+ - @ifrc-go/ui@1.3.1
+
+## 7.12.1
+
+### Patch Changes
+
+- Fix nullable type of assessment for NS capacity
+
+## 7.12.0
+
+### Minor Changes
+
+- f766bc7: Add link to IFRC Survey Designer in the tools section under learn menu
+
+### Patch Changes
+
+- 7f51854: - Surge CoS: Health fix
+- 3a1cac8: Hide focal point details based on user permissions
+- 43d3bf1: - Add Surge CoS Administration section
+ - Add Surge CoS Faecal Sludge Management (FSM) section
+ - Update Surge CoS IT&T section
+ - Update Surge CoS Basecamp section (as OSH)
+
+## 7.11.1
+
+### Patch Changes
+
+- ff426cd: Use current language for field report title generation
+
+## 7.11.0
+
+### Minor Changes
+
+- Field report number generation: Change only when the country or event changes
+
+## 7.10.1
+
+### Patch Changes
+
+- 14567f1: Improved tables by adding default and second-level ordering in [#1633](https://github.com/IFRCGo/go-web-app/issues/1633)
+
+ - Appeal Documents table, `emergencies/{xxx}/reports` page
+ - Recent Emergencies in Regions – All Appeals table
+ - All Deployed Personnel – Default sorting (filters to be added)
+ - Deployed ERUs – Changed filter title
+ - Key Documents tables in Countries
+ - Response documents
+ - Main page – Active Operations table
+ - The same `AppealsTable` is used in:
+ - Active Operations in Regions
+ - Previous Operations in Countries
+
+- 78d25b2:
+
+ - Update on the ERU MHPSS Module in the Catalogue of Services in [#1648](https://github.com/IFRCGo/go-web-app/issues/1648)
+ - Update on a PER role profile in [#1648](https://github.com/IFRCGo/go-web-app/issues/1648)
+ - Update link to the IM Technical Competency Framework in [#1483](https://github.com/IFRCGo/go-web-app/issues/1483)
+
+- 44623a7: Undo DREF Imminent changes
+- b57c453: Show the number of people assisted in the DREF Final Report export in [#1665](https://github.com/IFRCGo/go-web-app/issues/1665)
+
+## 7.10.0
+
+### Minor Changes
+
+- 4f89133: Fix DREF PGA export styling
+
+## 7.9.0
+
+### Minor Changes
+
+- 7927522: Update Imminent DREF Application in [#1455](https://github.com/IFRCGo/go-web-app/issues/1455)
+
+ - Hide sections/fields
+ - Rename sections/fields
+ - Remove sections/fields
+ - Reflect changes in the PDF export
+
+### Patch Changes
+
+- Updated dependencies [4032688]
+ - @ifrc-go/ui@1.3.0
+
+## 7.8.1
+
+### Patch Changes
+
+- 9c51dee: Remove `summary` field from field report form
+- Update @ifrc-go/ui version
+
+## 7.8.0
+
+### Minor Changes
+
+- 4843cb0: Added Operational Learning 2.0
+
+ - Key Figures Overview in Operational Learning
+ - Map View for Operational Learning
+ - Learning by Sector Bar Chart
+ - Learning by Region Bar Chart
+ - Sources Over Time Line Chart
+ - Methodology changes for the prioritization step
+ - Added an option to regenerate cached summaries
+ - Summary post-processing and cleanup
+ - Enabled MDR code search in admin
+
+### Patch Changes
+
+- f96e177: Move field report/emergency title generation logic from client to server
+- e85fc32: Integrate `crate-ci/typos` for code spell checking
+- 4cdea2b: Add redirection logic for `preparedness#operational-learning`
+- 9a50443: Add appeal doc type for appeal documents
+- 817d56d: Display properly formatted appeal type in search results
+- 1159fa4: Redirect obsolete URLs to recent ones
+ - redirect `/reports/` to `/field-reports/`
+ - redirect `/deployments/` -> `/surge/overview`
+- Updated dependencies [4843cb0]
+ - @ifrc-go/ui@1.2.3
+
+## 7.7.0
+
+### Minor Changes
+
+- 3258b96: Add local unit validation workflow
+
+### Patch Changes
+
+- Updated dependencies [c5a446f]
+ - @ifrc-go/ui@1.2.2
+
+## 7.6.6
+
+### Patch Changes
+
+- 8cdc946: Hide Local unit contact details on the list view for logged in users in [#1485](https://github.com/ifRCGo/go-web-app/issues/1485)
+ Update `tinymce-react` plugin to the latest version and enabled additional plugins, including support for lists in [#1481](https://github.com/ifRCGo/go-web-app/issues/1481)
+- ecca810: Replace the from-communication-copied text of CoS Health header
+- 7cf2514: Prioritize GDACS as the Primary Source for Imminent Risk Watch in [#1547](https://github.com/IFRCGo/go-web-app/issues/1547)
+- 8485076: Add Organization type and Learning type filter in Operational learning in [#1469](https://github.com/IFRCGo/go-web-app/issues/1469)
+- 766d98d: Auto append https:// for incomplete URLs in [#1505](https://github.com/IFRCGo/go-web-app/issues/1505)
+
+## 7.6.5
+
+### Patch Changes
+
+- 478e73b: Update labels for severity control in Imminent Risk Map
+ Update navigation for the events in Imminent Risk Map
+ Fix issue displayed when opening a DREF import template
+ Fix submission issue when importing a DREF import file
+- f82f846: Update Health Section in Catalogue of Surge Services
+- ade84aa: Display ICRC Presence
+ - Display ICRC presence across partner countries
+ - Highlight key operational countries
+
+## 7.6.4
+
+### Patch Changes
+
+- d85f64d: Update Imminent Events
+
+ - Hide WFP ADAM temporarily from list sources
+ - Show exposure control for cyclones from GDACS only
+
+## 7.6.3
+
+### Patch Changes
+
+- 7bbf3d2: Update key insights disclaimer text in Ops. Learning
+- 0e40681: Update FDRS data in Country / Context and Structure / NS indicators
+
+ - Add separate icon for each field for data year
+ - Use separate icon for disaggregation
+ - Update descriptions on dref import template (more details on _Missing / to be implemented_ section in https://github.com/IFRCGo/go-web-app/pull/1434#issuecomment-2459034932)
+
+- Updated dependencies [801ec3c]
+ - @ifrc-go/ui@1.2.1
+
+## 7.6.2
+
+### Patch Changes
+
+- 4fa6a36: Updated PER terminology and add PER logo in PER PDF export
+- 813e93f: Add link to GO UI storybook in resources page
+- 20dfeb3: Update DREF import template
+ - Update guidance
+ - Improve template stylings
+ - Update message in error popup when import fails
+- 8a18ad8: Add beta tag, URL redirect, and link to old dashboard on Ops Learning
+
+## 7.6.1
+
+### Patch Changes
+
+- 7afaf34: Fix null event in appeal for operational learning
+
+## 7.6.0
+
+### Minor Changes
+
+- Add new Operational Learning Page
+
+ - Add link to Operational Learning page under `Learn` navigation menu
+ - Integrate LLM summaries for Operational Learning
+
+## 7.5.3
+
+### Patch Changes
+
+- d7f5f53: Revamp risk imminent events for cyclone
+ - Visualize storm position, forecast uncertainty, track line and exposed area differently
+ - Add option to toggle visibility of these different layers
+ - Add severity legend for exposure
+ - Update styling for items in event list
+ - Update styling for event details page
+- 36a64fa: Integrate multi-select functionality in operational learning filters to allow selection of multiple filter items.
+- 894d00c: Add a new 404 page
+- 7757e54: Add an option to download excel import template for DREF (Response) which user can fill up and import.
+- a8d021d: Update resources page
+ - Add a new video for LocalUnits
+ - Update ordering of videos
+- aea512d: Prevent users from pasting images into rich text field
+- fd54657: Add Terms and Conditions page
+- bf55ccc: Add Cookie Policy page
+- df80c4f: Fix contact details in Field Report being always required when filled once
+- 81dc3bd: Added color mapping based on PER Area and Rating across all PER charts
+- Updated dependencies [dd92691]
+- Updated dependencies [d7f5f53]
+- Updated dependencies [fe6a455]
+- Updated dependencies [81dc3bd]
+ - @ifrc-go/ui@1.2.0
+
+## 7.5.2
+
+### Patch Changes
+
+- 37bba31: Add collaboration guide
+
+## 7.5.1
+
+### Patch Changes
+
+- 2a5e4a1: Add Core Competency Framework link to Resources page in [#1331](https://github.com/IFRCGo/go-web-app/issues/1331)
+- 31eaa97: Add Health Mapping Report to Resources page in [#1331](https://github.com/IFRCGo/go-web-app/issues/1331)
+- 4192da1: - Local Units popup, view/edit mode improvements in [#1178](https://github.com/IFRCGo/go-web-app/issues/1178)
+ - Remove ellipsize heading option in local units map popup
+ - Local units title on popup are now clickable that opens up a modal to show details
+ - Added an Edit button to the View Mode for users with edit permissions
+ - Users will now see a **disabled grey button** when the content is already validated
+- 5c7ab88: Display the public visibility field report to public users in [#1743](https://github.com/IFRCGo/go-web-app/issues/1343)
+
+## 7.5.0
+
+### Minor Changes
+
+- 5845699: Clean up Resources page
+
+## 7.4.2
+
+### Patch Changes
+
+- d734e04: - Fix duplication volunteer label in the Field Report details
+ - Fix rating visibility in the Country > NS Overview > Strategic priorities page
+
+## 7.4.1
+
+### Patch Changes
+
+- a4f77ab: Fetch and use latest available WorldBank data in [#571](https://github.com/IFRCGo/go-api/issues/2224)
+- ebf033a: Update Technical Competencies Link on the Cash page of the Catalogue of Surge Services in [#1290](https://github.com/IFRCGo/go-web-app/issues/1290)
+- 18d0dc9: Use `molnix status` to filter surge alerts in [#2208](https://github.com/IFRCGo/go-api/issues/2208)
+- b070c66: Check guest user permission for local units
+- 72df1f2: Add new drone icon for UAV team in [#1280](https://github.com/IFRCGo/go-web-app/issues/1280)
+- 2ff7940: Link version number to release notes on GitHub in [#1004](https://github.com/IFRCGo/go-web-app/issues/1004)
+ Updated @ifrc-go/icons to v2.0.1
+- Updated dependencies [72df1f2]
+ - @ifrc-go/ui@1.1.6
+
+## 7.4.0
+
+### Minor Changes
+
+- b6bd6aa: Implement Guest User Permission in [#1237](https://github.com/IFRCGo/go-web-app/issues/1237)
+
+## 7.3.13
+
+### Patch Changes
+
+- 453a397: - Update Local Unit map, table and form to match the updated design in [#1178](https://github.com/IFRCGo/go-web-app/issues/1178)
+ - Add delete button in Local units table and form
+ - Use filter prop in container and remove manual stylings
+ - Update size of WikiLink to match height of other action items
+ - Add error boundary to BaseMap component
+- Updated dependencies [453a397]
+ - @ifrc-go/ui@1.1.5
+
+## 7.3.12
+
+### Patch Changes
+
+- ba6734e: Show admin labels in maps in different languages, potentially fixing [#1036](https://github.com/IFRCGo/go-web-app/issues/1036)
+
+## 7.3.11
+
+### Patch Changes
+
+- d9491a2: Fix appeals statistics calculation
+
+## 7.3.10
+
+### Patch Changes
+
+- 3508c83: Add missing validations in DREF forms
+- 3508c83: Fix region filter in All Appeals table
+- 073fa1e: Remove personal detail for focal point in local units table
+- b508475: Add June 2024 Catalogue of Surge Services Updates
+- 3508c83: Handle countries with no bounding box
+- d9491a2: Fix appeals based statistics calculation
+- Updated dependencies [073fa1e]
+ - @ifrc-go/ui@1.1.4
+
+## 7.3.9
+
+### Patch Changes
+
+- 49f5410: - Reorder CoS list
+ - Update texts in CoS strategic partnerships resource mobilisation
+
+## 7.3.8
+
+### Patch Changes
+
+- 478ab69: Hide contact information from IFRC Presence
+- 3fbe60f: Hide add/edit local units on production environment
+- 90678ed: Show Organization Type properly in Account Details page
+
+## 7.3.7
+
+### Patch Changes
+
+- 909a5e2: Fix Appeals table for Africa Region
+- 5a1ae43: Add presentation mode in local units map
+- 96120aa: Fix DREF exports margins and use consistent date format
+- 8a4f26d: Avoid crash on country pages for countries without bbox
+
+## 7.3.6
+
+### Patch Changes
+
+- 1b4b6df: Add local unit form
+- 2631a9f: Add office type and location information for IFRC delegation office
+- 2d7a6a5: - Enable ability to start PER in IFRC supported languages
+ - Make PER forms `readOnly` in case of language mismatch
+- e4bf098: Fix incorrect statistics for past appeals of a country
+- Updated dependencies [0ab207d]
+- Updated dependencies [66151a7]
+ - @ifrc-go/ui@1.1.3
+
+## 7.3.5
+
+### Patch Changes
+
+- 894a762: Fix seasonal risk score in regional and global risk watch
+
+## 7.3.4
+
+### Patch Changes
+
+- d368ada: Fix GNI per capita in country profile overview
+
+## 7.3.3
+
+### Patch Changes
+
+- 73e1966: Update CoS pages as mentioned in #913
+- 179a073: Show all head of delegation under IFRC Presence
+- 98d6b62: Fix region operation map to apply filter for Africa
+
+## 7.3.2
+
+### Patch Changes
+
+- f83c12b: Show Local name when available and use English name as fallback for local units data
+
+## 7.3.1
+
+### Patch Changes
+
+- 7f0212b: Integrate mapbox street view for local units map
+- Updated dependencies [7f0212b]
+ - @ifrc-go/ui@1.1.2
+
+## 7.3.0
+
+### Minor Changes
+
+- 0dffd52: Add table view in NS local units
+
+## 7.2.5
+
+### Patch Changes
+
+- 556766e: - Refetch token list after new token is created
+ - Update link for terms and conditions for Montandon
+
+## 7.2.4
+
+### Patch Changes
+
+- 30eac3c: Add option to generate API token for Montandon in the user profile
+
+## 7.2.3
+
+### Patch Changes
+
+- Fix crash due to undefined ICRC presence in country page
+
+## 7.2.2
+
+### Patch Changes
+
+- - Update country risk page sources
+ - Update CoS pages
+- Updated dependencies [a1c0554]
+- Updated dependencies [e9552b4]
+ - @ifrc-go/ui@1.1.1
+
+## 7.2.1
+
+### Patch Changes
+
+- Remove personal identifiable information for local units
+
+## 7.2.0
+
+### Minor Changes
+
+- 9657d4b: Update country pages with appropriate source links
+- 66fa7cf: Show FDRS data retrieval year in NS indicators
+- b69e8e5: Update IFRC legal status link
+- 300250a: Show latest strategic plan of National Society under Strategic Priorities
+- 9657d4b: Add GO Wiki links for country page sections
+- b38d9d9: Improve overall styling of country pages
+ - Make loading animation consistent across all pages
+ - Make empty message consistent
+ - Use ChartContainer and update usage of charting hooks
+ - Update BaseMap to extend defaultMapOptions (instead of replacing it)
+ - Add an option to provide popupClassName in MapPopup
+- 80be711: Rename `Supporting Partners` to `Partners`.
+ - Update IFRC legal status link.
+ - Update the name of the strategic priorities link to indicate that they were created by the National Society.
+- 176e01b: Simplify usage of PER question group in PER assessment form
+ - Add min widths in account table columns
+
+## 7.1.5
+
+### Patch Changes
+
+- Updated dependencies
+ - @ifrc-go/ui@1.0.0
diff --git a/go-web-app-develop/app/env.ts b/go-web-app-develop/app/env.ts
new file mode 100644
index 0000000000000000000000000000000000000000..15b21e39f86e5effc692e283922162ccd961b39a
--- /dev/null
+++ b/go-web-app-develop/app/env.ts
@@ -0,0 +1,29 @@
+import { defineConfig, Schema } from '@julr/vite-plugin-validate-env';
+
+export default defineConfig({
+ APP_TITLE: Schema.string(),
+ APP_ENVIRONMENT: (key, value) => {
+ // NOTE: APP_ENVIRONMENT_PLACEHOLDER is meant to be used with image builds
+ // The value will be later replaced with the actual value
+ const regex = /^production|staging|testing|alpha-\d+|development|APP_ENVIRONMENT_PLACEHOLDER$/;
+ const valid = !!value && (value.match(regex) !== null);
+ if (!valid) {
+ throw new Error(`Value for environment variable "${key}" must match regex "${regex}", instead received "${value}"`);
+ }
+ if (value === 'APP_ENVIRONMENT_PLACEHOLDER') {
+ console.warn(`Using ${value} for app environment. Make sure to not use this for builds without helm chart`)
+ }
+ return value as ('production' | 'staging' | 'testing' | `alpha-${number}` | 'development' | 'APP_ENVIRONMENT_PLACEHOLDER');
+ },
+ APP_API_ENDPOINT: Schema.string({ format: 'url', protocol: true, tld: false }),
+ APP_ADMIN_URL: Schema.string.optional({ format: 'url', protocol: true, tld: false }),
+ APP_MAPBOX_ACCESS_TOKEN: Schema.string(),
+ APP_TINY_API_KEY: Schema.string(),
+ APP_RISK_API_ENDPOINT: Schema.string({ format: 'url', protocol: true }),
+ APP_SDT_URL: Schema.string.optional({ format: 'url', protocol: true, tld: false }),
+ APP_SENTRY_DSN: Schema.string.optional(),
+ APP_SENTRY_TRACES_SAMPLE_RATE: Schema.number.optional(),
+ APP_SENTRY_REPLAYS_SESSION_SAMPLE_RATE: Schema.number.optional(),
+ APP_SENTRY_REPLAYS_ON_ERROR_SAMPLE_RATE: Schema.number.optional(),
+ APP_GOOGLE_ANALYTICS_ID: Schema.string.optional(),
+});
diff --git a/go-web-app-develop/app/eslint.config.js b/go-web-app-develop/app/eslint.config.js
new file mode 100644
index 0000000000000000000000000000000000000000..dbe98504a293278684a903af8f11e4e565acaf6b
--- /dev/null
+++ b/go-web-app-develop/app/eslint.config.js
@@ -0,0 +1,165 @@
+import { FlatCompat } from '@eslint/eslintrc';
+import js from '@eslint/js';
+import json from "@eslint/json";
+import tseslint from "typescript-eslint";
+import process from 'process';
+
+const dirname = process.cwd();
+
+const compat = new FlatCompat({
+ baseDirectory: dirname,
+ resolvePluginsRelativeTo: dirname,
+});
+
+const appConfigs = compat.config({
+ env: {
+ node: true,
+ browser: true,
+ es2020: true,
+ },
+ root: true,
+ extends: [
+ 'airbnb',
+ 'airbnb/hooks',
+ 'plugin:@typescript-eslint/recommended',
+ 'plugin:react-hooks/recommended',
+ ],
+ parser: '@typescript-eslint/parser',
+ parserOptions: {
+ ecmaVersion: 'latest',
+ sourceType: 'module',
+ },
+ plugins: [
+ '@typescript-eslint',
+ 'react-refresh',
+ 'simple-import-sort',
+ 'import-newlines'
+ ],
+ settings: {
+ 'import/parsers': {
+ '@typescript-eslint/parser': ['.ts', '.tsx']
+ },
+ 'import/resolver': {
+ typescript: {
+ project: [
+ './tsconfig.json',
+ ],
+ },
+ },
+ },
+ rules: {
+ 'react-refresh/only-export-components': 'warn',
+
+ 'no-unused-vars': 0,
+ '@typescript-eslint/no-unused-vars': 1,
+
+ 'no-use-before-define': 0,
+ '@typescript-eslint/no-use-before-define': 1,
+
+ 'no-shadow': 0,
+ '@typescript-eslint/no-shadow': ['error'],
+
+ '@typescript-eslint/consistent-type-imports': [
+ 'warn',
+ {
+ disallowTypeAnnotations: false,
+ fixStyle: 'inline-type-imports',
+ prefer: 'type-imports',
+ },
+ ],
+
+ 'import/no-extraneous-dependencies': [
+ 'error',
+ {
+ devDependencies: [
+ '**/*.test.{ts,tsx}',
+ 'eslint.config.js',
+ 'postcss.config.cjs',
+ 'stylelint.config.cjs',
+ 'vite.config.ts',
+ ],
+ optionalDependencies: false,
+ },
+ ],
+
+ indent: ['error', 4, { SwitchCase: 1 }],
+
+ 'import/no-cycle': ['error', { allowUnsafeDynamicCyclicDependency: true }],
+
+ 'react/react-in-jsx-scope': 'off',
+ 'camelcase': 'off',
+
+ 'react/jsx-indent': ['error', 4],
+ 'react/jsx-indent-props': ['error', 4],
+ 'react/jsx-filename-extension': ['error', { extensions: ['.js', '.jsx', '.ts', '.tsx'] }],
+
+ 'import/extensions': ['off', 'never'],
+
+ 'react-hooks/rules-of-hooks': 'error',
+ 'react-hooks/exhaustive-deps': 'warn',
+
+ 'react/require-default-props': ['warn', { ignoreFunctionalComponents: true }],
+ 'simple-import-sort/imports': 'warn',
+ 'simple-import-sort/exports': 'warn',
+ 'import-newlines/enforce': ['warn', 1]
+ },
+ overrides: [
+ {
+ files: ['*.js', '*.jsx', '*.ts', '*.tsx'],
+ rules: {
+ 'simple-import-sort/imports': [
+ 'error',
+ {
+ 'groups': [
+ // side effect imports
+ ['^\\u0000'],
+ // packages `react` related packages come first
+ ['^react', '^@?\\w'],
+ // internal packages
+ ['^#.+$'],
+ // parent imports. Put `..` last
+ // other relative imports. Put same-folder imports and `.` last
+ ['^\\.\\.(?!/?$)', '^\\.\\./?$', '^\\./(?=.*/)(?!/?$)', '^\\.(?!/?$)', '^\\./?$'],
+ // style imports
+ ['^.+\\.json$', '^.+\\.module.css$'],
+ ]
+ }
+ ]
+ }
+ }
+ ]
+}).map((conf) => ({
+ ...conf,
+ files: ['src/**/*.tsx', 'src/**/*.jsx', 'src/**/*.ts', 'src/**/*.js'],
+ ignores: [
+ "node_modules/",
+ "build/",
+ "coverage/",
+ 'src/generated/types.ts'
+ ],
+}));
+
+const otherConfig = {
+ files: ['*.js', '*.ts', '*.cjs'],
+ ...js.configs.recommended,
+ ...tseslint.configs.recommended,
+};
+
+const jsonConfig = {
+ files: ['**/*.json'],
+ language: 'json/json',
+ rules: {
+ 'json/no-duplicate-keys': 'error',
+ },
+};
+
+export default [
+ {
+ plugins: {
+ json,
+ },
+ },
+ ...appConfigs,
+ otherConfig,
+ jsonConfig,
+];
diff --git a/go-web-app-develop/app/index.html b/go-web-app-develop/app/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..bb312224511aebc4fe7569dd61ba7c28148b5a8d
--- /dev/null
+++ b/go-web-app-develop/app/index.html
@@ -0,0 +1,69 @@
+
+
+
+
+
+
+
+
+ %APP_TITLE%
+
+
+
+
+
+
+
+
+
+
+
+ %APP_TITLE% needs JS.
+
+
+
+ %APP_TITLE% loading...
+
+
+
+
+
diff --git a/go-web-app-develop/app/package.json b/go-web-app-develop/app/package.json
new file mode 100644
index 0000000000000000000000000000000000000000..b3f3790cb176aa814dbd99d55abfdbbd959ddc07
--- /dev/null
+++ b/go-web-app-develop/app/package.json
@@ -0,0 +1,119 @@
+{
+ "name": "go-web-app",
+ "version": "7.21.0-beta.2",
+ "type": "module",
+ "private": true,
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/IFRCGo/go-web-app.git",
+ "directory": "app"
+ },
+ "scripts": {
+ "translatte": "tsx scripts/translatte/main.ts",
+ "translatte:generate": "pnpm translatte generate-migration ../translationMigrations ./src/**/i18n.json ../packages/ui/src/**/i18n.json",
+ "translatte:lint": "pnpm translatte lint ./src/**/i18n.json ../packages/ui/src/**/i18n.json",
+ "initialize:type": "mkdir -p generated/ && pnpm initialize:type:go-api && pnpm initialize:type:risk-api",
+ "initialize:type:go-api": "test -f ./generated/types.ts && true || cp types.stub.ts ./generated/types.ts",
+ "initialize:type:risk-api": "test -f ./generated/riskTypes.ts && true || cp types.stub.ts ./generated/riskTypes.ts",
+ "generate:type": "pnpm generate:type:go-api && pnpm generate:type:risk-api",
+ "generate:type:go-api": "dotenv -- cross-var openapi-typescript \"%APP_API_ENDPOINT%api-docs/\" -o ./generated/types.ts --alphabetize",
+ "generate:type:risk-api": "dotenv -- cross-var openapi-typescript \"%APP_RISK_API_ENDPOINT%api-docs/\" -o ./generated/riskTypes.ts --alphabetize",
+ "prestart": "pnpm initialize:type",
+ "start": "pnpm -F @ifrc-go/ui build && vite",
+ "prebuild": "pnpm initialize:type",
+ "build": "pnpm -F @ifrc-go/ui build && vite build",
+ "preview": "vite preview",
+ "pretypecheck": "pnpm initialize:type",
+ "typecheck": "tsc",
+ "prelint:js": "pnpm initialize:type",
+ "lint:js": "eslint src",
+ "lint:css": "stylelint \"./src/**/*.css\"",
+ "lint:translation": "pnpm translatte:lint",
+ "lint": "pnpm lint:js && pnpm lint:css && pnpm lint:translation",
+ "lint:fix": "pnpm lint:js --fix && pnpm lint:css --fix",
+ "test": "vitest",
+ "test:coverage": "vitest run --coverage",
+ "surge:deploy": "branch=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD); branch=$(echo $branch | tr ./ -); cp ../build/index.html ../build/200.html; surge -p ../build/ -d https://ifrc-go-$branch.surge.sh",
+ "surge:teardown": "branch=$(git rev-parse --symbolic-full-name --abbrev-ref HEAD); branch=$(echo $branch | tr ./ -); surge teardown https://ifrc-go-$branch.surge.sh"
+ },
+ "dependencies": {
+ "@ifrc-go/icons": "^2.0.1",
+ "@ifrc-go/ui": "workspace:^",
+ "@sentry/react": "^7.81.1",
+ "@tinymce/tinymce-react": "^5.1.1",
+ "@togglecorp/fujs": "^2.1.1",
+ "@togglecorp/re-map": "^0.3.0",
+ "@togglecorp/toggle-form": "^2.0.4",
+ "@togglecorp/toggle-request": "^1.0.0-beta.3",
+ "@turf/bbox": "^6.5.0",
+ "@turf/buffer": "^6.5.0",
+ "exceljs": "^4.3.0",
+ "file-saver": "^2.0.5",
+ "html-to-image": "^1.11.11",
+ "mapbox-gl": "^1.13.0",
+ "papaparse": "^5.4.1",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^6.18.0",
+ "sanitize-html": "^2.10.0"
+ },
+ "devDependencies": {
+ "@eslint/eslintrc": "^3.1.0",
+ "@eslint/js": "^9.20.0",
+ "@eslint/json": "^0.5.0",
+ "@julr/vite-plugin-validate-env": "^1.0.1",
+ "@types/file-saver": "^2.0.5",
+ "@types/mapbox-gl": "^1.13.0",
+ "@types/node": "^20.11.6",
+ "@types/papaparse": "^5.3.8",
+ "@types/react": "^18.0.28",
+ "@types/react-dom": "^18.0.11",
+ "@types/sanitize-html": "^2.9.0",
+ "@types/yargs": "^17.0.32",
+ "@typescript-eslint/eslint-plugin": "^8.11.0",
+ "@typescript-eslint/parser": "^8.11.0",
+ "@vitejs/plugin-react-swc": "^3.5.0",
+ "@vitest/coverage-v8": "^1.2.2",
+ "autoprefixer": "^10.4.14",
+ "cross-var": "^1.1.0",
+ "dotenv-cli": "^7.4.2",
+ "eslint": "^9.20.1",
+ "eslint-config-airbnb": "^19.0.4",
+ "eslint-import-resolver-typescript": "^3.6.3",
+ "eslint-plugin-import": "^2.31.0",
+ "eslint-plugin-import-exports-imports-resolver": "^1.0.1",
+ "eslint-plugin-import-newlines": "^1.3.4",
+ "eslint-plugin-jsx-a11y": "^6.10.1",
+ "eslint-plugin-react": "^7.37.4",
+ "eslint-plugin-react-hooks": "^5.0.0",
+ "eslint-plugin-react-refresh": "^0.4.13",
+ "eslint-plugin-simple-import-sort": "^12.1.1",
+ "fast-glob": "^3.3.2",
+ "happy-dom": "^9.18.3",
+ "openapi-typescript": "6.5.5",
+ "postcss": "^8.5.3",
+ "postcss-nested": "^7.0.2",
+ "postcss-normalize": "^13.0.1",
+ "postcss-preset-env": "^10.1.5",
+ "rollup-plugin-visualizer": "^5.9.0",
+ "stylelint": "^16.17.0",
+ "stylelint-config-concentric": "^2.0.2",
+ "stylelint-config-recommended": "^15.0.0",
+ "stylelint-value-no-unknown-custom-properties": "^6.0.1",
+ "surge": "^0.23.1",
+ "ts-md5": "^1.3.1",
+ "tsx": "^4.7.2",
+ "typescript": "^5.5.2",
+ "typescript-eslint": "^8.26.0",
+ "vite": "^5.0.10",
+ "vite-plugin-checker": "^0.7.0",
+ "vite-plugin-compression2": "^0.11.0",
+ "vite-plugin-radar": "^0.9.2",
+ "vite-plugin-svgr": "^4.2.0",
+ "vite-plugin-webfont-dl": "^3.9.4",
+ "vite-tsconfig-paths": "^4.2.2",
+ "vitest": "^1.2.2",
+ "yargs": "^17.7.2"
+ }
+}
diff --git a/go-web-app-develop/app/postcss.config.cjs b/go-web-app-develop/app/postcss.config.cjs
new file mode 100644
index 0000000000000000000000000000000000000000..c1a23f7496e4e16cf1f98ce8dee7885f81b8617c
--- /dev/null
+++ b/go-web-app-develop/app/postcss.config.cjs
@@ -0,0 +1,8 @@
+module.exports = {
+ plugins: [
+ require('postcss-preset-env'),
+ require('postcss-nested'),
+ require('postcss-normalize'),
+ require('autoprefixer'),
+ ],
+};
diff --git a/go-web-app-develop/app/public/go-icon.svg b/go-web-app-develop/app/public/go-icon.svg
new file mode 100644
index 0000000000000000000000000000000000000000..94198ca8d673c10698724c92037ba039d00f74cc
--- /dev/null
+++ b/go-web-app-develop/app/public/go-icon.svg
@@ -0,0 +1,4 @@
+
+
+
+
diff --git a/go-web-app-develop/app/scripts/translatte/README.md b/go-web-app-develop/app/scripts/translatte/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..4e8b1db69c8fa96b3439f2bd53318bc27a6a64a4
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/README.md
@@ -0,0 +1,59 @@
+# translatte
+
+A simple script to synchronize translations in source code to translations in
+server
+
+## Usecase
+
+### Generating migrations
+
+When adding a new feature or updating existing feature or removing an
+existing feature on the codebase, we may need to update the strings used
+in the application.
+
+Developers can change the translations using their preferred choice of editor.
+
+Once all of the changes have been made, we can generate a migration file for the translations using:
+
+```bash
+pnpm translatte generate-migration ./src/translationMigrations ./src/**/i18n.json
+```
+
+Once the migration file has been created, the migration file can be committed to the VCS.
+
+### Applying migrations
+
+When we are deploying the changes to the server, we will need to update
+the strings in the server.
+
+We can generate the new set of strings for the server using:
+
+```bash
+pnpm translatte apply-migrations ./src/translationMigrations --last-migration "name_of_last_migration" --source "strings_json_from_server.json" --destination "new_strings_json_for_server.json"
+```
+
+### Merge migrations
+
+Once the migrations are applied to the strings in the server, we can merge the migrations into a single file.
+
+To merge migrations, we can run the following command:
+
+```bash
+pnpm translatte merge-migrations ./src/translationMigrations --from 'initial_migration.json' --to 'final_migration.json'
+```
+
+### Checking migrations
+
+We can use the following command to check for valid migrations:
+
+```bash
+pnpm translatte lint ./src/**/i18n.json
+```
+
+### Listing migrations
+
+We can use the following command to list all migrations:
+
+```bash
+pnpm translatte list-migrations ./src/translationMigrations
+```
diff --git a/go-web-app-develop/app/scripts/translatte/commands/applyMigrations.test.ts b/go-web-app-develop/app/scripts/translatte/commands/applyMigrations.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..735dbea4cdca797da16b1d3140e5c60894f7e68c
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/commands/applyMigrations.test.ts
@@ -0,0 +1,104 @@
+import { expect } from 'vitest';
+import { mkdirSync } from 'fs';
+import { join } from 'path';
+
+import { testWithTmpDir } from '../testHelpers';
+import {
+ writeFilePromisify,
+ readJsonFilesContents,
+} from '../utils';
+import {
+ migrationContent1,
+ migrationContent2,
+ migrationContent3,
+ migrationContent4,
+ migrationContent5,
+ migrationContent6,
+
+ strings1,
+ strings2,
+} from '../mockData';
+import applyMigrations from './applyMigrations';
+import { SourceFileContent } from '../types';
+
+testWithTmpDir('test applyMigrations with no data in server', async ({ tmpdir }) => {
+ mkdirSync(join(tmpdir, 'migrations'));
+ const migrations = [
+ { name: '000001-1000000000000.json', content: migrationContent1 },
+ { name: '000002-1000000000000.json', content: migrationContent2 },
+ { name: '000003-1000000000000.json', content: migrationContent3 },
+ { name: '000004-1000000000000.json', content: migrationContent4 },
+ { name: '000005-1000000000000.json', content: migrationContent5 },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, 'migrations', name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(migrations);
+
+ mkdirSync(join(tmpdir, 'strings'));
+
+ const emptySourceFile: SourceFileContent = {
+ last_migration: undefined,
+ strings: [],
+ };
+ await writeFilePromisify(
+ join(tmpdir, 'strings', 'before.json'),
+ JSON.stringify(emptySourceFile),
+ 'utf8',
+ );
+
+ await applyMigrations(
+ tmpdir,
+ join(tmpdir, 'strings', 'before.json'),
+ join(tmpdir, 'strings', 'after.json'),
+ 'migrations',
+ ['np'],
+ undefined,
+ false,
+ );
+
+ const newSourceFiles = await readJsonFilesContents([
+ join(tmpdir, 'strings', 'after.json'),
+ ]);
+ const newSourceFileContent = newSourceFiles[0].content;
+
+ expect(newSourceFileContent).toEqual(strings1)
+});
+
+testWithTmpDir('test applyMigrations with data in server', async ({ tmpdir }) => {
+ mkdirSync(join(tmpdir, 'migrations'));
+ const migrations = [
+ { name: '000006-1000000000000.json', content: migrationContent6 },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, 'migrations', name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(migrations);
+
+ mkdirSync(join(tmpdir, 'strings'));
+
+ await writeFilePromisify(
+ join(tmpdir, 'strings', 'before.json'),
+ JSON.stringify(strings1),
+ 'utf8',
+ );
+
+ await applyMigrations(
+ tmpdir,
+ join(tmpdir, 'strings', 'before.json'),
+ join(tmpdir, 'strings', 'after.json'),
+ 'migrations',
+ ['np'],
+ undefined,
+ false,
+ );
+
+ const newSourceFiles = await readJsonFilesContents([
+ join(tmpdir, 'strings', 'after.json'),
+ ]);
+ const newSourceFileContent = newSourceFiles[0].content;
+
+ expect(newSourceFileContent).toEqual(strings2)
+});
diff --git a/go-web-app-develop/app/scripts/translatte/commands/applyMigrations.ts b/go-web-app-develop/app/scripts/translatte/commands/applyMigrations.ts
new file mode 100644
index 0000000000000000000000000000000000000000..dbcb8b39e512be836b3c23818bd2d2ade3bbbff3
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/commands/applyMigrations.ts
@@ -0,0 +1,177 @@
+import { Md5 } from 'ts-md5';
+import { listToMap, isDefined, unique } from '@togglecorp/fujs';
+import { isAbsolute, join, basename } from 'path';
+import {
+ readSource,
+ getMigrationFilesAttrs,
+ readMigrations,
+ writeFilePromisify,
+} from '../utils';
+import { merge } from './mergeMigrations';
+import {
+ SourceFileContent,
+ MigrationFileContent,
+ SourceStringItem,
+} from '../types';
+
+function apply(
+ strings: SourceStringItem[],
+ migrationActions: MigrationFileContent['actions'],
+ languages: string[],
+): SourceStringItem[] {
+ const stringsMapping = listToMap(
+ strings,
+ (item) => `${item.page_name}:${item.key}:${item.language}` as string,
+ (item) => item,
+ );
+
+ const newMapping: {
+ [key: string]: SourceStringItem | null;
+ } = { };
+
+ unique(['en', ...languages]).forEach((language) => {
+ migrationActions.forEach((action) => {
+ const isSourceLanguage = language === 'en';
+ const key = `${action.namespace}:${action.key}:${language}`;
+ if (action.action === 'add') {
+ const hash = Md5.hashStr(action.value);
+
+ const prevValue = stringsMapping[key];
+ // NOTE: we are comparing hash instead of value so that this works for source language as well as other languages
+ if (prevValue && prevValue.hash !== hash) {
+ throw `Add: We already have string with different value for namespace '${action.namespace}' and key '${action.key}'`;
+ }
+
+ if (newMapping[key]) {
+ throw `Add: We already have string for namespace '${action.namespace}' and key '${action.key}' in migration`;
+ }
+
+ newMapping[key] = {
+ hash,
+ key: action.key,
+ page_name: action.namespace,
+ language,
+ value: isSourceLanguage
+ ? action.value
+ : '',
+ };
+ } else if (action.action === 'remove') {
+ // NOTE: We can add or move string so we might have value in newMapping
+ if (!newMapping[key]) {
+ newMapping[key] = null;
+ }
+ } else {
+ const prevValue = stringsMapping[key];
+ if (!prevValue) {
+ throw `Update: We do not have string with namespace '${action.namespace}' and key '${action.key}'`;
+ }
+
+ const newKey = action.newKey ?? prevValue.key;
+ const newNamespace = action.newNamespace ?? prevValue.page_name;
+ const newValue = isSourceLanguage
+ ? action.newValue ?? prevValue.value
+ : prevValue.value;
+ const newHash = isSourceLanguage
+ ? Md5.hashStr(newValue)
+ : prevValue.hash;
+
+ const newCanonicalKey = `${newNamespace}:${newKey}:${language}`;
+
+
+ // NOTE: remove the old key and add new key
+ if (!newMapping[key]) {
+ newMapping[key] = null;
+ }
+
+ const newItem = {
+ hash: newHash,
+ key: newKey,
+ page_name: newNamespace,
+ language,
+ value: newValue,
+ }
+
+ if (newMapping[newCanonicalKey]) {
+ throw `Update: We already have string for namespace '${action.namespace}' and key '${action.key}' in migration`;
+ }
+ newMapping[newCanonicalKey] = newItem;
+ }
+ });
+ });
+
+ const finalMapping: typeof newMapping = {
+ ...stringsMapping,
+ ...newMapping,
+ };
+
+ return Object.values(finalMapping)
+ .filter(isDefined)
+ .sort((foo, bar) => (
+ foo.page_name.localeCompare(bar.page_name)
+ || foo.key.localeCompare(bar.key)
+ || foo.language.localeCompare(bar.language)
+ ))
+}
+
+async function applyMigrations(
+ projectPath: string,
+ sourceFileName: string,
+ destinationFileName: string,
+ migrationFilePath: string,
+ languages: string[],
+ from: string | undefined,
+ dryRun: boolean | undefined,
+) {
+ const sourcePath = isAbsolute(sourceFileName)
+ ? sourceFileName
+ : join(projectPath, sourceFileName)
+ const sourceFile = await readSource(sourcePath)
+
+ const migrationFilesAttrs = await getMigrationFilesAttrs(projectPath, migrationFilePath);
+ const selectedMigrationFilesAttrs = from
+ ? migrationFilesAttrs.filter((item) => (item.migrationName > from))
+ : migrationFilesAttrs;
+
+ console.info(`Found ${selectedMigrationFilesAttrs.length} migration files`);
+
+ if (selectedMigrationFilesAttrs.length < 1) {
+ throw 'There should be at least 1 migration file';
+ }
+
+ const selectedMigrations = await readMigrations(
+ selectedMigrationFilesAttrs.map((migration) => migration.fileName),
+ );
+
+ const lastMigration = selectedMigrations[selectedMigrations.length - 1];
+
+ const mergedMigrationActions = merge(
+ selectedMigrations.map((migration) => migration.content),
+ );
+
+ const outputSourceFileContent: SourceFileContent = {
+ ...sourceFile.content,
+ last_migration: basename(lastMigration.file),
+ strings: apply(
+ sourceFile.content.strings,
+ mergedMigrationActions,
+ languages,
+ ),
+ };
+
+ const destinationPath = isAbsolute(destinationFileName)
+ ? destinationFileName
+ : join(projectPath, destinationFileName)
+
+ if (dryRun) {
+ console.info(`Creating file '${destinationPath}'`);
+ console.info(outputSourceFileContent);
+ } else {
+ await writeFilePromisify(
+ destinationPath,
+ JSON.stringify(outputSourceFileContent, null, 4),
+ 'utf8',
+ );
+ }
+}
+
+export default applyMigrations;
diff --git a/go-web-app-develop/app/scripts/translatte/commands/exportMigration.ts b/go-web-app-develop/app/scripts/translatte/commands/exportMigration.ts
new file mode 100644
index 0000000000000000000000000000000000000000..6572868a188ada0dff7d98ea40efe32d9869fb24
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/commands/exportMigration.ts
@@ -0,0 +1,62 @@
+import xlsx from 'exceljs';
+
+import { readMigrations } from '../utils';
+import { isNotDefined } from '@togglecorp/fujs';
+
+async function exportMigration(
+ migrationFilePath: string,
+ exportFileName: string,
+) {
+ const migrations = await readMigrations(
+ [migrationFilePath]
+ );
+
+ const actions = migrations[0].content.actions;
+ const workbook = new xlsx.Workbook();
+ const now = new Date();
+ workbook.created = now;
+
+ const yyyy = now.getFullYear();
+ const mm = (now.getMonth() + 1).toString().padStart(2, '0');
+ const dd = now.getDate().toString().padStart(2, '0');
+ const worksheet = workbook.addWorksheet(
+ `${yyyy}-${mm}-${dd}`
+ );
+
+ worksheet.columns = [
+ { header: 'Namespace', key: 'namespace' },
+ { header: 'Key', key: 'key' },
+ { header: 'EN', key: 'en' },
+ { header: 'FR', key: 'fr' },
+ { header: 'ES', key: 'es' },
+ { header: 'AR', key: 'ar' },
+ ]
+
+ actions.forEach((actionItem) => {
+ if (actionItem.action === 'remove') {
+ return;
+ }
+
+ if (actionItem.action === 'update' && isNotDefined(actionItem.newValue)) {
+ return;
+ }
+
+ const value = actionItem.action === 'update'
+ ? actionItem.newValue
+ : actionItem.value;
+
+ worksheet.addRow({
+ namespace: actionItem.namespace,
+ key: actionItem.key,
+ en: value,
+ });
+ });
+
+ const fileName = isNotDefined(exportFileName)
+ ? `go-strings-${yyyy}-${mm}-${dd}`
+ : exportFileName;
+
+ await workbook.xlsx.writeFile(`${fileName}.xlsx`);
+}
+
+export default exportMigration;
diff --git a/go-web-app-develop/app/scripts/translatte/commands/generateMigration.test.ts b/go-web-app-develop/app/scripts/translatte/commands/generateMigration.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..680a61d087e7e0b5208dc8a6a3db30c3768ec193
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/commands/generateMigration.test.ts
@@ -0,0 +1,102 @@
+import { expect } from 'vitest';
+import { mkdirSync } from 'fs';
+import { join } from 'path';
+
+import generateMigration from './generateMigration';
+import { testWithTmpDir } from '../testHelpers';
+import { writeFilePromisify, readMigrations } from '../utils';
+import {
+ migrationContent1,
+ migrationContent2,
+ migrationContent3,
+ migrationContent4,
+ migrationContent5,
+ loginContent,
+ registerContent,
+ updatedLoginContent,
+ updatedRegisterContent,
+ migrationContent6,
+} from '../mockData';
+
+
+testWithTmpDir('test generateMigration with no change', async ({ tmpdir }) => {
+ mkdirSync(join(tmpdir, 'migrations'));
+ const migrations = [
+ { name: '000001-1000000000000.json', content: migrationContent1 },
+ { name: '000002-1000000000000.json', content: migrationContent2 },
+ { name: '000003-1000000000000.json', content: migrationContent3 },
+ { name: '000004-1000000000000.json', content: migrationContent4 },
+ { name: '000005-1000000000000.json', content: migrationContent5 },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, 'migrations', name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(migrations);
+
+ mkdirSync(join(tmpdir, 'src'));
+ const translations = [
+ { name: 'home.i18n.json', content: loginContent },
+ { name: 'register.i18n.json', content: registerContent },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, 'src', name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(translations);
+
+ await expect(
+ () => generateMigration(
+ tmpdir,
+ 'migrations',
+ 'src/**/*.i18n.json',
+ new Date().getTime(),
+ false,
+ ),
+ ).rejects.toThrow('Nothing to do');
+});
+
+testWithTmpDir('test generateMigration with change', async ({ tmpdir }) => {
+ mkdirSync(join(tmpdir, 'migrations'));
+ const migrations = [
+ { name: '000001-1000000000000.json', content: migrationContent1 },
+ { name: '000002-1000000000000.json', content: migrationContent2 },
+ { name: '000003-1000000000000.json', content: migrationContent3 },
+ { name: '000004-1000000000000.json', content: migrationContent4 },
+ { name: '000005-1000000000000.json', content: migrationContent5 },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, 'migrations', name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(migrations);
+
+ mkdirSync(join(tmpdir, 'src'));
+
+ const translations = [
+ { name: 'home.i18n.json', content: updatedLoginContent },
+ { name: 'register.i18n.json', content: updatedRegisterContent },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, 'src', name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(translations);
+
+ const timestamp = new Date().getTime();
+
+ await generateMigration(
+ tmpdir,
+ 'migrations',
+ 'src/**/*.i18n.json',
+ timestamp,
+ false,
+ );
+
+ const generatedMigrations = await readMigrations([
+ join(tmpdir, 'migrations', `000006-${timestamp}.json`)
+ ]);
+ const generatedMigrationContent = generatedMigrations[0].content;
+
+ expect(generatedMigrationContent).toEqual(migrationContent6)
+});
diff --git a/go-web-app-develop/app/scripts/translatte/commands/generateMigration.ts b/go-web-app-develop/app/scripts/translatte/commands/generateMigration.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f42e6525d56f426f093ab3406b00e004a66dc9cd
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/commands/generateMigration.ts
@@ -0,0 +1,195 @@
+import { Md5 } from 'ts-md5';
+import { join, isAbsolute } from 'path';
+
+import {
+ writeFilePromisify,
+ oneOneMapping,
+ readTranslations,
+ getTranslationFileNames,
+ getMigrationFilesAttrs,
+ readMigrations,
+ oneOneMappingNonUnique,
+} from '../utils';
+import { MigrationActionItem, MigrationFileContent } from '../types';
+import { merge } from './mergeMigrations';
+
+function getCombinedKey(key: string, namespace: string) {
+ return `${namespace}:${key}`;
+}
+
+type StateItem = {
+ filename?: string;
+ namespace: string;
+ key: string;
+ value: string;
+}
+
+// FIXME: The output should be stable
+function generateMigration(
+ prevState: StateItem[],
+ currentState: StateItem[],
+): MigrationActionItem[] {
+ /*
+ console.info('prevState length', prevState.length);
+ console.info('currentState length', currentState.length);
+ console.info('Total change', Math.abs(prevState.length - currentState.length));
+ */
+
+ const {
+ // Same, key, namespace and same value
+ validCommonItems: identicalStateItems,
+
+ // Same, key, namespace but different value
+ invalidCommonItems: valueUpdatedStateItems,
+
+ // items with different key or namespace or both
+ prevStateRemainder: potentiallyRemovedStateItems,
+
+ // items with different key or namespace or both
+ currentStateRemainder: potentiallyAddedStateItems,
+ } = oneOneMapping(
+ prevState,
+ currentState,
+ ({ key, namespace }) => getCombinedKey(key, namespace),
+ (prev, current) => prev.value === current.value,
+ );
+
+ console.info(`Unchanged strings: ${identicalStateItems.length}`)
+ console.info(`Value updated strings: ${valueUpdatedStateItems.length}`)
+
+ console.info(`Potentially removed: ${potentiallyRemovedStateItems.length}`)
+ console.info(`Potentially added: ${potentiallyAddedStateItems.length}`)
+
+ const {
+ commonItems: namespaceUpdatedStateItems,
+ prevStateRemainder: potentiallyRemovedStateItemsAfterNamespaceChange,
+ currentStateRemainder: potentiallyAddedStateItemsAfterNamespaceChange,
+ } = oneOneMappingNonUnique(
+ potentiallyRemovedStateItems,
+ potentiallyAddedStateItems,
+ (item) => getCombinedKey(item.key, Md5.hashStr(item.value)),
+ );
+
+ const {
+ commonItems: keyUpdatedStateItems,
+ prevStateRemainder: removedStateItems,
+ currentStateRemainder: addedStateItems,
+ } = oneOneMappingNonUnique(
+ potentiallyRemovedStateItemsAfterNamespaceChange,
+ potentiallyAddedStateItemsAfterNamespaceChange,
+ (item) => getCombinedKey(item.namespace, Md5.hashStr(item.value)),
+ );
+
+ console.info(`Namespace updated strings: ${namespaceUpdatedStateItems.length}`)
+ console.info(`Added strings: ${addedStateItems.length}`)
+ console.info(`Removed strings: ${removedStateItems.length}`)
+
+ return [
+ ...valueUpdatedStateItems.map(({ prevStateItem, currentStateItem }) => ({
+ action: 'update' as const,
+ key: prevStateItem.key,
+ namespace: prevStateItem.namespace,
+ newValue: currentStateItem.value,
+ })),
+ ...namespaceUpdatedStateItems.map(({ prevStateItem, currentStateItem }) => ({
+ action: 'update' as const,
+ key: prevStateItem.key,
+ namespace: prevStateItem.namespace,
+ newNamespace: currentStateItem.namespace,
+ })),
+ ...keyUpdatedStateItems.map(({ prevStateItem, currentStateItem }) => ({
+ action: 'update' as const,
+ key: prevStateItem.key,
+ newKey: currentStateItem.key,
+ namespace: prevStateItem.namespace,
+ })),
+ ...addedStateItems.map((item) => ({
+ action: 'add' as const,
+ key: item.key,
+ namespace: item.namespace,
+ value: item.value,
+ })),
+ ...removedStateItems.map((item) => ({
+ action: 'remove' as const,
+ key: item.key,
+ namespace: item.namespace,
+ })),
+ ].sort((foo, bar) => (
+ foo.namespace.localeCompare(bar.namespace)
+ || foo.action.localeCompare(bar.action)
+ || foo.key.localeCompare(bar.key)
+ ));
+}
+
+async function generate(
+ projectPath: string,
+ migrationFilePath: string,
+ translationFileName: string | string[],
+ timestamp: number,
+ dryRun: boolean | undefined,
+) {
+ const migrationFilesAttrs = await getMigrationFilesAttrs(projectPath, migrationFilePath);
+ const selectedMigrationFilesAttrs = migrationFilesAttrs;
+ console.info(`Found ${selectedMigrationFilesAttrs.length} migration files`);
+ const selectedMigrations = await readMigrations(
+ selectedMigrationFilesAttrs.map((migration) => migration.fileName),
+ );
+ const mergedMigrationActions = merge(
+ selectedMigrations.map((migration) => migration.content),
+ );
+
+ const serverState: StateItem[] = mergedMigrationActions.map((item) => {
+ if (item.action !== 'add') {
+ throw `The action should be "add" but found "${item.action}"`;
+ }
+ return {
+ filename: undefined,
+ namespace: item.namespace,
+ key: item.key,
+ value: item.value,
+ }
+ });
+ const translationFiles = await getTranslationFileNames(
+ projectPath,
+ Array.isArray(translationFileName) ? translationFileName : [translationFileName],
+ );
+ const { translations } = await readTranslations(translationFiles);
+ const fileState = translations.map((item) => ({
+ ...item,
+ }));
+
+ const migrationActionItems = generateMigration(
+ serverState,
+ fileState,
+ );
+
+ if (migrationActionItems.length <= 0) {
+ throw 'Nothing to do';
+ }
+
+ const lastMigration = migrationFilesAttrs[migrationFilesAttrs.length - 1];
+
+ const migrationContent: MigrationFileContent = {
+ parent: lastMigration?.migrationName,
+ actions: migrationActionItems,
+ }
+
+ const num = String(Number(lastMigration?.num ?? '000000') + 1).padStart(6, '0');
+
+ const outputMigrationFile = isAbsolute(migrationFilePath)
+ ? join(migrationFilePath, `${num}-${timestamp}.json`)
+ : join(projectPath, migrationFilePath, `${num}-${timestamp}.json`)
+
+ if (dryRun) {
+ console.info(`Creating migration file '${outputMigrationFile}'`);
+ console.info(migrationContent);
+ } else {
+ await writeFilePromisify(
+ outputMigrationFile,
+ JSON.stringify(migrationContent, null, 4),
+ 'utf8',
+ );
+ }
+}
+
+export default generate;
diff --git a/go-web-app-develop/app/scripts/translatte/commands/lint.test.ts b/go-web-app-develop/app/scripts/translatte/commands/lint.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b3f287199a5877a6fcb79a31b6ac34476f0854fe
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/commands/lint.test.ts
@@ -0,0 +1,75 @@
+import { expect } from 'vitest';
+import { join } from 'path';
+import { mkdirSync } from 'fs';
+
+import { loginContent, registerContent } from '../mockData';
+import { testWithTmpDir } from '../testHelpers';
+import { writeFilePromisify } from '../utils';
+import lint from './lint';
+
+testWithTmpDir('test lint with duplicate file', async ({ tmpdir }) => {
+ mkdirSync(join(tmpdir, 'i18n'));
+
+ const writes = [
+ { name: 'login.i18n.json', content: loginContent },
+ { name: 'register.i18n.json', content: registerContent },
+ { name: 'register-form.i18n.json', content: registerContent },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, 'i18n', name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(writes);
+
+ await expect(
+ () => lint(tmpdir, ['**/*.i18n.json'], false)
+ ).rejects.toThrow('Found 12 duplicated strings.');
+});
+
+testWithTmpDir('test lint with duplicate string and same text', async ({ tmpdir }) => {
+ mkdirSync(join(tmpdir, 'i18n'));
+
+ const writes = [
+ { name: 'login.i18n.json', content: loginContent },
+ { name: 'register.i18n.json', content: registerContent },
+ { name: 'register-form.i18n.json', content: {
+ namespace: 'register',
+ strings: {
+ firstNameLabel: 'First Name',
+ },
+ } },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, 'i18n', name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(writes);
+
+ await expect(
+ () => lint(tmpdir, ['**/*.i18n.json'], false)
+ ).rejects.toThrow('Found 2 duplicated strings.');
+});
+
+testWithTmpDir('test lint with duplicate string and different text', async ({ tmpdir }) => {
+ mkdirSync(join(tmpdir, 'i18n'));
+
+ const writes = [
+ { name: 'login.i18n.json', content: loginContent },
+ { name: 'register.i18n.json', content: registerContent },
+ { name: 'register-form.i18n.json', content: {
+ namespace: 'register',
+ strings: {
+ firstNameLabel: 'First Name*',
+ },
+ } },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, 'i18n', name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(writes);
+
+ await expect(
+ () => lint(tmpdir, ['**/*.i18n.json'], false)
+ ).rejects.toThrow('Found 2 duplicated strings.');
+});
diff --git a/go-web-app-develop/app/scripts/translatte/commands/lint.ts b/go-web-app-develop/app/scripts/translatte/commands/lint.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c0b8acab693965572d34f60aee1eaf11eeef47ed
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/commands/lint.ts
@@ -0,0 +1,106 @@
+import { listToMap } from '@togglecorp/fujs';
+
+import {
+ getDuplicateItems,
+ getTranslationFileNames,
+ readTranslations,
+ writeFilePromisify,
+} from '../utils';
+
+function lowercaseFirstChar(chars: string) {
+ return chars.charAt(0).toLowerCase() + chars.slice(1);
+}
+
+async function lint(
+ projectPath: string,
+ translationFileName: string[],
+ fix: boolean | undefined,
+) {
+ const fileNames = await getTranslationFileNames(projectPath, translationFileName);
+ const { translations, filesContents } = await readTranslations(fileNames);
+
+ const namespaces = new Set(translations.map((item) => item.namespace));
+
+ console.info(`Found ${namespaces.size} namespaces.`);
+ console.info(`Found ${translations.length} strings.`);
+
+ const duplicates = getDuplicateItems(
+ translations,
+ (string) => `${string.namespace}:${string.key}`,
+ );
+
+ if (duplicates.length > 0) {
+ console.info(JSON.stringify(duplicates, null, 2));
+ throw `Found ${duplicates.length} duplicated strings.`;
+ }
+
+ // FIXME: We should get these custom rules from config file later
+ const customRules: {
+ location: string,
+ namespace: ((match: RegExpMatchArray) => string) | string;
+ }[] = [
+ { location: '.*/app/src/views/(\\w+)/(?:.*/)?i18n.json$', namespace: (match) => lowercaseFirstChar(match[1]) },
+ { location: '.*/app/src/components/domain/(\\w+)/(?:.*/)?i18n.json$', namespace: (match) => lowercaseFirstChar(match[1]) },
+ { location: '.*/app/src/.*/i18n.json$', namespace: 'common' },
+ { location: '.*/packages/ui/src/.*/i18n.json$', namespace: 'common' },
+ ];
+
+ const namespaceErrors: {
+ fileName: string,
+ expectedNamespace: string,
+ receivedNamespace: string,
+ }[] = [];
+ for (const item of filesContents) {
+ const { file: fileName, content: { namespace } } = item;
+ for (const rule of customRules) {
+ const match = fileName.match(new RegExp(rule.location));
+ if (match) {
+ const correctNamespace = typeof rule.namespace === 'string'
+ ? rule.namespace
+ : rule.namespace(match);
+ if (correctNamespace !== namespace) {
+ namespaceErrors.push({
+ fileName,
+ expectedNamespace: correctNamespace,
+ receivedNamespace: namespace,
+ })
+ }
+ break;
+ }
+ };
+ };
+
+ if (namespaceErrors.length > 0) {
+ if (fix) {
+ const metadataMapping = listToMap(
+ filesContents,
+ (fileContents) => fileContents.file,
+ (fileContents) => fileContents.content,
+ );
+ const updates = namespaceErrors.map((namespaceError) => {
+ const content = metadataMapping[namespaceError.fileName];
+ const updatedContent = {
+ ...content,
+ namespace: namespaceError.expectedNamespace,
+ }
+ return writeFilePromisify(
+ namespaceError.fileName,
+ JSON.stringify(updatedContent, null, 4),
+ 'utf8',
+ );
+ });
+ await Promise.all(updates);
+ console.info(`Fixed namespace in ${namespaceErrors.length} files`);
+ } else {
+ console.info(JSON.stringify(namespaceErrors, null, 2));
+ throw `Found ${namespaceErrors.length} issues with namespaces.`;
+ }
+ }
+
+ // TODO: Throw error
+ // - if the naming of migration files is not correct
+ // - if the parent field is not correct
+ // - if we have duplicates
+}
+
+export default lint;
diff --git a/go-web-app-develop/app/scripts/translatte/commands/listMigrations.test.ts b/go-web-app-develop/app/scripts/translatte/commands/listMigrations.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..5fa7a3699e0648bc5c38b69392d39ecb20baa7c8
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/commands/listMigrations.test.ts
@@ -0,0 +1,48 @@
+import { expect } from 'vitest';
+import { join } from 'path';
+import { mkdirSync } from 'fs';
+
+import { testWithTmpDir } from '../testHelpers';
+import { writeFilePromisify, getMigrationFilesAttrs } from '../utils';
+import {
+ migrationContent1,
+ migrationContent2,
+ migrationContent3,
+ migrationContent4,
+ migrationContent5,
+} from '../mockData';
+
+testWithTmpDir('test listMigrations', async ({ tmpdir }) => {
+ mkdirSync(join(tmpdir, 'migrations'));
+
+ const writes = [
+ { name: '001-1000000000000.json', content: migrationContent1 },
+ { name: '002-1000000000000.json', content: migrationContent2 },
+ { name: '003-1000000000000.json', content: migrationContent3 },
+ { name: '004-1000000000000.json', content: migrationContent4 },
+ { name: '005-1000000000000.json', content: migrationContent5 },
+
+ { name: 'xyz-1000000000000.json', content: migrationContent5 },
+ { name: '006-abcdefghijklm.json', content: migrationContent5 },
+ { name: '005-1000000000000', content: migrationContent5 },
+ { name: 'migration-6.json', content: migrationContent5 },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, 'migrations', name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(writes);
+
+ expect(
+ (await getMigrationFilesAttrs(
+ tmpdir,
+ 'migrations',
+ )).map((item) => ({ ...item, fileName: undefined })),
+ ).toEqual([
+ { migrationName: '001-1000000000000.json', num: '001', timestamp: '1000000000000' },
+ { migrationName: '002-1000000000000.json', num: '002', timestamp: '1000000000000' },
+ { migrationName: '003-1000000000000.json', num: '003', timestamp: '1000000000000' },
+ { migrationName: '004-1000000000000.json', num: '004', timestamp: '1000000000000' },
+ { migrationName: '005-1000000000000.json', num: '005', timestamp: '1000000000000' },
+ ]);
+});
diff --git a/go-web-app-develop/app/scripts/translatte/commands/listMigrations.ts b/go-web-app-develop/app/scripts/translatte/commands/listMigrations.ts
new file mode 100644
index 0000000000000000000000000000000000000000..f131ec13b018f6cb98fe94e4be9b2960fcb8f827
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/commands/listMigrations.ts
@@ -0,0 +1,11 @@
+import { getMigrationFilesAttrs } from '../utils';
+
+async function listMigrations(projectPath: string, path: string) {
+ const migrationFileAttrs = await getMigrationFilesAttrs(projectPath, path);
+ console.info(`Found ${migrationFileAttrs.length} migration files.`);
+ if (migrationFileAttrs.length > 0) {
+ console.info(migrationFileAttrs);
+ }
+}
+
+export default listMigrations;
diff --git a/go-web-app-develop/app/scripts/translatte/commands/mergeMigrations.test.ts b/go-web-app-develop/app/scripts/translatte/commands/mergeMigrations.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..33c6c705480b43bc9a6ec7d534c980ac0438867f
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/commands/mergeMigrations.test.ts
@@ -0,0 +1,371 @@
+import { expect, test } from 'vitest';
+import { existsSync } from 'fs';
+import { join } from 'path';
+
+import mergeMigrations, { merge } from './mergeMigrations';
+import { testWithTmpDir } from '../testHelpers';
+import { writeFilePromisify, readMigrations } from '../utils';
+import {
+ migrationContent1,
+ migrationContent2,
+ migrationContent3,
+ migrationContent4,
+ migrationContent5,
+} from '../mockData';
+
+test('Test merge migrations 1-5', () => {
+ expect(merge([
+ migrationContent1,
+ migrationContent2,
+ migrationContent3,
+ migrationContent4,
+ migrationContent5,
+ ])).toEqual([
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "emailLabel",
+ "value": "Email/Username"
+ },
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "emailPlaceholder",
+ "value": "Email/Username*"
+ },
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "passwordLabel",
+ "value": "Password"
+ },
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "loginButton",
+ "value": "Login"
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "firstNameLabel",
+ "value": "First Name"
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "lastNameLabel",
+ "value": "Last Name"
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "emailLabel",
+ "value": "Email"
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "passwordLabel",
+ "value": "Password"
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "confirmPasswordLabel",
+ "value": "Confirm Password"
+ },
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "header",
+ "value": "If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with you email and password."
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "registerButton",
+ "value": "Register"
+ },
+ ]);
+});
+
+test('Test merge migrations 2-5', () => {
+ expect(merge([
+ migrationContent2,
+ migrationContent3,
+ migrationContent4,
+ migrationContent5,
+ ])).toEqual([
+ {
+ "action": "update",
+ "namespace": "login",
+ "key": "emailLabel",
+ "newValue": "Email/Username"
+ },
+ {
+ "action": "update",
+ "namespace": "login",
+ "key": "passwordLabel",
+ "newValue": "Password"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "firstNameLabel",
+ "newValue": "First Name"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "lastNameLabel",
+ "newValue": "Last Name"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "emailLabel",
+ "newValue": "Email"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "passwordLabel",
+ "newValue": "Password"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "confirmPasswordLabel",
+ "newValue": "Confirm Password"
+ },
+ {
+ "action": "update",
+ "key": "signUpButton",
+ "namespace": "register",
+ "newKey": "registerButton",
+ "newNamespace": undefined,
+ "newValue": "Register",
+ },
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "header",
+ "value": "If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with you email and password."
+ },
+ {
+ "action": "remove",
+ "namespace": "home",
+ "key": "header"
+ },
+ {
+ "action": "remove",
+ "namespace": "home",
+ "key": "subHeader"
+ },
+ ]);
+});
+
+test('Test merge migrations 3-5', () => {
+ expect(merge([
+ migrationContent3,
+ migrationContent4,
+ migrationContent5,
+ ])).toEqual([
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "header",
+ "value": "If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with you email and password."
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "signUpButton",
+ "newKey": "registerButton"
+ },
+ {
+ "action": "remove",
+ "namespace": "home",
+ "key": "header"
+ },
+ {
+ "action": "remove",
+ "namespace": "home",
+ "key": "subHeader"
+ },
+ ]);
+});
+
+test('Test merge migrations 4-5', () => {
+ expect(merge([
+ migrationContent4,
+ migrationContent5,
+ ])).toEqual([
+ {
+ "action": "remove",
+ "namespace": "login",
+ "key": "header"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "header",
+ "newNamespace": "login"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "signUpButton",
+ "newKey": "registerButton"
+ },
+ {
+ "action": "remove",
+ "namespace": "home",
+ "key": "header"
+ },
+ {
+ "action": "remove",
+ "namespace": "home",
+ "key": "subHeader"
+ },
+ ]);
+});
+
+test('Test merge migrations 5-5', () => {
+ expect(merge([
+ migrationContent5,
+ ])).toEqual(migrationContent5.actions)
+})
+
+testWithTmpDir('test mergeMigrations 1-5', async ({ tmpdir }) => {
+ const writes = [
+ { name: '000001-1000000000000.json', content: migrationContent1 },
+ { name: '000002-1000000000000.json', content: migrationContent2 },
+ { name: '000003-1000000000000.json', content: migrationContent3 },
+ { name: '000004-1000000000000.json', content: migrationContent4 },
+ { name: '000005-1000000000000.json', content: migrationContent5 },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(writes);
+
+ await mergeMigrations(
+ tmpdir,
+ '.',
+ '000001-1000000000000.json',
+ '000005-1000000000000.json',
+ false,
+ );
+
+ expect(existsSync(join(tmpdir, '000001-1000000000000.json'))).toBeFalsy();
+ expect(existsSync(join(tmpdir, '000002-1000000000000.json'))).toBeFalsy();
+ expect(existsSync(join(tmpdir, '000003-1000000000000.json'))).toBeFalsy();
+ expect(existsSync(join(tmpdir, '000004-1000000000000.json'))).toBeFalsy();
+ expect(existsSync(join(tmpdir, '000005-1000000000000.json'))).toBeTruthy();
+
+ const generatedFiles = await readMigrations([join(tmpdir, '000005-1000000000000.json')]);
+ const generatedFile = generatedFiles[0];
+ expect(generatedFile.content.parent).toBe(undefined);
+});
+
+testWithTmpDir('test mergeMigrations 2-5', async ({ tmpdir }) => {
+ const writes = [
+ { name: '000001-1000000000000.json', content: migrationContent1 },
+ { name: '000002-1000000000000.json', content: migrationContent2 },
+ { name: '000003-1000000000000.json', content: migrationContent3 },
+ { name: '000004-1000000000000.json', content: migrationContent4 },
+ { name: '000005-1000000000000.json', content: migrationContent5 },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(writes);
+
+ await mergeMigrations(
+ tmpdir,
+ '.',
+ '000002-1000000000000.json',
+ '000005-1000000000000.json',
+ false,
+ );
+
+ expect(existsSync(join(tmpdir, '000001-1000000000000.json'))).toBeTruthy();
+ expect(existsSync(join(tmpdir, '000002-1000000000000.json'))).toBeFalsy();
+ expect(existsSync(join(tmpdir, '000003-1000000000000.json'))).toBeFalsy();
+ expect(existsSync(join(tmpdir, '000004-1000000000000.json'))).toBeFalsy();
+ expect(existsSync(join(tmpdir, '000005-1000000000000.json'))).toBeTruthy();
+
+ const generatedFiles = await readMigrations([join(tmpdir, '000005-1000000000000.json')]);
+ const generatedFile = generatedFiles[0];
+ expect(generatedFile.content.parent).toBe('000001-1000000000000');
+});
+
+testWithTmpDir('test mergeMigrations 3-5', async ({ tmpdir }) => {
+ const writes = [
+ { name: '000001-1000000000000.json', content: migrationContent1 },
+ { name: '000002-1000000000000.json', content: migrationContent2 },
+ { name: '000003-1000000000000.json', content: migrationContent3 },
+ { name: '000004-1000000000000.json', content: migrationContent4 },
+ { name: '000005-1000000000000.json', content: migrationContent5 },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(writes);
+
+ await mergeMigrations(
+ tmpdir,
+ '.',
+ '000003-1000000000000.json',
+ '000005-1000000000000.json',
+ false,
+ );
+
+ expect(existsSync(join(tmpdir, '000001-1000000000000.json'))).toBeTruthy();
+ expect(existsSync(join(tmpdir, '000002-1000000000000.json'))).toBeTruthy();
+ expect(existsSync(join(tmpdir, '000003-1000000000000.json'))).toBeFalsy();
+ expect(existsSync(join(tmpdir, '000004-1000000000000.json'))).toBeFalsy();
+ expect(existsSync(join(tmpdir, '000005-1000000000000.json'))).toBeTruthy();
+
+ const generatedFiles = await readMigrations([join(tmpdir, '000005-1000000000000.json')]);
+ const generatedFile = generatedFiles[0];
+ expect(generatedFile.content.parent).toBe('000002-1000000000000');
+});
+
+testWithTmpDir('test mergeMigrations 4-5', async ({ tmpdir }) => {
+ const writes = [
+ { name: '000001-1000000000000.json', content: migrationContent1 },
+ { name: '000002-1000000000000.json', content: migrationContent2 },
+ { name: '000003-1000000000000.json', content: migrationContent3 },
+ { name: '000004-1000000000000.json', content: migrationContent4 },
+ { name: '000005-1000000000000.json', content: migrationContent5 },
+ ].map(({ name, content }) => writeFilePromisify(
+ join(tmpdir, name),
+ JSON.stringify(content, null, 4),
+ 'utf8',
+ ));
+ await Promise.all(writes);
+
+ await mergeMigrations(
+ tmpdir,
+ '.',
+ '000004-1000000000000.json',
+ '000005-1000000000000.json',
+ false,
+ );
+
+ expect(existsSync(join(tmpdir, '000001-1000000000000.json'))).toBeTruthy();
+ expect(existsSync(join(tmpdir, '000002-1000000000000.json'))).toBeTruthy();
+ expect(existsSync(join(tmpdir, '000003-1000000000000.json'))).toBeTruthy();
+ expect(existsSync(join(tmpdir, '000004-1000000000000.json'))).toBeFalsy();
+ expect(existsSync(join(tmpdir, '000005-1000000000000.json'))).toBeTruthy();
+
+ const generatedFiles = await readMigrations([join(tmpdir, '000005-1000000000000.json')]);
+ const generatedFile = generatedFiles[0];
+ expect(generatedFile.content.parent).toBe('000003-1000000000000');
+});
diff --git a/go-web-app-develop/app/scripts/translatte/commands/mergeMigrations.ts b/go-web-app-develop/app/scripts/translatte/commands/mergeMigrations.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4be6b0f0abc1389e7a7ce62d05260d57e68739ad
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/commands/mergeMigrations.ts
@@ -0,0 +1,220 @@
+import { listToMap, isDefined } from '@togglecorp/fujs';
+
+import { MigrationActionItem, MigrationFileContent } from '../types';
+import {
+ concat,
+ removeUndefinedKeys,
+ getMigrationFilesAttrs,
+ readMigrations,
+ removeFiles,
+ writeFilePromisify
+} from '../utils';
+
+function getCanonicalKey(
+ item: MigrationActionItem,
+ opts: { useNewKey: boolean },
+) {
+ if (opts.useNewKey && item.action === 'update') {
+ return concat(
+ item.newNamespace ?? item.namespace,
+ item.newKey ?? item.key,
+ );
+ }
+ return concat(
+ item.namespace,
+ item.key,
+ );
+}
+
+function mergeMigrationActionItems(
+ prevMigrationActionItems: MigrationActionItem[],
+ nextMigrationActionItems: MigrationActionItem[],
+) {
+ interface PrevMappings {
+ [key: string]: MigrationActionItem,
+ }
+
+ const prevCanonicalKeyMappings: PrevMappings = listToMap(
+ prevMigrationActionItems,
+ (item) => getCanonicalKey(item, { useNewKey: true }),
+ (item) => item,
+ );
+
+ interface NextMappings {
+ [key: string]: MigrationActionItem | null,
+ }
+
+ const nextMappings = nextMigrationActionItems.reduce(
+ (acc, nextMigrationActionItem) => {
+ const canonicalKey = getCanonicalKey(nextMigrationActionItem, { useNewKey: false })
+
+ const prevItemWithCanonicalKey = prevCanonicalKeyMappings[canonicalKey];
+ // const prevItemWithKey = prevKeyMappings[nextMigrationActionItem.key];
+
+ if (!prevItemWithCanonicalKey) {
+ return {
+ ...acc,
+ [canonicalKey]: nextMigrationActionItem,
+ };
+ }
+
+ if (prevItemWithCanonicalKey.action === 'add' && nextMigrationActionItem.action === 'add') {
+ throw `Action 'add' already exists for '${canonicalKey}'`;
+ }
+ if (prevItemWithCanonicalKey.action === 'add' && nextMigrationActionItem.action === 'remove') {
+ return {
+ ...acc,
+ [canonicalKey]: null,
+ };
+ }
+ if (prevItemWithCanonicalKey.action === 'add' && nextMigrationActionItem.action === 'update') {
+ const newKey = nextMigrationActionItem.newKey
+ ?? prevItemWithCanonicalKey.key;
+ const newNamespace = nextMigrationActionItem.newNamespace
+ ?? prevItemWithCanonicalKey.namespace;
+
+ const newMigrationItem = removeUndefinedKeys({
+ action: 'add',
+ namespace: newNamespace,
+ key: newKey,
+ value: nextMigrationActionItem.newValue
+ ?? prevItemWithCanonicalKey.value,
+ });
+
+ const newCanonicalKey = getCanonicalKey(newMigrationItem, { useNewKey: true });
+ if (acc[newCanonicalKey] !== undefined && acc[newCanonicalKey] !== null) {
+ throw `Action 'update' cannot be applied to '${newCanonicalKey}' as the key already exists`;
+ }
+
+ return {
+ ...acc,
+ // Setting null so that we remove them on the mappings.
+ // No need to set null, if we have already overridden with other value
+ [canonicalKey]: acc[canonicalKey] === undefined || acc[canonicalKey] === null
+ ? null
+ : acc[canonicalKey],
+ [newCanonicalKey]: newMigrationItem,
+ }
+ }
+ if (prevItemWithCanonicalKey.action === 'remove' && nextMigrationActionItem.action === 'add') {
+ return {
+ ...acc,
+ [canonicalKey]: removeUndefinedKeys({
+ action: 'update',
+ namespace: prevItemWithCanonicalKey.namespace,
+ key: prevItemWithCanonicalKey.key,
+ newValue: nextMigrationActionItem.value,
+ })
+ };
+ }
+ if (prevItemWithCanonicalKey.action === 'remove' && nextMigrationActionItem.action === 'remove') {
+ // pass
+ return acc;
+ }
+ if (prevItemWithCanonicalKey.action === 'remove' && nextMigrationActionItem.action === 'update') {
+ throw `Action 'update' cannot be applied to '${canonicalKey}' after action 'remove'`;
+ }
+ if (prevItemWithCanonicalKey.action === 'update' && nextMigrationActionItem.action === 'add') {
+ throw `Action 'add' cannot be applied to '${canonicalKey}' after action 'update'`;
+ }
+ if (prevItemWithCanonicalKey.action === 'update' && nextMigrationActionItem.action === 'update') {
+ return {
+ ...acc,
+ [canonicalKey]: removeUndefinedKeys({
+ action: 'update',
+ namespace: prevItemWithCanonicalKey.namespace,
+ key: prevItemWithCanonicalKey.key,
+ newNamespace: nextMigrationActionItem.newNamespace ?? prevItemWithCanonicalKey.newNamespace,
+ newKey: nextMigrationActionItem.newKey ?? prevItemWithCanonicalKey.newKey,
+ newValue: nextMigrationActionItem.newValue ?? prevItemWithCanonicalKey.newValue,
+ }),
+ };
+ }
+ if (prevItemWithCanonicalKey.action === 'update' && nextMigrationActionItem.action === 'remove') {
+ return {
+ ...acc,
+ [canonicalKey]: removeUndefinedKeys({
+ action: 'remove',
+ namespace: prevItemWithCanonicalKey.namespace,
+ key: prevItemWithCanonicalKey.key,
+ }),
+ };
+ }
+ return acc;
+ },
+ {},
+ );
+
+ const finalMappings = {
+ ...prevCanonicalKeyMappings,
+ ...nextMappings,
+ };
+
+ return Object.values(finalMappings).filter(isDefined);
+}
+
+export function merge(migrationFileContents: MigrationFileContent[]) {
+ const migrationActionItems = migrationFileContents.reduce(
+ (acc, migrationActionItem) => {
+ const newMigrationItems = mergeMigrationActionItems(acc, migrationActionItem.actions)
+ return newMigrationItems;
+ },
+ [],
+ );
+
+ return migrationActionItems;
+}
+
+async function mergeMigrations(
+ projectPath: string,
+ path: string,
+ from: string,
+ to: string,
+ dryRun: boolean | undefined,
+) {
+ const migrationFilesAttrs = await getMigrationFilesAttrs(projectPath, path);
+ const selectedMigrationFilesAttrs = migrationFilesAttrs.filter(
+ (item) => (item.migrationName >= from && item.migrationName <= to)
+ );
+ console.info(`Found ${selectedMigrationFilesAttrs.length} migration files`);
+
+ if (selectedMigrationFilesAttrs.length <= 1) {
+ throw 'There should be atleast 2 migration files';
+ }
+ const selectedMigrations = await readMigrations(
+ selectedMigrationFilesAttrs.map((migration) => migration.fileName),
+ );
+
+ const firstMigration= selectedMigrations[0];
+ const lastMigration = selectedMigrations[selectedMigrations.length - 1];
+
+ const selectedMigrationsFileNames = selectedMigrationFilesAttrs.map((migration) => migration.fileName);
+
+ const mergedMigrationContent = {
+ actions: merge(selectedMigrations.map((migration) => migration.content)),
+ parent: firstMigration.content.parent,
+ };
+
+ if (dryRun) {
+ console.info('Deleting the following migration files');
+ console.info(selectedMigrationsFileNames);
+ } else {
+ await removeFiles(
+ selectedMigrationsFileNames,
+ );
+ }
+
+ const newFileName = lastMigration.file;
+ if (dryRun) {
+ console.info(`Creating migration file '${newFileName}'`);
+ console.info(mergedMigrationContent);
+ } else {
+ await writeFilePromisify(
+ newFileName,
+ JSON.stringify(mergedMigrationContent, null, 4),
+ 'utf8',
+ );
+ }
+}
+
+export default mergeMigrations;
diff --git a/go-web-app-develop/app/scripts/translatte/main.ts b/go-web-app-develop/app/scripts/translatte/main.ts
new file mode 100644
index 0000000000000000000000000000000000000000..079f9c7472fa335937ef8eb6947127f6940d108e
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/main.ts
@@ -0,0 +1,193 @@
+import yargs from 'yargs/yargs';
+import { hideBin } from 'yargs/helpers';
+import { cwd } from 'process';
+
+import lint from './commands/lint';
+import listMigrations from './commands/listMigrations';
+import mergeMigrations from './commands/mergeMigrations';
+
+import applyMigrations from './commands/applyMigrations';
+import generateMigration from './commands/generateMigration';
+import exportMigration from './commands/exportMigration';
+import { join, basename } from 'path';
+
+const currentDir = cwd();
+
+// CLI
+
+yargs(hideBin(process.argv))
+ .scriptName('translatte')
+ .usage('$0 [args]')
+ .demandCommand(1)
+ .command(
+ 'lint ',
+ 'Lint i18n.json files for duplicated strings',
+ (yargs) => {
+ yargs.positional('TRANSLATION_FILE', {
+ type: 'string',
+ describe: 'Read the files from TRANSLATION_FILE',
+ });
+ yargs.options({
+ 'fix': {
+ type: 'boolean',
+ description: 'Fix fixable issues',
+ },
+ });
+ },
+ async (argv) => {
+ await lint(currentDir, argv.TRANSLATION_FILE as string[], argv.fix as boolean | undefined);
+ },
+ )
+ .command(
+ 'list-migrations ',
+ 'List migration files',
+ (yargs) => {
+ yargs.positional('MIGRATION_FILE_PATH', {
+ type: 'string',
+ describe: 'Find the migration files on MIGRATION_FILE_PATH',
+ });
+ },
+ async (argv) => {
+ await listMigrations(currentDir, argv.MIGRATION_FILE_PATH as string);
+ },
+ )
+ .command(
+ 'merge-migrations ',
+ 'Merge migration files',
+ (yargs) => {
+ yargs.positional('MIGRATION_FILE_PATH', {
+ type: 'string',
+ describe: 'Find the migration files on MIGRATION_FILE_PATH',
+ });
+ yargs.options({
+ 'from': {
+ type: 'string',
+ description: 'The first file that will be included in the merge',
+ demandOption: true,
+ },
+ 'to': {
+ type: 'string',
+ description: 'The to file that will be included in the merge',
+ demandOption: true,
+ },
+ 'dry-run': {
+ alias: 'd',
+ description: 'Dry run',
+ type: 'boolean',
+ },
+ });
+ },
+ async (argv) => {
+ await mergeMigrations(
+ currentDir,
+ argv.MIGRATION_FILE_PATH as string,
+ argv.from as string,
+ argv.to as string,
+ argv.dryRun as (boolean | undefined),
+ );
+ },
+ )
+ .command(
+ 'apply-migrations ',
+ 'Apply migrations',
+ (yargs) => {
+ yargs.positional('MIGRATION_FILE_PATH', {
+ type: 'string',
+ describe: 'Find the migration file on MIGRATION_FILE_PATH',
+ });
+ yargs.options({
+ 'dry-run': {
+ alias: 'd',
+ description: 'Dry run',
+ type: 'boolean',
+ },
+ 'last-migration': {
+ type: 'string',
+ description: 'The file after which the migration will be applied',
+ },
+ 'source': {
+ type: 'string',
+ description: 'The source file to which migration is applied',
+ demandOption: true,
+ },
+ 'destination': {
+ type: 'string',
+ description: 'The file where new source file is saved',
+ demandOption: true,
+ },
+ });
+ },
+ async (argv) => {
+ await applyMigrations(
+ currentDir,
+ argv.SOURCE_FILE as string,
+ argv.DESTINATION_FILE as string,
+ argv.MIGRATION_FILE_PATH as string,
+ ['es', 'ar', 'fr'],
+ argv.lastMigration as (string | undefined),
+ argv.dryRun as (boolean | undefined),
+ );
+ },
+ )
+ .command(
+ 'generate-migration ',
+ 'Generate migration file',
+ (yargs) => {
+ yargs.positional('MIGRATION_FILE_PATH', {
+ type: 'string',
+ describe: 'Find the migration files on MIGRATION_FILE_PATH',
+ });
+ yargs.positional('TRANSLATION_FILE', {
+ type: 'string',
+ describe: 'Read the files from TRANSLATION_FILE',
+ });
+ yargs.options({
+ 'dry-run': {
+ alias: 'd',
+ description: 'Dry run',
+ type: 'boolean',
+ },
+ });
+ },
+ async (argv) => {
+ await generateMigration(
+ currentDir,
+ argv.MIGRATION_FILE_PATH as string,
+ argv.TRANSLATION_FILE as string,
+ new Date().getTime(),
+ argv.dryRun as (boolean | undefined),
+ );
+ },
+ )
+ .command(
+ 'export-migration ',
+ 'Export migration file to excel format which can be used to translate the new and updated strings',
+ (yargs) => {
+ yargs.positional('MIGRATION_FILE_PATH', {
+ type: 'string',
+ describe: 'Find the migration file on MIGRATION_FILE_PATH',
+ });
+ yargs.positional('OUTPUT_DIR', {
+ type: 'string',
+ describe: 'Directory where the output xlsx should be saved',
+ });
+ },
+ async (argv) => {
+ const migrationFilePath = (argv.MIGRATION_FILE_PATH as string);
+
+ const outputDir = argv.OUTPUT_DIR as string;
+
+ // Get only the filename without extension
+ const exportFileName = basename(migrationFilePath, '.json');
+
+ const exportFilePath = join(outputDir, exportFileName);
+
+ await exportMigration(
+ argv.MIGRATION_FILE_PATH as string,
+ exportFilePath,
+ );
+ },
+ )
+ .strictCommands()
+ .showHelpOnFail(false)
+ .parse()
diff --git a/go-web-app-develop/app/scripts/translatte/mockData.ts b/go-web-app-develop/app/scripts/translatte/mockData.ts
new file mode 100644
index 0000000000000000000000000000000000000000..a148c129fc6cf00c96c55ef734a8ed3231c3a46c
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/mockData.ts
@@ -0,0 +1,655 @@
+import {
+ MigrationFileContent,
+ TranslationFileContent,
+ SourceFileContent,
+} from './types';
+
+export const migrationContent1: MigrationFileContent = {
+ "actions": [
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "emailLabel",
+ "value": "Email/Username*"
+ },
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "emailPlaceholder",
+ "value": "Email/Username*"
+ },
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "passwordLabel",
+ "value": "Password*"
+ },
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "loginButton",
+ "value": "Login"
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "firstNameLabel",
+ "value": "First Name*"
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "lastNameLabel",
+ "value": "Last Name*"
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "emailLabel",
+ "value": "Email*"
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "passwordLabel",
+ "value": "Password*"
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "confirmPasswordLabel",
+ "value": "Confirm Password*"
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "signUpButton",
+ "value": "Sign Up"
+ },
+ {
+ "action": "add",
+ "namespace": "home",
+ "key": "header",
+ "value": "IFRC Disaster Response and Preparedness"
+ },
+ {
+ "action": "add",
+ "namespace": "home",
+ "key": "subHeader",
+ "value": "IFRC GO aims to make all disaster information universally accessible and useful to IFRC responders for better decision making."
+ }
+ ],
+};
+// Story: using the migration file below
+// 1. we need to remove the asterisks from all the labels
+// 2. we need to use register consistenlty, so use "Register" instead of "Sign Up"
+export const migrationContent2: MigrationFileContent = {
+ "parent": "000001-1000000000000",
+ "actions": [
+ {
+ "action": "update",
+ "namespace": "login",
+ "key": "emailLabel",
+ "newValue": "Email/Username"
+ },
+ {
+ "action": "update",
+ "namespace": "login",
+ "key": "passwordLabel",
+ "newValue": "Password"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "firstNameLabel",
+ "newValue": "First Name"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "lastNameLabel",
+ "newValue": "Last Name"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "emailLabel",
+ "newValue": "Email"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "passwordLabel",
+ "newValue": "Password"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "confirmPasswordLabel",
+ "newValue": "Confirm Password"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "signUpButton",
+ "newValue": "Register"
+ }
+ ],
+};
+// Story: using the migration file below
+// 1. we need to add header for login and register page
+export const migrationContent3: MigrationFileContent = {
+ "parent": "000002-1000000000000",
+ "actions": [
+ {
+ "action": "add",
+ "namespace": "login",
+ "key": "header",
+ "value": "Staff, members and volunteers of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) are welcome to register for a user account on GO, to access information for the Membership. Other responders and members of the public may browse the public areas of the site without registering for an account."
+ },
+ {
+ "action": "add",
+ "namespace": "register",
+ "key": "header",
+ "value": "If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with you email and password."
+ }
+ ],
+};
+// Story: using the migration file below
+// 1. we need to fix the header text for login and register page. They have been mistakenly swapped
+// 2. we need to use "register" consistently, even on keys.
+export const migrationContent4: MigrationFileContent = {
+ "parent": "000003-1000000000000",
+ "actions": [
+ {
+ "action": "update",
+ "namespace": "login",
+ "key": "header",
+ "newNamespace": "register"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "header",
+ "newNamespace": "login"
+ },
+ {
+ "action": "update",
+ "namespace": "register",
+ "key": "signUpButton",
+ "newKey": "registerButton"
+ }
+ ],
+};
+// Story: using the migration file below
+// 1. we need to delete unused strings for home page
+// 2. we need to delete unused strings for register page
+export const migrationContent5: MigrationFileContent = {
+ "parent": "000004-1000000000000",
+ "actions": [
+ {
+ "action": "remove",
+ "namespace": "home",
+ "key": "header"
+ },
+ {
+ "action": "remove",
+ "namespace": "home",
+ "key": "subHeader"
+ },
+ {
+ "action": "remove",
+ "namespace": "register",
+ "key": "header"
+ }
+ ]
+};
+
+// Story: we now have 2 translations files after applying the above migrations
+export const loginContent = {
+ namespace: 'login',
+ strings: {
+ emailLabel: 'Email/Username',
+ emailPlaceholder: 'Email/Username*',
+ passwordLabel: 'Password',
+ loginButton: 'Login',
+ header: 'If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with you email and password.',
+ },
+} satisfies TranslationFileContent;
+export const registerContent = {
+ namespace: 'register',
+ strings: {
+ firstNameLabel: 'First Name',
+ lastNameLabel: 'Last Name',
+ emailLabel: 'Email',
+ passwordLabel: 'Password',
+ confirmPasswordLabel: 'Confirm Password',
+ registerButton: 'Register',
+ },
+} satisfies TranslationFileContent;
+
+// Story: if the server has no translation data and we push the
+// above migrations, we get the following strings in server
+export const strings1: SourceFileContent = {
+ strings: [
+ {
+ hash: 'c1e1b9ea3f9cfc3eb3e7b8804139ed00',
+ key: 'emailLabel',
+ page_name: 'login',
+ language: 'en',
+ value: 'Email/Username',
+ },
+ {
+ hash: 'c1e1b9ea3f9cfc3eb3e7b8804139ed00',
+ key: 'emailLabel',
+ page_name: 'login',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: '7257816a20f2dda660c44809235ea44a',
+ key: 'emailPlaceholder',
+ language: 'en',
+ page_name: 'login',
+ value: 'Email/Username*',
+ },
+ {
+ hash: '7257816a20f2dda660c44809235ea44a',
+ key: 'emailPlaceholder',
+ language: 'np',
+ page_name: 'login',
+ value: '',
+ },
+ {
+ hash: 'd7d3cc6191dfa630aaa8bcf8d83d8a71',
+ key: 'header',
+ page_name: 'login',
+ language: 'en',
+ value: 'If you are staff, member or volunteer of the Red Cross Red Crescent Movement (National Societies, the IFRC and the ICRC) login with you email and password.',
+ },
+ {
+ hash: 'd7d3cc6191dfa630aaa8bcf8d83d8a71',
+ key: 'header',
+ page_name: 'login',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: '99dea78007133396a7b8ed70578ac6ae',
+ key: 'loginButton',
+ page_name: 'login',
+ language: 'en',
+ value: 'Login',
+ },
+ {
+ hash: '99dea78007133396a7b8ed70578ac6ae',
+ key: 'loginButton',
+ page_name: 'login',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: 'dc647eb65e6711e155375218212b3964',
+ key: 'passwordLabel',
+ page_name: 'login',
+ language: 'en',
+ value: 'Password',
+ },
+ {
+ hash: 'dc647eb65e6711e155375218212b3964',
+ key: 'passwordLabel',
+ page_name: 'login',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: '887f7db126221fe60d18c895d41dc8f6',
+ key: 'confirmPasswordLabel',
+ page_name: 'register',
+ language: 'en',
+ value: 'Confirm Password',
+ },
+ {
+ hash: '887f7db126221fe60d18c895d41dc8f6',
+ key: 'confirmPasswordLabel',
+ page_name: 'register',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: 'ce8ae9da5b7cd6c3df2929543a9af92d',
+ key: 'emailLabel',
+ page_name: 'register',
+ language: 'en',
+ value: 'Email',
+ },
+ {
+ hash: 'ce8ae9da5b7cd6c3df2929543a9af92d',
+ key: 'emailLabel',
+ page_name: 'register',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: 'bc910f8bdf70f29374f496f05be0330c',
+ key: 'firstNameLabel',
+ page_name: 'register',
+ language: 'en',
+ value: 'First Name',
+ },
+ {
+ hash: 'bc910f8bdf70f29374f496f05be0330c',
+ key: 'firstNameLabel',
+ page_name: 'register',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: '77587239bf4c54ea493c7033e1dbf636',
+ key: 'lastNameLabel',
+ page_name: 'register',
+ language: 'en',
+ value: 'Last Name',
+ },
+ {
+ hash: '77587239bf4c54ea493c7033e1dbf636',
+ key: 'lastNameLabel',
+ page_name: 'register',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: 'dc647eb65e6711e155375218212b3964',
+ key: 'passwordLabel',
+ page_name: 'register',
+ language: 'en',
+ value: 'Password',
+ },
+ {
+ hash: 'dc647eb65e6711e155375218212b3964',
+ key: 'passwordLabel',
+ page_name: 'register',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: '0ba7583639a274c434bbe6ef797115a4',
+ key: 'registerButton',
+ page_name: 'register',
+ language: 'en',
+ value: 'Register',
+ },
+ {
+ hash: '0ba7583639a274c434bbe6ef797115a4',
+ key: 'registerButton',
+ page_name: 'register',
+ language: 'np',
+ value: '',
+ },
+ ],
+ last_migration: '000005-1000000000000.json',
+};
+
+// Story: code has been updated as presented below by the developers
+export const updatedLoginContent: TranslationFileContent = {
+ ...loginContent,
+ strings: {
+ ...loginContent.strings,
+ }
+};
+export const updatedRegisterContent: TranslationFileContent = {
+ ...registerContent,
+ strings: {
+ ...registerContent.strings,
+ }
+};
+// Delete
+delete updatedLoginContent.strings.header;
+// Add
+updatedLoginContent.strings.footer = 'All rights reserved.';
+// Update key
+updatedLoginContent.strings.loginBtn = updatedLoginContent.strings.loginButton;
+delete updatedLoginContent.strings.loginButton;
+// Update content
+updatedLoginContent.strings.emailLabel = 'Email';
+// Delete
+delete updatedRegisterContent.strings.confirmPasswordLabel;
+// Add
+updatedRegisterContent.strings.backLink = 'Back to login';
+// Update key
+updatedRegisterContent.strings.registerBtn = updatedRegisterContent.strings.registerButton;
+delete updatedRegisterContent.strings.registerButton;
+// Update content
+updatedRegisterContent.strings.emailLabel = 'Email or Username';
+// Update namespace
+delete updatedRegisterContent.strings.firstNameLabel;
+updatedLoginContent.strings.firstNameLabel = 'First Name';
+delete updatedRegisterContent.strings.lastNameLabel;
+updatedLoginContent.strings.lastNameLabel = 'Last Name';
+
+// Story: we can get the following migration file after generating
+// migration comparing the code with previous migrations
+export const migrationContent6: MigrationFileContent = {
+ parent: '000005-1000000000000.json',
+ actions: [
+ {
+ action: 'add',
+ key: 'footer',
+ namespace: 'login',
+ value: 'All rights reserved.',
+ },
+ {
+ action: 'remove',
+ key: 'header',
+ namespace: 'login',
+ },
+ {
+ action: 'update',
+ key: 'emailLabel',
+ namespace: 'login',
+ newValue: 'Email',
+ },
+ {
+ action: 'update',
+ key: 'loginButton',
+ namespace: 'login',
+ newKey: 'loginBtn',
+ },
+ {
+ action: 'add',
+ key: 'backLink',
+ namespace: 'register',
+ value: 'Back to login',
+ },
+ {
+ action: 'remove',
+ key: 'confirmPasswordLabel',
+ namespace: 'register',
+ },
+ {
+ action: 'update',
+ key: 'emailLabel',
+ namespace: 'register',
+ newValue: 'Email or Username',
+ },
+ {
+ action: 'update',
+ key: 'firstNameLabel',
+ namespace: 'register',
+ newNamespace: 'login',
+ },
+ {
+ action: 'update',
+ key: 'lastNameLabel',
+ namespace: 'register',
+ newNamespace: 'login',
+ },
+ {
+ action: 'update',
+ key: 'registerButton',
+ namespace: 'register',
+ newKey: 'registerBtn',
+ },
+ ],
+}
+
+// Story: if we push new migration above, we get the following strings in server
+export const strings2: SourceFileContent = {
+ strings: [
+ {
+ hash: 'ce8ae9da5b7cd6c3df2929543a9af92d',
+ key: 'emailLabel',
+ page_name: 'login',
+ language: 'en',
+ value: 'Email',
+ },
+ {
+ hash: 'c1e1b9ea3f9cfc3eb3e7b8804139ed00',
+ key: 'emailLabel',
+ page_name: 'login',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: '7257816a20f2dda660c44809235ea44a',
+ key: 'emailPlaceholder',
+ language: 'en',
+ page_name: 'login',
+ value: 'Email/Username*',
+ },
+ {
+ hash: '7257816a20f2dda660c44809235ea44a',
+ key: 'emailPlaceholder',
+ language: 'np',
+ page_name: 'login',
+ value: '',
+ },
+ {
+ hash: 'bc910f8bdf70f29374f496f05be0330c',
+ key: 'firstNameLabel',
+ page_name: 'login',
+ language: 'en',
+ value: 'First Name',
+ },
+ {
+ hash: 'bc910f8bdf70f29374f496f05be0330c',
+ key: 'firstNameLabel',
+ page_name: 'login',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: '1efc109bdddbb6e51e9b69cc0a1b0701',
+ key: 'footer',
+ page_name: 'login',
+ language: 'en',
+ value: 'All rights reserved.',
+ },
+ {
+ hash: '1efc109bdddbb6e51e9b69cc0a1b0701',
+ key: 'footer',
+ page_name: 'login',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: '77587239bf4c54ea493c7033e1dbf636',
+ key: 'lastNameLabel',
+ page_name: 'login',
+ language: 'en',
+ value: 'Last Name',
+ },
+ {
+ hash: '77587239bf4c54ea493c7033e1dbf636',
+ key: 'lastNameLabel',
+ page_name: 'login',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: '99dea78007133396a7b8ed70578ac6ae',
+ key: 'loginBtn',
+ page_name: 'login',
+ language: 'en',
+ value: 'Login',
+ },
+ {
+ hash: '99dea78007133396a7b8ed70578ac6ae',
+ key: 'loginBtn',
+ page_name: 'login',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: 'dc647eb65e6711e155375218212b3964',
+ key: 'passwordLabel',
+ page_name: 'login',
+ language: 'en',
+ value: 'Password',
+ },
+ {
+ hash: 'dc647eb65e6711e155375218212b3964',
+ key: 'passwordLabel',
+ page_name: 'login',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: '463e58c1d35fb5a4a8d717c99a60d257',
+ key: 'backLink',
+ page_name: 'register',
+ language: 'en',
+ value: 'Back to login',
+ },
+ {
+ hash: '463e58c1d35fb5a4a8d717c99a60d257',
+ key: 'backLink',
+ page_name: 'register',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: 'b7a9ca78ae5ec6f8c6af39f000b07da9',
+ key: 'emailLabel',
+ page_name: 'register',
+ language: 'en',
+ value: 'Email or Username',
+ },
+ {
+ hash: 'ce8ae9da5b7cd6c3df2929543a9af92d',
+ key: 'emailLabel',
+ page_name: 'register',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: 'dc647eb65e6711e155375218212b3964',
+ key: 'passwordLabel',
+ page_name: 'register',
+ language: 'en',
+ value: 'Password',
+ },
+ {
+ hash: 'dc647eb65e6711e155375218212b3964',
+ key: 'passwordLabel',
+ page_name: 'register',
+ language: 'np',
+ value: '',
+ },
+ {
+ hash: '0ba7583639a274c434bbe6ef797115a4',
+ key: 'registerBtn',
+ page_name: 'register',
+ language: 'en',
+ value: 'Register',
+ },
+ {
+ hash: '0ba7583639a274c434bbe6ef797115a4',
+ key: 'registerBtn',
+ page_name: 'register',
+ language: 'np',
+ value: '',
+ },
+ ],
+ last_migration: '000006-1000000000000.json',
+};
diff --git a/go-web-app-develop/app/scripts/translatte/testHelpers.ts b/go-web-app-develop/app/scripts/translatte/testHelpers.ts
new file mode 100644
index 0000000000000000000000000000000000000000..0426bf07186047311aaea18ce50c033984d2bb40
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/testHelpers.ts
@@ -0,0 +1,24 @@
+import { test } from "vitest";
+import os from "node:os";
+import fs from "node:fs/promises";
+import path from "node:path";
+
+interface TmpDirFixture {
+ tmpdir: string;
+}
+
+async function createTempDir() {
+ const ostmpdir = os.tmpdir();
+ const tmpdir = path.join(ostmpdir, "unit-test-");
+ return await fs.mkdtemp(tmpdir);
+}
+
+export const testWithTmpDir = test.extend({
+ tmpdir: async ({}, use) => {
+ const directory = await createTempDir();
+
+ await use(directory);
+
+ await fs.rm(directory, { recursive: true });
+ },
+});
diff --git a/go-web-app-develop/app/scripts/translatte/types.ts b/go-web-app-develop/app/scripts/translatte/types.ts
new file mode 100644
index 0000000000000000000000000000000000000000..37e0b90639a554b8e95d0fdeced6a085032b3b8f
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/types.ts
@@ -0,0 +1,50 @@
+interface MigrationAddActionItem {
+ action: 'add',
+ key: string,
+ value: string,
+ namespace: string,
+}
+
+interface MigrationRemoveActionItem {
+ action: 'remove',
+ key: string,
+ namespace: string,
+}
+
+interface MigrationUpdateActionItem {
+ action: 'update',
+ key: string,
+ namespace: string,
+ value?: string,
+ newValue?: string,
+ newKey?: string,
+ newNamespace?: string,
+}
+
+export type MigrationActionItem = MigrationAddActionItem | MigrationRemoveActionItem | MigrationUpdateActionItem;
+
+export interface TranslationFileContent {
+ namespace: string,
+ strings: {
+ [key: string]: string,
+ },
+}
+
+export interface MigrationFileContent {
+ parent?: string;
+ actions: MigrationActionItem[],
+}
+
+export interface SourceStringItem {
+ hash: string;
+ // id: string;
+ key: string;
+ language: string;
+ page_name: string;
+ value: string;
+}
+
+export interface SourceFileContent {
+ last_migration?: string;
+ strings: SourceStringItem[];
+}
diff --git a/go-web-app-develop/app/scripts/translatte/utils.ts b/go-web-app-develop/app/scripts/translatte/utils.ts
new file mode 100644
index 0000000000000000000000000000000000000000..84543278f6c90832ded9802def7a051282dafea4
--- /dev/null
+++ b/go-web-app-develop/app/scripts/translatte/utils.ts
@@ -0,0 +1,325 @@
+import { join, isAbsolute, basename } from 'path';
+import { promisify } from 'util';
+import { readFile, writeFile, unlink } from 'fs';
+import glob from 'fast-glob';
+import {
+ isDefined,
+ intersection,
+ listToMap,
+ mapToList,
+ unique,
+ difference,
+} from '@togglecorp/fujs';
+
+import {
+ TranslationFileContent,
+ MigrationFileContent,
+ SourceFileContent,
+} from './types';
+
+const readFilePromisify = promisify(readFile);
+export const writeFilePromisify = promisify(writeFile);
+const unlinkPromisify = promisify(unlink);
+
+// Utilities
+
+export function oneOneMapping(
+ prevState: T[],
+ currentState: T[],
+ keySelector: (item: T) => K,
+ validate: (prevStateItem: T, currentStateItem: T) => boolean,
+) {
+ const prevStateMapping = listToMap(
+ prevState,
+ keySelector,
+ (item) => item,
+ );
+ const currentStateMapping = listToMap(
+ currentState,
+ keySelector,
+ (item) => item,
+ );
+
+ const prevStateKeySet = new Set(
+ Object.keys(prevStateMapping) as Array
+ );
+ const currentStateKeySet = new Set(
+ Object.keys(currentStateMapping) as Array,
+ );
+
+ const commonKeySet = intersection(prevStateKeySet, currentStateKeySet);
+ const prevStateExclusiveKeySet = difference(prevStateKeySet, commonKeySet);
+ const currentStateExclusiveKeySet = difference(currentStateKeySet, commonKeySet);
+
+ const commonItems = [...commonKeySet].map(
+ (key) => ({
+ key,
+ prevStateItem: prevStateMapping[key],
+ currentStateItem: currentStateMapping[key],
+ })
+ )
+
+ const commonItemsMap = listToMap(
+ commonItems,
+ ({ key }) => key,
+ );
+
+ const validCommonItems = commonItems.filter(
+ ({ prevStateItem, currentStateItem }) => validate(prevStateItem, currentStateItem)
+ );
+
+ const validCommonItemsKeySet = new Set(
+ validCommonItems.map(({ key }) => key)
+ );
+
+ const invalidCommonItemsKeySet = difference(commonKeySet, validCommonItemsKeySet);
+ const invalidCommonItems = Array.from(invalidCommonItemsKeySet).map(
+ (key) => commonItemsMap[key],
+ );
+
+ return {
+ validCommonItems,
+ invalidCommonItems,
+ prevStateRemainder: Array.from(prevStateExclusiveKeySet).map((key) => prevStateMapping[key]),
+ currentStateRemainder: Array.from(currentStateExclusiveKeySet).map((key) => currentStateMapping[key]),
+ };
+}
+
+export function oneOneMappingNonUnique(
+ prevState: T[],
+ currentState: T[],
+ keySelector: (item: T) => K,
+) {
+ const prevStateWithKey = prevState.map(
+ (item) => {
+ const key = keySelector(item);
+
+ return {
+ key,
+ item,
+ };
+ }
+ );
+
+ const currentStateWithKey = currentState.map(
+ (item) => {
+ const key = keySelector(item);
+
+ return {
+ key,
+ item,
+ };
+ }
+ );
+
+ const {
+ commonItems,
+ prevStateRemainder,
+ currentStateRemainder,
+ } = prevStateWithKey.reduce(
+ (acc, prevStateItem) => {
+ const matchIndex = acc.currentStateRemainder.findIndex(
+ ({ key }) => prevStateItem.key === key,
+ )
+
+ if (matchIndex === -1) {
+ return acc;
+ }
+
+ const prevStateMatchIndex = acc.prevStateRemainder.findIndex(
+ ({ key }) => prevStateItem.key === key,
+ );
+
+ if (prevStateMatchIndex === -1) {
+ return acc;
+ }
+
+ return {
+ commonItems: [
+ ...acc.commonItems,
+ {
+ prevStateItem: prevStateItem,
+ currentStateItem: acc.currentStateRemainder[matchIndex],
+ },
+ ],
+ prevStateRemainder: [
+ ...acc.prevStateRemainder.slice(0, prevStateMatchIndex),
+ ...acc.prevStateRemainder.slice(prevStateMatchIndex + 1),
+ ],
+ currentStateRemainder: [
+ ...acc.currentStateRemainder.slice(0, matchIndex),
+ ...acc.currentStateRemainder.slice(matchIndex + 1),
+ ],
+ }
+ },
+ {
+ prevStateRemainder: [...prevStateWithKey],
+ commonItems: [] as {
+ prevStateItem: { key: K, item: T },
+ currentStateItem: { key: K, item: T },
+ }[],
+ currentStateRemainder: [...currentStateWithKey],
+ },
+ );
+
+ return {
+ commonItems: commonItems.map(
+ ({ prevStateItem, currentStateItem }) => ({
+ prevStateItem: prevStateItem.item,
+ currentStateItem: currentStateItem.item,
+ })
+ ),
+ prevStateRemainder: prevStateRemainder.map(
+ ({ item }) => item,
+ ),
+ currentStateRemainder: currentStateRemainder.map(
+ ({ item }) => item,
+ ),
+ };
+}
+
+export function getDuplicateItems(
+ list: T[],
+ keySelector: (value: T) => string,
+) {
+ if (!list) {
+ return [];
+ }
+ const counts = listToMap(
+ list,
+ keySelector,
+ (_, key, __, acc) => {
+ const value = acc[key];
+ return isDefined(value) ? value + 1 : 1;
+ },
+ );
+
+ return list
+ .filter((item) => counts[keySelector(item)] > 1)
+ .sort((foo, bar) => keySelector(foo).localeCompare(keySelector(bar)));
+}
+
+export function concat(...args: string[]) {
+ return args.join(":");
+}
+
+export function removeUndefinedKeys(itemFromArgs: T) {
+ const item = {...itemFromArgs};
+ Object.keys(item).forEach(key => {
+ if (item[key as keyof T] === undefined) {
+ delete item[key as keyof T];
+ }
+ });
+ return item;
+}
+
+export async function getMigrationFilesAttrs(basePath: string, pathName: string) {
+ const fullPath = isAbsolute(pathName)
+ ? join(pathName, '[0-9]+-[0-9]+.json')
+ : join(basePath, pathName, '[0-9]+-[0-9]+.json')
+
+ const files = await glob(fullPath, { ignore: ['node_modules'], absolute: true });
+
+ interface MigrationFileAttrs {
+ migrationName: string;
+ fileName: string;
+ num: string;
+ timestamp: string;
+ }
+
+ const migrationFiles = files
+ .map((file): MigrationFileAttrs | undefined => {
+ const migrationName = basename(file);
+ const attrs = migrationName.match(/(?[0-9]+)-(?[0-9]+)/)?.groups as (Omit | undefined)
+ if (attrs) {
+ return {
+ ...attrs,
+ migrationName,
+ fileName: file,
+ }
+ }
+ return undefined;
+ })
+ .filter(isDefined)
+ .sort((a, b) => a.migrationName.localeCompare(b.migrationName));
+ return migrationFiles;
+}
+
+export async function getTranslationFileNames(basePath: string, pathNames: string[]) {
+ const fullPathNames = pathNames.map((pathName) => (
+ isAbsolute(pathName)
+ ? pathName
+ : join(basePath, pathName)
+ ));
+
+ const fileNamesPromise = fullPathNames.map(async (fullPathName) => {
+ return glob(fullPathName, { ignore: ['node_modules'], absolute: true });
+ });
+ const fileNames = (await Promise.all(fileNamesPromise)).flat();
+ return unique(fileNames);
+}
+
+export async function readJsonFilesContents(fileNames: string[]) {
+ const contentsPromise = fileNames.map(async (fileName) => {
+ const fileDescriptor = await readFilePromisify(fileName);
+ try {
+ const content = JSON.parse(fileDescriptor.toString());
+ return {
+ file: fileName,
+ content,
+ };
+ } catch {
+ throw `Error while parsing JSON for ${fileName}}`;
+ }
+ });
+ const contents = await Promise.all(contentsPromise);
+ return contents;
+}
+
+export async function readTranslations(fileNames: string[]) {
+ const filesContents = await readJsonFilesContents(fileNames);
+
+ const translations = filesContents.flatMap((fileContent) => {
+ // TODO: validate the schema for content
+ const {
+ file,
+ content,
+ } = fileContent as {
+ file: string,
+ content: TranslationFileContent,
+ };
+
+ return mapToList(
+ content.strings,
+ (item, key) => ({
+ file,
+ namespace: content.namespace,
+ key,
+ value: item,
+ }),
+ );
+ });
+
+ return { translations, filesContents };
+}
+
+export async function readMigrations(fileNames: string[]) {
+ const fileContents = await readJsonFilesContents(fileNames);
+ // TODO: validate the schema for content
+ return fileContents as { file: string, content: MigrationFileContent }[];
+}
+
+export async function readSource(fileName: string) {
+ const fileContents = await readJsonFilesContents([fileName]);
+ // TODO: validate the schema for content
+ return fileContents[0] as {
+ file: string, content: SourceFileContent
+ };
+}
+
+export async function removeFiles(files: string[]) {
+ const removePromises = files.map(async (file) => (
+ unlinkPromisify(file)
+ ));
+ await Promise.all(removePromises);
+}
diff --git a/go-web-app-develop/app/src/App/Auth.tsx b/go-web-app-develop/app/src/App/Auth.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..999e9dd14a28f16c805c3f4203d70f9554503274
--- /dev/null
+++ b/go-web-app-develop/app/src/App/Auth.tsx
@@ -0,0 +1,63 @@
+import {
+ Fragment,
+ type ReactElement,
+} from 'react';
+import {
+ Navigate,
+ useParams,
+} from 'react-router-dom';
+
+import FourHundredThree from '#components/FourHundredThree';
+import useAuth from '#hooks/domain/useAuth';
+import usePermissions from '#hooks/domain/usePermissions';
+
+import { type ExtendedProps } from './routes/common';
+
+interface Props {
+ children: ReactElement,
+ context: ExtendedProps,
+ absolutePath: string,
+}
+function Auth(props: Props) {
+ const {
+ context,
+ children,
+ absolutePath,
+ } = props;
+
+ const urlParams = useParams();
+ const perms = usePermissions();
+
+ const { isAuthenticated } = useAuth();
+
+ if (context.visibility === 'is-authenticated' && !isAuthenticated) {
+ return (
+
+ );
+ }
+ if (context.visibility === 'is-not-authenticated' && isAuthenticated) {
+ return (
+
+ );
+ }
+
+ if (context.permissions) {
+ const hasPermission = context.permissions(perms, urlParams);
+
+ if (!hasPermission) {
+ return (
+
+ );
+ }
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default Auth;
diff --git a/go-web-app-develop/app/src/App/PageError/i18n.json b/go-web-app-develop/app/src/App/PageError/i18n.json
new file mode 100644
index 0000000000000000000000000000000000000000..cae3ca02dc7d9642313627cd7e65faf3b1f26f83
--- /dev/null
+++ b/go-web-app-develop/app/src/App/PageError/i18n.json
@@ -0,0 +1,13 @@
+{
+ "namespace": "common",
+ "strings": {
+ "errorPageIssueMessage": "Oops! Looks like we ran into some issue!",
+ "errorPageUnexpectedMessage": "Something unexpected happened!",
+ "errorPageHide": "Hide Error",
+ "errorPageShowError": "Show Full Error",
+ "errorPageStackTrace": "Stack trace not available",
+ "errorSeeDeveloperConsole": "See the developer console for more details",
+ "errorPageGoBack": "Go back to homepage",
+ "errorPageReload": "Reload"
+ }
+}
\ No newline at end of file
diff --git a/go-web-app-develop/app/src/App/PageError/index.tsx b/go-web-app-develop/app/src/App/PageError/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..6a10d7acb62cb64f99bfe9abc984312a53a272b7
--- /dev/null
+++ b/go-web-app-develop/app/src/App/PageError/index.tsx
@@ -0,0 +1,98 @@
+import {
+ useCallback,
+ useEffect,
+} from 'react';
+import { useRouteError } from 'react-router-dom';
+import { Button } from '@ifrc-go/ui';
+import {
+ useBooleanState,
+ useTranslation,
+} from '@ifrc-go/ui/hooks';
+
+import Link from '#components/Link';
+
+import i18n from './i18n.json';
+import styles from './styles.module.css';
+
+function PageError() {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const errorResponse = useRouteError() as unknown as any;
+ const strings = useTranslation(i18n);
+
+ useEffect(
+ () => {
+ // eslint-disable-next-line no-console
+ console.error(errorResponse);
+ },
+ [errorResponse],
+ );
+
+ const [
+ fullErrorVisible,
+ {
+ toggle: toggleFullErrorVisibility,
+ },
+ ] = useBooleanState(false);
+
+ const handleReloadButtonClick = useCallback(
+ () => {
+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment
+ // @ts-ignore
+ window.location.reload(true);
+ },
+ [],
+ );
+
+ return (
+
+
+
+
+ {strings.errorPageIssueMessage}
+
+
+ {errorResponse?.error?.message
+ ?? errorResponse?.message
+ ?? strings.errorPageUnexpectedMessage}
+
+
+ {fullErrorVisible ? strings.errorPageHide : strings.errorPageShowError}
+
+ {fullErrorVisible && (
+ <>
+
+ {errorResponse?.error?.stack
+ ?? errorResponse?.stack ?? strings.errorPageStackTrace}
+
+
+ {strings.errorSeeDeveloperConsole}
+
+ >
+ )}
+
+
+ {/* NOTE: using the anchor element as it will refresh the page */}
+
+ {strings.errorPageGoBack}
+
+
+ {strings.errorPageReload}
+
+
+
+
+ );
+}
+
+export default PageError;
diff --git a/go-web-app-develop/app/src/App/PageError/styles.module.css b/go-web-app-develop/app/src/App/PageError/styles.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..cea5a9d39b84cf31c4d6819c8f3bc84f0d7480c0
--- /dev/null
+++ b/go-web-app-develop/app/src/App/PageError/styles.module.css
@@ -0,0 +1,49 @@
+.page-error {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100vw;
+ height: 100vh;
+
+ .container {
+ display: flex;
+ flex-direction: column;
+ border-top: var(--go-ui-width-separator-large) solid var(--go-ui-color-primary-red);
+ border-radius: var(--go-ui-border-radius-xl);
+ box-shadow: var(--go-ui-box-shadow-2xl);
+ background-color: var(--go-ui-color-white);
+ padding: var(--go-ui-spacing-2xl);
+ width: calc(100% - var(--go-ui-spacing-2xl));
+ max-width: 60rem;
+ max-height: 60rem;
+ gap: var(--go-ui-spacing-2xl);
+
+ .content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--go-ui-spacing-md);
+
+ .heading {
+ margin: 0;
+ font-weight: var(--go-ui-font-weight-medium);
+ }
+
+ .stack {
+ flex-grow: 1;
+ background-color: var(--go-ui-color-background);
+ padding: var(--go-ui-spacing-md);
+ width: 100%;
+ overflow: auto;
+ white-space: pre;
+ font-family: var(--go-ui-font-family-mono);
+ }
+ }
+
+ .footer {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: var(--go-ui-spacing-md);
+ }
+ }
+}
diff --git a/go-web-app-develop/app/src/App/index.tsx b/go-web-app-develop/app/src/App/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ba1e866f064da56f8be640bddc8c385f57764d5f
--- /dev/null
+++ b/go-web-app-develop/app/src/App/index.tsx
@@ -0,0 +1,269 @@
+import {
+ useCallback,
+ useEffect,
+ useMemo,
+ useState,
+} from 'react';
+import {
+ createBrowserRouter,
+ RouterProvider,
+} from 'react-router-dom';
+import {
+ AlertContext,
+ type AlertContextProps,
+ type AlertParams,
+ type Language,
+ LanguageContext,
+ type LanguageContextProps,
+ type LanguageNamespaceStatus,
+} from '@ifrc-go/ui/contexts';
+import * as Sentry from '@sentry/react';
+import {
+ isDefined,
+ unique,
+} from '@togglecorp/fujs';
+import mapboxgl from 'mapbox-gl';
+
+import goLogo from '#assets/icons/go-logo-2020.svg';
+import {
+ appTitle,
+ mbtoken,
+} from '#config';
+import RouteContext from '#contexts/route';
+import UserContext, {
+ type UserAuth,
+ type UserContextProps,
+} from '#contexts/user';
+import {
+ KEY_LANGUAGE_STORAGE,
+ KEY_USER_STORAGE,
+} from '#utils/constants';
+import {
+ getFromStorage,
+ removeFromStorage,
+ setToStorage,
+} from '#utils/localStorage';
+import { RequestContext } from '#utils/restRequest';
+import {
+ processGoError,
+ processGoOptions,
+ processGoResponse,
+ processGoUrls,
+} from '#utils/restRequest/go';
+
+import wrappedRoutes, { unwrappedRoutes } from './routes';
+
+import styles from './styles.module.css';
+
+const requestContextValue = {
+ transformUrl: processGoUrls,
+ transformOptions: processGoOptions,
+ transformResponse: processGoResponse,
+ transformError: processGoError,
+};
+const sentryCreateBrowserRouter = Sentry.wrapCreateBrowserRouter(createBrowserRouter);
+const router = sentryCreateBrowserRouter(unwrappedRoutes);
+mapboxgl.accessToken = mbtoken;
+mapboxgl.setRTLTextPlugin(
+ 'https://api.mapbox.com/mapbox-gl-js/plugins/mapbox-gl-rtl-text/v0.2.3/mapbox-gl-rtl-text.js',
+ // eslint-disable-next-line no-console
+ (err) => { console.error(err); },
+ true,
+);
+
+function Application() {
+ // ALERTS
+
+ const [alerts, setAlerts] = useState([]);
+
+ const addAlert = useCallback((alert: AlertParams) => {
+ setAlerts((prevAlerts) => unique(
+ [...prevAlerts, alert],
+ (a) => a.name,
+ ) ?? prevAlerts);
+ }, [setAlerts]);
+
+ const removeAlert = useCallback((name: AlertParams['name']) => {
+ setAlerts((prevAlerts) => {
+ const i = prevAlerts.findIndex((a) => a.name === name);
+ if (i === -1) {
+ return prevAlerts;
+ }
+
+ const newAlerts = [...prevAlerts];
+ newAlerts.splice(i, 1);
+
+ return newAlerts;
+ });
+ }, [setAlerts]);
+
+ const updateAlert = useCallback((name: AlertParams['name'], paramsWithoutName: Omit) => {
+ setAlerts((prevAlerts) => {
+ const i = prevAlerts.findIndex((a) => a.name === name);
+ if (i === -1) {
+ return prevAlerts;
+ }
+
+ const updatedAlert = {
+ ...prevAlerts[i],
+ paramsWithoutName,
+ };
+
+ const newAlerts = [...prevAlerts];
+ newAlerts.splice(i, 1, updatedAlert);
+
+ return newAlerts;
+ });
+ }, [setAlerts]);
+
+ const alertContextValue: AlertContextProps = useMemo(() => ({
+ alerts,
+ addAlert,
+ updateAlert,
+ removeAlert,
+ }), [alerts, addAlert, updateAlert, removeAlert]);
+
+ // AUTH
+
+ const [userAuth, setUserAuth] = useState();
+
+ const hydrateUserAuth = useCallback(() => {
+ const userDetailsFromStorage = getFromStorage(KEY_USER_STORAGE);
+ if (userDetailsFromStorage) {
+ setUserAuth(userDetailsFromStorage);
+ }
+ }, []);
+
+ const removeUserAuth = useCallback(() => {
+ removeFromStorage(KEY_USER_STORAGE);
+ setUserAuth(undefined);
+ }, []);
+
+ const setAndStoreUserAuth = useCallback((newUserDetails: UserAuth) => {
+ setUserAuth(newUserDetails);
+ setToStorage(
+ KEY_USER_STORAGE,
+ newUserDetails,
+ );
+ }, []);
+
+ // Translation
+
+ const [strings, setStrings] = useState({});
+ const [currentLanguage, setCurrentLanguage] = useState('en');
+ const [
+ languageNamespaceStatus,
+ setLanguageNamespaceStatus,
+ ] = useState>({});
+
+ const setAndStoreCurrentLanguage = useCallback(
+ (newLanguage: Language) => {
+ setCurrentLanguage(newLanguage);
+ setToStorage(KEY_LANGUAGE_STORAGE, newLanguage);
+ },
+ [],
+ );
+
+ const registerLanguageNamespace = useCallback(
+ (namespace: string, fallbackStrings: Record) => {
+ setStrings(
+ (prevValue) => {
+ if (isDefined(prevValue[namespace])) {
+ return {
+ ...prevValue,
+ [namespace]: {
+ ...fallbackStrings,
+ ...prevValue[namespace],
+ },
+ };
+ }
+
+ return {
+ ...prevValue,
+ [namespace]: fallbackStrings,
+ };
+ },
+ );
+
+ setLanguageNamespaceStatus((prevValue) => {
+ if (isDefined(prevValue[namespace])) {
+ return prevValue;
+ }
+
+ return {
+ ...prevValue,
+ // NOTE: This will fetch if the data is not already fetched
+ [namespace]: prevValue[namespace] === 'fetched' ? 'fetched' : 'queued',
+ };
+ });
+ },
+ [setStrings],
+ );
+
+ // Hydration
+ useEffect(() => {
+ hydrateUserAuth();
+
+ const language = getFromStorage(KEY_LANGUAGE_STORAGE);
+ setCurrentLanguage(language ?? 'en');
+ }, [hydrateUserAuth]);
+
+ const userContextValue = useMemo(
+ () => ({
+ userAuth,
+ hydrateUserAuth,
+ setUserAuth: setAndStoreUserAuth,
+ removeUserAuth,
+ }),
+ [userAuth, hydrateUserAuth, setAndStoreUserAuth, removeUserAuth],
+ );
+
+ const languageContextValue = useMemo(
+ () => ({
+ languageNamespaceStatus,
+ setLanguageNamespaceStatus,
+ currentLanguage,
+ setCurrentLanguage: setAndStoreCurrentLanguage,
+ strings,
+ setStrings,
+ registerNamespace: registerLanguageNamespace,
+ }),
+ [
+ languageNamespaceStatus,
+ setLanguageNamespaceStatus,
+ currentLanguage,
+ setAndStoreCurrentLanguage,
+ strings,
+ registerLanguageNamespace,
+ ],
+ );
+
+ return (
+
+
+
+
+
+
+
+ {`${appTitle} loading...`}
+
+ )}
+ />
+
+
+
+
+
+ );
+}
+
+const App = Sentry.withProfiler(Application);
+export default App;
diff --git a/go-web-app-develop/app/src/App/routes/CountryRoutes.tsx b/go-web-app-develop/app/src/App/routes/CountryRoutes.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ec04b860e8218088c2b37d9cace16020001d955d
--- /dev/null
+++ b/go-web-app-develop/app/src/App/routes/CountryRoutes.tsx
@@ -0,0 +1,515 @@
+import {
+ generatePath,
+ Navigate,
+ useParams,
+} from 'react-router-dom';
+import {
+ isDefined,
+ isTruthyString,
+} from '@togglecorp/fujs';
+
+import { countryIdToRegionIdMap } from '#utils/domain/country';
+
+import Auth from '../Auth';
+import {
+ customWrapRoute,
+ rootLayout,
+} from './common';
+import regionRoutes from './RegionRoutes';
+import SmartNavigate from './SmartNavigate';
+
+type DefaultCountriesChild = 'ongoing-activities';
+const countriesLayout = customWrapRoute({
+ parent: rootLayout,
+ path: 'countries/:countryId',
+ forwardPath: 'ongoing-activities' satisfies DefaultCountriesChild,
+ component: {
+ render: () => import('#views/Country'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Country',
+ visibility: 'anything',
+ },
+});
+
+interface Props {
+ to?: string;
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+function CountryNavigate(props: Props) {
+ // FIXME: this function might not be necessary anymore
+ const { to } = props;
+
+ const params = useParams<{ countryId: string }>();
+
+ const countryId = isTruthyString(params.countryId) ? parseInt(params.countryId, 10) : undefined;
+ const regionId = isDefined(countryId) ? countryIdToRegionIdMap[countryId] : undefined;
+
+ if (isDefined(regionId)) {
+ const regionPath = generatePath(
+ regionRoutes.regionIndex.absoluteForwardPath,
+ { regionId },
+ );
+ return (
+
+ );
+ }
+
+ if (to) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+const countryIndex = customWrapRoute({
+ parent: countriesLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: CountryNavigate,
+ props: {},
+ },
+ context: {
+ title: 'Country',
+ visibility: 'anything',
+ },
+});
+
+type DefaultOngoingActivitiesChild = 'emergencies';
+const countryOngoingActivitiesLayout = customWrapRoute({
+ parent: countriesLayout,
+ path: 'ongoing-activities',
+ forwardPath: 'emergencies' satisfies DefaultOngoingActivitiesChild,
+ component: {
+ render: () => import('#views/CountryOngoingActivities'),
+ props: {},
+ },
+ context: {
+ title: 'Country Ongoing Activities',
+ visibility: 'anything',
+ },
+});
+
+const countryOngoingActivitiesIndex = customWrapRoute({
+ parent: countryOngoingActivitiesLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: 'emergencies' satisfies DefaultOngoingActivitiesChild,
+ replace: true,
+ },
+ },
+ context: {
+ title: 'Country Ongoing Activities Index',
+ visibility: 'anything',
+ },
+});
+
+const countryOngoingActivitiesEmergencies = customWrapRoute({
+ parent: countryOngoingActivitiesLayout,
+ path: 'emergencies' satisfies DefaultOngoingActivitiesChild,
+ component: {
+ render: () => import('#views/CountryOngoingActivitiesEmergencies'),
+ props: {},
+ },
+ context: {
+ title: 'Country Ongoing Emergencies',
+ visibility: 'anything',
+ },
+});
+
+const countryOngoingActivitiesThreeWActivities = customWrapRoute({
+ parent: countryOngoingActivitiesLayout,
+ path: 'three-w/activities',
+ component: {
+ render: () => import('#views/CountryOngoingActivitiesThreeWActivities'),
+ props: {},
+ },
+ context: {
+ title: 'Country 3W Activities',
+ visibility: 'anything',
+ },
+});
+
+const countryOngoingActivitiesThreeWProjects = customWrapRoute({
+ parent: countryOngoingActivitiesLayout,
+ path: 'three-w/projects',
+ component: {
+ render: () => import('#views/ThreeWDecommission'),
+ props: {},
+ },
+ context: {
+ title: 'Country 3W Projects',
+ visibility: 'anything',
+ },
+});
+
+type DefaultNsOverviewChild = 'activities';
+const countryNsOverviewLayout = customWrapRoute({
+ parent: countriesLayout,
+ path: 'ns-overview',
+ forwardPath: 'activities' satisfies DefaultNsOverviewChild,
+ component: {
+ render: () => import('#views/CountryNsOverview'),
+ props: {},
+ },
+ context: {
+ title: 'Country NS Overview',
+ visibility: 'anything',
+ },
+});
+
+const countryNsOverviewIndex = customWrapRoute({
+ parent: countryNsOverviewLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: 'activities' satisfies DefaultNsOverviewChild,
+ replace: true,
+ },
+ },
+ context: {
+ title: 'Country National Society Overview Index',
+ visibility: 'anything',
+ },
+});
+
+const countryNsOverviewActivities = customWrapRoute({
+ parent: countryNsOverviewLayout,
+ path: 'activities',
+ component: {
+ render: () => import('#views/CountryNsOverviewActivities'),
+ props: {},
+ },
+ context: {
+ title: 'Country NS Activities',
+ visibility: 'anything',
+ },
+});
+
+const countryNsOverviewContextAndStructure = customWrapRoute({
+ parent: countryNsOverviewLayout,
+ path: 'context-and-structure',
+ component: {
+ render: () => import('#views/CountryNsOverviewContextAndStructure'),
+ props: {},
+ },
+ context: {
+ title: 'Country NS Context and Structure',
+ visibility: 'anything',
+ },
+});
+
+const countryNsOverviewStrategicPriorities = customWrapRoute({
+ parent: countryNsOverviewLayout,
+ path: 'strategic-priorities',
+ component: {
+ render: () => import('#views/CountryNsOverviewStrategicPriorities'),
+ props: {},
+ },
+ context: {
+ title: 'Country NS Strategic Priorities',
+ visibility: 'anything',
+ },
+});
+
+const countryNsOverviewCapacity = customWrapRoute({
+ parent: countryNsOverviewLayout,
+ path: 'capacity',
+ component: {
+ render: () => import('#views/CountryNsOverviewCapacity'),
+ props: {},
+ },
+ context: {
+ title: 'Country NS Capacity',
+ visibility: 'anything',
+ },
+});
+
+const countryNsOverviewSupportingPartners = customWrapRoute({
+ parent: countryNsOverviewLayout,
+ path: 'partners',
+ component: {
+ render: () => import('#views/CountryNsOverviewSupportingPartners'),
+ props: {},
+ },
+ context: {
+ title: 'Country NS Partners',
+ visibility: 'anything',
+ },
+});
+
+const countryPreparedness = customWrapRoute({
+ parent: countriesLayout,
+ path: 'ns-overview/per/:perId',
+ component: {
+ render: () => import('#views/CountryPreparedness'),
+ props: {},
+ },
+ context: {
+ title: 'Country Preparedness',
+ visibility: 'anything',
+ },
+});
+
+const perExport = customWrapRoute({
+ parent: rootLayout,
+ path: 'countries/:countryId/per/:perId/export',
+ component: {
+ render: () => import('#views/PerExport'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'PER Export',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+type DefaultCountryProfileChild = 'overview';
+const countryProfileLayout = customWrapRoute({
+ parent: countriesLayout,
+ path: 'profile',
+ forwardPath: 'overview' satisfies DefaultCountryProfileChild,
+ component: {
+ render: () => import('#views/CountryProfile'),
+ props: {},
+ },
+ context: {
+ title: 'Country Profile',
+ visibility: 'anything',
+ },
+});
+
+const countryProfileIndex = customWrapRoute({
+ parent: countryProfileLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: 'overview' satisfies DefaultCountryProfileChild,
+ replace: true,
+ },
+ },
+ context: {
+ title: 'Country Profile Index',
+ visibility: 'anything',
+ },
+});
+
+const countryProfileOverview = customWrapRoute({
+ parent: countryProfileLayout,
+ path: 'overview',
+ component: {
+ render: () => import('#views/CountryProfileOverview'),
+ props: {},
+ },
+ context: {
+ title: 'Country Profile Overview',
+ visibility: 'anything',
+ },
+});
+
+const countryProfilePreviousEvents = customWrapRoute({
+ parent: countryProfileLayout,
+ path: 'previous-events',
+ component: {
+ render: () => import('#views/CountryProfilePreviousEvents'),
+ props: {},
+ },
+ context: {
+ title: 'Country Profile Previous Events',
+ visibility: 'anything',
+ },
+});
+
+const countryProfileSeasonalRisks = customWrapRoute({
+ parent: countryProfileLayout,
+ path: 'risk-watch',
+ component: {
+ render: () => import('#views/CountryProfileRiskWatch'),
+ props: {},
+ },
+ context: {
+ title: 'Country Profile Seasonal Risks',
+ visibility: 'anything',
+ },
+});
+
+const countryAdditionalInfo = customWrapRoute({
+ parent: countriesLayout,
+ path: 'additional-info',
+ component: {
+ render: () => import('#views/CountryAdditionalInfo'),
+ props: {},
+ },
+ context: {
+ title: 'Country Additional Info',
+ visibility: 'anything',
+ },
+});
+
+// Redirect routes
+const countryOperations = customWrapRoute({
+ parent: countriesLayout,
+ path: 'operations',
+ component: {
+ eagerLoad: true,
+ render: CountryNavigate,
+ props: {
+ to: countryOngoingActivitiesEmergencies.absolutePath,
+ },
+ },
+ context: {
+ title: 'Country Ongoing Activities Emergencies',
+ visibility: 'anything',
+ },
+});
+
+const countriesThreeW = customWrapRoute({
+ parent: countriesLayout,
+ path: 'three-w/ns-projects',
+ component: {
+ eagerLoad: true,
+ render: CountryNavigate,
+ props: {
+ to: countryNsOverviewActivities.absolutePath,
+ },
+ },
+ context: {
+ title: 'Country 3W Activities',
+ visibility: 'anything',
+ },
+});
+
+const countriesThreeWProjects = customWrapRoute({
+ parent: countriesLayout,
+ path: 'three-w/projects',
+ component: {
+ eagerLoad: true,
+ render: CountryNavigate,
+ props: {
+ to: countryOngoingActivitiesThreeWProjects.absolutePath,
+ },
+ },
+ context: {
+ title: 'Country 3W Projects',
+ visibility: 'anything',
+ },
+});
+
+const countryRiskWatch = customWrapRoute({
+ parent: countriesLayout,
+ path: 'risk-watch',
+ component: {
+ eagerLoad: true,
+ render: CountryNavigate,
+ props: {
+ to: countryProfileSeasonalRisks.absolutePath,
+ },
+ },
+ context: {
+ title: 'Country Profile Seasonal Risks',
+ visibility: 'anything',
+ },
+});
+
+const countryPreparednessRedirect = customWrapRoute({
+ parent: countriesLayout,
+ path: 'preparedness',
+ component: {
+ eagerLoad: true,
+ render: CountryNavigate,
+ props: {
+ to: countryNsOverviewCapacity.absolutePath,
+ },
+ },
+ context: {
+ title: 'Country NS Capacity',
+ visibility: 'anything',
+ },
+});
+
+const countryPlan = customWrapRoute({
+ parent: countriesLayout,
+ path: 'plan',
+ component: {
+ eagerLoad: true,
+ render: CountryNavigate,
+ props: {
+ to: countryNsOverviewStrategicPriorities.absolutePath,
+ },
+ },
+ context: {
+ title: 'Country NS Strategic Priorities',
+ visibility: 'anything',
+ },
+});
+
+export default {
+ countriesLayout,
+ countryIndex,
+
+ countryOngoingActivitiesLayout,
+ countryOngoingActivitiesIndex,
+ countryOngoingActivitiesEmergencies,
+ countryOngoingActivitiesThreeWActivities,
+ countryOngoingActivitiesThreeWProjects,
+
+ countryNsOverviewLayout,
+ countryNsOverviewIndex,
+ countryNsOverviewActivities,
+ countryNsOverviewContextAndStructure,
+ countryNsOverviewStrategicPriorities,
+ countryNsOverviewCapacity,
+ countryPreparedness,
+ perExport,
+
+ countryProfileLayout,
+ countryProfileIndex,
+ countryProfileOverview,
+ countryNsOverviewSupportingPartners,
+ countryProfilePreviousEvents,
+ countryProfileSeasonalRisks,
+
+ countryAdditionalInfo,
+
+ // Redirects
+ countryOperations,
+ countriesThreeW,
+ countriesThreeWProjects,
+ countryRiskWatch,
+ countryPreparednessRedirect,
+ countryPlan,
+};
diff --git a/go-web-app-develop/app/src/App/routes/RegionRoutes.tsx b/go-web-app-develop/app/src/App/routes/RegionRoutes.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..c75ead1e7d0e2e6b700bc0cf38eb3beaf23465c4
--- /dev/null
+++ b/go-web-app-develop/app/src/App/routes/RegionRoutes.tsx
@@ -0,0 +1,186 @@
+import { Navigate } from 'react-router-dom';
+
+import Auth from '../Auth';
+import {
+ customWrapRoute,
+ rootLayout,
+} from './common';
+import SmartNavigate from './SmartNavigate';
+
+type DefaultRegionsChild = 'operations';
+const regionsLayout = customWrapRoute({
+ parent: rootLayout,
+ path: 'regions/:regionId',
+ forwardPath: 'operations' satisfies DefaultRegionsChild,
+ component: {
+ render: () => import('#views/Region'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Region',
+ visibility: 'anything',
+ },
+});
+
+const regionIndex = customWrapRoute({
+ parent: regionsLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: SmartNavigate,
+ props: {
+ to: 'operations' satisfies DefaultRegionsChild,
+ replace: true,
+ hashToRouteMap: {
+ '#operations': 'operations',
+ '#3w': 'three-w',
+ '#risk-watch': 'risk-watch',
+ '#regional-profile': 'profile',
+ '#preparedness': 'preparedness',
+ '#additional-info': 'additional-info',
+ },
+ },
+ },
+ context: {
+ title: 'Region',
+ visibility: 'anything',
+ },
+});
+
+const regionOperations = customWrapRoute({
+ parent: regionsLayout,
+ path: 'operations' satisfies DefaultRegionsChild,
+ component: {
+ render: () => import('#views/RegionOperations'),
+ props: {},
+ },
+ context: {
+ title: 'Region Operations',
+ visibility: 'anything',
+ },
+});
+
+const regionThreeW = customWrapRoute({
+ parent: regionsLayout,
+ path: 'three-w',
+ component: {
+ render: () => import('#views/ThreeWDecommission'),
+ props: {},
+ },
+ context: {
+ title: 'Region 3W',
+ visibility: 'anything',
+ },
+});
+
+type DefaultRegionRiskWatchChild = 'seasonal';
+const regionRiskWatchLayout = customWrapRoute({
+ parent: regionsLayout,
+ path: 'risk-watch',
+ forwardPath: 'seasonal' satisfies DefaultRegionRiskWatchChild,
+ component: {
+ render: () => import('#views/RegionRiskWatch'),
+ props: {},
+ },
+ context: {
+ title: 'Region Risk Watch',
+ visibility: 'anything',
+ },
+});
+
+const regionRiskIndex = customWrapRoute({
+ parent: regionRiskWatchLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: 'seasonal' satisfies DefaultRegionRiskWatchChild,
+ replace: true,
+ },
+ },
+ context: {
+ title: 'Region Risk Watch',
+ visibility: 'anything',
+ },
+});
+
+const regionImminentRiskWatch = customWrapRoute({
+ parent: regionRiskWatchLayout,
+ path: 'imminent',
+ component: {
+ render: () => import('#views/RegionRiskWatchImminent'),
+ props: {},
+ },
+ context: {
+ title: 'Region Imminent Risk Watch',
+ visibility: 'anything',
+ },
+});
+
+const regionSeasonalRiskWatch = customWrapRoute({
+ parent: regionRiskWatchLayout,
+ path: 'seasonal' satisfies DefaultRegionRiskWatchChild,
+ component: {
+ render: () => import('#views/RegionRiskWatchSeasonal'),
+ props: {},
+ },
+ context: {
+ title: 'Region Seasonal Risk Watch',
+ visibility: 'anything',
+ },
+});
+
+const regionPreparedness = customWrapRoute({
+ parent: regionsLayout,
+ path: 'preparedness',
+ component: {
+ render: () => import('#views/RegionPreparedness'),
+ props: {},
+ },
+ context: {
+ title: 'Region Preparedness',
+ visibility: 'anything',
+ },
+});
+
+const regionProfile = customWrapRoute({
+ parent: regionsLayout,
+ path: 'profile',
+ component: {
+ render: () => import('#views/RegionProfile'),
+ props: {},
+ },
+ context: {
+ title: 'Region Profile',
+ visibility: 'anything',
+ },
+});
+
+const regionAdditionalInfo = customWrapRoute({
+ parent: regionsLayout,
+ path: 'additional-info',
+ component: {
+ render: () => import('#views/RegionAdditionalInfo'),
+ props: {},
+ },
+ context: {
+ title: 'Region Additional Info',
+ visibility: 'anything',
+ },
+});
+
+export default {
+ regionsLayout,
+ regionIndex,
+ regionOperations,
+ regionThreeW,
+ regionRiskWatchLayout,
+ regionRiskIndex,
+ regionImminentRiskWatch,
+ regionSeasonalRiskWatch,
+ regionPreparedness,
+ regionProfile,
+ regionAdditionalInfo,
+};
diff --git a/go-web-app-develop/app/src/App/routes/SmartNavigate.tsx b/go-web-app-develop/app/src/App/routes/SmartNavigate.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..e278fc052ca7b085ce28ac0c03aaf4b19ac9d4fc
--- /dev/null
+++ b/go-web-app-develop/app/src/App/routes/SmartNavigate.tsx
@@ -0,0 +1,49 @@
+import {
+ Navigate,
+ type NavigateProps,
+ useLocation,
+} from 'react-router-dom';
+import {
+ isDefined,
+ isTruthyString,
+} from '@togglecorp/fujs';
+
+type RouteKey = string;
+
+interface Props extends NavigateProps {
+ hashToRouteMap: Record
;
+ forwardUnmatchedHashTo?: string;
+}
+
+function SmartNavigate(props: Props) {
+ const {
+ hashToRouteMap,
+ forwardUnmatchedHashTo,
+ ...navigateProps
+ } = props;
+
+ const location = useLocation();
+ const newRoute = isTruthyString(location.hash)
+ ? (hashToRouteMap[location.hash] ?? forwardUnmatchedHashTo)
+ : undefined;
+
+ if (isDefined(newRoute)) {
+ return (
+
+ );
+ }
+
+ return (
+
+ );
+}
+
+export default SmartNavigate;
diff --git a/go-web-app-develop/app/src/App/routes/SurgeRoutes.tsx b/go-web-app-develop/app/src/App/routes/SurgeRoutes.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1d9b63a271b1c6cfac9d376236e404f7900ba63f
--- /dev/null
+++ b/go-web-app-develop/app/src/App/routes/SurgeRoutes.tsx
@@ -0,0 +1,1702 @@
+import {
+ Navigate,
+ Outlet,
+ useParams,
+} from 'react-router-dom';
+import {
+ isDefined,
+ isTruthyString,
+} from '@togglecorp/fujs';
+
+import type { MyOutputNonIndexRouteObject } from '#utils/routes';
+
+import Auth from '../Auth';
+import {
+ customWrapRoute,
+ type ExtendedProps,
+ rootLayout,
+} from './common';
+
+type DefaultSurgeChild = 'active-surge-deployments';
+const surgeLayout = customWrapRoute({
+ parent: rootLayout,
+ path: 'surge',
+ forwardPath: 'active-surge-deployments' satisfies DefaultSurgeChild,
+ component: {
+ render: () => import('#views/Surge'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Surge',
+ visibility: 'anything',
+ },
+});
+
+const surgeIndex = customWrapRoute({
+ parent: surgeLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: 'active-surge-deployments' satisfies DefaultSurgeChild,
+ replace: true,
+ },
+ },
+ context: {
+ title: 'Surge',
+ visibility: 'anything',
+ },
+});
+
+const activeSurgeDeployments = customWrapRoute({
+ parent: surgeLayout,
+ path: 'active-surge-deployments',
+ component: {
+ render: () => import('#views/ActiveSurgeDeployments'),
+ props: {},
+ },
+ context: {
+ title: 'Active Surge Deployments',
+ visibility: 'anything',
+ },
+});
+
+type DefaultSurgeOverviewChild = 'rapid-response-personnel';
+
+const surgeOverviewLayout = customWrapRoute({
+ parent: surgeLayout,
+ path: 'overview',
+ forwardPath: 'rapid-response-personnel' satisfies DefaultSurgeOverviewChild,
+ component: {
+ render: () => import('#views/SurgeOverview'),
+ props: {},
+ },
+ context: {
+ title: 'Surge Overview',
+ visibility: 'anything',
+ },
+});
+
+const surgeOverviewIndex = customWrapRoute({
+ parent: surgeOverviewLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: 'rapid-response-personnel' satisfies DefaultSurgeOverviewChild,
+ replace: true,
+ },
+ },
+ context: {
+ title: 'Surge Overview',
+ visibility: 'anything',
+ },
+});
+
+const rapidResponsePersonnel = customWrapRoute({
+ parent: surgeOverviewLayout,
+ path: 'rapid-response-personnel',
+ component: {
+ render: () => import('#views/SurgeOverview/RapidResponsePersonnel'),
+ props: {},
+ },
+ context: {
+ title: 'Rapid Response Personnel',
+ visibility: 'anything',
+ },
+});
+
+const emergencyResponseUnit = customWrapRoute({
+ parent: surgeOverviewLayout,
+ path: 'emergency-response-unit',
+ component: {
+ render: () => import('#views/SurgeOverview/EmergencyResponseUnit'),
+ props: {},
+ },
+ context: {
+ title: 'Emergency Response Unit',
+ visibility: 'anything',
+ },
+});
+
+const eruReadinessForm = customWrapRoute({
+ parent: rootLayout,
+ path: 'eru-readiness',
+ component: {
+ render: () => import('#views/EruReadinessForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'ERU Readiness Update Form',
+ visibility: 'is-authenticated',
+ permissions: ({
+ isRegionalOrCountryAdmin,
+ isSuperUser,
+ }) => isSuperUser || isRegionalOrCountryAdmin,
+ },
+});
+
+const surgeOperationalToolbox = customWrapRoute({
+ parent: surgeLayout,
+ path: 'operational-toolbox',
+ component: {
+ render: () => import('#views/SurgeOperationalToolbox'),
+ props: {},
+ },
+ context: {
+ title: 'Surge Operational Toolbox',
+ visibility: 'anything',
+ },
+});
+
+type DefaultSurgeCatalogueChild = 'overview';
+const surgeCatalogueLayout = customWrapRoute({
+ parent: surgeLayout,
+ path: 'catalogue',
+ forwardPath: 'overview' satisfies DefaultSurgeCatalogueChild,
+ component: {
+ render: () => import('#views/SurgeCatalogue'),
+ props: {},
+ },
+ context: {
+ title: 'Surge Catalogue',
+ visibility: 'anything',
+ },
+});
+
+const catalogueIndex = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: 'overview' satisfies DefaultSurgeCatalogueChild,
+ replace: true,
+ },
+ },
+ context: {
+ title: 'Surge Catalogue',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOverview = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'overview' satisfies DefaultSurgeCatalogueChild,
+ component: {
+ render: () => import('#views/SurgeCatalogueOverview'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Surge Catalogue Overview',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueEmergencyNeedsAssessment = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'emergency-needs-assessment',
+ component: {
+ render: () => import('#views/SurgeCatalogueEmergencyNeedsAssessment'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Emergency Needs Assessment',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueEmergencyNeedsAssessmentCell = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'emergency-needs-assessment/cell',
+ component: {
+ render: () => import('#views/SurgeCatalogueEmergencyNeedsAssessmentCell'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Assessment Cell',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueAdministration = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'administration',
+ component: {
+ render: () => import('#views/SurgeCatalogueAdministration'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Administration',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueBasecamp = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'basecamp',
+ component: {
+ render: () => import('#views/SurgeCatalogueBasecamp'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Basecamp Catalogue',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueBasecampEruSmall = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'basecamp/eru-small',
+ component: {
+ render: () => import('#views/SurgeCatalogueBasecampEruSmall'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Basecamp ERU Small',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueBasecampEruMedium = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'basecamp/eru-medium',
+ component: {
+ render: () => import('#views/SurgeCatalogueBasecampEruMedium'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Basecamp ERU Medium',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueBasecampEruLarge = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'basecamp/eru-large',
+ component: {
+ render: () => import('#views/SurgeCatalogueBasecampEruLarge'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Basecamp ERU Large',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueBasecampFacilityManagement = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'basecamp/facility-management',
+ component: {
+ render: () => import('#views/SurgeCatalogueBasecampFacilityManagement'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Basecamp Facility Management',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueBasecampOffice = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'basecamp/office',
+ component: {
+ render: () => import('#views/SurgeCatalogueBasecampOffice'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Basecamp Office',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueBasecampWelcome = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'basecamp/welcome',
+ component: {
+ render: () => import('#views/SurgeCatalogueBasecampWelcome'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Basecamp Admin and Welcome Service',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueCash = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'cash',
+ component: {
+ render: () => import('#views/SurgeCatalogueCash'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Cash and Vouchers Assistance',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueCashRapidResponse = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'cash/rapid-response',
+ component: {
+ render: () => import('#views/SurgeCatalogueCashRapidResponse'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Cash and Vouchers Assistance - Rapid Response',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueCommunityEngagement = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'community-engagement',
+ component: {
+ render: () => import('#views/SurgeCatalogueCommunityEngagement'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Community Engagement and Accountability (CEA)',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueCommunityEngagementRapidResponse = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'community/rapid-response',
+ component: {
+ render: () => import('#views/SurgeCatalogueCommunityEngagementRapidResponse'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Community Engagement and Accountability (CEA) - Rapid Response',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueCommunication = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'communication',
+ component: {
+ render: () => import('#views/SurgeCatalogueCommunication'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Communication',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueCommunicationErtOne = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'communication/cert-1',
+ component: {
+ render: () => import('#views/SurgeCatalogueCommunicationErtOne'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Communication Emergency Response Tool 1',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueCommunicationErtTwo = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'communication/cert-2',
+ component: {
+ render: () => import('#views/SurgeCatalogueCommunicationErtTwo'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Communication Emergency Response Tool 2',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueCommunicationErtThree = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'communication/cert-3',
+ component: {
+ render: () => import('#views/SurgeCatalogueCommunicationErtThree'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Communication Emergency Response Tool 3',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealth = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealth'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Health',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthEruClinic = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/eru-clinic',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthEruClinic'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'ERU Red Cross Red Crescent Emergency Clinic',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthEruHospital = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/eru-hospital',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthEruHospital'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'ERU Red Cross Red Crescent Emergency Hospital',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthEruSurgical = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/eru-surgical',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthEruSurgical'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Health Surgical',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthMaternalNewbornClinic = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/maternal-newborn-clinic',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthMaternalNewbornClinic'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Maternal NewBorn Health Clinic',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthEmergencyClinic = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/emergency-clinic',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthEmergencyClinic'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Emergency Mobile Clinic',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthEruCholeraTreatment = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/eru-cholera-treatment',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthEruCholeraTreatment'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Emergency Response Unit Cholera Treatment Center',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthCommunityCaseManagementCholera = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/community-case-management-cholera',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthCommunityCaseManagementCholera'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Community Case Management of Cholera',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthCommunityBasedSurveillance = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/community-based-surveillance',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthCommunityBasedSurveillance'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Community Based Surveillance',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthSafeDignifiedBurials = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/safe-dignified-burials',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthSafeDignifiedBurials'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Safe and Dignified Burials',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthInfectionPreventionAndControl = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/infection-prevention-and-control',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthInfectionPreventionAndControl'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Infection Prevention and Control',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthCommunityManagementMalnutrition = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/community-management-malnutrition',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthCommunityManagementMalnutrition'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Community Case Management of Malnutrition',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueHealthEruPsychosocialSupport = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'health/eru-psychosocial-support',
+ component: {
+ render: () => import('#views/SurgeCatalogueHealthEruPsychosocialSupport'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Emergency Response Unit Psychosocial Support',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueInformationManagement = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'information-management',
+ component: {
+ render: () => import('#views/SurgeCatalogueInformationManagement'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Information Management',
+ visibility: 'anything',
+ },
+
+});
+
+const surgeCatalogueInformationManagementSatelliteImagery = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'information-management/satellite-imagery',
+ component: {
+ render: () => import('#views/SurgeCatalogueInformationManagementSatelliteImagery'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Satellite Imagery',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueInformationManagementRolesResponsibility = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'information-management/roles-responsibility',
+ component: {
+ render: () => import('#views/SurgeCatalogueInformationManagementRolesResponsibility'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Roles and Responsibilities',
+ visibility: 'anything',
+ },
+});
+
+// TODO: update view name
+const surgeCatalogueInformationManagementRegionalOfficeSupport = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'information-management/regional-office-support',
+ component: {
+ render: () => import('#views/SurgeCatalogueInformationManagementSupport'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Information Management Support - Regional Office',
+ visibility: 'anything',
+ },
+});
+
+// TODO: update view name
+const surgeCatalogueInformationManagementGenevaSupport = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'information-management/geneva-support',
+ component: {
+ render: () => import('#views/SurgeCatalogueInformationManagementOperationSupport'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Information Management Support - Geneva',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueInformationManagementComposition = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'information-management/composition',
+ component: {
+ render: () => import('#views/SurgeCatalogueInformationManagementComposition'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Composition of IM Resources',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueInformationTechnology = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'information-technology',
+ component: {
+ render: () => import('#views/SurgeCatalogueInformationTechnology'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Information Technology',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueInformationTechnologyEruItTelecom = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'information-technology/eru-it-telecom',
+ component: {
+ render: () => import('#views/SurgeCatalogueInformationTechnologyEruItTelecom'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Information Technology Service',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueLivelihood = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'livelihood',
+ component: {
+ render: () => import('#views/SurgeCatalogueLivelihood'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Livelihoods and Basic Needs',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueLivelihoodServices = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'livelihood/services',
+ component: {
+ render: () => import('#views/SurgeCatalogueLivelihoodServices'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Livelihood Service',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueLogistics = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'logistics',
+ component: {
+ render: () => import('#views/SurgeCatalogueLogistics'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Logistics',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueLogisticsEru = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'logistics/eru',
+ component: {
+ render: () => import('#views/SurgeCatalogueLogisticsEru'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Emergency Response Unit',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueLogisticsLpscmNs = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'logistics/lpscm-ns',
+ component: {
+ render: () => import('#views/SurgeCatalogueLogisticsLpscmNs'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'LPSCM for National Societies',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOperationsManagement = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'operations-management',
+ component: {
+ render: () => import('#views/SurgeCatalogueOperationsManagement'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Operations Management',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOperationManagementHeops = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'operations-management/heops',
+ component: {
+ render: () => import('#views/SurgeCatalogueOperationsManagementHeops'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Head of Emergency Operations (HEOPS)',
+ visibility: 'anything',
+ },
+});
+
+const surgeCataloguePgi = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'pgi',
+ component: {
+ render: () => import('#views/SurgeCataloguePgi'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Protection, Gender and inclusion (PGI)',
+ visibility: 'anything',
+ },
+});
+
+const surgeCataloguePgiServices = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'pgi/services',
+ component: {
+ render: () => import('#views/SurgeCataloguePgiServices'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Protection Gender and Inclusion - Services',
+ visibility: 'anything',
+ },
+});
+
+const surgeCataloguePmer = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'pmer',
+ component: {
+ render: () => import('#views/SurgeCataloguePmer'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Planning, Monitoring, Evaluation And Reporting (PMER)',
+ visibility: 'anything',
+ },
+});
+
+const surgeCataloguePmerEmergencyPlanAction = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'pmer/emergency-plan-action',
+ component: {
+ render: () => import('#views/SurgeCataloguePmerEmergencyPlanAction'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Emergency plan of action EPOA monitoring evaluation plan',
+ visibility: 'anything',
+ },
+});
+
+const surgeCataloguePmerRealTimeEvaluation = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'pmer/real-time-evaluation',
+ component: {
+ render: () => import('#views/SurgeCataloguePmerRealTimeEvaluation'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Real time evaluation RTE and guidance',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueShelter = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'shelter',
+ component: {
+ render: () => import('#views/SurgeCatalogueShelter'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Shelter',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueShelterCoordinatorTeam = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'shelter/coordinator-team',
+ component: {
+ render: () => import('#views/SurgeCatalogueShelterCoordinatorTeam'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Shelter Surge Coordinator',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueShelterTechnicalTeam = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'shelter/technical-team',
+ component: {
+ render: () => import('#views/SurgeCatalogueShelterTechnicalTeam'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Shelter Technical Team',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueWash = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'wash',
+ component: {
+ render: () => import('#views/SurgeCatalogueWash'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Water, Sanitation and Hygiene (WASH)',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueWashKit2 = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'wash/kit-2',
+ component: {
+ render: () => import('#views/SurgeCatalogueWashKit2'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'WASH Kit-2',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueWashKit5 = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'wash/kit-5',
+ component: {
+ render: () => import('#views/SurgeCatalogueWashKit5'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'WASH Kit-5',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueWashKitM15Eru = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'wash/m15-eru',
+ component: {
+ render: () => import('#views/SurgeCatalogueWashKitM15Eru'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'WASH Kit-M15 ERU',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueWashKitMsm20Eru = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'wash/msm20-eru',
+ component: {
+ render: () => import('#views/SurgeCatalogueWashKitMsm20Eru'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Wash Kit-MSM20 ERU',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueWashKitM40Eru = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'wash/m40-eru',
+ component: {
+ render: () => import('#views/SurgeCatalogueWashKitM40Eru'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Wash Kit-M40 ERU',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueWashWaterSupplyRehabilitation = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'wash/water-supply-rehabilitation',
+ component: {
+ render: () => import('#views/SurgeCatalogueWashWaterSupplyRehabilitation'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Water Supply Rehabilitation',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueWashHwts = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'wash/hwts',
+ component: {
+ render: () => import('#views/SurgeCatalogueWashHwts'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Household Water Treatment and Safe Storage (HWTS)',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueWashSludge = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'wash/sludge',
+ component: {
+ render: () => import('#views/SurgeCatalogueWashSludge'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Faecal Sludge Management',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueRelief = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'relief',
+ component: {
+ render: () => import('#views/SurgeCatalogueRelief'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Relief',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueReliefEru = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'relief/eru',
+ component: {
+ render: () => import('#views/SurgeCatalogueReliefEru'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Relief ERU',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueSecurity = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'security',
+ component: {
+ render: () => import('#views/SurgeCatalogueSecurity'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Security',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueSecurityManagement = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'security/management',
+ component: {
+ render: () => import('#views/SurgeCatalogueSecurityManagement'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Security Management',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherCivilMilitaryRelations = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/civil-military-relations',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherCivilMilitaryRelations'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Civil Military Relations',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherDisasterRiskReduction = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/disaster-risk-reduction',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherDisasterRiskReduction'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Disaster Risk Reduction',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherHumanitarianDiplomacy = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/humanitarian-diplomacy',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherHumanitarianDiplomacy'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Humanitarian Diplomacy',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherHumanResources = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/human-resources',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherHumanResources'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Human Resources',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherInternationalDisasterResponseLaw = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/international-disaster-response-law',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherInternationalDisasterResponseLaw'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'International Disaster Response Law',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherMigration = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/migration',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherMigration'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Migration',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherNationalSocietyDevelopment = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/national-society-development',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherNationalSocietyDevelopment'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'National Society Development',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherStrategicPartnershipsResourceMobilisation = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/strategic-partnership-resource-mobilisation',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherStrategicPartnershipsResourceMobilisation'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Partnership Resource Development',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherPreparednessEffectiveResponse = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/preparedness-effective-response',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherPreparednessEffectiveResponse'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Preparedness Effective Response',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherRecovery = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/recovery',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherRecovery'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Recovery',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherGreenResponse = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/green-response',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherGreenResponse'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Green Response',
+ visibility: 'anything',
+ },
+});
+
+const surgeCatalogueOtherUAV = customWrapRoute({
+ parent: surgeCatalogueLayout,
+ path: 'other/uav',
+ component: {
+ render: () => import('#views/SurgeCatalogueOtherUAV'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Uncrewed Aerial Vehicles (Drones)',
+ visibility: 'anything',
+ },
+});
+
+const allDeployedPersonnel = customWrapRoute({
+ parent: rootLayout,
+ path: 'deployed-personnels/all',
+ component: {
+ render: () => import('#views/AllDeployedPersonnel'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'All Deployed Personnel',
+ visibility: 'anything',
+ },
+});
+
+const allDeployedEmergencyResponseUnits = customWrapRoute({
+ parent: rootLayout,
+ path: 'deployed-erus/all',
+ component: {
+ render: () => import('#views/AllDeployedEmergencyResponseUnits'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'All Deployed Emergency Response Units',
+ visibility: 'anything',
+ },
+});
+
+// eslint-disable-next-line react-refresh/only-export-components
+function DeploymentNavigate() {
+ const params = useParams<{ surgeId: string }>();
+
+ const deploymentRouteMap: Record> = {
+ overview: surgeOverviewLayout,
+ 'operational-toolbox': surgeOperationalToolbox,
+ personnel: allDeployedPersonnel,
+ erus: allDeployedEmergencyResponseUnits,
+ };
+
+ const newRoute = isDefined(params.surgeId)
+ ? deploymentRouteMap[params.surgeId]
+ : undefined;
+
+ const path = isDefined(newRoute)
+ ? newRoute.absoluteForwardPath
+ : surgeOverviewLayout.absoluteForwardPath;
+
+ return (
+
+ );
+}
+
+const deploymentOthers = customWrapRoute({
+ parent: rootLayout,
+ path: 'deployments/:surgeId/*',
+ component: {
+ eagerLoad: true,
+ render: DeploymentNavigate,
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Catalogue of surge services',
+ visibility: 'anything',
+ },
+});
+
+const deploymentCatalogueLayout = customWrapRoute({
+ parent: rootLayout,
+ path: 'deployments/catalogue',
+ component: {
+ eagerLoad: true,
+ render: () => ,
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Catalogue of surge services',
+ visibility: 'anything',
+ },
+});
+
+// eslint-disable-next-line react-refresh/only-export-components
+function DeploymentCatalogueNavigate() {
+ const params = useParams<{
+ catalogueId: string,
+ subCatalogueId: string,
+ }>();
+
+ type WrappedRoute = MyOutputNonIndexRouteObject;
+
+ const catalogueRouteMap: Record = {
+ overview: surgeCatalogueOverview,
+ emergency: surgeCatalogueEmergencyNeedsAssessment,
+ administration: surgeCatalogueAdministration,
+ basecamp: surgeCatalogueBasecamp,
+ cash: surgeCatalogueCash,
+ community: surgeCatalogueCommunityEngagement,
+ communications: surgeCatalogueCommunication,
+ health: surgeCatalogueHealth,
+ infoMgt: surgeCatalogueInformationManagement,
+ informationTech: surgeCatalogueInformationTechnology,
+ livelihoods: surgeCatalogueLivelihood,
+ logistics: surgeCatalogueLogistics,
+ operations: surgeCatalogueOperationsManagement,
+ protection: surgeCataloguePgi,
+ planning: surgeCataloguePmer,
+ relief: surgeCatalogueRelief,
+ security: surgeCatalogueSecurity,
+ shelter: surgeCatalogueShelter,
+ water: surgeCatalogueWash,
+ };
+
+ const subCatalogueRouteMap: Record> = {
+ emergency: {
+ 'assessment-cell': surgeCatalogueEmergencyNeedsAssessmentCell,
+ },
+ basecamp: {
+ 'eru-base-camp-small': surgeCatalogueBasecampEruSmall,
+ 'eru-base-camp-medium': surgeCatalogueBasecampEruMedium,
+ 'eru-base-camp-large': surgeCatalogueBasecampEruLarge,
+ 'facility-management': surgeCatalogueBasecampFacilityManagement,
+ office: surgeCatalogueBasecampOffice,
+ welcome: surgeCatalogueBasecampWelcome,
+ },
+ cash: {
+ cva: surgeCatalogueCashRapidResponse,
+ },
+ community: {
+ 'community-engagement-and-accountability': surgeCatalogueCommunityEngagementRapidResponse,
+ },
+ communications: {
+ 'communications-emergency-response-tool-cert-3': surgeCatalogueCommunicationErtThree,
+ 'communications-emergency-response-tool-cert-2': surgeCatalogueCommunicationErtTwo,
+ 'communications-emergency-response-tool-cert-1': surgeCatalogueCommunicationErtOne,
+ },
+ health: {
+ 'eru-pss-module': surgeCatalogueHealthEruPsychosocialSupport,
+ 'community-case-management-of-malnutrition-ccmm': surgeCatalogueHealthCommunityManagementMalnutrition,
+ 'safe-and-dignified-burials': surgeCatalogueHealthSafeDignifiedBurials,
+ 'infection-prevention-and-control': surgeCatalogueHealthInfectionPreventionAndControl,
+ 'community-based-surveillance-cbs': surgeCatalogueHealthCommunityBasedSurveillance,
+ 'community-case-management-of-cholera-ccmc': surgeCatalogueHealthCommunityCaseManagementCholera,
+ 'eru-cholera-treatment-center': surgeCatalogueHealthEruCholeraTreatment,
+ 'emergency-mobile-clinic': surgeCatalogueHealthEmergencyClinic,
+ 'maternal-newborn-health-clinic': surgeCatalogueHealthMaternalNewbornClinic,
+ 'surgical-surge': surgeCatalogueHealthEruSurgical,
+ 'eru-red-cross-red-crescent-emergency-hospital': surgeCatalogueHealthEruHospital,
+ 'eru-red-cross-red-crescent-emergency-clinic': surgeCatalogueHealthEruClinic,
+ },
+ infoMgt: {
+ // NOTE: sims was probably replace with link to its site
+ // 'surge-information-management-support-sims': ,
+ 'roles-and-resps': surgeCatalogueInformationManagementRolesResponsibility,
+ 'im-support-for-op': surgeCatalogueInformationManagementRegionalOfficeSupport,
+ 'ifrc-geneva-im': surgeCatalogueInformationManagementGenevaSupport,
+ 'composition-of-im-res': surgeCatalogueInformationManagementComposition,
+ 'Satellite-imagery': surgeCatalogueInformationManagementSatelliteImagery,
+ },
+ informationTech: {
+ 'eru-it-telecom': surgeCatalogueInformationTechnologyEruItTelecom,
+ },
+ livelihoods: {
+ 'livelihoods-and-basic-needs': surgeCatalogueLivelihoodServices,
+ },
+ logistics: {
+ 'lpscm-for-national-societies': surgeCatalogueLogisticsLpscmNs,
+ 'logistics-eru': surgeCatalogueLogisticsEru,
+ },
+ operations: {
+ 'head-of-emergency-operations-heops': surgeCatalogueOperationManagementHeops,
+ },
+ protection: {
+ 'protection-gender-and-inclusion': surgeCataloguePgiServices,
+ },
+ planning: {
+ 'real-time-evaluation-rte-and-guidance': surgeCataloguePmerRealTimeEvaluation,
+ 'emergency-plan-of-action-epoa-monitoring-evaluation-plan': surgeCataloguePmerEmergencyPlanAction,
+ },
+ relief: {
+ 'eru-relief': surgeCatalogueReliefEru,
+ },
+ security: {
+ 'security-management': surgeCatalogueSecurityManagement,
+ },
+ shelter: {
+ 'stt-shelter-technical-team': surgeCatalogueShelterTechnicalTeam,
+ 'sct-shelter-coordination-team': surgeCatalogueShelterCoordinatorTeam,
+ },
+ water: {
+ 'faecal-sludge-management': surgeCatalogueWashSludge,
+ 'household-water-treatment-and-safe-storage-hwts': surgeCatalogueWashHwts,
+ 'water-supply-rehabilitation-wsr': surgeCatalogueWashWaterSupplyRehabilitation,
+ 'm40-eru': surgeCatalogueWashKitM40Eru,
+ 'msm20-eru': surgeCatalogueWashKitMsm20Eru,
+ 'm15-eru': surgeCatalogueWashKitM15Eru,
+ 'kit-5': surgeCatalogueWashKit5,
+ 'kit-2': surgeCatalogueWashKit2,
+ },
+ other: {
+ 'civil-military-relations': surgeCatalogueOtherCivilMilitaryRelations,
+ 'disaster-risk-reduction-drr': surgeCatalogueOtherDisasterRiskReduction,
+ 'humanitarian-diplomacy': surgeCatalogueOtherHumanitarianDiplomacy,
+ 'human-resources': surgeCatalogueOtherHumanResources,
+ 'international-disaster-response-law': surgeCatalogueOtherInternationalDisasterResponseLaw,
+ migration: surgeCatalogueOtherMigration,
+ 'national-society-development': surgeCatalogueOtherNationalSocietyDevelopment,
+ 'strategic-partnership-resource-mobilisation': surgeCatalogueOtherStrategicPartnershipsResourceMobilisation,
+ 'preparedness-for-effective-response-per': surgeCatalogueOtherPreparednessEffectiveResponse,
+ recovery: surgeCatalogueOtherRecovery,
+ greenresponse: surgeCatalogueOtherGreenResponse,
+ uav: surgeCatalogueOtherUAV,
+ },
+ };
+
+ const newCatalogueRoute = isTruthyString(params.catalogueId)
+ ? catalogueRouteMap[params.catalogueId]
+ : undefined;
+
+ const newSubCatalogueRoute = isTruthyString(params.catalogueId)
+ && isTruthyString(params.subCatalogueId)
+ ? subCatalogueRouteMap[params.catalogueId]?.[params.subCatalogueId]
+ : undefined;
+
+ const path = newSubCatalogueRoute?.absoluteForwardPath
+ ?? newCatalogueRoute?.absoluteForwardPath
+ ?? surgeCatalogueOverview.absoluteForwardPath;
+
+ return (
+
+ );
+}
+
+const deploymentCatalogueIndex = customWrapRoute({
+ parent: deploymentCatalogueLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: '/surge/catalogue',
+ replace: true,
+ },
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Catalogue of Surge Services',
+ visibility: 'anything',
+ },
+});
+
+const deploymentCatalogueChildren = customWrapRoute({
+ parent: deploymentCatalogueLayout,
+ path: ':catalogueId/:subCatalogueId?',
+ component: {
+ eagerLoad: true,
+ render: DeploymentCatalogueNavigate,
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Catalogue of Surge Services',
+ visibility: 'anything',
+ },
+});
+
+const obsoleteUrlDeployments = customWrapRoute({
+ parent: rootLayout,
+ path: 'deployments',
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: surgeOverviewLayout.absolutePath,
+ },
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Surge Overview',
+ visibility: 'anything',
+ },
+});
+
+export default {
+ surgeLayout,
+ surgeOverviewLayout,
+ surgeOperationalToolbox,
+ surgeCatalogueLayout,
+ surgeOverviewIndex,
+ surgeIndex,
+ catalogueIndex,
+ surgeCatalogueOverview,
+ surgeCatalogueEmergencyNeedsAssessment,
+ surgeCatalogueEmergencyNeedsAssessmentCell,
+ surgeCatalogueAdministration,
+ surgeCatalogueBasecamp,
+ surgeCatalogueBasecampEruSmall,
+ surgeCatalogueBasecampEruMedium,
+ surgeCatalogueBasecampEruLarge,
+ surgeCatalogueBasecampFacilityManagement,
+ surgeCatalogueBasecampOffice,
+ surgeCatalogueBasecampWelcome,
+ surgeCatalogueCash,
+ surgeCatalogueCashRapidResponse,
+ surgeCatalogueCommunityEngagement,
+ surgeCatalogueCommunityEngagementRapidResponse,
+ surgeCatalogueCommunication,
+ surgeCatalogueCommunicationErtOne,
+ surgeCatalogueCommunicationErtTwo,
+ surgeCatalogueCommunicationErtThree,
+ surgeCatalogueHealth,
+ surgeCatalogueHealthEruClinic,
+ surgeCatalogueHealthEruHospital,
+ surgeCatalogueHealthEruSurgical,
+ surgeCatalogueHealthMaternalNewbornClinic,
+ surgeCatalogueHealthEmergencyClinic,
+ surgeCatalogueHealthEruCholeraTreatment,
+ surgeCatalogueHealthCommunityCaseManagementCholera,
+ surgeCatalogueHealthCommunityBasedSurveillance,
+ surgeCatalogueHealthSafeDignifiedBurials,
+ surgeCatalogueHealthInfectionPreventionAndControl,
+ surgeCatalogueHealthCommunityManagementMalnutrition,
+ surgeCatalogueHealthEruPsychosocialSupport,
+ surgeCatalogueInformationManagement,
+ surgeCatalogueInformationManagementSatelliteImagery,
+ surgeCatalogueInformationManagementRolesResponsibility,
+ surgeCatalogueInformationManagementRegionalOfficeSupport,
+ surgeCatalogueInformationManagementGenevaSupport,
+ surgeCatalogueInformationManagementComposition,
+ surgeCatalogueInformationTechnology,
+ surgeCataloguePmer,
+ surgeCataloguePmerEmergencyPlanAction,
+ surgeCataloguePmerRealTimeEvaluation,
+ surgeCatalogueInformationTechnologyEruItTelecom,
+ surgeCatalogueLivelihood,
+ surgeCatalogueLivelihoodServices,
+ surgeCatalogueSecurity,
+ surgeCatalogueSecurityManagement,
+ surgeCatalogueLogistics,
+ surgeCatalogueLogisticsEru,
+ surgeCatalogueLogisticsLpscmNs,
+ surgeCatalogueOperationsManagement,
+ surgeCatalogueOperationManagementHeops,
+ surgeCataloguePgi,
+ surgeCatalogueRelief,
+ surgeCatalogueReliefEru,
+ surgeCataloguePgiServices,
+ surgeCatalogueShelter,
+ surgeCatalogueShelterTechnicalTeam,
+ surgeCatalogueShelterCoordinatorTeam,
+ surgeCatalogueWash,
+ surgeCatalogueWashKit2,
+ surgeCatalogueWashKit5,
+ surgeCatalogueWashKitM15Eru,
+ surgeCatalogueWashKitMsm20Eru,
+ surgeCatalogueWashKitM40Eru,
+ surgeCatalogueWashWaterSupplyRehabilitation,
+ surgeCatalogueWashHwts,
+ surgeCatalogueWashSludge,
+ surgeCatalogueOtherCivilMilitaryRelations,
+ surgeCatalogueOtherDisasterRiskReduction,
+ surgeCatalogueOtherHumanitarianDiplomacy,
+ surgeCatalogueOtherHumanResources,
+ surgeCatalogueOtherInternationalDisasterResponseLaw,
+ surgeCatalogueOtherMigration,
+ surgeCatalogueOtherNationalSocietyDevelopment,
+ surgeCatalogueOtherStrategicPartnershipsResourceMobilisation,
+ surgeCatalogueOtherPreparednessEffectiveResponse,
+ surgeCatalogueOtherRecovery,
+ surgeCatalogueOtherGreenResponse,
+ surgeCatalogueOtherUAV,
+
+ allDeployedPersonnel,
+ allDeployedEmergencyResponseUnits,
+
+ // Redirect routes
+ deploymentCatalogueLayout,
+ deploymentCatalogueIndex,
+ deploymentCatalogueChildren,
+ deploymentOthers,
+ obsoleteUrlDeployments,
+ activeSurgeDeployments,
+ rapidResponsePersonnel,
+ emergencyResponseUnit,
+ eruReadinessForm,
+};
diff --git a/go-web-app-develop/app/src/App/routes/common.tsx b/go-web-app-develop/app/src/App/routes/common.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..4cd480efda22a640b469ef814f97a017413fb959
--- /dev/null
+++ b/go-web-app-develop/app/src/App/routes/common.tsx
@@ -0,0 +1,61 @@
+import {
+ type MyInputIndexRouteObject,
+ type MyInputNonIndexRouteObject,
+ type MyOutputIndexRouteObject,
+ type MyOutputNonIndexRouteObject,
+ wrapRoute,
+} from '#utils/routes';
+import { Component as RootLayout } from '#views/RootLayout';
+
+import Auth from '../Auth';
+import PageError from '../PageError';
+
+interface Perms {
+ isDrefRegionalCoordinator: (regionId: number | undefined) => boolean,
+ isRegionAdmin: (regionId: number | undefined) => boolean,
+ isCountryAdmin: (countryId: number | undefined) => boolean,
+ isRegionPerAdmin: (regionId: number | undefined) => boolean,
+ isCountryPerAdmin: (countryId: number | undefined) => boolean,
+ isRegionalOrCountryAdmin: boolean,
+ isPerAdmin: boolean,
+ isIfrcAdmin: boolean,
+ isSuperUser: boolean,
+ isGuestUser: boolean,
+}
+
+export type ExtendedProps = {
+ title: string,
+ visibility: 'is-authenticated' | 'is-not-authenticated' | 'anything',
+ permissions?: (
+ permissions: Perms,
+ params: Record | undefined | null,
+ ) => boolean;
+};
+
+interface CustomWrapRoute {
+ (
+ myRouteOptions: MyInputIndexRouteObject
+ ): MyOutputIndexRouteObject
+ (
+ myRouteOptions: MyInputNonIndexRouteObject
+ ): MyOutputNonIndexRouteObject
+}
+
+export const customWrapRoute: CustomWrapRoute = wrapRoute;
+
+// NOTE: We should not use layout or index routes in links
+
+export const rootLayout = customWrapRoute({
+ path: '/',
+ errorElement: ,
+ component: {
+ eagerLoad: true,
+ render: RootLayout,
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'IFRC GO',
+ visibility: 'anything',
+ },
+});
diff --git a/go-web-app-develop/app/src/App/routes/index.tsx b/go-web-app-develop/app/src/App/routes/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8a92455042854ed74fb9a1e6b88f0505fb5cf820
--- /dev/null
+++ b/go-web-app-develop/app/src/App/routes/index.tsx
@@ -0,0 +1,1371 @@
+import {
+ generatePath,
+ Navigate,
+ useParams,
+} from 'react-router-dom';
+
+import { unwrapRoute } from '#utils/routes';
+
+import Auth from '../Auth';
+import {
+ customWrapRoute,
+ rootLayout,
+} from './common';
+import countryRoutes from './CountryRoutes';
+import regionRoutes from './RegionRoutes';
+import SmartNavigate from './SmartNavigate';
+import surgeRoutes from './SurgeRoutes';
+
+const fourHundredFour = customWrapRoute({
+ parent: rootLayout,
+ path: '*',
+ component: {
+ render: () => import('#views/FourHundredFour'),
+ props: {},
+ },
+ context: {
+ title: '404',
+ visibility: 'anything',
+ },
+});
+
+const login = customWrapRoute({
+ parent: rootLayout,
+ path: 'login',
+ component: {
+ render: () => import('#views/Login'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Login',
+ visibility: 'is-not-authenticated',
+ },
+});
+
+const register = customWrapRoute({
+ parent: rootLayout,
+ path: 'register',
+ component: {
+ render: () => import('#views/Register'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Register',
+ visibility: 'is-not-authenticated',
+ },
+});
+
+const recoverAccount = customWrapRoute({
+ parent: rootLayout,
+ path: 'recover-account',
+ component: {
+ render: () => import('#views/RecoverAccount'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Recover Account',
+ visibility: 'is-not-authenticated',
+ },
+});
+
+const recoverAccountConfirm = customWrapRoute({
+ parent: rootLayout,
+ path: 'recover-account/:username/:token',
+ component: {
+ render: () => import('#views/RecoverAccountConfirm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Recover Account Confirm',
+ visibility: 'is-not-authenticated',
+ },
+});
+
+const resendValidationEmail = customWrapRoute({
+ parent: rootLayout,
+ path: 'resend-validation-email',
+ component: {
+ render: () => import('#views/ResendValidationEmail'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Resend Validation Email',
+ visibility: 'is-not-authenticated',
+ },
+});
+
+const home = customWrapRoute({
+ parent: rootLayout,
+ index: true,
+ component: {
+ render: () => import('#views/Home'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Home',
+ visibility: 'anything',
+ },
+});
+
+const emergencies = customWrapRoute({
+ parent: rootLayout,
+ path: 'emergencies',
+ component: {
+ render: () => import('#views/Emergencies'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Emergencies',
+ visibility: 'anything',
+ },
+});
+const cookiePolicy = customWrapRoute({
+ parent: rootLayout,
+ path: 'cookie-policy',
+ component: {
+ render: () => import('#views/CookiePolicy'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Cookie Policy',
+ visibility: 'anything',
+ },
+});
+
+type DefaultEmergenciesChild = 'details';
+const emergenciesLayout = customWrapRoute({
+ parent: rootLayout,
+ path: 'emergencies/:emergencyId',
+ forwardPath: 'details' satisfies DefaultEmergenciesChild,
+ component: {
+ render: () => import('#views/Emergency'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Emergency',
+ visibility: 'anything',
+ },
+});
+
+const emergencySlug = customWrapRoute({
+ parent: rootLayout,
+ path: 'emergencies/slug/:slug',
+ component: {
+ render: () => import('#views/EmergencySlug'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Emergency',
+ visibility: 'anything',
+ },
+});
+
+const emergencyFollow = customWrapRoute({
+ parent: rootLayout,
+ path: 'emergencies/:emergencyId/follow',
+ component: {
+ render: () => import('#views/EmergencyFollow'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Follow Emergency',
+ visibility: 'is-authenticated',
+ },
+});
+
+const emergencyIndex = customWrapRoute({
+ parent: emergenciesLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: SmartNavigate,
+ props: {
+ to: 'details' satisfies DefaultEmergenciesChild,
+ replace: true,
+ hashToRouteMap: {
+ '#details': 'details',
+ '#reports': 'reports',
+ '#activities': 'activities',
+ '#surge': 'surge',
+ },
+ // TODO: make this typesafe
+ forwardUnmatchedHashTo: 'additional-info',
+ },
+ },
+ context: {
+ title: 'Emergency',
+ visibility: 'anything',
+ },
+});
+
+const emergencyDetails = customWrapRoute({
+ parent: emergenciesLayout,
+ path: 'details' satisfies DefaultEmergenciesChild,
+ component: {
+ render: () => import('#views/EmergencyDetails'),
+ props: {},
+ },
+ context: {
+ title: 'Emergency Details',
+ visibility: 'anything',
+ },
+});
+
+const emergencyReportsAndDocuments = customWrapRoute({
+ parent: emergenciesLayout,
+ path: 'reports',
+ component: {
+ render: () => import('#views/EmergencyReportAndDocument'),
+ props: {},
+ },
+ context: {
+ title: 'Emergency Reports and Documents',
+ visibility: 'anything',
+ },
+});
+
+const emergencyActivities = customWrapRoute({
+ parent: emergenciesLayout,
+ path: 'activities',
+ component: {
+ render: () => import('#views/EmergencyActivities'),
+ props: {},
+ },
+ context: {
+ title: 'Emergency Activities',
+ visibility: 'anything',
+ },
+});
+const emergencySurge = customWrapRoute({
+ parent: emergenciesLayout,
+ path: 'surge',
+ component: {
+ render: () => import('#views/EmergencySurge'),
+ props: {},
+ },
+ context: {
+ title: 'Emergency Surge',
+ visibility: 'anything',
+ },
+});
+
+// TODO: remove this route
+const emergencyAdditionalInfoOne = customWrapRoute({
+ parent: emergenciesLayout,
+ path: 'additional-info-1',
+ component: {
+ render: () => import('#views/EmergencyAdditionalTab'),
+ props: {
+ infoPageId: 1,
+ },
+ },
+ context: {
+ title: 'Emergency Additional Tab 1',
+ visibility: 'anything',
+ },
+});
+// TODO: remove this route
+const emergencyAdditionalInfoTwo = customWrapRoute({
+ parent: emergenciesLayout,
+ path: 'additional-info-2',
+ component: {
+ render: () => import('#views/EmergencyAdditionalTab'),
+ props: {
+ infoPageId: 2,
+ },
+ },
+ context: {
+ title: 'Emergency Additional Tab 2',
+ visibility: 'anything',
+ },
+});
+// TODO: remove this route
+const emergencyAdditionalInfoThree = customWrapRoute({
+ parent: emergenciesLayout,
+ path: 'additional-info-3',
+ component: {
+ render: () => import('#views/EmergencyAdditionalTab'),
+ props: {
+ infoPageId: 3,
+ },
+ },
+ context: {
+ title: 'Emergency Additional Tab 3',
+ visibility: 'anything',
+ },
+});
+
+const emergencyAdditionalInfo = customWrapRoute({
+ parent: emergenciesLayout,
+ path: 'additional-info/:tabId?',
+ component: {
+ render: () => import('#views/EmergencyAdditionalTab'),
+ props: {},
+ },
+ context: {
+ title: 'Emergency Additional Info Tab',
+ visibility: 'anything',
+ },
+});
+
+type DefaultPreparednessChild = 'global-summary';
+const preparednessLayout = customWrapRoute({
+ parent: rootLayout,
+ path: 'preparedness',
+ forwardPath: 'global-summary' satisfies DefaultPreparednessChild,
+ component: {
+ render: () => import('#views/Preparedness'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Preparedness',
+ visibility: 'anything',
+ },
+});
+
+const preparednessIndex = customWrapRoute({
+ parent: preparednessLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: SmartNavigate,
+ props: {
+ to: 'global-summary' satisfies DefaultPreparednessChild,
+ replace: true,
+ hashToRouteMap: {
+ '#global-summary': 'global-summary',
+ '#global-performance': 'global-performance',
+ '#resources-catalogue': 'resources-catalogue',
+ '#operational-learning': 'operational-learning',
+ },
+ },
+ },
+ context: {
+ title: 'Preparedness',
+ visibility: 'anything',
+ },
+});
+
+const preparednessGlobalSummary = customWrapRoute({
+ parent: preparednessLayout,
+ path: 'global-summary' satisfies DefaultPreparednessChild,
+ component: {
+ render: () => import('#views/PreparednessGlobalSummary'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Preparedness - Global Summary',
+ visibility: 'anything',
+ },
+});
+
+const preparednessGlobalPerformance = customWrapRoute({
+ parent: preparednessLayout,
+ path: 'global-performance',
+ component: {
+ render: () => import('#views/PreparednessGlobalPerformance'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Preparedness - Global Performance',
+ visibility: 'anything',
+ },
+});
+
+const preparednessGlobalCatalogue = customWrapRoute({
+ parent: preparednessLayout,
+ path: 'resources-catalogue',
+ component: {
+ render: () => import('#views/PreparednessCatalogueResources'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Preparedness - Catalogue of Learning',
+ visibility: 'anything',
+ },
+});
+
+const globalThreeW = customWrapRoute({
+ parent: rootLayout,
+ path: 'three-w/projects',
+ component: {
+ render: () => import('#views/GlobalThreeW'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Global 3W',
+ visibility: 'anything',
+ },
+});
+
+const newThreeWProject = customWrapRoute({
+ parent: rootLayout,
+ path: 'three-w/projects/new',
+ component: {
+ render: () => import('#views/ThreeWDecommission'),
+ props: { variant: 'page' },
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'New 3W Project',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const threeWProjectDetail = customWrapRoute({
+ parent: rootLayout,
+ path: 'three-w/projects/:projectId/',
+ component: {
+ render: () => import('#views/ThreeWDecommission'),
+ props: { variant: 'page' },
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: '3W Project Details',
+ visibility: 'anything',
+ },
+});
+
+const threeWProjectEdit = customWrapRoute({
+ parent: rootLayout,
+ path: 'three-w/projects/:projectId/edit',
+ component: {
+ render: () => import('#views/ThreeWDecommission'),
+ props: { variant: 'page' },
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit 3W Project',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const newThreeWActivity = customWrapRoute({
+ parent: rootLayout,
+ path: 'three-w/activities/new',
+ component: {
+ render: () => import('#views/ThreeWActivityForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'New 3W Activity',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const threeWActivityDetail = customWrapRoute({
+ parent: rootLayout,
+ path: 'three-w/activities/:activityId',
+ component: {
+ render: () => import('#views/ThreeWActivityDetail'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: '3W Activity Detail',
+ visibility: 'anything',
+ },
+});
+
+const threeWActivityEdit = customWrapRoute({
+ parent: rootLayout,
+ path: 'three-w/activities/:activityId/edit',
+ component: {
+ render: () => import('#views/ThreeWActivityForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit 3W Activity',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+type DefaultRiskWatchChild = 'seasonal';
+const riskWatchLayout = customWrapRoute({
+ parent: rootLayout,
+ path: 'risk-watch',
+ forwardPath: 'seasonal' satisfies DefaultRiskWatchChild,
+ component: {
+ render: () => import('#views/RiskWatch'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Risk Watch',
+ visibility: 'anything',
+ },
+});
+
+const riskWatchIndex = customWrapRoute({
+ parent: riskWatchLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: 'seasonal' satisfies DefaultRiskWatchChild,
+ replace: true,
+ },
+ },
+ context: {
+ title: 'Risk Watch',
+ visibility: 'anything',
+ },
+});
+
+const riskWatchSeasonal = customWrapRoute({
+ parent: riskWatchLayout,
+ path: 'seasonal' satisfies DefaultRiskWatchChild,
+ component: {
+ render: () => import('#views/RiskWatchSeasonal'),
+ props: {},
+ },
+ context: {
+ title: 'Seasonal Risk Watch',
+ visibility: 'anything',
+ },
+});
+
+const riskWatchImminent = customWrapRoute({
+ parent: riskWatchLayout,
+ path: 'imminent',
+ component: {
+ render: () => import('#views/RiskWatchImminent'),
+ props: {},
+ },
+ context: {
+ title: 'Imminent Risk Watch',
+ visibility: 'anything',
+ },
+});
+
+type DefaultAccountChild = 'details';
+const accountLayout = customWrapRoute({
+ parent: rootLayout,
+ path: 'account',
+ forwardPath: 'details' satisfies DefaultAccountChild,
+ component: {
+ render: () => import('#views/Account'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Account',
+ visibility: 'is-authenticated',
+ },
+});
+
+const accountIndex = customWrapRoute({
+ parent: accountLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: SmartNavigate,
+ props: {
+ to: 'details' satisfies DefaultAccountChild,
+ replace: true,
+ hashToRouteMap: {
+ '#account-information': 'details',
+ '#notifications': 'notifications',
+ '#per-forms': 'my-forms/per',
+ '#my-dref-applications': 'my-forms/dref',
+ '#three-w-forms': 'my-forms/three-w',
+ },
+ },
+ },
+ context: {
+ title: 'Account',
+ visibility: 'anything',
+ },
+});
+
+const accountDetails = customWrapRoute({
+ parent: accountLayout,
+ path: 'details' satisfies DefaultAccountChild,
+ component: {
+ render: () => import('#views/AccountDetails'),
+ props: {},
+ },
+ context: {
+ title: 'Account Details',
+ visibility: 'is-authenticated',
+ },
+});
+
+const termsAndConditions = customWrapRoute({
+ parent: rootLayout,
+ path: 'terms-and-conditions',
+ component: {
+ render: () => import('#views/TermsAndConditions'),
+ props: {},
+ },
+ context: {
+ title: 'Terms And Conditions',
+ visibility: 'anything',
+ },
+});
+type DefaultAccountMyFormsChild = 'field-report';
+const accountMyFormsLayout = customWrapRoute({
+ parent: accountLayout,
+ path: 'my-forms',
+ forwardPath: 'field-report' satisfies DefaultAccountMyFormsChild,
+ component: {
+ render: () => import('#views/AccountMyFormsLayout'),
+ props: {},
+ },
+ context: {
+ title: 'Account - My Forms',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const accountMyFormsIndex = customWrapRoute({
+ parent: accountMyFormsLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: 'field-report' satisfies DefaultAccountMyFormsChild,
+ replace: true,
+ },
+ },
+ context: {
+ title: 'Account - My Forms',
+ visibility: 'anything',
+ },
+});
+
+const accountMyFormsFieldReport = customWrapRoute({
+ parent: accountMyFormsLayout,
+ path: 'field-report' satisfies DefaultAccountMyFormsChild,
+ component: {
+ render: () => import('#views/AccountMyFormsFieldReport'),
+ props: {},
+ },
+ context: {
+ title: 'Account - Field Report Forms',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const accountMyFormsPer = customWrapRoute({
+ parent: accountMyFormsLayout,
+ path: 'per',
+ component: {
+ render: () => import('#views/AccountMyFormsPer'),
+ props: {},
+ },
+ context: {
+ title: 'Account - PER Forms',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const accountMyFormsDref = customWrapRoute({
+ parent: accountMyFormsLayout,
+ path: 'dref',
+ component: {
+ render: () => import('#views/AccountMyFormsDref'),
+ props: {},
+ },
+ context: {
+ title: 'Account - DREF Applications',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const accountMyFormsThreeW = customWrapRoute({
+ parent: accountMyFormsLayout,
+ path: 'three-w',
+ component: {
+ render: () => import('#views/AccountMyFormsThreeW'),
+ props: {},
+ },
+ context: {
+ title: 'Account - 3W',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const accountNotifications = customWrapRoute({
+ parent: accountLayout,
+ path: 'notifications',
+ component: {
+ render: () => import('#views/AccountNotifications'),
+ props: {},
+ },
+ context: {
+ title: 'Account - Notifications',
+ visibility: 'is-authenticated',
+ },
+});
+
+const resources = customWrapRoute({
+ parent: rootLayout,
+ path: 'resources',
+ component: {
+ render: () => import('#views/Resources'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Resources',
+ visibility: 'anything',
+ },
+});
+const operationalLearning = customWrapRoute({
+ parent: rootLayout,
+ path: 'operational-learning',
+ component: {
+ render: () => import('#views/OperationalLearning'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Operational Learning',
+ visibility: 'anything',
+ },
+});
+
+const search = customWrapRoute({
+ parent: rootLayout,
+ path: 'search',
+ component: {
+ render: () => import('#views/Search'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Search',
+ visibility: 'anything',
+ },
+});
+
+const allThreeWProject = customWrapRoute({
+ parent: rootLayout,
+ path: 'three-w/projects/all',
+ component: {
+ render: () => import('#views/ThreeWDecommission'),
+ props: { variant: 'page' },
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'All 3W Projects',
+ visibility: 'anything',
+ },
+});
+
+const allThreeWActivity = customWrapRoute({
+ parent: rootLayout,
+ path: 'three-w/activities/all',
+ component: {
+ render: () => import('#views/AllThreeWActivity'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'All 3W Activities',
+ visibility: 'anything',
+ },
+});
+
+const allAppeals = customWrapRoute({
+ parent: rootLayout,
+ path: 'appeals/all',
+ component: {
+ render: () => import('#views/AllAppeals'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'All Appeals',
+ visibility: 'anything',
+ },
+});
+
+const allEmergencies = customWrapRoute({
+ parent: rootLayout,
+ path: 'emergencies/all',
+ component: {
+ render: () => import('#views/AllEmergencies'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'All Emergencies',
+ visibility: 'anything',
+ },
+});
+
+const allFieldReports = customWrapRoute({
+ parent: rootLayout,
+ path: 'field-reports/all',
+ component: {
+ render: () => import('#views/AllFieldReports'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'All Field Reports',
+ visibility: 'anything',
+ },
+});
+
+const allFlashUpdates = customWrapRoute({
+ parent: rootLayout,
+ path: 'flash-updates/all',
+ component: {
+ render: () => import('#views/AllFlashUpdates'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'All Flash Updates',
+ visibility: 'is-authenticated',
+ permissions: ({ isIfrcAdmin }) => isIfrcAdmin,
+ },
+});
+
+const flashUpdateFormNew = customWrapRoute({
+ parent: rootLayout,
+ path: 'flash-updates/new',
+ component: {
+ render: () => import('#views/FlashUpdateForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'New Flash Update',
+ visibility: 'is-authenticated',
+ permissions: ({ isIfrcAdmin }) => isIfrcAdmin,
+ },
+});
+
+const flashUpdateFormEdit = customWrapRoute({
+ parent: rootLayout,
+ path: 'flash-updates/:flashUpdateId/edit',
+ component: {
+ render: () => import('#views/FlashUpdateForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit Flash Update',
+ visibility: 'is-authenticated',
+ permissions: ({ isIfrcAdmin }) => isIfrcAdmin,
+ },
+});
+
+// FIXME: rename this route to flashUpdateDetails
+const flashUpdateFormDetails = customWrapRoute({
+ parent: rootLayout,
+ path: 'flash-updates/:flashUpdateId',
+ component: {
+ render: () => import('#views/FlashUpdateDetails'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Flash Update Details',
+ visibility: 'anything',
+ permissions: ({ isIfrcAdmin }) => isIfrcAdmin,
+ },
+});
+
+const allSurgeAlerts = customWrapRoute({
+ parent: rootLayout,
+ path: 'alerts/all',
+ component: {
+ render: () => import('#views/AllSurgeAlerts'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'All Surge Alerts',
+ visibility: 'anything',
+ },
+});
+
+const newDrefApplicationForm = customWrapRoute({
+ parent: rootLayout,
+ path: 'dref-applications/new',
+ component: {
+ render: () => import('#views/DrefApplicationForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'New DREF Application Form',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const drefApplicationForm = customWrapRoute({
+ parent: rootLayout,
+ path: 'dref-applications/:drefId/edit',
+ component: {
+ render: () => import('#views/DrefApplicationForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit DREF Application Form',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const drefApplicationExport = customWrapRoute({
+ path: 'dref-applications/:drefId/export',
+ component: {
+ render: () => import('#views/DrefApplicationExport'),
+ props: {},
+ },
+ parent: rootLayout,
+ wrapperComponent: Auth,
+ context: {
+ title: 'DREF Application Export',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const drefOperationalUpdateForm = customWrapRoute({
+ parent: rootLayout,
+ path: 'dref-operational-updates/:opsUpdateId/edit',
+ component: {
+ render: () => import('#views/DrefOperationalUpdateForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit DREF Operational Update Form',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const drefOperationalUpdateExport = customWrapRoute({
+ path: 'dref-operational-updates/:opsUpdateId/export',
+ component: {
+ render: () => import('#views/DrefOperationalUpdateExport'),
+ props: {},
+ },
+ parent: rootLayout,
+ wrapperComponent: Auth,
+ context: {
+ title: 'DREF Operational Update Export',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+const drefFinalReportForm = customWrapRoute({
+ parent: rootLayout,
+ path: 'dref-final-reports/:finalReportId/edit',
+ component: {
+ render: () => import('#views/DrefFinalReportForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit DREF Final Report Form',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const drefFinalReportExport = customWrapRoute({
+ path: 'dref-final-reports/:finalReportId/export',
+ component: {
+ render: () => import('#views/DrefFinalReportExport'),
+ props: {},
+ },
+ parent: rootLayout,
+ wrapperComponent: Auth,
+ context: {
+ title: 'DREF Final Report Export',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+// TODO: Remove me after implementation of DrefFinalReport for imminent
+const oldDrefFinalReportForm = customWrapRoute({
+ parent: rootLayout,
+ path: 'old-dref-final-reports/:finalReportId/edit',
+ component: {
+ render: () => import('#views/OldDrefFinalReportForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit DREF Final Report Form',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const oldDrefFinalReportExport = customWrapRoute({
+ path: 'old-dref-final-reports/:finalReportId/export',
+ component: {
+ render: () => import('#views/OldDrefFinalReportExport'),
+ props: {},
+ },
+ parent: rootLayout,
+ wrapperComponent: Auth,
+ context: {
+ title: 'DREF Final Report Export',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const fieldReportFormNew = customWrapRoute({
+ parent: rootLayout,
+ path: 'field-reports/new',
+ component: {
+ render: () => import('#views/FieldReportForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'New Field Report Form',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const fieldReportFormEdit = customWrapRoute({
+ parent: rootLayout,
+ path: 'field-reports/:fieldReportId/edit',
+ component: {
+ render: () => import('#views/FieldReportForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit Field Report Form',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const fieldReportDetails = customWrapRoute({
+ parent: rootLayout,
+ path: 'field-reports/:fieldReportId',
+ component: {
+ render: () => import('#views/FieldReportDetails'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Field Report Details',
+ visibility: 'anything',
+ },
+});
+
+type DefaultPerProcessChild = 'new';
+const perProcessLayout = customWrapRoute({
+ parent: rootLayout,
+ path: 'per-process',
+ forwardPath: 'new' satisfies DefaultPerProcessChild,
+ component: {
+ render: () => import('#views/PerProcessForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'PER Process',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const perProcessFormIndex = customWrapRoute({
+ parent: perProcessLayout,
+ index: true,
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: 'new' satisfies DefaultPerProcessChild,
+ replace: true,
+ },
+ },
+ context: {
+ title: 'PER Process',
+ visibility: 'anything',
+ },
+});
+
+const newPerOverviewForm = customWrapRoute({
+ parent: perProcessLayout,
+ path: 'new' satisfies DefaultPerProcessChild,
+ component: {
+ render: () => import('#views/PerOverviewForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'New PER Overview',
+ visibility: 'is-authenticated',
+ permissions: ({
+ isSuperUser,
+ isPerAdmin,
+ }) => isSuperUser || isPerAdmin,
+ },
+});
+
+const perOverviewForm = customWrapRoute({
+ parent: perProcessLayout,
+ path: ':perId/overview',
+ component: {
+ render: () => import('#views/PerOverviewForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit PER Overview',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const perAssessmentForm = customWrapRoute({
+ parent: perProcessLayout,
+ path: ':perId/assessment',
+ component: {
+ render: () => import('#views/PerAssessmentForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit PER Assessment',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const perPrioritizationForm = customWrapRoute({
+ parent: perProcessLayout,
+ path: ':perId/prioritization',
+ component: {
+ render: () => import('#views/PerPrioritizationForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit PER Prioritization',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+const perWorkPlanForm = customWrapRoute({
+ parent: perProcessLayout,
+ path: ':perId/work-plan',
+ component: {
+ render: () => import('#views/PerWorkPlanForm'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Edit PER Work Plan',
+ visibility: 'is-authenticated',
+ permissions: ({ isGuestUser }) => !isGuestUser,
+ },
+});
+
+// Redirect Routes
+const preparednessOperationalLearning = customWrapRoute({
+ parent: preparednessLayout,
+ path: 'operational-learning',
+ component: {
+ eagerLoad: true,
+ render: Navigate,
+ props: {
+ to: operationalLearning.absolutePath,
+ },
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Operational Learning',
+ visibility: 'anything',
+ },
+});
+
+// eslint-disable-next-line react-refresh/only-export-components
+function ObsoleteFieldReportRedirection() {
+ const params = useParams<{
+ fieldReportId: string,
+ }>();
+
+ const path = generatePath(
+ fieldReportDetails.absoluteForwardPath,
+ { fieldReportId: params.fieldReportId },
+ );
+
+ return (
+
+ );
+}
+
+const obsoleteFieldReportDetails = customWrapRoute({
+ parent: rootLayout,
+ path: 'reports/:fieldReportId',
+ component: {
+ eagerLoad: true,
+ render: ObsoleteFieldReportRedirection,
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Field Report Details',
+ visibility: 'anything',
+ },
+});
+
+const montandonLandingPage = customWrapRoute({
+ parent: rootLayout,
+ path: 'montandon-landing',
+ component: {
+ render: () => import('#views/MontandonLandingPage'),
+ props: {},
+ },
+ wrapperComponent: Auth,
+ context: {
+ title: 'Montandon',
+ visibility: 'anything',
+ },
+});
+
+const wrappedRoutes = {
+ fourHundredFour,
+ rootLayout,
+ login,
+ register,
+ recoverAccount,
+ recoverAccountConfirm,
+ resendValidationEmail,
+ home,
+ emergencies,
+ cookiePolicy,
+ emergencySlug,
+ emergencyFollow,
+ emergenciesLayout,
+ emergencyDetails,
+ emergencyIndex,
+ emergencyReportsAndDocuments,
+ emergencyActivities,
+ emergencySurge,
+ emergencyAdditionalInfoOne,
+ emergencyAdditionalInfoTwo,
+ emergencyAdditionalInfoThree,
+ emergencyAdditionalInfo,
+ preparednessLayout,
+ preparednessGlobalSummary,
+ preparednessGlobalPerformance,
+ preparednessGlobalCatalogue,
+ preparednessIndex,
+ perProcessFormIndex,
+ globalThreeW,
+ newThreeWProject,
+ threeWProjectEdit,
+ threeWActivityEdit,
+ threeWActivityDetail,
+ newThreeWActivity,
+ accountLayout,
+ accountIndex,
+ accountDetails,
+ accountMyFormsLayout,
+ accountMyFormsIndex,
+ accountNotifications,
+ accountMyFormsFieldReport,
+ accountMyFormsPer,
+ accountMyFormsDref,
+ accountMyFormsThreeW,
+ resources,
+ search,
+ allThreeWProject,
+ allThreeWActivity,
+ allAppeals,
+ allEmergencies,
+ allFieldReports,
+ allSurgeAlerts,
+ allFlashUpdates,
+ newDrefApplicationForm,
+ drefApplicationForm,
+ drefApplicationExport,
+ drefOperationalUpdateForm,
+ drefOperationalUpdateExport,
+ drefFinalReportForm,
+ drefFinalReportExport,
+ fieldReportFormNew,
+ fieldReportFormEdit,
+ fieldReportDetails,
+ flashUpdateFormNew,
+ flashUpdateFormDetails,
+ flashUpdateFormEdit,
+ riskWatchLayout,
+ riskWatchIndex,
+ riskWatchImminent,
+ riskWatchSeasonal,
+ perProcessLayout,
+ perOverviewForm,
+ newPerOverviewForm,
+ perAssessmentForm,
+ perPrioritizationForm,
+ perWorkPlanForm,
+ threeWProjectDetail,
+ termsAndConditions,
+ operationalLearning,
+ montandonLandingPage,
+ ...regionRoutes,
+ ...countryRoutes,
+ ...surgeRoutes,
+
+ // TODO: Remove me after implementation of DrefFinalReport for imminent
+ oldDrefFinalReportForm,
+ oldDrefFinalReportExport,
+ // Redirects
+ preparednessOperationalLearning,
+ obsoleteFieldReportDetails,
+};
+
+export const unwrappedRoutes = unwrapRoute(Object.values(wrappedRoutes));
+
+export default wrappedRoutes;
+
+export type WrappedRoutes = typeof wrappedRoutes;
diff --git a/go-web-app-develop/app/src/App/styles.module.css b/go-web-app-develop/app/src/App/styles.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..ea94ad144d1300c1ab97473d814151b6028e6d26
--- /dev/null
+++ b/go-web-app-develop/app/src/App/styles.module.css
@@ -0,0 +1,27 @@
+.fallback-element {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ justify-content: center;
+ background-color: var(--go-ui-color-background);
+ width: 100vw;
+ height: 100vh;
+ gap: 1rem;
+
+ .go-logo {
+ margin-top: -4rem;
+ height: 3rem;
+ animation: slide-up var(--go-ui-duration-animation-slow) ease-in-out forwards;
+ }
+}
+
+@keyframes slide-up {
+ from {
+ opacity: 0;
+ margin-top: -4rem;
+ }
+ to {
+ opacity: 1;
+ margin-top: 0;
+ }
+}
diff --git a/go-web-app-develop/app/src/assets/content/four_hundred_four.svg b/go-web-app-develop/app/src/assets/content/four_hundred_four.svg
new file mode 100644
index 0000000000000000000000000000000000000000..61679fc0569b326dbcbfd3d908d177eaa9de75ac
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/content/four_hundred_four.svg
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/go-web-app-develop/app/src/assets/content/four_hundred_four_background.svg b/go-web-app-develop/app/src/assets/content/four_hundred_four_background.svg
new file mode 100644
index 0000000000000000000000000000000000000000..04c548070e8aa658634f43ebbf7f36b771df3ed4
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/content/four_hundred_four_background.svg
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/go-web-app-develop/app/src/assets/content/operational_timeline_body.svg b/go-web-app-develop/app/src/assets/content/operational_timeline_body.svg
new file mode 100644
index 0000000000000000000000000000000000000000..2f259cddc5959a1db919ac99d24202fcdfdac255
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/content/operational_timeline_body.svg
@@ -0,0 +1,1023 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/go-web-app-develop/app/src/assets/content/operational_timeline_title.svg b/go-web-app-develop/app/src/assets/content/operational_timeline_title.svg
new file mode 100644
index 0000000000000000000000000000000000000000..af703ef0fd8f42b37c12e1cbe3b71aba9d2d92c2
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/content/operational_timeline_title.svg
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/go-web-app-develop/app/src/assets/content/per_approach_notext.svg b/go-web-app-develop/app/src/assets/content/per_approach_notext.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e846fa9d5b3e9281de320b8df7e4dee12131de40
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/content/per_approach_notext.svg
@@ -0,0 +1,830 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ O
+ P
+ E
+ R
+ A
+ T
+ I
+ O
+ N
+ A
+ L
+
+ S
+ U
+ P
+ P
+ O
+ R
+ T
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ P
+ O
+ L
+ I
+ C
+ Y
+ ,
+
+ S
+ T
+ R
+ A
+ T
+ E
+ G
+ Y
+
+ &
+
+ S
+ T
+ A
+ N
+ D
+ A
+ R
+ D
+ S
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A
+ N
+ A
+ L
+ Y
+ S
+ I
+ S
+
+ &
+
+ P
+ L
+ A
+ N
+ N
+ I
+ N
+ G
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ C
+ O
+ O
+ R
+ D
+ I
+ N
+ A
+ T
+ I
+ O
+ N
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ O
+ P
+ E
+ R
+ A
+ T
+ I
+ O
+ N
+ A
+ L
+
+ C
+ A
+ P
+ A
+ C
+ I
+ T
+ Y
+
+
+
+
+
+
+ &
+
+ A
+ n
+ a
+ l
+ y
+ s
+ i
+ s
+ A
+ c
+ t
+ i
+ o
+ n
+
+ &
+
+
+
+
+
+ O
+ r
+ i
+ e
+ n
+ t
+ a
+ t
+ i
+ o
+ n
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ A
+ s
+ s
+ e
+ s
+ s
+ m
+ e
+ n
+ t
+
+ W
+ o
+ r
+ k
+ -
+ P
+ l
+ a
+ n
+ A
+ c
+ c
+ o
+ u
+ n
+ t
+ a
+ b
+ i
+ l
+ i
+ t
+ y
+ P
+ r
+ i
+ o
+ r
+ i
+ t
+ i
+ s
+ a
+ t
+ i
+ o
+ n
+
+
+
diff --git a/go-web-app-develop/app/src/assets/icons/arc_logo.png b/go-web-app-develop/app/src/assets/icons/arc_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..a475a47a22b2926fcbe04ed181faa6b9e56d0dfb
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/arc_logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:575f079564d18af5c22349026d3fb2066245a48f86ca0cdf560ed03a91db04b6
+size 9605
diff --git a/go-web-app-develop/app/src/assets/icons/aurc_logo.svg b/go-web-app-develop/app/src/assets/icons/aurc_logo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..f961ba64e03caa0a22d6ecb08b1b30811120bc5b
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/aurc_logo.svg
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/go-web-app-develop/app/src/assets/icons/brc_logo.png b/go-web-app-develop/app/src/assets/icons/brc_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..a5ab19de8b54807322fc51171ce68350863caebb
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/brc_logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:48de29c95428cfd12dcf9bf811a2861a35443b1330114a00dfe0e24364e6d2bc
+size 43856
diff --git a/go-web-app-develop/app/src/assets/icons/crc_logo.png b/go-web-app-develop/app/src/assets/icons/crc_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..903ee353e6f4142d1fb8974094635412d8ed1219
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/crc_logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e26f53a76b5254429d985dea503f6d3a563e0de79036c0805996d2aa00c7b65d
+size 7209
diff --git a/go-web-app-develop/app/src/assets/icons/dnk_logo.png b/go-web-app-develop/app/src/assets/icons/dnk_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..353b624584e0177deaaa1ce7b8fbe1feb0629d32
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/dnk_logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:1952a53b1c018a8c7f86f7da3653fcea6e8e59fb80c361360a762cc42d10f8c4
+size 1810
diff --git a/go-web-app-develop/app/src/assets/icons/early_actions.svg b/go-web-app-develop/app/src/assets/icons/early_actions.svg
new file mode 100644
index 0000000000000000000000000000000000000000..3b93b121b462b7f2f1e2103b0dffdee1ac795e68
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/early_actions.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/go-web-app-develop/app/src/assets/icons/early_response.svg b/go-web-app-develop/app/src/assets/icons/early_response.svg
new file mode 100644
index 0000000000000000000000000000000000000000..c7f796fecb9c582073fb40302af8c45fe77c8fec
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/early_response.svg
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/go-web-app-develop/app/src/assets/icons/ericsson_logo.png b/go-web-app-develop/app/src/assets/icons/ericsson_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..67b8b300c7e2e58835a06e694629c2bbb241cfc0
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/ericsson_logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:82a8d71f11f1954eabfda04e4174b6c4b386825bb838d21ac03d6874a66cf1dd
+size 8472
diff --git a/go-web-app-develop/app/src/assets/icons/eru.jpg b/go-web-app-develop/app/src/assets/icons/eru.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..3bef2c7052a05ea0af7f416a1e5893447c22caa2
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/eru.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b31e6df96b68830109c9d3648183fc56d5bc2f03e73af951947231d6551074cd
+size 17503
diff --git a/go-web-app-develop/app/src/assets/icons/esp_logo.svg b/go-web-app-develop/app/src/assets/icons/esp_logo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..40388c0418f667790cac5e5e558f113d9fcbbd70
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/esp_logo.svg
@@ -0,0 +1,16 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/go-web-app-develop/app/src/assets/icons/frc_logo.png b/go-web-app-develop/app/src/assets/icons/frc_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..764181f4cde691f347ed0f2afdcbc41146d17dc5
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/frc_logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6ddfd2063a46d009d62e86c0ccf06d03e559b81466a87699221af49807096fbe
+size 5598
diff --git a/go-web-app-develop/app/src/assets/icons/go-logo-2020.svg b/go-web-app-develop/app/src/assets/icons/go-logo-2020.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e73b08f7c2648f946092c65ff2e93dad7a1354b9
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/go-logo-2020.svg
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/go-web-app-develop/app/src/assets/icons/ifrc-square.png b/go-web-app-develop/app/src/assets/icons/ifrc-square.png
new file mode 100644
index 0000000000000000000000000000000000000000..416494e6817d2fc3bd7c4450ba9276ae972c133c
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/ifrc-square.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:6091cea7550ff40df5074d8be9620214b980fbe381b281a625280f752b3ad8bb
+size 7158
diff --git a/go-web-app-develop/app/src/assets/icons/jrc_logo.png b/go-web-app-develop/app/src/assets/icons/jrc_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..1e550849d298a158bfbdc8243a9b6683a5b2330e
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/jrc_logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f377c674cfdd72b81e57eab13b6c094a03e322508172b292eb32630a22a659d8
+size 1116
diff --git a/go-web-app-develop/app/src/assets/icons/nlrc_logo.jpg b/go-web-app-develop/app/src/assets/icons/nlrc_logo.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..d2feae13025e54d1bd6c734e33e8336ac26c43be
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/nlrc_logo.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f07b2494d1602ae7f8af627e5be6203c5c8be92831eb21b2acc7ab6c6d2376d4
+size 59286
diff --git a/go-web-app-develop/app/src/assets/icons/pdc_logo.svg b/go-web-app-develop/app/src/assets/icons/pdc_logo.svg
new file mode 100644
index 0000000000000000000000000000000000000000..32fcd7f726f8c0d0d20a2a978e37fe2aa09a5d3f
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/pdc_logo.svg
@@ -0,0 +1,32 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/go-web-app-develop/app/src/assets/icons/per_logo.png b/go-web-app-develop/app/src/assets/icons/per_logo.png
new file mode 100644
index 0000000000000000000000000000000000000000..2fa710240f7361dea988ade9f3bb6284a95042ae
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/per_logo.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:e916c7202df7c4889963b92aac2452a60930866e19f8e66923f828e3349ed5e0
+size 19209
diff --git a/go-web-app-develop/app/src/assets/icons/risk/cyclone.png b/go-web-app-develop/app/src/assets/icons/risk/cyclone.png
new file mode 100644
index 0000000000000000000000000000000000000000..dd233c0c6cd3b1bdf1c3b810f93c039d96903364
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/risk/cyclone.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5ef7ff0c5eb55ff365d09cfe1dfd09c591efb50c452d9bd36d72042b03850e6d
+size 471
diff --git a/go-web-app-develop/app/src/assets/icons/risk/drought.png b/go-web-app-develop/app/src/assets/icons/risk/drought.png
new file mode 100644
index 0000000000000000000000000000000000000000..4aa837f9ac53c6856db4a841829147b03f3c99f9
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/risk/drought.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:f6584fd26c12de94660de02d3c02a2efe2ad8afebf1ccaf6915a2767496c119b
+size 532
diff --git a/go-web-app-develop/app/src/assets/icons/risk/earthquake.png b/go-web-app-develop/app/src/assets/icons/risk/earthquake.png
new file mode 100644
index 0000000000000000000000000000000000000000..b4a07f936f27b7f77a0eeb9abadfa0867e268f9b
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/risk/earthquake.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:2d3daa4c92dc8206cc7ee4fa746136e58e27540ff60bf2958b8400843f4a8119
+size 378
diff --git a/go-web-app-develop/app/src/assets/icons/risk/flood.png b/go-web-app-develop/app/src/assets/icons/risk/flood.png
new file mode 100644
index 0000000000000000000000000000000000000000..d1127853a23c1ebefc7b124d6b95b816451ec979
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/risk/flood.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b67ceba857965f4df441db3d696f61674c041b5754f2257f4f9a425312b0ae84
+size 343
diff --git a/go-web-app-develop/app/src/assets/icons/risk/wildfire.png b/go-web-app-develop/app/src/assets/icons/risk/wildfire.png
new file mode 100644
index 0000000000000000000000000000000000000000..7dc993b6c6c22aa35db1f7ac9e8abee7053769ed
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/risk/wildfire.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:5c2ba28d6babb2ba095588d331db38d5130889e363f18d1bfa3b5d05ef4e1051
+size 474
diff --git a/go-web-app-develop/app/src/assets/icons/swiss.svg b/go-web-app-develop/app/src/assets/icons/swiss.svg
new file mode 100644
index 0000000000000000000000000000000000000000..e388e797f0544be2330cc9d9047079e5d80e485b
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/swiss.svg
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/go-web-app-develop/app/src/assets/icons/us_aid.svg b/go-web-app-develop/app/src/assets/icons/us_aid.svg
new file mode 100644
index 0000000000000000000000000000000000000000..475415bfd02f3f3922e85f53d2660759a6069285
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/icons/us_aid.svg
@@ -0,0 +1,66 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/go-web-app-develop/app/src/assets/images/surge-im-composition.jpg b/go-web-app-develop/app/src/assets/images/surge-im-composition.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..355fee9540d0ca102809defd5addcbf473b60332
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/images/surge-im-composition.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d147e2bf02640fe345c4e1b0b5663c0ed692d71de0fb433cc0763e6d77345fe8
+size 107991
diff --git a/go-web-app-develop/app/src/assets/images/surge-im-pyramid.png b/go-web-app-develop/app/src/assets/images/surge-im-pyramid.png
new file mode 100644
index 0000000000000000000000000000000000000000..579881654cb17195fa3052f72b120eaef0d9b2c9
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/images/surge-im-pyramid.png
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:b233c2838100a88434ddcf3b80580d38a59cbb516c7210def8bf2c85c0e1178c
+size 88772
diff --git a/go-web-app-develop/app/src/assets/images/surge-im-support-responsible-operation.jpg b/go-web-app-develop/app/src/assets/images/surge-im-support-responsible-operation.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..718120b85e55bb4883c5d66fc8b7afb3150d81df
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/images/surge-im-support-responsible-operation.jpg
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:d5ceb3262e47bedb5a56c94a330e7307e6556aea653460fdb3868b35845658d1
+size 135310
diff --git a/go-web-app-develop/app/src/assets/images/surge-per.gif b/go-web-app-develop/app/src/assets/images/surge-per.gif
new file mode 100644
index 0000000000000000000000000000000000000000..cec015cf3a24fdcde6a9b2655346dbb00e3c0ba7
--- /dev/null
+++ b/go-web-app-develop/app/src/assets/images/surge-per.gif
@@ -0,0 +1,3 @@
+version https://git-lfs.github.com/spec/v1
+oid sha256:fe3049070a038ea52ffda9d1a32e845e5fe5e8de5d47f39d72c491c204208d2d
+size 102351
diff --git a/go-web-app-develop/app/src/components/CatalogueInfoCard/index.tsx b/go-web-app-develop/app/src/components/CatalogueInfoCard/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..1ddef83253255d0eb456358e1e6404f7d94bd913
--- /dev/null
+++ b/go-web-app-develop/app/src/components/CatalogueInfoCard/index.tsx
@@ -0,0 +1,83 @@
+import { useCallback } from 'react';
+import {
+ Container,
+ List,
+} from '@ifrc-go/ui';
+import { _cs } from '@togglecorp/fujs';
+
+import Link, { type Props as LinkProps } from '#components/Link';
+
+import styles from './styles.module.css';
+
+export type LinkData = LinkProps & {
+ title: string;
+}
+
+const catalogueInfoKeySelector = (item: LinkData) => item.title;
+interface Props {
+ className?: string;
+ title: string;
+ data: LinkData[];
+ description?: string;
+ descriptionClassName?: string;
+}
+
+function CatalogueInfoCard(props: Props) {
+ const {
+ className,
+ title,
+ data,
+ description,
+ descriptionClassName,
+ } = props;
+
+ const rendererParams = useCallback(
+ (_: string, value: LinkData): LinkProps => {
+ if (value.external) {
+ return {
+ href: value.href,
+ children: value.title,
+ external: true,
+ withLinkIcon: value.withLinkIcon,
+ };
+ }
+
+ return {
+ to: value.to,
+ urlParams: value.urlParams,
+ urlSearch: value.urlSearch,
+ urlHash: value.urlHash,
+ children: value.title,
+ withLinkIcon: value.withLinkIcon,
+ };
+ },
+ [],
+ );
+
+ return (
+
+
+
+ );
+}
+
+export default CatalogueInfoCard;
diff --git a/go-web-app-develop/app/src/components/CatalogueInfoCard/styles.module.css b/go-web-app-develop/app/src/components/CatalogueInfoCard/styles.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..ae165866ae58a0128c34374f25a6bbc735655f71
--- /dev/null
+++ b/go-web-app-develop/app/src/components/CatalogueInfoCard/styles.module.css
@@ -0,0 +1,10 @@
+.catalogue-info-card {
+ border-radius: var(--go-ui-border-radius-md);
+ box-shadow: var(--go-ui-box-shadow-md);
+
+ .list {
+ display: flex;
+ flex-direction: column;
+ gap: var(--go-ui-spacing-sm);
+ }
+}
diff --git a/go-web-app-develop/app/src/components/DiffWrapper/index.tsx b/go-web-app-develop/app/src/components/DiffWrapper/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..528ae4217efb8e0b6c982a4b1e4a70fd18cb55f6
--- /dev/null
+++ b/go-web-app-develop/app/src/components/DiffWrapper/index.tsx
@@ -0,0 +1,52 @@
+import { useMemo } from 'react';
+import { isNotDefined } from '@togglecorp/fujs';
+
+interface Props {
+ diffContainerClassName?: string;
+ value?: T;
+ oldValue?: T;
+ children: React.ReactNode;
+ enabled: boolean;
+ showOnlyDiff?: boolean;
+}
+
+function DiffWrapper(props: Props) {
+ const {
+ diffContainerClassName,
+ oldValue,
+ value,
+ children,
+ enabled = false,
+ showOnlyDiff,
+ } = props;
+
+ const hasChanged = useMemo(() => {
+ // NOTE: we consider `null` and `undefined` as same for
+ // this scenario
+ if (isNotDefined(oldValue) && isNotDefined(value)) {
+ return false;
+ }
+
+ return JSON.stringify(oldValue) !== JSON.stringify(value);
+ }, [oldValue, value]);
+
+ if (!enabled) {
+ return children;
+ }
+
+ if (!hasChanged && showOnlyDiff) {
+ return null;
+ }
+
+ if (!hasChanged) {
+ return children;
+ }
+
+ return (
+
+ {children}
+
+ );
+}
+
+export default DiffWrapper;
diff --git a/go-web-app-develop/app/src/components/DisplayName/index.tsx b/go-web-app-develop/app/src/components/DisplayName/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..48b161c4291b93d02f1c36a31fb3b1ef531fa70b
--- /dev/null
+++ b/go-web-app-develop/app/src/components/DisplayName/index.tsx
@@ -0,0 +1,9 @@
+interface DisplayNameOutputProps {
+ name: string;
+}
+
+function DisplayName({ name }: DisplayNameOutputProps) {
+ return name;
+}
+
+export default DisplayName;
diff --git a/go-web-app-develop/app/src/components/DropdownMenuItem/index.tsx b/go-web-app-develop/app/src/components/DropdownMenuItem/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..32647b00ff4e34f22a013be74b9bedfb68de29a2
--- /dev/null
+++ b/go-web-app-develop/app/src/components/DropdownMenuItem/index.tsx
@@ -0,0 +1,125 @@
+import {
+ useCallback,
+ useContext,
+} from 'react';
+import {
+ Button,
+ type ButtonProps,
+ ConfirmButton,
+ type ConfirmButtonProps,
+} from '@ifrc-go/ui';
+import { DropdownMenuContext } from '@ifrc-go/ui/contexts';
+import { isDefined } from '@togglecorp/fujs';
+
+import Link, { type Props as LinkProps } from '#components/Link';
+
+type CommonProp = {
+ persist?: boolean;
+}
+
+type ButtonTypeProps = Omit, 'type'> & {
+ type: 'button';
+}
+
+type LinkTypeProps = LinkProps & {
+ type: 'link';
+}
+
+type ConfirmButtonTypeProps = Omit, 'type'> & {
+ type: 'confirm-button',
+}
+
+type Props = CommonProp & (ButtonTypeProps | LinkTypeProps | ConfirmButtonTypeProps);
+
+function DropdownMenuItem(props: Props) {
+ const {
+ type,
+ onClick,
+ persist = false,
+ } = props;
+ const { setShowDropdown } = useContext(DropdownMenuContext);
+
+ const handleLinkClick = useCallback(
+ () => {
+ if (!persist) {
+ setShowDropdown(false);
+ }
+ // TODO: maybe add onClick here?
+ },
+ [setShowDropdown, persist],
+ );
+
+ const handleButtonClick = useCallback(
+ (name: NAME, e: React.MouseEvent) => {
+ if (!persist) {
+ setShowDropdown(false);
+ }
+ if (isDefined(onClick) && type !== 'link') {
+ onClick(name, e);
+ }
+ },
+ [setShowDropdown, type, onClick, persist],
+ );
+
+ if (type === 'link') {
+ const {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ type: _,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ persist: __,
+ variant = 'dropdown-item',
+ ...otherProps
+ } = props;
+
+ return (
+
+ );
+ }
+
+ if (type === 'button') {
+ const {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ type: _,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ persist: __,
+ variant = 'dropdown-item',
+ ...otherProps
+ } = props;
+
+ return (
+
+ );
+ }
+
+ if (type === 'confirm-button') {
+ const {
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ type: _,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ persist: __,
+ variant = 'dropdown-item',
+ ...otherProps
+ } = props;
+
+ return (
+
+ );
+ }
+}
+
+export default DropdownMenuItem;
diff --git a/go-web-app-develop/app/src/components/FourHundredThree/i18n.json b/go-web-app-develop/app/src/components/FourHundredThree/i18n.json
new file mode 100644
index 0000000000000000000000000000000000000000..0adadc11515ee21659aff4b7106948d87e3d510c
--- /dev/null
+++ b/go-web-app-develop/app/src/components/FourHundredThree/i18n.json
@@ -0,0 +1,13 @@
+{
+ "namespace": "common",
+ "strings": {
+ "permissionDeniedPageTitle":"IFRC GO - Permission Denied",
+ "permissionDeniedHeading":"Permission Denied",
+ "permissionDeniedAreYouSureUrlIsCorrect":"Are you sure the you have the right roles?",
+ "permissionDeniedGetInTouch":"Get in touch",
+ "permissionDeniedWithThePlatformTeam":"with the platform team.",
+ "permissionDeniedExploreOurHomepage":"Explore our homepage",
+ "permissionDeniedPageDescription":"Looks like you don't have the correct permissions to view this page.",
+ "permissionHeadingLabel": "403"
+ }
+}
diff --git a/go-web-app-develop/app/src/components/FourHundredThree/index.tsx b/go-web-app-develop/app/src/components/FourHundredThree/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..b528c1290d1a12356b46189af919cae0a34f3899
--- /dev/null
+++ b/go-web-app-develop/app/src/components/FourHundredThree/index.tsx
@@ -0,0 +1,63 @@
+import { SearchLineIcon } from '@ifrc-go/icons';
+import { Heading } from '@ifrc-go/ui';
+import { useTranslation } from '@ifrc-go/ui/hooks';
+
+import Link from '#components/Link';
+import Page from '#components/Page';
+
+import i18n from './i18n.json';
+import styles from './styles.module.css';
+
+function FourHundredThree() {
+ const strings = useTranslation(i18n);
+
+ return (
+
+
+
+
+
+
+ {strings.permissionHeadingLabel}
+
+
+ {strings.permissionDeniedHeading}
+
+
+ {strings.permissionDeniedPageDescription}
+
+
+
+ {strings.permissionDeniedAreYouSureUrlIsCorrect}
+
+
+ {strings.permissionDeniedGetInTouch}
+
+
+ {strings.permissionDeniedWithThePlatformTeam}
+
+
+ {strings.permissionDeniedExploreOurHomepage}
+
+
+
+ );
+}
+
+export default FourHundredThree;
diff --git a/go-web-app-develop/app/src/components/FourHundredThree/styles.module.css b/go-web-app-develop/app/src/components/FourHundredThree/styles.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..0b35e03243e8fe5e08f22420da8ad21a0cf1502b
--- /dev/null
+++ b/go-web-app-develop/app/src/components/FourHundredThree/styles.module.css
@@ -0,0 +1,54 @@
+.four-hundred-three {
+ display: flex;
+ flex-direction: column;
+
+ .main-section-container {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ }
+
+ .main {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ flex-grow: 1;
+ gap: var(--go-ui-spacing-2xl);
+
+ .top-section {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+ justify-content: center;
+ gap: var(--go-ui-spacing-xs);
+
+ .heading {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ padding: var(--go-ui-spacing-md);
+
+ .icons {
+ display: flex;
+ align-items: flex-start;
+
+ .search-icon {
+ font-size: 6rem;
+ }
+ }
+ }
+
+ .description {
+ text-align: center;
+ }
+ }
+
+ .bottom-section {
+ display: flex;
+ align-items: center;
+ flex-direction: column;
+ text-align: center;
+ gap: var(--go-ui-spacing-xs);
+ }
+ }
+}
diff --git a/go-web-app-develop/app/src/components/GlobalFooter/i18n.json b/go-web-app-develop/app/src/components/GlobalFooter/i18n.json
new file mode 100644
index 0000000000000000000000000000000000000000..5e77c0edc9151b88e5c66d1ce87bfa626d72561b
--- /dev/null
+++ b/go-web-app-develop/app/src/components/GlobalFooter/i18n.json
@@ -0,0 +1,20 @@
+{
+ "namespace": "common",
+ "strings": {
+ "footerAboutGo":"About GO",
+ "footerAboutGoDesc":"IFRC GO is a Red Cross Red Crescent platform to connect information on emergency needs with the right response.",
+ "footerFindOutMore":"Find out more",
+ "footerHelpfulLinks":"Helpful links",
+ "footerOpenSourceCode":"Open Source Code",
+ "footerApiDocumentation":"API Documentation",
+ "footerOtherResources":"Other Resources",
+ "footerGoWiki":"GO Wiki",
+ "footerContactUs":"Contact Us",
+ "footerIFRC":"© IFRC {year} v{appVersion}",
+ "globalFindOut": "Find Out More",
+ "policies": "Policies",
+ "cookiePolicy": "Cookie Policy",
+ "termsAndConditions": "Terms and Conditions",
+ "globalHelpfulLinks": "Helpful links"
+ }
+}
diff --git a/go-web-app-develop/app/src/components/GlobalFooter/index.tsx b/go-web-app-develop/app/src/components/GlobalFooter/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..f8f3f5f985c6f28990354c0c443acb7186bf4e16
--- /dev/null
+++ b/go-web-app-develop/app/src/components/GlobalFooter/index.tsx
@@ -0,0 +1,186 @@
+import {
+ SocialFacebookIcon,
+ SocialMediumIcon,
+ SocialYoutubeIcon,
+} from '@ifrc-go/icons';
+import {
+ Heading,
+ PageContainer,
+} from '@ifrc-go/ui';
+import { useTranslation } from '@ifrc-go/ui/hooks';
+import { resolveToComponent } from '@ifrc-go/ui/utils';
+import { _cs } from '@togglecorp/fujs';
+
+import Link from '#components/Link';
+import {
+ api,
+ appCommitHash,
+ appPackageName,
+ appRepositoryUrl,
+ appVersion,
+} from '#config';
+import { resolveUrl } from '#utils/resolveUrl';
+
+import i18n from './i18n.json';
+import styles from './styles.module.css';
+
+const date = new Date();
+const year = date.getFullYear();
+
+interface Props {
+ className?: string;
+}
+
+function GlobalFooter(props: Props) {
+ const {
+ className,
+ } = props;
+
+ const strings = useTranslation(i18n);
+ const versionTag = `${appPackageName}@${appVersion}`;
+ const versionUrl = `${appRepositoryUrl}/releases/tag/${versionTag}`;
+ const copyrightText = resolveToComponent(
+ strings.footerIFRC,
+ {
+ year,
+ appVersion: (
+
+ {appVersion}
+
+ ),
+ },
+ );
+
+ return (
+
+
+
+ {strings.footerAboutGo}
+
+
+ {strings.footerAboutGoDesc}
+
+
+ {copyrightText}
+
+
+
+
+ {strings.globalFindOut}
+
+
+
+ ifrc.org
+
+
+ rcrcsims.org
+
+
+ data.ifrc.org
+
+
+
+
+
+ {strings.policies}
+
+
+
+ {strings.cookiePolicy}
+
+
+ {strings.termsAndConditions}
+
+
+
+
+
+ {strings.globalHelpfulLinks}
+
+
+
+ {strings.footerOpenSourceCode}
+
+
+ {strings.footerApiDocumentation}
+
+
+ {strings.footerOtherResources}
+
+
+ {strings.footerGoWiki}
+
+
+
+
+
+ {strings.footerContactUs}
+
+
+ im@ifrc.org
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+export default GlobalFooter;
diff --git a/go-web-app-develop/app/src/components/GlobalFooter/styles.module.css b/go-web-app-develop/app/src/components/GlobalFooter/styles.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..7e052fdb023fa8d3374e08198df5c8f0dd69a8ef
--- /dev/null
+++ b/go-web-app-develop/app/src/components/GlobalFooter/styles.module.css
@@ -0,0 +1,38 @@
+.footer {
+ background-color: var(--go-ui-color-primary-gray);
+ color: var(--go-ui-color-white);
+
+ .content {
+ display: flex;
+ gap: var(--go-ui-spacing-2xl);
+ flex-wrap: wrap;
+
+ .section {
+ display: flex;
+ flex-basis: 12rem;
+ flex-direction: column;
+ flex-grow: 1;
+ gap: var(--go-ui-spacing-lg);
+
+ .sub-section {
+ display: flex;
+ flex-direction: column;
+ gap: var(--go-ui-spacing-sm);
+ }
+
+ .social-icons {
+ display: flex;
+ gap: var(--go-ui-spacing-sm);
+
+ .social-icon {
+ flex-shrink: 0;
+ font-size: var(--go-ui-height-social-icon);
+ }
+ }
+ }
+ }
+
+ @media print {
+ display: none;
+ }
+}
diff --git a/go-web-app-develop/app/src/components/Link/index.tsx b/go-web-app-develop/app/src/components/Link/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..d15c957b1d6341c6007960ebc32f7b1a046d7b1d
--- /dev/null
+++ b/go-web-app-develop/app/src/components/Link/index.tsx
@@ -0,0 +1,291 @@
+import {
+ useContext,
+ useMemo,
+} from 'react';
+import {
+ generatePath,
+ Link as InternalLink,
+ type LinkProps as RouterLinkProps,
+} from 'react-router-dom';
+import {
+ ChevronRightLineIcon,
+ ExternalLinkLineIcon,
+} from '@ifrc-go/icons';
+import type { ButtonFeatureProps } from '@ifrc-go/ui';
+import { useButtonFeatures } from '@ifrc-go/ui/hooks';
+import {
+ _cs,
+ isDefined,
+ isFalsyString,
+ isNotDefined,
+} from '@togglecorp/fujs';
+
+import RouteContext from '#contexts/route';
+import useAuth from '#hooks/domain/useAuth';
+import usePermissions from '#hooks/domain/usePermissions';
+
+import { type WrappedRoutes } from '../../App/routes';
+
+import styles from './styles.module.css';
+
+export interface UrlParams {
+ [key: string]: string | number | null | undefined;
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export function resolvePath(
+ to: keyof WrappedRoutes,
+ routes: WrappedRoutes,
+ urlParams: UrlParams | undefined,
+) {
+ const route = routes[to];
+ try {
+ const resolvedPath = generatePath(route.absoluteForwardPath, urlParams);
+ return {
+ ...route,
+ resolvedPath,
+ };
+ } catch {
+ return {
+ ...route,
+ resolvedPath: undefined,
+ };
+ }
+}
+
+// eslint-disable-next-line react-refresh/only-export-components
+export function useLink(props: {
+ external: true,
+ href: string | undefined | null,
+ to?: never,
+ urlParams?: never,
+} | {
+ external: false | undefined,
+ to: keyof WrappedRoutes | undefined | null,
+ urlParams?: UrlParams,
+ href?: never,
+}) {
+ const { isAuthenticated } = useAuth();
+ const routes = useContext(RouteContext);
+ const perms = usePermissions();
+
+ if (props.external) {
+ if (isNotDefined(props.href)) {
+ return { disabled: true, to: undefined };
+ }
+ return { disabled: false, to: props.href };
+ }
+
+ if (isNotDefined(props.to)) {
+ return { disabled: true, to: undefined };
+ }
+
+ const route = resolvePath(props.to, routes, props.urlParams);
+ const { resolvedPath } = route;
+
+ if (isNotDefined(resolvedPath)) {
+ return { disabled: true, to: undefined };
+ }
+
+ const disabled = (route.visibility === 'is-authenticated' && !isAuthenticated)
+ || (route.visibility === 'is-not-authenticated' && isAuthenticated)
+ || (route.permissions && !route.permissions(perms, props.urlParams));
+
+ return {
+ disabled,
+ to: resolvedPath,
+ };
+}
+
+export type CommonLinkProps = Omit &
+Omit<{
+ actions?: React.ReactNode;
+ actionsContainerClassName?: string;
+ disabled?: boolean;
+ icons?: React.ReactNode;
+ iconsContainerClassName?: string;
+ linkElementClassName?: string;
+ // to?: RouterLinkProps['to'];
+ variant?: ButtonFeatureProps['variant'];
+ withLinkIcon?: boolean;
+ withUnderline?: boolean;
+ ellipsize?: boolean;
+ spacing?: ButtonFeatureProps['spacing'];
+}, OMISSION>
+
+type InternalLinkProps = {
+ external?: never;
+ to: keyof WrappedRoutes | undefined | null;
+ urlParams?: UrlParams;
+ urlSearch?: string;
+ urlHash?: string;
+ href?: never;
+}
+
+export type ExternalLinkProps = {
+ external: true;
+ href: string | undefined | null;
+ urlParams?: never;
+ urlSearch?: never;
+ urlHash?: never;
+ to?: never;
+}
+
+export type Props = CommonLinkProps
+ & (InternalLinkProps | ExternalLinkProps)
+
+function Link(props: Props) {
+ const {
+ actions,
+ actionsContainerClassName,
+ children: childrenFromProps,
+ className,
+ disabled: disabledFromProps,
+ icons,
+ iconsContainerClassName,
+ linkElementClassName,
+ withUnderline,
+ withLinkIcon,
+ variant = 'tertiary',
+ ellipsize,
+ spacing,
+
+ external,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ to,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ urlParams,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ urlSearch,
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
+ urlHash,
+ ...otherProps
+ } = props;
+
+ const {
+ disabled: disabledLink,
+ to: toLink,
+ } = useLink(
+ // eslint-disable-next-line react/destructuring-assignment
+ props.external
+ // eslint-disable-next-line react/destructuring-assignment
+ ? { href: props.href, external: true }
+ // eslint-disable-next-line react/destructuring-assignment
+ : { to: props.to, external: false, urlParams: props.urlParams },
+ );
+
+ const disabled = disabledFromProps || disabledLink;
+
+ const nonLink = isFalsyString(toLink);
+
+ const {
+ children: content,
+ className: containerClassName,
+ } = useButtonFeatures({
+ className: styles.content,
+ icons,
+ children: childrenFromProps,
+ variant,
+ ellipsize,
+ disabled,
+ spacing,
+ actions: (isDefined(actions) || withLinkIcon) ? (
+ <>
+ {actions}
+ {withLinkIcon && external && (
+
+ )}
+ {withLinkIcon && !external && (
+
+ )}
+ >
+ ) : null,
+ iconsContainerClassName,
+ actionsContainerClassName,
+ });
+
+ const children = useMemo(
+ () => {
+ if (isNotDefined(toLink)) {
+ return (
+
+ {content}
+
+ );
+ }
+ // eslint-disable-next-line react/destructuring-assignment
+ if (props.external) {
+ return (
+
+ {content}
+
+ );
+ }
+
+ return (
+
+ {content}
+
+ );
+ },
+ [
+ linkElementClassName,
+ containerClassName,
+ content,
+ otherProps,
+ toLink,
+ // eslint-disable-next-line react/destructuring-assignment
+ props.urlSearch,
+ // eslint-disable-next-line react/destructuring-assignment
+ props.urlHash,
+ // eslint-disable-next-line react/destructuring-assignment
+ props.external,
+ ],
+ );
+
+ return (
+
+ {children}
+
+ );
+}
+export default Link;
diff --git a/go-web-app-develop/app/src/components/Link/styles.module.css b/go-web-app-develop/app/src/components/Link/styles.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..5e8a0e3b82b91176043375a027f82287f10181ed
--- /dev/null
+++ b/go-web-app-develop/app/src/components/Link/styles.module.css
@@ -0,0 +1,57 @@
+.link {
+ --decoration: none;
+
+ display: inline-flex;
+ align-items: flex-start;
+ flex-wrap: nowrap;
+ width: fit-content;
+
+ &.ellipsized {
+ width: 100%;
+ overflow: auto;
+ }
+
+ &.dropdown-item {
+ width: 100%;
+ }
+
+ &:not(.non-link) {
+ font-weight: var(--go-ui-font-weight-medium);
+
+ &.underline {
+ --decoration: underline;
+ }
+ }
+
+ &:not(.tertiary):not(.dropdown-item) {
+ text-align: center;
+ }
+
+ &:not(.disabled):not(.non-link) {
+ cursor: pointer;
+
+ &:hover {
+ --decoration: underline;
+ }
+ }
+
+ &.disabled {
+ pointer-events: none;
+ }
+
+ .link-element {
+ display: inline-flex;
+ align-items: center;
+ flex-grow: 1;
+ text-decoration: var(--decoration);
+
+ &:focus-visible {
+ outline: var(--go-ui-width-separator-thin) dashed var(--go-ui-color-gray-40);
+ }
+ }
+
+ .forward-icon {
+ font-size: var(--go-ui-height-icon-multiplier);
+ margin-inline-start: -0.3em;
+ }
+}
diff --git a/go-web-app-develop/app/src/components/MapContainerWithDisclaimer/i18n.json b/go-web-app-develop/app/src/components/MapContainerWithDisclaimer/i18n.json
new file mode 100644
index 0000000000000000000000000000000000000000..00fb41b79ab3921c34364af971b26cd444cbeab5
--- /dev/null
+++ b/go-web-app-develop/app/src/components/MapContainerWithDisclaimer/i18n.json
@@ -0,0 +1,19 @@
+{
+ "namespace": "common",
+ "strings": {
+ "infoLabel": "Sources: ICRC, UN CODs",
+ "mapDisclaimer":"The maps used do not imply the expression of any opinion on the part of the International Federation of Red Cross and Red Crescent Societies or National Society concerning the legal status of a territory or of its authorities.",
+ "copyrightMapbox":"© Mapbox",
+ "copyrightOSM":"© OpenStreetMap",
+ "downloadButtonTitle":"Download",
+ "failureToDownloadMessage":"Failed to download map. Try again.",
+ "improveMapLabel": "Improve this map",
+ "mapSourcesLabel": "Sources: ICRC, {uncodsLink}",
+ "mapSourceUNCODsLabel": "UNCODs",
+ "feedbackAriaLabel" : "Map feedback",
+ "mapContainerMapbox": "Mapbox",
+ "downloadHeaderLogoAltText":"IFRC GO logo",
+ "mapContainerOpenStreetMap": "OpenStreetMap",
+ "mapContainerIconButton": "Close"
+ }
+}
diff --git a/go-web-app-develop/app/src/components/MapContainerWithDisclaimer/index.tsx b/go-web-app-develop/app/src/components/MapContainerWithDisclaimer/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..ff97dce32117114a2c5f573e387ab9b0cffb8e3c
--- /dev/null
+++ b/go-web-app-develop/app/src/components/MapContainerWithDisclaimer/index.tsx
@@ -0,0 +1,230 @@
+import {
+ useCallback,
+ useRef,
+} from 'react';
+import {
+ CloseFillIcon,
+ DownloadTwoLineIcon,
+} from '@ifrc-go/icons';
+import {
+ Button,
+ DateOutput,
+ Header,
+ IconButton,
+ InfoPopup,
+ RawButton,
+} from '@ifrc-go/ui';
+import {
+ useBooleanState,
+ useTranslation,
+} from '@ifrc-go/ui/hooks';
+import { resolveToComponent } from '@ifrc-go/ui/utils';
+import { _cs } from '@togglecorp/fujs';
+import { MapContainer } from '@togglecorp/re-map';
+import FileSaver from 'file-saver';
+import { toPng } from 'html-to-image';
+
+import goLogo from '#assets/icons/go-logo-2020.svg';
+import Link from '#components/Link';
+import { mbtoken } from '#config';
+import useAlert from '#hooks/useAlert';
+
+import i18n from './i18n.json';
+import styles from './styles.module.css';
+
+interface Props {
+ className?: string;
+ title?: string;
+ footer?: React.ReactNode;
+ withoutDownloadButton?: boolean;
+}
+
+function MapContainerWithDisclaimer(props: Props) {
+ const {
+ className,
+ title = 'Map',
+ footer,
+ withoutDownloadButton = false,
+ } = props;
+
+ const strings = useTranslation(i18n);
+
+ const mapSources = resolveToComponent(
+ strings.mapSourcesLabel,
+ {
+ uncodsLink: (
+
+ {strings.mapSourceUNCODsLabel}
+
+ ),
+ },
+ );
+
+ const [
+ printMode,
+ {
+ setTrue: enterPrintMode,
+ setFalse: exitPrintMode,
+ },
+ ] = useBooleanState(false);
+
+ const containerRef = useRef(null);
+
+ const alert = useAlert();
+ const handleDownloadClick = useCallback(() => {
+ if (!containerRef?.current) {
+ alert.show(
+ strings.failureToDownloadMessage,
+ { variant: 'danger' },
+ );
+ exitPrintMode();
+ return;
+ }
+ toPng(containerRef.current, {
+ skipAutoScale: false,
+ })
+ .then((data) => FileSaver.saveAs(data, title))
+ .finally(exitPrintMode);
+ }, [
+ exitPrintMode,
+ title,
+ alert,
+ strings.failureToDownloadMessage,
+ ]);
+
+ return (
+
+ {printMode && (
+
+
+ )}
+ >
+ {strings.downloadButtonTitle}
+
+
+
+
+ >
+ )}
+ />
+ )}
+
+ {printMode && (
+
+ {title}
+
+ >
+ )}
+ actions={(
+
+ )}
+ />
+ )}
+
+
+
+
+ {strings.mapDisclaimer}
+
+
+ {mapSources}
+
+
+
+ {strings.copyrightMapbox}
+
+
+ {strings.copyrightOSM}
+
+
+ {strings.improveMapLabel}
+
+
+
+ )}
+ />
+
+ {!printMode && !withoutDownloadButton && (
+
+
+
+ )}
+ {footer}
+
+
+ );
+}
+
+export default MapContainerWithDisclaimer;
diff --git a/go-web-app-develop/app/src/components/MapContainerWithDisclaimer/styles.module.css b/go-web-app-develop/app/src/components/MapContainerWithDisclaimer/styles.module.css
new file mode 100644
index 0000000000000000000000000000000000000000..bdfa18e4a57d147476d101bd735179b655fb0387
--- /dev/null
+++ b/go-web-app-develop/app/src/components/MapContainerWithDisclaimer/styles.module.css
@@ -0,0 +1,94 @@
+.map-container {
+ display: flex;
+ flex-direction: column;
+ flex-grow: 1;
+
+ .download-header {
+ border-bottom: var(--go-ui-width-separator-lg) solid var(--go-ui-color-primary-red);
+ background-color: var(--go-ui-color-background);
+ padding: var(--go-ui-spacing-sm);
+
+ .download-heading {
+ align-items: baseline;
+ gap: var(--go-ui-spacing-xs);
+ color: var(--go-ui-color-primary-red);
+
+ .header-date {
+ font-size: var(--go-ui-font-size-xs);
+ }
+ }
+
+ .go-icon {
+ height: var(--go-ui-height-compact-status-icon);
+ }
+}
+
+.map-container-with-disclaimer {
+ display: flex;
+ position: relative;
+ flex-direction: column;
+ z-index: 0;
+
+ &.print-mode {
+ border: var(--go-ui-width-separator-sm) solid var(--go-ui-color-separator);
+ }
+
+ .map-wrapper {
+ display: flex;
+ position: relative;
+ flex-direction: column;
+ flex-grow: 1;
+
+ .container {
+ flex-grow: 1;
+ }
+ }
+
+ .map-disclaimer {
+ position: absolute;
+ bottom: var(--go-ui-spacing-xs);
+ left: calc(var(--mapbox-icon-width) + var(--go-ui-spacing-sm));
+ background-color: var(--go-ui-color-white);
+ padding: 0 var(--go-ui-spacing-2xs);
+ font-size: var(--go-ui-font-size-sm);
+ }
+
+ .download-button {
+ position: absolute;
+ top: 5rem;
+ /* NOTE: Exactly as mapbox */
+ right: calc(10px - var(--go-ui-width-separator-md));
+ border: var(--go-ui-width-separator-md) solid var(--go-ui-color-separator);
+ border-radius: var(--go-ui-border-radius-md);
+ background-color: var(--go-ui-color-foreground);
+ padding: 0 var(--go-ui-spacing-2xs);
+ font-size: var(--go-ui-height-icon-multiplier);
+
+ &:hover {
+ background-color: var(--go-ui-color-background);
+ }
+ }
+}
+
+.disclaimer-popup-content {
+ display: flex;
+ flex-direction: column;
+ gap: var(--go-ui-spacing-sm);
+
+ .attribution {
+ display: flex;
+ flex-wrap: wrap;
+ gap: var(--go-ui-spacing-2xs) var(--go-ui-spacing-sm);
+ }
+}
+
+.header {
+ background-color: var(--go-ui-color-background);
+ padding: var(--go-ui-spacing-md);
+
+ .actions {
+ align-items: center;
+ }
+}
+
+}
diff --git a/go-web-app-develop/app/src/components/MapPopup/i18n.json b/go-web-app-develop/app/src/components/MapPopup/i18n.json
new file mode 100644
index 0000000000000000000000000000000000000000..36e06a2a1ea0ef2a4840496d1a7525a8fa53eb55
--- /dev/null
+++ b/go-web-app-develop/app/src/components/MapPopup/i18n.json
@@ -0,0 +1,6 @@
+{
+ "namespace": "common",
+ "strings": {
+ "messagePopupClose": "Close"
+ }
+}
diff --git a/go-web-app-develop/app/src/components/MapPopup/index.tsx b/go-web-app-develop/app/src/components/MapPopup/index.tsx
new file mode 100644
index 0000000000000000000000000000000000000000..8d1b6c7932028f25aa71b96ed2ffe595aac0f296
--- /dev/null
+++ b/go-web-app-develop/app/src/components/MapPopup/index.tsx
@@ -0,0 +1,80 @@
+import { useMemo } from 'react';
+import { CloseLineIcon } from '@ifrc-go/icons';
+import {
+ Button,
+ Container,
+ type ContainerProps,
+} from '@ifrc-go/ui';
+import { useTranslation } from '@ifrc-go/ui/hooks';
+import { _cs } from '@togglecorp/fujs';
+import { MapPopup as BasicMapPopup } from '@togglecorp/re-map';
+
+import i18n from './i18n.json';
+import styles from './styles.module.css';
+
+interface Props extends ContainerProps {
+ coordinates: mapboxgl.LngLatLike;
+ children: React.ReactNode;
+ onCloseButtonClick: () => void;
+ popupClassName?: string;
+}
+
+function MapPopup(props: Props) {
+ const {
+ children,
+ coordinates,
+ onCloseButtonClick,
+ actions,
+ childrenContainerClassName,
+ popupClassName,
+ ...containerProps
+ } = props;
+
+ const strings = useTranslation(i18n);
+
+ const popupOptions = useMemo