diff --git a/.circleci/config.yml b/.circleci/config.yml index a9b09bd1a5..128fb6bbb8 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -101,16 +101,6 @@ commands: - *restore_yarn_cache - *install_packages - *save_yarn_cache - inject_instance_configuration: - steps: - - run: - name: Set branch environment - command: | - echo 'export REACT_APP_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV - - run: - name: Inject target environment configuration - command: | - yarn build:inject-config firebase_deploy: description: Deploy to Firebase @@ -301,13 +291,11 @@ jobs: - run: name: Set branch environment command: | - echo 'export REACT_APP_BRANCH=${CIRCLE_BRANCH}' >> $BASH_ENV - echo 'export REACT_APP_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV + echo 'export VITE_PROJECT_VERSION=${CIRCLE_SHA1}' >> $BASH_ENV - run: name: Check environment variables command: | - echo REACT_APP_BRANCH=$REACT_APP_BRANCH - echo $REACT_APP_PROJECT_VERSION + echo $VITE_PROJECT_VERSION - run: command: yarn build - persist_to_workspace: @@ -326,24 +314,74 @@ jobs: - run: command: yarn storybook:build deploy: - docker: *docker + docker: + - image: cimg/node:20.7.0 resource_class: medium+ parameters: # optional environment variables to set during build process DEPLOY_ALIAS: type: string default: 'default' + FLY_APP_NAME: + type: string + default: 'default' + FLY_TOML: + type: string + default: 'default' environment: CYPRESS_INSTALL_BINARY: 0 steps: - setup_repo - attach_workspace: at: '.' - - inject_instance_configuration - - firebase_deploy: - # token: $FIREBASE_DEPLOY_TOKEN # This should be set as environment variable - alias: << parameters.DEPLOY_ALIAS >> - + # - firebase_deploy: + # # token: $FIREBASE_DEPLOY_TOKEN # This should be set as environment variable + # alias: << parameters.DEPLOY_ALIAS >> + - run: + name: Prune Docker resources + command: | + docker system prune -a + - run: + name: Install fly command + command: curl -L https://fly.io/install.sh | sh + - run: + name: Add fly to PATH + command: echo "export PATH=\"/home/circleci/.fly/bin:$PATH\"" >> $BASH_ENV + - run: + name: Login fly + command: flyctl auth token $FLY_API_TOKEN --debug --verbose + - run: + name: Deploy to fly + command: | + flyctl deploy \ + --app << parameters.FLY_APP_NAME >> \ + --config << parameters.FLY_TOML >> \ + --debug --verbose \ + --build-secret VITE_BRANCH="$VITE_BRANCH" \ + --build-secret VITE_CDN_URL="$VITE_CDN_URL" \ + --build-secret VITE_FIREBASE_API_KEY="$VITE_FIREBASE_API_KEY" \ + --build-secret VITE_FIREBASE_AUTH_DOMAIN="$VITE_FIREBASE_AUTH_DOMAIN" \ + --build-secret VITE_FIREBASE_DATABASE_URL="$VITE_FIREBASE_DATABASE_URL" \ + --build-secret VITE_FIREBASE_MESSAGING_SENDER_ID="$VITE_FIREBASE_MESSAGING_SENDER_ID" \ + --build-secret VITE_FIREBASE_PROJECT_ID="$VITE_FIREBASE_PROJECT_ID" \ + --build-secret VITE_FIREBASE_STORAGE_BUCKET="$VITE_FIREBASE_STORAGE_BUCKET" \ + --build-secret VITE_SENTRY_DSN="$VITE_SENTRY_DSN" \ + --build-secret VITE_GA_TRACKING_ID="$VITE_GA_TRACKING_ID" \ + --build-secret VITE_PATREON_CLIENT_ID="$VITE_PATREON_CLIENT_ID" \ + --build-secret VITE_PLATFORM_THEME="$VITE_PLATFORM_THEME" \ + --build-secret VITE_PROJECT_VERSION="$VITE_PROJECT_VERSION" \ + --build-secret VITE_SUPPORTED_MODULES="$VITE_SUPPORTED_MODULES" \ + --build-secret VITE_ACADEMY_RESOURCE="$VITE_ACADEMY_RESOURCE" \ + --build-secret VITE_API_URL="$VITE_API_URL" \ + --build-secret VITE_PROFILE_GUIDELINES_URL="$VITE_PROFILE_GUIDELINES_URL" \ + --build-secret VITE_SITE_NAME="$VITE_SITE_NAME" \ + --build-secret VITE_THEME="$VITE_THEME" \ + --build-secret VITE_DONATIONS_BODY="$VITE_DONATIONS_BODY" \ + --build-secret VITE_DONATIONS_IFRAME_SRC="$VITE_DONATIONS_IFRAME_SRC" \ + --build-secret VITE_DONATIONS_IMAGE_URL="$VITE_DONATIONS_IMAGE_URL" \ + --build-secret VITE_HOWTOS_HEADING="$VITE_HOWTOS_HEADING" \ + --build-secret VITE_COMMUNITY_PROGRAM_URL="$VITE_COMMUNITY_PROGRAM_URL" \ + --build-secret VITE_QUESTIONS_GUIDELINES_URL="$VITE_QUESTIONS_GUIDELINES_URL" # Run cypress e2e tests on chrome test_e2e: docker: *docker @@ -360,7 +398,6 @@ jobs: # retrieve build folder - attach_workspace: at: '.' - - inject_instance_configuration # install testing browsers are required - when: condition: @@ -477,73 +514,54 @@ workflows: - e2e-tests <<: *filter_only_default_branch #---------------------- Development Instances Build and Deploy ---------------------- - - deploy: - name: 'Deploy: dev.onearmy.world' - requires: - - test_e2e - <<: *filter_only_default_branch - DEPLOY_ALIAS: 'default' - context: - - circle-ci-patreon-context - - community-platform-dev - - deploy: - name: 'Deploy: dev.community.projectkamp.com' - requires: - - test_e2e - <<: *filter_only_default_branch - DEPLOY_ALIAS: project-kamp-development - context: - - circle-ci-patreon-context - - project-kamp-dev - - deploy: - name: 'Deploy: dev.community.fixing.fashion' - requires: - - test_e2e - <<: *filter_only_default_branch - DEPLOY_ALIAS: fixing-fashion-dev - context: - - circle-ci-patreon-context - - fixing-fashion-dev - approve: type: approval name: 'Approve Production deployment' requires: - - 'Deploy: dev.onearmy.world' - - 'Deploy: dev.community.fixing.fashion' - - 'Deploy: dev.community.projectkamp.com' + - test_e2e #---------------------- Development Instances Build and Deploy ---------------------- - - release: - name: Release new version to GitHub - context: - - release-context - requires: - - "Approve Production deployment" - - build: - name: Build Production Release - context: build-context - requires: - - 'Release new version to GitHub' + # - release: + # name: Release new version to GitHub + # context: + # - release-context + # requires: + # - "Approve Production deployment" + # - build: + # name: Build Production Release + # context: build-context + # requires: + # - 'Release new version to GitHub' - deploy: name: 'Deploy: community.fixing.fashion' requires: - - 'Build Production Release' + - 'Approve Production deployment' DEPLOY_ALIAS: fixing-fashion-prod + FLY_APP_NAME: community-platform-ff + FLY_TOML: fly-ff.toml context: - circle-ci-patreon-context - fixing-fashion-prod + - fly-deploy - deploy: name: 'Deploy: community.preciousplastic.com' requires: - - 'Build Production Release' + - 'Approve Production deployment' DEPLOY_ALIAS: 'production' + FLY_APP_NAME: community-platform-pp + FLY_TOML: fly-pp.toml context: - circle-ci-patreon-context - community-platform-production + - fly-deploy - deploy: name: 'Deploy: community.projectkamp.com' requires: - - 'Build Production Release' + - 'Approve Production deployment' DEPLOY_ALIAS: project-kamp-production + FLY_APP_NAME: community-platform-pk + FLY_TOML: fly-pk.toml context: - circle-ci-patreon-context - project-kamp-production + - fly-deploy + \ No newline at end of file diff --git a/.dockerignore b/.dockerignore index 4d7d8c9361..8e6f92f0dc 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,3 +1,16 @@ containerization -node_modules -dump \ No newline at end of file +*node_modules* +dump +build +functions +extensions +.circleci +.github +.husky +.nxs +docs +packages/cypress +packages/documentation +packages/security-rules +packages/simulated-webhook-receiver +scripts \ No newline at end of file diff --git a/.env b/.env new file mode 100644 index 0000000000..315ce51557 --- /dev/null +++ b/.env @@ -0,0 +1,20 @@ +### Prefix VITE_ to use client-side (only for non-sensitive data!) +PORT=3000 +WS_URLS=localhost:24678,ws://localhost:24678 +VITE_THEME=precious-plastic +# VITE_THEME=fixing-fashion +# VITE_THEME=project-kamp +VITE_ACADEMY_RESOURCE=https://onearmy.github.io/academy/ +# VITE_ACADEMY_RESOURCE=https://project-kamp-academy.netlify.app/ +# VITE_ACADEMY_RESOURCE=https://fixing-fashion-academy.netlify.app/ +VITE_DONATIONS_BODY=All of the content here is free. Your donation supports this library of Open Source recycling knowledge. Making it possible for everyone in the world to use it and start recycling. +VITE_DONATIONS_IFRAME_SRC=https://donorbox.org/embed/ppcpdonor?language=en +VITE_DONATIONS_IMAGE_URL=/assets/img/precious-plastic/donation-banner.jpg +VITE_HOWTOS_HEADING=Learn & share how to recycle, build and work with plastic +VITE_SITE_NAME=Precious Plastic +VITE_COMMUNITY_PROGRAM_URL=https://community.preciousplastic.com/academy/guides/community-program +# VITE_COMMUNITY_PROGRAM_URL=https://community.fixing.fashion/academy/guides/community-program +VITE_PROFILE_GUIDELINES_URL=https://community.preciousplastic.com/academy/guides/platform +# VITE_PROFILE_GUIDELINES_URL=https://drive.google.com/file/d/1fXTtBbzgCO0EL6G9__aixwqc-Euqgqnd/view +# VITE_PROFILE_GUIDELINES_URL=https://community.fixing.fashion/academy/guides/profile +VITE_QUESTIONS_GUIDELINES_URL=https://community.preciousplastic.com/academy/guides/guidelines-questions \ No newline at end of file diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 8bcf17443b..7b081f5db2 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -45,9 +45,9 @@ jobs: - name: Install npm dependencies run: yarn install --immutable - name: Set environment variables - run: export REACT_APP_PROJECT_VERSION=${GITHUB_SHA} + run: export VITE_PROJECT_VERSION=${GITHUB_SHA} - name: Check environment variables - run: echo $REACT_APP_PROJECT_VERSION + run: echo $VITE_PROJECT_VERSION - name: Build for Preview run: npm run build env: @@ -55,7 +55,7 @@ jobs: # disable until fully resolved CI: false # specify the 'preview' site variant to populate the relevant firebase config - REACT_APP_SITE_VARIANT: preview + VITE_SITE_VARIANT: preview # The hosting-deploy action calls firebase tools via npx, however installing globally # gives us control over what version will be made available - name: Install firebase-tools globally diff --git a/.gitignore b/.gitignore index ce984931af..991180bab1 100644 --- a/.gitignore +++ b/.gitignore @@ -19,7 +19,6 @@ functions/backup.json # misc .DS_Store -.env .env.local .env.development.local .env.test.local diff --git a/.prettierignore b/.prettierignore index 1c3c8ea324..4fbd818a17 100644 --- a/.prettierignore +++ b/.prettierignore @@ -3,4 +3,5 @@ build storybook-static dist firestore.indexes.json -firebase.json \ No newline at end of file +firebase.json +.md \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..726b69d797 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,146 @@ +# syntax = docker/dockerfile:1 + +FROM node:20-slim AS base + +LABEL fly_launch_runtime="Remix" + +# Remix app lives here +WORKDIR /app + +# Set production environment +ENV NODE_ENV="production" +ARG YARN_VERSION=3.6.4 + +# Install Yarn 3 +RUN corepack enable && \ + yarn set version ${YARN_VERSION} + +# Add CircleCI context variables as ARGs +ARG VITE_BRANCH +ARG VITE_CDN_URL +ARG VITE_FIREBASE_API_KEY +ARG VITE_FIREBASE_AUTH_DOMAIN +ARG VITE_FIREBASE_DATABASE_URL +ARG VITE_FIREBASE_MESSAGING_SENDER_ID +ARG VITE_FIREBASE_PROJECT_ID +ARG VITE_FIREBASE_STORAGE_BUCKET +ARG VITE_SENTRY_DSN +ARG VITE_GA_TRACKING_ID +ARG VITE_PATREON_CLIENT_ID +ARG VITE_PLATFORM_THEME +ARG VITE_PROJECT_VERSION +ARG VITE_SUPPORTED_MODULES +ARG VITE_ACADEMY_RESOURCE +ARG VITE_API_URL +ARG VITE_PROFILE_GUIDELINES_URL +ARG VITE_SITE_NAME +ARG VITE_THEME +ARG VITE_DONATIONS_BODY +ARG VITE_DONATIONS_IFRAME_SRC +ARG VITE_DONATIONS_IMAGE_URL +ARG VITE_HOWTOS_HEADING +ARG VITE_COMMUNITY_PROGRAM_URL +ARG VITE_QUESTIONS_GUIDELINES_URL + +# Throw-away build stage to reduce size of final image +FROM base AS build + +# Install packages needed to build node modules +RUN apt-get update -qq && \ + apt-get install --no-install-recommends -y build-essential node-gyp pkg-config python-is-python3 + +# Copy source code +ADD . . + +# Install packages +RUN yarn install + +RUN --mount=type=secret,id=VITE_BRANCH \ + --mount=type=secret,id=VITE_CDN_URL \ + --mount=type=secret,id=VITE_FIREBASE_API_KEY \ + --mount=type=secret,id=VITE_FIREBASE_AUTH_DOMAIN \ + --mount=type=secret,id=VITE_FIREBASE_DATABASE_URL \ + --mount=type=secret,id=VITE_FIREBASE_MESSAGING_SENDER_ID \ + --mount=type=secret,id=VITE_FIREBASE_PROJECT_ID \ + --mount=type=secret,id=VITE_FIREBASE_STORAGE_BUCKET \ + --mount=type=secret,id=VITE_SENTRY_DSN \ + --mount=type=secret,id=VITE_GA_TRACKING_ID \ + --mount=type=secret,id=VITE_PATREON_CLIENT_ID \ + --mount=type=secret,id=VITE_PLATFORM_THEME \ + --mount=type=secret,id=VITE_PROJECT_VERSION \ + --mount=type=secret,id=VITE_SUPPORTED_MODULES \ + --mount=type=secret,id=VITE_ACADEMY_RESOURCE \ + --mount=type=secret,id=VITE_API_URL \ + --mount=type=secret,id=VITE_PROFILE_GUIDELINES_URL \ + --mount=type=secret,id=VITE_SITE_NAME \ + --mount=type=secret,id=VITE_THEME \ + --mount=type=secret,id=VITE_DONATIONS_BODY \ + --mount=type=secret,id=VITE_DONATIONS_IFRAME_SRC \ + --mount=type=secret,id=VITE_DONATIONS_IMAGE_URL \ + --mount=type=secret,id=VITE_HOWTOS_HEADING \ + --mount=type=secret,id=VITE_COMMUNITY_PROGRAM_URL \ + --mount=type=secret,id=VITE_QUESTIONS_GUIDELINES_URL \ + VITE_CDN_URL="$(cat /run/secrets/VITE_CDN_URL)" && \ + VITE_BRANCH="$(cat /run/secrets/VITE_BRANCH)" && \ + VITE_FIREBASE_API_KEY="$(cat /run/secrets/VITE_FIREBASE_API_KEY)" && \ + VITE_FIREBASE_AUTH_DOMAIN="$(cat /run/secrets/VITE_FIREBASE_AUTH_DOMAIN)" && \ + VITE_FIREBASE_DATABASE_URL="$(cat /run/secrets/VITE_FIREBASE_DATABASE_URL)" && \ + VITE_FIREBASE_MESSAGING_SENDER_ID="$(cat /run/secrets/VITE_FIREBASE_MESSAGING_SENDER_ID)" && \ + VITE_FIREBASE_PROJECT_ID="$(cat /run/secrets/VITE_FIREBASE_PROJECT_ID)" && \ + VITE_FIREBASE_STORAGE_BUCKET="$(cat /run/secrets/VITE_FIREBASE_STORAGE_BUCKET)" && \ + VITE_SENTRY_DSN="$(cat /run/secrets/VITE_SENTRY_DSN)" && \ + VITE_GA_TRACKING_ID="$(cat /run/secrets/VITE_GA_TRACKING_ID)" && \ + VITE_PATREON_CLIENT_ID="$(cat /run/secrets/VITE_PATREON_CLIENT_ID)" && \ + VITE_PLATFORM_THEME="$(cat /run/secrets/VITE_PLATFORM_THEME)" && \ + VITE_PROJECT_VERSION="$(cat /run/secrets/VITE_PROJECT_VERSION)" && \ + VITE_SUPPORTED_MODULES="$(cat /run/secrets/VITE_SUPPORTED_MODULES)" && \ + VITE_ACADEMY_RESOURCE="$(cat /run/secrets/VITE_ACADEMY_RESOURCE)" && \ + VITE_API_URL="$(cat /run/secrets/VITE_API_URL)" && \ + VITE_PROFILE_GUIDELINES_URL="$(cat /run/secrets/VITE_PROFILE_GUIDELINES_URL)" && \ + VITE_SITE_NAME="$(cat /run/secrets/VITE_SITE_NAME)" && \ + VITE_THEME="$(cat /run/secrets/VITE_THEME)" && \ + VITE_DONATIONS_BODY="$(cat /run/secrets/VITE_DONATIONS_BODY)" && \ + VITE_DONATIONS_IFRAME_SRC="$(cat /run/secrets/VITE_DONATIONS_IFRAME_SRC)" && \ + VITE_DONATIONS_IMAGE_URL="$(cat /run/secrets/VITE_DONATIONS_IMAGE_URL)" && \ + VITE_HOWTOS_HEADING="$(cat /run/secrets/VITE_HOWTOS_HEADING)" && \ + VITE_COMMUNITY_PROGRAM_URL="$(cat /run/secrets/VITE_COMMUNITY_PROGRAM_URL)" && \ + VITE_QUESTIONS_GUIDELINES_URL="$(cat /run/secrets/VITE_QUESTIONS_GUIDELINES_URL)" && \ + echo "VITE_CDN_URL=\"${VITE_CDN_URL}\"" >> .env && \ + echo "VITE_BRANCH=\"${VITE_BRANCH}\"" >> .env && \ + echo "VITE_FIREBASE_API_KEY=\"${VITE_FIREBASE_API_KEY}\"" >> .env && \ + echo "VITE_FIREBASE_AUTH_DOMAIN=\"${VITE_FIREBASE_AUTH_DOMAIN}\"" >> .env && \ + echo "VITE_FIREBASE_DATABASE_URL=\"${VITE_FIREBASE_DATABASE_URL}\"" >> .env && \ + echo "VITE_FIREBASE_MESSAGING_SENDER_ID=\"${VITE_FIREBASE_MESSAGING_SENDER_ID}\"" >> .env && \ + echo "VITE_FIREBASE_PROJECT_ID=\"${VITE_FIREBASE_PROJECT_ID}\"" >> .env && \ + echo "VITE_FIREBASE_STORAGE_BUCKET=\"${VITE_FIREBASE_STORAGE_BUCKET}\"" >> .env && \ + echo "VITE_SENTRY_DSN=\"${VITE_SENTRY_DSN}\"" >> .env && \ + echo "VITE_GA_TRACKING_ID=\"${VITE_GA_TRACKING_ID}\"" >> .env && \ + echo "VITE_PATREON_CLIENT_ID=\"${VITE_PATREON_CLIENT_ID}\"" >> .env && \ + echo "VITE_PLATFORM_THEME=\"${VITE_PLATFORM_THEME}\"" >> .env && \ + echo "VITE_PROJECT_VERSION=\"${VITE_PROJECT_VERSION}\"" >> .env && \ + echo "VITE_SUPPORTED_MODULES=\"${VITE_SUPPORTED_MODULES}\"" >> .env && \ + echo "VITE_ACADEMY_RESOURCE=\"${VITE_ACADEMY_RESOURCE}\"" >> .env && \ + echo "VITE_API_URL=\"${VITE_API_URL}\"" >> .env && \ + echo "VITE_PROFILE_GUIDELINES_URL=\"${VITE_PROFILE_GUIDELINES_URL}\"" >> .env && \ + echo "VITE_SITE_NAME=\"${VITE_SITE_NAME}\"" >> .env && \ + echo "VITE_THEME=\"${VITE_THEME}\"" >> .env && \ + echo "VITE_DONATIONS_BODY=\"${VITE_DONATIONS_BODY}\"" >> .env && \ + echo "VITE_DONATIONS_IFRAME_SRC=\"${VITE_DONATIONS_IFRAME_SRC}\"" >> .env && \ + echo "VITE_DONATIONS_IMAGE_URL=\"${VITE_DONATIONS_IMAGE_URL}\"" >> .env && \ + echo "VITE_HOWTOS_HEADING=\"${VITE_HOWTOS_HEADING}\"" >> .env && \ + echo "VITE_COMMUNITY_PROGRAM_URL=\"${VITE_COMMUNITY_PROGRAM_URL}\"" >> .env && \ + echo "VITE_QUESTIONS_GUIDELINES_URL=\"${VITE_QUESTIONS_GUIDELINES_URL}\"" >> .env + +# Build application +RUN yarn run build + +# Final stage for app image +FROM base + +# Copy built application +COPY --from=build /app /app + +# Start the server by default, this can be overwritten at runtime +EXPOSE 3000 +CMD [ "yarn", "run", "start" ] + diff --git a/docker-compose.yml b/docker-compose.yml deleted file mode 100644 index 61df69c9f7..0000000000 --- a/docker-compose.yml +++ /dev/null @@ -1,22 +0,0 @@ -# TODO: update to a more new version -version: "2.1" -services: - - emulator: - container_name: backend - build: - context: ./ - dockerfile: ./containerization/Dockerfile - depends_on: - simulated-webhook-receiver: - condition: service_healthy - ports: - - 4001-4008:4001-4008 - volumes: - - ./:/app - - simulated-webhook-receiver: - container_name: simulated-webhook-receiver - build: - context: ./packages/simulated-webhook-receiver - dockerfile: Dockerfile diff --git a/docs/remix.md b/docs/remix.md new file mode 100644 index 0000000000..1bc34d1d79 --- /dev/null +++ b/docs/remix.md @@ -0,0 +1,28 @@ +### What is Remix? +A React Fullstack Framework that provides server-side rendering and an API Layer. + +## Routing +It follows a [File System Route Convention](https://remix.run/docs/en/main/start/v2#file-system-route-convention) where each route is defined in the *routes* folder. + +(Soon will be changed to routes.ts approach -> https://www.youtube.com/live/fjTX8hQTlEc?si=PISGwpmF603tvre_&t=726) + +Each route is a normal React component file that should include a [loader](https://remix.run/docs/en/main/route/loader) function, that function runs exclusively on the server (or clientLoader for browser only). +Parts of the component might be rendered client side, for that we can use React.lazy or wrap them with component from `remix-utils`. + +Additionally, routes could also export [Links](https://remix.run/docs/en/main/route/links) and [Meta](https://remix.run/docs/en/main/route/meta) functions that will be added to the html head. + +For the API, we can use [action/loader](https://remix.run/docs/en/main/route/action) routes. + +## Migration +- Most files have changes only to update the imports from 'react-router' to '@remix-run/react'. +- MapPin.tsx file name changed to MapPin.client.tsx [so it's not run server-side](https://remix.run/docs/en/main/discussion/server-vs-client#splitting-up-client-and-server-code). Without this change, it throws an error 'window' isn't defined - from the leaflet package. +- A `routes` folder was created, following [Remix routing convention](https://remix.run/docs/en/main/file-conventions/routes) + - Notice the $ in `academy.$.tsx` it ensures academy sub-routes still load the academy page. + - _index.tsx was created to replicate the current behaviour of redirecting to the Academy page. Later it could be used for the `HubPage`. + - All routes have already been migrated + - More routing details in the link above 😊 + +- Current issues + - unit and e2e tests are not passing + - localStorage/sessionStorage/window usage might need a refactor + - DevSiteHeader (minor) -> needs a navigation/refresh after changing theme \ No newline at end of file diff --git a/firebase.json b/firebase.json index d8d1c6dd37..2a2b8424d2 100644 --- a/firebase.json +++ b/firebase.json @@ -1,95 +1,5 @@ { "$schema": "./node_modules/firebase-tools/schema/firebase-config.json", - "hosting": { - "public": "build", - "ignore": [ - "firebase.json", - "**/.*", - "**/node_modules/**" - ], - "rewrites": [ - { - "source": "/api", - "function": "api" - }, - { - "source": "/how-to/**", - "function": "seo-seoRender" - }, - { - "source": "/_logging", - "function": "logToCloudLogging-logToCloudLogging" - }, - { - "source": "/research/**", - "function": "seo-seoRender" - }, - { - "source": "/sitemap.xml", - "function": "seo-sitemapProxy" - }, - { - "source": "/static/**", - "destination": "/static/**" - }, - { - "source": "**", - "destination": "/index.html", - "headers": [ - { - "key": "Access-Control-Allow-Origin", - "value": "*" - } - ] - } - ], - "headers": [ - { - "source": "**/*.@(eot|otf|ttf|ttc|woff|font.css)", - "headers": [ - { - "key": "Access-Control-Allow-Origin", - "value": "*" - } - ] - }, - { - "source": "**/*.@(jpg|jpeg|gif|png|svg)", - "headers": [ - { - "key": "Cache-Control", - "value": "max-age=604800, s-maxage=3600" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "*" - } - ] - }, - { - "source": "static/**/*.@(js|css|map)", - "headers": [ - { - "key": "Cache-Control", - "value": "public, max-age=31536000" - }, - { - "key": "Access-Control-Allow-Origin", - "value": "*" - } - ] - }, - { - "source": "/service-worker.js", - "headers": [ - { - "key": "Cache-Control", - "value": "no-cache" - } - ] - } - ] - }, "functions": { "predeploy": [ "yarn workspace functions build" diff --git a/fly-ff.toml b/fly-ff.toml new file mode 100644 index 0000000000..0d74e09125 --- /dev/null +++ b/fly-ff.toml @@ -0,0 +1,18 @@ +app = 'community-platform-ff' +primary_region = 'cdg' + +[build] + +[http_service] + internal_port = 3000 + force_https = true + auto_stop_machines = 'off' + processes = ['app'] + +[env] + VITE_BRANCH = "production" + +[[vm]] + memory = '4gb' + cpu_kind = 'shared' + cpus = 4 diff --git a/fly-pk.toml b/fly-pk.toml new file mode 100644 index 0000000000..e54fc228ce --- /dev/null +++ b/fly-pk.toml @@ -0,0 +1,18 @@ +app = 'community-platform-pk' +primary_region = 'cdg' + +[build] + +[http_service] + internal_port = 3000 + force_https = true + auto_stop_machines = 'off' + processes = ['app'] + +[env] + VITE_BRANCH = "production" + +[[vm]] + memory = '4gb' + cpu_kind = 'shared' + cpus = 4 diff --git a/fly-pp.toml b/fly-pp.toml new file mode 100644 index 0000000000..cdd0b52960 --- /dev/null +++ b/fly-pp.toml @@ -0,0 +1,18 @@ +app = 'community-platform-pp' +primary_region = 'cdg' + +[build] + +[http_service] + internal_port = 3000 + force_https = true + auto_stop_machines = 'off' + processes = ['app'] + +[env] + VITE_BRANCH = "production" + +[[vm]] + memory = '4gb' + cpu_kind = 'shared' + cpus = 4 diff --git a/functions/scripts/runtimeConfig/model.ts b/functions/scripts/runtimeConfig/model.ts index c7adb3de91..420400fbaa 100644 --- a/functions/scripts/runtimeConfig/model.ts +++ b/functions/scripts/runtimeConfig/model.ts @@ -1,4 +1,4 @@ -import type { configVars } from '../../src/config/config' +import { configVars } from 'oa-shared/models/config' /** Variables populates in the same way firebase functions:config:set does for use in testing */ export const runtimeConfigTest: configVars = { diff --git a/functions/src/Firebase/firebaseSync.ts b/functions/src/Firebase/firebaseSync.ts index 7d95fba5ec..12d8762445 100644 --- a/functions/src/Firebase/firebaseSync.ts +++ b/functions/src/Firebase/firebaseSync.ts @@ -1,6 +1,7 @@ import rtdb from './realtimeDB' import * as firestore from './firestoreDB' -import { DBDoc, DB_ENDPOINTS } from '../models' +import { DB_ENDPOINTS } from '../models' +import { DBDoc } from 'oa-shared/models/db' /* Functions in this folder are used to sync data between firestore and firebase realtime databases The reason for this is to allow large collections to be 'cached' for cheap retrieval diff --git a/functions/src/Firebase/firestoreDB.ts b/functions/src/Firebase/firestoreDB.ts index dcd20f5b97..f8c5e0d3af 100644 --- a/functions/src/Firebase/firestoreDB.ts +++ b/functions/src/Firebase/firestoreDB.ts @@ -1,7 +1,8 @@ import { firebaseApp } from './admin' -import { DBDoc, IDBEndpoint, DB_ENDPOINTS } from '../models' +import { DB_ENDPOINTS } from '../models' import { getFirestore } from 'firebase-admin/firestore' +import { DBDoc, DBEndpoint } from 'oa-shared/models/db' // TODO - ideally should remove default export to force using functions which have mapping export const db = getFirestore(firebaseApp) @@ -17,7 +18,7 @@ export const get = (path: string) => db.doc(path) /************************************************************ * Specific firestore helpers for common ops ************************************************************/ -export const getLatestDoc = async (endpoint: IDBEndpoint, orderBy: string) => { +export const getLatestDoc = async (endpoint: DBEndpoint, orderBy: string) => { const mappedEndpoint = DB_ENDPOINTS[endpoint] const col = await db .collection(mappedEndpoint) @@ -27,7 +28,7 @@ export const getLatestDoc = async (endpoint: IDBEndpoint, orderBy: string) => { return col.docs[0] } export const getDoc = async ( - endpoint: IDBEndpoint, + endpoint: DBEndpoint, docId: string, ): Promise => { const mapping = DB_ENDPOINTS[endpoint] || endpoint @@ -40,7 +41,7 @@ export const getDoc = async ( return res.data() as T }) } -export const getCollection = async (endpoint: IDBEndpoint) => { +export const getCollection = async (endpoint: DBEndpoint) => { const mapping = DB_ENDPOINTS[endpoint] || endpoint console.log(`mapping [${endpoint}] -> [${mapping}]`) return db @@ -53,7 +54,7 @@ export const getCollection = async (endpoint: IDBEndpoint) => { }) } export const setDoc = async ( - endpoint: IDBEndpoint, + endpoint: DBEndpoint, docId: string, data: any, ) => { @@ -62,7 +63,7 @@ export const setDoc = async ( } export const updateDoc = async ( - endpoint: IDBEndpoint, + endpoint: DBEndpoint, docId: string, data: any, ) => { diff --git a/functions/src/Integrations/firebase-discord.test.ts b/functions/src/Integrations/firebase-discord.test.ts index 5655391947..cb2130a7e9 100644 --- a/functions/src/Integrations/firebase-discord.test.ts +++ b/functions/src/Integrations/firebase-discord.test.ts @@ -2,7 +2,7 @@ import { IModerationStatus, ResearchUpdateStatus } from 'oa-shared' import { handleResearchUpdatePublished } from './firebase-discord' -import type { IResearch } from '../models' +import type { IResearch } from 'oa-shared/models/research' const factoryResearch = { _id: 'id', diff --git a/functions/src/Integrations/firebase-discord.ts b/functions/src/Integrations/firebase-discord.ts index 3cb1f3fafa..d8a7f1e042 100644 --- a/functions/src/Integrations/firebase-discord.ts +++ b/functions/src/Integrations/firebase-discord.ts @@ -1,12 +1,12 @@ import axios from 'axios' import * as functions from 'firebase-functions' -import { IModerationStatus, ResearchUpdateStatus } from 'oa-shared' +import { IResearchDB, ResearchUpdateStatus } from 'oa-shared/models/research' +import { IModerationStatus } from 'oa-shared' import { CONFIG } from '../config/config' import type { AxiosError, AxiosResponse } from 'axios' -import type { IResearch } from '../../../src/models' -import type { IMapPin } from '../models' +import type { IResearch, IMapPin } from 'oa-shared/models' const SITE_URL = CONFIG.deployment.site_url // e.g. https://dev.onearmy.world or https://community.preciousplastic.com diff --git a/functions/src/Integrations/patreon.spec.ts b/functions/src/Integrations/patreon.spec.ts index eeaf84d6dc..287a2fdc40 100644 --- a/functions/src/Integrations/patreon.spec.ts +++ b/functions/src/Integrations/patreon.spec.ts @@ -1,6 +1,5 @@ -import * as functions from 'firebase-functions' import { isSupporter } from './patreon' -import { PatreonUser } from 'oa-shared' +import { PatreonUser } from 'oa-shared/models/user' jest.mock('../config/config', () => ({ CONFIG: { diff --git a/functions/src/Integrations/patreon.ts b/functions/src/Integrations/patreon.ts index dea1263497..462d3bda26 100644 --- a/functions/src/Integrations/patreon.ts +++ b/functions/src/Integrations/patreon.ts @@ -1,14 +1,14 @@ import * as functions from 'firebase-functions' +import { DB_ENDPOINTS } from 'oa-shared/models/db' import { - DB_ENDPOINTS, + IUserDB, PatreonMembershipAttributes, PatreonTierAttributes, PatreonUser, PatreonUserAttributes, -} from 'oa-shared' +} from 'oa-shared/models/user' import { db } from '../Firebase/firestoreDB' import { CONFIG } from '../config/config' -import { IUserDB } from '../models' import { MEMORY_LIMIT_512_MB } from '../consts' const PATREON_CLIENT_ID = CONFIG.integrations.patreon_client_id diff --git a/functions/src/aggregations/common.aggregations.ts b/functions/src/aggregations/common.aggregations.ts index 43103f9205..a594422726 100644 --- a/functions/src/aggregations/common.aggregations.ts +++ b/functions/src/aggregations/common.aggregations.ts @@ -1,10 +1,11 @@ import { firestore } from 'firebase-admin' import { Change, logger } from 'firebase-functions' -import { DB_ENDPOINTS, IDBEndpoint } from '../models' +import { DB_ENDPOINTS } from '../models' import { db } from '../Firebase/firestoreDB' import { compareObjectDiffs, splitArrayToChunks } from '../Utils/data.utils' import { FieldValue } from 'firebase-admin/firestore' -import { IModerationStatus } from 'oa-shared' +import { IModerationStatus } from 'oa-shared/models/moderation' +import { DBEndpoint } from 'oa-shared/models/db' type IDocumentRef = FirebaseFirestore.DocumentReference type ICollectionRef = FirebaseFirestore.CollectionReference @@ -22,7 +23,7 @@ export interface IAggregation { * */ changeType: 'updated' /** DB collection watched for changes */ - sourceCollection: IDBEndpoint + sourceCollection: DBEndpoint /** * Collection fields to trigger aggregation on update. * The first named field will be assumed required and used during initial seed query @@ -31,7 +32,7 @@ export interface IAggregation { /** function used to generate aggregation value from source data */ process: (aggregation: AggregationHandler) => Record | string[] /** Collection ID for output aggregated data */ - targetCollection: IDBEndpoint + targetCollection: DBEndpoint /** Document ID for aggregated data in target aggregation collection */ targetDocId: string } diff --git a/functions/src/aggregations/user.aggregations.ts b/functions/src/aggregations/user.aggregations.ts index 74e8163521..23492e489e 100644 --- a/functions/src/aggregations/user.aggregations.ts +++ b/functions/src/aggregations/user.aggregations.ts @@ -1,10 +1,11 @@ import * as functions from 'firebase-functions' -import { DB_ENDPOINTS, IUserDB } from '../models' +import { DB_ENDPOINTS } from '../models' import { VALUE_MODIFIERS, handleDBAggregations, IAggregation, } from './common.aggregations' +import { IUserDB } from 'oa-shared/models/user' interface IUserAggregation extends IAggregation { sourceFields: (keyof IUserDB)[] diff --git a/functions/src/aggregations/userNotifications.aggregations.spec.ts b/functions/src/aggregations/userNotifications.aggregations.spec.ts index 580a3fdb61..b397d0f91f 100644 --- a/functions/src/aggregations/userNotifications.aggregations.spec.ts +++ b/functions/src/aggregations/userNotifications.aggregations.spec.ts @@ -1,9 +1,11 @@ -import { IUserDB } from '../models' import { FirebaseEmulatedTest } from '../test/Firebase/emulator' -import type { INotification } from '../../../src/models' -import { EmailNotificationFrequency } from 'oa-shared' +import { + EmailNotificationFrequency, + INotification, +} from 'oa-shared/models/notifications' import { processNotifications } from './userNotifications.aggregations' import { FieldValue } from 'firebase-admin/firestore' +import { IUserDB } from 'oa-shared/models/user' const mockNotification: Partial = { _id: 'notification_1' } diff --git a/functions/src/aggregations/userNotifications.aggregations.ts b/functions/src/aggregations/userNotifications.aggregations.ts index c2e27bb9bf..958af4cef5 100644 --- a/functions/src/aggregations/userNotifications.aggregations.ts +++ b/functions/src/aggregations/userNotifications.aggregations.ts @@ -1,9 +1,13 @@ import { firestore } from 'firebase-admin' import * as functions from 'firebase-functions' -import { DB_ENDPOINTS, INotification, IUserDB } from '../models' -import { EmailNotificationFrequency } from 'oa-shared' +import { DB_ENDPOINTS } from '../models' +import { + EmailNotificationFrequency, + INotification, +} from 'oa-shared/models/notifications' import { FieldValue } from 'firebase-admin/firestore' import { db } from '../Firebase/firestoreDB' +import { IUserDB } from 'oa-shared/models/user' const VALUE_MODIFIERS = { delete: () => FieldValue.delete(), diff --git a/functions/src/config/config.ts b/functions/src/config/config.ts index e7ea926107..1722306ea6 100644 --- a/functions/src/config/config.ts +++ b/functions/src/config/config.ts @@ -1,4 +1,5 @@ import { config } from 'firebase-functions' +import { configVars } from 'oa-shared/models/config' /* config variables are attached directly to firebase using the cli $firebase functions:config:set ... @@ -38,46 +39,3 @@ if (c.service?.private_key) { export const CONFIG = c export const SERVICE_ACCOUNT_CONFIG = c.service export const ANALYTICS_CONFIG = c.analytics -/************** Interfaces ************** */ -interface IServiceAccount { - type: string - project_id: string - private_key_id: string - private_key: string - client_email: string - client_id: string - auth_uri: string - token_uri: string - auth_provider_x509_cert_url: string - client_x509_cert_url: string -} -interface IAnalytics { - tracking_code: string - view_id: string -} -interface IIntergrations { - slack_webhook: string - discord_webhook: string - discord_alert_channel_webhook: string - patreon_client_id: string - patreon_client_secret: string -} -interface IDeployment { - site_url: string -} - -export interface configVars { - service: IServiceAccount - analytics: IAnalytics - integrations: IIntergrations - deployment: IDeployment - prerender: { - api_key: string - } -} - -// if passing complex config variables, may want to -// encode as b64 and unencode here to avoid character escape challenges -function _b64ToString(b64str: string) { - return Buffer.from(b64str, 'base64').toString('binary') -} diff --git a/functions/src/discussionUpdates/index.test.ts b/functions/src/discussionUpdates/index.test.ts index cd2d149d75..e4bfc7e446 100644 --- a/functions/src/discussionUpdates/index.test.ts +++ b/functions/src/discussionUpdates/index.test.ts @@ -6,8 +6,7 @@ import { v4 as uuid } from 'uuid' import { DB_ENDPOINTS } from '../models' import { handleDiscussionUpdate } from './index' - -import type { IUserDB } from '../models' +import { IUserDB } from 'oa-shared/models/user' describe('discussionUpdates', () => { let db diff --git a/functions/src/discussionUpdates/index.ts b/functions/src/discussionUpdates/index.ts index e64e03680e..d0724b7e78 100644 --- a/functions/src/discussionUpdates/index.ts +++ b/functions/src/discussionUpdates/index.ts @@ -4,7 +4,7 @@ import { db } from '../Firebase/firestoreDB' import { DB_ENDPOINTS } from '../models' import type { firestore } from 'firebase-admin' -import type { IDiscussion, IUserDB } from '../models' +import type { IDiscussion, IUserDB } from 'oa-shared/models' /********************************************************************* * Side-effects to be carried out on various question updates, namely: diff --git a/functions/src/emailNotifications/createModerationEmails.spec.ts b/functions/src/emailNotifications/createModerationEmails.spec.ts index 624efda29b..a73fb5fc4f 100644 --- a/functions/src/emailNotifications/createModerationEmails.spec.ts +++ b/functions/src/emailNotifications/createModerationEmails.spec.ts @@ -1,5 +1,5 @@ import { FirebaseEmulatedTest } from '../test/Firebase/emulator' -import { DB_ENDPOINTS, IUserDB } from '../models' +import { DB_ENDPOINTS } from '../models' import { HOW_TO_APPROVAL_SUBJECT, HOW_TO_NEEDS_IMPROVEMENTS_SUBJECT, @@ -17,6 +17,7 @@ import { handleModerationUpdate, } from './createModerationEmails' import { PP_SIGNOFF } from './constants' +import { IUserDB } from 'oa-shared/models/user' import { IModerationStatus } from 'oa-shared' jest.mock('../Firebase/auth', () => ({ diff --git a/functions/src/emailNotifications/createModerationEmails.ts b/functions/src/emailNotifications/createModerationEmails.ts index 3e3916fc8e..0f45f5bf93 100644 --- a/functions/src/emailNotifications/createModerationEmails.ts +++ b/functions/src/emailNotifications/createModerationEmails.ts @@ -1,7 +1,6 @@ import * as functions from 'firebase-functions' import { QueryDocumentSnapshot } from 'firebase-admin/firestore' -import { IHowtoDB, IMapPin, IModerable } from '../../../src/models' -import { IModerationStatus } from 'oa-shared' +import { IModerationStatus, IModerable } from 'oa-shared' import { db } from '../Firebase/firestoreDB' import { DB_ENDPOINTS } from '../models' import * as templates from './templateHelpers' @@ -9,6 +8,8 @@ import { getUserAndEmail } from './utils' import { Change } from 'firebase-functions/v1' import { withErrorAlerting } from '../alerting/errorAlerting' import { MEMORY_LIMIT_512_MB } from '../consts' +import { IHowtoDB } from 'oa-shared/models/howto' +import { IMapPin } from 'oa-shared/models/maps' export async function handleModerationUpdate( change: Change>, diff --git a/functions/src/emailNotifications/createNotificationEmails.spec.ts b/functions/src/emailNotifications/createNotificationEmails.spec.ts index 1dddabfc2c..76f5635815 100644 --- a/functions/src/emailNotifications/createNotificationEmails.spec.ts +++ b/functions/src/emailNotifications/createNotificationEmails.spec.ts @@ -1,10 +1,12 @@ -import { EmailNotificationFrequency } from 'oa-shared' +import { + EmailNotificationFrequency, + INotification, +} from 'oa-shared/models/notifications' import { DB_ENDPOINTS } from '../models' import { FirebaseEmulatedTest } from '../test/Firebase/emulator' import { createNotificationEmails } from './createNotificationEmails' - -import type { INotification, IUserDB } from '../models' +import { IUserDB } from 'oa-shared/models/user' jest.mock('../Firebase/auth', () => ({ firebaseAuth: { diff --git a/functions/src/emailNotifications/createNotificationEmails.ts b/functions/src/emailNotifications/createNotificationEmails.ts index a0fc407b9c..b233b32321 100644 --- a/functions/src/emailNotifications/createNotificationEmails.ts +++ b/functions/src/emailNotifications/createNotificationEmails.ts @@ -1,10 +1,14 @@ -import { EmailNotificationFrequency } from 'oa-shared' -import { INotification } from '../../../src/models' +import { + EmailNotificationFrequency, + INotification, + IPendingEmails, +} from 'oa-shared/models/notifications' import { db } from '../Firebase/firestoreDB' -import { DB_ENDPOINTS, IUserDB, IPendingEmails } from '../models' +import { DB_ENDPOINTS } from '../models' import { getNotificationEmail } from './templateHelpers' import { getUserEmail } from './utils' import { v4 as uuid } from 'uuid' +import { IUserDB } from 'oa-shared/models/user' const updateEmailedNotifications = async ( user: FirebaseFirestore.DocumentSnapshot, diff --git a/functions/src/emailNotifications/createSubmissionEmails.spec.ts b/functions/src/emailNotifications/createSubmissionEmails.spec.ts index 5b02efdd21..4d6017089b 100644 --- a/functions/src/emailNotifications/createSubmissionEmails.spec.ts +++ b/functions/src/emailNotifications/createSubmissionEmails.spec.ts @@ -1,5 +1,5 @@ import { IModerationStatus } from 'oa-shared' -import { UserRole } from 'oa-shared/models' +import { IUserDB, UserRole } from 'oa-shared/models/user' import { getMockHowto } from '../emulator/seed/content-generate' import { DB_ENDPOINTS } from '../models' @@ -15,8 +15,8 @@ import { MAP_PIN_SUBMISSION_SUBJECT, } from './templateHelpers' import * as utils from './utils' - -import type { IMapPin, IMessageDB, IUserDB } from '../models' +import { IMapPin } from 'oa-shared/models/maps' +import { IMessageDB } from 'oa-shared/models/messages' jest.mock('../Firebase/auth', () => ({ firebaseAuth: { diff --git a/functions/src/emailNotifications/createSubmissionEmails.ts b/functions/src/emailNotifications/createSubmissionEmails.ts index 7fcc79547d..4bd0924f06 100644 --- a/functions/src/emailNotifications/createSubmissionEmails.ts +++ b/functions/src/emailNotifications/createSubmissionEmails.ts @@ -11,8 +11,9 @@ import { getSenderMessageEmail, } from './templateHelpers' import { getUserAndEmail, isValidMessageRequest } from './utils' - -import type { IHowtoDB, IMapPin, IMessageDB } from '../../../src/models' +import { IMessageDB } from 'oa-shared/models/messages' +import { IHowtoDB } from 'oa-shared/models/howto' +import { IMapPin } from 'oa-shared/models/maps' export async function createMessageEmails(message: IMessageDB) { const isValid = await isValidMessageRequest(message) diff --git a/functions/src/emailNotifications/index.ts b/functions/src/emailNotifications/index.ts index 3d7f0ef751..e03437ba86 100644 --- a/functions/src/emailNotifications/index.ts +++ b/functions/src/emailNotifications/index.ts @@ -1,15 +1,15 @@ import * as functions from 'firebase-functions' import { createNotificationEmails } from './createNotificationEmails' import { db } from '../Firebase/firestoreDB' -import { DB_ENDPOINTS, IUserDB } from '../models' -import { EmailNotificationFrequency } from 'oa-shared' +import { DB_ENDPOINTS } from '../models' +import { EmailNotificationFrequency } from 'oa-shared/models/notifications' import { withErrorAlerting } from '../alerting/errorAlerting' import * as moderationEmails from './createModerationEmails' import * as submissionEmails from './createSubmissionEmails' import * as supporterBadgeEmails from './supporterBadgeEmails' import * as verifiedBadgeEmails from './verifiedBadgeEmails' import { MEMORY_LIMIT_512_MB } from '../consts' -import { UserRole } from 'oa-shared/models' +import { IUserDB, UserRole } from 'oa-shared/models/user' exports.sendDaily = functions .runWith({ memory: MEMORY_LIMIT_512_MB }) diff --git a/functions/src/emailNotifications/supporterBadgeEmails.spec.ts b/functions/src/emailNotifications/supporterBadgeEmails.spec.ts index 0716e76aad..c189f2a40b 100644 --- a/functions/src/emailNotifications/supporterBadgeEmails.spec.ts +++ b/functions/src/emailNotifications/supporterBadgeEmails.spec.ts @@ -1,5 +1,5 @@ import { FirebaseEmulatedTest } from '../test/Firebase/emulator' -import { DB_ENDPOINTS } from 'oa-shared' +import { DB_ENDPOINTS } from 'oa-shared/models/db' const fbTest = require('firebase-functions-test')() const fun = require('./supporterBadgeEmails') diff --git a/functions/src/emailNotifications/supporterBadgeEmails.ts b/functions/src/emailNotifications/supporterBadgeEmails.ts index db324e6774..75183629f6 100644 --- a/functions/src/emailNotifications/supporterBadgeEmails.ts +++ b/functions/src/emailNotifications/supporterBadgeEmails.ts @@ -1,14 +1,14 @@ import * as functions from 'firebase-functions' import { db } from '../Firebase/firestoreDB' import { getUserEmail } from './utils' -import { DB_ENDPOINTS } from 'oa-shared' +import { DB_ENDPOINTS } from 'oa-shared/models/db' import { getUserSupporterBadgeAddedEmail, getUserSupporterBadgeRemovedEmail, } from './templateHelpers' -import type { IUserDB } from '../models' import { withErrorAlerting } from '../alerting/errorAlerting' import type { QueryDocumentSnapshot } from 'firebase-admin/firestore' +import { IUserDB } from 'oa-shared/models/user' exports.handleUserSupporterBadgeUpdate = functions .runWith({ memory: '512MB' }) diff --git a/functions/src/emailNotifications/templateHelpers.ts b/functions/src/emailNotifications/templateHelpers.ts index ddf1389124..d5976cd456 100644 --- a/functions/src/emailNotifications/templateHelpers.ts +++ b/functions/src/emailNotifications/templateHelpers.ts @@ -1,10 +1,3 @@ -import { - IHowtoDB, - IMapPin, - IMessageDB, - INotification, - IUserDB, -} from '../../../src/models' import { NOTIFICATION_LIST_IMAGE } from './constants' import { getProjectImageSrc, @@ -14,6 +7,11 @@ import { getProjectSignoff, } from './utils' import { getEmailHtml } from './templates/index' +import { IUserDB } from 'oa-shared/models/user' +import { INotification } from 'oa-shared/models/notifications' +import { IHowtoDB } from 'oa-shared/models/howto' +import { IMapPin } from 'oa-shared/models/maps' +import { IMessageDB } from 'oa-shared/models/messages' export interface Email { html: string diff --git a/functions/src/emailNotifications/utils.test.ts b/functions/src/emailNotifications/utils.test.ts index a7e4c51009..d8eff5f91e 100644 --- a/functions/src/emailNotifications/utils.test.ts +++ b/functions/src/emailNotifications/utils.test.ts @@ -1,9 +1,6 @@ import * as utils from './utils' -import { db } from '../Firebase/firestoreDB' import { firebaseAuth } from '../Firebase/auth' -import { FirebaseEmulatedTest } from '../test/Firebase/emulator' -import { IMessageDB } from '../models' import { errors, isBelowMessageLimit, @@ -13,8 +10,9 @@ import { isValidMessageRequest, } from './utils' -import type { IUserDB } from '../models' import type { UserRecord } from 'firebase-admin/auth' +import { IUserDB } from 'oa-shared/models/user' +import { IMessageDB } from 'oa-shared/models/messages' const messageDocs = [] let isBlockedFromMessaging = false diff --git a/functions/src/emailNotifications/utils.ts b/functions/src/emailNotifications/utils.ts index 278cbf9b72..c634ac7273 100644 --- a/functions/src/emailNotifications/utils.ts +++ b/functions/src/emailNotifications/utils.ts @@ -1,3 +1,4 @@ +import { IUserDB } from 'oa-shared/models/user' import { CONFIG } from '../config/config' import { firebaseAuth } from '../Firebase/auth' import { db } from '../Firebase/firestoreDB' @@ -11,8 +12,11 @@ import { PP_SIGNOFF, } from './constants' -import type { NotificationType } from 'oa-shared' -import type { IMessageDB, INotification, IUserDB } from '../models' +import type { + INotification, + NotificationType, +} from 'oa-shared/models/notifications' +import { IMessageDB } from 'oa-shared/models/messages' export const errors = { MESSAGE_LIMIT: diff --git a/functions/src/emailNotifications/verifiedBadgeEmails.spec.ts b/functions/src/emailNotifications/verifiedBadgeEmails.spec.ts index 5f86dd3734..04b0e06d09 100644 --- a/functions/src/emailNotifications/verifiedBadgeEmails.spec.ts +++ b/functions/src/emailNotifications/verifiedBadgeEmails.spec.ts @@ -1,5 +1,5 @@ import { FirebaseEmulatedTest } from '../test/Firebase/emulator' -import { DB_ENDPOINTS } from 'oa-shared' +import { DB_ENDPOINTS } from 'oa-shared/models/db' const fbTest = require('firebase-functions-test')() const fun = require('./verifiedBadgeEmails') diff --git a/functions/src/emailNotifications/verifiedBadgeEmails.ts b/functions/src/emailNotifications/verifiedBadgeEmails.ts index ddd2a2b842..f79023e3a4 100644 --- a/functions/src/emailNotifications/verifiedBadgeEmails.ts +++ b/functions/src/emailNotifications/verifiedBadgeEmails.ts @@ -1,11 +1,11 @@ import * as functions from 'firebase-functions' import { db } from '../Firebase/firestoreDB' import { getUserEmail } from './utils' -import { DB_ENDPOINTS } from 'oa-shared' +import { DB_ENDPOINTS } from 'oa-shared/models/db' import { getUserVerifiedBadgeAddedEmail } from './templateHelpers' -import type { IUserDB } from '../models' import { withErrorAlerting } from '../alerting/errorAlerting' import type { QueryDocumentSnapshot } from 'firebase-admin/firestore' +import { IUserDB } from 'oa-shared/models/user' export const handleUserVerifiedBadgeUpdate = functions .runWith({ memory: '512MB' }) diff --git a/functions/src/emulator/seed/content-generate.ts b/functions/src/emulator/seed/content-generate.ts index 7e4dc14a41..fcc4b157fb 100644 --- a/functions/src/emulator/seed/content-generate.ts +++ b/functions/src/emulator/seed/content-generate.ts @@ -1,10 +1,11 @@ -import { DifficultyLevel, IModerationStatus } from 'oa-shared' import { MOCK_AUTH_USERS } from 'oa-shared/mocks/auth' import { setDoc, updateDoc } from '../../Firebase/firestoreDB' import type { IMockAuthUser } from 'oa-shared/mocks/auth' -import type { IHowtoDB, IUserDB } from '../../models' +import { DifficultyLevel, IHowtoDB } from 'oa-shared/models/howto' +import { IModerationStatus } from 'oa-shared' +import { IUserDB } from 'oa-shared/models/user' /** * Populate additional mock howtos alongside production data for ease of testing diff --git a/functions/src/emulator/seed/users-create.ts b/functions/src/emulator/seed/users-create.ts index 1a5308495d..0a963dc0ed 100644 --- a/functions/src/emulator/seed/users-create.ts +++ b/functions/src/emulator/seed/users-create.ts @@ -3,7 +3,7 @@ import { MOCK_AUTH_USERS } from 'oa-shared/mocks/auth' import { firebaseAuth } from '../../Firebase/auth' import { setDoc } from '../../Firebase/firestoreDB' -import type { IUserDB } from '../../models' +import type { IUserDB } from 'oa-shared/models' /** * Create auth users to allow sign-in on firebase emulators diff --git a/functions/src/messages/messages.spec.ts b/functions/src/messages/messages.spec.ts index 0fb4e8d421..fea9595b03 100644 --- a/functions/src/messages/messages.spec.ts +++ b/functions/src/messages/messages.spec.ts @@ -1,5 +1,5 @@ import { handleSendMessage } from './messages' // Path to your cloud function file -import { SendMessage } from 'oa-shared' +import { SendMessage } from 'oa-shared/models/messages' import * as functions from 'firebase-functions' const defaultData: SendMessage = { diff --git a/functions/src/messages/messages.ts b/functions/src/messages/messages.ts index d23a96402c..e05f9c0ae3 100644 --- a/functions/src/messages/messages.ts +++ b/functions/src/messages/messages.ts @@ -3,7 +3,7 @@ import { DB_ENDPOINTS } from '../models' import { firebaseAdmin } from '../Firebase/admin' import { createDoc } from '../Utils/doc.utils' -import type { SendMessage } from 'oa-shared' +import type { SendMessage } from 'oa-shared/models/messages' const EMAIL_ADDRESS_SEND_LIMIT = 100 diff --git a/functions/src/models.ts b/functions/src/models.ts index 29a63b01f0..4b277b1a0c 100644 --- a/functions/src/models.ts +++ b/functions/src/models.ts @@ -6,24 +6,11 @@ import type * as functions from 'firebase-functions' // Importing from outside the src code is still fine because we make single builds with webpack // which can resolve at build time, but would not work if deploying direct to firebase functions. // Alternative fix would be to put the platform code one level further nested e.g. /platform/src -export type { - IDiscussion, - IHowtoDB, - IMapPin, - IMessageDB, - IModerable, - INotification, - IPendingEmails, - IResearchDB, - IResearch, - IUserDB, - IQuestionDB, -} from '../../src/models' -export type { IDBEndpoint } from '../../src/models/dbEndpoints' -export type { DBDoc } from '../../src/models/dbDoc.model' - -import { dbEndpointSubcollections, generateDBEndpoints } from 'oa-shared' +import { + dbEndpointSubcollections, + generateDBEndpoints, +} from 'oa-shared/models/db' export const DB_ENDPOINTS = generateDBEndpoints() export const DB_ENDPOINT_SUBCOLLECTIONS = dbEndpointSubcollections diff --git a/functions/src/questionUpdates/index.test.ts b/functions/src/questionUpdates/index.test.ts index cb0f7bb960..0957b7f699 100644 --- a/functions/src/questionUpdates/index.test.ts +++ b/functions/src/questionUpdates/index.test.ts @@ -10,8 +10,7 @@ import { handleQuestionUpdate, handleQuestionDelete, } from './index' - -import type { IUserDB } from '../models' +import { IUserDB } from 'oa-shared/models/user' describe('questionUpdates', () => { let db diff --git a/functions/src/questionUpdates/index.ts b/functions/src/questionUpdates/index.ts index 8bc2441e13..86b078c278 100644 --- a/functions/src/questionUpdates/index.ts +++ b/functions/src/questionUpdates/index.ts @@ -4,7 +4,7 @@ import { db } from '../Firebase/firestoreDB' import { DB_ENDPOINTS } from '../models' import type { firestore } from 'firebase-admin' -import type { IQuestionDB, IUserDB } from '../models' +import type { IQuestionDB, IUserDB } from 'oa-shared/models' /********************************************************************* * Side-effects to be carried out on various question updates, namely: diff --git a/functions/src/seo/sitemap/sitemapGenerate.ts b/functions/src/seo/sitemap/sitemapGenerate.ts index 8968417e85..db4fb86b92 100644 --- a/functions/src/seo/sitemap/sitemapGenerate.ts +++ b/functions/src/seo/sitemap/sitemapGenerate.ts @@ -7,8 +7,8 @@ import { Readable } from 'stream' import { CONFIG } from '../../config/config' import { uploadLocalFileToStorage } from '../../Firebase/storage' import { getCollection } from '../../Firebase/firestoreDB' -import { IDBEndpoint } from '../../models' import axios from 'axios' +import { DBEndpoint } from 'oa-shared/models/db' import { IModerationStatus } from 'oa-shared' /************************************************************************* @@ -46,7 +46,7 @@ export async function generateSitemap() { async function generateSitemapItems() { const items: SitemapItem[] = [] for (const [dbEndpoint, generator] of Object.entries(sitemapItemGenerators)) { - const docs = await getCollection(dbEndpoint as IDBEndpoint) + const docs = await getCollection(dbEndpoint as DBEndpoint) const { docFilterFn, slugField, lastModField } = endpointDbDefaults const filtered = docs.filter((doc) => docFilterFn(doc)) filtered.forEach((doc) => { @@ -136,7 +136,7 @@ const endpointItemDefaults = { * For more info about sitemaps see https://www.sitemaps.org/protocol.html **/ const sitemapItemGenerators: { - [endpoint in IDBEndpoint]?: (slug: string) => SitemapItem + [endpoint in DBEndpoint]?: (slug: string) => SitemapItem } = { howtos: (slug: string) => ({ ...endpointItemDefaults, diff --git a/functions/src/test/Firebase/emulator.ts b/functions/src/test/Firebase/emulator.ts index e477615f65..a9f52f0e02 100644 --- a/functions/src/test/Firebase/emulator.ts +++ b/functions/src/test/Firebase/emulator.ts @@ -4,7 +4,7 @@ import http from 'http' import { firebaseAdmin, firebaseApp } from '../../Firebase/admin' import type { CallableContextOptions } from 'firebase-functions-test/lib/v1' import type { FeaturesList } from 'firebase-functions-test/lib/features' -import { DB_ENDPOINTS, IDBEndpoint } from '../../models' +import { DB_ENDPOINTS, DBEndpoint } from 'oa-shared/models/db' /** * Utility class for executing a firebase function with a user-provided context. @@ -61,7 +61,7 @@ class FirebaseEmulatedTestClass { public mockFirestoreChangeObject( beforeData: Record, afterData: Record, - collection: IDBEndpoint, + collection: DBEndpoint, docId = 'doc_1', ) { const docPath = `${DB_ENDPOINTS[collection] || collection}/${docId}` @@ -84,7 +84,7 @@ class FirebaseEmulatedTestClass { * required for emulators to not throw `toQualifiedResourcePath` error for uninitialised endpoint */ public async seedFirestoreDB( - endpoint: IDBEndpoint, + endpoint: DBEndpoint, docs: T[] = [], ) { const db = this.admin.firestore() diff --git a/functions/src/userUpdates/backupUser.ts b/functions/src/userUpdates/backupUser.ts index fb082b0576..d438da0396 100644 --- a/functions/src/userUpdates/backupUser.ts +++ b/functions/src/userUpdates/backupUser.ts @@ -1,4 +1,5 @@ -import { DBDoc, IDBDocChange } from '../models' +import { DBDoc } from 'oa-shared/models/db' +import { IDBDocChange } from '../models' /** Helper function to check if the only field changed is lastActive * (updates on login), in which case we will not want diff --git a/functions/src/userUpdates/index.ts b/functions/src/userUpdates/index.ts index 36aef48c8f..d69b819e85 100644 --- a/functions/src/userUpdates/index.ts +++ b/functions/src/userUpdates/index.ts @@ -6,7 +6,8 @@ import { backupUser } from './backupUser' import { updateDiscussionComments } from './updateDiscussionComments' import { updateMapPins } from './updateMapPins' -import type { IDBDocChange, IUserDB } from '../models' +import type { IDBDocChange } from '../models' +import { IUserDB } from 'oa-shared/models/user' /********************************************************************* * Side-effects to be carried out on various user updates, namely: diff --git a/functions/src/userUpdates/updateDiscussionComments.test.ts b/functions/src/userUpdates/updateDiscussionComments.test.ts index 768c8b6d85..f676726fb9 100644 --- a/functions/src/userUpdates/updateDiscussionComments.test.ts +++ b/functions/src/userUpdates/updateDiscussionComments.test.ts @@ -1,7 +1,6 @@ +import { IUserDB } from 'oa-shared/models/user' import { updateDiscussionComments } from './updateDiscussionComments' -import type { IUserDB } from '../models' - const prevUser = { _id: 'hjg235z', location: { countryCode: 'UK' }, diff --git a/functions/src/userUpdates/updateDiscussionComments.ts b/functions/src/userUpdates/updateDiscussionComments.ts index 9c8df29138..bd47358740 100644 --- a/functions/src/userUpdates/updateDiscussionComments.ts +++ b/functions/src/userUpdates/updateDiscussionComments.ts @@ -1,9 +1,9 @@ -import { DB_ENDPOINTS } from 'oa-shared' +import { DB_ENDPOINTS } from 'oa-shared/models/db' import { db } from '../Firebase/firestoreDB' import { getCreatorImage, hasDetailsForCommentsChanged } from './utils' -import type { IDiscussion, IUserDB } from '../models' +import type { IDiscussion, IUserDB } from 'oa-shared/models' export const updateDiscussionComments = async ( prevUser: IUserDB, diff --git a/functions/src/userUpdates/updateMapPins.ts b/functions/src/userUpdates/updateMapPins.ts index 15ff04cdc5..1c83bbbded 100644 --- a/functions/src/userUpdates/updateMapPins.ts +++ b/functions/src/userUpdates/updateMapPins.ts @@ -7,7 +7,7 @@ import { hasDetailsForMapPinChanged, } from './utils' -import type { IUserDB } from '../models' +import type { IUserDB } from 'oa-shared/models/user' export const updateMapPins = async (prevUser: IUserDB, user: IUserDB) => { if (!hasDetailsForMapPinChanged(prevUser, user)) { diff --git a/functions/src/userUpdates/utils.test.ts b/functions/src/userUpdates/utils.test.ts index 6739fcb3f1..98755ad987 100644 --- a/functions/src/userUpdates/utils.test.ts +++ b/functions/src/userUpdates/utils.test.ts @@ -1,3 +1,4 @@ +import { IUserDB } from 'oa-shared/models/user' import { hasDetailsChanged, hasDetailsForCommentsChanged, @@ -6,8 +7,6 @@ import { hasUserImageChanged, } from './utils' -import type { IUserDB } from '../../../src/models' - describe('hasDetailsChanged', () => { it("returns false for every field that's the same", () => { const user = { diff --git a/functions/src/userUpdates/utils.ts b/functions/src/userUpdates/utils.ts index 91ba552cf6..cc5b34fd03 100644 --- a/functions/src/userUpdates/utils.ts +++ b/functions/src/userUpdates/utils.ts @@ -1,6 +1,6 @@ import { valuesAreDeepEqual } from '../Utils' -import type { IUserDB } from '../models' +import type { IUserDB } from 'oa-shared/models/user' export const hasDetailsChanged = ( prevUser: IUserDB, diff --git a/functions/webpack.config.ts b/functions/webpack.config.ts index 8e4e221812..310017f4ce 100644 --- a/functions/webpack.config.ts +++ b/functions/webpack.config.ts @@ -62,7 +62,7 @@ const config: webpack.Configuration = { }, // copy src index.html to be served during seoRender function { - from: path.resolve(__dirname, '../build/index.html'), + from: path.resolve(__dirname, '../index.html'), to: 'index.html', }, { diff --git a/index.html b/index.html index 00ca487d54..256e031e63 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + @@ -20,7 +20,7 @@ content="A series of tools for the Precious Plastic community to collaborate around the world. Connect, share and meet each other to tackle plastic waste." /> - + - diff --git a/package.json b/package.json index e006ca20e0..bd277337e2 100644 --- a/package.json +++ b/package.json @@ -18,27 +18,25 @@ "start-ci": "concurrently --kill-others --names themes,components,platform --prefix-colors cyan,blue,magenta \"yarn start:themes\" \"yarn start:components\" \"yarn start:platform-ci\"", "start:themes": "yarn workspace oa-themes dev", "start:components": "yarn workspace oa-components dev", - "start:platform": "yarn build:shared && vite", - "start:platform:for-emulated-backend": "yarn build:shared && vite --port 4000", - "start:platform-ci": "yarn build:shared && vite --port 3456", "start:shared": "yarn workspace oa-shared dev", + "start:platform": "yarn build:shared && node ./server.js", + "start:platform:for-emulated-backend": "yarn build:shared && vite --port 4000", + "start:platform-ci": "yarn build:shared && cross-env NODE_ENV=production node ./server.js", "frontend:for-emulated-backend:watch": "concurrently --kill-others --names themes,components,platform --prefix-colors yellow,cyan,blue,magenta \"yarn start:themes\" \"yarn start:components\" \"yarn start:platform:for-emulated-backend\"", "backend:emulator:watch": "docker-compose up --force-recreate --build", "build:themes": "yarn workspace oa-themes build", "build:components": "yarn workspace oa-components build", - "build:vite": "tsc && vite build", - "build:post": "yarn workspace oa-scripts post-build", - "build:inject-config": "yarn build:post", + "build:vite": "tsc && remix vite:build", "build:shared": "yarn workspace oa-shared build", "build": "yarn build:shared && yarn build:themes && yarn build:components && yarn build:vite", "lint": "yarn lint:style && yarn lint:code", "lint:commits": " npx commitlint --from=$(git merge-base master HEAD) --verbose", "lint:code": "eslint . --ext .js,.jsx,.ts,.tsx src --color", "lint:spell": "cspell \"**/*.md\" --config ./.cspell.json", - "lint:style": "prettier --check '**/*.{md,json,js,tsx,ts}'", + "lint:style": "prettier --check '**/*.{json,js,tsx,ts}'", "format": "yarn format:code && yarn format:style", "format:code": "eslint . --ext .js,.jsx,.ts,.tsx src --color --fix", - "format:style": "prettier --write '**/*.{md,json,js,tsx,ts}'", + "format:style": "prettier --write '**/*.{json,js,tsx,ts}'", "serve": "npx serve -s build", "test": "yarn workspace oa-cypress start", "test:components": "yarn workspace oa-components test", @@ -77,8 +75,13 @@ }, "dependencies": { "@emotion/react": "^11.11.4", + "@emotion/server": "^11.11.0", "@emotion/styled": "^11.11.5", + "@remix-run/express": "^2.11.1", + "@remix-run/node": "^2.11.1", + "@remix-run/react": "^2.11.1", "@sentry/react": "7.56.0", + "@sentry/remix": "^8.26.0", "@uppy/compressor": "^1.1.4", "@uppy/core": "^3.11.3", "@uppy/dashboard": "^3.8.3", @@ -86,6 +89,7 @@ "@uppy/file-input": "^3.1.2", "@uppy/progress-bar": "^3.1.1", "@uppy/react": "^3.3.1", + "compression": "^1.7.4", "compressorjs": "^1.2.1", "countries-list": "^2.6.1", "date-fns": "^3.3.0", @@ -99,6 +103,8 @@ "framer-motion": "^11.2.10", "fs-extra": "^10.0.0", "fuse.js": "^6.4.6", + "helmet": "^7.1.0", + "isbot": "^5.1.13", "leaflet": "^1.5.1", "leaflet.markercluster": "^1.4.1", "mobx": "6.9.0", @@ -118,7 +124,8 @@ "react-leaflet": "^2.5.0", "react-leaflet-markercluster": "^2.0.0-rc3", "react-router": "^6.24.1", - "react-router-dom": "^6.24.1", + "react-router-dom": "^6.26.0", + "remix-utils": "^7.6.0", "rxjs": "^7.8.1", "theme-ui": "^0.16.2", "tslog": "^4.9.2", @@ -131,6 +138,8 @@ "@emotion/babel-plugin": "^11.11.0", "@esbuild-plugins/node-globals-polyfill": "^0.2.3", "@faker-js/faker": "^8.4.1", + "@remix-run/dev": "^2.10.3", + "@remix-run/testing": "^2.11.0", "@semantic-release/changelog": "^6.0.3", "@semantic-release/git": "^10.0.1", "@testing-library/jest-dom": "^6.4.6", diff --git a/packages/components/.storybook/preview.tsx b/packages/components/.storybook/preview.tsx index 7844c708d9..e079bac9db 100644 --- a/packages/components/.storybook/preview.tsx +++ b/packages/components/.storybook/preview.tsx @@ -10,13 +10,7 @@ import { projectKampTheme, fixingFashionTheme, } from 'oa-themes' - -import { - Route, - RouterProvider, - createMemoryRouter, - createRoutesFromElements, -} from 'react-router-dom' +import { createRemixStub } from '@remix-run/testing' const themes = { pp: preciousPlasticTheme.styles, @@ -56,17 +50,21 @@ const preview: Preview = { }, decorators: [ (Story, context) => { - const router = createMemoryRouter( - createRoutesFromElements(}>), - ) - return ( - <> - - - - - - ) + const RemixStub = createRemixStub([ + { + path: '/', + Component: () => ( + <> + + + + + + ), + }, + ]) + + return }, ], } diff --git a/packages/components/package.json b/packages/components/package.json index 7b4842d740..ef97f8565c 100644 --- a/packages/components/package.json +++ b/packages/components/package.json @@ -24,6 +24,8 @@ "@faker-js/faker": "^7.6.0", "@mui/base": "^5.0.0-beta.18", "@react-icons/all-files": "^4.1.0", + "@remix-run/node": "^2.11.1", + "@remix-run/react": "^2.11.1", "chromatic": "^11.4.0", "date-fns": "^2.29.3", "linkify-plugin-mention": "^4.0.2", @@ -38,7 +40,7 @@ "react-portal": "^4.2.2", "react-responsive-masonry": "2.1.7", "react-router": "6.24.1", - "react-router-dom": "^6.24.1", + "react-router-dom": "^6.26.0", "react-select": "^5.4.0", "react-tooltip": "^4.2.21", "storybook": "^7.6.0", @@ -53,6 +55,8 @@ }, "devDependencies": { "@babel/core": "^7.14.3", + "@remix-run/dev": "^2.11.1", + "@remix-run/testing": "^2.11.1", "@storybook/addon-actions": "^7.4.1", "@storybook/addon-essentials": "^7.4.1", "@storybook/addon-links": "^7.4.1", diff --git a/packages/components/src/Breadcrumbs/BreadcrumbsItem.tsx b/packages/components/src/Breadcrumbs/BreadcrumbsItem.tsx index 69a61f225c..7455679447 100644 --- a/packages/components/src/Breadcrumbs/BreadcrumbsItem.tsx +++ b/packages/components/src/Breadcrumbs/BreadcrumbsItem.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router-dom' +import { Link } from '@remix-run/react' import { Box, Text } from 'theme-ui' import { Button } from '../Button/Button' diff --git a/packages/components/src/CreateComment/CreateComment.tsx b/packages/components/src/CreateComment/CreateComment.tsx index 65feb50749..790b74e5e3 100644 --- a/packages/components/src/CreateComment/CreateComment.tsx +++ b/packages/components/src/CreateComment/CreateComment.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router-dom' +import { Link } from '@remix-run/react' import { Box, Button, Flex, Text, Textarea } from 'theme-ui' import { MemberBadge } from '../MemberBadge/MemberBadge' diff --git a/packages/components/src/FollowButton/FollowButton.tsx b/packages/components/src/FollowButton/FollowButton.tsx index e52f9cd153..5540e755f3 100644 --- a/packages/components/src/FollowButton/FollowButton.tsx +++ b/packages/components/src/FollowButton/FollowButton.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@remix-run/react' import { Button } from '../Button/Button' import { Tooltip } from '../Tooltip/Tooltip' diff --git a/packages/components/src/InternalLink/InternalLink.tsx b/packages/components/src/InternalLink/InternalLink.tsx index b90eb9ad60..d2dd6c660a 100644 --- a/packages/components/src/InternalLink/InternalLink.tsx +++ b/packages/components/src/InternalLink/InternalLink.tsx @@ -1,9 +1,9 @@ /* eslint-disable no-restricted-imports */ import { forwardRef } from 'react' -import { Link as RouterLink } from 'react-router-dom' +import { Link as RouterLink } from '@remix-run/react' import { Link } from 'theme-ui' -import type { LinkProps as RouterLinkProps } from 'react-router-dom' +import type { LinkProps as RouterLinkProps } from '@remix-run/react' import type { LinkProps as ThemedUILinkProps } from 'theme-ui' export type Props = RouterLinkProps & ThemedUILinkProps diff --git a/packages/components/src/Map/Map.tsx b/packages/components/src/Map/Map.client.tsx similarity index 100% rename from packages/components/src/Map/Map.tsx rename to packages/components/src/Map/Map.client.tsx diff --git a/packages/components/src/Map/Map.stories.tsx b/packages/components/src/Map/Map.stories.tsx index f75428f78a..9e7dd3090d 100644 --- a/packages/components/src/Map/Map.stories.tsx +++ b/packages/components/src/Map/Map.stories.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' -import { Map } from './Map' +import { Map } from './Map.client' import type { Meta, StoryFn } from '@storybook/react' diff --git a/packages/components/src/MapWithPin/MapPin.tsx b/packages/components/src/MapWithPin/MapPin.client.tsx similarity index 100% rename from packages/components/src/MapWithPin/MapPin.tsx rename to packages/components/src/MapWithPin/MapPin.client.tsx diff --git a/packages/components/src/MapWithPin/MapPin.stories.tsx b/packages/components/src/MapWithPin/MapPin.stories.tsx index f9bcd27225..b2857d3af6 100644 --- a/packages/components/src/MapWithPin/MapPin.stories.tsx +++ b/packages/components/src/MapWithPin/MapPin.stories.tsx @@ -1,4 +1,4 @@ -import { MapPin } from './MapPin' +import { MapPin } from './MapPin.client' import type { Meta, StoryFn } from '@storybook/react' diff --git a/packages/components/src/MapWithPin/MapWithPin.tsx b/packages/components/src/MapWithPin/MapWithPin.client.tsx similarity index 97% rename from packages/components/src/MapWithPin/MapWithPin.tsx rename to packages/components/src/MapWithPin/MapWithPin.client.tsx index 3d8b9cbe67..b7973fad0c 100644 --- a/packages/components/src/MapWithPin/MapWithPin.tsx +++ b/packages/components/src/MapWithPin/MapWithPin.client.tsx @@ -3,9 +3,9 @@ import { ZoomControl } from 'react-leaflet' import { Alert, Box, Flex, Text } from 'theme-ui' import { Button } from '../Button/Button' -import { Map } from '../Map/Map' +import { Map } from '../Map/Map.client' import { OsmGeocoding } from '../OsmGeocoding/OsmGeocoding' -import { MapPin } from './MapPin' +import { MapPin } from './MapPin.client' import type { LeafletMouseEvent } from 'leaflet' import type { Result } from '../OsmGeocoding/types' diff --git a/packages/components/src/MapWithPin/MapWithPin.stories.tsx b/packages/components/src/MapWithPin/MapWithPin.stories.tsx index 7b9273c667..f2376c5512 100644 --- a/packages/components/src/MapWithPin/MapWithPin.stories.tsx +++ b/packages/components/src/MapWithPin/MapWithPin.stories.tsx @@ -1,4 +1,4 @@ -import { MapWithPin } from './MapWithPin' +import { MapWithPin } from './MapWithPin.client' import type { Meta, StoryFn } from '@storybook/react' diff --git a/packages/components/src/ResearchEditorOverview/__snapshots__/ResearchEditorOverview.test.tsx.snap b/packages/components/src/ResearchEditorOverview/__snapshots__/ResearchEditorOverview.test.tsx.snap index a9e0925423..1f77d5d53b 100644 --- a/packages/components/src/ResearchEditorOverview/__snapshots__/ResearchEditorOverview.test.tsx.snap +++ b/packages/components/src/ResearchEditorOverview/__snapshots__/ResearchEditorOverview.test.tsx.snap @@ -20,6 +20,7 @@ exports[`ResearchEditorOverview > handles empty updates 1`] = ` > Create update @@ -35,6 +36,7 @@ exports[`ResearchEditorOverview > handles empty updates 1`] = ` > Back to research @@ -65,6 +67,7 @@ exports[`ResearchEditorOverview > handles falsey updates 1`] = ` > Create update @@ -80,6 +83,7 @@ exports[`ResearchEditorOverview > handles falsey updates 1`] = ` > Back to research @@ -123,6 +127,7 @@ exports[`ResearchEditorOverview > renders correctly 1`] = ` Update 2 Edit @@ -138,6 +143,7 @@ exports[`ResearchEditorOverview > renders correctly 1`] = ` Update 3 Edit @@ -155,6 +161,7 @@ exports[`ResearchEditorOverview > renders correctly 1`] = ` > Create update @@ -170,6 +177,7 @@ exports[`ResearchEditorOverview > renders correctly 1`] = ` > Back to research diff --git a/packages/components/src/Select/DropdownIndicator.tsx b/packages/components/src/Select/DropdownIndicator.tsx index a17e1ceca4..d51b617de1 100644 --- a/packages/components/src/Select/DropdownIndicator.tsx +++ b/packages/components/src/Select/DropdownIndicator.tsx @@ -9,7 +9,7 @@ import type { DropdownIndicatorProps } from 'react-select' export const DropdownIndicator = (props: DropdownIndicatorProps) => { return ( - + ) } diff --git a/packages/components/src/SettingsFormWrapper/SettingsFormTabList.tsx b/packages/components/src/SettingsFormWrapper/SettingsFormTabList.tsx index 68768f8054..55ee0dcce9 100644 --- a/packages/components/src/SettingsFormWrapper/SettingsFormTabList.tsx +++ b/packages/components/src/SettingsFormWrapper/SettingsFormTabList.tsx @@ -1,8 +1,8 @@ -import { useNavigate } from 'react-router-dom' import styled from '@emotion/styled' import { Tab as BaseTab, tabClasses } from '@mui/base/Tab' import { TabsList as BaseTabsList } from '@mui/base/TabsList' import { prepareForSlot } from '@mui/base/utils' +import { useNavigate } from '@remix-run/react' import { Flex } from 'theme-ui' import { Icon } from '../Icon/Icon' diff --git a/packages/components/src/SettingsFormWrapper/SettingsFormWrapper.tsx b/packages/components/src/SettingsFormWrapper/SettingsFormWrapper.tsx index ff31f03784..26724c5ef7 100644 --- a/packages/components/src/SettingsFormWrapper/SettingsFormWrapper.tsx +++ b/packages/components/src/SettingsFormWrapper/SettingsFormWrapper.tsx @@ -1,7 +1,7 @@ // Used the guide at https://mui.com/base-ui/react-tabs/ as a fundation -import { matchPath, useLocation } from 'react-router-dom' import { Tabs } from '@mui/base/Tabs' +import { matchPath, useLocation } from '@remix-run/react' import { Flex } from 'theme-ui' import { SettingsFormTab } from './SettingsFormTab' diff --git a/packages/components/src/TabbedContent/TabbedContent.tsx b/packages/components/src/TabbedContent/TabbedContent.tsx index 42fbc97e66..42b86c67a9 100644 --- a/packages/components/src/TabbedContent/TabbedContent.tsx +++ b/packages/components/src/TabbedContent/TabbedContent.tsx @@ -4,7 +4,7 @@ import { TabPanel as MuiTabPanel } from '@mui/base/TabPanel' import { Tabs } from '@mui/base/Tabs' import { TabsList } from '@mui/base/TabsList' -import type { PlatformTheme } from 'oa-themes/dist' +import type { PlatformTheme } from 'oa-themes' type Theme = PlatformTheme['styles'] diff --git a/packages/components/src/UsefulStatsButton/UsefulStatsButton.tsx b/packages/components/src/UsefulStatsButton/UsefulStatsButton.tsx index bcaf769656..c1ee686ac2 100644 --- a/packages/components/src/UsefulStatsButton/UsefulStatsButton.tsx +++ b/packages/components/src/UsefulStatsButton/UsefulStatsButton.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' -import { useNavigate } from 'react-router-dom' import { useTheme } from '@emotion/react' +import { useNavigate } from '@remix-run/react' import { Text } from 'theme-ui' import { Button } from '../Button/Button' diff --git a/packages/components/src/index.ts b/packages/components/src/index.ts index daac4d9d34..0b9c78fc86 100644 --- a/packages/components/src/index.ts +++ b/packages/components/src/index.ts @@ -43,9 +43,9 @@ export { ImageGallery } from './ImageGallery/ImageGallery' export { InternalLink } from './InternalLink/InternalLink' export { LinkifyText } from './LinkifyText/LinkifyText' export { Loader } from './Loader/Loader' -export { Map } from './Map/Map' +export { Map } from './Map/Map.client' export { MapMemberCard } from './MapMemberCard/MapMemberCard' -export { MapWithPin } from './MapWithPin/MapWithPin' +export { MapWithPin } from './MapWithPin/MapWithPin.client' export { MemberBadge } from './MemberBadge/MemberBadge' export { Modal } from './Modal/Modal' export { ModerationStatus } from './ModerationStatus/ModerationStatus' diff --git a/packages/components/src/test/utils.tsx b/packages/components/src/test/utils.tsx index 2f15e430d4..7de0820ff3 100644 --- a/packages/components/src/test/utils.tsx +++ b/packages/components/src/test/utils.tsx @@ -1,10 +1,5 @@ -import { - createMemoryRouter, - createRoutesFromElements, - Route, - RouterProvider, -} from 'react-router-dom' import { ThemeProvider } from '@emotion/react' +import { createRemixStub } from '@remix-run/testing' import { render as testLibReact } from '@testing-library/react' import { preciousPlasticTheme } from 'oa-themes' @@ -17,13 +12,18 @@ const customRender = ( ) => testLibReact(ui, { wrapper: ({ children }: { children: React.ReactNode }) => { - const router = createMemoryRouter( - createRoutesFromElements(), - ) + const RemixStub = createRemixStub([ + { + path: '', + Component() { + return <>{children} + }, + }, + ]) return ( - + ) }, diff --git a/packages/cypress/package.json b/packages/cypress/package.json index 415484635f..808356fdc9 100644 --- a/packages/cypress/package.json +++ b/packages/cypress/package.json @@ -3,7 +3,7 @@ "version": "1.0.0", "type": "module", "scripts": { - "start": "node --loader ts-node/esm scripts/start.mts" + "start": "cross-env PORT=3456 node --loader ts-node/esm scripts/start.mts" }, "devDependencies": { "@faker-js/faker": "^7.6.0", diff --git a/packages/cypress/scripts/start.mts b/packages/cypress/scripts/start.mts index ae9e552344..d9c7f23d87 100644 --- a/packages/cypress/scripts/start.mts +++ b/packages/cypress/scripts/start.mts @@ -47,7 +47,7 @@ export const generateAlphaNumeric = (length: number) => { const e2eEnv = config() const isCi = process.argv.includes('ci') -const isProduction = process.argv.includes('prod') +// const isProduction = process.argv.includes('prod') // Prevent unhandled errors being silently ignored process.on('unhandledRejection', (err) => { @@ -86,31 +86,18 @@ async function main() { * There are npm packages like start-server-and-test but they seem to have flaky * performance in some environments (https://github.com/bahmutov/start-server-and-test/issues/250). * Instead manually track via child spawns - * */ async function startAppServer() { - const { CROSSENV_BIN, BUILD_SERVE_JSON } = PATHS + const { CROSSENV_BIN } = PATHS // by default spawns will not respect colours used in stdio, so try to force - const crossEnvArgs = `FORCE_COLOR=1 REACT_APP_SITE_VARIANT=test-ci` + const crossEnvArgs = `FORCE_COLOR=1 VITE_SITE_VARIANT=test-ci` // run local debug server for testing unless production build specified - let serverCmd = `${CROSSENV_BIN} ${crossEnvArgs} BROWSER=none yarn start-ci` - - // for production will instead serve from production build folder - if (isProduction) { - // create local build if not running on ci (which will have build already generated) - if (!isCi) { - // specify CI=false to prevent throwing lint warnings as errors - spawnSync(`${CROSSENV_BIN} ${crossEnvArgs} CI=false yarn build`, { - shell: true, - stdio: ['inherit', 'inherit', 'pipe'], - }) - } - // create a rewrites file for handling local server behaviour - const opts = { rewrites: [{ source: '/**', destination: '/index.html' }] } - fs.writeFile(BUILD_SERVE_JSON, JSON.stringify(opts)) + let serverCmd = `${CROSSENV_BIN} ${crossEnvArgs} BROWSER=none yarn start` - serverCmd = `npx serve build -l 3456` + // create local build if not running on ci (which will have build already generated) + if (isCi) { + serverCmd = 'yarn start-ci' } /******************* Run the main commands ******************* */ diff --git a/packages/cypress/src/integration/SignUp.spec.ts b/packages/cypress/src/integration/SignUp.spec.ts index 6f622ea343..1fea73220f 100644 --- a/packages/cypress/src/integration/SignUp.spec.ts +++ b/packages/cypress/src/integration/SignUp.spec.ts @@ -76,7 +76,7 @@ describe('[User sign-up]', () => { cy.step('Go to settings page') cy.visit('/settings') - cy.get('[data-cy="loader"]').not('visible') + // cy.get('[data-cy="loader"]').not('visible') cy.get('[data-cy="tab-Account"]').click() cy.step('Update Email') diff --git a/packages/cypress/src/integration/common.spec.ts b/packages/cypress/src/integration/common.spec.ts index 62df0f344d..629b0f48ba 100644 --- a/packages/cypress/src/integration/common.spec.ts +++ b/packages/cypress/src/integration/common.spec.ts @@ -19,10 +19,6 @@ describe('[Common]', () => { it('[Page Navigation]', () => { cy.visit('/how-to') - cy.step('Go to Map page') - cy.get('[data-cy=page-link]').contains('Map').click() - cy.url().should('include', '/map') - cy.step('Go to Academy page') cy.get('[data-cy=page-link]').contains('Academy').click() cy.url().should('include', '/academy') @@ -30,6 +26,10 @@ describe('[Common]', () => { cy.step('Go to How-to page') cy.get('[data-cy=page-link]').contains('How-to').click() cy.url().should('include', '/how-to') + + cy.step('Go to Map page') + cy.get('[data-cy=page-link]').contains('Map').click() + cy.url().should('include', '/map') }) describe.only('[User feeback button]', () => { @@ -91,10 +91,12 @@ describe('[Common]', () => { cy.url().should('include', `/u/${user.username}`) cy.step('Go to Settings') + cy.toggleUserMenuOn() cy.clickMenuItem(UserMenuItem.Settings) cy.url().should('include', 'settings') cy.step('Logout the session') + cy.toggleUserMenuOn() cy.clickMenuItem(UserMenuItem.LogOut) cy.get('[data-cy=login]', { timeout: 20000 }).should('be.visible') cy.get('[data-cy=join]').should('be.visible') diff --git a/packages/cypress/src/integration/howto/read.spec.ts b/packages/cypress/src/integration/howto/read.spec.ts index 1f80d4f8a4..636b7fa2dd 100644 --- a/packages/cypress/src/integration/howto/read.spec.ts +++ b/packages/cypress/src/integration/howto/read.spec.ts @@ -62,7 +62,7 @@ describe('[How To]', () => { cy.get('[data-cy=edit]').should('not.exist') cy.step('How-to has basic info') - cy.title().should('eq', `${howto.title} - How-to - Community Platform`) + cy.title().should('eq', `${howto.title} - How-to - Precious Plastic`) cy.get('[data-cy=how-to-basis]').then(($summary) => { expect($summary).to.contain('howto_creator', 'Author') expect($summary).to.contain('Last update', 'Edit') diff --git a/packages/cypress/src/integration/howto/seo.spec.ts b/packages/cypress/src/integration/howto/seo.spec.ts index 967c3eff84..5b102f6666 100644 --- a/packages/cypress/src/integration/howto/seo.spec.ts +++ b/packages/cypress/src/integration/howto/seo.spec.ts @@ -5,7 +5,7 @@ describe('[How To]', () => { const { slug, title, description, cover_image } = MOCK_DATA.howtos.cmMzzlQP00fCckYIeL2e - const pageTitle = `${title} - How-to - Community Platform` + const pageTitle = `${title} - How-to - Precious Plastic` it('[Populates title and description tags]', () => { cy.visit(`/how-to/${slug}`) diff --git a/packages/cypress/src/integration/howto/write.spec.ts b/packages/cypress/src/integration/howto/write.spec.ts index b02f2831da..e3476f8bc0 100644 --- a/packages/cypress/src/integration/howto/write.spec.ts +++ b/packages/cypress/src/integration/howto/write.spec.ts @@ -201,6 +201,7 @@ describe('[How To]', () => { cy.login(creatorEmail, creatorPassword) cy.get('[data-cy=loader]').should('not.exist') + cy.get('[data-cy="MemberBadge-member"]').should('be.visible') cy.step('Access the create-how-to') cy.get('a[href="/how-to/create"]').should('be.visible') cy.get('[data-cy=create]').click() diff --git a/packages/cypress/src/integration/profile.spec.ts b/packages/cypress/src/integration/profile.spec.ts index 0f2edd8852..ee319601a4 100644 --- a/packages/cypress/src/integration/profile.spec.ts +++ b/packages/cypress/src/integration/profile.spec.ts @@ -27,7 +27,7 @@ describe('[Profile]', () => { cy.visit(`/u/${eventReader.userName}`) cy.title().should( 'eq', - `${eventReader.displayName} - Profile - Community Platform`, + `${eventReader.displayName} - Profile - Precious Plastic`, ) cy.get('[data-cy=userDisplayName]').contains(eventReader.userName) diff --git a/packages/cypress/src/integration/questions/read.spec.ts b/packages/cypress/src/integration/questions/read.spec.ts index 67adc9ce02..f3b8627fbf 100644 --- a/packages/cypress/src/integration/questions/read.spec.ts +++ b/packages/cypress/src/integration/questions/read.spec.ts @@ -38,7 +38,7 @@ describe('[Questions]', () => { (discussion) => discussion.sourceId === _id, ) - const pageTitle = `${title} - Question - Community Platform` + const pageTitle = `${title} - Question - Precious Plastic` const image = images[0].downloadUrl cy.step('Can visit question') diff --git a/packages/cypress/src/integration/questions/write.spec.ts b/packages/cypress/src/integration/questions/write.spec.ts index d6db8f0c10..1e781ded2f 100644 --- a/packages/cypress/src/integration/questions/write.spec.ts +++ b/packages/cypress/src/integration/questions/write.spec.ts @@ -22,7 +22,7 @@ describe('[Question]', () => { cy.step('Go to create page') cy.visit('/questions/create') - cy.get('[data-cy=question-create-title]') + cy.get('[data-cy=question-create-title]', { timeout: 20000 }) cy.step('Warn if title is identical to an existing one') cy.get('[data-cy=field-title]').type(item.title).blur({ force: true }) diff --git a/packages/cypress/src/integration/research/discussions.spec.ts b/packages/cypress/src/integration/research/discussions.spec.ts index 2a4206a109..ef42727270 100644 --- a/packages/cypress/src/integration/research/discussions.spec.ts +++ b/packages/cypress/src/integration/research/discussions.spec.ts @@ -52,6 +52,7 @@ describe('[Research.Discussions]', () => { cy.addReply(newReply) cy.contains(`${discussion.comments.length + 1} Comments`) cy.contains(newReply) + cy.wait(1000) cy.queryDocuments('research', '_id', '==', item._id).then((docs) => { const [research] = docs expect(research.totalCommentCount).to.eq(discussion.comments.length + 1) diff --git a/packages/cypress/src/integration/research/read.spec.ts b/packages/cypress/src/integration/research/read.spec.ts index 37a4546818..497697e720 100644 --- a/packages/cypress/src/integration/research/read.spec.ts +++ b/packages/cypress/src/integration/research/read.spec.ts @@ -7,7 +7,7 @@ describe('[Research]', () => { const authoredResearchArticleUrl = '/research/a-test-research' const image = updates[0].images[0].downloadUrl - const pageTitle = `${title} - Research - Community Platform` + const pageTitle = `${title} - Research - Precious Plastic` const researchArticleUrl = `/research/${slug}` describe('[Read a research article]', () => { @@ -19,7 +19,7 @@ describe('[Research]', () => { cy.title().should( 'eq', - `${article.title} - Research - Community Platform`, + `${article.title} - Research - Precious Plastic`, ) cy.step('[Populates title, SEO and social tags]') cy.title().should('eq', pageTitle) diff --git a/packages/cypress/src/support/db/endpoints.ts b/packages/cypress/src/support/db/endpoints.ts index 4c09245e68..41e84ed50e 100644 --- a/packages/cypress/src/support/db/endpoints.ts +++ b/packages/cypress/src/support/db/endpoints.ts @@ -11,7 +11,10 @@ const e = process.env || ({} as any) * e.g. oa_ * SessionStorage prefixes are used to allow test ci environments to dynamically set a db endpoint */ -const DB_PREFIX = sessionStorage.DB_PREFIX || e.REACT_APP_DB_PREFIX || '' +const DB_PREFIX = + (typeof sessionStorage !== 'undefined' && sessionStorage.DB_PREFIX) || + e.VITE_DB_PREFIX || + '' /** * Mapping of generic database endpoints to specific prefixed and revisioned versions for the diff --git a/packages/cypress/src/support/rules.ts b/packages/cypress/src/support/rules.ts index 3f95e7145d..4b088626e2 100644 --- a/packages/cypress/src/support/rules.ts +++ b/packages/cypress/src/support/rules.ts @@ -4,6 +4,9 @@ Cypress.on('uncaught:exception', (err) => { 'No document to update', 'KeyPath previousSlugs', 'KeyPath slug', + // 'There was an error while hydrating', + // 'Hydration failed because the initial UI does not match what was rendered on the server', + // 'An error occurred during hydration.', ] const foundSkipError = skipErrors.find((error) => err.message.includes(error)) @@ -12,6 +15,16 @@ Cypress.on('uncaught:exception', (err) => { return false } + // Cypress and React Hydrating the document don't get along + // for some unknown reason. Hopefully, we figure out why eventually. + // Maybe https://github.com/cypress-io/cypress/issues/27204#issuecomment-2224833564 + if ( + /hydrat/i.test(err.message) || + /Minified React error #418/.test(err.message) || + /Minified React error #423/.test(err.message) + ) { + return false + } // we still want to ensure there are no other unexpected // errors, so we let them fail the test }) diff --git a/packages/documentation/docs/Deployment/circle-ci.md b/packages/documentation/docs/Deployment/circle-ci.md index 9cc82753ef..5c877be187 100644 --- a/packages/documentation/docs/Deployment/circle-ci.md +++ b/packages/documentation/docs/Deployment/circle-ci.md @@ -44,29 +44,29 @@ See [circleci slack orb](https://github.com/CircleCI-Public/slack-orb) for info) ### Runtime Variables -Any variables prefixed with `REACT_APP_` are automatically included with the runtime build. Currently we require: +Any variables prefixed with `VITE_` are automatically included with the runtime build. Currently we require: Firebase configuration ``` -REACT_APP_FIREBASE_API_KEY -REACT_APP_FIREBASE_AUTH_DOMAIN -REACT_APP_FIREBASE_DATABASE_URL -REACT_APP_FIREBASE_MESSAGING_SENDER_ID -REACT_APP_FIREBASE_PROJECT_ID -REACT_APP_FIREBASE_STORAGE_BUCKET +VITE_FIREBASE_API_KEY +VITE_FIREBASE_AUTH_DOMAIN +VITE_FIREBASE_DATABASE_URL +VITE_FIREBASE_MESSAGING_SENDER_ID +VITE_FIREBASE_PROJECT_ID +VITE_FIREBASE_STORAGE_BUCKET ``` Sentry error tracking ``` -REACT_APP_SENTRY_DSN +VITE_SENTRY_DSN ``` Google Analytics ``` -REACT_APP_GA_TRACKING_ID +VITE_GA_TRACKING_ID ``` ### Misc Variables diff --git a/packages/documentation/docs/Install.md b/packages/documentation/docs/Install.md index 6dd0b3122e..534f7c84c4 100644 --- a/packages/documentation/docs/Install.md +++ b/packages/documentation/docs/Install.md @@ -38,17 +38,17 @@ You will need to set up a CircleCI context for each target environment. This con - `FIREBASE_TOKEN` - `GOOGLE_APPLICATION_CREDENTIALS_JSON` -- `REACT_APP_BRANCH` -- `REACT_APP_FIREBASE_API_KEY` -- `REACT_APP_FIREBASE_AUTH_DOMAIN` -- `REACT_APP_FIREBASE_DATABASE_URL` -- `REACT_APP_FIREBASE_MESSAGING_SENDER_ID` -- `REACT_APP_FIREBASE_PROJECT_ID` -- `REACT_APP_FIREBASE_STORAGE_BUCKET` -- `REACT_APP_GA_TRACKING_ID` -- `REACT_APP_PLATFORM_THEME` -- `REACT_APP_CDN_URL` - `https://cdn-url.com` - this is the URL to the CDN where the assets are stored. This is used to load the assets from the CDN instead of the local server. It should **not** include a trailing slash. -- `REACT_APP_PLATFORM_PROFILES` - comma separated list of available profiles. Use `ProfileType` from modules/profile/index for guidance here. For example: `member,workspace` -- `REACT_APP_SUPPORTED_MODULES` – comma separated list of available modules. See `/src/modules/index.ts` for the definitions. -- `REACT_APP_API_URL` – 'https://api-url.com' - this is the URL to the API service. It should **not** include a trailing slash. +- `VITE_BRANCH` +- `VITE_FIREBASE_API_KEY` +- `VITE_FIREBASE_AUTH_DOMAIN` +- `VITE_FIREBASE_DATABASE_URL` +- `VITE_FIREBASE_MESSAGING_SENDER_ID` +- `VITE_FIREBASE_PROJECT_ID` +- `VITE_FIREBASE_STORAGE_BUCKET` +- `VITE_GA_TRACKING_ID` +- `VITE_PLATFORM_THEME` +- `VITE_CDN_URL` - `https://cdn-url.com` - this is the URL to the CDN where the assets are stored. This is used to load the assets from the CDN instead of the local server. It should **not** include a trailing slash. +- `VITE_PLATFORM_PROFILES` - comma separated list of available profiles. Use `ProfileType` from modules/profile/index for guidance here. For example: `member,workspace` +- `VITE_SUPPORTED_MODULES` – comma separated list of available modules. See `/src/modules/index.ts` for the definitions. +- `VITE_API_URL` – 'https://api-url.com' - this is the URL to the API service. It should **not** include a trailing slash. - `SITE_NAME` diff --git a/packages/themes/assets/fonts/README.md b/packages/themes/assets/fonts/README.md index 5ef07031db..254015b0f6 100644 --- a/packages/themes/assets/fonts/README.md +++ b/packages/themes/assets/fonts/README.md @@ -45,18 +45,3 @@ Define a `fonts.css` file ``` - -Import in `index.html` (note, seemed to break with lazy-loading) - - - -``` - -``` - - diff --git a/packages/themes/assets/images/themes/fixing-fashion/favicon.ico b/packages/themes/assets/images/themes/fixing-fashion/favicon.ico new file mode 100644 index 0000000000..0d34039b3e Binary files /dev/null and b/packages/themes/assets/images/themes/fixing-fashion/favicon.ico differ diff --git a/packages/themes/assets/images/themes/precious-plastic/favicon.ico b/packages/themes/assets/images/themes/precious-plastic/favicon.ico new file mode 100644 index 0000000000..83d26ad98a Binary files /dev/null and b/packages/themes/assets/images/themes/precious-plastic/favicon.ico differ diff --git a/packages/themes/assets/images/themes/project-kamp/favicon.ico b/packages/themes/assets/images/themes/project-kamp/favicon.ico new file mode 100644 index 0000000000..2b0a67eb7c Binary files /dev/null and b/packages/themes/assets/images/themes/project-kamp/favicon.ico differ diff --git a/packages/themes/src/fixing-fashion/index.ts b/packages/themes/src/fixing-fashion/index.ts index 8bef6d5aac..4591ac7100 100644 --- a/packages/themes/src/fixing-fashion/index.ts +++ b/packages/themes/src/fixing-fashion/index.ts @@ -1,5 +1,6 @@ import badge from '../../assets/images/themes/fixing-fashion/avatar_member_sm.svg' import avatar from '../../assets/images/themes/fixing-fashion/avatar_space_sm.svg' +import favicon from '../../assets/images/themes/fixing-fashion/favicon.ico' import logo from '../../assets/images/themes/fixing-fashion/fixing-fashion-header.png' import { StyledComponentTheme } from './styles' @@ -8,16 +9,11 @@ import type { PlatformTheme } from '../types' export const Theme: PlatformTheme = { id: 'fixing-fashion', siteName: 'Fixing Fashion', + description: + 'A series of tools for the Fixing Fashion community to collaborate around the world. Connect, share and meet each other to reduce textile waste.', logo, + favicon, badge, avatar, - howtoHeading: `Learn & share how to recycle, build and work`, styles: StyledComponentTheme, - academyResource: 'https://fixing-fashion-academy.netlify.app/', - externalLinks: [ - { - url: 'https://fixing.fashion/', - label: 'Project Homepage', - }, - ], } diff --git a/packages/themes/src/fixing-fashion/styles.ts b/packages/themes/src/fixing-fashion/styles.ts index 2d85b132f6..d0045d66c9 100644 --- a/packages/themes/src/fixing-fashion/styles.ts +++ b/packages/themes/src/fixing-fashion/styles.ts @@ -27,11 +27,6 @@ export const alerts = { export const StyledComponentTheme: ThemeWithName = { name: 'Fixing Fashion', logo: logo, - profileGuidelinesURL: - 'https://community.fixing.fashion/academy/guides/profile', - communityProgramURL: - 'https://community.fixing.fashion/academy/guides/community-program', - ...baseTheme, alerts, colors, diff --git a/packages/themes/src/precious-plastic/index.ts b/packages/themes/src/precious-plastic/index.ts index 9acc732ec5..11f48b7add 100644 --- a/packages/themes/src/precious-plastic/index.ts +++ b/packages/themes/src/precious-plastic/index.ts @@ -1,6 +1,6 @@ import logo from '../../assets/images/precious-plastic-logo-official.svg' import badge from '../../assets/images/themes/precious-plastic/avatar_member_sm.svg' -import donationBanner from '../../assets/images/themes/precious-plastic/donation-banner.jpg' +import favicon from '../../assets/images/themes/precious-plastic/favicon.ico' import { styles } from './styles' import type { PlatformTheme } from '../types' @@ -8,25 +8,11 @@ import type { PlatformTheme } from '../types' export const Theme: PlatformTheme = { id: 'precious-plastic', siteName: 'Precious Plastic', + description: + 'A series of tools for the Precious Plastic community to collaborate around the world. Connect, share and meet each other to tackle plastic waste.', logo, + favicon, badge, - donations: { - body: 'All of the content here is free. Your donation supports this library of Open Source recycling knowledge. Making it possible for everyone in the world to use it and start recycling.', - iframeSrc: 'https://donorbox.org/embed/ppcpdonor?language=en', - imageURL: donationBanner, - }, avatar: '', - howtoHeading: `Learn & share how to recycle, build and work with plastic`, styles, - academyResource: 'https://onearmy.github.io/academy/', - externalLinks: [ - { - url: 'https://bazar.preciousplastic.com/', - label: 'bazar', - }, - { - url: 'https://preciousplastic.com/', - label: 'Global Site', - }, - ], } diff --git a/packages/themes/src/precious-plastic/styles.ts b/packages/themes/src/precious-plastic/styles.ts index becd4f3b7f..df4d82a32c 100644 --- a/packages/themes/src/precious-plastic/styles.ts +++ b/packages/themes/src/precious-plastic/styles.ts @@ -35,12 +35,6 @@ export const alerts = { export const styles: ThemeWithName = { name: 'Precious Plastic', logo: logo, - profileGuidelinesURL: - 'https://community.preciousplastic.com/academy/guides/platform', - questionsGuidelinesURL: - 'https://community.preciousplastic.com/academy/guides/guidelines-questions', - communityProgramURL: - 'https://community.preciousplastic.com/academy/guides/community-program', ...baseTheme, alerts, badges: { diff --git a/packages/themes/src/project-kamp/index.ts b/packages/themes/src/project-kamp/index.ts index f110b39a08..7acc8abbf1 100644 --- a/packages/themes/src/project-kamp/index.ts +++ b/packages/themes/src/project-kamp/index.ts @@ -1,5 +1,6 @@ import badge from '../../assets/images/themes/project-kamp/avatar_member_sm.svg' import avatar from '../../assets/images/themes/project-kamp/avatar_space_sm.svg' +import favicon from '../../assets/images/themes/project-kamp/favicon.ico' import logo from '../../assets/images/themes/project-kamp/project-kamp-header.png' import { StyledComponentTheme } from './styles' @@ -8,20 +9,11 @@ import type { PlatformTheme } from '../types' export const Theme: PlatformTheme = { id: 'project-kamp', siteName: 'Project Kamp', + description: + 'A series of tools for the Project Kamp community to collaborate around the world. Connect, share and meet each other to try and figure out how to life more sustainable.', logo, + favicon, badge, avatar, - howtoHeading: `Learn & share how to recycle, build and work`, styles: StyledComponentTheme, - academyResource: 'https://project-kamp-academy.netlify.app/', - externalLinks: [ - { - url: 'https://projectkamp.com/support.html', - label: 'Support Us', - }, - { - url: 'https://projectkamp.com/', - label: 'Project Homepage', - }, - ], } diff --git a/packages/themes/src/project-kamp/styles.ts b/packages/themes/src/project-kamp/styles.ts index d4ed772130..58602c3733 100644 --- a/packages/themes/src/project-kamp/styles.ts +++ b/packages/themes/src/project-kamp/styles.ts @@ -25,10 +25,6 @@ export const alerts = { export const StyledComponentTheme: ThemeWithName = { name: 'Project Kamp', - profileGuidelinesURL: - 'https://drive.google.com/file/d/1fXTtBbzgCO0EL6G9__aixwqc-Euqgqnd/view', - communityProgramURL: - 'https://community.preciousplastic.com/academy/guides/community-program', logo: logo, ...baseTheme, alerts, diff --git a/packages/themes/src/types/images.d.ts b/packages/themes/src/types/images.d.ts index 397cc9b35b..410d7349b5 100644 --- a/packages/themes/src/types/images.d.ts +++ b/packages/themes/src/types/images.d.ts @@ -1,3 +1,4 @@ declare module '*.svg' declare module '*.png' declare module '*.jpg' +declare module '*.ico' diff --git a/packages/themes/src/types/index.ts b/packages/themes/src/types/index.ts index a6f347297c..1b19c58db5 100644 --- a/packages/themes/src/types/index.ts +++ b/packages/themes/src/types/index.ts @@ -1,21 +1,14 @@ import type { ProfileTypeName } from 'oa-shared' -interface LinkList { - label: string - url: string -} - export interface PlatformTheme { id: string siteName: string + description: string logo: string + favicon: string badge: string - donations?: Donations avatar: string - howtoHeading: string styles: ThemeWithName - academyResource: string - externalLinks: LinkList[] } type Badge = { @@ -23,20 +16,10 @@ type Badge = { normal: string } -type Donations = { - body: string - iframeSrc: string - imageURL: string -} - export interface ThemeWithName { name: string logo: string - profileGuidelinesURL: string - questionsGuidelinesURL?: string - communityProgramURL: string - alerts: any badges: { diff --git a/packages/themes/assets/images/themes/precious-plastic/donation-banner.jpg b/public/assets/img/precious-plastic/donation-banner.jpg similarity index 100% rename from packages/themes/assets/images/themes/precious-plastic/donation-banner.jpg rename to public/assets/img/precious-plastic/donation-banner.jpg diff --git a/public/index.html b/public/index.html deleted file mode 100644 index 0ecb9a9d9a..0000000000 --- a/public/index.html +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - Precious Plastic Community - - - - - - -
- - - diff --git a/remix.env.d.ts b/remix.env.d.ts new file mode 100644 index 0000000000..dcf8c45e1d --- /dev/null +++ b/remix.env.d.ts @@ -0,0 +1,2 @@ +/// +/// diff --git a/scripts/package.json b/scripts/package.json index 0ead25d8b7..f1357a45fe 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -3,8 +3,7 @@ "version": "1.0.0", "type": "module", "scripts": { - "install:clean": "node --loader ts-node/esm ./installClean.ts", - "post-build": "node --loader ts-node/esm ./postBuild.ts" + "install:clean": "node --loader ts-node/esm ./installClean.ts" }, "dependencies": { "cheerio": "^1.0.0-rc.10", diff --git a/scripts/postBuild.ts b/scripts/postBuild.ts deleted file mode 100644 index b182b0bab9..0000000000 --- a/scripts/postBuild.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { load } from 'cheerio' -import dotenv from 'dotenv' -import fs from 'fs' -import fsExtra from 'fs-extra' -import path from 'path' - -import { _supportedConfigurationOptions } from '../src/config/constants.ts' - -import type { CheerioAPI } from 'cheerio' - -main() - -function main() { - initializeEnvironmentVariables('../.env') - - const $ = loadWebpage('../build/index.html') - - setupFrontendConfiguration($) - - console.log('Applying theme...') - const platformTheme = process.env.REACT_APP_PLATFORM_THEME - if (platformTheme) { - console.log('theme: ' + platformTheme) - console.log('Copying assets.') - fsExtra.copySync( - '../src/assets/images/themes/' + platformTheme + '/public', - '../build', - ) - } else { - console.log('No theme found, skipping.') - } - console.log('') - - console.log('Making SEO changes...') - const siteName = process.env.SITE_NAME || 'Community Platform' - console.log('site name: ' + siteName) - - $('title').text(siteName) - $('meta[property="og:title"]').attr('content', siteName) - $('meta[name="twitter:title"]').attr('content', siteName) - - if (platformTheme) { - const siteDescription = - platformTheme === 'precious-plastic' - ? 'A series of tools for the Precious Plastic community to collaborate around the world. Connect, share and meet each other to tackle plastic waste.' - : 'A platform for the Project Kamp community to collaborate around the world. Connect, share and meet each other to figure out how to live more sustainably' - - console.log('site description: ' + siteDescription) - - $('meta[property="og:description"]').attr('content', siteDescription) - $('meta[name="twitter:description"]').attr('content', siteDescription) - $('meta[name="description"]').attr('content', siteDescription) - } - console.log('') - - if (process.env.REACT_APP_BRANCH !== 'production') { - $('head').append('') - } - - console.log('Saving...') - const output = $.html() - fs.writeFileSync('../build/index.html', output, { encoding: 'utf-8' }) - console.log('') -} - -function initializeEnvironmentVariables(filepath: string) { - dotenv.config({ path: path.resolve(filepath), debug: true }) -} - -function loadWebpage(filepath: string) { - const builtHTML = fs.readFileSync(filepath, { - encoding: 'utf-8', - }) - return load(builtHTML, { recognizeSelfClosing: true }) -} - -function setupFrontendConfiguration(webpage: CheerioAPI) { - console.log('Writing configuration into the global window object...') - const configuration = getWindowVariableObject() - setupScriptTagWithConfiguration(webpage, configuration) - console.log('') -} - -function setupScriptTagWithConfiguration(webpage: CheerioAPI, configuration) { - webpage('script#CommunityPlatform').html( - 'window.__OA_COMMUNITY_PLATFORM_CONFIGURATION=' + - JSON.stringify(configuration) + - ';', - ) -} - -function getWindowVariableObject() { - const configurationObject = {} - - _supportedConfigurationOptions.forEach((variable: string) => { - configurationObject[variable] = process.env[variable] || '' - }) - - if (_supportedConfigurationOptions.filter((v) => !process.env[v]).length) { - console.log( - 'The following properties were not found within the current environment:', - ) - console.log( - _supportedConfigurationOptions.filter((v) => !process.env[v]).join('\n'), - ) - } - - return configurationObject -} diff --git a/server.js b/server.js new file mode 100644 index 0000000000..f280eecf01 --- /dev/null +++ b/server.js @@ -0,0 +1,152 @@ +/* eslint-disable no-undef */ +import { createRequestHandler } from '@remix-run/express' +import compression from 'compression' +import dotenv from 'dotenv' +import express from 'express' +import helmet from 'helmet' + +dotenv.config() + +const viteDevServer = + process.env.NODE_ENV === 'production' + ? undefined + : await import('vite').then((vite) => + vite.createServer({ + server: { middlewareMode: true }, + }), + ) + +const remixHandler = createRequestHandler({ + build: viteDevServer + ? () => viteDevServer.ssrLoadModule('virtual:remix/server-build') + : // eslint-disable-next-line import/no-unresolved + await import('./build/server/index.js'), // comment necessary because lint runs before build +}) + +const app = express() + +app.use(compression()) + +// http://expressjs.com/en/advanced/best-practice-security.html#at-a-minimum-disable-x-powered-by-header +app.disable('x-powered-by') + +const wsUrls = process.env.WS_URLS?.split(',').map((url) => url.trim()) + +const imgSrc = [ + "'self'", + 'data:', + 'blob:', + 'google.com', + '*.openstreetmap.org', + 'firebasestorage.googleapis.com', + 'onearmy.github.io', + 'cdn.jsdelivr.net', + '*.google-analytics.com', +] + +const cdnUrl = import.meta.env?.VITE_CDN_URL || process.env?.VITE_CDN_URL + +if (cdnUrl) { + imgSrc.push(cdnUrl) +} + +// helmet config +app.use( + helmet.contentSecurityPolicy({ + directives: { + fontSrc: ["'self'", 'fonts.gstatic.com', 'fonts.googleapis.com'], + connectSrc: [ + "'self'", + '*.run.app', + 'securetoken.googleapis.com', + 'firestore.googleapis.com', + 'identitytoolkit.googleapis.com', + '*.openstreetmap.org', + '*.firebaseio.com', + '*.firebasedatabase.app', + '*.google-analytics.com', + 'firebasestorage.googleapis.com', + '*.cloudfunctions.net', + ...wsUrls, + ], + defaultSrc: [ + "'self'", + 'googletagmanager.com', + '*.googletagmanager.com', + 'analytics.google.com', + '*.analytics.google.com', + '*.google-analytics.com', + '*.firebaseio.com', + 'googleapis.com', + ], + scriptSrc: [ + "'self'", + 'googletagmanager.com', + '*.googletagmanager.com', + 'fonts.gstatic.com', + 'fonts.googleapis.com', + '*.analytics.google.com', + '*.google-analytics.com', + 'www.youtube.com', + "'unsafe-eval'", + "'unsafe-inline'", + '*.firebasedatabase.app', + '*.firebaseio.com', + ], + frameSrc: [ + "'self'", + 'onearmy.github.io', + '*.youtube.com', + '*.donorbox.org', + '*.netlify.app', + ], + imgSrc: imgSrc, + objectSrc: ["'self'"], + // Enforce HTTPS only on production + upgradeInsecureRequests: + process.env.NODE_ENV === 'production' ? [] : null, + }, + }), +) +// Enforce HTTPS only on production +process.env.NODE_ENV === 'production' ?? + app.use( + helmet.hsts({ maxAge: 31536000, preload: true, includeSubDomains: false }), + ) +app.use(helmet.dnsPrefetchControl({ allow: true })) +app.use(helmet.hidePoweredBy()) +app.use(helmet.noSniff()) +app.use(helmet.referrerPolicy({ policy: ['origin'] })) +app.use(helmet.xssFilter()) +app.use(helmet.hidePoweredBy()) + +app.use(function (req, res, next) { + if (!('JSONResponse' in res)) { + return next() + } + + res.set('Cache-Control', 'public, max-age=31557600') + res.json(res.JSONResponse) +}) + +// handle asset requests +if (viteDevServer) { + app.use(viteDevServer.middlewares) +} else { + // Vite fingerprints its assets so we can cache forever. + app.use( + '/assets', + express.static('build/client/assets', { immutable: true, maxAge: '1y' }), + ) +} + +app.use(express.static('build/client', { maxAge: '1h' })) + +app.all('*', remixHandler) + +let port = process.env.PORT || 3456 // 3456 is default port for ci + +app.listen(port, '0.0.0.0', () => { + // eslint-disable-next-line no-console, no-undef + console.log(`Express server started on http://0.0.0.0:${port}`) +}) diff --git a/src/models/categories.model.tsx b/shared/models/categories.ts similarity index 75% rename from src/models/categories.model.tsx rename to shared/models/categories.ts index c743805cf7..46450ea787 100644 --- a/src/models/categories.model.tsx +++ b/shared/models/categories.ts @@ -1,4 +1,4 @@ -import type { DBDoc } from './dbDoc.model' +import type { DBDoc } from './db' export interface ISelectedCategories { [key: string]: boolean diff --git a/shared/models/common.ts b/shared/models/common.ts index 0ee8dceeef..8c000b7ed9 100644 --- a/shared/models/common.ts +++ b/shared/models/common.ts @@ -1,7 +1,34 @@ -export enum IModerationStatus { - DRAFT = 'draft', - AWAITING_MODERATION = 'awaiting-moderation', - IMPROVEMENTS_NEEDED = 'improvements-needed', - REJECTED = 'rejected', - ACCEPTED = 'accepted', +// A reminder that dates should be saved in the ISOString format +// i.e. new Date().toISOString() => 2011-10-05T14:48:00.000Z +// This is more consistent than others and allows better querying +export type ISODateString = string + +export interface IConvertedFileMeta { + photoData: Blob + objectUrl: string + name: string + type: string +} + +export type FetchState = 'idle' | 'fetching' | 'completed' + +export interface ILatLng { + lat: number + lng: number +} + +export interface ILocation { + name: string + country: string + countryCode: string + administrative: string + latlng: ILatLng + postcode: string + value: string +} + +export type Collaborator = { + countryCode?: string | null + userName: string + isVerified: boolean } diff --git a/shared/models/config.ts b/shared/models/config.ts new file mode 100644 index 0000000000..283e9bdf51 --- /dev/null +++ b/shared/models/config.ts @@ -0,0 +1,37 @@ +/************** Interfaces ************** */ +export interface IServiceAccount { + type: string + project_id: string + private_key_id: string + private_key: string + client_email: string + client_id: string + auth_uri: string + token_uri: string + auth_provider_x509_cert_url: string + client_x509_cert_url: string +} +export interface IAnalytics { + tracking_code: string + view_id: string +} +export interface IIntergrations { + slack_webhook: string + discord_webhook: string + discord_alert_channel_webhook: string + patreon_client_id: string + patreon_client_secret: string +} +export interface IDeployment { + site_url: string +} + +export interface configVars { + service: IServiceAccount + analytics: IAnalytics + integrations: IIntergrations + deployment: IDeployment + prerender: { + api_key: string + } +} diff --git a/shared/models/db.ts b/shared/models/db.ts index 510800f846..c7ab75093c 100644 --- a/shared/models/db.ts +++ b/shared/models/db.ts @@ -1,3 +1,5 @@ +import type { ISODateString } from './common' + /************************************************************************************* * Generate a list of DB endpoints used in the app * @@ -55,7 +57,7 @@ const storage = * e.g. oa_ * SessionStorage prefixes are used to allow test ci environments to dynamically set a db endpoint */ -const DB_PREFIX = storage.DB_PREFIX || e.REACT_APP_DB_PREFIX || '' +const DB_PREFIX = storage.DB_PREFIX || e.VITE_DB_PREFIX || '' /** * Mapping of generic database endpoints to specific prefixed and revisioned versions for the @@ -68,3 +70,11 @@ const DB_PREFIX = storage.DB_PREFIX || e.REACT_APP_DB_PREFIX || '' export const DB_ENDPOINTS = generateDBEndpoints(DB_PREFIX) export type DBEndpoint = keyof typeof DB_ENDPOINTS + +export interface DBDoc { + _id: string + _created: ISODateString + _modified: ISODateString + _deleted: boolean + _contentModifiedTimestamp: ISODateString +} diff --git a/src/models/discussion.models.tsx b/shared/models/discussions.ts similarity index 81% rename from src/models/discussion.models.tsx rename to shared/models/discussions.ts index cc30a755c7..085ea75d20 100644 --- a/src/models/discussion.models.tsx +++ b/shared/models/discussions.ts @@ -1,5 +1,5 @@ -import type { DBDoc } from './dbDoc.model' -import type { IQuestion } from './question.models' +import type { DBDoc } from './db' +import type { IQuestion } from './questions' export type IComment = { _id: string @@ -29,3 +29,7 @@ export type IDiscussion = { export type IDiscussionDB = IDiscussion & DBDoc export type IDiscussionSourceModelOptions = IQuestion.Item + +export interface UserComment extends IComment { + isEditable: boolean +} diff --git a/shared/models/howto.ts b/shared/models/howto.ts index e5570c36f8..6a780a9fb4 100644 --- a/shared/models/howto.ts +++ b/shared/models/howto.ts @@ -1,6 +1,76 @@ +import type { ICategory } from './categories' +import type { IConvertedFileMeta } from './common' +import type { DBDoc } from './db' +import type { IModerable } from './moderation' +import type { IUploadedFileMeta } from './storage' +import type { ISelectedTags } from './tags' +import type { UserMention } from './user' +import type { ISharedFeatures } from './voteUseful' + export enum DifficultyLevel { EASY = 'Easy', MEDIUM = 'Medium', HARD = 'Hard', VERY_HARD = 'Very Hard', } + +// By default all how-to form input fields come as strings +// The IHowto interface can imposes the correct formats on fields +// Additionally convert from local filemeta to uploaded filemeta +export interface IHowto extends IHowtoFormInput { + _createdBy: string + _deleted: boolean + cover_image?: IUploadedFileMeta + fileLink?: string + total_downloads?: number + latestCommentDate?: string | undefined + mentions: UserMention[] + previousSlugs: string[] + totalComments: number + totalUsefulVotes?: number + keywords?: string[] +} + +/** + * Howtos retrieved from the database also include metadata such as _id, _created and _modified + */ +export type IHowtoDB = IHowto & DBDoc + +export interface IHowtoStep extends IHowToStepFormInput { + // *** NOTE - adding an '_animationKey' field to track when specific array element removed for + images?: Array + videoUrl?: string + title?: string + text?: string + _animationKey?: string +} + +export interface IHowToStepFormInput { + images?: Array + title?: string + text?: string + videoUrl?: string + _animationKey?: string +} + +export interface IHowtoFormInput extends IModerable, ISharedFeatures { + slug: string + title: string + allowDraftSave?: boolean + category?: ICategory + // NOTE cover image input starts as convertedFileMeta but is transformed on upload + cover_image?: IUploadedFileMeta | IConvertedFileMeta + // Added to be able to recover on edit by admin + creatorCountry?: string + totalComments?: number + latestCommentDate?: string + description?: string + difficulty_level?: DifficultyLevel + files?: Array + fileLink?: string + mentions?: UserMention[] + steps: IHowToStepFormInput[] + // note, tags will remain optional as if populated {} will be stripped by db (firestore) + tags?: ISelectedTags + time?: string +} diff --git a/shared/models/index.ts b/shared/models/index.ts index 85d30b8406..d9036a8728 100644 --- a/shared/models/index.ts +++ b/shared/models/index.ts @@ -6,3 +6,11 @@ export * from './notifications' export * from './research' export * from './user' export * from './messages' +export * from './categories' +export * from './config' +export * from './discussions' +export * from './moderation' +export * from './storage' +export * from './tags' +export * from './voteUseful' +export * from './questions' diff --git a/shared/models/maps.ts b/shared/models/maps.ts index 6da990a04f..e4c40f94ab 100644 --- a/shared/models/maps.ts +++ b/shared/models/maps.ts @@ -1,8 +1,50 @@ -import type { ProfileTypeName } from './user' +import type { ILatLng } from './common' +import type { IModerationStatus } from './moderation' +import type { IUserBadges, ProfileTypeName, WorkspaceType } from './user' -type UserBadges = { +/** + * Map pins have a `type` which correspond to icon + * They can also optionally have a subtype for additional filtering + */ +export interface IMapPin { + moderation: IModerationStatus + _createdBy?: string + _id: string + _deleted: boolean + type: ProfileTypeName + location: ILatLng verified: boolean - supporter: boolean + subType?: IMapPinSubtype + comments?: string + creator?: IProfileCreator +} + +/** + * Map pins keep minimal information required for pin display. + * @param _id - The id that will be used to pull pin details + * currently this is a user profile id + * @param type - Pin type icon to use - currently mapped to profile types + * @param subtype - currently used for workspacetype filtering + */ + +export type IMapPinSubtype = WorkspaceType + +/** + * @param detail - by default details are pulled on pin open, using + * the pin _id param as the user profile ID required for lookup + */ +export interface IMapPinWithDetail extends IMapPin { + detail: IMapPinDetail +} + +export interface IMapGrouping { + _count?: number + grouping: IPinGrouping + displayName: string + type: ProfileTypeName + subType?: IMapPinSubtype + icon: string + hidden?: boolean } export interface IBoundingBox { @@ -15,11 +57,6 @@ export enum IPinGrouping { PLACE = 'place', } -export interface ILatLng { - lat: number - lng: number -} - export interface IMapPinDetail { country: string | null displayName?: string @@ -35,7 +72,7 @@ export interface IProfileCreator { _id: string _lastActive: string about?: string - badges?: UserBadges + badges?: IUserBadges countryCode: string coverImage?: string displayName: string diff --git a/shared/models/messages.ts b/shared/models/messages.ts index 2de213826d..4e1ab232d7 100644 --- a/shared/models/messages.ts +++ b/shared/models/messages.ts @@ -1,5 +1,21 @@ +import type { DBDoc } from './db' +import type { IUser } from './user' + export type SendMessage = { to: string message: string name: string } + +export type IMessageDB = DBDoc & IMessage + +export interface IMessage extends IMessageInput { + isSent: boolean +} + +export interface IMessageInput { + email: string + text: string + toUserName: IUser['userName'] + name?: string +} diff --git a/src/models/moderation.model.ts b/shared/models/moderation.ts similarity index 55% rename from src/models/moderation.model.ts rename to shared/models/moderation.ts index 9f5c71f4ab..f1ca034a58 100644 --- a/src/models/moderation.model.ts +++ b/shared/models/moderation.ts @@ -1,4 +1,10 @@ -import type { IModerationStatus } from 'oa-shared' +export enum IModerationStatus { + DRAFT = 'draft', + AWAITING_MODERATION = 'awaiting-moderation', + IMPROVEMENTS_NEEDED = 'improvements-needed', + REJECTED = 'rejected', + ACCEPTED = 'accepted', +} export interface IModeration { moderation: IModerationStatus diff --git a/shared/models/notifications.ts b/shared/models/notifications.ts index 905d7f8649..09ebd9b208 100644 --- a/shared/models/notifications.ts +++ b/shared/models/notifications.ts @@ -28,3 +28,35 @@ export type UserNotificationItem = { type: NotificationType children: React.ReactNode } + +export interface INotification { + _id: string + _created: string + triggeredBy: { + displayName: string + // this field is the userName of the user, which we use as a unique id as of https://github.com/ONEARMY/community-platform/pull/2479/files + userId: string + } + relevantUrl?: string + type: NotificationType + read: boolean + notified: boolean + // email contains the id of the doc in the emails collection if the notification was included in + // an email or 'failed' if an email with this notification was attempted and encountered an error + email?: string + title?: string +} + +export type INotificationSettings = { + enabled?: { + [T in NotificationType]: boolean + } + emailFrequency: EmailNotificationFrequency +} + +export interface IPendingEmails { + _authID: string + _userId: string + emailFrequency?: INotificationSettings['emailFrequency'] + notifications: INotification[] +} diff --git a/src/models/question.models.tsx b/shared/models/questions.ts similarity index 67% rename from src/models/question.models.tsx rename to shared/models/questions.ts index f3d70f2585..9652c9df31 100644 --- a/src/models/question.models.tsx +++ b/shared/models/questions.ts @@ -1,10 +1,9 @@ -import type { IUploadedFileMeta } from '../stores/storage' -import type { IConvertedFileMeta } from '../types' -import type { DBDoc } from './dbDoc.model' -import type { IModerable } from './moderation.model' -import type { IQuestionCategory } from './questionCategories.model' -import type { ISelectedTags } from './tags.model' -import type { ISharedFeatures } from './voteUseful.model' +import type { IConvertedFileMeta } from './common' +import type { DBDoc } from './db' +import type { IModerable } from './moderation' +import type { IUploadedFileMeta } from './storage' +import type { ISelectedTags } from './tags' +import type { ISharedFeatures } from './voteUseful' /** * Question retrieved from the database also include metadata such as _id, _created and _modified @@ -38,3 +37,9 @@ export namespace IQuestion { images?: (IUploadedFileMeta | IConvertedFileMeta | null)[] } } + +export type ISelectedQuestionCategories = Record + +export interface IQuestionCategory extends DBDoc { + label: string +} diff --git a/shared/models/research.ts b/shared/models/research.ts index d17631965f..e1e23663f7 100644 --- a/shared/models/research.ts +++ b/shared/models/research.ts @@ -1,3 +1,11 @@ +import type { IConvertedFileMeta } from './common' +import type { DBDoc } from './db' +import type { IModerable } from './moderation' +import type { IUploadedFileMeta } from './storage' +import type { ISelectedTags } from './tags' +import type { UserMention } from './user' +import type { ISharedFeatures } from './voteUseful' + export enum ResearchStatus { IN_PROGRESS = 'In progress', COMPLETED = 'Completed', @@ -8,3 +16,94 @@ export enum ResearchUpdateStatus { DRAFT = 'draft', PUBLISHED = 'published', } + +/** + * Research retrieved from the database also include metadata such as _id, _created and _modified + */ +export type IResearchDB = DBDoc & IResearch.ItemDB + +export type IResearchStats = { + votedUsefulCount: number +} + +type UserId = string +type DateString = string + +type ResearchDocumentLockInformation = { + by: UserId + at: DateString +} + +type ResearchDocumentLock = ResearchDocumentLockInformation | null + +export const researchStatusOptions = ( + Object.keys(ResearchStatus) as (keyof typeof ResearchStatus)[] +).map((status) => { + return { + label: ResearchStatus[status], + value: ResearchStatus[status], + } +}) + +type UserIdList = UserId[] + +export namespace IResearch { + /** The main research item, as created by a user */ + export type Item = { + updates: Update[] + mentions?: UserMention[] + _createdBy: string + collaborators: string[] + subscribers?: UserIdList + locked?: ResearchDocumentLock + totalUpdates?: number + totalUsefulVotes?: number + totalCommentCount: number + keywords?: string[] + } & Omit & + DBDoc + + /** A research item update */ + export type Update = { + title: string + description: string + images: Array + files: Array + fileLink: string + downloadCount: number + videoUrl?: string + collaborators?: string[] + commentCount?: number + status?: ResearchUpdateStatus + researchStatus?: ResearchStatus + locked?: ResearchDocumentLock + _id: string + } & DBDoc + + export interface FormInput extends IModerable, ISharedFeatures { + title: string + description: string + researchCategory?: IResearchCategory + slug: string + tags: ISelectedTags + creatorCountry?: string + collaborators: string + previousSlugs?: string[] + researchStatus?: ResearchStatus + } + + /** Research items synced from the database will contain additional metadata */ + // Use of Omit to override the 'updates' type to UpdateDB + export type ItemDB = Omit & { + totalCommentCount: number + updates: UpdateDB[] + } & DBDoc + + export type UpdateDB = Update & DBDoc +} + +export type ISelectedResearchCategories = Record + +export interface IResearchCategory extends DBDoc { + label: string +} diff --git a/shared/models/storage.ts b/shared/models/storage.ts new file mode 100644 index 0000000000..934e647bf9 --- /dev/null +++ b/shared/models/storage.ts @@ -0,0 +1,10 @@ +export interface IUploadedFileMeta { + downloadUrl: string + contentType?: string | null + fullPath: string + name: string + type: string + size: number + timeCreated: string + updated: string +} diff --git a/src/models/tags.model.tsx b/shared/models/tags.ts similarity index 95% rename from src/models/tags.model.tsx rename to shared/models/tags.ts index 94b26dcd67..54486318e2 100644 --- a/src/models/tags.model.tsx +++ b/shared/models/tags.ts @@ -1,8 +1,8 @@ -import type { DBDoc } from './dbDoc.model' - // when tags are saved in things like how-tos, it is done so as a json object which // maps tag keys to boolean values. e.g. [{tag1:true,tag2:true}] +import type { DBDoc } from './db' + // this is to allow easier query of multiple tags within the database (e.g. selectedTags.tag1==true && selectedTags.tag2==true) export interface ISelectedTags { [key: string]: boolean diff --git a/shared/models/user.ts b/shared/models/user.ts index 7b7a899c70..dd1361f2cd 100644 --- a/shared/models/user.ts +++ b/shared/models/user.ts @@ -1,3 +1,9 @@ +import type { ILocation, ISODateString } from './common' +import type { DBDoc } from './db' +import type { IModerationStatus } from './moderation' +import type { INotification, INotificationSettings } from './notifications' +import type { IUploadedFileMeta } from './storage' + /* eslint-disable @typescript-eslint/naming-convention */ export enum UserRole { SUPER_ADMIN = 'super-admin', @@ -17,6 +23,8 @@ export enum ExternalLinkLabel { SOCIAL_MEDIA = 'social media', } +export type IUserDB = IUser & DBDoc + // See https://docs.patreon.com/?javascript#user-v2 export interface PatreonUserAttributes { about: string @@ -144,3 +152,96 @@ export interface IOpeningHours { openFrom: string openTo: string } + +export type UserMention = { + username: string + location: string +} + +// IUser retains most of the fields from legacy users (omitting passwords), +// and has a few additional fields. Note 'email' is excluded +// _uid is unique/fixed identifier +// ALL USER INFO BELOW IS PUBLIC +export interface IUser { + // authID is additional id populated by firebase auth, required for some auth operations + _authID: string + _lastActive?: ISODateString + // userName is same as legacy 'mention_name', e.g. @my-name. It will also be the doc _id and + // firebase auth displayName property + userName: string + displayName: string + verified: boolean + badges?: IUserBadges + // images will be in different formats if they are pending upload vs pulled from db + coverImages: IUploadedFileMeta[] + userImage?: IUploadedFileMeta + links: IExternalLink[] + userRoles?: UserRole[] + about?: string | null + country?: string | null + location?: ILocation | null + year?: ISODateString + stats?: IUserStats + notification_settings?: INotificationSettings + notifications?: INotification[] + profileCreated?: ISODateString + profileCreationTrigger?: string + // Used to generate an encrypted unsubscribe url in emails + unsubscribeToken?: string | null + impact?: IUserImpact + isBlockedFromMessaging?: boolean + isContactableByPublic?: boolean + patreon?: PatreonUser | null + totalUseful?: number + + // Primary PP profile type related fields + profileType: ProfileTypeName + subType?: WorkspaceType | null + workspaceType?: WorkspaceType | null + mapPinDescription?: string | null + openingHours?: IOpeningHours[] + collectedPlasticTypes?: PlasticTypeLabel[] | null + machineBuilderXp?: IMAchineBuilderXp[] | null + isExpert?: boolean | null + isV4Member?: boolean | null +} + +export interface IUserBadges { + verified?: boolean + supporter?: boolean +} + +export interface IExternalLink { + key: string + url: string + label: ExternalLinkLabel +} + +/** + * Track the ids and moderation status as summary for user stats + */ +interface IUserStats { + userCreatedHowtos: { [id: string]: IModerationStatus } + userCreatedResearch: { [id: string]: IModerationStatus } + userCreatedQuestions: { [id: string]: IModerationStatus } + userCreatedComments: { [id: string]: string | null } +} + +export interface IUserImpact { + [key: number]: IImpactYearFieldList +} + +export interface IImpactDataField { + id: string + value: number + isVisible: boolean +} + +export type IImpactYearFieldList = IImpactDataField[] + +export type IImpactYear = 2019 | 2020 | 2021 | 2022 | 2023 + +export type INotificationUpdate = { + _id: string + notifications?: INotification[] +} diff --git a/src/models/voteUseful.model.ts b/shared/models/voteUseful.ts similarity index 100% rename from src/models/voteUseful.model.ts rename to shared/models/voteUseful.ts diff --git a/src/.eslintrc.json b/src/.eslintrc.json index 31813b0715..6479217883 100644 --- a/src/.eslintrc.json +++ b/src/.eslintrc.json @@ -1,15 +1,5 @@ { "rules": { - "no-console": "error", - "prefer-arrow-functions/prefer-arrow-functions": [ - "error", - { - "classPropertiesAllowed": false, - "disallowPrototype": false, - "returnStyle": "unchanged", - "singleReturnOnly": false - } - ] - }, - "plugins": ["prefer-arrow-functions"] + "no-console": "warn" + } } diff --git a/src/App.tsx b/src/App.tsx deleted file mode 100644 index f415b4782d..0000000000 --- a/src/App.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import { Global, ThemeProvider } from '@emotion/react' -import { observer } from 'mobx-react' -import { GlobalStyles } from 'oa-components' - -import ErrorBoundary from './common/Error/ErrorBoundary' -import { - rootStoreContext, - useCommonStores, -} from './common/hooks/useCommonStores' -import { Pages } from './pages' - -export const App = observer(() => { - const rootStore = useCommonStores() - - return ( - - - <> - - - - - - - - ) -}) diff --git a/src/common/Alerts/AlertIncompleteProfile.tsx b/src/common/Alerts/AlertIncompleteProfile.tsx index 9e6e355086..07fd2e60c8 100644 --- a/src/common/Alerts/AlertIncompleteProfile.tsx +++ b/src/common/Alerts/AlertIncompleteProfile.tsx @@ -1,5 +1,5 @@ -import { Link } from 'react-router-dom' -import { observer } from 'mobx-react' +import { Link } from '@remix-run/react' +import { observer } from 'mobx-react-lite' import { Banner } from 'oa-components' import { useCommonStores } from 'src/common/hooks/useCommonStores' import { isProfileComplete } from 'src/utils/isProfileComplete' diff --git a/src/common/Analytics/GoogleAnalytics.tsx b/src/common/Analytics/GoogleAnalytics.tsx index 520bc765b2..2faf9d8734 100644 --- a/src/common/Analytics/GoogleAnalytics.tsx +++ b/src/common/Analytics/GoogleAnalytics.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react' import ReactGA from 'react-ga4' -import { useLocation } from 'react-router-dom' +import { useLocation } from '@remix-run/react' import { GA_TRACKING_ID } from 'src/config/config' export const GoogleAnalytics = () => { diff --git a/src/common/AuthWrapper.tsx b/src/common/AuthWrapper.tsx index a712f260d5..37d41027bd 100644 --- a/src/common/AuthWrapper.tsx +++ b/src/common/AuthWrapper.tsx @@ -1,11 +1,11 @@ import React from 'react' import { observer } from 'mobx-react' import { useCommonStores } from 'src/common/hooks/useCommonStores' -import { DEV_SITE_ROLE, SITE } from 'src/config/config' +import { SITE } from 'src/config/config' +import { getDevSiteRole } from 'src/config/devSiteConfig' import { isTestEnvironment } from 'src/utils/isTestEnvironment' -import type { UserRole } from 'oa-shared' -import type { IUserDB } from 'src/models' +import type { IUserDB, UserRole } from 'oa-shared' /* Simple wrapper to only render a component if the user is logged in (plus optional user role required) @@ -47,8 +47,8 @@ const isUserAuthorized = ( // if running dev or preview site allow wwwuser-overridden permissions (ignoring db user role) if (!isTestEnvironment && (SITE === 'dev_site' || SITE === 'preview')) { - if (DEV_SITE_ROLE) { - return rolesRequired.includes(DEV_SITE_ROLE) + if (getDevSiteRole()) { + return rolesRequired.includes(getDevSiteRole()) } } // otherwise use logged in user profile values diff --git a/src/common/DiscussionWrapper.test.tsx b/src/common/DiscussionWrapper.test.tsx index 22a2c55ea7..8f416d0791 100644 --- a/src/common/DiscussionWrapper.test.tsx +++ b/src/common/DiscussionWrapper.test.tsx @@ -9,7 +9,7 @@ import { describe, expect, it, vi } from 'vitest' import { DiscussionWrapper } from './DiscussionWrapper' -import type { IDiscussion } from 'src/models' +import type { IDiscussion } from 'oa-shared' const Theme = testingThemeStyles const mockUser = FactoryUser() diff --git a/src/common/DiscussionWrapper.tsx b/src/common/DiscussionWrapper.tsx index 04ce69cfb1..e1aec127ea 100644 --- a/src/common/DiscussionWrapper.tsx +++ b/src/common/DiscussionWrapper.tsx @@ -11,7 +11,7 @@ import { Text } from 'theme-ui' import { HideDiscussionContainer } from './HideDiscussionContainer' -import type { IDiscussion } from 'src/models' +import type { IDiscussion } from 'oa-shared' const DISCUSSION_NOT_FOUND = 'Discussion not found :(' const LOADING_LABEL = 'Loading the awesome discussion' diff --git a/src/common/DownloadWithDonationAsk.test.tsx b/src/common/DownloadWithDonationAsk.test.tsx index ff23b58b53..fbd0a03548 100644 --- a/src/common/DownloadWithDonationAsk.test.tsx +++ b/src/common/DownloadWithDonationAsk.test.tsx @@ -8,12 +8,11 @@ import { describe, expect, it, vi } from 'vitest' import { useCommonStores } from './hooks/useCommonStores' import { DownloadWithDonationAsk } from './DownloadWithDonationAsk' -import type { IUserDB } from 'src/models' -import type { IUploadedFileMeta } from 'src/stores/storage' +import type { IUploadedFileMeta, IUserDB } from 'oa-shared' import type { Mock } from 'vitest' const mockedUsedNavigate = vi.fn() -vi.mock('react-router-dom', () => ({ +vi.mock('@remix-run/react', () => ({ useNavigate: () => mockedUsedNavigate, })) @@ -25,15 +24,6 @@ const userToMock = (user?: IUserDB) => { return (useCommonStores as Mock).mockImplementation(() => ({ stores: { userStore: { user: user ?? undefined }, - themeStore: { - currentTheme: { - donations: { - body: '', - iframeSrc: '', - imageURL: '', - }, - }, - }, }, })) } diff --git a/src/common/DownloadWithDonationAsk.tsx b/src/common/DownloadWithDonationAsk.tsx index 46cd06170f..b64c92813f 100644 --- a/src/common/DownloadWithDonationAsk.tsx +++ b/src/common/DownloadWithDonationAsk.tsx @@ -1,5 +1,5 @@ import { useState } from 'react' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@remix-run/react' import { DonationRequestModal, DownloadButton, @@ -7,9 +7,7 @@ import { DownloadStaticFile, } from 'oa-components' -import { useCommonStores } from './hooks/useCommonStores' - -import type { IUploadedFileMeta } from 'src/stores/storage' +import type { IUploadedFileMeta } from 'oa-shared' export interface IProps { handleClick: () => Promise @@ -29,7 +27,6 @@ export const DownloadWithDonationAsk = (props: IProps) => { const [isModalOpen, setIsModalOpen] = useState(false) const [link, setLink] = useState('') const navigate = useNavigate() - const { themeStore } = useCommonStores().stores const toggleIsModalOpen = () => setIsModalOpen(!isModalOpen) @@ -45,10 +42,10 @@ export const DownloadWithDonationAsk = (props: IProps) => { return ( <> toggleIsModalOpen()} diff --git a/src/common/Error/handler.ts b/src/common/Error/handler.ts index e48d1ab784..6e4c8831a3 100644 --- a/src/common/Error/handler.ts +++ b/src/common/Error/handler.ts @@ -3,6 +3,7 @@ import * as Sentry from '@sentry/react' import { SENTRY_CONFIG } from '../../config/config' import { logger } from '../../logger' +// TODO: this can likely be removed after Remix migration export const initErrorHandler = () => { const { location } = window if ( diff --git a/src/common/Form/ImageInput/ImageConverter.tsx b/src/common/Form/ImageInput/ImageConverter.tsx index 5698bc4bce..312bf4e98c 100644 --- a/src/common/Form/ImageInput/ImageConverter.tsx +++ b/src/common/Form/ImageInput/ImageConverter.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react' import { Flex } from 'theme-ui' -import type { IConvertedFileMeta } from 'src/types' +import type { IConvertedFileMeta } from 'oa-shared' interface IProps { file: File diff --git a/src/common/Form/ImageInput/ImageInput.tsx b/src/common/Form/ImageInput/ImageInput.tsx index 9838bc15f4..b8aa0c599d 100644 --- a/src/common/Form/ImageInput/ImageInput.tsx +++ b/src/common/Form/ImageInput/ImageInput.tsx @@ -12,7 +12,7 @@ import { ImageInputWrapper } from './ImageInputWrapper' import { imageValid } from './imageValid' import { setSrc } from './setSrc' -import type { IConvertedFileMeta } from 'src/types' +import type { IConvertedFileMeta } from 'oa-shared' import type { ThemeUIStyleObject } from 'theme-ui' import type { IInputValue, IMultipleInputValue, IValue } from './types' diff --git a/src/common/Form/ImageInput/getPresentFiles.ts b/src/common/Form/ImageInput/getPresentFiles.ts index 08d3b4373d..a729c21ed9 100644 --- a/src/common/Form/ImageInput/getPresentFiles.ts +++ b/src/common/Form/ImageInput/getPresentFiles.ts @@ -3,8 +3,7 @@ * require extra function to separate out to handle preview of previously uploaded */ -import type { IConvertedFileMeta } from 'src/types' -import type { IUploadedFileMeta } from '../../../stores/storage' +import type { IConvertedFileMeta, IUploadedFileMeta } from 'oa-shared' import type { IMultipleInputValue, IValue } from './types' type Value = IValue | undefined diff --git a/src/common/Form/ImageInput/setSrc.ts b/src/common/Form/ImageInput/setSrc.ts index 9af12df9b7..2e2f663a69 100644 --- a/src/common/Form/ImageInput/setSrc.ts +++ b/src/common/Form/ImageInput/setSrc.ts @@ -1,5 +1,4 @@ -import type { IConvertedFileMeta } from 'src/types' -import type { IUploadedFileMeta } from '../../../stores/storage' +import type { IConvertedFileMeta, IUploadedFileMeta } from 'oa-shared' import type { IInputValue } from './types' export const setSrc = (file: IInputValue): string => { diff --git a/src/common/Form/ImageInput/types.ts b/src/common/Form/ImageInput/types.ts index c2ad1f797b..75099fa713 100644 --- a/src/common/Form/ImageInput/types.ts +++ b/src/common/Form/ImageInput/types.ts @@ -1,5 +1,4 @@ -import type { IConvertedFileMeta } from 'src/types' -import type { IUploadedFileMeta } from '../../../stores/storage' +import type { IConvertedFileMeta, IUploadedFileMeta } from 'oa-shared' export type IInputValue = IUploadedFileMeta | IConvertedFileMeta export type IMultipleInputValue = IInputValue[] diff --git a/src/common/Form/UnsavedChangesDialog.tsx b/src/common/Form/UnsavedChangesDialog.tsx index 853fd217bd..275f658b70 100644 --- a/src/common/Form/UnsavedChangesDialog.tsx +++ b/src/common/Form/UnsavedChangesDialog.tsx @@ -1,4 +1,4 @@ -import { useBlocker } from 'react-router' +import { useBlocker } from '@remix-run/react' import { ConfirmModal } from 'oa-components' const CONFIRM_DIALOG_MSG = diff --git a/src/common/ScrollToTop.test.tsx b/src/common/ScrollToTop.test.tsx index 9e2a10fbe5..dfe4d9b410 100644 --- a/src/common/ScrollToTop.test.tsx +++ b/src/common/ScrollToTop.test.tsx @@ -1,5 +1,5 @@ import { act } from 'react' -import { useLocation } from 'react-router-dom' +import { useLocation } from '@remix-run/react' import { render } from '@testing-library/react' import { describe, expect, it, vi } from 'vitest' @@ -7,8 +7,8 @@ import { ScrollToTop } from './ScrollToTop' import type { Mock } from 'vitest' -vi.mock('react-router-dom', async () => ({ - ...(await vi.importActual('react-router-dom')), +vi.mock('@remix-run/react', async () => ({ + ...(await vi.importActual('@remix-run/react')), useLocation: vi.fn(), })) diff --git a/src/common/ScrollToTop.tsx b/src/common/ScrollToTop.tsx index 77178f0c50..02e91a6461 100644 --- a/src/common/ScrollToTop.tsx +++ b/src/common/ScrollToTop.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react' -import { useLocation } from 'react-router-dom' +import { useLocation } from '@remix-run/react' export const ScrollToTop = () => { const { pathname } = useLocation() diff --git a/src/common/Tags/TagsList.tsx b/src/common/Tags/TagsList.tsx index 0382e26cec..76b2e57159 100644 --- a/src/common/Tags/TagsList.tsx +++ b/src/common/Tags/TagsList.tsx @@ -1,7 +1,7 @@ import { TagList as TagListUI } from 'oa-components' import { useCommonStores } from 'src/common/hooks/useCommonStores' -import type { ISelectedTags } from 'src/models' +import type { ISelectedTags } from 'oa-shared' interface IProps { tags: ISelectedTags | undefined diff --git a/src/common/Tags/TagsSelect.tsx b/src/common/Tags/TagsSelect.tsx index bb064ed4dd..25ab10b0fc 100644 --- a/src/common/Tags/TagsSelect.tsx +++ b/src/common/Tags/TagsSelect.tsx @@ -4,8 +4,8 @@ import { useCommonStores } from 'src/common/hooks/useCommonStores' import { FieldContainer } from '../Form/FieldContainer' +import type { ISelectedTags, ITag } from 'oa-shared' import type { FieldRenderProps } from 'react-final-form' -import type { ISelectedTags, ITag } from 'src/models/tags.model' // we include props from react-final-form fields so it can be used as a custom field component export interface IProps extends Partial> { diff --git a/src/common/hooks/contributorsData.ts b/src/common/hooks/contributorsData.ts index b203157aa9..be0b8856da 100644 --- a/src/common/hooks/contributorsData.ts +++ b/src/common/hooks/contributorsData.ts @@ -3,7 +3,7 @@ import { getUserCountry } from 'src/utils/getUserCountry' import { useCommonStores } from './useCommonStores' -import type { Collaborator } from '../../models/common.models' +import type { Collaborator } from 'oa-shared' export const useContributorsData = (collaborators: string[]) => { const { userStore } = useCommonStores().stores diff --git a/src/common/transformToUserComments.ts b/src/common/transformToUserComments.ts index c5c9fb4cd5..1d57bb8915 100644 --- a/src/common/transformToUserComments.ts +++ b/src/common/transformToUserComments.ts @@ -1,5 +1,5 @@ import { UserRole } from 'oa-shared' -import { type IComment, type IUserDB } from 'src/models' +import { type IComment, type IUserDB } from 'oa-shared' export const transformToUserComments = ( comments: IComment[], diff --git a/src/config/config.ts b/src/config/config.ts index f2843f0011..e611499b44 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -10,28 +10,19 @@ Dev config is hardcoded - You can find more information about potential security https://javebratt.com/hide-firebase-api/ *****************************************************************************************/ -import type { UserRole } from 'oa-shared' import type { ConfigurationOption } from './constants' import type { IFirebaseConfig, ISentryConfig, siteVariants } from './types' /** * Helper function to load configuration property * from the global configuration object - * During the development cycle this will be process.env - * when running this application with the output of `yarn build` - * we will instead load from the global window * * @param property * @param fallbackValue - optional fallback value * @returns string */ const _c = (property: ConfigurationOption, fallbackValue?: string): string => { - const configurationSource = ['development', 'test'].includes( - process.env.NODE_ENV || '', - ) - ? process.env - : window?.__OA_COMMUNITY_PLATFORM_CONFIGURATION - return configurationSource?.[property] || fallbackValue + return import.meta.env?.[property] || fallbackValue || '' } export const getConfigurationOption = _c @@ -41,12 +32,13 @@ export const getConfigurationOption = _c /********************************************************************************************** */ // On dev sites user can override default role -const devSiteRole: UserRole = localStorage.getItem('devSiteRole') as UserRole +// const devSiteRole: UserRole = localStorage.getItem('devSiteRole') as UserRole const getSiteVariant = (): siteVariants => { - const devSiteVariant: siteVariants = localStorage.getItem( - 'devSiteVariant', - ) as any + const devSiteVariant: siteVariants = + typeof localStorage !== 'undefined' && + localStorage && + (localStorage.getItem('devSiteVariant') as any) if (devSiteVariant === 'preview') { return 'preview' @@ -57,19 +49,20 @@ const getSiteVariant = (): siteVariants => { if (devSiteVariant === 'dev_site') { return 'dev_site' } - if (location.host === 'localhost:4000') { + + if (typeof location !== 'undefined' && location.host === 'localhost:4000') { return 'emulated_site' } if ( - location.host === 'localhost:3456' || - _c('REACT_APP_SITE_VARIANT') === 'test-ci' + (typeof location !== 'undefined' && location.host === 'localhost:3456') || + _c('VITE_SITE_VARIANT') === 'test-ci' ) { return 'test-ci' } - if (_c('REACT_APP_SITE_VARIANT') === 'preview') { + if (_c('VITE_SITE_VARIANT') === 'preview') { return 'preview' } - switch (_c('REACT_APP_BRANCH')) { + switch (_c('VITE_BRANCH')) { case 'production': return 'production' case 'master': @@ -131,12 +124,12 @@ const firebaseConfigs: { [variant in siteVariants]: IFirebaseConfig } = { }, /** Production/live backend with released frontend */ production: { - apiKey: _c('REACT_APP_FIREBASE_API_KEY'), - authDomain: _c('REACT_APP_FIREBASE_AUTH_DOMAIN'), - databaseURL: _c('REACT_APP_FIREBASE_DATABASE_URL'), - messagingSenderId: _c('REACT_APP_FIREBASE_MESSAGING_SENDER_ID'), - projectId: _c('REACT_APP_FIREBASE_PROJECT_ID'), - storageBucket: _c('REACT_APP_FIREBASE_STORAGE_BUCKET'), + apiKey: _c('VITE_FIREBASE_API_KEY'), + authDomain: _c('VITE_FIREBASE_AUTH_DOMAIN'), + databaseURL: _c('VITE_FIREBASE_DATABASE_URL'), + messagingSenderId: _c('VITE_FIREBASE_MESSAGING_SENDER_ID'), + projectId: _c('VITE_FIREBASE_PROJECT_ID'), + storageBucket: _c('VITE_FIREBASE_STORAGE_BUCKET'), }, } /*********************************************************************************************** / @@ -144,28 +137,29 @@ const firebaseConfigs: { [variant in siteVariants]: IFirebaseConfig } = { /********************************************************************************************** */ export const SITE = siteVariant -export const DEV_SITE_ROLE = devSiteRole +// export const DEV_SITE_ROLE = devSiteRole export const FIREBASE_CONFIG = firebaseConfigs[siteVariant] export const SENTRY_CONFIG: ISentryConfig = { dsn: _c( - 'REACT_APP_SENTRY_DSN', + 'VITE_SENTRY_DSN', 'https://8c1f7eb4892e48b18956af087bdfa3ac@sentry.io/1399729', ), environment: siteVariant, } -export const CDN_URL = _c('REACT_APP_CDN_URL', '') -export const VERSION = _c('REACT_APP_PROJECT_VERSION', '') -export const GA_TRACKING_ID = _c('REACT_APP_GA_TRACKING_ID') -export const PATREON_CLIENT_ID = _c('REACT_APP_PATREON_CLIENT_ID') +export const CDN_URL = _c('VITE_CDN_URL', '') +export const VERSION = _c('VITE_PROJECT_VERSION', '') +export const GA_TRACKING_ID = _c('VITE_GA_TRACKING_ID') +export const PATREON_CLIENT_ID = _c('VITE_PATREON_CLIENT_ID') export const API_URL = _c( - 'REACT_APP_API_URL', + 'VITE_API_URL', 'https://platform-api-voymtdup6a-uc.a.run.app', ) export const isPreciousPlastic = (): boolean => { return ( - (_c('REACT_APP_PLATFORM_THEME') || + _c('VITE_PLATFORM_THEME') === 'precious-plastic' || + (typeof localStorage !== 'undefined' && localStorage.getItem('platformTheme')) === 'precious-plastic' ) } diff --git a/src/config/constants.ts b/src/config/constants.ts index 6ed2c6e866..f53999ccbf 100644 --- a/src/config/constants.ts +++ b/src/config/constants.ts @@ -5,28 +5,28 @@ * type exported from this file */ export const _supportedConfigurationOptions = [ - 'REACT_APP_SENTRY_DSN', - 'REACT_APP_PROJECT_VERSION', - 'REACT_APP_GA_TRACKING_ID', - 'REACT_APP_FIREBASE_API_KEY', - 'REACT_APP_FIREBASE_AUTH_DOMAIN', - 'REACT_APP_FIREBASE_DATABASE_URL', - 'REACT_APP_FIREBASE_MESSAGING_SENDER_ID', - 'REACT_APP_FIREBASE_PROJECT_ID', - 'REACT_APP_FIREBASE_STORAGE_BUCKET', - 'REACT_APP_ALGOLIA_PLACES_APP_ID', - 'REACT_APP_ALGOLIA_PLACES_API_KEY', - 'REACT_APP_BRANCH', - 'REACT_APP_SITE_VARIANT', - 'REACT_APP_LOG_LEVEL', - 'REACT_APP_LOG_TRANSPORT', - 'REACT_APP_SUPPORTED_MODULES', - 'REACT_APP_PLATFORM_THEME', - 'REACT_APP_PLATFORM_PROFILES', - 'REACT_APP_CDN_URL', - 'REACT_APP_PATREON_CLIENT_ID', - 'REACT_APP_API_URL', - 'SITE_NAME', + 'VITE_SENTRY_DSN', + 'VITE_PROJECT_VERSION', + 'VITE_GA_TRACKING_ID', + 'VITE_FIREBASE_API_KEY', + 'VITE_FIREBASE_AUTH_DOMAIN', + 'VITE_FIREBASE_DATABASE_URL', + 'VITE_FIREBASE_MESSAGING_SENDER_ID', + 'VITE_FIREBASE_PROJECT_ID', + 'VITE_FIREBASE_STORAGE_BUCKET', + 'VITE_ALGOLIA_PLACES_APP_ID', + 'VITE_ALGOLIA_PLACES_API_KEY', + 'VITE_BRANCH', + 'VITE_SITE_VARIANT', + 'VITE_LOG_LEVEL', + 'VITE_LOG_TRANSPORT', + 'VITE_SUPPORTED_MODULES', + 'VITE_PLATFORM_THEME', + 'VITE_PLATFORM_PROFILES', + 'VITE_CDN_URL', + 'VITE_PATREON_CLIENT_ID', + 'VITE_API_URL', + 'VITE_SITE_NAME', ] as const export type ConfigurationOption = diff --git a/src/config/devSiteConfig.ts b/src/config/devSiteConfig.ts new file mode 100644 index 0000000000..eea1c2778c --- /dev/null +++ b/src/config/devSiteConfig.ts @@ -0,0 +1,4 @@ +import type { UserRole } from 'oa-shared' + +export const getDevSiteRole = () => + localStorage.getItem('devSiteRole') as UserRole diff --git a/src/config/types.ts b/src/config/types.ts index 7dbe82db52..fbe8220d8d 100644 --- a/src/config/types.ts +++ b/src/config/types.ts @@ -13,13 +13,6 @@ export interface ISentryConfig { environment: string } -declare global { - interface Window { - // eslint-disable-next-line @typescript-eslint/naming-convention - __OA_COMMUNITY_PLATFORM_CONFIGURATION: any - } -} - export type siteVariants = | 'emulated_site' | 'dev_site' diff --git a/src/entry.client.tsx b/src/entry.client.tsx new file mode 100644 index 0000000000..5b1a132c69 --- /dev/null +++ b/src/entry.client.tsx @@ -0,0 +1,32 @@ +import { startTransition, useCallback, useState } from 'react' +import { hydrateRoot } from 'react-dom/client' +import { CacheProvider } from '@emotion/react' +import { RemixBrowser } from '@remix-run/react' +import * as Sentry from '@sentry/remix' + +import { SENTRY_CONFIG } from './config/config' +import { ClientStyleContext } from './styles/context' +import createEmotionCache from './styles/createEmotionCache' + +Sentry.init({ ...SENTRY_CONFIG, autoInstrumentRemix: true }) + +const ClientCacheProvider = ({ children }) => { + const [cache, setCache] = useState(createEmotionCache()) + + const reset = useCallback(() => setCache(createEmotionCache()), []) + + return ( + + {children} + + ) +} + +startTransition(() => { + hydrateRoot( + document, + + + , + ) +}) diff --git a/src/entry.server.tsx b/src/entry.server.tsx new file mode 100644 index 0000000000..60dcc5f9f9 --- /dev/null +++ b/src/entry.server.tsx @@ -0,0 +1,143 @@ +import { renderToPipeableStream, renderToString } from 'react-dom/server' +import { CacheProvider } from '@emotion/react' +import createEmotionServer from '@emotion/server/create-instance' +import { createReadableStreamFromReadable } from '@remix-run/node' +import { RemixServer } from '@remix-run/react' +import * as Sentry from '@sentry/remix' +import { isbot } from 'isbot' +import { PassThrough } from 'node:stream' + +import { SENTRY_CONFIG } from './config/config' +import { ServerStyleContext } from './styles/context' +import createEmotionCache from './styles/createEmotionCache' + +import type { EntryContext } from '@remix-run/node' + +const ABORT_DELAY = 5_000 + +Sentry.init({ ...SENTRY_CONFIG, autoInstrumentRemix: true }) + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + // loadContext: AppLoadContext, +) { + return isbot(request.headers.get('user-agent') || '') + ? handleBotRequest( + request, + responseStatusCode, + responseHeaders, + remixContext, + ) + : handleBrowserRequest( + request, + responseStatusCode, + responseHeaders, + remixContext, + ) +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + const body = new PassThrough() + + responseHeaders.set('Content-Type', 'text/html') + + resolve( + new Response(createReadableStreamFromReadable(body), { + headers: responseHeaders, + status: responseStatusCode, + }), + ) + + pipe(body) + }, + onShellError(error: unknown) { + reject(error) + }, + onError(error: unknown) { + responseStatusCode = 500 + // eslint-disable-next-line no-console + console.error(error) + }, + }, + ) + + setTimeout(abort, ABORT_DELAY) + }) +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + const cache = createEmotionCache() + const { extractCriticalToChunks } = createEmotionServer(cache) + + const html = renderToString( + + + + + , + ) + + const chunks = extractCriticalToChunks(html) + + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + + + + + , + { + onShellReady() { + const body = new PassThrough() + + responseHeaders.set('Content-Type', 'text/html') + + resolve( + new Response(createReadableStreamFromReadable(body), { + headers: responseHeaders, + status: responseStatusCode, + }), + ) + + pipe(body) + }, + onShellError(error: unknown) { + reject(error) + }, + onError(error: unknown) { + // eslint-disable-next-line no-console + console.error(error) + responseStatusCode = 500 + }, + }, + ) + + setTimeout(abort, ABORT_DELAY) + }) +} diff --git a/src/index.tsx b/src/index.tsx deleted file mode 100644 index 311e8dbef7..0000000000 --- a/src/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { createRoot } from 'react-dom/client' - -import { initErrorHandler } from './common/Error/handler' -import { App } from './App' - -initErrorHandler() - -const container = document.getElementById('root') -const root = createRoot(container!) -root.render() diff --git a/src/logger/index.ts b/src/logger/index.ts index e09bd0be28..407014d049 100644 --- a/src/logger/index.ts +++ b/src/logger/index.ts @@ -2,8 +2,8 @@ import { Logger } from 'tslog' import { getConfigurationOption } from '../config/config' -const logLevel = getConfigurationOption('REACT_APP_LOG_LEVEL', 'info') -const logTransport = getConfigurationOption('REACT_APP_LOG_TRANSPORT', 'none') +const logLevel = getConfigurationOption('VITE_LOG_LEVEL', 'info') +const logTransport = getConfigurationOption('VITE_LOG_TRANSPORT', 'none') const levelNumberToNameMap = { silly: 0, diff --git a/src/models/common.models.tsx b/src/models/common.models.tsx deleted file mode 100644 index 1bac0512b8..0000000000 --- a/src/models/common.models.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// A reminder that dates should be saved in the ISOString format -// i.e. new Date().toISOString() => 2011-10-05T14:48:00.000Z -// This is more consistent than others and allows better querying -export type ISODateString = string - -export type UserMention = { - username: string - location: string -} - -export type Collaborator = { - countryCode?: string | null - userName: string - isVerified: boolean -} - -export interface ILocation { - name: string - country: string - countryCode: string - administrative: string - latlng: ILatLng - postcode: string - value: string -} -interface ILatLng { - lat: number - lng: number -} - -export type FetchState = 'idle' | 'fetching' | 'completed' diff --git a/src/models/dbDoc.model.ts b/src/models/dbDoc.model.ts deleted file mode 100644 index 0524a939aa..0000000000 --- a/src/models/dbDoc.model.ts +++ /dev/null @@ -1,3 +0,0 @@ -import type { DBDoc as DBDocImport } from '../stores/databaseV2/types/dbDoc' - -export type DBDoc = DBDocImport diff --git a/src/models/howto.models.tsx b/src/models/howto.models.tsx deleted file mode 100644 index afadc8d3a6..0000000000 --- a/src/models/howto.models.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import type { DifficultyLevel } from 'oa-shared' -import type { IUploadedFileMeta } from '../stores/storage' -import type { IConvertedFileMeta } from '../types' -import type { ICategory } from './categories.model' -import type { UserMention } from './common.models' -import type { DBDoc } from './dbDoc.model' -import type { IModerable } from './moderation.model' -import type { ISelectedTags } from './tags.model' -import type { ISharedFeatures } from './voteUseful.model' - -// By default all how-to form input fields come as strings -// The IHowto interface can imposes the correct formats on fields -// Additionally convert from local filemeta to uploaded filemeta -export interface IHowto extends IHowtoFormInput { - _createdBy: string - _deleted: boolean - cover_image?: IUploadedFileMeta - fileLink?: string - total_downloads?: number - latestCommentDate?: string | undefined - mentions: UserMention[] - previousSlugs: string[] - totalComments: number - totalUsefulVotes?: number - keywords?: string[] -} - -/** - * Howtos retrieved from the database also include metadata such as _id, _created and _modified - */ -export type IHowtoDB = IHowto & DBDoc - -export interface IHowtoStep extends IHowToStepFormInput { - // *** NOTE - adding an '_animationKey' field to track when specific array element removed for - images?: Array - videoUrl?: string - title?: string - text?: string - _animationKey?: string -} - -export interface IHowToStepFormInput { - images?: Array - title?: string - text?: string - videoUrl?: string - _animationKey?: string -} - -export interface IHowtoFormInput extends IModerable, ISharedFeatures { - slug: string - title: string - allowDraftSave?: boolean - category?: ICategory - // NOTE cover image input starts as convertedFileMeta but is transformed on upload - cover_image?: IUploadedFileMeta | IConvertedFileMeta - // Added to be able to recover on edit by admin - creatorCountry?: string - totalComments?: number - latestCommentDate?: string - description?: string - difficulty_level?: DifficultyLevel - files?: Array - fileLink?: string - mentions?: UserMention[] - steps: IHowToStepFormInput[] - // note, tags will remain optional as if populated {} will be stripped by db (firestore) - tags?: ISelectedTags - time?: string -} diff --git a/src/models/index.ts b/src/models/index.ts index 57f5dea5b6..e8082bf46f 100644 --- a/src/models/index.ts +++ b/src/models/index.ts @@ -1,19 +1,2 @@ -import type { IComment } from './discussion.models' - -export * from './common.models' -export * from './discussion.models' -export * from './howto.models' -export * from './maps.models' -export * from './message.models' -export * from './notifications.models' export * from './project.models' -export * from './question.models' -export * from './research.models' export * from './selectorList.models' -export * from './tags.model' -export * from './user.models' -export * from './moderation.model' - -export interface UserComment extends IComment { - isEditable: boolean -} diff --git a/src/models/maps.models.tsx b/src/models/maps.models.tsx deleted file mode 100644 index 99f77da2dc..0000000000 --- a/src/models/maps.models.tsx +++ /dev/null @@ -1,54 +0,0 @@ -import type { - ILatLng, - IMapPinDetail, - IModerationStatus, - IPinGrouping, - IProfileCreator, - ProfileTypeName, - WorkspaceType, -} from 'oa-shared' - -/** - * Map pins keep minimal information required for pin display. - * @param _id - The id that will be used to pull pin details - * currently this is a user profile id - * @param type - Pin type icon to use - currently mapped to profile types - * @param subtype - currently used for workspacetype filtering - */ - -export type IMapPinSubtype = WorkspaceType - -/** - * Map pins have a `type` which correspond to icon - * They can also optionally have a subtype for additional filtering - */ -export interface IMapPin { - moderation: IModerationStatus - _createdBy?: string - _id: string - _deleted: boolean - type: ProfileTypeName - location: ILatLng - verified: boolean - subType?: IMapPinSubtype - comments?: string - creator?: IProfileCreator -} - -/** - * @param detail - by default details are pulled on pin open, using - * the pin _id param as the user profile ID required for lookup - */ -export interface IMapPinWithDetail extends IMapPin { - detail: IMapPinDetail -} - -export interface IMapGrouping { - _count?: number - grouping: IPinGrouping - displayName: string - type: ProfileTypeName - subType?: IMapPinSubtype - icon: string - hidden?: boolean -} diff --git a/src/models/message.models.tsx b/src/models/message.models.tsx deleted file mode 100644 index e91e6f536b..0000000000 --- a/src/models/message.models.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import type { DBDoc } from './dbDoc.model' -import type { IUser } from './user.models' - -export type IMessageDB = DBDoc & IMessage - -export interface IMessage extends IMessageInput { - isSent: boolean -} - -export interface IMessageInput { - email: string - text: string - toUserName: IUser['userName'] - name?: string -} diff --git a/src/models/notifications.models.tsx b/src/models/notifications.models.tsx deleted file mode 100644 index 18cd25132b..0000000000 --- a/src/models/notifications.models.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import type { INotification, INotificationSettings } from './user.models' - -export interface IPendingEmails { - _authID: string - _userId: string - emailFrequency?: INotificationSettings['emailFrequency'] - notifications: INotification[] -} diff --git a/src/models/project.models.tsx b/src/models/project.models.tsx index eec82305df..1d356a0188 100644 --- a/src/models/project.models.tsx +++ b/src/models/project.models.tsx @@ -1,4 +1,4 @@ -import type { IUser } from './user.models' +import type { IUser } from 'oa-shared' export interface IProject { id: string diff --git a/src/models/questionCategories.model.ts b/src/models/questionCategories.model.ts deleted file mode 100644 index 4ded1d6494..0000000000 --- a/src/models/questionCategories.model.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { DBDoc } from './dbDoc.model' - -export type ISelectedQuestionCategories = Record - -export interface IQuestionCategory extends DBDoc { - label: string -} diff --git a/src/models/research.models.tsx b/src/models/research.models.tsx deleted file mode 100644 index b79cc779be..0000000000 --- a/src/models/research.models.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import { ResearchStatus } from 'oa-shared' - -import type { ResearchUpdateStatus } from 'oa-shared' -import type { IUploadedFileMeta } from '../stores/storage' -import type { IConvertedFileMeta } from '../types' -import type { UserMention } from './common.models' -import type { DBDoc } from './dbDoc.model' -import type { IModerable } from './moderation.model' -import type { IResearchCategory } from './researchCategories.model' -import type { ISelectedTags } from './tags.model' -import type { ISharedFeatures } from './voteUseful.model' - -/** - * Research retrieved from the database also include metadata such as _id, _created and _modified - */ -export type IResearchDB = DBDoc & IResearch.ItemDB - -export type IResearchStats = { - votedUsefulCount: number -} - -type UserId = string -type DateString = string - -type ResearchDocumentLockInformation = { - by: UserId - at: DateString -} - -type ResearchDocumentLock = ResearchDocumentLockInformation | null - -export const researchStatusOptions = ( - Object.keys(ResearchStatus) as (keyof typeof ResearchStatus)[] -).map((status) => { - return { - label: ResearchStatus[status], - value: ResearchStatus[status], - } -}) - -type UserIdList = UserId[] - -export namespace IResearch { - /** The main research item, as created by a user */ - export type Item = { - updates: Update[] - mentions?: UserMention[] - _createdBy: string - collaborators: string[] - subscribers?: UserIdList - locked?: ResearchDocumentLock - totalUpdates?: number - totalUsefulVotes?: number - totalCommentCount: number - keywords?: string[] - } & Omit & - DBDoc - - /** A research item update */ - export type Update = { - title: string - description: string - images: Array - files: Array - fileLink: string - downloadCount: number - videoUrl?: string - collaborators?: string[] - commentCount?: number - status?: ResearchUpdateStatus - researchStatus?: ResearchStatus - locked?: ResearchDocumentLock - _id: string - } & DBDoc - - export interface FormInput extends IModerable, ISharedFeatures { - title: string - description: string - researchCategory?: IResearchCategory - slug: string - tags: ISelectedTags - creatorCountry?: string - collaborators: string - previousSlugs?: string[] - researchStatus?: ResearchStatus - } - - /** Research items synced from the database will contain additional metadata */ - // Use of Omit to override the 'updates' type to UpdateDB - export type ItemDB = Omit & { - totalCommentCount: number - updates: UpdateDB[] - } & DBDoc - - export type UpdateDB = Update & DBDoc -} diff --git a/src/models/researchCategories.model.ts b/src/models/researchCategories.model.ts deleted file mode 100644 index c68ab27249..0000000000 --- a/src/models/researchCategories.model.ts +++ /dev/null @@ -1,7 +0,0 @@ -import type { DBDoc } from './dbDoc.model' - -export type ISelectedResearchCategories = Record - -export interface IResearchCategory extends DBDoc { - label: string -} diff --git a/src/models/user.models.tsx b/src/models/user.models.tsx deleted file mode 100644 index bdfedcd964..0000000000 --- a/src/models/user.models.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import type { - EmailNotificationFrequency, - ExternalLinkLabel, - IMAchineBuilderXp, - IModerationStatus, - IOpeningHours, - NotificationType, - PatreonUser, - PlasticTypeLabel, - ProfileTypeName, - UserRole, - WorkspaceType, -} from 'oa-shared' -import type { IUploadedFileMeta } from '../stores/storage' -import type { ILocation, ISODateString } from './common.models' -import type { DBDoc } from './dbDoc.model' - -// IUser retains most of the fields from legacy users (omitting passwords), -// and has a few additional fields. Note 'email' is excluded -// _uid is unique/fixed identifier -// ALL USER INFO BELOW IS PUBLIC -export interface IUser { - // authID is additional id populated by firebase auth, required for some auth operations - _authID: string - _lastActive?: ISODateString - // userName is same as legacy 'mention_name', e.g. @my-name. It will also be the doc _id and - // firebase auth displayName property - userName: string - displayName: string - verified: boolean - badges?: IUserBadges - // images will be in different formats if they are pending upload vs pulled from db - coverImages: IUploadedFileMeta[] - userImage?: IUploadedFileMeta - links: IExternalLink[] - userRoles?: UserRole[] - about?: string | null - country?: string | null - location?: ILocation | null - year?: ISODateString - stats?: IUserStats - notification_settings?: INotificationSettings - notifications?: INotification[] - profileCreated?: ISODateString - profileCreationTrigger?: string - // Used to generate an encrypted unsubscribe url in emails - unsubscribeToken?: string | null - impact?: IUserImpact - isBlockedFromMessaging?: boolean - isContactableByPublic?: boolean - patreon?: PatreonUser | null - totalUseful?: number - - // Primary PP profile type related fields - profileType: ProfileTypeName - subType?: WorkspaceType | null - workspaceType?: WorkspaceType | null - mapPinDescription?: string | null - openingHours?: IOpeningHours[] - collectedPlasticTypes?: PlasticTypeLabel[] | null - machineBuilderXp?: IMAchineBuilderXp[] | null - isExpert?: boolean | null - isV4Member?: boolean | null -} - -export interface IUserImpact { - [key: number]: IImpactYearFieldList -} - -export interface IImpactDataField { - id: string - value: number - isVisible: boolean -} - -export type IImpactYearFieldList = IImpactDataField[] - -export type IImpactYear = 2019 | 2020 | 2021 | 2022 | 2023 - -export interface IUserBadges { - verified?: boolean - supporter?: boolean -} - -export interface IExternalLink { - key: string - url: string - label: ExternalLinkLabel -} - -/** - * Track the ids and moderation status as summary for user stats - */ -interface IUserStats { - userCreatedHowtos: { [id: string]: IModerationStatus } - userCreatedResearch: { [id: string]: IModerationStatus } - userCreatedQuestions: { [id: string]: IModerationStatus } - userCreatedComments: { [id: string]: string | null } -} - -export type IUserDB = IUser & DBDoc - -export interface INotification { - _id: string - _created: string - triggeredBy: { - displayName: string - // this field is the userName of the user, which we use as a unique id as of https://github.com/ONEARMY/community-platform/pull/2479/files - userId: string - } - relevantUrl?: string - type: NotificationType - read: boolean - notified: boolean - // email contains the id of the doc in the emails collection if the notification was included in - // an email or 'failed' if an email with this notification was attempted and encountered an error - email?: string - title?: string -} - -export type INotificationSettings = { - enabled?: { - [T in NotificationType]: boolean - } - emailFrequency: EmailNotificationFrequency -} - -export type INotificationUpdate = { - _id: string - notifications?: INotification[] -} diff --git a/src/modules/index.test.ts b/src/modules/index.test.ts index 44ee96e3f6..74e4373456 100644 --- a/src/modules/index.test.ts +++ b/src/modules/index.test.ts @@ -3,13 +3,13 @@ import { afterAll, describe, expect, it } from 'vitest' import { getSupportedModules, isModuleSupported, MODULE } from './index' describe('getSupportedModules', () => { - const defaultModules = import.meta.env.REACT_APP_SUPPORTED_MODULES + const defaultModules = import.meta.env.VITE_SUPPORTED_MODULES afterAll(() => { - import.meta.env.REACT_APP_SUPPORTED_MODULES = defaultModules + import.meta.env.VITE_SUPPORTED_MODULES = defaultModules }) it('returns a default set of modules', () => { - import.meta.env.REACT_APP_SUPPORTED_MODULES = '' + import.meta.env.VITE_SUPPORTED_MODULES = '' expect(getSupportedModules()).toStrictEqual([ MODULE.CORE, MODULE.HOWTO, @@ -22,17 +22,17 @@ describe('getSupportedModules', () => { }) it('loads an additional module based on env configuration', () => { - import.meta.env.REACT_APP_SUPPORTED_MODULES = ` ${MODULE.HOWTO} ` + import.meta.env.VITE_SUPPORTED_MODULES = ` ${MODULE.HOWTO} ` expect(getSupportedModules()).toStrictEqual([MODULE.CORE, MODULE.HOWTO]) }) it('loads multiple modules based on env configuration', () => { - import.meta.env.REACT_APP_SUPPORTED_MODULES = ` ${MODULE.HOWTO} ` + import.meta.env.VITE_SUPPORTED_MODULES = ` ${MODULE.HOWTO} ` expect(getSupportedModules()).toStrictEqual([MODULE.CORE, MODULE.HOWTO]) }) it('ignores a malformed module definitions', () => { - import.meta.env.REACT_APP_SUPPORTED_MODULES = `fake module,${MODULE.HOWTO},malicious ` + import.meta.env.VITE_SUPPORTED_MODULES = `fake module,${MODULE.HOWTO},malicious ` expect(getSupportedModules()).toStrictEqual([MODULE.CORE, MODULE.HOWTO]) }) }) @@ -43,12 +43,12 @@ describe('isModuleSupported', () => { }) it('returns true for module enabled via env', () => { - import.meta.env.REACT_APP_SUPPORTED_MODULES = `${MODULE.RESEARCH}` + import.meta.env.VITE_SUPPORTED_MODULES = `${MODULE.RESEARCH}` expect(isModuleSupported(MODULE.RESEARCH)).toBe(true) }) it('returns false for unsupported module', () => { - import.meta.env.REACT_APP_SUPPORTED_MODULES = `${MODULE.HOWTO}` + import.meta.env.VITE_SUPPORTED_MODULES = `${MODULE.HOWTO}` expect(isModuleSupported(MODULE.RESEARCH)).toBe(false) }) }) diff --git a/src/modules/index.ts b/src/modules/index.ts index aa767701a3..dc79e9bea4 100644 --- a/src/modules/index.ts +++ b/src/modules/index.ts @@ -13,7 +13,7 @@ export enum MODULE { export const getSupportedModules = (): MODULE[] => { const envModules: string[] = getConfigurationOption( - 'REACT_APP_SUPPORTED_MODULES', + 'VITE_SUPPORTED_MODULES', 'howto,map,research,academy,user,question', ) .split(',') diff --git a/src/modules/profile/SupportedProfileTypesFactory.ts b/src/modules/profile/SupportedProfileTypesFactory.ts index 57232deca8..497d9e2094 100644 --- a/src/modules/profile/SupportedProfileTypesFactory.ts +++ b/src/modules/profile/SupportedProfileTypesFactory.ts @@ -10,7 +10,6 @@ import LogoWorkspaceVerified from 'src/assets/icons/map-workspace-verified.svg' import CollectionBadge from 'src/assets/images/badges/pt-collection-point.svg' import LocalComBadge from 'src/assets/images/badges/pt-local-community.svg' import MachineBadge from 'src/assets/images/badges/pt-machine-shop.svg' -import SpaceBadge from 'src/assets/images/badges/pt-space.svg' import WorkspaceBadge from 'src/assets/images/badges/pt-workspace.svg' import FixingFashionMember from 'src/assets/images/themes/fixing-fashion/avatar_member_sm.svg' import FixingFashionSpace from 'src/assets/images/themes/fixing-fashion/avatar_space_sm.svg' @@ -18,7 +17,6 @@ import PreciousPlasticMember from 'src/assets/images/themes/precious-plastic/ava import ProjectKampMember from 'src/assets/images/themes/project-kamp/avatar_member_sm.svg' import ProjectKampSpace from 'src/assets/images/themes/project-kamp/avatar_space_sm.svg' -import type { PlatformTheme } from 'oa-themes' import type { IProfileTypeDetails } from './types' const DEFAULT_PROFILE_TYPES = @@ -38,34 +36,26 @@ const MemberAndSpace = { space: ProjectKampSpace, }, } +type ProfileType = keyof typeof MemberAndSpace + +const getProfileTypes = () => { + const theme: ProfileType = + import.meta.env.VITE_THEME || process.env.VITE_THEME || 'precious-plastic' -const getProfileTypes = (currentTheme?: PlatformTheme) => { const PROFILE_TYPES: IProfileTypeDetails[] = [ { label: ProfileTypeList.MEMBER, textLabel: 'I am a member', - imageSrc: currentTheme - ? MemberAndSpace[currentTheme.id].member - : PreciousPlasticMember, - cleanImageSrc: currentTheme - ? MemberAndSpace[currentTheme.id].member - : PreciousPlasticMember, - cleanImageVerifiedSrc: currentTheme - ? MemberAndSpace[currentTheme.id].member - : PreciousPlasticMember, + imageSrc: MemberAndSpace[theme].member, + cleanImageSrc: MemberAndSpace[theme].member, + cleanImageVerifiedSrc: MemberAndSpace[theme].member, }, { label: ProfileTypeList.SPACE, textLabel: 'I run a space', - imageSrc: currentTheme - ? MemberAndSpace[currentTheme.id].space - : SpaceBadge, - cleanImageSrc: currentTheme - ? MemberAndSpace[currentTheme.id].space - : SpaceBadge, - cleanImageVerifiedSrc: currentTheme - ? MemberAndSpace[currentTheme.id].space - : SpaceBadge, + imageSrc: MemberAndSpace[theme].space, + cleanImageSrc: MemberAndSpace[theme].space, + cleanImageVerifiedSrc: MemberAndSpace[theme].space, }, { label: ProfileTypeList.WORKSPACE, @@ -100,15 +90,12 @@ const getProfileTypes = (currentTheme?: PlatformTheme) => { return PROFILE_TYPES } -export const SupportedProfileTypesFactory = ( - configurationString: string, - currentTheme?: PlatformTheme, -) => { +export const SupportedProfileTypesFactory = (configurationString: string) => { const supportedProfileTypes = (configurationString || DEFAULT_PROFILE_TYPES) .split(',') .map((s) => s.trim()) return () => - getProfileTypes(currentTheme).filter(({ label }) => + getProfileTypes().filter(({ label }) => supportedProfileTypes.includes(label), ) } diff --git a/src/modules/profile/index.ts b/src/modules/profile/index.ts index 1c28723c13..b8037b4c3d 100644 --- a/src/modules/profile/index.ts +++ b/src/modules/profile/index.ts @@ -1,12 +1,9 @@ import { getConfigurationOption } from '../../config/config' import { SupportedProfileTypesFactory } from './SupportedProfileTypesFactory' -import type { PlatformTheme } from 'oa-themes' - -export const getSupportedProfileTypes = (currentTheme?: PlatformTheme) => { +export const getSupportedProfileTypes = () => { const supportedProfileTypes = SupportedProfileTypesFactory( - getConfigurationOption('REACT_APP_PLATFORM_PROFILES', ''), - currentTheme, + getConfigurationOption('VITE_PLATFORM_PROFILES', ''), )() return supportedProfileTypes diff --git a/src/pages/Academy/Academy.test.tsx b/src/pages/Academy/Academy.test.tsx index 4268bb1e02..13bb8e2133 100644 --- a/src/pages/Academy/Academy.test.tsx +++ b/src/pages/Academy/Academy.test.tsx @@ -1,18 +1,7 @@ -import { describe, expect, it, vi } from 'vitest' +import { describe, expect, it } from 'vitest' import { getFrameSrc } from './Academy' -// Mock out the useCommonStores method -// to prevent excessive amount of application -// being instantiated as part of the loading process -// This is a code smell, which needs to be resolved but -// is out of scope for the current task. -vi.mock('src/common/hooks/useCommonStores', () => { - return { - useCommonStores: vi.fn(), - } -}) - describe('getFrameSrc', () => { const basePath = `https://example.com/` diff --git a/src/pages/Academy/Academy.tsx b/src/pages/Academy/Academy.tsx index d97160c8c4..fd2178a9fb 100644 --- a/src/pages/Academy/Academy.tsx +++ b/src/pages/Academy/Academy.tsx @@ -1,22 +1,25 @@ import React from 'react' -import { useLocation } from 'react-router-dom' -import { useCommonStores } from 'src/common/hooks/useCommonStores' +import { useLocation } from '@remix-run/react' import ExternalEmbed from 'src/pages/Academy/ExternalEmbed/ExternalEmbed' -export const getFrameSrc = (base, path): string => +export const getFrameSrc = (base: string, path: string): string => `${base}${path .split('/') .filter((str) => str !== 'academy' && Boolean(str)) .join('/')}` const Academy = () => { - const { stores } = useCommonStores() const location = useLocation() - const src = stores.themeStore.currentTheme.academyResource return ( - // NOTE - for embed to work github.io site also must host at same path, i.e. /academy - + ) } diff --git a/src/pages/Academy/ExternalEmbed/ExternalEmbed.tsx b/src/pages/Academy/ExternalEmbed/ExternalEmbed.tsx index 5898ab56b9..36645a8498 100644 --- a/src/pages/Academy/ExternalEmbed/ExternalEmbed.tsx +++ b/src/pages/Academy/ExternalEmbed/ExternalEmbed.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@remix-run/react' /************************************************************************************* * Embed an Iframe diff --git a/src/pages/Howto/Content/Common/Howto.form.test.tsx b/src/pages/Howto/Content/Common/Howto.form.test.tsx index dc994434f0..0ca53e2146 100644 --- a/src/pages/Howto/Content/Common/Howto.form.test.tsx +++ b/src/pages/Howto/Content/Common/Howto.form.test.tsx @@ -1,12 +1,7 @@ import '@testing-library/jest-dom/vitest' -import { - createMemoryRouter, - createRoutesFromElements, - Route, - RouterProvider, -} from 'react-router-dom' import { ThemeProvider } from '@emotion/react' +import { createRemixStub } from '@remix-run/testing' import { act, fireEvent, render, waitFor } from '@testing-library/react' import { Provider } from 'mobx-react' import { useCommonStores } from 'src/common/hooks/useCommonStores' @@ -16,7 +11,7 @@ import { describe, expect, it, vi } from 'vitest' import { HowtoForm } from './Howto.form' -import type { IHowtoDB } from 'src/models' +import type { IHowtoDB } from 'oa-shared' import type { ParentType } from './Howto.form' const Theme = testingThemeStyles @@ -167,26 +162,25 @@ describe('Howto form', () => { }) const Wrapper = (formValues: IHowtoDB, parentType: ParentType, navProps) => { - const router = createMemoryRouter( - createRoutesFromElements( - - } - />, - ), + const ReactStub = createRemixStub( + [ + { + index: true, + Component: () => ( + + + + + + ), + }, + ], + { initialEntries: ['/'] }, ) - return render( - - - - - , - ) + return render() } diff --git a/src/pages/Howto/Content/Common/Howto.form.tsx b/src/pages/Howto/Content/Common/Howto.form.tsx index eb450cd627..de91146567 100644 --- a/src/pages/Howto/Content/Common/Howto.form.tsx +++ b/src/pages/Howto/Content/Common/Howto.form.tsx @@ -34,7 +34,7 @@ import { HowtoPostingGuidelines } from './HowtoPostingGuidelines' import { HowToSubmitStatus } from './SubmitStatus' import type { FormApi } from 'final-form' -import type { IHowtoFormInput } from 'src/models/howto.models' +import type { IHowtoFormInput } from 'oa-shared' export type ParentType = 'create' | 'edit' diff --git a/src/pages/Howto/Content/Common/HowtoCategoryGuidance.tsx b/src/pages/Howto/Content/Common/HowtoCategoryGuidance.tsx index d92bc1ce10..663c05393c 100644 --- a/src/pages/Howto/Content/Common/HowtoCategoryGuidance.tsx +++ b/src/pages/Howto/Content/Common/HowtoCategoryGuidance.tsx @@ -2,7 +2,7 @@ import { Alert, Text } from 'theme-ui' import { guidance } from '../../labels' -import type { ICategory } from 'src/models/categories.model' +import type { ICategory } from 'oa-shared' interface IProps { category: ICategory | undefined diff --git a/src/pages/Howto/Content/Common/HowtoFieldStep.tsx b/src/pages/Howto/Content/Common/HowtoFieldStep.tsx index a1acf5e2d9..864c0b030c 100644 --- a/src/pages/Howto/Content/Common/HowtoFieldStep.tsx +++ b/src/pages/Howto/Content/Common/HowtoFieldStep.tsx @@ -21,8 +21,7 @@ import { } from '../../constants' import { buttons, errors, steps } from '../../labels' -import type { IHowtoStep } from 'src/models/howto.models' -import type { IUploadedFileMeta } from 'src/stores/storage' +import type { IHowtoStep, IUploadedFileMeta } from 'oa-shared' const ImageInputFieldWrapper = styled.div` width: 150px; diff --git a/src/pages/Howto/Content/Common/SubmitStatus.tsx b/src/pages/Howto/Content/Common/SubmitStatus.tsx index 81c4a20b7c..70641cc996 100644 --- a/src/pages/Howto/Content/Common/SubmitStatus.tsx +++ b/src/pages/Howto/Content/Common/SubmitStatus.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@remix-run/react' import { observer } from 'mobx-react' import { Button, Icon, Modal } from 'oa-components' import { useCommonStores } from 'src/common/hooks/useCommonStores' diff --git a/src/pages/Howto/Content/CreateHowto/CreateHowto.tsx b/src/pages/Howto/Content/CreateHowto/CreateHowto.tsx index 75ea4af394..f946836de4 100644 --- a/src/pages/Howto/Content/CreateHowto/CreateHowto.tsx +++ b/src/pages/Howto/Content/CreateHowto/CreateHowto.tsx @@ -4,7 +4,7 @@ import { HowtoForm } from 'src/pages/Howto/Content/Common/Howto.form' import TEMPLATE from './Template' -import type { IHowtoFormInput } from 'src/models/howto.models' +import type { IHowtoFormInput } from 'oa-shared' const CreateHowto = observer(() => { const formValues = { ...TEMPLATE.INITIAL_VALUES } as IHowtoFormInput diff --git a/src/pages/Howto/Content/CreateHowto/Template.tsx b/src/pages/Howto/Content/CreateHowto/Template.tsx index 959166822a..99c0b0ab1a 100644 --- a/src/pages/Howto/Content/CreateHowto/Template.tsx +++ b/src/pages/Howto/Content/CreateHowto/Template.tsx @@ -1,6 +1,6 @@ import { DifficultyLevel } from 'oa-shared' -import type { IHowtoFormInput } from 'src/models/howto.models' +import type { IHowtoFormInput } from 'oa-shared' // initialise fields which contain nested objects (and steps to have 3 placeholders) const INITIAL_VALUES: Partial = { diff --git a/src/pages/Howto/Content/EditHowto/EditHowto.tsx b/src/pages/Howto/Content/EditHowto/EditHowto.tsx index f56bad5643..48f8b24c84 100644 --- a/src/pages/Howto/Content/EditHowto/EditHowto.tsx +++ b/src/pages/Howto/Content/EditHowto/EditHowto.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react' -import { Navigate, useParams } from 'react-router-dom' +import { Navigate, useParams } from '@remix-run/react' import { toJS } from 'mobx' import { Loader } from 'oa-components' import { useCommonStores } from 'src/common/hooks/useCommonStores' @@ -8,8 +8,7 @@ import { Text } from 'theme-ui' import { HowtoForm } from '../Common/Howto.form' -import type { IHowtoDB } from 'src/models/howto.models' -import type { IUser } from 'src/models/user.models' +import type { IHowtoDB, IUser } from 'oa-shared' interface IState { formValues: IHowtoDB diff --git a/src/pages/Howto/Content/EditHowto/Template.tsx b/src/pages/Howto/Content/EditHowto/Template.tsx index e37d35b5d8..5ba1d19339 100644 --- a/src/pages/Howto/Content/EditHowto/Template.tsx +++ b/src/pages/Howto/Content/EditHowto/Template.tsx @@ -1,6 +1,6 @@ import { DifficultyLevel } from 'oa-shared' -import type { IHowtoFormInput } from 'src/models/howto.models' +import type { IHowtoFormInput } from 'oa-shared' // initialise fields which contain nested objects (and steps to have 3 placeholders) const INITIAL_VALUES: Partial = { diff --git a/src/pages/Howto/Content/Howto/Howto.test.tsx b/src/pages/Howto/Content/Howto/Howto.test.tsx index da05ede55a..fde747c323 100644 --- a/src/pages/Howto/Content/Howto/Howto.test.tsx +++ b/src/pages/Howto/Content/Howto/Howto.test.tsx @@ -1,19 +1,18 @@ import '@testing-library/jest-dom/vitest' -import { - createMemoryRouter, - createRoutesFromElements, - Route, - RouterProvider, -} from 'react-router-dom' -import { ThemeProvider } from '@emotion/react' +import { Global, ThemeProvider } from '@emotion/react' import { faker } from '@faker-js/faker' +import { createRemixStub } from '@remix-run/testing' import { act, render, waitFor, within } from '@testing-library/react' import { Provider } from 'mobx-react' +import { GlobalStyles } from 'oa-components' +import { IModerationStatus } from 'oa-shared' import { preciousPlasticTheme } from 'oa-themes' import { FactoryHowto, FactoryHowtoStep } from 'src/test/factories/Howto' import { describe, expect, it, vi } from 'vitest' +import { Howto } from './Howto' + import type { HowtoStore } from 'src/stores/Howto/howto.store' const Theme = preciousPlasticTheme.styles @@ -46,35 +45,33 @@ vi.mock('src/common/hooks/useCommonStores', () => ({ }), })) -import { IModerationStatus } from 'oa-shared' - -import { Howto } from './Howto' - const factory = (howtoStore?: Partial) => { - const router = createMemoryRouter( - createRoutesFromElements( - } />, - ), - { initialEntries: ['/howto/article'] }, - ) - - return render( - - - - - , - ) + const ReactStub = createRemixStub([ + { + index: true, + Component: () => ( + <> + + + + + + + + ), + }, + ]) + + return render() } describe('Howto', () => { describe('moderator feedback', () => { it('displays feedback for items which are not accepted', async () => { let wrapper + howto.moderation = IModerationStatus.AWAITING_MODERATION + howto.moderatorFeedback = 'Moderation comments' act(() => { - howto.moderation = IModerationStatus.AWAITING_MODERATION - howto.moderatorFeedback = 'Moderation comments' - wrapper = factory() }) @@ -98,13 +95,13 @@ describe('Howto', () => { it('displays content statistics', async () => { let wrapper - act(() => { - howto._id = 'testid' - howto._createdBy = 'HowtoAuthor' - howto.steps = [FactoryHowtoStep({})] - howto.moderation = IModerationStatus.ACCEPTED - howto.total_views = 0 + howto._id = 'testid' + howto._createdBy = 'HowtoAuthor' + howto.steps = [FactoryHowtoStep({})] + howto.moderation = IModerationStatus.ACCEPTED + howto.total_views = 0 + act(() => { wrapper = factory() }) diff --git a/src/pages/Howto/Content/Howto/Howto.tsx b/src/pages/Howto/Content/Howto/Howto.tsx index 43d73ef911..7f23f87315 100644 --- a/src/pages/Howto/Content/Howto/Howto.tsx +++ b/src/pages/Howto/Content/Howto/Howto.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react' -import { Navigate, useParams } from 'react-router-dom' +import { Navigate, useParams } from '@remix-run/react' import { observer } from 'mobx-react' import { ArticleCallToAction, @@ -19,7 +19,7 @@ import HowtoDescription from './HowtoDescription/HowtoDescription' import { HowtoDiscussion } from './HowToDiscussion/HowToDiscussion' import Step from './Step/Step' -import type { IUser } from 'src/models' +import type { IUser } from 'oa-shared' export const Howto = observer(() => { const { slug } = useParams() diff --git a/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx b/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx index 6f860e49b8..c3f91cb80b 100644 --- a/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx +++ b/src/pages/Howto/Content/Howto/HowtoDescription/HowtoDescription.tsx @@ -1,5 +1,5 @@ import { Fragment, useEffect, useState } from 'react' -import { Link, useNavigate } from 'react-router-dom' +import { Link, useNavigate } from '@remix-run/react' import { Button, Category, @@ -29,9 +29,7 @@ import { Alert, Box, Card, Divider, Flex, Heading, Image, Text } from 'theme-ui' import { ContentAuthorTimestamp } from '../../../../common/ContentAuthorTimestamp/ContentAuthorTimestamp' import { HowtoDownloads } from '../HowtoDownloads/HowtoDownloads' -import type { ITag } from 'src/models' -import type { IHowtoDB } from 'src/models/howto.models' -import type { IUser } from 'src/models/user.models' +import type { IHowtoDB, ITag, IUser } from 'oa-shared' interface IProps { howto: IHowtoDB & { tagList?: ITag[] } diff --git a/src/pages/Howto/Content/Howto/HowtoDownloads/HowtoDownloads.tsx b/src/pages/Howto/Content/Howto/HowtoDownloads/HowtoDownloads.tsx index 5f01e09c35..5dc0f8a256 100644 --- a/src/pages/Howto/Content/Howto/HowtoDownloads/HowtoDownloads.tsx +++ b/src/pages/Howto/Content/Howto/HowtoDownloads/HowtoDownloads.tsx @@ -10,7 +10,7 @@ import { updateHowtoDownloadCooldown, } from './downloadCooldown' -import type { IHowtoDB, IUser } from 'src/models' +import type { IHowtoDB, IUser } from 'oa-shared' interface IProps { howto: IHowtoDB diff --git a/src/pages/Howto/Content/Howto/Step/Step.tsx b/src/pages/Howto/Content/Howto/Step/Step.tsx index e0a9e0e5c7..9edd2c3ee7 100644 --- a/src/pages/Howto/Content/Howto/Step/Step.tsx +++ b/src/pages/Howto/Content/Howto/Step/Step.tsx @@ -4,7 +4,7 @@ import { formatImagesForGallery } from 'src/utils/formatImageListForGallery' import { capitalizeFirstLetter } from 'src/utils/helpers' import { Box, Card, Flex, Heading, Text } from 'theme-ui' -import type { IHowtoStep } from 'src/models/howto.models' +import type { IHowtoStep } from 'oa-shared' interface IProps { step: IHowtoStep diff --git a/src/pages/Howto/Content/HowtoList/HowToCard.tsx b/src/pages/Howto/Content/HowtoList/HowToCard.tsx index ff7f6bfcb6..36fe0c65ed 100644 --- a/src/pages/Howto/Content/HowtoList/HowToCard.tsx +++ b/src/pages/Howto/Content/HowtoList/HowToCard.tsx @@ -1,4 +1,4 @@ -import { Link as RouterLink } from 'react-router-dom' +import { Link as RouterLink } from '@remix-run/react' import { Category, IconCountWithTooltip, @@ -11,7 +11,7 @@ import { cdnImageUrl } from 'src/utils/cdnImageUrl' import { capitalizeFirstLetter } from 'src/utils/helpers' import { Box, Card, Flex, Heading, Image } from 'theme-ui' -import type { IHowto } from 'src/models/howto.models' +import type { IHowto } from 'oa-shared' interface IProps { howto: IHowto diff --git a/src/pages/Howto/Content/HowtoList/HowtoFilterHeader.tsx b/src/pages/Howto/Content/HowtoList/HowtoFilterHeader.tsx index 12e85d1738..7f5a22fc05 100644 --- a/src/pages/Howto/Content/HowtoList/HowtoFilterHeader.tsx +++ b/src/pages/Howto/Content/HowtoList/HowtoFilterHeader.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react' -import { useSearchParams } from 'react-router-dom' +import { useSearchParams } from '@remix-run/react' import debounce from 'debounce' import { Select } from 'oa-components' import { FieldContainer } from 'src/common/Form/FieldContainer' diff --git a/src/pages/Howto/Content/HowtoList/HowtoList.tsx b/src/pages/Howto/Content/HowtoList/HowtoList.tsx index 28efe0361f..919e87ac79 100644 --- a/src/pages/Howto/Content/HowtoList/HowtoList.tsx +++ b/src/pages/Howto/Content/HowtoList/HowtoList.tsx @@ -1,5 +1,5 @@ import React, { useEffect, useState } from 'react' -import { Link, useSearchParams } from 'react-router-dom' +import { Link, useSearchParams } from '@remix-run/react' import { observer } from 'mobx-react' import { Button, Loader, MoreContainer } from 'oa-components' import { useCommonStores } from 'src/common/hooks/useCommonStores' @@ -15,12 +15,13 @@ import HowToCard from './HowToCard' import { HowtoFilterHeader } from './HowtoFilterHeader' import type { DocumentData, QueryDocumentSnapshot } from 'firebase/firestore' -import type { IHowto } from 'src/models' +import type { IHowto } from 'oa-shared' import type { HowtoSortOption } from './HowtoSortOptions' export const HowtoList = observer(() => { - const { themeStore, userStore } = useCommonStores().stores - const theme = themeStore.currentTheme + const siteName = import.meta.env.VITE_SITE_NAME + + const { userStore } = useCommonStores().stores const [isFetching, setIsFetching] = useState(true) const [howtos, setHowtos] = useState([]) const [total, setTotal] = useState(0) @@ -108,7 +109,7 @@ export const HowtoList = observer(() => { fontSize: 5, }} > - {theme && theme.howtoHeading} + {import.meta.env.VITE_HOWTOS_HEADING} { - Inspire the {theme.siteName} world. + Inspire the {siteName} world.
Share your how-to!
diff --git a/src/pages/Howto/Howto.tsx b/src/pages/Howto/Howto.tsx deleted file mode 100644 index 0f849caa9f..0000000000 --- a/src/pages/Howto/Howto.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import React, { lazy, Suspense } from 'react' -import { Route, Routes } from 'react-router-dom' - -import { AuthRoute } from '../common/AuthRoute' -import { Howto } from './Content/Howto/Howto' -import { HowtoList } from './Content/HowtoList/HowtoList' - -// lazy load editor pages -const CreateHowto = lazy(() => import('./Content/CreateHowto/CreateHowto')) -const EditHowto = lazy(() => import('./Content/EditHowto/EditHowto')) - -const HowtoPage = () => { - return ( - }> - - } /> - - - - } - /> - } /> - - - - } - /> - - - ) -} - -export default HowtoPage diff --git a/src/pages/Howto/howto.service.ts b/src/pages/Howto/howto.service.ts index 10ae380a41..77c5e27b7a 100644 --- a/src/pages/Howto/howto.service.ts +++ b/src/pages/Howto/howto.service.ts @@ -22,8 +22,7 @@ import type { QueryFilterConstraint, QueryNonFilterConstraint, } from 'firebase/firestore' -import type { IHowto, IUserDB } from '../../models' -import type { ICategory } from '../../models/categories.model' +import type { ICategory, IHowto, IUserDB } from 'oa-shared' import type { HowtoSortOption } from './Content/HowtoList/HowtoSortOptions' export enum HowtosSearchParams { diff --git a/src/pages/Maps/Content/Controls/Controls.tsx b/src/pages/Maps/Content/Controls/Controls.tsx index 0c55b1c48f..f8a53c06b0 100644 --- a/src/pages/Maps/Content/Controls/Controls.tsx +++ b/src/pages/Maps/Content/Controls/Controls.tsx @@ -1,5 +1,5 @@ -import { useState } from 'react' -import { Link, useNavigate } from 'react-router-dom' +import React, { useState } from 'react' +import { Link, useNavigate } from '@remix-run/react' import { Button, Modal, OsmGeocoding } from 'oa-components' import filterIcon from 'src/assets/icons/icon-filters-mobile.png' import { useCommonStores } from 'src/common/hooks/useCommonStores' diff --git a/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx b/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx index 67f389e203..16ceb4c2db 100644 --- a/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx +++ b/src/pages/Maps/Content/Controls/transformAvailableFiltersToGroups.tsx @@ -5,8 +5,13 @@ import { Image } from 'theme-ui' import { transformSpecialistWorkspaceTypeToWorkspace } from './transformSpecialistWorkspaceTypeToWorkspace' -import type { IPinGrouping, ProfileTypeName, WorkspaceType } from 'oa-shared' -import type { IMapGrouping, IMapPin } from 'src/models' +import type { + IMapGrouping, + IMapPin, + IPinGrouping, + ProfileTypeName, + WorkspaceType, +} from 'oa-shared' const ICON_SIZE = 30 diff --git a/src/pages/Maps/Content/MapView/Cluster.tsx b/src/pages/Maps/Content/MapView/Cluster.client.tsx similarity index 76% rename from src/pages/Maps/Content/MapView/Cluster.tsx rename to src/pages/Maps/Content/MapView/Cluster.client.tsx index 55b3d4a936..d361532a8e 100644 --- a/src/pages/Maps/Content/MapView/Cluster.tsx +++ b/src/pages/Maps/Content/MapView/Cluster.client.tsx @@ -1,11 +1,10 @@ import * as React from 'react' import { Marker } from 'react-leaflet' import MarkerClusterGroup from 'react-leaflet-markercluster' -import { useCommonStores } from 'src/common/hooks/useCommonStores' import { createClusterIcon, createMarkerIcon } from './Sprites' -import type { IMapPin } from 'src/models/maps.models' +import type { IMapPin } from 'oa-shared' import 'react-leaflet-markercluster/dist/styles.min.css' @@ -15,11 +14,11 @@ interface IProps { prefix?: string // Temporarily needed while two maps are rendered } -export const Clusters = (props: IProps) => { - const { pins, prefix, onPinClick } = props - const { stores } = useCommonStores() - const currentTheme = stores.themeStore.currentTheme - +export const Clusters: React.FunctionComponent = ({ + pins, + prefix, + onPinClick, +}) => { /** * Documentation of Leaflet Clusters for better understanding * https://github.com/Leaflet/Leaflet.markercluster#clusters-methods @@ -40,7 +39,7 @@ export const Clusters = (props: IProps) => { { onPinClick(pin) }} diff --git a/src/pages/Maps/Content/MapView/MapView.tsx b/src/pages/Maps/Content/MapView/MapView.client.tsx similarity index 88% rename from src/pages/Maps/Content/MapView/MapView.tsx rename to src/pages/Maps/Content/MapView/MapView.client.tsx index 84a3c27748..9e804b662c 100644 --- a/src/pages/Maps/Content/MapView/MapView.tsx +++ b/src/pages/Maps/Content/MapView/MapView.client.tsx @@ -1,13 +1,12 @@ import { useEffect } from 'react' import { Map } from 'oa-components' -import { Clusters } from './Cluster' -import { Popup } from './Popup' +import { Clusters } from './Cluster.client' +import { Popup } from './Popup.client' import type { LatLngExpression } from 'leaflet' +import type { ILatLng, IMapPin } from 'oa-shared' import type { Map as MapType } from 'react-leaflet' -import type { ILatLng } from 'shared/models' -import type { IMapPin } from 'src/models/maps.models' interface IProps { activePin: IMapPin | null diff --git a/src/pages/Maps/Content/MapView/MapWithList.tsx b/src/pages/Maps/Content/MapView/MapWithList.tsx index 428fc34aa3..0d777ffe12 100644 --- a/src/pages/Maps/Content/MapView/MapWithList.tsx +++ b/src/pages/Maps/Content/MapView/MapWithList.tsx @@ -2,15 +2,14 @@ import { useEffect, useMemo, useState } from 'react' import { Button, Map } from 'oa-components' import { Box, Flex } from 'theme-ui' -import { Clusters } from './Cluster' +import { Clusters } from './Cluster.client' import { latLongFilter } from './latLongFilter' import { MapWithListHeader } from './MapWithListHeader' -import { Popup } from './Popup' +import { Popup } from './Popup.client' import type { LatLngExpression } from 'leaflet' +import type { ILatLng, IMapPin } from 'oa-shared' import type { Map as MapType } from 'react-leaflet' -import type { ILatLng } from 'shared/models' -import type { IMapPin } from 'src/models/maps.models' const allFilters = [ { diff --git a/src/pages/Maps/Content/MapView/MapWithListHeader.tsx b/src/pages/Maps/Content/MapView/MapWithListHeader.tsx index 88340441bc..37ef4aea01 100644 --- a/src/pages/Maps/Content/MapView/MapWithListHeader.tsx +++ b/src/pages/Maps/Content/MapView/MapWithListHeader.tsx @@ -1,8 +1,7 @@ import { CardList, FilterList, OsmGeocoding } from 'oa-components' import { Box, Flex, Heading } from 'theme-ui' -import type { ILatLng } from 'oa-shared' -import type { IMapPin } from 'src/models' +import type { ILatLng, IMapPin } from 'oa-shared' interface IProps { activePinFilters: string[] diff --git a/src/pages/Maps/Content/MapView/Popup.tsx b/src/pages/Maps/Content/MapView/Popup.client.tsx similarity index 97% rename from src/pages/Maps/Content/MapView/Popup.tsx rename to src/pages/Maps/Content/MapView/Popup.client.tsx index 1fe8717387..2f9aa25aaa 100644 --- a/src/pages/Maps/Content/MapView/Popup.tsx +++ b/src/pages/Maps/Content/MapView/Popup.client.tsx @@ -5,8 +5,8 @@ import { MapMemberCard, PinProfile } from 'oa-components' import { IModerationStatus } from 'oa-shared' import { MAP_GROUPINGS } from 'src/stores/Maps/maps.groupings' +import type { IMapPin, IMapPinWithDetail } from 'oa-shared' import type { Map } from 'react-leaflet' -import type { IMapPin, IMapPinWithDetail } from 'src/models/maps.models' import './popup.css' diff --git a/src/pages/Maps/Content/MapView/Sprites.tsx b/src/pages/Maps/Content/MapView/Sprites.tsx index 2c7dc495fb..0c76f7fa59 100644 --- a/src/pages/Maps/Content/MapView/Sprites.tsx +++ b/src/pages/Maps/Content/MapView/Sprites.tsx @@ -6,8 +6,7 @@ import { logger } from 'src/logger' import Workspace from 'src/pages/User/workspace/Workspace' import type { MarkerCluster } from 'leaflet' -import type { PlatformTheme } from 'oa-themes' -import type { IMapPin } from 'src/models/maps.models' +import type { IMapPin } from 'oa-shared' import './sprites.css' @@ -36,10 +35,10 @@ export const createClusterIcon = () => { } } -export const createMarkerIcon = (pin: IMapPin, currentTheme: PlatformTheme) => { +export const createMarkerIcon = (pin: IMapPin) => { const icon = pin.moderation === IModerationStatus.ACCEPTED - ? Workspace.findWorkspaceBadge(pin.type, true, pin.verified, currentTheme) + ? Workspace.findWorkspaceBadge(pin.type, true, pin.verified) : AwaitingModerationHighlight if (!pin.type) { logger.debug('NO TYPE', pin) diff --git a/src/pages/Maps/Content/MapView/latLongFilter.tsx b/src/pages/Maps/Content/MapView/latLongFilter.tsx index 1bc16a2472..4be7a39a49 100644 --- a/src/pages/Maps/Content/MapView/latLongFilter.tsx +++ b/src/pages/Maps/Content/MapView/latLongFilter.tsx @@ -1,5 +1,4 @@ -import type { IBoundingBox } from 'shared/models' -import type { IMapPin } from 'src/models' +import type { IBoundingBox, IMapPin } from 'oa-shared' export const latLongFilter = (boundaries: IBoundingBox, pinList: IMapPin[]) => { const result = pinList.filter(({ location }) => { diff --git a/src/pages/Maps/Content/index.ts b/src/pages/Maps/Content/index.ts index 5b522e1d75..0a8d8bee9b 100644 --- a/src/pages/Maps/Content/index.ts +++ b/src/pages/Maps/Content/index.ts @@ -1,2 +1,2 @@ -export { MapView } from './MapView/MapView' +export { MapView } from './MapView/MapView.client' export { Controls } from './Controls/Controls' diff --git a/src/pages/Maps/Maps.tsx b/src/pages/Maps/Maps.client.tsx similarity index 97% rename from src/pages/Maps/Maps.tsx rename to src/pages/Maps/Maps.client.tsx index c17ae7e48c..37732dff94 100644 --- a/src/pages/Maps/Maps.tsx +++ b/src/pages/Maps/Maps.client.tsx @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useMemo, useState } from 'react' -import { useLocation, useNavigate } from 'react-router-dom' +import { useLocation, useNavigate } from '@remix-run/react' import { observer } from 'mobx-react' import { useCommonStores } from 'src/common/hooks/useCommonStores' import { filterMapPinsByType } from 'src/stores/Maps/filter' @@ -14,9 +14,8 @@ import { GetLocation } from './utils/geolocation' import { Controls, MapView } from './Content' import { MapPinServiceContext } from './map.service' +import type { ILatLng, IMapPin } from 'oa-shared' import type { Map } from 'react-leaflet' -import type { ILatLng } from 'shared/models' -import type { IMapPin } from 'src/models/maps.models' import './styles.css' diff --git a/src/pages/Maps/Maps.test.tsx b/src/pages/Maps/Maps.test.tsx index 5d489bdae3..506a78af96 100644 --- a/src/pages/Maps/Maps.test.tsx +++ b/src/pages/Maps/Maps.test.tsx @@ -1,12 +1,8 @@ import '@testing-library/jest-dom/vitest' -import { - createMemoryRouter, - createRoutesFromElements, - Route, - RouterProvider, -} from 'react-router-dom' +import { createMemoryRouter, RouterProvider } from 'react-router-dom' import { ThemeProvider } from '@emotion/react' +import { createRoutesFromElements, Route } from '@remix-run/react' import { act, render, waitFor } from '@testing-library/react' import { Provider } from 'mobx-react' import { useCommonStores } from 'src/common/hooks/useCommonStores' @@ -16,7 +12,7 @@ import { testingThemeStyles } from 'src/test/utils/themeUtils' import { describe, expect, it, vi } from 'vitest' import { MapPinServiceContext } from './map.service' -import Maps from './Maps' +import Maps from './Maps.client' import type { IMapPinService } from './map.service' @@ -37,13 +33,6 @@ vi.mock('src/common/hooks/useCommonStores', () => ({ HowtoAuthor: true, }, }, - themeStore: { - currentTheme: { - styles: { - communityProgramURL: '', - }, - }, - }, tagsStore: {}, }, }), diff --git a/src/pages/Maps/map.service.ts b/src/pages/Maps/map.service.ts index 9b01cea15f..1ef4c7327a 100644 --- a/src/pages/Maps/map.service.ts +++ b/src/pages/Maps/map.service.ts @@ -6,7 +6,7 @@ import { DB_ENDPOINTS } from 'src/models/dbEndpoints' import { cdnImageUrl } from 'src/utils/cdnImageUrl' import { firestore } from 'src/utils/firebase' -import type { IMapPin } from '../../models' +import type { IMapPin } from 'oa-shared' export interface IMapPinService { getMapPins: () => Promise diff --git a/src/pages/NotFound/NotFound.tsx b/src/pages/NotFound/NotFound.tsx index 3d88b76cec..50d971580c 100644 --- a/src/pages/NotFound/NotFound.tsx +++ b/src/pages/NotFound/NotFound.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router-dom' +import { Link } from '@remix-run/react' import Main from 'src/pages/common/Layout/Main' import { Flex, Image, Text } from 'theme-ui' diff --git a/src/pages/PageList.tsx b/src/pages/PageList.tsx index 4341ebcee6..7288595a8a 100644 --- a/src/pages/PageList.tsx +++ b/src/pages/PageList.tsx @@ -1,178 +1,60 @@ -import { lazy } from 'react' import { MODULE } from 'src/modules' -import { mapPinService, MapPinServiceContext } from './Maps/map.service' -import { QuestionModule } from './Question' -import { ResearchModule } from './Research' - -import type { UserRole } from 'oa-shared' -import type { CSSObject } from 'theme-ui' -/** - * Import all pages for use in lazy loading - * NOTE - requires default export in page class (https://reactjs.org/docs/code-splitting.html#named-exports) - */ -const HowtoPage = lazy(() => import('./Howto/Howto')) -const SettingsPage = lazy(() => import('./UserSettings')) - -const AcademyPage = lazy(() => import('./Academy/Academy')) -const MapsPage = lazy(() => import('./Maps/Maps')) -const User = lazy(() => import('./User/user.routes')) - -const SignUpMessagePage = lazy(() => import('./SignUp/SignUpMessage')) -const SignUpPage = lazy(() => import('./SignUp/SignUp')) -const SignInPage = lazy(() => import('./SignIn/SignIn')) -const PrivacyPolicy = lazy(() => import('./policy/PrivacyPolicy')) -const TermsPolicy = lazy(() => import('./policy/TermsPolicy')) -const Unsubscribe = lazy(() => import('./Unsubscribe/Unsubscribe')) - -const Patreon = lazy(() => import('./Patreon/Patreon')) - -export const getAvailablePageList = (supportedModules: MODULE[]): IPageMeta[] => - COMMUNITY_PAGES.filter((pageItem) => - supportedModules.includes(pageItem.moduleName), - ) - -export interface IPageMeta { - moduleName: MODULE +interface IPageNavigation { + module: MODULE path: string - component: any title: string - description: string - exact?: boolean - fullPageWidth?: boolean - customStyles?: CSSObject - requiredRole?: UserRole } -const howTo = { - moduleName: MODULE.HOWTO, +const QuestionModule: IPageNavigation = { + module: MODULE.QUESTION, + path: '/questions', + title: 'Questions', +} + +const ResearchModule: IPageNavigation = { + module: MODULE.RESEARCH, + path: '/research', + title: 'Research', +} + +const howTo: IPageNavigation = { + module: MODULE.HOWTO, path: '/how-to', - component: , title: 'How-tos', - description: 'Welcome to how-to', } -const settings = { - moduleName: MODULE.USER, +const settings: IPageNavigation = { + module: MODULE.CORE, path: '/settings', - component: , title: 'Settings', - description: 'Settings', } -const user = { - moduleName: MODULE.USER, - path: '/u', - component: , - title: 'Profile', - description: 'Profile', -} -const academy = { - moduleName: MODULE.ACADEMY, +const academy: IPageNavigation = { + module: MODULE.ACADEMY, path: '/academy', - component: , title: 'Academy', - description: 'Demo external page embed', - customStyles: { - flex: 1, - }, - fullPageWidth: true, } -const maps = { - moduleName: MODULE.MAP, +const maps: IPageNavigation = { + module: MODULE.MAP, path: '/map', - component: ( - - - - ), title: 'Map', - description: 'Welcome to the Map', - customStyles: { - position: 'relative', - margin: '0', - padding: '0', - width: '100vw', - }, - fullPageWidth: true, -} - -const signup = { - moduleName: MODULE.USER, - path: '/sign-up', - exact: true, - component: , - title: 'Sign Up', - description: '', } -const signin = { - moduleName: MODULE.USER, - path: '/sign-in', - exact: true, - component: , - title: 'Sign In', - description: '', -} - -const signupmessage = { - moduleName: MODULE.USER, - path: '/sign-up-message', - exact: true, - component: , - title: 'Sign Up Message', - description: '', -} - -const privacyPolicy = { - moduleName: MODULE.CORE, - path: '/privacy', - exact: true, - component: , - title: 'Privacy Policy', - description: '', -} -const termsPolicy = { - moduleName: MODULE.CORE, - path: '/terms', - exact: true, - component: , - title: 'Terms of Use', - description: '', -} - -const unsubscribe = { - moduleName: MODULE.CORE, - path: '/unsubscribe', - component: , - title: 'Unsubscribe', - description: '', -} - -const patreon = { - moduleName: MODULE.CORE, - path: '/patreon', - component: , - title: 'Patreon', - description: '', +export const getAvailablePageList = ( + supportedModules: MODULE[], +): IPageNavigation[] => { + return COMMUNITY_PAGES.filter((pageItem) => + supportedModules.includes(pageItem.module), + ) } -export const COMMUNITY_PAGES: IPageMeta[] = [ +export const COMMUNITY_PAGES: IPageNavigation[] = [ howTo, maps, academy, ResearchModule, QuestionModule, ] + /** Additional pages to show in signed-in profile dropdown */ -export const COMMUNITY_PAGES_PROFILE: IPageMeta[] = [settings] -export const POLICY_PAGES: IPageMeta[] = [privacyPolicy, termsPolicy] -export const NO_HEADER_PAGES: IPageMeta[] = [ - user, - signup, - signupmessage, - signin, - ResearchModule, // CC 2021-06-24 - Temporary - make research module accessible to all in production but hide from nav - unsubscribe, - QuestionModule, - patreon, -] +export const COMMUNITY_PAGES_PROFILE: IPageNavigation[] = [settings] diff --git a/src/pages/Patreon/Patreon.tsx b/src/pages/Patreon/Patreon.tsx index 71c0fa06e2..cf7898d166 100644 --- a/src/pages/Patreon/Patreon.tsx +++ b/src/pages/Patreon/Patreon.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { useSearchParams } from 'react-router-dom' +import { useSearchParams } from '@remix-run/react' import { Loader } from 'oa-components' import { logger } from 'src/logger' import { functions } from 'src/utils/firebase' diff --git a/src/pages/Question/Content/Common/QuestionForm.tsx b/src/pages/Question/Content/Common/QuestionForm.tsx index 644aaa0f6a..5fea330eb3 100644 --- a/src/pages/Question/Content/Common/QuestionForm.tsx +++ b/src/pages/Question/Content/Common/QuestionForm.tsx @@ -1,5 +1,5 @@ import { Form } from 'react-final-form' -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@remix-run/react' import { Button, ElWithBeforeIcon } from 'oa-components' import { IModerationStatus } from 'oa-shared' import IconHeaderHowto from 'src/assets/images/header-section/howto-header-icon.svg' @@ -19,8 +19,8 @@ import { QuestionTitleField, } from './FormFields' +import type { IQuestion } from 'oa-shared' import type { MainFormAction } from 'src/common/Form/types' -import type { IQuestion } from 'src/models' interface IProps { 'data-testid'?: string diff --git a/src/pages/Question/Content/Common/QuestionPostingGuidelines.tsx b/src/pages/Question/Content/Common/QuestionPostingGuidelines.tsx index c6fe413664..47bf13c726 100644 --- a/src/pages/Question/Content/Common/QuestionPostingGuidelines.tsx +++ b/src/pages/Question/Content/Common/QuestionPostingGuidelines.tsx @@ -1,8 +1,9 @@ -import { useTheme } from '@emotion/react' import { ExternalLink, Guidelines } from 'oa-components' export const QuestionPostingGuidelines = () => { - const theme = useTheme() + const guidelinesUrl = + import.meta.env.VITE_QUESTIONS_GUIDELINES_URL || + process.env.VITE_QUESTIONS_GUIDELINES_URL const steps = [ <> @@ -38,14 +39,11 @@ export const QuestionPostingGuidelines = () => { , ] - if (theme.questionsGuidelinesURL) { + if (guidelinesUrl) { steps.unshift( <> Have a look at our{' '} - + question guidelines. , diff --git a/src/pages/Question/QuestionEdit.tsx b/src/pages/Question/QuestionEdit.tsx index ee8a9d9e6e..947c5c39fb 100644 --- a/src/pages/Question/QuestionEdit.tsx +++ b/src/pages/Question/QuestionEdit.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { useNavigate, useParams } from 'react-router-dom' +import { useNavigate, useParams } from '@remix-run/react' import { toJS } from 'mobx' import { Loader } from 'oa-components' import { UserRole } from 'oa-shared' @@ -7,7 +7,7 @@ import { logger } from 'src/logger' import { QuestionForm } from 'src/pages/Question/Content/Common/QuestionForm' import { useQuestionStore } from 'src/stores/Question/question.store' -import type { IQuestion } from 'src/models' +import type { IQuestion } from 'oa-shared' export const QuestionEdit = () => { const { slug } = useParams() diff --git a/src/pages/Question/QuestionFilterHeader.tsx b/src/pages/Question/QuestionFilterHeader.tsx index 798326293c..3e47a461c2 100644 --- a/src/pages/Question/QuestionFilterHeader.tsx +++ b/src/pages/Question/QuestionFilterHeader.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react' -import { useSearchParams } from 'react-router-dom' +import { useSearchParams } from '@remix-run/react' import debounce from 'debounce' import { Select } from 'oa-components' import { FieldContainer } from 'src/common/Form/FieldContainer' diff --git a/src/pages/Question/QuestionListItem.tsx b/src/pages/Question/QuestionListItem.tsx index cf59a54af0..3898065995 100644 --- a/src/pages/Question/QuestionListItem.tsx +++ b/src/pages/Question/QuestionListItem.tsx @@ -10,7 +10,7 @@ import { Box, Card, Flex, Heading } from 'theme-ui' import { UserNameTag } from '../common/UserNameTag/UserNameTag' import { listing } from './labels' -import type { IQuestion } from 'src/models' +import type { IQuestion } from 'oa-shared' interface IProps { question: IQuestion.Item diff --git a/src/pages/Question/QuestionListing.tsx b/src/pages/Question/QuestionListing.tsx index 532a875e42..0877fa61a5 100644 --- a/src/pages/Question/QuestionListing.tsx +++ b/src/pages/Question/QuestionListing.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Link, useSearchParams } from 'react-router-dom' +import { Link, useSearchParams } from '@remix-run/react' import { Button, Loader } from 'oa-components' import { useCommonStores } from 'src/common/hooks/useCommonStores' import { logger } from 'src/logger' @@ -12,7 +12,7 @@ import { QuestionFilterHeader } from './QuestionFilterHeader' import { QuestionListItem } from './QuestionListItem' import type { DocumentData, QueryDocumentSnapshot } from 'firebase/firestore' -import type { IQuestion } from 'src/models' +import type { IQuestion } from 'oa-shared' import type { QuestionSortOption } from './QuestionSortOptions' export const QuestionListing = () => { @@ -125,13 +125,11 @@ export const QuestionListing = () => { )} {questions && questions.length > 0 && ( - <> -
    - {questions.map((question, index) => ( - - ))} -
- +
    + {questions.map((question, index) => ( + + ))} +
)} {showLoadMore && ( diff --git a/src/pages/Question/QuestionPage.test.tsx b/src/pages/Question/QuestionPage.test.tsx index 094a5e5531..85977bfc84 100644 --- a/src/pages/Question/QuestionPage.test.tsx +++ b/src/pages/Question/QuestionPage.test.tsx @@ -1,13 +1,9 @@ import '@testing-library/jest-dom/vitest' -import { - createMemoryRouter, - createRoutesFromElements, - Route, - RouterProvider, -} from 'react-router-dom' +import { createMemoryRouter, RouterProvider } from 'react-router-dom' import { ThemeProvider } from '@emotion/react' import { faker } from '@faker-js/faker' +import { createRoutesFromElements, Route } from '@remix-run/react' import { act, render, waitFor, within } from '@testing-library/react' import { Provider } from 'mobx-react' import { UserRole } from 'oa-shared' diff --git a/src/pages/Question/QuestionPage.tsx b/src/pages/Question/QuestionPage.tsx index ee93ac4cc4..e531c24200 100644 --- a/src/pages/Question/QuestionPage.tsx +++ b/src/pages/Question/QuestionPage.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Link, useNavigate, useParams } from 'react-router-dom' +import { Link, useNavigate, useParams } from '@remix-run/react' import { Category, ContentStatistics, @@ -22,8 +22,7 @@ import { Box, Button, Card, Divider, Flex, Heading, Text } from 'theme-ui' import { ContentAuthorTimestamp } from '../common/ContentAuthorTimestamp/ContentAuthorTimestamp' import { QuestionDiscussion } from './QuestionDiscussion' -import type { IQuestion } from 'src/models' -import type { IUploadedFileMeta } from 'src/stores/storage' +import type { IQuestion, IUploadedFileMeta } from 'oa-shared' export const QuestionPage = () => { const [isLoading, setIsLoading] = useState(true) diff --git a/src/pages/Question/index.tsx b/src/pages/Question/index.tsx deleted file mode 100644 index e0a3d7173b..0000000000 --- a/src/pages/Question/index.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { MODULE } from 'src/modules' -import { - DiscussionStore, - DiscussionStoreContext, -} from 'src/stores/Discussions/discussions.store' -import { - QuestionStore, - QuestionStoreContext, -} from 'src/stores/Question/question.store' - -import { useCommonStores } from '../../common/hooks/useCommonStores' -import QuestionRoutes from './question.routes' - -import type { IPageMeta } from 'src/pages/PageList' - -export const QuestionModuleContainer = () => { - const rootStore = useCommonStores() - return ( - - - - - - ) -} - -export const QuestionModule: IPageMeta = { - moduleName: MODULE.QUESTION, - path: '/questions', - component: , - title: 'Questions', - description: 'Welcome to question and answer', -} diff --git a/src/pages/Question/question.routes.test.tsx b/src/pages/Question/question.routes.test.tsx deleted file mode 100644 index f7cd211729..0000000000 --- a/src/pages/Question/question.routes.test.tsx +++ /dev/null @@ -1,471 +0,0 @@ -import '@testing-library/jest-dom/vitest' - -import { - createMemoryRouter, - createRoutesFromElements, - Route, - RouterProvider, -} from 'react-router-dom' -import { ThemeProvider } from '@emotion/react' -import { faker } from '@faker-js/faker' -import { act, cleanup, render, screen, waitFor } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import { Provider } from 'mobx-react' -import { UserRole } from 'oa-shared' -import { questionService } from 'src/pages/Question/question.service' -import { useQuestionStore } from 'src/stores/Question/question.store' -import { FactoryDiscussion } from 'src/test/factories/Discussion' -import { FactoryQuestionItem } from 'src/test/factories/Question' -import { FactoryUser } from 'src/test/factories/User' -import { testingThemeStyles } from 'src/test/utils/themeUtils' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -import { questionRouteElements } from './question.routes' - -import type { QuestionStore } from 'src/stores/Question/question.store' -import type { Mock } from 'vitest' - -vi.mock('../../stores/common/module.store') -vi.mock('src/utils/validators') - -const Theme = testingThemeStyles -let mockActiveUser = FactoryUser() -const mockDiscussionItem = FactoryDiscussion() - -// Similar to issues in Academy.test.tsx - stub methods called in user store constructor -// TODO - replace with mock store or avoid direct call -vi.mock('src/common/hooks/useCommonStores', () => ({ - __esModule: true, - useCommonStores: () => ({ - stores: { - userStore: { - user: mockActiveUser, - }, - aggregationsStore: { - isVerified: vi.fn(), - users_verified: { - HowtoAuthor: true, - }, - }, - howtoStore: {}, - tagsStore: { - allTags: [ - { - label: 'test tag 1', - image: 'test img', - }, - ], - }, - questionCategoriesStore: { - allQuestionCategories: [], - }, - discussionStore: { - fetchOrCreateDiscussionBySource: vi.fn().mockResolvedValue({ - mockDiscussionItem, - }), - activeUser: vi.fn().mockResolvedValue(mockActiveUser), - }, - }, - }), -})) - -const mockedUsedNavigate = vi.fn() -vi.mock('react-router-dom', async () => ({ - ...((await vi.importActual('react-router-dom')) as any), - useNavigate: () => mockedUsedNavigate, -})) - -class mockQuestionStoreClass implements Partial { - setActiveQuestionItemBySlug = vi.fn() - needsModeration = vi.fn().mockResolvedValue(true) - incrementViewCount = vi.fn() - activeQuestionItem = FactoryQuestionItem({ - title: 'Question article title', - }) - QuestionUploadStatus = {} as any - updateUploadStatus = {} as any - formatQuestionCommentList = vi.fn() - getActiveQuestionUpdateComments = vi.fn() - lockQuestionItem = vi.fn() - lockQuestionUpdate = vi.fn() - unlockQuestionUpdate = vi.fn() - upsertQuestion = vi.fn() - fetchQuestions = vi.fn().mockResolvedValue([]) - fetchQuestionBySlug = vi.fn() - votedUsefulCount = 0 - subscriberCount = 0 - userCanEditQuestion = true -} - -const mockQuestionService = { - getQuestionCategories: vi.fn(() => { - return new Promise((resolve) => { - resolve([]) - }) - }), - search: vi.fn(() => { - return new Promise((resolve) => { - resolve({ items: [], total: 0, lastVisible: undefined }) - }) - }), -} -const mockQuestionStore = new mockQuestionStoreClass() - -vi.mock('src/stores/Question/question.store') -vi.mock('src/stores/Discussions/discussions.store') -vi.mock('src/pages/Question/question.service') - -describe('question.routes', () => { - beforeEach(() => { - ;(useQuestionStore as Mock).mockReturnValue(mockQuestionStore) - questionService.getQuestionCategories = vi.fn().mockResolvedValue([]) - }) - - afterEach(() => { - vi.restoreAllMocks() - cleanup() - }) - - describe('/questions/', () => { - it('renders a loading state', async () => { - let wrapper - mockQuestionService.search = vi.fn(() => { - return new Promise((resolve) => { - setTimeout( - () => resolve({ items: [], total: 0, lastVisible: undefined }), - 4000, - ) - }) - }) - - act(() => { - wrapper = renderFn('/questions') - }) - expect(wrapper.getByText(/loading/)).toBeInTheDocument() - - await waitFor(() => { - expect(() => wrapper.getByText(/loading/)).toThrow() - }) - }) - - it('renders an empty state', async () => { - let wrapper - - act(() => { - wrapper = renderFn('/questions') - }) - - await waitFor(() => { - expect( - wrapper.getByText(/Ask your questions and help others out/), - ).toBeInTheDocument() - - expect( - wrapper.getByText(/No questions have been asked yet/), - ).toBeInTheDocument() - expect( - wrapper.getByRole('link', { name: 'Ask a question' }), - ).toHaveAttribute('href', '/questions/create') - }) - }) - - it('renders the question listing', async () => { - let wrapper - const questionTitle = faker.lorem.words(3) - const questionSlug = faker.lorem.slug() - - questionService.search = vi.fn(() => { - return new Promise((resolve) => { - resolve({ - items: [ - { - ...FactoryQuestionItem({ - title: questionTitle, - slug: questionSlug, - }), - _id: '123', - }, - ], - total: 1, - lastVisible: undefined, - }) - }) - }) - - act(() => { - wrapper = renderFn('/questions') - }) - - await waitFor(async () => { - expect( - wrapper.getByText(/Ask your questions and help others out/), - ).toBeInTheDocument() - - expect(wrapper.getByText(questionTitle)).toBeInTheDocument() - }) - }) - }) - - describe('/questions/:slug', () => { - it('renders the question single page', async () => { - let wrapper - const question = FactoryQuestionItem() - const mockFetchQuestionBySlug = vi.fn().mockResolvedValue(question) - const mockIncrementViewCount = vi.fn() - - ;(useQuestionStore as Mock).mockReturnValue({ - ...mockQuestionStore, - fetchQuestionBySlug: mockFetchQuestionBySlug, - activeUser: mockActiveUser, - incrementViewCount: mockIncrementViewCount, - }) - - act(() => { - wrapper = renderFn(`/questions/${question.slug}`) - }) - expect(wrapper.getByText(/loading/)).toBeInTheDocument() - - await waitFor(async () => { - expect(() => wrapper.getByText(/loading/)).toThrow() - expect(wrapper.queryByTestId('question-title')).toHaveTextContent( - question.title, - ) - expect( - wrapper.getByText( - new RegExp(`^${question.description.split(' ')[0]}`), - ), - ).toBeInTheDocument() - - // Content statistics - expect(wrapper.getByText(`0 views`)).toBeInTheDocument() - expect(wrapper.getByText(`0 following`)).toBeInTheDocument() - expect(wrapper.getByText(`0 useful`)).toBeInTheDocument() - - expect(mockFetchQuestionBySlug).toBeCalledWith(question.slug) - expect(mockIncrementViewCount).toBeCalledWith(question) - }) - }) - - describe('Follow', () => { - it('displays following status', async () => { - const user = FactoryUser() - const question = FactoryQuestionItem({ - subscribers: [user.userName], - }) - const mockFetchQuestionBySlug = vi.fn().mockResolvedValue(question) - ;(useQuestionStore as Mock).mockReturnValue({ - ...mockQuestionStore, - activeUser: user, - fetchQuestionBySlug: mockFetchQuestionBySlug, - userHasSubscribed: true, - }) - - let wrapper - act(() => { - wrapper = renderFn(`/questions/${question.slug}`) - }) - - await waitFor( - () => { - expect(wrapper.getByText('Following')).toBeInTheDocument() - }, - { - timeout: 2000, - }, - ) - }) - - it('supports follow behaviour', async () => { - let wrapper - const question = FactoryQuestionItem() - const mockFetchQuestionBySlug = vi.fn().mockResolvedValue(question) - ;(useQuestionStore as Mock).mockReturnValue({ - ...mockQuestionStore, - fetchQuestionBySlug: mockFetchQuestionBySlug, - }) - - act(() => { - wrapper = renderFn(`/questions/${question.slug}`) - }) - - await waitFor( - () => { - expect(wrapper.getByText('Follow')).toBeInTheDocument() - }, - { - timeout: 2000, - }, - ) - }) - }) - - it('does not show edit call to action', async () => { - let wrapper - mockActiveUser = FactoryUser() - const question = FactoryQuestionItem() - const mockFetchQuestionBySlug = vi.fn().mockResolvedValue(question) - ;(useQuestionStore as Mock).mockReturnValue({ - ...mockQuestionStore, - fetchQuestionBySlug: mockFetchQuestionBySlug, - activeUser: mockActiveUser, - userCanEditQuestion: false, - }) - - act(() => { - wrapper = renderFn(`/questions/${question.slug}`) - }) - - // Ability to edit - await waitFor(async () => { - expect(() => wrapper.getByText(/Edit/)).toThrow() - }) - }) - - it('shows edit call to action', async () => { - let wrapper - mockActiveUser = FactoryUser() - const question = FactoryQuestionItem({ - _createdBy: mockActiveUser.userName, - }) - - const mockFetchQuestionBySlug = vi.fn().mockResolvedValue(question) - - ;(useQuestionStore as Mock).mockReturnValue({ - ...mockQuestionStore, - fetchQuestionBySlug: mockFetchQuestionBySlug, - activeUser: mockActiveUser, - }) - - act(() => { - wrapper = renderFn(`/questions/${question.slug}`) - }) - - // Ability to edit - await waitFor(async () => { - expect(wrapper.getByText(/Edit/)).toBeInTheDocument() - }) - }) - }) - - describe('/questions/:slug/edit', () => { - const editFormTitle = /Edit your question/ - it('renders the question edit page', async () => { - let wrapper - act(() => { - wrapper = renderFn('/questions/slug/edit') - }) - - await waitFor(() => { - expect(wrapper.getByText(editFormTitle)).toBeInTheDocument() - }) - }) - - it('allows admin access', async () => { - let wrapper - - mockActiveUser = FactoryUser({ - userName: 'not-author', - userRoles: [UserRole.ADMIN], - }) - - const questionItem = FactoryQuestionItem({ - slug: 'slug', - title: faker.lorem.words(1), - _createdBy: 'author', - }) - const mockUpsertQuestion = vi.fn().mockResolvedValue({ - slug: 'question-title', - }) - - ;(useQuestionStore as Mock).mockReturnValue({ - ...mockQuestionStore, - fetchQuestionBySlug: vi.fn().mockResolvedValue(questionItem), - upsertQuestion: mockUpsertQuestion, - activeUser: mockActiveUser, - }) - - act(() => { - wrapper = renderFn('/questions/slug/edit') - }) - - await waitFor(async () => { - await new Promise((r) => setTimeout(r, 500)) - expect(wrapper.getByText(editFormTitle)).toBeInTheDocument() - expect(screen.getByDisplayValue(questionItem.title)).toBeInTheDocument() - expect(() => wrapper.getByText('Draft')).toThrow() - }) - - // Fill in form - const title = wrapper.getByLabelText('The Question', { exact: false }) - const description = wrapper.getByLabelText('Description', { - exact: false, - }) - const submitButton = wrapper.getByText('Update') - - // Submit form - await userEvent.clear(title) - await userEvent.type(title, 'Question title') - await userEvent.clear(description) - await userEvent.type(description, 'Question description') - - act(() => { - submitButton.click() - }) - - expect(mockUpsertQuestion).toHaveBeenCalledWith( - expect.objectContaining({ - title: 'Question title', - description: 'Question description', - _createdBy: 'author', - }), - ) - }) - - it('redirects non-author', async () => { - let wrapper - mockActiveUser = FactoryUser({ userName: 'not-author' }) - ;(useQuestionStore as Mock).mockReturnValue({ - ...mockQuestionStore, - fetchQuestionBySlug: vi.fn().mockResolvedValue( - FactoryQuestionItem({ - slug: 'slug', - _createdBy: 'author', - }), - ), - activeUser: mockActiveUser, - }) - - act(() => { - wrapper = renderFn('/questions/slug/edit') - }) - - await waitFor(() => { - expect(() => wrapper.getByText(editFormTitle)).toThrow() - expect(mockedUsedNavigate).toBeCalledWith('/questions/slug') - }) - }) - }) -}, 15000) - -const renderFn = (url: string) => { - const router = createMemoryRouter( - createRoutesFromElements( - {questionRouteElements}, - ), - { - initialEntries: [url], - }, - ) - - return render( - - - - - , - ) -} diff --git a/src/pages/Question/question.routes.tsx b/src/pages/Question/question.routes.tsx deleted file mode 100644 index ed27c6b8cb..0000000000 --- a/src/pages/Question/question.routes.tsx +++ /dev/null @@ -1,41 +0,0 @@ -import { Suspense } from 'react' -import { Route, Routes } from 'react-router-dom' - -import { AuthRoute } from '../common/AuthRoute' -import { QuestionCreate } from './QuestionCreate' -import { QuestionEdit } from './QuestionEdit' -import { QuestionListing } from './QuestionListing' -import { QuestionPage } from './QuestionPage' - -export const questionRouteElements = ( - <> - } /> - } /> - - - - } - /> - - - - } - /> - -) - -const QuestionRoutes = () => { - return ( - }> - {questionRouteElements} - - ) -} - -export default QuestionRoutes diff --git a/src/pages/Question/question.service.ts b/src/pages/Question/question.service.ts index 780ca9bf40..49bb2df6ad 100644 --- a/src/pages/Question/question.service.ts +++ b/src/pages/Question/question.service.ts @@ -20,8 +20,7 @@ import type { QueryFilterConstraint, QueryNonFilterConstraint, } from 'firebase/firestore' -import type { IQuestion } from '../../models' -import type { ICategory } from '../../models/categories.model' +import type { ICategory, IQuestion } from 'oa-shared' import type { QuestionSortOption } from './QuestionSortOptions' export enum QuestionSearchParams { diff --git a/src/pages/Research/Content/Common/Research.form.tsx b/src/pages/Research/Content/Common/Research.form.tsx index 0fb976109f..069cb1d52a 100644 --- a/src/pages/Research/Content/Common/Research.form.tsx +++ b/src/pages/Research/Content/Common/Research.form.tsx @@ -9,12 +9,11 @@ import { FieldTextarea, ResearchEditorOverview, } from 'oa-components' -import { IModerationStatus } from 'oa-shared' +import { IModerationStatus, researchStatusOptions } from 'oa-shared' import IconHeaderHowto from 'src/assets/images/header-section/howto-header-icon.svg' import { SelectField } from 'src/common/Form/Select.field' import { TagsSelectField } from 'src/common/Form/TagsSelect.field' import { UnsavedChangesDialog } from 'src/common/Form/UnsavedChangesDialog' -import { researchStatusOptions } from 'src/models/research.models' import { ResearchErrors, ResearchPostingGuidelines, @@ -42,8 +41,8 @@ import { import { buttons, headings, overview } from '../../labels' import ResearchFieldCategory from './ResearchCategorySelect' +import type { IResearch } from 'oa-shared' import type { MainFormAction } from 'src/common/Form/types' -import type { IResearch } from 'src/models/research.models' interface IProps { 'data-testid'?: string diff --git a/src/pages/Research/Content/Common/ResearchUpdate.form.test.tsx b/src/pages/Research/Content/Common/ResearchUpdate.form.test.tsx index 01edb12a4f..980adb677f 100644 --- a/src/pages/Research/Content/Common/ResearchUpdate.form.test.tsx +++ b/src/pages/Research/Content/Common/ResearchUpdate.form.test.tsx @@ -1,12 +1,8 @@ import '@testing-library/jest-dom/vitest' -import { - createMemoryRouter, - createRoutesFromElements, - Route, - RouterProvider, -} from 'react-router-dom' +import { createMemoryRouter, RouterProvider } from 'react-router-dom' import { ThemeProvider } from '@emotion/react' +import { createRoutesFromElements, Route } from '@remix-run/react' import { render } from '@testing-library/react' import { FactoryResearchItemUpdate } from 'src/test/factories/ResearchItem' import { testingThemeStyles } from 'src/test/utils/themeUtils' diff --git a/src/pages/Research/Content/Common/ResearchUpdate.form.tsx b/src/pages/Research/Content/Common/ResearchUpdate.form.tsx index 4f35897e14..5dbfff57e3 100644 --- a/src/pages/Research/Content/Common/ResearchUpdate.form.tsx +++ b/src/pages/Research/Content/Common/ResearchUpdate.form.tsx @@ -27,8 +27,8 @@ import { TitleField } from '../CreateResearch/Form/TitleField' import { ResearchErrors } from './ResearchErrors' import { UpdateSubmitStatus } from './SubmitStatus' +import type { IResearch } from 'oa-shared' import type { MainFormAction } from 'src/common/Form/types' -import type { IResearch } from 'src/models/research.models' const CONFIRM_DIALOG_MSG = 'You have unsaved changes. Are you sure you want to leave this page?' diff --git a/src/pages/Research/Content/Common/SubmitStatus.tsx b/src/pages/Research/Content/Common/SubmitStatus.tsx index 57185467ab..f867205b60 100644 --- a/src/pages/Research/Content/Common/SubmitStatus.tsx +++ b/src/pages/Research/Content/Common/SubmitStatus.tsx @@ -1,4 +1,4 @@ -import { useNavigate } from 'react-router-dom' +import { useNavigate } from '@remix-run/react' import { observer } from 'mobx-react' import { Button, Icon, Modal } from 'oa-components' import { useResearchStore } from 'src/stores/Research/research.store' diff --git a/src/pages/Research/Content/CreateResearch/Template.tsx b/src/pages/Research/Content/CreateResearch/Template.tsx index 0cb82f8ded..01fee9f00c 100644 --- a/src/pages/Research/Content/CreateResearch/Template.tsx +++ b/src/pages/Research/Content/CreateResearch/Template.tsx @@ -1,4 +1,4 @@ -import type { IResearch } from 'src/models/research.models' +import type { IResearch } from 'oa-shared' const INITIAL_VALUES: Partial = { tags: {}, diff --git a/src/pages/Research/Content/CreateResearch/index.tsx b/src/pages/Research/Content/CreateResearch/index.tsx index 2c63a24cd9..df7d23ea0c 100644 --- a/src/pages/Research/Content/CreateResearch/index.tsx +++ b/src/pages/Research/Content/CreateResearch/index.tsx @@ -6,7 +6,7 @@ import { Flex, Text } from 'theme-ui' import TEMPLATE from './Template' -import type { IUser } from 'src/models/user.models' +import type { IUser } from 'oa-shared' const CreateResearch = observer(() => { const { userStore } = useCommonStores().stores diff --git a/src/pages/Research/Content/CreateUpdate/CreateUpdate.tsx b/src/pages/Research/Content/CreateUpdate/CreateUpdate.tsx index 854914039e..7a62c0984c 100644 --- a/src/pages/Research/Content/CreateUpdate/CreateUpdate.tsx +++ b/src/pages/Research/Content/CreateUpdate/CreateUpdate.tsx @@ -1,5 +1,5 @@ import React from 'react' -import { Navigate, useParams } from 'react-router-dom' +import { Navigate, useParams } from '@remix-run/react' import { observer } from 'mobx-react' import { Loader } from 'oa-components' import { useResearchStore } from 'src/stores/Research/research.store' diff --git a/src/pages/Research/Content/CreateUpdate/Template.tsx b/src/pages/Research/Content/CreateUpdate/Template.tsx index 2b49ca5f89..a214a8973d 100644 --- a/src/pages/Research/Content/CreateUpdate/Template.tsx +++ b/src/pages/Research/Content/CreateUpdate/Template.tsx @@ -1,6 +1,6 @@ import { ResearchUpdateStatus } from 'oa-shared' -import type { IResearch } from 'src/models/research.models' +import type { IResearch } from 'oa-shared' const INITIAL_VALUES: Partial = { title: '', diff --git a/src/pages/Research/Content/EditResearch/index.tsx b/src/pages/Research/Content/EditResearch/index.tsx index 85552d7adb..540b6b661d 100644 --- a/src/pages/Research/Content/EditResearch/index.tsx +++ b/src/pages/Research/Content/EditResearch/index.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { useNavigate, useParams } from 'react-router-dom' +import { useNavigate, useParams } from '@remix-run/react' import { toJS } from 'mobx' import { observer } from 'mobx-react' import { BlockedRoute, Loader } from 'oa-components' @@ -10,8 +10,7 @@ import { Text } from 'theme-ui' import { logger } from '../../../../logger' -import type { IResearch } from 'src/models/research.models' -import type { IUser } from 'src/models/user.models' +import type { IResearch, IUser } from 'oa-shared' interface IState { formValues: IResearch.ItemDB | null diff --git a/src/pages/Research/Content/EditUpdate/index.tsx b/src/pages/Research/Content/EditUpdate/index.tsx index f6bf45326a..2c9ee27818 100644 --- a/src/pages/Research/Content/EditUpdate/index.tsx +++ b/src/pages/Research/Content/EditUpdate/index.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { useNavigate, useParams } from 'react-router-dom' +import { useNavigate, useParams } from '@remix-run/react' import { toJS } from 'mobx' import { observer } from 'mobx-react' import { BlockedRoute, Loader } from 'oa-components' @@ -8,8 +8,7 @@ import { useResearchStore } from 'src/stores/Research/research.store' import { isAllowedToEditContent } from 'src/utils/helpers' import { Text } from 'theme-ui' -import type { IResearch } from 'src/models/research.models' -import type { IUser } from 'src/models/user.models' +import type { IResearch, IUser } from 'oa-shared' interface IState { formValues: IResearch.UpdateDB diff --git a/src/pages/Research/Content/ResearchArticle.test.tsx b/src/pages/Research/Content/ResearchArticle.test.tsx index b437c93cf6..0d9f4d2ef1 100644 --- a/src/pages/Research/Content/ResearchArticle.test.tsx +++ b/src/pages/Research/Content/ResearchArticle.test.tsx @@ -1,13 +1,9 @@ import '@testing-library/jest-dom/vitest' -import { - createMemoryRouter, - createRoutesFromElements, - Route, - RouterProvider, -} from 'react-router-dom' +import { createMemoryRouter, RouterProvider } from 'react-router-dom' import { ThemeProvider } from '@emotion/react' import { faker } from '@faker-js/faker' +import { createRoutesFromElements, Route } from '@remix-run/react' import { act, render, waitFor, within } from '@testing-library/react' import { formatDistanceToNow } from 'date-fns' import { Provider } from 'mobx-react' diff --git a/src/pages/Research/Content/ResearchArticle.tsx b/src/pages/Research/Content/ResearchArticle.tsx index dd681b43c2..c27a40ca44 100644 --- a/src/pages/Research/Content/ResearchArticle.tsx +++ b/src/pages/Research/Content/ResearchArticle.tsx @@ -1,5 +1,5 @@ import React, { useEffect } from 'react' -import { Link, useLocation, useParams } from 'react-router-dom' +import { Link, useLocation, useParams } from '@remix-run/react' import { observer } from 'mobx-react' import { ArticleCallToAction, @@ -24,7 +24,7 @@ import { isAllowedToDeleteContent, isAllowedToEditContent, } from 'src/utils/helpers' -import { seoTagsUpdate } from 'src/utils/seo' +import { seoTagsUpdate, SeoTagsUpdateComponent } from 'src/utils/seo' import { Box, Flex } from 'theme-ui' import { @@ -34,8 +34,7 @@ import { import ResearchDescription from './ResearchDescription' import ResearchUpdate from './ResearchUpdate' -import type { IUser } from 'src/models' -import type { IUploadedFileMeta } from 'src/stores/storage' +import type { IUploadedFileMeta, IUser } from 'oa-shared' const areCommentsVisible = (updateId) => { return updateId === getResearchUpdateId(window.location.hash) @@ -188,6 +187,12 @@ const ResearchArticle = observer(() => { return ( + + { diff --git a/src/pages/Research/Content/ResearchListItem.tsx b/src/pages/Research/Content/ResearchListItem.tsx index 06ac728600..603c410a70 100644 --- a/src/pages/Research/Content/ResearchListItem.tsx +++ b/src/pages/Research/Content/ResearchListItem.tsx @@ -20,8 +20,7 @@ import { Box, Card, Flex, Grid, Heading, Image, Text } from 'theme-ui' import defaultResearchThumbnail from '../../../assets/images/default-research-thumbnail.jpg' import { researchStatusColour } from '../researchHelpers' -import type { IResearch } from 'src/models/research.models' -import type { IUploadedFileMeta } from 'src/stores/storage' +import type { IResearch, IUploadedFileMeta } from 'oa-shared' interface IProps { item: IResearch.Item diff --git a/src/pages/Research/Content/ResearchUpdate.tsx b/src/pages/Research/Content/ResearchUpdate.tsx index 7ad699f007..63fd633ca3 100644 --- a/src/pages/Research/Content/ResearchUpdate.tsx +++ b/src/pages/Research/Content/ResearchUpdate.tsx @@ -1,4 +1,4 @@ -import { Link, useNavigate } from 'react-router-dom' +import { Link, useNavigate } from '@remix-run/react' import { Button, DisplayDate, @@ -20,7 +20,7 @@ import { Box, Card, Flex, Heading, Text } from 'theme-ui' import { ResearchLinkToUpdate } from './ResearchLinkToUpdate' import { ResearchUpdateDiscussion } from './ResearchUpdateDiscussion' -import type { IResearch } from 'src/models/research.models' +import type { IResearch } from 'oa-shared' interface IProps { update: IResearch.Update diff --git a/src/pages/Research/Content/ResearchUpdateDiscussion.tsx b/src/pages/Research/Content/ResearchUpdateDiscussion.tsx index 50b0847604..44e646cbdd 100644 --- a/src/pages/Research/Content/ResearchUpdateDiscussion.tsx +++ b/src/pages/Research/Content/ResearchUpdateDiscussion.tsx @@ -1,7 +1,7 @@ import { useState } from 'react' import { DiscussionWrapper } from 'src/common/DiscussionWrapper' -import type { IResearch } from 'src/models' +import type { IResearch } from 'oa-shared' interface IProps { update: IResearch.Update diff --git a/src/pages/Research/index.tsx b/src/pages/Research/index.tsx deleted file mode 100644 index 62911453a7..0000000000 --- a/src/pages/Research/index.tsx +++ /dev/null @@ -1,35 +0,0 @@ -import { useCommonStores } from 'src/common/hooks/useCommonStores' -import { MODULE } from 'src/modules' -import { - ResearchStore, - ResearchStoreContext, -} from 'src/stores/Research/research.store' - -import ResearchRoutes from './research.routes' - -import type { IPageMeta } from 'src/pages/PageList' - -/** - * Wraps the research module routing elements with the research module provider - */ -const ResearchModuleContainer = () => { - const rootStore = useCommonStores() - return ( - - - - ) -} - -/** - * Default export format used for integrating with the platform - * @description The research module enables users to share ongoing updates for - * experimental projects - */ -export const ResearchModule: IPageMeta = { - moduleName: MODULE.RESEARCH, - path: '/research', - component: , - title: 'Research', - description: 'Welcome to research', -} diff --git a/src/pages/Research/research.routes.test.tsx b/src/pages/Research/research.routes.test.tsx deleted file mode 100644 index a3a530b56d..0000000000 --- a/src/pages/Research/research.routes.test.tsx +++ /dev/null @@ -1,640 +0,0 @@ -import '@testing-library/jest-dom/vitest' - -import { Suspense } from 'react' -import { - createMemoryRouter, - createRoutesFromElements, - Route, - RouterProvider, -} from 'react-router-dom' -import { ThemeProvider } from '@emotion/react' -import { faker } from '@faker-js/faker' -import { act, cleanup, render, waitFor } from '@testing-library/react' -import { Provider } from 'mobx-react' -import { UserRole } from 'oa-shared' -import { useResearchStore } from 'src/stores/Research/research.store' -import { - FactoryResearchItem, - FactoryResearchItemUpdate, -} from 'src/test/factories/ResearchItem' -import { FactoryUser } from 'src/test/factories/User' -import { testingThemeStyles } from 'src/test/utils/themeUtils' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' - -import { researchRouteElements } from './research.routes' -import { researchService } from './research.service' - -import type { ResearchStore } from 'src/stores/Research/research.store' -import type { Mock } from 'vitest' - -const Theme = testingThemeStyles -const mockActiveUser = FactoryUser() - -vi.mock('src/pages/Research/research.service', () => ({ - researchService: { - search: vi.fn(), - getDraftCount: vi.fn(), - getDrafts: vi.fn(), - getResearchCategories: vi.fn(() => []), - }, -})) - -// Similar to issues in Academy.test.tsx - stub methods called in user store constructor -// TODO - replace with mock store or avoid direct call -vi.mock('src/common/hooks/useCommonStores', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - __esModule: true, - useCommonStores: () => ({ - stores: { - userStore: { - fetchAllVerifiedUsers: vi.fn(), - user: mockActiveUser, - }, - aggregationsStore: { - isVerified: vi.fn(), - users_verified: { - HowtoAuthor: true, - }, - }, - tagsStore: {}, - }, - }), -})) - -const mockedUsedNavigate = vi.fn() -vi.mock('react-router-dom', async () => ({ - ...((await vi.importActual('react-router-dom')) as any), - useNavigate: () => mockedUsedNavigate, -})) - -/** When mocking research routes replace default store methods with below */ -class MockResearchStoreClass implements Partial { - setActiveResearchItemBySlug = vi.fn() - needsModeration = vi.fn().mockResolvedValue(true) - incrementViewCount = vi.fn() - activeResearchItem = FactoryResearchItem({ - title: 'Research article title', - updates: [], - _createdBy: 'jasper', - }) - researchUploadStatus = {} as any - updateUploadStatus = {} as any - formatResearchCommentList = vi.fn() - getActiveResearchUpdateComments = vi.fn() - lockResearchItem = vi.fn() - lockResearchUpdate = vi.fn() - unlockResearchUpdate = vi.fn() - unlockResearchItem = vi.fn() - - get activeUser() { - return { - name: 'Jaasper', - userName: 'jasper', - userRoles: [UserRole.ADMIN], - } as any - } - get filteredResearches() { - return [] - } -} -const mockResearchStore = new MockResearchStoreClass() - -vi.mock('src/stores/Research/research.store') - -describe('research.routes', () => { - beforeEach(() => { - ;(useResearchStore as Mock).mockReturnValue(mockResearchStore) - }) - - afterEach(() => { - vi.restoreAllMocks() - cleanup() - }) - - describe('/research/', () => { - it('renders the research listing', async () => { - const researchTitle = faker.lorem.words(3) - const researchSlug = faker.lorem.slug() - - researchService.search = vi.fn(() => { - return new Promise((resolve) => { - resolve({ - items: [ - { - ...FactoryResearchItem({ - title: researchTitle, - slug: researchSlug, - }), - _id: '123', - }, - ], - total: 1, - lastVisible: undefined, - }) - }) - }) - - let wrapper - act(() => { - wrapper = renderFn('/research') - }) - - await waitFor( - () => - expect( - wrapper.getByText(/Help out with Research & Development/), - ).toBeInTheDocument(), - { - timeout: 10000, - }, - ) - }) - }) - - describe('/research/:slug', () => { - it('renders an individual research article', async () => { - let wrapper - act(() => { - wrapper = renderFn('/research/research-slug') - }) - - await waitFor( - () => { - expect(wrapper.queryByTestId('research-title')).toHaveTextContent( - 'Research article title', - ) - }, - { - timeout: 10000, - }, - ) - }) - }) - - describe('/research/create', () => { - it('rejects a request without a user present', async () => { - mockActiveUser.userRoles = [] - let wrapper - act(() => { - wrapper = renderFn('/research/create') - }) - - await waitFor( - () => { - expect( - wrapper.getByText(/role required to access this page/), - ).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - - it('rejects a logged in user missing required role', async () => { - let wrapper - act(() => { - wrapper = renderFn('/research/create') - }) - - await waitFor( - () => { - expect( - wrapper.getByText(/role required to access this page/), - ).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - - it('accepts a logged in user with required role [research_creator]', async () => { - let wrapper - act(() => { - mockActiveUser.userRoles = [UserRole.RESEARCH_CREATOR] - - wrapper = renderFn('/research/create') - }) - - await waitFor( - () => { - expect(wrapper.getByText(/start your research/i)).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - - it('accepts a logged in user with required role [research_editor]', async () => { - let wrapper - act(() => { - mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] - - wrapper = renderFn('/research/create') - }) - await waitFor( - () => { - expect(wrapper.getByText(/start your research/i)).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - }) - - describe('/research/:slug/edit', () => { - it('rejects a request without a user present', async () => { - mockActiveUser.userRoles = [] - - let wrapper - act(() => { - wrapper = renderFn('/research/an-example/edit') - }) - - await waitFor( - () => { - expect( - wrapper.getByText(/role required to access this page/), - ).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - - it('accepts a logged in user with required role', async () => { - let wrapper - act(() => { - mockActiveUser.userName = 'Jaasper' - mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] - - wrapper = renderFn('/research/an-example/edit') - }) - - await waitFor( - () => { - expect(wrapper.getByText(/edit your research/i)).toBeInTheDocument() - }, - { timeout: 10000 }, - ) - }) - - it('rejects a logged in user with required role but not author of document', async () => { - mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] - - // Arrange - ;(useResearchStore as Mock).mockReturnValue({ - ...mockResearchStore, - activeUser: mockActiveUser, - activeResearchItem: FactoryResearchItem({ - collaborators: undefined, - slug: 'an-example', - }), - }) - - act(() => { - renderFn('/research/an-example/edit') - }) - - await waitFor( - () => { - expect(mockedUsedNavigate).toHaveBeenCalledWith( - '/research/an-example', - ) - }, - { - timeout: 10000, - }, - ) - }) - - it('blocks a valid editor when document is locked by another user', async () => { - mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] - ;(useResearchStore as Mock).mockReturnValue({ - ...mockResearchStore, - activeUser: mockActiveUser, - activeResearchItem: FactoryResearchItem({ - collaborators: [mockActiveUser.userName], - slug: 'research-slug', - locked: { - by: 'jasper', // user_id - at: new Date().toISOString(), - }, - }), - }) - - let wrapper - act(() => { - wrapper = renderFn('/research/an-example/edit') - }) - - await waitFor( - () => { - expect( - wrapper.getByText( - 'The research description is currently being edited by another editor.', - ), - ).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - - it('accepts a user when document is mark locked by them', async () => { - mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] - ;(useResearchStore as Mock).mockReturnValue({ - ...mockResearchStore, - activeUser: mockActiveUser, - activeResearchItem: FactoryResearchItem({ - collaborators: [mockActiveUser.userName], - slug: 'research-slug', - locked: { - by: mockActiveUser.userName, - at: new Date().toISOString(), - }, - }), - }) - - let wrapper - act(() => { - wrapper = renderFn('/research/an-example/edit') - }) - - await waitFor( - () => { - expect(wrapper.getByText('Edit your Research')).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - - it('accepts a user with required role and contributor acccess', async () => { - mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] - ;(useResearchStore as Mock).mockReturnValue({ - ...mockResearchStore, - activeUser: mockActiveUser, - activeResearchItem: FactoryResearchItem({ - collaborators: [mockActiveUser.userName], - slug: 'research-slug', - }), - }) - - let wrapper - act(() => { - wrapper = renderFn('/research/an-example/edit') - }) - - await waitFor( - () => { - expect(wrapper.getByText(/edit your research/i)).toBeInTheDocument() - }, - { timeout: 10000 }, - ) - }) - }) - - describe('/research/:slug/new-update', () => { - it('rejects a request without a user present', async () => { - mockActiveUser.userRoles = [] - - let wrapper - await act(() => { - wrapper = renderFn('/research/an-example/new-update') - }) - - await waitFor( - () => { - expect( - wrapper.getByText(/role required to access this page/), - ).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - - it('accepts a logged in user with required role', async () => { - let wrapper - await act(() => { - mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] - wrapper = renderFn('/research/an-example/new-update') - }) - - await waitFor( - () => { - expect(wrapper.getByTestId('EditResearchUpdate')).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - }) - - describe('/research/:slug/edit-update/:id', () => { - it('rejects a request without a user present', async () => { - mockActiveUser.userRoles = [] - const wrapper = renderFn( - '/research/an-example/edit-update/nested-research-update', - ) - - await waitFor(() => { - expect( - wrapper.getByText(/role required to access this page/), - ).toBeInTheDocument() - }) - }) - - it('accept logged in author present', async () => { - mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] - // Arrange - ;(useResearchStore as Mock).mockReturnValue({ - ...mockResearchStore, - activeUser: mockActiveUser, - activeResearchItem: FactoryResearchItem({ - collaborators: undefined, - _createdBy: mockActiveUser.userName, - slug: 'an-example', - updates: [ - FactoryResearchItemUpdate({ - _id: 'nested-research-update', - }), - ], - }), - }) - - let wrapper - act(() => { - wrapper = renderFn( - '/research/an-example/edit-update/nested-research-update', - ) - }) - - await waitFor( - () => { - expect(wrapper.getByTestId(/EditResearchUpdate/i)).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - - it('blocks valid author when document is locked', async () => { - // Arrange - mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] - ;(useResearchStore as Mock).mockReturnValue({ - ...mockResearchStore, - activeUser: mockActiveUser, - activeResearchItem: FactoryResearchItem({ - collaborators: undefined, - _createdBy: mockActiveUser.userName, - slug: 'an-example', - updates: [ - FactoryResearchItemUpdate({ - _id: 'nested-research-update', - locked: { - by: 'jasper', // user_id - at: new Date().toISOString(), - }, - }), - ], - }), - }) - - let wrapper - act(() => { - wrapper = renderFn( - '/research/an-example/edit-update/nested-research-update', - ) - }) - - await waitFor(() => { - expect( - wrapper.getByText( - /This research update is currently being edited by another editor/, - ), - ).toBeInTheDocument() - }) - }) - - it('accepts a user when document is mark locked by them', async () => { - mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] - ;(useResearchStore as Mock).mockReturnValue({ - ...mockResearchStore, - activeUser: mockActiveUser, - activeResearchItem: FactoryResearchItem({ - collaborators: [mockActiveUser.userName], - slug: 'research-slug', - updates: [ - FactoryResearchItemUpdate({ - _id: 'nested-research-update', - locked: { - by: mockActiveUser.userName, - at: new Date().toISOString(), - }, - }), - ], - }), - }) - - let wrapper - act(() => { - wrapper = renderFn( - '/research/an-example/edit-update/nested-research-update', - ) - }) - - await waitFor( - () => { - expect(wrapper.getByText('Edit your update')).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - - it('rejects logged in user who is not author', async () => { - mockActiveUser.userRoles = [] - - let wrapper - act(() => { - wrapper = renderFn( - '/research/an-example/edit-update/nested-research-update', - ) - }) - - await waitFor(() => { - expect( - wrapper.getByText(/role required to access this page/), - ).toBeInTheDocument() - }) - }) - - it('accept logged in user who is collaborator', async () => { - mockActiveUser.userRoles = [UserRole.RESEARCH_EDITOR] - - // Arrange - ;(useResearchStore as Mock).mockReturnValue({ - ...mockResearchStore, - activeUser: mockActiveUser, - activeResearchItem: FactoryResearchItem({ - collaborators: [mockActiveUser.userName], - slug: 'an-example', - updates: [ - FactoryResearchItemUpdate({ - _id: 'nested-research-update', - }), - ], - }), - }) - - let wrapper - act(() => { - wrapper = renderFn( - '/research/an-example/edit-update/nested-research-update', - ) - }) - - await waitFor( - () => { - expect(wrapper.getByTestId(/EditResearchUpdate/i)).toBeInTheDocument() - }, - { - timeout: 10000, - }, - ) - }) - }) -}) - -const renderFn = (url: string) => { - const router = createMemoryRouter( - createRoutesFromElements( - {researchRouteElements}, - ), - { - initialEntries: [url], - }, - ) - - return render( - }> - - - - - - , - ) -} diff --git a/src/pages/Research/research.routes.tsx b/src/pages/Research/research.routes.tsx deleted file mode 100644 index 2b4af31437..0000000000 --- a/src/pages/Research/research.routes.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import { lazy, Suspense } from 'react' -import { Route, Routes } from 'react-router-dom' -import { isPreciousPlastic } from 'src/config/config' - -import { AuthRoute } from '../common/AuthRoute' -import { RESEARCH_EDITOR_ROLES } from './constants' - -const CreateResearch = lazy(() => import('./Content/CreateResearch')) -const CreateUpdate = lazy(() => import('./Content/CreateUpdate/CreateUpdate')) -const ResearchItemEditor = lazy(() => import('./Content/EditResearch')) -const UpdateItemEditor = lazy(() => import('./Content/EditUpdate')) -const ResearchArticle = lazy(() => import('./Content/ResearchArticle')) -const ResearchList = lazy(() => import('./Content/ResearchList')) - -const getRandomInt = (max: number) => { - return Math.floor(Math.random() * max) -} - -const roles = isPreciousPlastic() ? [] : RESEARCH_EDITOR_ROLES - -export const researchRouteElements = ( - <> - } /> - - - - } - /> - - - - } - /> - - - - } - /> - - - - } - /> - } - /> - -) - -const routes = () => ( - }> - {researchRouteElements} - -) - -export default routes diff --git a/src/pages/Research/research.service.ts b/src/pages/Research/research.service.ts index d0ae0d8baf..0c38104a62 100644 --- a/src/pages/Research/research.service.ts +++ b/src/pages/Research/research.service.ts @@ -20,9 +20,7 @@ import type { QueryFilterConstraint, QueryNonFilterConstraint, } from 'firebase/firestore' -import type { ResearchStatus } from 'oa-shared' -import type { IResearch } from '../../models' -import type { ICategory } from '../../models/categories.model' +import type { ICategory, IResearch, ResearchStatus } from 'oa-shared' import type { ResearchSortOption } from './ResearchSortOptions' const search = async ( diff --git a/src/pages/Research/researchHelpers.test.ts b/src/pages/Research/researchHelpers.test.ts index ca9a54bc2a..ea230aa072 100644 --- a/src/pages/Research/researchHelpers.test.ts +++ b/src/pages/Research/researchHelpers.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest' import { researchUpdateStatusFilter } from './researchHelpers' -import type { IResearch, IUserDB } from 'src/models' +import type { IResearch, IUserDB } from 'oa-shared' describe('Research Helpers', () => { describe('Research Update Status Filter', () => { diff --git a/src/pages/Research/researchHelpers.ts b/src/pages/Research/researchHelpers.ts index 210918033b..e851d9fc81 100644 --- a/src/pages/Research/researchHelpers.ts +++ b/src/pages/Research/researchHelpers.ts @@ -1,6 +1,6 @@ import { ResearchStatus, ResearchUpdateStatus, UserRole } from 'oa-shared' -import type { IResearch, IUserDB } from 'src/models' +import type { IResearch, IUserDB } from 'oa-shared' export const researchUpdateStatusFilter = ( item: IResearch.Item, diff --git a/src/pages/SignIn/SignIn.tsx b/src/pages/SignIn/SignIn.tsx index 8a2fc8b3c1..921d4000d0 100644 --- a/src/pages/SignIn/SignIn.tsx +++ b/src/pages/SignIn/SignIn.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from 'react' import { Field, Form } from 'react-final-form' -import { Link, useNavigate } from 'react-router-dom' +import { Link, useNavigate } from '@remix-run/react' import { observer } from 'mobx-react' import { Button, FieldInput, HeroBanner, TextNotification } from 'oa-components' import { getFriendlyMessage } from 'oa-shared' diff --git a/src/pages/SignUp/SignUp.tsx b/src/pages/SignUp/SignUp.tsx index 42bb8563e4..5736e40284 100644 --- a/src/pages/SignUp/SignUp.tsx +++ b/src/pages/SignUp/SignUp.tsx @@ -1,6 +1,6 @@ import { useState } from 'react' import { Field, Form } from 'react-final-form' -import { Link, Navigate, useNavigate } from 'react-router-dom' +import { Link, Navigate, useNavigate } from '@remix-run/react' import { observer } from 'mobx-react' import { Button, ExternalLink, FieldInput, HeroBanner } from 'oa-components' import { FRIENDLY_MESSAGES } from 'oa-shared' diff --git a/src/pages/SignUp/SignUpMessage.tsx b/src/pages/SignUp/SignUpMessage.tsx index 9f2561d801..4ac45af0d0 100644 --- a/src/pages/SignUp/SignUpMessage.tsx +++ b/src/pages/SignUp/SignUpMessage.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router-dom' +import { Link } from '@remix-run/react' import { Button, HeroBanner } from 'oa-components' import { Card, Flex, Heading, Text } from 'theme-ui' diff --git a/src/pages/Unsubscribe/Unsubscribe.tsx b/src/pages/Unsubscribe/Unsubscribe.tsx index 913309a24e..c7d1be22ab 100644 --- a/src/pages/Unsubscribe/Unsubscribe.tsx +++ b/src/pages/Unsubscribe/Unsubscribe.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { Route, Routes, useParams } from 'react-router-dom' +import { Route, Routes, useParams } from '@remix-run/react' import { observer } from 'mobx-react' import { useCommonStores } from 'src/common/hooks/useCommonStores' import { logger } from 'src/logger' diff --git a/src/pages/User/contact/UserContactForm.tsx b/src/pages/User/contact/UserContactForm.tsx index fd6ae7b6e9..124f21c940 100644 --- a/src/pages/User/contact/UserContactForm.tsx +++ b/src/pages/User/contact/UserContactForm.tsx @@ -14,7 +14,7 @@ import { messageService } from 'src/services/message.service' import { isUserContactable } from 'src/utils/helpers' import { Box, Flex, Heading } from 'theme-ui' -import type { IUser } from 'src/models' +import type { IUser } from 'oa-shared' interface Props { user: IUser @@ -23,14 +23,16 @@ interface Props { type SubmitResults = { type: 'success' | 'error'; message: string } export const UserContactForm = observer(({ user }: Props) => { - if (!isUserContactable(user)) return null - const { userStore } = useCommonStores().stores + const [submitResults, setSubmitResults] = useState(null) - if (!userStore.activeUser) - return + if (!isUserContactable(user)) { + return null + } - const [submitResults, setSubmitResults] = useState(null) + if (!userStore.activeUser) { + return + } const { button, title, successMessage } = contact const buttonName = 'contact-submit' diff --git a/src/pages/User/contact/UserContactNotLoggedIn.tsx b/src/pages/User/contact/UserContactNotLoggedIn.tsx index ad76d47ba6..5f24fd9341 100644 --- a/src/pages/User/contact/UserContactNotLoggedIn.tsx +++ b/src/pages/User/contact/UserContactNotLoggedIn.tsx @@ -1,4 +1,4 @@ -import { Link } from 'react-router-dom' +import { Link } from '@remix-run/react' import { Button } from 'oa-components' import { Alert, Flex, Text } from 'theme-ui' diff --git a/src/pages/User/content/MemberProfile.tsx b/src/pages/User/content/MemberProfile.tsx index 21909c52d0..3df1d9e4b1 100644 --- a/src/pages/User/content/MemberProfile.tsx +++ b/src/pages/User/content/MemberProfile.tsx @@ -9,7 +9,7 @@ import { Avatar, Box, Card, Flex, Heading, Paragraph } from 'theme-ui' import UserContactAndLinks from './UserContactAndLinks' import UserCreatedDocuments from './UserCreatedDocuments' -import type { IUserDB } from 'src/models' +import type { IUserDB } from 'oa-shared' import type { UserCreatedDocs } from '../types' interface IProps { diff --git a/src/pages/User/content/SpaceProfile.tsx b/src/pages/User/content/SpaceProfile.tsx index 83e5d73b61..f553249e5f 100644 --- a/src/pages/User/content/SpaceProfile.tsx +++ b/src/pages/User/content/SpaceProfile.tsx @@ -41,9 +41,9 @@ import UserCreatedDocuments from './UserCreatedDocuments' import type { IMAchineBuilderXp, IOpeningHours, + IUser, PlasticTypeLabel, } from 'oa-shared' -import type { IUser } from 'src/models' import type { UserCreatedDocs } from '../types' interface IProps { diff --git a/src/pages/User/content/UserCreatedDocumentsItem.tsx b/src/pages/User/content/UserCreatedDocumentsItem.tsx index a23d441205..1cd7e4b4cc 100644 --- a/src/pages/User/content/UserCreatedDocumentsItem.tsx +++ b/src/pages/User/content/UserCreatedDocumentsItem.tsx @@ -1,5 +1,5 @@ import * as React from 'react' -import { Link } from 'react-router-dom' +import { Link } from '@remix-run/react' import { Icon } from 'oa-components' import { Flex, Heading, Text } from 'theme-ui' diff --git a/src/pages/User/content/UserProfile.tsx b/src/pages/User/content/UserProfile.tsx index 1baae2a9da..45470c4595 100644 --- a/src/pages/User/content/UserProfile.tsx +++ b/src/pages/User/content/UserProfile.tsx @@ -1,5 +1,5 @@ import { useEffect, useState } from 'react' -import { useParams } from 'react-router-dom' +import { useParams } from '@remix-run/react' import { observer } from 'mobx-react-lite' import { Button, InternalLink, Loader } from 'oa-components' import { ProfileTypeList } from 'oa-shared' @@ -11,7 +11,7 @@ import { logger } from '../../../logger' import { MemberProfile } from './MemberProfile' import { SpaceProfile } from './SpaceProfile' -import type { IUserDB } from 'src/models' +import type { IUserDB } from 'oa-shared' import type { UserCreatedDocs } from '../types' /** diff --git a/src/pages/User/impact/Impact.tsx b/src/pages/User/impact/Impact.tsx index a916b8101d..b2cd4b3334 100644 --- a/src/pages/User/impact/Impact.tsx +++ b/src/pages/User/impact/Impact.tsx @@ -3,7 +3,7 @@ import { Flex } from 'theme-ui' import { IMPACT_YEARS } from './constants' import { ImpactItem } from './ImpactItem' -import type { IUser, IUserImpact } from 'src/models' +import type { IUser, IUserImpact } from 'oa-shared' interface Props { impact: IUserImpact | undefined diff --git a/src/pages/User/impact/ImpactField.tsx b/src/pages/User/impact/ImpactField.tsx index da622145c7..1e5036272f 100644 --- a/src/pages/User/impact/ImpactField.tsx +++ b/src/pages/User/impact/ImpactField.tsx @@ -4,7 +4,7 @@ import { Box, Text } from 'theme-ui' import { ImpactIcon } from './ImpactIcon' -import type { IImpactDataField } from 'src/models' +import type { IImpactDataField } from 'oa-shared' interface Props { field: IImpactDataField diff --git a/src/pages/User/impact/ImpactIcon.tsx b/src/pages/User/impact/ImpactIcon.tsx index b63e581291..c2ecae5bba 100644 --- a/src/pages/User/impact/ImpactIcon.tsx +++ b/src/pages/User/impact/ImpactIcon.tsx @@ -1,7 +1,7 @@ import { Icon } from 'oa-components' import { impactQuestions } from 'src/pages/UserSettings/content/impactQuestions' -import type { IImpactDataField } from 'src/models' +import type { IImpactDataField } from 'oa-shared' interface Props { id: IImpactDataField['id'] diff --git a/src/pages/User/impact/ImpactItem.tsx b/src/pages/User/impact/ImpactItem.tsx index dc3eb84658..3e38b674a4 100644 --- a/src/pages/User/impact/ImpactItem.tsx +++ b/src/pages/User/impact/ImpactItem.tsx @@ -4,7 +4,7 @@ import { Box, Heading } from 'theme-ui' import { ImpactField } from './ImpactField' import { ImpactMissing } from './ImpactMissing' -import type { IImpactYear, IImpactYearFieldList, IUser } from 'src/models' +import type { IImpactYear, IImpactYearFieldList, IUser } from 'oa-shared' interface Props { year: IImpactYear diff --git a/src/pages/User/impact/ImpactMissing.tsx b/src/pages/User/impact/ImpactMissing.tsx index c71f7ae5fe..5b0e55241e 100644 --- a/src/pages/User/impact/ImpactMissing.tsx +++ b/src/pages/User/impact/ImpactMissing.tsx @@ -7,7 +7,7 @@ import { Flex, Text } from 'theme-ui' import { IMPACT_REPORT_LINKS } from './constants' import { invisible, missing, reportYearLabel } from './labels' -import type { IImpactYear, IImpactYearFieldList, IUser } from 'src/models' +import type { IImpactYear, IImpactYearFieldList, IUser } from 'oa-shared' interface Props { fields: IImpactYearFieldList | undefined diff --git a/src/pages/User/impact/constants.ts b/src/pages/User/impact/constants.ts index a85a8a587c..b62c9c7816 100644 --- a/src/pages/User/impact/constants.ts +++ b/src/pages/User/impact/constants.ts @@ -1,4 +1,4 @@ -import type { IImpactYear } from 'src/models' +import type { IImpactYear } from 'oa-shared' export const IMPACT_REPORT_LINKS = { 2022: 'https://www.preciousplastic.com/impact/2023', diff --git a/src/pages/User/user.routes.test.tsx b/src/pages/User/user.routes.test.tsx index 30705d78e8..8eaaa6e932 100644 --- a/src/pages/User/user.routes.test.tsx +++ b/src/pages/User/user.routes.test.tsx @@ -1,11 +1,8 @@ import '@testing-library/jest-dom/vitest' -import { - createMemoryRouter, - createRoutesFromElements, - RouterProvider, -} from 'react-router-dom' +import { createMemoryRouter, RouterProvider } from 'react-router-dom' import { ThemeProvider } from '@emotion/react' +import { createRoutesFromElements } from '@remix-run/react' import { act, render, waitFor } from '@testing-library/react' import { Provider } from 'mobx-react' import { useCommonStores } from 'src/common/hooks/useCommonStores' @@ -38,13 +35,6 @@ vi.mock('src/common/hooks/useCommonStores', () => ({ HowtoAuthor: true, }, }, - themeStore: { - currentTheme: { - styles: { - communityProgramURL: '', - }, - }, - }, mapsStore: { getPin: mockGetPin, }, diff --git a/src/pages/User/user.routes.tsx b/src/pages/User/user.routes.tsx index 5f43f408f5..9e1fe01dc9 100644 --- a/src/pages/User/user.routes.tsx +++ b/src/pages/User/user.routes.tsx @@ -1,4 +1,4 @@ -import { Route, Routes } from 'react-router-dom' +import { Route, Routes } from '@remix-run/react' import { UserRole } from 'oa-shared' import { AuthRoute } from '../common/AuthRoute' diff --git a/src/pages/User/workspace/Workspace.tsx b/src/pages/User/workspace/Workspace.tsx index c5b6fcf614..43df7253d6 100644 --- a/src/pages/User/workspace/Workspace.tsx +++ b/src/pages/User/workspace/Workspace.tsx @@ -7,8 +7,6 @@ import MemberHighlight from 'src/assets/images/highlights/highlight-member.svg' import WorkspaceHighlight from 'src/assets/images/highlights/highlight-workspace.svg' import { getSupportedProfileTypes } from 'src/modules/profile' -import type { PlatformTheme } from 'oa-themes' - const findWordspaceHighlight = (workspaceType?: string): string => { switch (workspaceType) { case ProfileTypeList.WORKSPACE: @@ -53,13 +51,12 @@ const findWorkspaceBadge = ( workspaceType?: string, ifCleanImage?: boolean, verifiedUser?: boolean, - currentTheme?: PlatformTheme, ): string => { if (!workspaceType) { return MemberBadge } - const foundProfileTypeObj = getSupportedProfileTypes(currentTheme).find( + const foundProfileTypeObj = getSupportedProfileTypes().find( (type) => type.label === workspaceType, ) if (foundProfileTypeObj) { diff --git a/src/pages/UserSettings/SettingsPageMapPin.test.tsx b/src/pages/UserSettings/SettingsPageMapPin.test.tsx index 0fa6d46d9f..d24c3b4309 100644 --- a/src/pages/UserSettings/SettingsPageMapPin.test.tsx +++ b/src/pages/UserSettings/SettingsPageMapPin.test.tsx @@ -9,7 +9,7 @@ import { describe, expect, it, vi } from 'vitest' import { FormProvider } from './__mocks__/FormProvider' import { SettingsPageMapPin } from './SettingsPageMapPin' -import type { ILocation } from 'src/models' +import type { ILocation } from 'oa-shared' let mockUser = FactoryUser() let mockPin = FactoryMapPin() diff --git a/src/pages/UserSettings/SettingsPageMapPin.tsx b/src/pages/UserSettings/SettingsPageMapPin.tsx index 4c1b3945e4..b52b78e763 100644 --- a/src/pages/UserSettings/SettingsPageMapPin.tsx +++ b/src/pages/UserSettings/SettingsPageMapPin.tsx @@ -25,7 +25,7 @@ import { Alert, Box, Flex, Heading, Text } from 'theme-ui' import { SettingsFormNotifications } from './content/SettingsFormNotifications' import { MAX_PIN_LENGTH } from './constants' -import type { ILocation, IMapPin, IUserDB } from 'src/models' +import type { ILocation, IMapPin, IUserDB } from 'oa-shared' import type { IFormNotification } from './content/SettingsFormNotifications' interface IPinProps { @@ -145,13 +145,16 @@ const DeleteMapPin = (props: IPropsDeletePin) => { } export const SettingsPageMapPin = () => { + const communityProgramUrl = + import.meta.env.VITE_COMMUNITY_PROGRAM_URL || + process.env.VITE_COMMUNITY_PROGRAM_URL const [mapPin, setMapPin] = useState() const [isLoading, setIsLoading] = useState(true) const [notification, setNotification] = useState< IFormNotification | undefined >(undefined) - const { mapsStore, themeStore, userStore } = useCommonStores().stores + const { mapsStore, userStore } = useCommonStores().stores const user = userStore.activeUser const { addPinTitle, yourPinTitle } = headings.map @@ -246,7 +249,7 @@ export const SettingsPageMapPin = () => { {headings.workspace.description} diff --git a/src/pages/UserSettings/SettingsPageUserProfile.tsx b/src/pages/UserSettings/SettingsPageUserProfile.tsx index 5a87b7e7f4..bbaeb85ebe 100644 --- a/src/pages/UserSettings/SettingsPageUserProfile.tsx +++ b/src/pages/UserSettings/SettingsPageUserProfile.tsx @@ -23,7 +23,7 @@ import { SettingsFormNotifications } from './content/SettingsFormNotifications' import { DEFAULT_PUBLIC_CONTACT_PREFERENCE } from './constants' import { buttons } from './labels' -import type { IUser } from 'src/models' +import type { IUser } from 'oa-shared' import type { IFormNotification } from './content/SettingsFormNotifications' export const SettingsPageUserProfile = () => { diff --git a/src/pages/UserSettings/__mocks__/FormProvider.tsx b/src/pages/UserSettings/__mocks__/FormProvider.tsx index 827461d149..2bb87c85c6 100644 --- a/src/pages/UserSettings/__mocks__/FormProvider.tsx +++ b/src/pages/UserSettings/__mocks__/FormProvider.tsx @@ -11,7 +11,7 @@ import { useCommonStores } from 'src/common/hooks/useCommonStores' import { testingThemeStyles } from 'src/test/utils/themeUtils' import { vi } from 'vitest' -import type { IUserDB } from 'src/models' +import type { IUserDB } from 'oa-shared' const Theme = testingThemeStyles diff --git a/src/pages/UserSettings/content/fields/ImpactYearDisplay.field.tsx b/src/pages/UserSettings/content/fields/ImpactYearDisplay.field.tsx index c542a227f4..78ce160eb7 100644 --- a/src/pages/UserSettings/content/fields/ImpactYearDisplay.field.tsx +++ b/src/pages/UserSettings/content/fields/ImpactYearDisplay.field.tsx @@ -4,7 +4,7 @@ import { ImpactField } from 'src/pages/User/impact/ImpactField' import { buttons, missingData } from 'src/pages/UserSettings/labels' import { Box, Flex, Text } from 'theme-ui' -import type { IImpactYearFieldList } from 'src/models' +import type { IImpactYearFieldList } from 'oa-shared' interface Props { fields: IImpactYearFieldList | undefined diff --git a/src/pages/UserSettings/content/fields/PatreonIntegration.test.tsx b/src/pages/UserSettings/content/fields/PatreonIntegration.test.tsx index 94f2ffb6ed..2589630f5d 100644 --- a/src/pages/UserSettings/content/fields/PatreonIntegration.test.tsx +++ b/src/pages/UserSettings/content/fields/PatreonIntegration.test.tsx @@ -13,7 +13,7 @@ import { UPDATE_BUTTON_TEXT, } from './PatreonIntegration' -import type { IUser } from 'src/models' +import type { IUser } from 'oa-shared' const mockUser = { userName: 'test', diff --git a/src/pages/UserSettings/content/sections/Collection.section.tsx b/src/pages/UserSettings/content/sections/Collection.section.tsx index f202674a14..21d06da873 100644 --- a/src/pages/UserSettings/content/sections/Collection.section.tsx +++ b/src/pages/UserSettings/content/sections/Collection.section.tsx @@ -15,8 +15,7 @@ import { FlexSectionContainer } from '../elements' import { CustomCheckbox } from '../fields/CustomCheckbox.field' import { OpeningHoursPicker } from '../fields/OpeningHoursPicker.field' -import type { IPlasticType } from 'oa-shared' -import type { IUser } from 'src/models' +import type { IPlasticType, IUser } from 'oa-shared' interface IProps { formValues: IUser diff --git a/src/pages/UserSettings/content/sections/EmailNotifications.section.tsx b/src/pages/UserSettings/content/sections/EmailNotifications.section.tsx index 977227d09f..3112bd45a9 100644 --- a/src/pages/UserSettings/content/sections/EmailNotifications.section.tsx +++ b/src/pages/UserSettings/content/sections/EmailNotifications.section.tsx @@ -5,7 +5,7 @@ import { Select } from 'oa-components' import { EmailNotificationFrequency } from 'oa-shared' import { FieldContainer } from 'src/common/Form/FieldContainer' -import type { INotificationSettings } from 'src/models/user.models' +import type { INotificationSettings } from 'oa-shared' interface IProps { notificationSettings?: INotificationSettings diff --git a/src/pages/UserSettings/content/sections/Focus.section.tsx b/src/pages/UserSettings/content/sections/Focus.section.tsx index d26a61d8d2..90248442d5 100644 --- a/src/pages/UserSettings/content/sections/Focus.section.tsx +++ b/src/pages/UserSettings/content/sections/Focus.section.tsx @@ -11,6 +11,9 @@ import { CustomRadioField } from '../fields/CustomRadio.field' import type { ProfileTypeName } from 'oa-shared' const ProfileTypes = () => { + const profileGuidelinesUrl = + import.meta.env.VITE_PROFILE_GUIDELINES_URL || + process.env.VITE_PROFILE_GUIDELINES_URL const { description, error } = fields.activities const theme = useTheme() const profileTypes = getSupportedProfileTypes().filter(({ label }) => @@ -31,7 +34,7 @@ const ProfileTypes = () => { {description}{' '} diff --git a/src/pages/UserSettings/content/sections/ImpactYear.section.tsx b/src/pages/UserSettings/content/sections/ImpactYear.section.tsx index 44d12aa2f2..3e20b40616 100644 --- a/src/pages/UserSettings/content/sections/ImpactYear.section.tsx +++ b/src/pages/UserSettings/content/sections/ImpactYear.section.tsx @@ -1,6 +1,6 @@ import { useEffect, useRef, useState } from 'react' import { Form } from 'react-final-form' -import { useLocation } from 'react-router-dom' +import { useLocation } from '@remix-run/react' import { observer } from 'mobx-react' import { useCommonStores } from 'src/common/hooks/useCommonStores' import { UserContactError } from 'src/pages/User/contact' @@ -15,7 +15,7 @@ import { import { ImpactYearField } from '../fields/ImpactYear.field' import { ImpactYearDisplayField } from '../fields/ImpactYearDisplay.field' -import type { IImpactYear, IImpactYearFieldList } from 'src/models' +import type { IImpactYear, IImpactYearFieldList } from 'oa-shared' import type { SubmitResults } from 'src/pages/User/contact/UserContactError' interface Props { diff --git a/src/pages/UserSettings/content/sections/PublicContact.section.tsx b/src/pages/UserSettings/content/sections/PublicContact.section.tsx index 03aeaabaa1..fb55204b93 100644 --- a/src/pages/UserSettings/content/sections/PublicContact.section.tsx +++ b/src/pages/UserSettings/content/sections/PublicContact.section.tsx @@ -4,7 +4,7 @@ import { fields } from 'src/pages/UserSettings/labels' import { isContactable } from 'src/utils/helpers' import { Flex, Heading, Switch, Text } from 'theme-ui' -import type { IUser } from 'src/models' +import type { IUser } from 'oa-shared' interface Props { isContactableByPublic: IUser['isContactableByPublic'] diff --git a/src/pages/UserSettings/content/sections/UserImages.section.tsx b/src/pages/UserSettings/content/sections/UserImages.section.tsx index 59c91d78e4..b6d73fda63 100644 --- a/src/pages/UserSettings/content/sections/UserImages.section.tsx +++ b/src/pages/UserSettings/content/sections/UserImages.section.tsx @@ -4,8 +4,7 @@ import { ImageInputField } from 'src/common/Form/ImageInput.field' import { fields, headings } from 'src/pages/UserSettings/labels' import { Box, Flex, Heading, Text } from 'theme-ui' -import type { IUser } from 'src/models' -import type { IUploadedFileMeta } from 'src/stores/storage' +import type { IUploadedFileMeta, IUser } from 'oa-shared' interface IProps { values: IUser diff --git a/src/pages/UserSettings/content/sections/UserInfos.section.tsx b/src/pages/UserSettings/content/sections/UserInfos.section.tsx index 950e06e0fb..f2ba6b3de1 100644 --- a/src/pages/UserSettings/content/sections/UserInfos.section.tsx +++ b/src/pages/UserSettings/content/sections/UserInfos.section.tsx @@ -1,6 +1,6 @@ import { Field } from 'react-final-form' import { FieldArray } from 'react-final-form-arrays' -import { countries } from 'countries-list' +import countriesList from 'countries-list' import { Button, FieldInput, FieldTextarea, InternalLink } from 'oa-components' import { ProfileTypeList } from 'oa-shared' import { SelectField } from 'src/common/Form/Select.field' @@ -16,13 +16,14 @@ import { import { FlexSectionContainer } from '../elements' import { ProfileLinkField } from '../fields/ProfileLink.field' -import type { IUser } from 'src/models' +import type { IUser } from 'oa-shared' interface IProps { formValues: Partial } export const UserInfosSection = ({ formValues }: IProps) => { + const { countries } = countriesList const { profileType, links, location } = formValues const isMemberProfile = profileType === ProfileTypeList.MEMBER const { about, country, displayName, userName } = fields diff --git a/src/pages/UserSettings/index.tsx b/src/pages/UserSettings/index.tsx index 513a2cd4d3..74d9b55d87 100644 --- a/src/pages/UserSettings/index.tsx +++ b/src/pages/UserSettings/index.tsx @@ -5,7 +5,7 @@ import { useCommonStores } from 'src/common/hooks/useCommonStores' import { SettingsPage } from './SettingsPage' -import type { IUser } from 'src/models/user.models' +import type { IUser } from 'oa-shared' const Settings = observer(() => { const { userStore } = useCommonStores().stores diff --git a/src/pages/UserSettings/utils.ts b/src/pages/UserSettings/utils.ts index 0733887457..0c3b68a0c8 100644 --- a/src/pages/UserSettings/utils.ts +++ b/src/pages/UserSettings/utils.ts @@ -1,6 +1,6 @@ import { impactQuestions } from './content/impactQuestions' -import type { IImpactDataField, IImpactYearFieldList } from 'src/models' +import type { IImpactDataField, IImpactYearFieldList } from 'oa-shared' import type { IImpactQuestion } from './content/impactQuestions' export interface ImpactDataFieldInputs { diff --git a/src/pages/common/AuthRoute.tsx b/src/pages/common/AuthRoute.tsx index 193b564818..10518c8e1b 100644 --- a/src/pages/common/AuthRoute.tsx +++ b/src/pages/common/AuthRoute.tsx @@ -1,4 +1,4 @@ -import { Navigate } from 'react-router-dom' +import { Navigate } from '@remix-run/react' import { observer } from 'mobx-react' import { BlockedRoute } from 'oa-components' import { AuthWrapper } from 'src/common/AuthWrapper' diff --git a/src/pages/common/Breadcrumbs/Breadcrumbs.tsx b/src/pages/common/Breadcrumbs/Breadcrumbs.tsx index 7ee3b8b27f..f5296a5547 100644 --- a/src/pages/common/Breadcrumbs/Breadcrumbs.tsx +++ b/src/pages/common/Breadcrumbs/Breadcrumbs.tsx @@ -1,6 +1,6 @@ import { Breadcrumbs as BreadcrumbsComponent } from 'oa-components' -import type { IHowto, IQuestion, IResearch } from 'src/models' +import type { IHowto, IQuestion, IResearch } from 'oa-shared' type Step = { text: string; link?: string } diff --git a/src/pages/common/Category/CategoriesSelectV2.tsx b/src/pages/common/Category/CategoriesSelectV2.tsx index e40650f852..fd4d5d5598 100644 --- a/src/pages/common/Category/CategoriesSelectV2.tsx +++ b/src/pages/common/Category/CategoriesSelectV2.tsx @@ -2,7 +2,7 @@ import { Select } from 'oa-components' import { FieldContainer } from '../../../common/Form/FieldContainer' -import type { ICategory } from 'src/models/categories.model' +import type { ICategory } from 'oa-shared' export type SelectValue = { label: string; value: string | ICategory } diff --git a/src/pages/common/DevSiteHeader/DevSiteHeader.tsx b/src/pages/common/DevSiteHeader/DevSiteHeader.tsx index 2b36df5cc9..1b603e79ea 100644 --- a/src/pages/common/DevSiteHeader/DevSiteHeader.tsx +++ b/src/pages/common/DevSiteHeader/DevSiteHeader.tsx @@ -1,8 +1,8 @@ import { observer } from 'mobx-react-lite' import { Select } from 'oa-components' import { UserRole } from 'oa-shared' -import { useCommonStores } from 'src/common/hooks/useCommonStores' -import { DEV_SITE_ROLE, SITE, VERSION } from 'src/config/config' +import { SITE, VERSION } from 'src/config/config' +import { getDevSiteRole } from 'src/config/devSiteConfig' import { Box, Flex, Text } from 'theme-ui' /** @@ -10,14 +10,12 @@ import { Box, Flex, Text } from 'theme-ui' * version of the platform, and provide the option to toggle between different dev sites */ const DevSiteHeader = observer(() => { - const { themeStore } = useCommonStores().stores - const theme = themeStore.currentTheme.styles return ( <> {showDevSiteHeader() && ( { options={siteRoles} placeholder="Role" defaultValue={ - siteRoles.find((s) => s.value === DEV_SITE_ROLE) || + siteRoles.find((s) => s.value === getDevSiteRole()) || siteRoles[0] } onChange={(s: any) => setSiteRole(s.value)} @@ -65,23 +63,6 @@ const DevSiteHeader = observer(() => { />
- - - Theme: - - -