diff --git a/.env.example b/.env.example deleted file mode 100644 index 57afba9aee..0000000000 --- a/.env.example +++ /dev/null @@ -1,42 +0,0 @@ -COMPOSE_PROFILES=backend,frontend - -CELERY_TASK_ALWAYS_EAGER=True -DJANGO_LOG_LEVEL=INFO -LOG_LEVEL=info -MITOPEN_BASE_URL=http://od.odl.local:8063 -MITOPEN_COOKIE_NAME=discussions -MITOPEN_COOKIE_DOMAIN=odl.local -MITOPEN_JWT_SECRET= -MITOPEN_USE_S3=False -MITOPEN_AXIOS_WITH_CREDENTIALS=False -MITOPEN_AXIOS_BASE_PATH="" -INDEXING_API_USERNAME=mitodl -MAILGUN_SENDER_DOMAIN= -MAILGUN_KEY= -MAILGUN_RECIPIENT_OVERRIDE= -STATUS_TOKEN= -OPENSEARCH_INDEX=discussions_local -OPENSEARCH_INDEXING_CHUNK_SIZE=100 -AWS_ACCESS_KEY_ID= -AWS_SECRET_ACCESS_KEY= -AWS_STORAGE_BUCKET_NAME= -SOCIAL_AUTH_MICROMASTERS_LOGIN_URL= -YOUTUBE_DEVELOPER_KEY= -TIKA_SERVER_ENDPOINT=http://tika:9998/ -TIKA_CLIENT_ONLY=True -WEBPACK_ANALYZE=False - -SOCIAL_AUTH_OL_OIDC_OIDC_ENDPOINT= -SOCIAL_AUTH_OL_OIDC_KEY= -SOCIAL_AUTH_OL_OIDC_SECRET= -AUTHORIZATION_URL= -ACCESS_TOKEN_URL= -USERINFO_URL= -KEYCLOAK_BASE_URL= -KEYCLOAK_REALM_NAME= - -POSTHOG_PROJECT_ID= -POSTHOG_PROJECT_API_KEY= -POSTHOG_PERSONAL_API_KEY= -POSTHOG_HOST=https://app.posthog.com -POSTHOG_TIMEOUT_MS=1500 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 478ee415e9..d17a5de1dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: - 9200:9200 steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - name: update apt run: sudo apt-get update -y @@ -85,7 +85,7 @@ jobs: CELERY_BROKER_URL: redis://localhost:6379/4 CELERY_RESULT_BACKEND: redis://localhost:6379/4 TIKA_CLIENT_ONLY: "True" - MITOPEN_BASE_URL: http://localhost:8063/ + MITOPEN_APP_BASE_URL: http://localhost:8062/ MAILGUN_KEY: fake_mailgun_key MAILGUN_SENDER_DOMAIN: other.fake.site OPENSEARCH_INDEX: testindex @@ -101,7 +101,7 @@ jobs: javascript-tests: runs-on: ubuntu-latest steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 with: node-version: "^20" @@ -123,7 +123,7 @@ jobs: - name: Webpack build run: yarn run build env: - MITOPEN_AXIOS_BASE_PATH: https://api.mitopen-test.odl.mit.edu + MITOPEN_API_BASE_URL: https://api.mitopen-test.odl.mit.edu - name: Lints run: yarn run lint-check @@ -156,7 +156,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 with: @@ -167,25 +167,8 @@ jobs: - name: Install dependencies run: yarn install - - name: Copy static assets - run: | - mkdir -p frontends/mit-open/build/static; - cp -R frontends/mit-open/public/ frontends/mit-open/build/static; - - - run: PUBLIC_URL=/mit-open yarn build-github-pages - working-directory: frontends - - - run: PUBLIC_URL=/mit-open yarn build-storybook --output-dir ../github-pages/build/storybook - working-directory: frontends - - - name: Setup Pages - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 - - - name: Upload artifact - uses: actions/upload-artifact@65462800fd760344b1a7b4382951275a0abb4808 # v4 - with: - name: pages-site - path: frontends/github-pages/build + - name: Build Storybook + run: yarn workspace mit-open build-storybook openapi-generated-client-check-v0: # This job checks that the output of openapi-generator-typescript-axios that @@ -197,7 +180,7 @@ jobs: GENERATOR_OUTPUT_DIR_VC: ./frontends/api/src/generated/v0 runs-on: ubuntu-latest steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 with: node-version: "^20" @@ -236,7 +219,7 @@ jobs: GENERATOR_OUTPUT_DIR_VC: ./frontends/api/src/generated/v1 runs-on: ubuntu-latest steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 with: node-version: "^20" @@ -271,7 +254,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 with: diff --git a/.github/workflows/openapi-diff.yml b/.github/workflows/openapi-diff.yml index 1145023d81..3866b94472 100644 --- a/.github/workflows/openapi-diff.yml +++ b/.github/workflows/openapi-diff.yml @@ -5,12 +5,12 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout HEAD - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: ref: ${{ github.head_ref }} path: head - name: Checkout BASE - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: ref: ${{ github.base_ref }} path: base diff --git a/.github/workflows/production.yml b/.github/workflows/production.yml index 3ca4b0abc8..fbc1ca38c5 100644 --- a/.github/workflows/production.yml +++ b/.github/workflows/production.yml @@ -15,7 +15,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: ref: release @@ -41,7 +41,8 @@ jobs: POSTHOG_PROJECT_ID: ${{ secrets.POSTHOG_PROJECT_ID_PROD }} POSTHOG_PROJECT_API_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY_PROD }} MITOPEN_AXIOS_WITH_CREDENTIALS: true - MITOPEN_AXIOS_BASE_PATH: https://api.mitopen.odl.mit.edu + MITOPEN_API_BASE_URL: https://api.mitopen.odl.mit.edu + MITOPEN_SUPPORT_EMAIL: mitopen-support@mit.edu - uses: akhileshns/heroku-deploy@581dd286c962b6972d427fcf8980f60755c15520 with: diff --git a/.github/workflows/publish-pages.yml b/.github/workflows/publish-pages.yml new file mode 100644 index 0000000000..4e9d044a61 --- /dev/null +++ b/.github/workflows/publish-pages.yml @@ -0,0 +1,48 @@ +on: + # Runs on pushes targeting the default branch + push: + branches: [$default-branch] + + # Allows you to run this workflow manually from the Actions tab + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + + - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4 + with: + node-version: "^20" + cache: yarn + cache-dependency-path: yarn.lock + + - name: Install dependencies + run: yarn install + + - name: Build Storybook + run: yarn workspace mit-open build-storybook + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + with: + path: ./frontends/mit-open/storybook-static + + deploy: + needs: build + + permissions: + pages: write # to deploy to Pages + id-token: write # to verify the deployment originates from an appropriate source + + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + runs-on: ubuntu-latest + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/release-candidate.yml b/.github/workflows/release-candidate.yml index 097fdd442d..4d0454255f 100644 --- a/.github/workflows/release-candidate.yml +++ b/.github/workflows/release-candidate.yml @@ -15,7 +15,7 @@ jobs: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success' steps: - - uses: actions/checkout@0ad4b8fadaa221de15dcec353f45205ec38ea70b # v4 + - uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4 with: ref: release-candidate @@ -41,7 +41,8 @@ jobs: POSTHOG_PROJECT_ID: ${{ secrets.POSTHOG_PROJECT_ID_RC }} POSTHOG_PROJECT_API_KEY: ${{ secrets.POSTHOG_PROJECT_API_KEY_RC }} MITOPEN_AXIOS_WITH_CREDENTIALS: true - MITOPEN_AXIOS_BASE_PATH: https://api.mitopen-rc.odl.mit.edu + MITOPEN_API_BASE_URL: https://api.mitopen-rc.odl.mit.edu + MITOPEN_SUPPORT_EMAIL: odl-mitopen-rc-support@mit.edu - uses: akhileshns/heroku-deploy@581dd286c962b6972d427fcf8980f60755c15520 with: diff --git a/.gitignore b/.gitignore index d82f14b80e..05e6d520b2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,7 +9,6 @@ __pycache__/ # Distribution / packaging .Python -env/ build/ build-exports/ develop-eggs/ diff --git a/README.md b/README.md index 8740c88be8..dcca76c8b5 100644 --- a/README.md +++ b/README.md @@ -17,18 +17,25 @@ MIT Open follows the same [initial setup steps outlined in the common OL web app Run through those steps **including the addition of `/etc/hosts` aliases and the optional step for running the `createsuperuser` command**. -### Configure required `.env` settings +### Configuration + +Configuration can be put in the following filess which are gitignored: + +``` +mit-open/ + ├── env/ + │ ├── shared.local.env (provided to both frontend and backend containers) + │ ├── frontend.local.env (provided only to frontend containers) + │ └── backend.local.env (provided only to frontend containers) + └── .env (legacy file) +``` The following settings must be configured before running the app: - `COMPOSE_PROFILES` Controls which docker containers run. To run them all, use `COMPOSE_PROFILES=backend,frontend`. See [Frontend Development](./frontends/README.md) for more. - -- `INDEXING_API_USERNAME` - - At least to start out, this should be set to the username of the superuser - you created above. + This can be set either in a top-level `.env` that `docker compose` [automatically ingests](https://docs.docker.com/compose/environment-variables/envvars/#compose_env_files) or through any other method of setting an environment variable in your shell (e.g. `direnv`). - `MAILGUN_KEY` and `MAILGUN_SENDER_DOMAIN` @@ -216,7 +223,7 @@ Once these are set (and you've restarted the app), you should see events flowing A Javascript bundle of exported frontend components can be generated for use in external websites that have CORS allowance into a given instance of `mit-open`. There are a few settings you might want to change in order to get the expected results. - `MITOPEN_AXIOS_WITH_CREDENTIALS` - This sets `withCredentials: true` when initializing the Axios API, which tells the end user's browser to send along any browser level cookies for the current domain when making CORS requests -- `MITOPEN_AXIOS_BASE_PATH` - This sets the base path used for API requests, which will need to be set to a fully qualified url pointing to an instance of `mit-open` (i.e. https://mitopen.odl.mit.edu) in order for requests from the external site to reach the proper destination +- `MITOPEN_API_BASE_URL` - This sets the base url used for API requests, which will need to be set to a fully qualified url pointing to an instance of `mit-open` (i.e. https://mitopen.odl.mit.edu) in order for requests from the external site to reach the proper destination - `CORS_ALLOWED_ORIGINS`, `CSRF_TRUSTED_ORIGINS` - On the instance of `mit-open` that the externally hosted components will access via the API, the domains of any sites that need CORS access need to be here as a list of strings To build the bundle of exported components, run: @@ -247,6 +254,6 @@ $("#add-to-user-list-button").on("click", async (event) => { This is just an example, and you could input any `readable_id` to bring up a dialog to add any given `LearningResource` object to a `UserList`. -## GitHub Pages +## GitHub Pages Storybook -A static site for this repo with developer resources publishes to https://mitodl.github.io/mit-open/ during CI runs. +Demos and documentation of reusable UI components in this repo are published as a [storybook](https://storybook.js.org/) at https://mitodl.github.io/mit-open/. diff --git a/RELEASE.rst b/RELEASE.rst index 6c5e076eab..b3903d7f30 100644 --- a/RELEASE.rst +++ b/RELEASE.rst @@ -1,6 +1,37 @@ Release Notes ============= +Version 0.13.18 +--------------- + +- Fix logout view (#1236) +- remove manage widgets (#1239) +- Unit and detail page copy updates (#1235) +- Align departments listing colors to designs (#1238) +- resource drawer UI fixes (#1237) +- Remove "Top picks" carousel if no results (#1195) +- fix learning path count, increase item page size (#1230) +- Use ovewrite=True when calling pluggy function from upsert_offered_by (#1227) +- open resources in new tab (#1220) +- extra weight for instructors (#1231) +- Homepage and nav drawer copy edits (#1233) +- Update dependency eslint-plugin-jest to v28 (#1038) +- Only publish enrollable mitxonline courses (#1229) +- Navigation UI fixes (#1228) +- better spacing around pagination component (#1219) +- Update resource drawer text and URL for podcast episodes (#1191) +- resource type (#1222) +- Data fixtures app for loading static fixtures (#1218) +- Webpack build config loads .env files for running outside of Docker (#1221) +- Updates icons to use Remixicons where they don't already (#1157) +- make primary buttons shadowy, remove edge=none (#1213) +- resource category tabs (#1211) +- Fix storybook github pages publishing (#1200) +- Fix and reenable onboarding page tests (#1216) +- Removed nginx serving of frontend locally (#1179) +- Update actions/checkout digest to 692973e (#961) +- Privacy policy updates (#1208) + Version 0.13.17 (Released July 02, 2024) --------------- diff --git a/app.json b/app.json index a129046b63..d4e43ecf3d 100644 --- a/app.json +++ b/app.json @@ -4,9 +4,6 @@ { "url": "https://github.com/heroku/heroku-buildpack-apt" }, - { - "url": "https://github.com/heroku/heroku-buildpack-nodejs" - }, { "url": "https://github.com/moneymeets/python-poetry-buildpack" }, @@ -172,9 +169,6 @@ "description": "Prefix path for cached images generated by imagekit", "required": false }, - "INDEXING_API_USERNAME": { - "description": "Username used for indexing" - }, "INDEXING_ERROR_RETRIES": { "description": "Number of times to retry an indexing operation on failure", "required": false @@ -298,20 +292,12 @@ "description": "List of pluggy plugins to use for authentication", "required": false }, - "MITOPEN_AXIOS_WITH_CREDENTIALS": { - "description": "When building the Axios API, set defaults.withCredentials to this value", - "required": false - }, - "MITOPEN_AXIOS_BASE_PATH": { - "description": "The base path to use when making API requests", - "required": false - }, "MITOPEN_LEARNING_RESOURCES_PLUGINS": { "description": "List of pluggy plugins to use for learning resources", "required": false }, - "MITOPEN_BASE_URL": { - "description": "Base url to link users to in emails" + "MITOPEN_APP_BASE_URL": { + "description": "Base url to create links to the app" }, "MITOPEN_COOKIE_NAME": { "description": "Name of the cookie for the JWT auth token" diff --git a/authentication/views.py b/authentication/views.py index 7e8114494f..55f7cdd3cc 100644 --- a/authentication/views.py +++ b/authentication/views.py @@ -4,7 +4,6 @@ from django.conf import settings from django.contrib.auth import views -from django.http import Http404 from django.shortcuts import redirect from social_django.utils import load_strategy @@ -37,25 +36,38 @@ def _keycloak_logout_url(self, user): qs = urlencode( { "id_token_hint": id_token, - "post_logout_redirect_uri": settings.LOGOUT_REDIRECT_URL, + "post_logout_redirect_uri": self.request.build_absolute_uri( + settings.LOGOUT_REDIRECT_URL + ), } ) - return f"{settings.KEYCLOAK_BASE_URL}/realms/{settings.KEYCLOAK_REALM_NAME}/protocol/openid-connect/logout?{qs}" # noqa: E501 + + return ( + f"{settings.KEYCLOAK_BASE_URL}/realms/" + f"{settings.KEYCLOAK_REALM_NAME}/protocol/openid-connect/logout" + f"?{qs}" + ) def get( self, request, *args, # noqa: ARG002 **kwargs, # noqa: ARG002 - ): # pylint:disable=unused-argument + ): """ GET endpoint for loggin a user out. - Raises 404 if the user is not included in the request. + + The logout redirect path the user follows is: + + - api.example.com/logout (this view) + - keycloak.example.com/realms/REALM/protocol/openid-connect/logout + - api.example.com/app (see main/urls.py) + - app.example.com + """ user = getattr(request, "user", None) if user and user.is_authenticated: super().get(request) return redirect(self._keycloak_logout_url(user)) else: - msg = "Not currently logged in." - raise Http404(msg) + return redirect("/app") diff --git a/channels/models.py b/channels/models.py index c56967f5a6..22f7dd2308 100644 --- a/channels/models.py +++ b/channels/models.py @@ -1,8 +1,5 @@ """Models for channels""" -from urllib.parse import urljoin - -from django.conf import settings from django.contrib.auth.models import Group from django.core.validators import RegexValidator from django.db import models @@ -18,6 +15,7 @@ LearningResourceTopic, ) from main.models import TimestampedModel +from main.utils import frontend_absolute_url from profiles.utils import avatar_uri, banner_uri from widgets.models import WidgetList @@ -99,7 +97,7 @@ class Meta: @property def channel_url(self) -> str: """Return the channel url""" - return urljoin(settings.SITE_BASE_URL, f"/c/{self.channel_type}/{self.name}/") + return frontend_absolute_url(f"/c/{self.channel_type}/{self.name}/") class ChannelTopicDetail(TimestampedModel): diff --git a/channels/serializers_test.py b/channels/serializers_test.py index 30211e8571..cdd78d1dad 100644 --- a/channels/serializers_test.py +++ b/channels/serializers_test.py @@ -1,10 +1,8 @@ """Tests for channels.serializers""" from types import SimpleNamespace -from urllib.parse import urljoin import pytest -from django.conf import settings from django.core.files.uploadedfile import SimpleUploadedFile from channels.constants import CHANNEL_ROLE_MODERATORS, ChannelType @@ -37,6 +35,7 @@ ) from learning_resources.serializers import LearningResourceOfferorDetailSerializer from main.factories import UserFactory +from main.utils import frontend_absolute_url # pylint:disable=redefined-outer-name pytestmark = pytest.mark.django_db @@ -124,8 +123,8 @@ def test_serialize_channel( # pylint: disable=too-many-arguments "updated_on": mocker.ANY, "created_on": mocker.ANY, "id": channel.id, - "channel_url": urljoin( - settings.SITE_BASE_URL, f"/c/{channel.channel_type}/{channel.name}/" + "channel_url": frontend_absolute_url( + f"/c/{channel.channel_type}/{channel.name}/" ), "lists": [ LearningPathPreviewSerializer(channel_list.channel_list).data diff --git a/config/nginx.conf b/config/nginx.conf deleted file mode 100644 index f6f9bbc25a..0000000000 --- a/config/nginx.conf +++ /dev/null @@ -1,42 +0,0 @@ -# This is the version used in development environments -server { - server_name lemelsonx.mit.edu; - listen 8063; - return 301 https://open.mit.edu/c/lemelsoneducators; -} - -server { - server_name themove.mit.edu; - listen 8063; - return 301 https://open.mit.edu/c/themove; -} - -server { - listen 8063 default_server; - - root /src/frontends/mit-open/build; - - location / { - try_files /static$uri $uri @index; - } - - location ~ ^/program_letter/([0-9]+)/view$ { - try_files /index.html =404; - } - - location @index { - try_files /index.html =404; - } - - location = /.well-known/dnt-policy.txt { - return 204; - } - - location ~ ^/(api|login|complete/ol-oidc|logout|admin|static/admin|static/rest_framework|static/hijack|_/features/|scim/|o/|disconnect/|hijack/|podcasts/rss_feed|__debug__/|media/|profile/|program_letter/([0-9]+)/) { - include uwsgi_params; - uwsgi_pass web:8061; - uwsgi_pass_request_headers on; - uwsgi_pass_request_body on; - client_max_body_size 25M; - } -} diff --git a/config/nginx.conf.erb b/config/nginx.conf.erb index 29a7423481..04eb208e10 100644 --- a/config/nginx.conf.erb +++ b/config/nginx.conf.erb @@ -38,49 +38,15 @@ http { default_type application/octet-stream; sendfile on; - server { - server_name lemelsonx.mit.edu; - listen <%= ENV["PORT"] %>; - return 301 https://open.mit.edu/c/lemelsoneducators; - } - - server { - server_name themove.mit.edu; - listen <%= ENV["PORT"] %>; - return 301 https://open.mit.edu/c/themove; - } - - server { - server_name discussions.odl.mit.edu; - listen <%= ENV["PORT"] %>; - return 301 https://open.mit.edu$request_uri; - } - server { listen <%= ENV["PORT"] %> default_server; server_name _; - root /app; - - location / { - expires max; - try_files /frontends/mit-open/build/static$uri /frontends/mit-open/build$uri @index; - } - - location ~ ^/program_letter/([0-9]+)/view$ { - expires 1m; - try_files /frontends/mit-open/build/index.html =404; - } - - location @index { - expires 1m; - try_files /frontends/mit-open/build/index.html =404; - } location = /.well-known/dnt-policy.txt { return 204; } - location ~ ^/(api|login|complete/ol-oidc|logout|admin|static/admin|static/rest_framework|static/hijack|_/features/|hijack/|scim/|o/|disconnect/|podcasts/rss_feed|__debug__/|media/|profile/|program_letter/([0-9]+)/) { + location / { uwsgi_param QUERY_STRING $query_string; uwsgi_param REQUEST_METHOD $request_method; uwsgi_param CONTENT_TYPE $content_type; @@ -98,7 +64,7 @@ http { uwsgi_param X-Forwarded-Proto $http_x_forwarded_proto; uwsgi_param X-Forwarded-Port $http_x_forwarded_port; uwsgi_param X-Forwarded-Host $http_x_forwarded_host; - uwsgi_pass unix:/tmp/nginx.socket; + uwsgi_pass <%= ENV["NGINX_UWSGI_PASS"] || "unix:/tmp/nginx.socket" %>; uwsgi_pass_request_headers on; uwsgi_pass_request_body on; client_max_body_size 25M; diff --git a/config/static-app.conf b/config/static-app.conf new file mode 100644 index 0000000000..5a7d8c47d7 --- /dev/null +++ b/config/static-app.conf @@ -0,0 +1,10 @@ +# This is the version used ONLY for e2e tests because we statically compile to production mode +server { + listen 8063 $APP_BASE_URL; + root /src/frontends/mit-open/build; + + location / { + try_files /index.html =404; + } + +} diff --git a/data_fixtures/__init__.py b/data_fixtures/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/data_fixtures/apps.py b/data_fixtures/apps.py new file mode 100644 index 0000000000..075133ff20 --- /dev/null +++ b/data_fixtures/apps.py @@ -0,0 +1,6 @@ +from django.apps import AppConfig + + +class DataFixturesConfig(AppConfig): + default_auto_field = "django.db.models.BigAutoField" + name = "data_fixtures" diff --git a/data_fixtures/migrations/0001_add_testimonial_data.py b/data_fixtures/migrations/0001_add_testimonial_data.py new file mode 100644 index 0000000000..ad9001688a --- /dev/null +++ b/data_fixtures/migrations/0001_add_testimonial_data.py @@ -0,0 +1,256 @@ +# Generated by Django 4.2.13 on 2024-06-25 15:30 + +import logging +from pathlib import Path + +from django.conf import settings +from django.core.files import File +from django.db import migrations + +""" +Fix an issue with PIL's logger when running in test +https://github.com/camptocamp/pytest-odoo/issues/15 +""" +pil_logger = logging.getLogger("PIL") +pil_logger.setLevel(logging.INFO) + +fixtures = [ + { + "attestant_name": "Maria Eduarda", + "title": "Independent Learner, Brazil", + "quote": ( + "I am not exaggerating when I say MIT OpenCourseWare " + "has changed my life significantly for the better… " + "I would never have had the opportunity to " + "take Calculus during high school, let alone Solid State Chemistry. " + "These courses have greatly expanded my " + "interests and opportunities and I'm forever grateful for that." + ), + "position": 1, + "offerors": ["ocw"], + }, + { + "attestant_name": "Chansa Kabwe", + "title": "Independent Learner, Zambia", + "quote": ( + "OpenCourseWare continues to be a big part of my career." + " My foundation is linked to it — I don't know if " + "I would be the same engineer today if not for OpenCourseWare." + ), + "position": 1, + "offerors": ["ocw"], + }, + { + "attestant_name": "Emmanuel Kasigazi", + "title": "Entrepreneur, Uganda", + "quote": ( + "This is what OpenCourseWare has enabled me to do: " + "I get the chance to not only watch the " + "future happen, but I can actually be a part of it and create it." + ), + "position": 1, + "offerors": ["ocw"], + }, + { + "attestant_name": "Arthur Julio Nelson", + "title": "Brand Strategy Lead, Google", + "quote": ( + "The Bootcamp is, well, it's an accelerator " + "meets a creativity incubator. " + "It's all of these things mashed into one. " + "If you come into the Bootcamp with a " + "purpose and desire for impact and where you want " + "to channel it, the Bootcamp will help you unleash it." + ), + "position": 1, + "offerors": ["bootcamps"], + }, + { + "attestant_name": "Jasmine Latham", + "title": "Lead Data Scientist, Office for National Statistics", + "quote": ( + "I am very pleased with the course content, " + "it is exactly the level I am looking for. " + "Each professor/course presenter has packed " + "a lot of information and has explained complex " + "algorithms in good detail. " + "Some with good sense of humor. Thank you very much." + ), + "position": 1, + "offerors": ["xpro"], + }, + { + "attestant_name": "Lauren Moscioni", + "title": "Associate, Morgan Stanley", + "quote": ( + "Coming from the finance sector, " + "I wanted to learn more about real estate as an " + "alternative investment for ultra-high-net-worth clients. " + "What I got from MIT was a well-rounded, " + "holistic view of the state of the field." + ), + "position": 1, + "offerors": ["mitpe"], + }, + { + "attestant_name": "Sebastian Bello", + "title": "MicroMasters learner", + "quote": ( + "I did the Micromaster in supply chain, " + "what motivated me the most to finish it was " + "to have a certificate in supply chain " + "from MIT to improve my career. I was so engaged with " + "the courses that I wanted to continue learning, " + "now I'm doing the Supply Chain Management Master's at MIT." + ), + "position": 1, + "offerors": ["mitx"], + }, + { + "attestant_name": "John Goodloe", + "title": ( + "Senior Director for Society Business Solutions, " + "American Chemical Society (ACS)" + ), + "quote": ( + "The courses at MIT Sloan Executive Education brought together " + "true leaders from across the world. You're in classes with diplomats, " + "CEOs, heads of family businesses and " + "Fortune 500s—every course is a different mix." + ), + "position": 1, + "offerors": ["see"], + }, + { + "attestant_name": "Linda Obregon", + "title": "Founder and CEO", + "quote": ( + "The program experience was 10-fold compared to what I expected. " + "They not only taught me how to create " + "a company in a week, but they gave me a " + "supportive network of the best " + "and brightest minds. After I left the Bootcamp, " + "I founded a startup that uses science and" + " plants to develop high-protein and tasty new foods." + ), + "position": 1, + "offerors": ["bootcamps"], + }, + { + "attestant_name": "Murali Thyagarajan", + "title": "DBA & Application Support, NASDAQ", + "quote": ( + "The course was easy to understand and had depth. " + "All the concepts were clearly laid out and explained. " + "This is the best course I have come across on this topic." + ), + "position": 2, + "offerors": ["xpro"], + }, + { + "attestant_name": "Claudio Mirti", + "title": "Senior Advanced Analytics and AI Specialist, Microsoft", + "quote": ( + "The Certificate Program in Machine Learning and AI is a great experience. " + "You get a lot of powerful insights during " + "exchanges with the professors and other " + "students—and you learn how to incorporate the " + "material into your daily work." + ), + "position": 2, + "offerors": ["mitpe"], + }, + { + "attestant_name": "Stephen Okiya", + "title": "MITx Learner", + "quote": ( + "I am really grateful for the efforts by MIT in coming up with innovative " + "ways of making quality education affordable " + "and accessible to everyone all over the world. " + "I am sure that the programs impact the lives of millions of " + "people either directly or indirectly " + "hence making the world a better place." + ), + "position": 2, + "offerors": ["mitx"], + }, + { + "attestant_name": "Brenda Patel", + "title": "CEO, Bonova Advisory", + "quote": ( + "You're a part of an ecosystem that is a life-long community of " + "people with the same goal to create something new and valuable. " + "It's an investment of five days, but it stays with you forever." + ), + "position": 2, + "offerors": ["see"], + }, + { + "attestant_name": "Emily R. Wright", + "title": "Versatile Technologist", + "quote": ( + "This is an awesome program for emerging leaders " + "and for those currently in leadership positions. " + "The knowledge, tools, and techniques " + "are very useful to companies worldwide. " + "I would highly recommend this program to all levels of employees." + ), + "position": 3, + "offerors": ["xpro"], + }, + { + "attestant_name": "Anesha Santhanam", + "title": "MITx Learner", + "quote": ( + "I recently completed an MITx course in Python, " + "and it was phenomenal. I looked into this course to " + "give me a better foundation and a deeper " + "understanding so I could use Python " + "in other settings and applications. " + "I'm using what I learned everyday to further my passion, " + "so thank you for providing me with this opportunity!" + ), + "position": 3, + "offerors": ["mitx"], + }, +] + + +def load_fixtures(apps, schema_editor): + """ + Load fixtures for testimonials + """ + Attestation = apps.get_model("testimonials", "Attestation") + LearningResourceOfferor = apps.get_model( + "learning_resources", "LearningResourceOfferor" + ) + + for fixture in fixtures: + offerors = fixture.pop("offerors") + testimonial, _ = Attestation.objects.get_or_create( + attestant_name=fixture["attestant_name"], + title=fixture["title"], + defaults=fixture, + ) + # make sure related offerors exists in system before setting + if LearningResourceOfferor.objects.filter(pk__in=offerors).count() == len( + offerors + ): + testimonial.offerors.set(offerors) + if not testimonial.avatar: + """ + Save the image from fixture so alternate sizes are generated + """ + with Path.open( + f"{settings.BASE_DIR}/testimonials/fixtures/avatars/{fixture["attestant_name"]}.png", + "rb", + ) as imagefile: + testimonial.avatar.save("avatar.png", File(imagefile), save=True) + + +class Migration(migrations.Migration): + dependencies = [] + + operations = [ + migrations.RunPython(load_fixtures, migrations.RunPython.noop), + ] diff --git a/data_fixtures/migrations/0002_unit_page_copy_updates.py b/data_fixtures/migrations/0002_unit_page_copy_updates.py new file mode 100644 index 0000000000..d8d3b29523 --- /dev/null +++ b/data_fixtures/migrations/0002_unit_page_copy_updates.py @@ -0,0 +1,131 @@ +# Generated by Django 4.2.13 on 2024-07-08 17:08 + +from django.db import migrations + +fixtures = [ + { + "name": "ocw", + "offeror_configuration": { + "audience": ["Students", "Professionals", "Educators", "Lifelong Learners"], + "value_prop": ( + "For millions of learners and educators around the " + "world, OpenCourseWare shares free open educational resources from " + "across the entire MIT curriculum. With no " + "sign-up needed, thousands of downloadable materials, " + "and convenient online access, you're " + "empowered for self-paced learning and adapting these materials in " + "the ways that suit you best." + ), + }, + "channel_configuration": { + "sub_heading": ( + "For millions of learners and educators around " + "the world, OpenCourseWare shares free open educational resources" + " from across the entire MIT curriculum. With no sign-up needed, " + "thousands of downloadable materials, and convenient online access, " + "you're empowered for self-paced learning and adapting these " + "materials in the ways that suit you best." + ), + }, + }, + { + "name": "mitx", + "channel_configuration": {}, + "offeror_configuration": { + "audience": ["Lifelong Learners", "Students", "Professionals"], + "certifications": ["Certificate of Completion", "MicroMasters Credential"], + }, + }, + { + "name": "mitpe", + "offeror_configuration": { + "audience": [ + "Engineering Professionals", + "Technical Professionals", + "Scientists and Researchers", + "Senior Executives", + ], + "formats": ["Online", "In-Person", "Hybrid"], + "certifications": ["Certificate of Completion", "Professional Certificate"], + "content_types": ["Professional"], + "value_prop": ( + "MIT Professional Education is a leader in " + "technology and engineering education" + " for working professionals pursuing career advancement, and " + "for organizations seeking to meet modern-day challenges " + "by expanding the knowledge and skills of their employees. " + "Courses are delivered in a range of formats—in-person " + "(on-campus and live virtual), online, and through hybrid " + "approaches—to meet the needs of today's learners." + ), + }, + "channel_configuration": { + "heading": ( + "Join a powerful network of innovators and master " + "skills the global market needs in years to come." + ), + "sub_heading": ( + "MIT Professional Education is a leader in technology and engineering " + "education for working professionals pursuing career " + "advancement, and for organizations seeking to meet modern-day " + "challenges by expanding the knowledge and skills of their employees. " + "Courses are delivered in a range of formats—in-person (on-campus and" + " live virtual), online, and through hybrid approaches—to " + "meet the needs of today's learners." + ), + }, + }, + { + "name": "see", + "offeror_configuration": { + "audience": [ + "Business Professionals", + "Senior Executives", + "Entrepreneurs", + "Intrapreneurs", + ], + "formats": ["Online", "In-Person", "Hybrid"], + "fee": ["Paid"], + "certifications": ["Certificate of Completion", "Professional Certificate"], + }, + "channel_configuration": {}, + }, + { + "name": "xpro", + "offeror_configuration": { + "formats": ["Online", "In Person", "Hybrid"], + "certifications": ["Professional Certificate"], + }, + "channel_configuration": {}, + }, +] + + +def update_copy(apps, schema_editor): + Channel = apps.get_model("channels", "Channel") + LearningResourceOfferor = apps.get_model( + "learning_resources", "LearningResourceOfferor" + ) + for fixture in fixtures: + channel_configuration_updates = fixture["channel_configuration"] + offeror_configuration_updates = fixture["offeror_configuration"] + channel = Channel.objects.get(name=fixture["name"]) + if Channel.objects.filter(name=fixture["name"]).exists(): + for key, val in channel_configuration_updates.items(): + channel.configuration[key] = val + channel.save() + if LearningResourceOfferor.objects.filter(code=fixture["name"]).exists(): + offeror = LearningResourceOfferor.objects.get(code=fixture["name"]) + for key, val in offeror_configuration_updates.items(): + setattr(offeror, key, val) + offeror.save() + + +class Migration(migrations.Migration): + dependencies = [ + ("data_fixtures", "0001_add_testimonial_data"), + ] + + operations = [ + migrations.RunPython(update_copy, migrations.RunPython.noop), + ] diff --git a/data_fixtures/migrations/__init__.py b/data_fixtures/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/docker-compose-e2e-tests.yml b/docker-compose-e2e-tests.yml index 826998eec9..56c922aa03 100644 --- a/docker-compose-e2e-tests.yml +++ b/docker-compose-e2e-tests.yml @@ -23,7 +23,15 @@ services: - web volumes: - ./config/nginx.conf:/etc/nginx/conf.d/web.conf + - ./config/static-app.conf:/etc/nginx/templates/static-app.template - ./frontends/mit-open/build:/src/frontends/mit-open/build + env_file: + - path: ./env/shared.env + networks: + default: + aliases: + - "open.odl.local" + - "api.open.odl.local" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8063"] interval: 30s @@ -48,7 +56,7 @@ services: environment: DATABASE_URL: postgres://postgres:postgres@db:5432/e2e_postgres # pragma: allowlist secret PORT: 8061 - env_file: .env.ci + env_file: env/ci.env depends_on: db: condition: service_healthy @@ -72,7 +80,7 @@ services: context: e2e_testing environment: - CI=true - - BASE_URL=http://nginx:8063 + - BASE_URL=http://open.odl.local:8063 depends_on: nginx: condition: service_healthy diff --git a/docker-compose.yml b/docker-compose.yml index ea06411db5..93c84fca44 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,4 @@ x-environment: &py-environment - DEBUG: ${DEBUG:-True} - DEV_ENV: "True" # necessary to have nginx connect to web container - NODE_ENV: ${NODE_ENV:-development} - DATABASE_URL: postgres://postgres:postgres@db:5432/postgres - MITOPEN_SECURE_SSL_REDIRECT: "False" - MITOPEN_DB_DISABLE_SSL: "True" - MITOPEN_FEATURES_DEFAULT: ${MITOPEN_FEATURES_DEFAULT:-True} - OPENSEARCH_URL: opensearch-node-mitopen:9200 - CELERY_TASK_ALWAYS_EAGER: "False" - CELERY_BROKER_URL: redis://redis:6379/4 - CELERY_RESULT_BACKEND: redis://redis:6379/4 - TIKA_SERVER_ENDPOINT: ${TIKA_SERVER_ENDPOINT:-http://tika:9998/} - TIKA_CLIENT_ONLY: "True" services: db: @@ -60,14 +47,18 @@ services: nginx: profiles: - backend - image: nginx:1.27.0 + build: + context: ./nginx ports: - "8063:8063" links: - web + environment: + PORT: 8063 + NGINX_WORKERS: 1 + NGINX_UWSGI_PASS: "web:8061" volumes: - - ./config/nginx.conf:/etc/nginx/conf.d/web.conf - - ./frontends/mit-open/build:/src/frontends/mit-open/build + - ./config:/etc/nginx/templates web: profiles: @@ -75,10 +66,16 @@ services: build: context: . dockerfile: Dockerfile - environment: - <<: *py-environment - PORT: 8061 - env_file: .env + env_file: + - path: env/shared.env + - path: env/shared.local.env + required: false + - path: env/backend.env + - path: env/backend.local.env + required: false + # DEPRECATED: legacy .env file at the repo root + - path: .env + required: false command: ./scripts/run-django-dev.sh stdin_open: true tty: true @@ -103,11 +100,16 @@ services: yarn install --immutable yarn workspace mit-open storybook & yarn workspace mit-open watch:docker - environment: - NODE_ENV: ${NODE_ENV:-development} - PORT: 8062 - MITOPEN_AXIOS_BASE_PATH: ${MITOPEN_BASE_URL} - env_file: .env + env_file: + - path: env/shared.env + - path: env/shared.local.env + required: false + - path: env/frontend.env + - path: env/frontend.local.env + required: false + # DEPRECATED: legacy .env file at the repo root + - path: .env + required: false ports: - "8062:8062" - "6006:6006" @@ -120,8 +122,16 @@ services: build: context: . dockerfile: Dockerfile - environment: *py-environment - env_file: .env + env_file: + - path: env/shared.env + - path: env/shared.local.env + required: false + - path: env/backend.env + - path: env/backend.local.env + required: false + # DEPRECATED: legacy .env file at the repo root + - path: .env + required: false command: > /bin/bash -c ' sleep 3; diff --git a/docs/404.html b/docs/404.html deleted file mode 100644 index c472b4ea0a..0000000000 --- a/docs/404.html +++ /dev/null @@ -1,24 +0,0 @@ ---- -layout: default ---- - - - -
-

404

- -

Page not found :(

-

The requested page could not be found.

-
diff --git a/docs/Gemfile b/docs/Gemfile deleted file mode 100644 index 0462a1a160..0000000000 --- a/docs/Gemfile +++ /dev/null @@ -1,33 +0,0 @@ -source "https://rubygems.org" - -# Hello! This is where you manage which Jekyll version is used to run. -# When you want to use a different version, change it below, save the -# file and run `bundle install`. Run Jekyll with `bundle exec`, like so: -# -# bundle exec jekyll serve -# -# This will help ensure the proper Jekyll version is running. -# Happy Jekylling! -# gem "jekyll", "~> 3.8.7" - -# This is the default theme for new Jekyll sites. You may change this to anything you like. -gem "minima", "~> 2.0" - -# If you want to use GitHub Pages, remove the "gem "jekyll"" above and -# uncomment the line below. To upgrade, run `bundle update github-pages`. -gem "github-pages", "~> 228", group: :jekyll_plugins - -# If you have any plugins, put them here! -group :jekyll_plugins do - gem "jekyll-feed", "~> 0.6" -end - -# Windows does not include zoneinfo files, so bundle the tzinfo-data gem -# and associated library. -install_if -> { RUBY_PLATFORM =~ %r!mingw|mswin|java! } do - gem "tzinfo", "~> 1.2" - gem "tzinfo-data" -end - -# Performance-booster for watching directories on Windows -gem "wdm", "~> 0.1.0", :install_if => Gem.win_platform? diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock deleted file mode 100644 index f087a71f23..0000000000 --- a/docs/Gemfile.lock +++ /dev/null @@ -1,271 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - activesupport (6.0.6.1) - concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (>= 0.7, < 2) - minitest (~> 5.1) - tzinfo (~> 1.1) - zeitwerk (~> 2.2, >= 2.2.2) - addressable (2.8.6) - public_suffix (>= 2.0.2, < 6.0) - coffee-script (2.4.1) - coffee-script-source - execjs - coffee-script-source (1.11.1) - colorator (1.1.0) - commonmarker (0.23.10) - concurrent-ruby (1.3.1) - dnsruby (1.72.1) - simpleidn (~> 0.2.1) - em-websocket (0.5.3) - eventmachine (>= 0.12.9) - http_parser.rb (~> 0) - ethon (0.16.0) - ffi (>= 1.15.0) - eventmachine (1.2.7) - execjs (2.9.1) - faraday (2.9.0) - faraday-net_http (>= 2.0, < 3.2) - faraday-net_http (3.1.0) - net-http - ffi (1.16.3) - forwardable-extended (2.6.0) - gemoji (3.0.1) - github-pages (228) - github-pages-health-check (= 1.17.9) - jekyll (= 3.9.3) - jekyll-avatar (= 0.7.0) - jekyll-coffeescript (= 1.1.1) - jekyll-commonmark-ghpages (= 0.4.0) - jekyll-default-layout (= 0.1.4) - jekyll-feed (= 0.15.1) - jekyll-gist (= 1.5.0) - jekyll-github-metadata (= 2.13.0) - jekyll-include-cache (= 0.2.1) - jekyll-mentions (= 1.6.0) - jekyll-optional-front-matter (= 0.3.2) - jekyll-paginate (= 1.1.0) - jekyll-readme-index (= 0.3.0) - jekyll-redirect-from (= 0.16.0) - jekyll-relative-links (= 0.6.1) - jekyll-remote-theme (= 0.4.3) - jekyll-sass-converter (= 1.5.2) - jekyll-seo-tag (= 2.8.0) - jekyll-sitemap (= 1.4.0) - jekyll-swiss (= 1.0.0) - jekyll-theme-architect (= 0.2.0) - jekyll-theme-cayman (= 0.2.0) - jekyll-theme-dinky (= 0.2.0) - jekyll-theme-hacker (= 0.2.0) - jekyll-theme-leap-day (= 0.2.0) - jekyll-theme-merlot (= 0.2.0) - jekyll-theme-midnight (= 0.2.0) - jekyll-theme-minimal (= 0.2.0) - jekyll-theme-modernist (= 0.2.0) - jekyll-theme-primer (= 0.6.0) - jekyll-theme-slate (= 0.2.0) - jekyll-theme-tactile (= 0.2.0) - jekyll-theme-time-machine (= 0.2.0) - jekyll-titles-from-headings (= 0.5.3) - jemoji (= 0.12.0) - kramdown (= 2.3.2) - kramdown-parser-gfm (= 1.1.0) - liquid (= 4.0.4) - mercenary (~> 0.3) - minima (= 2.5.1) - nokogiri (>= 1.13.6, < 2.0) - rouge (= 3.26.0) - terminal-table (~> 1.4) - github-pages-health-check (1.17.9) - addressable (~> 2.3) - dnsruby (~> 1.60) - octokit (~> 4.0) - public_suffix (>= 3.0, < 5.0) - typhoeus (~> 1.3) - html-pipeline (2.14.3) - activesupport (>= 2) - nokogiri (>= 1.4) - http_parser.rb (0.8.0) - i18n (1.14.5) - concurrent-ruby (~> 1.0) - jekyll (3.9.3) - addressable (~> 2.4) - colorator (~> 1.0) - em-websocket (~> 0.5) - i18n (>= 0.7, < 2) - jekyll-sass-converter (~> 1.0) - jekyll-watch (~> 2.0) - kramdown (>= 1.17, < 3) - liquid (~> 4.0) - mercenary (~> 0.3.3) - pathutil (~> 0.9) - rouge (>= 1.7, < 4) - safe_yaml (~> 1.0) - jekyll-avatar (0.7.0) - jekyll (>= 3.0, < 5.0) - jekyll-coffeescript (1.1.1) - coffee-script (~> 2.2) - coffee-script-source (~> 1.11.1) - jekyll-commonmark (1.4.0) - commonmarker (~> 0.22) - jekyll-commonmark-ghpages (0.4.0) - commonmarker (~> 0.23.7) - jekyll (~> 3.9.0) - jekyll-commonmark (~> 1.4.0) - rouge (>= 2.0, < 5.0) - jekyll-default-layout (0.1.4) - jekyll (~> 3.0) - jekyll-feed (0.15.1) - jekyll (>= 3.7, < 5.0) - jekyll-gist (1.5.0) - octokit (~> 4.2) - jekyll-github-metadata (2.13.0) - jekyll (>= 3.4, < 5.0) - octokit (~> 4.0, != 4.4.0) - jekyll-include-cache (0.2.1) - jekyll (>= 3.7, < 5.0) - jekyll-mentions (1.6.0) - html-pipeline (~> 2.3) - jekyll (>= 3.7, < 5.0) - jekyll-optional-front-matter (0.3.2) - jekyll (>= 3.0, < 5.0) - jekyll-paginate (1.1.0) - jekyll-readme-index (0.3.0) - jekyll (>= 3.0, < 5.0) - jekyll-redirect-from (0.16.0) - jekyll (>= 3.3, < 5.0) - jekyll-relative-links (0.6.1) - jekyll (>= 3.3, < 5.0) - jekyll-remote-theme (0.4.3) - addressable (~> 2.0) - jekyll (>= 3.5, < 5.0) - jekyll-sass-converter (>= 1.0, <= 3.0.0, != 2.0.0) - rubyzip (>= 1.3.0, < 3.0) - jekyll-sass-converter (1.5.2) - sass (~> 3.4) - jekyll-seo-tag (2.8.0) - jekyll (>= 3.8, < 5.0) - jekyll-sitemap (1.4.0) - jekyll (>= 3.7, < 5.0) - jekyll-swiss (1.0.0) - jekyll-theme-architect (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-cayman (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-dinky (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-hacker (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-leap-day (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-merlot (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-midnight (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-minimal (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-modernist (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-primer (0.6.0) - jekyll (> 3.5, < 5.0) - jekyll-github-metadata (~> 2.9) - jekyll-seo-tag (~> 2.0) - jekyll-theme-slate (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-tactile (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-theme-time-machine (0.2.0) - jekyll (> 3.5, < 5.0) - jekyll-seo-tag (~> 2.0) - jekyll-titles-from-headings (0.5.3) - jekyll (>= 3.3, < 5.0) - jekyll-watch (2.2.1) - listen (~> 3.0) - jemoji (0.12.0) - gemoji (~> 3.0) - html-pipeline (~> 2.2) - jekyll (>= 3.0, < 5.0) - kramdown (2.3.2) - rexml - kramdown-parser-gfm (1.1.0) - kramdown (~> 2.0) - liquid (4.0.4) - listen (3.9.0) - rb-fsevent (~> 0.10, >= 0.10.3) - rb-inotify (~> 0.9, >= 0.9.10) - mercenary (0.3.6) - mini_portile2 (2.8.6) - minima (2.5.1) - jekyll (>= 3.5, < 5.0) - jekyll-feed (~> 0.9) - jekyll-seo-tag (~> 2.1) - minitest (5.23.1) - net-http (0.4.1) - uri - nokogiri (1.16.5) - mini_portile2 (~> 2.8.2) - racc (~> 1.4) - octokit (4.25.1) - faraday (>= 1, < 3) - sawyer (~> 0.9) - pathutil (0.16.2) - forwardable-extended (~> 2.6) - public_suffix (4.0.7) - racc (1.8.0) - rb-fsevent (0.11.2) - rb-inotify (0.11.1) - ffi (~> 1.0) - rexml (3.2.8) - strscan (>= 3.0.9) - rouge (3.26.0) - rubyzip (2.3.2) - safe_yaml (1.0.5) - sass (3.7.4) - sass-listen (~> 4.0.0) - sass-listen (4.0.0) - rb-fsevent (~> 0.9, >= 0.9.4) - rb-inotify (~> 0.9, >= 0.9.7) - sawyer (0.9.2) - addressable (>= 2.3.5) - faraday (>= 0.17.3, < 3) - simpleidn (0.2.3) - strscan (3.1.0) - terminal-table (1.8.0) - unicode-display_width (~> 1.1, >= 1.1.1) - thread_safe (0.3.6) - typhoeus (1.4.1) - ethon (>= 0.9.0) - tzinfo (1.2.11) - thread_safe (~> 0.1) - tzinfo-data (1.2024.1) - tzinfo (>= 1.0.0) - unicode-display_width (1.8.0) - uri (0.13.0) - wdm (0.1.1) - zeitwerk (2.6.15) - -PLATFORMS - ruby - -DEPENDENCIES - github-pages (~> 228) - jekyll-feed (~> 0.6) - minima (~> 2.0) - tzinfo (~> 1.2) - tzinfo-data - wdm (~> 0.1.0) - -BUNDLED WITH - 2.1.4 diff --git a/docs/_config.yml b/docs/_config.yml deleted file mode 100644 index 2eff3861b1..0000000000 --- a/docs/_config.yml +++ /dev/null @@ -1,10 +0,0 @@ -title: Open Discussions -description: This provides a discussion forum for use with other MIT applications. - -show_downloads: false -google_analytics: - -remote_theme: pmarsceill/just-the-docs -markdown: kramdown -plugins: - - jekyll-feed diff --git a/env/.gitignore b/env/.gitignore new file mode 100644 index 0000000000..dd8d87118d --- /dev/null +++ b/env/.gitignore @@ -0,0 +1 @@ +*.local.env diff --git a/env/backend.env b/env/backend.env new file mode 100644 index 0000000000..0ed73a7b1d --- /dev/null +++ b/env/backend.env @@ -0,0 +1,32 @@ +CELERY_BROKER_URL=redis://redis:6379/4 +CELERY_RESULT_BACKEND=redis://redis:6379/4 +CELERY_TASK_ALWAYS_EAGER=False + +# local hostname shenanigans +CORS_ALLOWED_ORIGINS='["http://open.odl.local:8062"]' +CSRF_TRUSTED_ORIGINS='["http://open.odl.local:8062", "http://api.open.odl.local:8063"]' +CSRF_COOKIE_DOMAIN=open.odl.local +CSRF_COOKIE_SECURE=False +MITOPEN_COOKIE_DOMAIN=open.odl.local +MITOPEN_COOKIE_NAME=discussions + + +DEBUG=True +DJANGO_LOG_LEVEL=INFO +LOG_LEVEL=info +DEV_ENV=true + +DATABASE_URL=postgres://postgres:postgres@db:5432/postgres + +MITOPEN_DB_DISABLE_SSL=True +MITOPEN_FEATURES_DEFAULT=True +MITOPEN_SECURE_SSL_REDIRECT=False + +OPENSEARCH_URL=opensearch-node-mitopen:9200 +OPENSEARCH_INDEX=discussions_local +OPENSEARCH_INDEXING_CHUNK_SIZE=100 + +PORT=8061 + +TIKA_SERVER_ENDPOINT=http://tika:9998/ +TIKA_CLIENT_ONLY=True diff --git a/env/backend.local.example.env b/env/backend.local.example.env new file mode 100644 index 0000000000..3180b3bd70 --- /dev/null +++ b/env/backend.local.example.env @@ -0,0 +1,22 @@ +# MITOPEN_JWT_SECRET= +# MITOPEN_USE_S3=True +# MAILGUN_SENDER_DOMAIN= +# MAILGUN_KEY= +# MAILGUN_RECIPIENT_OVERRIDE= +# STATUS_TOKEN= +# AWS_ACCESS_KEY_ID= +# AWS_SECRET_ACCESS_KEY= +# AWS_STORAGE_BUCKET_NAME= +# YOUTUBE_DEVELOPER_KEY= +# +# SOCIAL_AUTH_OL_OIDC_OIDC_ENDPOINT= +# SOCIAL_AUTH_OL_OIDC_KEY= +# SOCIAL_AUTH_OL_OIDC_SECRET= +# AUTHORIZATION_URL= +# ACCESS_TOKEN_URL= +# USERINFO_URL= +# KEYCLOAK_BASE_URL= +# KEYCLOAK_REALM_NAME= +# +# POSTHOG_PROJECT_ID= +# POSTHOG_PERSONAL_API_KEY= diff --git a/.env.ci b/env/ci.env similarity index 90% rename from .env.ci rename to env/ci.env index 83c2ade6b7..d86ad0ff77 100644 --- a/.env.ci +++ b/env/ci.env @@ -10,10 +10,9 @@ CELERY_TASK_ALWAYS_EAGER=False CELERY_BROKER_URL=redis://redis:6379/4 CELERY_RESULT_BACKEND=redis://redis:6379/4 TIKA_CLIENT_ONLY=True -MITOPEN_BASE_URL=http://localhost:8063/ +MITOPEN_APP_BASE_URL=http://localhost:8063/ MAILGUN_KEY=fake_mailgun_key MAILGUN_SENDER_DOMAIN=other.fake.site OPENSEARCH_INDEX=testindex -INDEXING_API_USERNAME=mitodl MITOPEN_COOKIE_DOMAIN=localhost MITOPEN_COOKIE_NAME=cookie_monster diff --git a/env/frontend.env b/env/frontend.env new file mode 100644 index 0000000000..a6adf9277f --- /dev/null +++ b/env/frontend.env @@ -0,0 +1,3 @@ +NODE_ENV=development +PORT=8062 +MITOPEN_AXIOS_WITH_CREDENTIALS=true diff --git a/env/frontend.local.example.env b/env/frontend.local.example.env new file mode 100644 index 0000000000..e69de29bb2 diff --git a/env/shared.env b/env/shared.env new file mode 100644 index 0000000000..67c604a1df --- /dev/null +++ b/env/shared.env @@ -0,0 +1,5 @@ +MITOPEN_API_BASE_URL=http://api.open.odl.local:8063 +MITOPEN_APP_BASE_URL=http://open.odl.local:8062 +MITOPEN_SUPPORT_EMAIL=support@localhost + +POSTHOG_TIMEOUT_MS=1500 diff --git a/env/shared.local.example.env b/env/shared.local.example.env new file mode 100644 index 0000000000..c18b9bdde5 --- /dev/null +++ b/env/shared.local.example.env @@ -0,0 +1,5 @@ +# MITOPEN_API_BASE_URL=http://api.open.odl.local:8063 +# MITOPEN_APP_BASE_URL=http://open.odl.local:8063 +# POSTHOG_PROJECT_API_KEY= +# POSTHOG_TIMEOUT_MS=1500 +# EMBEDLY_KEY= diff --git a/fixtures/common.py b/fixtures/common.py index fbd6097be8..2166abab96 100644 --- a/fixtures/common.py +++ b/fixtures/common.py @@ -11,8 +11,6 @@ from pytest_mock import PytestMockWarning from urllib3.exceptions import InsecureRequestWarning -from main.factories import UserFactory - @pytest.fixture(autouse=True) def silence_factory_logging(): # noqa: PT004 @@ -80,14 +78,6 @@ def mocked_celery(mocker): ) -@pytest.fixture() -def indexing_user(settings): - """Sets and returns the indexing user""" # noqa: D401 - user = UserFactory.create() - settings.INDEXING_API_USERNAME = user.username - return user - - @pytest.fixture() def mocked_responses(): """Mock responses fixture""" diff --git a/frontends/.eslintrc.js b/frontends/.eslintrc.js index a2843799ac..acf1615251 100644 --- a/frontends/.eslintrc.js +++ b/frontends/.eslintrc.js @@ -154,11 +154,6 @@ function restrictedImports({ paths = [], patterns = [] } = {}) { message: "Please use @faker-js/faker/locale/en instead.", allowTypeImports: true, }, - { - name: "@mui/icons-material", - message: "Please use @mui/icons-material/ instead.", - allowTypeImports: true, - }, { name: "@mui/material", message: "Please use @mui/material/ instead.", diff --git a/frontends/api/src/clients.ts b/frontends/api/src/clients.ts index e5a3c8249b..b0768a3f5a 100644 --- a/frontends/api/src/clients.ts +++ b/frontends/api/src/clients.ts @@ -24,7 +24,7 @@ import { import axiosInstance from "./axios" -const BASE_PATH = process.env.MITOPEN_AXIOS_BASE_PATH?.replace(/\/+$/, "") ?? "" +const BASE_PATH = process.env.MITOPEN_API_BASE_URL?.replace(/\/+$/, "") ?? "" const learningResourcesApi = new LearningResourcesApi( undefined, diff --git a/frontends/api/src/generated/v0/api.ts b/frontends/api/src/generated/v0/api.ts index 7ac7efb3a2..305da411e6 100644 --- a/frontends/api/src/generated/v0/api.ts +++ b/frontends/api/src/generated/v0/api.ts @@ -2071,18 +2071,6 @@ export interface ProgramCertificate { * @memberof ProgramCertificate */ record_hash: string - /** - * - * @type {string} - * @memberof ProgramCertificate - */ - program_letter_generate_url: string - /** - * - * @type {string} - * @memberof ProgramCertificate - */ - program_letter_share_url: string /** * * @type {string} diff --git a/frontends/api/src/generated/v1/api.ts b/frontends/api/src/generated/v1/api.ts index 273caa2827..73f118a282 100644 --- a/frontends/api/src/generated/v1/api.ts +++ b/frontends/api/src/generated/v1/api.ts @@ -4285,18 +4285,6 @@ export interface ProgramCertificate { * @memberof ProgramCertificate */ record_hash: string - /** - * - * @type {string} - * @memberof ProgramCertificate - */ - program_letter_generate_url: string - /** - * - * @type {string} - * @memberof ProgramCertificate - */ - program_letter_share_url: string /** * * @type {string} diff --git a/frontends/api/src/hooks/learningResources/index.ts b/frontends/api/src/hooks/learningResources/index.ts index a97bc4509c..13b930754a 100644 --- a/frontends/api/src/hooks/learningResources/index.ts +++ b/frontends/api/src/hooks/learningResources/index.ts @@ -427,6 +427,13 @@ const useSchoolsList = () => { return useQuery(learningResources.schools()) } +/* + * Not intended to be imported except for special cases. + * It's used in the ResourceCarousel to dynamically build a single useQueries hook + * from config because a React component cannot conditionally call hooks during renders. + */ +export { default as learningResourcesKeyFactory } from "./keyFactory" + export { useLearningResourcesList, useFeaturedLearningResourcesList, diff --git a/frontends/api/src/test-utils/factories/learningResources.ts b/frontends/api/src/test-utils/factories/learningResources.ts index 209be56bc5..d496c85ce5 100644 --- a/frontends/api/src/test-utils/factories/learningResources.ts +++ b/frontends/api/src/test-utils/factories/learningResources.ts @@ -412,6 +412,7 @@ const podcastEpisode: LearningResourceFactory = ( podcast_episode: { id: uniqueEnforcerId.enforce(() => faker.number.int()), duration: faker.helpers.arrayElement(["PT1H13M44S", "PT2H30M", "PT1M"]), + episode_link: faker.internet.url(), }, }, overrides, diff --git a/frontends/api/src/test-utils/urls.ts b/frontends/api/src/test-utils/urls.ts index c1478f4cdf..ca3575339f 100644 --- a/frontends/api/src/test-utils/urls.ts +++ b/frontends/api/src/test-utils/urls.ts @@ -26,7 +26,7 @@ import type { import type { BaseAPI } from "../generated/v1/base" import type { BaseAPI as BaseAPIv0 } from "../generated/v0/base" -const API_BASE_URL = process.env.MITOPEN_AXIOS_BASE_PATH +const API_BASE_URL = process.env.MITOPEN_API_BASE_URL // OpenAPI Generator declares parameters using interfaces, which makes passing // them to functions a little annoying. diff --git a/frontends/github-pages/.stylelintignore b/frontends/github-pages/.stylelintignore deleted file mode 100644 index 325e0cd214..0000000000 --- a/frontends/github-pages/.stylelintignore +++ /dev/null @@ -1 +0,0 @@ -build/static/css diff --git a/frontends/github-pages/craco.config.js b/frontends/github-pages/craco.config.js deleted file mode 100644 index 673a158751..0000000000 --- a/frontends/github-pages/craco.config.js +++ /dev/null @@ -1,27 +0,0 @@ -module.exports = { - webpack: { - configure: (webpackConfig, { env, paths }) => { - webpackConfig.module.rules.push( - { - test: /\.tsx?$/, - loader: "babel-loader", - }, - { - test: /\.(svg|ttf|woff|woff2|eot|gif|png)$/, - type: "asset/inline", - }, - { - test: /\.tsx?$/, - use: { - loader: "swc-loader", - options: { - parseMap: true, - }, - }, - exclude: /node_modules/, - }, - ) - return webpackConfig - }, - }, -} diff --git a/frontends/github-pages/package.json b/frontends/github-pages/package.json deleted file mode 100644 index 55fc103019..0000000000 --- a/frontends/github-pages/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "github-pages", - "version": "0.1.0", - "private": true, - "dependencies": { - "ol-components": "0.0.0", - "react": "18.2.0", - "react-dom": "18.2.0", - "react-scripts": "5.0.1" - }, - "scripts": { - "start": "craco start", - "build": "craco build" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - }, - "devDependencies": { - "@craco/craco": "^7.1.0", - "@types/eslint": "^8", - "babel-loader": "^9.1.3", - "eslint": "8", - "eslint-config-react-app": "^7.0.1", - "swc-loader": "^0.2.6" - } -} diff --git a/frontends/github-pages/public/favicon.ico b/frontends/github-pages/public/favicon.ico deleted file mode 100644 index 95d530203d..0000000000 Binary files a/frontends/github-pages/public/favicon.ico and /dev/null differ diff --git a/frontends/github-pages/public/index.html b/frontends/github-pages/public/index.html deleted file mode 100644 index 47e579a5c4..0000000000 --- a/frontends/github-pages/public/index.html +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - MIT Open - Developer Materials - - - -
- - diff --git a/frontends/github-pages/public/robots.txt b/frontends/github-pages/public/robots.txt deleted file mode 100644 index e9e57dc4d4..0000000000 --- a/frontends/github-pages/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/frontends/github-pages/public/static/images/course_search_banner.png b/frontends/github-pages/public/static/images/course_search_banner.png deleted file mode 100644 index f4b6b18c86..0000000000 Binary files a/frontends/github-pages/public/static/images/course_search_banner.png and /dev/null differ diff --git a/frontends/github-pages/public/static/images/mit-logo-transparent.svg b/frontends/github-pages/public/static/images/mit-logo-transparent.svg deleted file mode 100644 index 67da18b013..0000000000 --- a/frontends/github-pages/public/static/images/mit-logo-transparent.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - -logo2 - - - - - - - diff --git a/frontends/github-pages/src/App.tsx b/frontends/github-pages/src/App.tsx deleted file mode 100644 index 0218eab0c0..0000000000 --- a/frontends/github-pages/src/App.tsx +++ /dev/null @@ -1,87 +0,0 @@ -import React from "react" -import Header from "./Header" -import { Grid, ThemeProvider, styled, Typography } from "ol-components" -import { createBrowserRouter, RouterProvider } from "react-router-dom" -import { RouteObject, Outlet } from "react-router" -import "./style.css" - -const PUBLIC_URL = process.env.PUBLIC_URL || "" - -const Page = styled.div` - margin: auto; - max-width: 1200px; -` - -const Links = styled.div` - margin-top: 4rem; -` - -const LinkItem = styled.div` - margin-bottom: 3rem; - - a { - text-decoration: underline; - } -` - -const routes: RouteObject[] = [ - { - element: ( - <> -
- - - ), - children: [ - { - path: "/", - element: ( - - - Developer Materials -

- Libraries and tools for application development for use in your - projects -

- - -

- - React Component Library - -

- MIT Open Learning's React component library, presented with{" "} - Storybook. -
- -

- - E2E Test Report - -

- The report from the most recent{" "} - Playwright E2E testing - run. -
-
-
-
- ), - }, - ], - }, -] - -const router = createBrowserRouter(routes, { - basename: PUBLIC_URL, -}) - -function App() { - return ( - - - - ) -} - -export default App diff --git a/frontends/github-pages/src/Header.tsx b/frontends/github-pages/src/Header.tsx deleted file mode 100644 index 5be09b0018..0000000000 --- a/frontends/github-pages/src/Header.tsx +++ /dev/null @@ -1,53 +0,0 @@ -import React, { FunctionComponent } from "react" -import { styled, AppBar, Divider, Toolbar } from "ol-components" -import { MITLogoLink } from "ol-utilities" -import { Link } from "react-router-dom" - -const Bar = styled(AppBar)` - background-color: ${({ theme }) => theme.custom.colors.white}; - color: ${({ theme }) => theme.typography.body1.color}; - min-height: 80px; - display: flex; - flex-direction: column; - box-shadow: 0 2px 10px rgba(120 169 197 / 15%); -` - -const StyledToolbar = styled(Toolbar)({ - flex: 1, -}) - -const LogoLink = styled(MITLogoLink)({ - width: 45, - height: "auto", - img: { - height: 20, - }, -}) - -const StyledDivider = styled(Divider)({ - margin: "0.5em 1em", -}) - -const BrandLink = styled(Link)` - font-weight: bold; - color: ${({ theme }) => theme.palette.secondary.main}; -` - -const Spacer = styled.div` - flex: 1; -` - -const Header: FunctionComponent = () => { - return ( - - - - - MIT Open - - - - ) -} - -export default Header diff --git a/frontends/github-pages/src/index.tsx b/frontends/github-pages/src/index.tsx deleted file mode 100644 index ad0f4f1ae1..0000000000 --- a/frontends/github-pages/src/index.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import React from "react" -import ReactDOM from "react-dom/client" -import App from "./App" - -const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement) -root.render( - - - , -) diff --git a/frontends/github-pages/src/style.css b/frontends/github-pages/src/style.css deleted file mode 100644 index c77e67f46c..0000000000 --- a/frontends/github-pages/src/style.css +++ /dev/null @@ -1,32 +0,0 @@ -html { - font-family: "Source Sans Pro", helvetica, arial, sans-serif; - color: #03152d; -} - -body { - background-color: #edeff5; - margin: 0; - padding: 0; -} - -a { - text-decoration: none; - - &:hover { - text-decoration: underline; - } - - color: inherit; -} - -h1 { - font-size: 24px; -} - -h2 { - font-size: 18px; -} - -h4 { - font-size: 14px; -} diff --git a/frontends/github-pages/src/style.scss b/frontends/github-pages/src/style.scss deleted file mode 100644 index 361bdebb94..0000000000 --- a/frontends/github-pages/src/style.scss +++ /dev/null @@ -1,36 +0,0 @@ -html { - font-family: "Source Sans Pro", helvetica, arial, sans-serif; - color: #03152d; -} - -body { - background-color: #edeff5; - margin: 0; - padding: 0; -} - -* { - box-sizing: border-box; -} - -a { - text-decoration: none; - - &:hover { - text-decoration: underline; - } - - color: inherit; -} - -h1 { - font-size: 24px; -} - -h2 { - font-size: 18px; -} - -h4 { - font-size: 14px; -} diff --git a/frontends/github-pages/tsconfig.json b/frontends/github-pages/tsconfig.json deleted file mode 100644 index 3cf966d7cf..0000000000 --- a/frontends/github-pages/tsconfig.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "../tsconfig.json", - "include": ["src"] -} diff --git a/frontends/mit-open/.storybook/main.ts b/frontends/mit-open/.storybook/main.ts index 13ef9317e5..eef19855b4 100644 --- a/frontends/mit-open/.storybook/main.ts +++ b/frontends/mit-open/.storybook/main.ts @@ -31,7 +31,6 @@ const config = { docs: { autodocs: "tag", }, - staticDirs: ["../build"], env: (config: any) => ({ ...config, PUBLIC_URL: process.env.PUBLIC_URL || "", diff --git a/frontends/mit-open/.storybook/preview.tsx b/frontends/mit-open/.storybook/preview.tsx index cc3f15b864..dff37eda94 100644 --- a/frontends/mit-open/.storybook/preview.tsx +++ b/frontends/mit-open/.storybook/preview.tsx @@ -17,10 +17,15 @@ const preview: Preview = { options: { // @ts-expect-error These have type {import("@storybook/types").IndexEntry} // But this function is run in JS and seems not to be compiled. - storySort: (a, b) => - a.id === b.id - ? 0 - : a.id.localeCompare(b.id, undefined, { numeric: true }), + storySort: ({ id: a }, { id: b }) => { + if (a.slice(0, 3) === "old" && b.slice(0, 3) !== "old") { + return 1 + } + if (b.slice(0, 3) === "old" && a.slice(0, 3) !== "old") { + return -1 + } + return a.localeCompare(b, undefined, { numeric: true }) + }, }, }, globals: { diff --git a/frontends/mit-open/jest.config.ts b/frontends/mit-open/jest.config.ts index 5500ff1746..0f60c4a731 100644 --- a/frontends/mit-open/jest.config.ts +++ b/frontends/mit-open/jest.config.ts @@ -18,7 +18,7 @@ const config: Config.InitialOptions = { embedlyKey: "embedly_key", axios_base_path: "https://api.mitopen-test.odl.mit.edu", }, - MITOPEN_AXIOS_BASE_PATH: "https://api.mitopen-test.odl.mit.edu", + MITOPEN_API_BASE_URL: "https://api.mitopen-test.odl.mit.edu", }, } diff --git a/frontends/mit-open/package.json b/frontends/mit-open/package.json index da54eb16d7..497c1a6771 100644 --- a/frontends/mit-open/package.json +++ b/frontends/mit-open/package.json @@ -9,12 +9,12 @@ }, "scripts": { "watch": "NODE_ENV=development ENVIRONMENT=local webpack serve", - "watch:docker": "API_DEV_PROXY_BASE_URL=http://nginx:8063 NODE_ENV=development ENVIRONMENT=local webpack serve", + "watch:docker": "API_DEV_PROXY_BASE_URL=http://nginx:8063 NODE_ENV=development ENVIRONMENT=docker webpack serve", "watch:rc": "API_DEV_PROXY_BASE_URL=https://api.mitopen-rc.odl.mit.edu/ NODE_ENV=development ENVIRONMENT=local webpack serve", "build": "webpack --config webpack.config.js --bail", "build-exports": "webpack --config webpack.exports.js --bail", "storybook": "storybook dev -p 6006", - "build-storybook": "storybook build --output-dir build/storybook" + "build-storybook": "storybook build" }, "devDependencies": { "@emotion/react": "^11.11.1", @@ -33,6 +33,7 @@ "@testing-library/react": "14.3.1", "@testing-library/user-event": "14.5.2", "@types/lodash": "^4.14.182", + "@types/react-refresh": "^0", "@types/react-slick": "^0", "@types/slick-carousel": "^1", "copy-webpack-plugin": "^12.0.2", @@ -42,6 +43,7 @@ "html-webpack-plugin": "^5.6.0", "mini-css-extract-plugin": "^2.6.1", "ol-test-utilities": "0.0.0", + "react-refresh": "^0.14.2", "storybook": "^8.1.10", "swc-loader": "^0.2.6", "tsconfig-paths-webpack-plugin": "^4.1.0", @@ -57,8 +59,7 @@ }, "dependencies": { "@ebay/nice-modal-react": "^1.2.13", - "@mitodl/course-search-utils": "3.1.3", - "@mui/icons-material": "^5.15.15", + "@mitodl/course-search-utils": "^3.1.4", "@remixicon/react": "^4.2.0", "@sentry/react": "^7.57.0", "@tanstack/react-query": "^4.36.1", diff --git a/frontends/mit-open/public/images/navdrawer/certificate.svg b/frontends/mit-open/public/images/navdrawer/certificate.svg deleted file mode 100644 index 37410ceb24..0000000000 --- a/frontends/mit-open/public/images/navdrawer/certificate.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/frontends/mit-open/public/images/navdrawer/courses.svg b/frontends/mit-open/public/images/navdrawer/courses.svg deleted file mode 100644 index 7017824928..0000000000 --- a/frontends/mit-open/public/images/navdrawer/courses.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/frontends/mit-open/public/images/navdrawer/departments.svg b/frontends/mit-open/public/images/navdrawer/departments.svg deleted file mode 100644 index cd266fe5a5..0000000000 --- a/frontends/mit-open/public/images/navdrawer/departments.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/frontends/mit-open/public/images/navdrawer/free.svg b/frontends/mit-open/public/images/navdrawer/free.svg deleted file mode 100644 index 5c77692f56..0000000000 --- a/frontends/mit-open/public/images/navdrawer/free.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/frontends/mit-open/public/images/navdrawer/learning_materials.svg b/frontends/mit-open/public/images/navdrawer/learning_materials.svg deleted file mode 100644 index 061cc55e0f..0000000000 --- a/frontends/mit-open/public/images/navdrawer/learning_materials.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - diff --git a/frontends/mit-open/public/images/navdrawer/new.svg b/frontends/mit-open/public/images/navdrawer/new.svg deleted file mode 100644 index b338bb99da..0000000000 --- a/frontends/mit-open/public/images/navdrawer/new.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/frontends/mit-open/public/images/navdrawer/pathways.svg b/frontends/mit-open/public/images/navdrawer/pathways.svg deleted file mode 100644 index efe88552f3..0000000000 --- a/frontends/mit-open/public/images/navdrawer/pathways.svg +++ /dev/null @@ -1,7 +0,0 @@ - - - - - - - diff --git a/frontends/mit-open/public/images/navdrawer/popular.svg b/frontends/mit-open/public/images/navdrawer/popular.svg deleted file mode 100644 index 8988481082..0000000000 --- a/frontends/mit-open/public/images/navdrawer/popular.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - diff --git a/frontends/mit-open/public/images/navdrawer/programs.svg b/frontends/mit-open/public/images/navdrawer/programs.svg deleted file mode 100644 index 6fb3c243d2..0000000000 --- a/frontends/mit-open/public/images/navdrawer/programs.svg +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - - - - - - - - - diff --git a/frontends/mit-open/public/images/navdrawer/provider.svg b/frontends/mit-open/public/images/navdrawer/provider.svg deleted file mode 100644 index 1f5f1db289..0000000000 --- a/frontends/mit-open/public/images/navdrawer/provider.svg +++ /dev/null @@ -1,11 +0,0 @@ - - - - - - - - - - - diff --git a/frontends/mit-open/public/images/navdrawer/topics.svg b/frontends/mit-open/public/images/navdrawer/topics.svg deleted file mode 100644 index 266ce1b713..0000000000 --- a/frontends/mit-open/public/images/navdrawer/topics.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - - - - - - diff --git a/frontends/mit-open/public/images/ocw-logo.png b/frontends/mit-open/public/images/ocw-logo.png index 9a5e2d0b2b..c4ffd9a2f0 100644 Binary files a/frontends/mit-open/public/images/ocw-logo.png and b/frontends/mit-open/public/images/ocw-logo.png differ diff --git a/frontends/mit-open/src/common/urls.test.ts b/frontends/mit-open/src/common/urls.test.ts index 7ffd04c531..d1f3ed85dc 100644 --- a/frontends/mit-open/src/common/urls.test.ts +++ b/frontends/mit-open/src/common/urls.test.ts @@ -1,22 +1,20 @@ import { login } from "./urls" -const { MITOPEN_AXIOS_BASE_PATH } = process.env +const { MITOPEN_API_BASE_URL } = process.env test("login encodes the next parameter appropriately", () => { expect(login()).toBe( - `${MITOPEN_AXIOS_BASE_PATH}/login/ol-oidc/?next=http://localhost/`, + `${MITOPEN_API_BASE_URL}/login/ol-oidc/?next=http://localhost/`, ) expect(login({})).toBe( - `${MITOPEN_AXIOS_BASE_PATH}/login/ol-oidc/?next=http://localhost/`, + `${MITOPEN_API_BASE_URL}/login/ol-oidc/?next=http://localhost/`, ) expect( login({ pathname: "/foo/bar", }), - ).toBe( - `${MITOPEN_AXIOS_BASE_PATH}/login/ol-oidc/?next=http://localhost/foo/bar`, - ) + ).toBe(`${MITOPEN_API_BASE_URL}/login/ol-oidc/?next=http://localhost/foo/bar`) expect( login({ @@ -24,6 +22,6 @@ test("login encodes the next parameter appropriately", () => { search: "?cat=meow", }), ).toBe( - `${MITOPEN_AXIOS_BASE_PATH}/login/ol-oidc/?next=http://localhost/foo/bar%3Fcat%3Dmeow`, + `${MITOPEN_API_BASE_URL}/login/ol-oidc/?next=http://localhost/foo/bar%3Fcat%3Dmeow`, ) }) diff --git a/frontends/mit-open/src/common/urls.ts b/frontends/mit-open/src/common/urls.ts index 623e603d98..0fafaf37c5 100644 --- a/frontends/mit-open/src/common/urls.ts +++ b/frontends/mit-open/src/common/urls.ts @@ -40,8 +40,8 @@ export const makeChannelManageWidgetsPath = ( name: string, ) => generatePath(CHANNEL_EDIT_WIDGETS, { channelType, name }) -export const LOGIN = `${process.env.MITOPEN_AXIOS_BASE_PATH}/login/ol-oidc/` -export const LOGOUT = `${process.env.MITOPEN_AXIOS_BASE_PATH}/logout/` +export const LOGIN = `${process.env.MITOPEN_API_BASE_URL}/login/ol-oidc/` +export const LOGOUT = `${process.env.MITOPEN_API_BASE_URL}/logout/` /** * Returns the URL to the login page, with a `next` parameter to redirect back diff --git a/frontends/mit-open/src/components/ChannelMenu/ChannelMenu.test.tsx b/frontends/mit-open/src/components/ChannelMenu/ChannelMenu.test.tsx index c7b8ea36ea..fcf720665a 100644 --- a/frontends/mit-open/src/components/ChannelMenu/ChannelMenu.test.tsx +++ b/frontends/mit-open/src/components/ChannelMenu/ChannelMenu.test.tsx @@ -9,7 +9,7 @@ import { channels as factory } from "api/test-utils/factories" import { ThemeProvider } from "ol-components" describe("ChannelMenu", () => { - it("Includes links to channel management and widget management", async () => { + it("Includes links to channel management", async () => { const channel = factory.channel() setMockResponse.get( urls.channels.details(channel.channel_type, channel.name), @@ -29,10 +29,5 @@ describe("ChannelMenu", () => { expect((item1 as HTMLAnchorElement).href).toContain( `/c/${channel.channel_type}/${channel.name}/manage`, ) - - const item2 = screen.getByRole("menuitem", { name: "Manage Widgets" }) - expect((item2 as HTMLAnchorElement).href).toContain( - `/c/${channel.channel_type}/${channel.name}/manage/widgets`, - ) }) }) diff --git a/frontends/mit-open/src/components/ChannelMenu/ChannelMenu.tsx b/frontends/mit-open/src/components/ChannelMenu/ChannelMenu.tsx index ba08fb2b58..c80f773ecd 100644 --- a/frontends/mit-open/src/components/ChannelMenu/ChannelMenu.tsx +++ b/frontends/mit-open/src/components/ChannelMenu/ChannelMenu.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from "react" import * as routes from "../../common/urls" import { SimpleMenu, ActionButton, styled } from "ol-components" import type { SimpleMenuItem } from "ol-components" -import SettingsIcon from "@mui/icons-material/Settings" +import { RiSettings4Fill } from "@remixicon/react" const InvertedButton = styled(ActionButton)({ color: "white" }) @@ -17,11 +17,6 @@ const ChannelMenu: React.FC<{ channelType: string; name: string }> = ({ label: "Channel Settings", href: routes.makeChannelEditPath(channelType, name), }, - { - key: "widget", - label: "Manage Widgets", - href: routes.makeChannelManageWidgetsPath(channelType, name), - }, ] }, [channelType, name]) return ( @@ -29,7 +24,7 @@ const ChannelMenu: React.FC<{ channelType: string; name: string }> = ({ items={items} trigger={ - + } /> diff --git a/frontends/mit-open/src/components/RootTopicIcon/RootTopicIcon.tsx b/frontends/mit-open/src/components/RootTopicIcon/RootTopicIcon.tsx index 2b0c298b48..6cf4f4a06e 100644 --- a/frontends/mit-open/src/components/RootTopicIcon/RootTopicIcon.tsx +++ b/frontends/mit-open/src/components/RootTopicIcon/RootTopicIcon.tsx @@ -1,31 +1,31 @@ import { RiPaletteLine, - RiSeedlingLine, - RiBriefcaseLine, - RiMacbookLine, - RiBarChartBoxLine, + RiShakeHandsLine, RiEarthLine, - RiLightbulbLine, - RiBuildingLine, - RiServiceLine, RiQuillPenLine, - RiMicroscopeLine, + RiBriefcase3Line, + RiLightbulbFlashLine, + RiRobot2Line, + RiStethoscopeLine, + RiInfinityLine, + RiTestTubeLine, + RiUserSearchLine, } from "@remixicon/react" import React from "react" /* TODO Using any icons until we have a solution for specifying them */ const ICON_MAP = { - Business: RiBriefcaseLine, - Energy: RiLightbulbLine, - Engineering: RiBuildingLine, + Business: RiBriefcase3Line, + Energy: RiLightbulbFlashLine, + Engineering: RiRobot2Line, "Fine Arts": RiPaletteLine, - "Health and Medicine": RiServiceLine, + "Health and Medicine": RiStethoscopeLine, Humanities: RiQuillPenLine, - Mathematics: RiBarChartBoxLine, - Science: RiMicroscopeLine, - "Social Science": RiSeedlingLine, + Mathematics: RiInfinityLine, + Science: RiTestTubeLine, + "Social Science": RiUserSearchLine, Society: RiEarthLine, - "Teaching and Education": RiMacbookLine, + "Teaching and Education": RiShakeHandsLine, } type RootTopicIconProps = { name: string } diff --git a/frontends/mit-open/src/page-components/CardTemplate/CardTemplate.tsx b/frontends/mit-open/src/page-components/CardTemplate/CardTemplate.tsx index 77aa74385a..d88b45050c 100644 --- a/frontends/mit-open/src/page-components/CardTemplate/CardTemplate.tsx +++ b/frontends/mit-open/src/page-components/CardTemplate/CardTemplate.tsx @@ -7,7 +7,7 @@ import { styled, TruncateText, } from "ol-components" -import DragIndicatorIcon from "@mui/icons-material/DragIndicator" +import { RiDraggable } from "@remixicon/react" import { DEFAULT_RESOURCE_IMG, EmbedlyConfig, @@ -238,7 +238,7 @@ const CardTemplate = ({ {sortable ? ( - + ) : null} diff --git a/frontends/mit-open/src/page-components/ChannelDetails/ChannelDetails.tsx b/frontends/mit-open/src/page-components/ChannelDetails/ChannelDetails.tsx index 61337d8c2e..27dee8fa8e 100644 --- a/frontends/mit-open/src/page-components/ChannelDetails/ChannelDetails.tsx +++ b/frontends/mit-open/src/page-components/ChannelDetails/ChannelDetails.tsx @@ -2,7 +2,7 @@ import React, { useMemo } from "react" import { styled, Typography, Box } from "ol-components" import { capitalize } from "ol-utilities" import { ChannelTypeEnum, Channel } from "api/v0" -import OpenInNewIcon from "@mui/icons-material/OpenInNew" +import { RiExternalLinkLine } from "@remixicon/react" type ChannelDetailsProps = { channel: Channel @@ -88,9 +88,11 @@ const getFacetManifest = (channelType: ChannelTypeEnum) => { { name: "more_information", title: "More Information", + labelFunction: (key: string, channelTitle: string) => ( - - {channelTitle} website + // eslint-disable react/jsx-no-target-blank + + {channelTitle} Website ), order: 1, @@ -107,7 +109,7 @@ const getFacetManifest = (channelType: ChannelTypeEnum) => { }, { name: "certifications", - title: "Certificate", + title: "Certificates", order: 0, }, { diff --git a/frontends/mit-open/src/page-components/Dialogs/AddToListDialog.tsx b/frontends/mit-open/src/page-components/Dialogs/AddToListDialog.tsx index 5e9dcb9371..b087365b2f 100644 --- a/frontends/mit-open/src/page-components/Dialogs/AddToListDialog.tsx +++ b/frontends/mit-open/src/page-components/Dialogs/AddToListDialog.tsx @@ -11,10 +11,8 @@ import { styled, } from "ol-components" -import LockOpenIcon from "@mui/icons-material/LockOpen" -import LockIcon from "@mui/icons-material/Lock" +import { RiLockLine, RiLockUnlockLine, RiAddLine } from "@remixicon/react" -import AddIcon from "@mui/icons-material/Add" import * as NiceModal from "@ebay/nice-modal-react" import { @@ -209,7 +207,7 @@ const PrivacyChip: React.FC = ({ selectedOption, }) => { const isPublic = selectedOption === publicOption - const icon = isPublic ? : + const icon = isPublic ? : return } @@ -253,7 +251,7 @@ const AddToListDialogInner: React.FC = ({ manageListDialogs.upsertLearningPath()} > - + @@ -269,7 +267,7 @@ const AddToListDialogInner: React.FC = ({ manageListDialogs.upsertUserList()} > - + diff --git a/frontends/mit-open/src/page-components/Header/Header.tsx b/frontends/mit-open/src/page-components/Header/Header.tsx index 9f1a613a15..799cd61b45 100644 --- a/frontends/mit-open/src/page-components/Header/Header.tsx +++ b/frontends/mit-open/src/page-components/Header/Header.tsx @@ -9,7 +9,21 @@ import { ClickAwayListener, ActionButtonLink, } from "ol-components" -import { RiSearch2Line } from "@remixicon/react" +import { + RiSearch2Line, + RiPencilRulerLine, + RiStackLine, + RiSignpostLine, + RiBookMarkedLine, + RiPresentationLine, + RiNodeTree, + RiVerifiedBadgeLine, + RiFileAddLine, + RiTimeLine, + RiHeartLine, + RiPriceTag3Line, + RiAwardLine, +} from "@remixicon/react" import { MITLogoLink, useToggle } from "ol-utilities" import UserMenu from "./UserMenu" import { MenuButton } from "./MenuButton" @@ -79,10 +93,14 @@ const LogoLink = styled(MITLogoLink)(({ theme }) => ({ const LeftDivider = styled(Divider)({ margin: "0 24px", + height: "24px", + alignSelf: "auto", }) const RightDivider = styled(Divider)(({ theme }) => ({ margin: "0 32px", + height: "24px", + alignSelf: "auto", [theme.breakpoints.down("sm")]: { margin: "0 16px", }, @@ -151,29 +169,30 @@ const navData: NavData = { items: [ { title: "Courses", - icon: "/static/images/navdrawer/courses.svg", - description: "Learn with MIT instructors", - href: querifiedSearchUrl({ tab: "courses" }), + icon: , + description: + "Single courses on a specific subject, taught by MIT instructors", + href: querifiedSearchUrl({ resource_category: "course" }), }, { title: "Programs", - icon: "/static/images/navdrawer/programs.svg", + icon: , description: - "Learn in-depth from a series of courses and earn a certificate", - href: querifiedSearchUrl({ tab: "programs" }), + "A series of courses for in-depth learning across a range of topics", + href: querifiedSearchUrl({ resource_category: "program" }), }, { title: "Pathways", - icon: "/static/images/navdrawer/pathways.svg", + icon: , description: "Achieve your learning goals with a curated collection of courses", }, { title: "Learning Materials", - icon: "/static/images/navdrawer/learning_materials.svg", + icon: , description: - "Free teaching and learning materials including videos, podcasts, lecture notes, etc.", - href: querifiedSearchUrl({ tab: "learning-materials" }), + "Free learning and teaching materials, including videos, podcasts, lecture notes, and more", + href: querifiedSearchUrl({ resource_category: "learning_material" }), }, ], }, @@ -182,17 +201,17 @@ const navData: NavData = { items: [ { title: "By Topic", - icon: "/static/images/navdrawer/topics.svg", + icon: , href: TOPICS, }, { - title: "By Departments", - icon: "/static/images/navdrawer/departments.svg", + title: "By Department", + icon: , href: DEPARTMENTS, }, { title: "By Provider", - icon: "/static/images/navdrawer/provider.svg", + icon: , href: UNITS, }, ], @@ -202,27 +221,27 @@ const navData: NavData = { items: [ { title: "New", - icon: "/static/images/navdrawer/new.svg", + icon: , href: querifiedSearchUrl({ sortby: "new" }), }, { title: "Upcoming", - icon: "/static/images/navdrawer/free.svg", + icon: , href: querifiedSearchUrl({ sortby: "upcoming" }), }, { title: "Popular", - icon: "/static/images/navdrawer/popular.svg", href: querifiedSearchUrl({ sortby: "-views" }), + icon: , }, { title: "Free", - icon: "/static/images/navdrawer/free.svg", + icon: , href: querifiedSearchUrl({ free: "true" }), }, { title: "With Certificate", - icon: "/static/images/navdrawer/certificate.svg", + icon: , href: querifiedSearchUrl({ certification: "true" }), }, ], diff --git a/frontends/mit-open/src/page-components/LearningResourceCard/LearningResourceCard.tsx b/frontends/mit-open/src/page-components/LearningResourceCard/LearningResourceCard.tsx index 52689ea190..11b26ae985 100644 --- a/frontends/mit-open/src/page-components/LearningResourceCard/LearningResourceCard.tsx +++ b/frontends/mit-open/src/page-components/LearningResourceCard/LearningResourceCard.tsx @@ -10,8 +10,6 @@ import * as NiceModal from "@ebay/nice-modal-react" import LearningResourceCardTemplate from "@/page-components/LearningResourceCardTemplate/LearningResourceCardTemplate" import type { LearningResourceCardTemplateProps } from "@/page-components/LearningResourceCardTemplate/LearningResourceCardTemplate" import { ActionButton, imgConfigs } from "ol-components" -import PlaylistAddIcon from "@mui/icons-material/PlaylistAdd" -import BookmarkBorderIcon from "@mui/icons-material/BookmarkBorder" import { AddToLearningPathDialog, AddToUserListDialog, @@ -20,6 +18,8 @@ import { LearningResource } from "api" import { useUserMe } from "api/hooks/user" import { useOpenLearningResourceDrawer } from "../LearningResourceDrawer/LearningResourceDrawer" +import { RiMenuAddLine, RiBookmarkLine } from "@remixicon/react" + type LearningResourceCardProps = Pick< LearningResourceCardTemplateProps, "variant" | "resource" | "className" | "sortable" | "suppressImage" @@ -77,7 +77,7 @@ const LearningResourceCard: React.FC = ({ aria-label="Add to Learning Path" onClick={showAddToLearningPathDialog} > - + )} {user?.is_authenticated && ( @@ -88,7 +88,7 @@ const LearningResourceCard: React.FC = ({ aria-label="Add to User List" onClick={showAddToUserListDialog} > - + )} diff --git a/frontends/mit-open/src/page-components/LearningResourceCardTemplate/LearningResourceCardTemplate.test.tsx b/frontends/mit-open/src/page-components/LearningResourceCardTemplate/LearningResourceCardTemplate.test.tsx index 948901a64b..fc121f569d 100644 --- a/frontends/mit-open/src/page-components/LearningResourceCardTemplate/LearningResourceCardTemplate.test.tsx +++ b/frontends/mit-open/src/page-components/LearningResourceCardTemplate/LearningResourceCardTemplate.test.tsx @@ -142,7 +142,7 @@ describe("LearningResourceCard", () => { />, ) - expect(!!screen.queryByTestId("DragIndicatorIcon")).toBe(sortable) + expect(!!screen.queryByTestId("CardDraggable")).toBe(sortable) }) it.each([{ variant: "row" }, { variant: "column" }] as const)( diff --git a/frontends/mit-open/src/page-components/LearningResourceCardTemplate/LearningResourceCardTemplate.tsx b/frontends/mit-open/src/page-components/LearningResourceCardTemplate/LearningResourceCardTemplate.tsx index 1909c8dd66..7fda5f83e6 100644 --- a/frontends/mit-open/src/page-components/LearningResourceCardTemplate/LearningResourceCardTemplate.tsx +++ b/frontends/mit-open/src/page-components/LearningResourceCardTemplate/LearningResourceCardTemplate.tsx @@ -2,7 +2,7 @@ import React, { useCallback } from "react" import { ResourceTypeEnum } from "api" import type { LearningResource } from "api" import { Chip, styled } from "ol-components" -import CalendarTodayIcon from "@mui/icons-material/CalendarToday" +import { RiCalendarLine } from "@remixicon/react" import { formatDate, pluralize, @@ -72,7 +72,7 @@ const ResourceFooterDetails: React.FC< return ( } + icon={} label={formattedDate} /> ) diff --git a/frontends/mit-open/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx b/frontends/mit-open/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx index 358f21718d..ed54fb90f0 100644 --- a/frontends/mit-open/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx +++ b/frontends/mit-open/src/page-components/ResourceCarousel/ResourceCarousel.test.tsx @@ -83,19 +83,13 @@ describe("ResourceCarousel", () => { }, ] - const { resources, resolve } = setupApis({ autoResolve: false }) + const { resources } = setupApis({ autoResolve: true }) renderWithProviders( , ) - expectLastProps(spyLearningResourceCard, { - isLoading: true, - ...cardProps, - }) - resolve() - - const tabs = screen.getAllByRole("tab") + const tabs = await screen.findAllByRole("tab") expect(tabs).toHaveLength(2) expect(tabs[0]).toHaveTextContent("Resources") @@ -178,7 +172,7 @@ describe("ResourceCarousel", () => { expect(urlParams.get("professional")).toEqual("true") }) - it("Shows the correct title", () => { + it("Shows the correct title", async () => { const config: ResourceCarouselProps["config"] = [ { label: "Resources", @@ -193,8 +187,7 @@ describe("ResourceCarousel", () => { renderWithProviders( , ) - expect( - screen.getByRole("heading", { name: "My Favorite Carousel" }), - ).toBeInTheDocument() + + await screen.findByRole("heading", { name: "My Favorite Carousel" }) }) }) diff --git a/frontends/mit-open/src/page-components/ResourceCarousel/ResourceCarousel.tsx b/frontends/mit-open/src/page-components/ResourceCarousel/ResourceCarousel.tsx index b88abefe26..feac14bc43 100644 --- a/frontends/mit-open/src/page-components/ResourceCarousel/ResourceCarousel.tsx +++ b/frontends/mit-open/src/page-components/ResourceCarousel/ResourceCarousel.tsx @@ -1,9 +1,5 @@ import React from "react" -import { - useFeaturedLearningResourcesList, - useLearningResourcesList, - useLearningResourcesSearch, -} from "api/hooks/learningResources" +import { learningResourcesKeyFactory } from "api/hooks/learningResources" import { Carousel, TabButton, @@ -13,14 +9,14 @@ import { styled, Typography, } from "ol-components" -import type { - TabConfig, - ResourceDataSource, - SearchDataSource, - FeaturedDataSource, -} from "./types" -import { LearningResource } from "api" +import type { TabConfig } from "./types" +import { LearningResource, PaginatedLearningResourceList } from "api" import { ResourceCard } from "../ResourceCard/ResourceCard" +import { + useQueries, + UseQueryResult, + UseQueryOptions, +} from "@tanstack/react-query" const StyledCarousel = styled(Carousel)({ /** @@ -40,107 +36,6 @@ const StyledCarousel = styled(Carousel)({ }, }) -type DataPanelProps = { - dataConfig: T - isLoading?: boolean - children: ({ - resources, - childrenLoading, - }: { - resources: LearningResource[] - childrenLoading: boolean - }) => React.ReactNode -} - -type LoadTabButtonProps = { - config: FeaturedDataSource - label: React.ReactNode - key: number - value: string -} - -const ResourcesData: React.FC> = ({ - dataConfig, - children, -}) => { - const { data, isLoading } = useLearningResourcesList(dataConfig.params) - return children({ - resources: data?.results ?? [], - childrenLoading: isLoading, - }) -} - -const SearchData: React.FC> = ({ - dataConfig, - children, -}) => { - const { data, isLoading } = useLearningResourcesSearch(dataConfig.params) - return children({ - resources: data?.results ?? [], - childrenLoading: isLoading, - }) -} - -const FeaturedData: React.FC> = ({ - dataConfig, - children, -}) => { - const { data, isLoading } = useFeaturedLearningResourcesList( - dataConfig.params, - ) - return children({ - resources: data?.results ?? [], - childrenLoading: isLoading, - }) -} - -/** - * A wrapper to load data based `TabConfig.data`. - * - * For each `TabConfig.data.type`, a different API endpoint, and hence - * react-query hook, is used. Since hooks can't be called conditionally within - * a single component, each type of data is handled in a separate component. - */ -const DataPanel: React.FC = ({ - dataConfig, - isLoading, - children, -}) => { - if (!isLoading) { - switch (dataConfig.type) { - case "resources": - return {children} - case "lr_search": - return {children} - case "lr_featured": - return {children} - default: - // @ts-expect-error This will always be an error if the switch statement - // is exhaustive since dataConfig will have type `never` - throw new Error(`Unknown data type: ${dataConfig.type}`) - } - } else - return children({ - resources: [], - childrenLoading: true, - }) -} - -/** - * Tab button that loads the resource, so we can determine if it needs to be - * displayed or not. This shouldn't cause double-loading since React Query - * should only run the thing once - when you switch into the tab, the data - * should already be in the cache. - */ - -const LoadFeaturedTabButton: React.FC = (props) => { - const { data, isLoading } = useFeaturedLearningResourcesList( - props.config.params, - ) - - return !isLoading && data && data.count > 0 ? : null -} - const HeaderRow = styled.div(({ theme }) => ({ display: "flex", flexWrap: "wrap", @@ -195,48 +90,46 @@ const TabsList = styled(TabButtonList)({ type ContentProps = { resources: LearningResource[] - isLoading?: boolean + childrenLoading?: boolean tabConfig: TabConfig } type PanelChildrenProps = { config: TabConfig[] + queries: UseQueryResult[] children: (props: ContentProps) => React.ReactNode - isLoading?: boolean } const PanelChildren: React.FC = ({ config, + queries, children, - isLoading, }) => { if (config.length === 1) { - return ( - - {({ resources, childrenLoading }) => - children({ - resources, - isLoading: childrenLoading || isLoading, - tabConfig: config[0], - }) - } - - ) + const { data, isLoading } = queries[0] + const resources = data?.results ?? [] + + return children({ + resources, + childrenLoading: isLoading, + tabConfig: config[0], + }) } return ( <> - {config.map((tabConfig, index) => ( - - - {({ resources, childrenLoading }) => - children({ - resources, - isLoading: childrenLoading || isLoading, - tabConfig, - }) - } - - - ))} + {config.map((tabConfig, index) => { + const { data, isLoading } = queries[index] + const resources = data?.results ?? [] + + return ( + + {children({ + resources, + childrenLoading: isLoading, + tabConfig, + })} + + ) + })} ) } @@ -258,6 +151,7 @@ type ResourceCarouselProps = { title: string className?: string isLoading?: boolean + "data-testid"?: string } /** * A tabbed carousel that fetches resources based on the configuration provided. @@ -275,12 +169,45 @@ const ResourceCarousel: React.FC = ({ title, className, isLoading, + "data-testid": dataTestId, }) => { const [tab, setTab] = React.useState("0") const [ref, setRef] = React.useState(null) + const queries = useQueries({ + queries: config.map( + ( + tab, + ): UseQueryOptions< + PaginatedLearningResourceList, + unknown, + unknown, + // The factory-generated types for queryKeys are very specific (tuples not arrays) + // and assignable to the loose QueryKey (readonly unknown[]) on the UseQueryOptions generic. + // But! as a queryFn arg the more specific QueryKey cannot be assigned to the looser QueryKey. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + any + > => { + switch (tab.data.type) { + case "resources": + return learningResourcesKeyFactory.list(tab.data.params) + case "lr_search": + return learningResourcesKeyFactory.search(tab.data.params) + case "lr_featured": + return learningResourcesKeyFactory.featured(tab.data.params) + } + }, + ), + }) + + const allChildrenLoaded = queries.every(({ isLoading }) => !isLoading) + const allChildrenEmpty = queries.every(({ data }) => !data?.count) + if (!isLoading && allChildrenLoaded && allChildrenEmpty) { + return null + } + return ( - + {title} @@ -288,29 +215,32 @@ const ResourceCarousel: React.FC = ({ {config.length > 1 ? ( setTab(newValue)}> - {config.map((tabConfig, index) => - tabConfig.data.type === "lr_featured" ? ( - - ) : ( + {config.map((tabConfig, index) => { + if ( + !isLoading && + !queries[index].isLoading && + !queries[index].data?.count + ) { + return null + } + return ( - ), - )} + ) + })} ) : null} - - {({ resources, isLoading: childrenLoading, tabConfig }) => ( + []} + > + {({ resources, childrenLoading, tabConfig }) => ( {isLoading || childrenLoading ? Array.from({ length: 6 }).map((_, index) => ( diff --git a/frontends/mit-open/src/page-components/SearchDisplay/ProfessionalToggle.tsx b/frontends/mit-open/src/page-components/SearchDisplay/ProfessionalToggle.tsx index a72c0975a8..9edae6eec9 100644 --- a/frontends/mit-open/src/page-components/SearchDisplay/ProfessionalToggle.tsx +++ b/frontends/mit-open/src/page-components/SearchDisplay/ProfessionalToggle.tsx @@ -109,12 +109,12 @@ const ProfessionalToggle: React.FC<{ - Content developed from MIT’s academic curriculum + Content developed from MIT's academic curriculum - Content developed from MIT’s professional curriculum + Content developed from MIT's professional curriculum diff --git a/frontends/mit-open/src/page-components/SearchDisplay/ResourceTypeTabs.tsx b/frontends/mit-open/src/page-components/SearchDisplay/ResourceCategoryTabs.tsx similarity index 53% rename from frontends/mit-open/src/page-components/SearchDisplay/ResourceTypeTabs.tsx rename to frontends/mit-open/src/page-components/SearchDisplay/ResourceCategoryTabs.tsx index d28955dde4..95e74ff481 100644 --- a/frontends/mit-open/src/page-components/SearchDisplay/ResourceTypeTabs.tsx +++ b/frontends/mit-open/src/page-components/SearchDisplay/ResourceCategoryTabs.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react" +import React from "react" import { TabButton, TabContext, @@ -6,7 +6,7 @@ import { TabPanel, styled, } from "ol-components" -import { ResourceTypeEnum, LearningResourcesSearchResponse } from "api" +import { ResourceCategoryEnum, LearningResourcesSearchResponse } from "api" const TabsList = styled(TabButtonList)({ ".MuiTabScrollButton-root.Mui-disabled": { @@ -22,19 +22,19 @@ type TabConfig = { label: string name: string defaultTab?: boolean - resource_type: ResourceTypeEnum[] + resource_category: ResourceCategoryEnum | null } type Aggregations = LearningResourcesSearchResponse["metadata"]["aggregations"] -const resourceTypeCounts = (aggregations?: Aggregations) => { +const resourceCategoryCounts = (aggregations?: Aggregations) => { if (!aggregations) return null - const buckets = aggregations?.resource_type ?? [] + const buckets = aggregations?.resource_category ?? [] const counts = buckets.reduce( (acc, bucket) => { - acc[bucket.key as ResourceTypeEnum] = bucket.doc_count + acc[bucket.key as ResourceCategoryEnum] = bucket.doc_count return acc }, - {} as Record, + {} as Record, ) return counts } @@ -52,68 +52,75 @@ const appendCount = (label: string, count?: number | null) => { /** * */ -const ResourceTypesTabContext: React.FC<{ +const ResourceCategoryTabContext: React.FC<{ activeTabName: string children: React.ReactNode }> = ({ activeTabName, children }) => { return {children} } -type ResourceTypeTabsProps = { +type ResourceCategoryTabsProps = { aggregations?: Aggregations tabs: TabConfig[] setSearchParams: (fn: (prev: URLSearchParams) => URLSearchParams) => void onTabChange?: () => void className?: string } -const ResourceTypeTabList: React.FC = ({ +const ResourceCategoryTabList: React.FC = ({ tabs, aggregations, setSearchParams, onTabChange, className, }) => { - const withCounts = useMemo(() => { - const counts = resourceTypeCounts(aggregations) - return tabs.map((t) => { - const resourceTypes = - t.resource_type.length === 0 - ? Object.values(ResourceTypeEnum) - : t.resource_type - const count = counts - ? resourceTypes - .map((rt) => counts[rt] ?? 0) - .reduce((acc, c) => acc + c, 0) - : null - return { ...t, label: appendCount(t.label, count) } - }) - }, [tabs, aggregations]) + const counts = resourceCategoryCounts(aggregations) + const allCount = aggregations?.resource_category + ? (aggregations.resource_category || []).reduce((count, bucket) => { + count = count + bucket.doc_count + return count + }, 0) + : undefined + return ( { const tab = tabs.find((t) => t.name === value) - if (!tab) return setSearchParams((prev) => { const next = new URLSearchParams(prev) - if (tab.defaultTab) { - next.delete("tab") + if (tab?.resource_category) { + next.set("resource_category", tab.resource_category) } else { - next.set("tab", value) + next.delete("resource_category") } return next }) onTabChange?.() }} > - {withCounts.map((t) => { - return + {tabs.map((t) => { + let count: number | undefined + if (t.name === "all") { + count = allCount + } else { + count = + counts && t.resource_category + ? counts[t.resource_category] ?? 0 + : undefined + } + return ( + + ) })} ) } -const ResourceTypeTabPanels: React.FC<{ +const ResourceCategoryTabPanels: React.FC<{ tabs: TabConfig[] children?: React.ReactNode }> = ({ tabs, children }) => { @@ -129,26 +136,26 @@ const ResourceTypeTabPanels: React.FC<{ } /** - * Components for a tabbed search UI with tabs controlling resource_type facet. + * Components for a tabbed search UI with tabs controlling resource_category facet. * * Intended usage is: * ```jsx - * - * - * + * + * + * * Panel Content - * - * + * + * * ``` * * These are exported as three separate components (Context, TabList, TabPanels) * to facilitate placement within a grid layout. */ -const ResourceTypeTabs = { - Context: ResourceTypesTabContext, - TabList: ResourceTypeTabList, - TabPanels: ResourceTypeTabPanels, +const ResourceCategoryTabs = { + Context: ResourceCategoryTabContext, + TabList: ResourceCategoryTabList, + TabPanels: ResourceCategoryTabPanels, } -export { ResourceTypeTabs } +export { ResourceCategoryTabs } export type { TabConfig } diff --git a/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx b/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx index 94ea6772bc..391737997b 100644 --- a/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx +++ b/frontends/mit-open/src/page-components/SearchDisplay/SearchDisplay.tsx @@ -25,7 +25,7 @@ import { import { LearningResourcesSearchApiLearningResourcesSearchRetrieveRequest as LRSearchRequest, - ResourceTypeEnum, + ResourceCategoryEnum, } from "api" import { useLearningResourcesSearch } from "api/hooks/learningResources" import { GridColumn, GridContainer } from "@/components/GridLayout/GridLayout" @@ -40,9 +40,9 @@ import type { FacetManifest, } from "@mitodl/course-search-utils" import _ from "lodash" -import { ResourceTypeTabs } from "./ResourceTypeTabs" +import { ResourceCategoryTabs } from "./ResourceCategoryTabs" import ProfessionalToggle from "./ProfessionalToggle" -import type { TabConfig } from "./ResourceTypeTabs" +import type { TabConfig } from "./ResourceCategoryTabs" import { ResourceListCard } from "../ResourceCard/ResourceCard" import { useSearchParams } from "@mitodl/course-search-utils/react-router" @@ -51,25 +51,25 @@ export const StyledSelect = styled(SimpleSelect)` min-width: 160px; ` -export const StyledResourceTabs = styled(ResourceTypeTabs.TabList)` +const StyledResourceTabs = styled(ResourceCategoryTabs.TabList)` margin-top: 0 px; ` -export const DesktopSortContainer = styled.div` +const DesktopSortContainer = styled.div` float: right; ${({ theme }) => theme.breakpoints.down("md")} { display: none; } ` -export const MobileSortContainer = styled.div` +const MobileSortContainer = styled.div` float: right; ${({ theme }) => theme.breakpoints.up("md")} { display: none; } ` -export const FacetStyles = styled.div` +const FacetStyles = styled.div` * { color: ${({ theme }) => theme.palette.secondary.main}; } @@ -270,7 +270,7 @@ export const FacetStyles = styled.div` } ` -export const FilterTitle = styled.div` +const FilterTitle = styled.div` svg { margin-left: 8px; } @@ -282,7 +282,7 @@ export const FilterTitle = styled.div` color: ${({ theme }) => theme.custom.colors.darkGray2}; ` -export const FacetsTitleContainer = styled.div` +const FacetsTitleContainer = styled.div` display: flex; flex-direction: row; justify-content: space-between; @@ -293,8 +293,13 @@ export const FacetsTitleContainer = styled.div` const PaginationContainer = styled.div` display: flex; justify-content: end; - margin-top: 16px; - margin-bottom: 16px; + margin-top: 24px; + margin-bottom: 80px; + + ${({ theme }) => theme.breakpoints.down("md")} { + margin-top: 16px; + margin-bottom: 24px; + } ul li button.Mui-selected { ${({ theme }) => css({ ...theme.typography.subtitle1 })} @@ -406,39 +411,36 @@ const StyledGridContainer = styled(GridContainer)` const PAGE_SIZE = 10 const MAX_PAGE = 50 -export const getLastPage = (count: number): number => { +const getLastPage = (count: number): number => { const pages = Math.ceil(count / PAGE_SIZE) return pages > MAX_PAGE ? MAX_PAGE : pages } -export const TABS: TabConfig[] = [ +const TABS: TabConfig[] = [ { name: "all", label: "All", defaultTab: true, - resource_type: [], + resource_category: null, }, { name: "courses", label: "Courses", - resource_type: [ResourceTypeEnum.Course], + resource_category: ResourceCategoryEnum.Course, }, { name: "programs", label: "Programs", - resource_type: [ResourceTypeEnum.Program], + resource_category: ResourceCategoryEnum.Program, }, { name: "learning-materials", label: "Learning Materials", - resource_type: Object.values(ResourceTypeEnum).filter( - (v) => v !== ResourceTypeEnum.Course && v !== ResourceTypeEnum.Program, - ), + resource_category: ResourceCategoryEnum.LearningMaterial, }, ] -export const ALL_RESOURCE_TABS = TABS.map((t) => t.resource_type) -export const SORT_OPTIONS = [ +const SORT_OPTIONS = [ { label: "Best Match", value: "", @@ -505,21 +507,27 @@ const SearchDisplay: React.FC = ({ const [searchParams] = useSearchParams() const scrollHook = useRef(null) const activeTab = - TABS.find((t) => t.name === searchParams.get("tab")) ?? + TABS.find( + (t) => t.resource_category === searchParams.get("resource_category"), + ) ?? TABS.find((t) => t.defaultTab) ?? TABS[0] const allParams = useMemo(() => { return { ...constantSearchParams, - resource_type: activeTab.resource_type, + resource_category: activeTab.resource_category + ? [activeTab.resource_category] + : undefined, ...requestParams, - aggregations: facetNames as LRSearchRequest["aggregations"], + aggregations: (facetNames || []).concat([ + "resource_category", + ]) as LRSearchRequest["aggregations"], offset: (page - 1) * PAGE_SIZE, } }, [ requestParams, constantSearchParams, - activeTab?.resource_type, + activeTab?.resource_category, facetNames, page, ]) @@ -571,7 +579,7 @@ const SearchDisplay: React.FC = ({ return ( - + = ({ aggregations={data?.metadata.aggregations} onTabChange={() => setPage(1)} /> - + } @@ -67,7 +67,7 @@ const SearchSubscriptionToggle: React.FC = ({ - - )) + return ( + + +
+                    variant={variant}
+                    
+ edge={edge} +
+
+ {SIZES.map((size) => ( + + + + ))} +
+ ) }), ), )} @@ -321,6 +332,11 @@ export const ActionButtonsShowcase: Story = { alignItems="center" sx={{ my: 2 }} > +
+              variant={variant}
+              
+ edge={edge} +
{SIZES.map((size) => ( {ICONS.map((icon) => ( diff --git a/frontends/ol-components/src/components/Button/Button.tsx b/frontends/ol-components/src/components/Button/Button.tsx index 245987c87e..a4e3f7f124 100644 --- a/frontends/ol-components/src/components/Button/Button.tsx +++ b/frontends/ol-components/src/components/Button/Button.tsx @@ -77,7 +77,7 @@ const ButtonStyled = styled.button((props) => { ...props, } const { colors } = theme.custom - const hasBorder = variant === "secondary" || variant === "text" + const hasBorder = variant === "secondary" return [ { @@ -110,17 +110,22 @@ const ButtonStyled = styled.button((props) => { backgroundColor: colors.mitRed, color: colors.white, border: "none", + /* Shadow/04dp */ + boxShadow: + "0px 2px 4px 0px rgba(37, 38, 43, 0.10), 0px 3px 8px 0px rgba(37, 38, 43, 0.12)", ":hover:not(:disabled)": { backgroundColor: colors.red, + boxShadow: "none", }, ":disabled": { backgroundColor: colors.silverGray, + boxShadow: "none", }, }, hasBorder && { backgroundColor: "transparent", borderColor: "currentcolor", - borderStyle: variant === "secondary" ? "solid" : "none", + borderStyle: "solid", }, variant === "secondary" && { color: colors.red, @@ -132,6 +137,8 @@ const ButtonStyled = styled.button((props) => { }, }, variant === "text" && { + backgroundColor: "transparent", + borderStyle: "none", color: colors.darkGray2, ":hover:not(:disabled)": { backgroundColor: tinycolor(colors.darkGray1).setAlpha(0.06).toString(), @@ -177,14 +184,6 @@ const ButtonStyled = styled.button((props) => { // Pill-shaped buttons... Overlapping border radius get clipped to pill. borderRadius: "100vh", }, - edge === "none" && { - border: "none", - ":hover:not(:disabled)": { - "&&": { - backgroundColor: "inherit", - }, - }, - }, // color color === "secondary" && { color: theme.custom.colors.silverGray, diff --git a/frontends/ol-components/src/components/Card/Card.stories.tsx b/frontends/ol-components/src/components/Card/Card.stories.tsx index 9d7f3b630a..6df43c1b34 100644 --- a/frontends/ol-components/src/components/Card/Card.stories.tsx +++ b/frontends/ol-components/src/components/Card/Card.stories.tsx @@ -6,7 +6,7 @@ import { RiMenuAddLine, RiBookmarkLine } from "@remixicon/react" import { withRouter } from "storybook-addon-react-router-v6" const meta: Meta = { - title: "ol-components/Card", + title: "smoot-design/Cards/Card", argTypes: { size: { options: ["small", "medium"], diff --git a/frontends/ol-components/src/components/Carousel/Carousel.stories.tsx b/frontends/ol-components/src/components/Carousel/Carousel.stories.tsx index 10a62482c3..1ff940a459 100644 --- a/frontends/ol-components/src/components/Carousel/Carousel.stories.tsx +++ b/frontends/ol-components/src/components/Carousel/Carousel.stories.tsx @@ -26,7 +26,7 @@ const StyledCarousel = styled(Carousel)({ }) const meta: Meta = { - title: "ol-components/Carousel", + title: "smoot-design/Carousel", render: (props) => (
This carousel: diff --git a/frontends/ol-components/src/components/Checkbox/Checkbox.stories.tsx b/frontends/ol-components/src/components/Checkbox/Checkbox.stories.tsx index d523119a07..214881b3bc 100644 --- a/frontends/ol-components/src/components/Checkbox/Checkbox.stories.tsx +++ b/frontends/ol-components/src/components/Checkbox/Checkbox.stories.tsx @@ -15,7 +15,7 @@ const StateWrapper = (props: CheckboxProps) => { } const meta: Meta = { - title: "ol-components/Checkbox", + title: "smoot-design/Checkbox", component: StateWrapper, argTypes: { onChange: { diff --git a/frontends/ol-components/src/components/Chips/Chip.stories.tsx b/frontends/ol-components/src/components/Chips/Chip.stories.tsx index 1cc86bfa66..e3edba480a 100644 --- a/frontends/ol-components/src/components/Chips/Chip.stories.tsx +++ b/frontends/ol-components/src/components/Chips/Chip.stories.tsx @@ -7,15 +7,17 @@ import Stack from "@mui/material/Stack" import { fn } from "@storybook/test" import { ChipLink } from "./ChipLink" import { withRouter } from "storybook-addon-react-router-v6" -import CalendarTodayIcon from "@mui/icons-material/CalendarToday" -import DeleteIcon from "@mui/icons-material/Delete" -import EditIcon from "@mui/icons-material/Edit" +import { + RiPencilFill, + RiCalendarLine, + RiDeleteBin7Fill, +} from "@remixicon/react" const icons = { None: undefined, - CalendarTodayIcon: , - DeleteIcon: , - EditIcon: , + CalendarTodayIcon: , + DeleteIcon: , + EditIcon: , } const VARIANTS: { @@ -125,7 +127,7 @@ export const Disabled: Story = { export const Icons: Story = { args: { - icon: , + icon: , }, } diff --git a/frontends/ol-components/src/components/ChoiceBox/ChoiceBox.stories.tsx b/frontends/ol-components/src/components/ChoiceBox/ChoiceBox.stories.tsx index 1ca5652ee7..1093acab61 100644 --- a/frontends/ol-components/src/components/ChoiceBox/ChoiceBox.stories.tsx +++ b/frontends/ol-components/src/components/ChoiceBox/ChoiceBox.stories.tsx @@ -15,7 +15,7 @@ const StateWrapper = (props: ChoiceBoxProps) => { } const meta: Meta = { - title: "ol-components/ChoiceBox", + title: "smoot-design/ChoiceBox", component: StateWrapper, argTypes: { type: { diff --git a/frontends/ol-components/src/components/ChoiceBox/ChoiceBoxFieldCheckbox.stories.tsx b/frontends/ol-components/src/components/ChoiceBox/ChoiceBoxFieldCheckbox.stories.tsx index c0a12525e1..aad62e7cd8 100644 --- a/frontends/ol-components/src/components/ChoiceBox/ChoiceBoxFieldCheckbox.stories.tsx +++ b/frontends/ol-components/src/components/ChoiceBox/ChoiceBoxFieldCheckbox.stories.tsx @@ -52,7 +52,7 @@ const StateWrapper = (props: CheckboxChoiceBoxFieldProps) => { } const meta: Meta = { - title: "ol-components/ChoiceBoxField", + title: "smoot-design/ChoiceBoxField", component: StateWrapper, } diff --git a/frontends/ol-components/src/components/ChoiceBox/ChoiceBoxFieldRadio.stories.tsx b/frontends/ol-components/src/components/ChoiceBox/ChoiceBoxFieldRadio.stories.tsx index fc8df86839..0110ccf909 100644 --- a/frontends/ol-components/src/components/ChoiceBox/ChoiceBoxFieldRadio.stories.tsx +++ b/frontends/ol-components/src/components/ChoiceBox/ChoiceBoxFieldRadio.stories.tsx @@ -44,7 +44,7 @@ const StateWrapper = (props: RadioChoiceBoxFieldProps) => { } const meta: Meta = { - title: "ol-components/ChoiceBoxField", + title: "smoot-design/ChoiceBoxField", component: StateWrapper, } diff --git a/frontends/ol-components/src/components/FormDialog/FormDialog.stories.tsx b/frontends/ol-components/src/components/FormDialog/FormDialog.stories.tsx index d759c6a3a7..878ed2fd94 100644 --- a/frontends/ol-components/src/components/FormDialog/FormDialog.stories.tsx +++ b/frontends/ol-components/src/components/FormDialog/FormDialog.stories.tsx @@ -35,7 +35,7 @@ const DialogDemo = (props: FormDialogProps) => { } const meta: Meta = { - title: "ol-components/FormDialog", + title: "smoot-design/FormDialog", component: DialogDemo, argTypes: { onReset: { diff --git a/frontends/ol-components/src/components/FormDialog/FormDialog.tsx b/frontends/ol-components/src/components/FormDialog/FormDialog.tsx index 24b16e648b..d50d1ab637 100644 --- a/frontends/ol-components/src/components/FormDialog/FormDialog.tsx +++ b/frontends/ol-components/src/components/FormDialog/FormDialog.tsx @@ -6,7 +6,7 @@ import DialogActions from "@mui/material/DialogActions" import DialogContent from "@mui/material/DialogContent" import DialogTitle from "@mui/material/DialogTitle" import type { DialogProps } from "@mui/material/Dialog" -import Close from "@mui/icons-material/Close" +import { RiCloseLine } from "@remixicon/react" const topRightStyle: React.CSSProperties = { position: "absolute", @@ -157,7 +157,7 @@ const FormDialog: React.FC = ({ onClick={onClose} aria-label="Close" > - +
{children} diff --git a/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx b/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx index 0e1e31c06f..608535d68f 100644 --- a/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx +++ b/frontends/ol-components/src/components/FormHelpers/FormHelpers.tsx @@ -1,6 +1,6 @@ import React from "react" import styled from "@emotion/styled" -import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined" +import { RiInformationLine } from "@remixicon/react" import Typography from "@mui/material/Typography" const Required = styled.span(({ theme }) => ({ @@ -129,7 +129,7 @@ const FormFieldWrapper: React.FC = ({ {helpText && {helpText}} {error && errorText && ( - + {errorText} )} diff --git a/frontends/ol-components/src/components/Input/Input.stories.tsx b/frontends/ol-components/src/components/Input/Input.stories.tsx index fad08dbf51..1cbc43f54f 100644 --- a/frontends/ol-components/src/components/Input/Input.stories.tsx +++ b/frontends/ol-components/src/components/Input/Input.stories.tsx @@ -4,9 +4,7 @@ import { Input, AdornmentButton } from "./Input" import type { InputProps } from "./Input" import Stack from "@mui/material/Stack" import Grid from "@mui/material/Grid" -import SearchIcon from "@mui/icons-material/Search" -import CalendarTodayIcon from "@mui/icons-material/CalendarToday" -import CloseIcon from "@mui/icons-material/Close" +import { RiCalendarLine, RiCloseLine, RiSearchLine } from "@remixicon/react" import { fn } from "@storybook/test" const SIZES = ["medium", "hero"] satisfies InputProps["size"][] @@ -14,26 +12,26 @@ const ADORNMENTS = { None: undefined, SearchIcon: ( - + ), CalendarTodayIcon: ( - + ), CloseIcon: ( - + ), "Close and Calendar": ( <> - + - + ), diff --git a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.stories.tsx b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.stories.tsx index 307bc4c962..59d16dbcda 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.stories.tsx +++ b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.stories.tsx @@ -22,7 +22,7 @@ const LearningResourceCardStyled = styled(LearningResourceCard)` ` const meta: Meta = { - title: "ol-components/LearningResourceCard", + title: "smoot-design/Cards/LearningResourceCard", argTypes: { resource: { options: ["Loading", "Without Image", ...Object.values(ResourceTypeEnum)], diff --git a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx index 817c8503d0..c8b0aa4b33 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx +++ b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceCard.tsx @@ -110,7 +110,7 @@ const StartDate: React.FC<{ resource: LearningResource; size?: Size }> = ({ resource, size, }) => { - const startDate = getStartDate(resource) + const startDate = getStartDate(resource, size) if (!startDate) return null diff --git a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.stories.tsx b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.stories.tsx index ce142c091f..78898821d7 100644 --- a/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.stories.tsx +++ b/frontends/ol-components/src/components/LearningResourceCard/LearningResourceListCard.stories.tsx @@ -15,7 +15,7 @@ const makeResource: typeof _makeResource = (overrides) => { } const meta: Meta = { - title: "ol-components/LearningResourceListCard", + title: "smoot-design/Cards/LearningResourceListCard", argTypes: { resource: { options: ["Loading", ...Object.values(ResourceTypeEnum)], diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSection.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSection.tsx index 14230cbf18..6b4ab05398 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/InfoSection.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/InfoSection.tsx @@ -3,13 +3,14 @@ import styled from "@emotion/styled" import ISO6391 from "iso-639-1" import { RemixiconComponentType, - RiMoneyDollarCircleFill, - RiBarChartFill, - RiGraduationCapFill, - RiGlobalLine, + RiVerifiedBadgeLine, RiTimeLine, RiCalendarLine, - RiBookReadFill, + RiListOrdered2, + RiPriceTag3Line, + RiDashboard3Line, + RiGraduationCapLine, + RiTranslate2, } from "@remixicon/react" import { LearningResource, @@ -66,7 +67,7 @@ type InfoItemConfig = { const INFO_ITEMS: InfoItemConfig = [ { label: "Price:", - Icon: RiMoneyDollarCircleFill, + Icon: RiPriceTag3Line, selector: (resource: LearningResource, run?: LearningResourceRun) => { if (!resource || !run) { return null @@ -84,7 +85,7 @@ const INFO_ITEMS: InfoItemConfig = [ { label: "Level:", - Icon: RiBarChartFill, + Icon: RiDashboard3Line, selector: (resource: LearningResource, run?: LearningResourceRun) => { return run?.level?.[0]?.name || null }, @@ -92,7 +93,7 @@ const INFO_ITEMS: InfoItemConfig = [ { label: "Instructors:", - Icon: RiGraduationCapFill, + Icon: RiGraduationCapLine, selector: (resource: LearningResource, run?: LearningResourceRun) => { return ( run?.instructors @@ -105,7 +106,7 @@ const INFO_ITEMS: InfoItemConfig = [ { label: "Languages:", - Icon: RiGlobalLine, + Icon: RiTranslate2, selector: (resource: LearningResource, run?: LearningResourceRun) => { return run?.languages?.length ? run.languages @@ -133,7 +134,7 @@ const INFO_ITEMS: InfoItemConfig = [ { label: "Offered By:", - Icon: RiBarChartFill, + Icon: RiVerifiedBadgeLine, selector: (resource: LearningResource) => { return resource.offered_by?.name || null }, @@ -150,7 +151,7 @@ const INFO_ITEMS: InfoItemConfig = [ { label: "Number of Courses:", - Icon: RiBookReadFill, + Icon: RiListOrdered2, selector: (resource: LearningResource) => { if (resource.resource_type === ResourceTypeEnum.Program) { return resource.program?.courses?.length || null diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.stories.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.stories.tsx index f86a942c6e..a7c5183a02 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.stories.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.stories.tsx @@ -18,7 +18,7 @@ const makeResource: typeof _makeResource = (overrides) => { } const meta: Meta = { - title: "ol-components/LearningResourceExpanded", + title: "smoot-design/LearningResourceExpanded", component: LearningResourceExpanded, args: { imgConfig: { diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.test.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.test.tsx index 139e11576b..705b66d457 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.test.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.test.tsx @@ -3,7 +3,7 @@ import { BrowserRouter } from "react-router-dom" import { render, screen, within } from "@testing-library/react" import { LearningResourceExpanded } from "./LearningResourceExpanded" import type { LearningResourceExpandedProps } from "./LearningResourceExpanded" -import { ResourceTypeEnum, PlatformEnum } from "api" +import { ResourceTypeEnum, PlatformEnum, PodcastEpisodeResource } from "api" import { factories } from "api/test-utils" import { ThemeProvider } from "../ThemeProvider/ThemeProvider" import { getReadableResourceType } from "ol-utilities" @@ -26,44 +26,51 @@ const setup = (resource: LearningResource) => { } describe("Learning Resource Expanded", () => { - test.each( - Object.values(ResourceTypeEnum).filter( - (type) => - type !== ResourceTypeEnum.Video && - type !== ResourceTypeEnum.VideoPlaylist, - ), - )('Renders image, title and link for resource type "%s"', (resourceType) => { - const resource = factories.learningResources.resource({ - resource_type: resourceType, - }) - - setup(resource) - - const images = screen.getAllByRole("img") - const image = images.find((img) => - img - .getAttribute("src") - ?.includes(encodeURIComponent(resource.image?.url ?? "")), - ) - expect(image).toBeInTheDocument() - invariant(image) - expect(image).toHaveAttribute("alt", resource.image?.alt ?? "") - - screen.getByRole("heading", { name: resource.title }) - - const linkName = - resource.resource_type === ResourceTypeEnum.Podcast || - resource.resource_type === ResourceTypeEnum.PodcastEpisode - ? `Listen to ${getReadableResourceType(resource.resource_type)}` - : `Take ${getReadableResourceType(resource.resource_type)}` - - if (linkName) { - const link = screen.getByRole("link", { - name: linkName, - }) as HTMLAnchorElement - expect(link.href).toMatch(new RegExp(`^${resource.url}/?$`)) - } - }) + const RESOURCE_TYPES = Object.values(ResourceTypeEnum) + const isVideo = (resourceType: ResourceTypeEnum) => + resourceType === ResourceTypeEnum.Video || + resourceType === ResourceTypeEnum.VideoPlaylist + + test.each(RESOURCE_TYPES.filter((type) => !isVideo(type)))( + 'Renders image and title for resource type "%s"', + (resourceType) => { + const resource = factories.learningResources.resource({ + resource_type: resourceType, + }) + + setup(resource) + + const images = screen.getAllByRole("img") + const image = images.find((img) => + img + .getAttribute("src") + ?.includes(encodeURIComponent(resource.image?.url ?? "")), + ) + expect(image).toBeInTheDocument() + invariant(image) + expect(image).toHaveAttribute("alt", resource.image?.alt ?? "") + + screen.getByRole("heading", { name: resource.title }) + + const linkName = + resource.resource_type === ResourceTypeEnum.Podcast || + resource.resource_type === ResourceTypeEnum.PodcastEpisode + ? "Listen to Podcast" + : `Take ${getReadableResourceType(resource.resource_type)}` + + const url = + resource.resource_type === ResourceTypeEnum.PodcastEpisode + ? (resource as PodcastEpisodeResource).podcast_episode?.episode_link + : resource.url + if (linkName) { + const link = screen.getByRole("link", { + name: linkName, + }) as HTMLAnchorElement + expect(link.target).toBe("_blank") + expect(link.href).toMatch(new RegExp(`^${url}/?$`)) + } + }, + ) test(`Renders card and title for resource type "${ResourceTypeEnum.Video}"`, () => { const resource = factories.learningResources.resource({ @@ -79,36 +86,76 @@ describe("Learning Resource Expanded", () => { screen.getByRole("heading", { name: resource.title }) }) - test.each( - Object.values(ResourceTypeEnum).filter( - (type) => type !== ResourceTypeEnum.Video, - ), - )('Renders topic section for resource type "%s"', (resourceType) => { - const resource = factories.learningResources.resource({ - resource_type: resourceType, - }) + test.each([ResourceTypeEnum.Program, ResourceTypeEnum.LearningPath])( + 'Renders CTA button for resource type "%s"', + (resourceType) => { + const resource = factories.learningResources.resource({ + resource_type: resourceType, + }) - setup(resource) + setup(resource) - const section = screen - .getByRole("heading", { name: "Topics" })! - .closest("section")! + const linkName = `Take ${getReadableResourceType(resource.resource_type)}` - const links = within(section).getAllByRole("link") + if (linkName) { + const link = screen.getByRole("link", { + name: linkName, + }) as HTMLAnchorElement - resource.topics!.forEach((topic, index) => { - expect(links[index]).toHaveAttribute("href", topic.channel_url) - expect(links[index]).toHaveTextContent(topic.name) - }) - }) + expect(link.href).toMatch(new RegExp(`^${resource.url}/?$`)) + } + }, + ) + + test.each([ResourceTypeEnum.PodcastEpisode])( + 'Renders CTA button for resource type "%s"', + (resourceType) => { + const resource = factories.learningResources.resource({ + resource_type: resourceType, + podcast_episode: { + episode_link: "https://example.com", + }, + }) + + setup(resource) + + const link = screen.getByRole("link", { + name: "Listen to Podcast", + }) as HTMLAnchorElement + + expect(link.href).toMatch( + new RegExp( + `^${(resource as PodcastEpisodeResource).podcast_episode?.episode_link}/?$`, + ), + ) + }, + ) + + test.each(RESOURCE_TYPES.filter((type) => !isVideo(type)))( + 'Renders topic section for resource type "%s"', + (resourceType) => { + const resource = factories.learningResources.resource({ + resource_type: resourceType, + }) + + setup(resource) + + const section = screen + .getByRole("heading", { name: "Topics" })! + .closest("section")! + + const links = within(section).getAllByRole("link") - test.each( - Object.values(ResourceTypeEnum).filter( - (type) => type !== ResourceTypeEnum.Video, - ), - )('Renders info section for resource type "%s"', (resourceType) => { + resource.topics!.forEach((topic, index) => { + expect(links[index]).toHaveAttribute("href", topic.channel_url) + expect(links[index]).toHaveTextContent(topic.name) + }) + }, + ) + + test(`Renders info section for resource type "${ResourceTypeEnum.Video}"`, () => { const resource = factories.learningResources.resource({ - resource_type: resourceType, + resource_type: ResourceTypeEnum.Video, }) setup(resource) diff --git a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx index 1a64331dcb..a906335138 100644 --- a/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx +++ b/frontends/ol-components/src/components/LearningResourceExpanded/LearningResourceExpanded.tsx @@ -90,13 +90,19 @@ const CallToAction = styled.div` display: flex; justify-content: space-between; align-items: center; + ${({ theme }) => theme.breakpoints.down("sm")} { + flex-wrap: wrap; + justify-content: center; + } ` -const StyledButton = styled(ButtonLink)` +const StyledLink = styled(ButtonLink)` text-align: center; width: 220px; ${({ theme }) => theme.breakpoints.down("sm")} { - width: 182px; + width: 100%; + margin-top: 10px; + margin-bottom: 10px; } ` @@ -182,6 +188,15 @@ const ImageSection: React.FC<{ } } +const getCallToActionUrl = (resource: LearningResource) => { + switch (resource.resource_type) { + case ResourceTypeEnum.PodcastEpisode: + return resource.podcast_episode?.episode_link + default: + return resource.url + } +} + const CallToActionSection = ({ resource, hide, @@ -205,19 +220,23 @@ const CallToActionSection = ({ const platformImage = PLATFORMS[resource?.platform?.code as PlatformEnum]?.image - const { resource_type: type, url, platform } = resource! + const { resource_type: type, platform } = resource! - const buttonPrefix = + const cta = type === ResourceTypeEnum.Podcast || type === ResourceTypeEnum.PodcastEpisode - ? "Listen to" - : "Take" + ? "Listen to Podcast" + : `Take ${getReadableResourceType(type)}` return ( - - {`${buttonPrefix} ${getReadableResourceType(type)}`} - + + {cta} + {platformImage ? ( on diff --git a/frontends/ol-components/src/components/LoadingSpinner/LoadingSpinner.stories.tsx b/frontends/ol-components/src/components/LoadingSpinner/LoadingSpinner.stories.tsx index 9363ca5e74..ebf4feab30 100644 --- a/frontends/ol-components/src/components/LoadingSpinner/LoadingSpinner.stories.tsx +++ b/frontends/ol-components/src/components/LoadingSpinner/LoadingSpinner.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react" import { LoadingSpinner } from "./LoadingSpinner" const meta: Meta = { - title: "ol-components/LoadingSpinner", + title: "smoot-design/LoadingSpinner", component: LoadingSpinner, } diff --git a/frontends/ol-components/src/components/Logo/Logo.tsx b/frontends/ol-components/src/components/Logo/Logo.tsx index 9637e0c34f..e0d9d0fda2 100644 --- a/frontends/ol-components/src/components/Logo/Logo.tsx +++ b/frontends/ol-components/src/components/Logo/Logo.tsx @@ -9,7 +9,7 @@ type PlatformObject = { export const PLATFORMS: Record = { [PlatformEnum.Ocw]: { name: "MIT OpenCourseWare", - image: "mit-ocw-logo-square.png", + image: "ocw-logo.png", }, [PlatformEnum.Edx]: { name: "edX", diff --git a/frontends/ol-components/src/components/NavDrawer/NavDrawer.stories.tsx b/frontends/ol-components/src/components/NavDrawer/NavDrawer.stories.tsx index 71cc683e73..98e258caac 100644 --- a/frontends/ol-components/src/components/NavDrawer/NavDrawer.stories.tsx +++ b/frontends/ol-components/src/components/NavDrawer/NavDrawer.stories.tsx @@ -52,7 +52,7 @@ const NavDrawerDemo = () => { } const meta: Meta = { - title: "ol-components/NavDrawer", + title: "smoot-design/NavDrawer", component: NavDrawerDemo, argTypes: { onReset: { diff --git a/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx b/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx index 70d1f89d82..f63d4cbf33 100644 --- a/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx +++ b/frontends/ol-components/src/components/NavDrawer/NavDrawer.tsx @@ -1,9 +1,9 @@ import Drawer, { DrawerProps } from "@mui/material/Drawer" import styled from "@emotion/styled" -import React from "react" +import React, { ReactElement } from "react" const DrawerContent = styled.div(({ theme }) => ({ - paddingTop: "80px", + paddingTop: "60px", width: "366px", height: "100%", background: theme.custom.colors.white, @@ -108,7 +108,7 @@ export interface NavSection { export interface NavItem { title: string - icon?: string + icon?: string | ReactElement description?: string href?: string } @@ -118,13 +118,14 @@ const NavItem: React.FC = (props) => { const navItem = ( - {icon ? ( + {typeof icon === "string" ? ( ) : null} + {typeof icon !== "string" ? icon : null} diff --git a/frontends/ol-components/src/components/PlainList/PlainList.stories.tsx b/frontends/ol-components/src/components/PlainList/PlainList.stories.tsx index 7468ddba69..8981a50103 100644 --- a/frontends/ol-components/src/components/PlainList/PlainList.stories.tsx +++ b/frontends/ol-components/src/components/PlainList/PlainList.stories.tsx @@ -8,7 +8,7 @@ const Item = styled.li` ` const meta: Meta = { - title: "ol-components/PlainList", + title: "smoot-design/PlainList", render: (args) => { return ( diff --git a/frontends/ol-components/src/components/Radio/Radio.stories.tsx b/frontends/ol-components/src/components/Radio/Radio.stories.tsx index d54413e056..e574968441 100644 --- a/frontends/ol-components/src/components/Radio/Radio.stories.tsx +++ b/frontends/ol-components/src/components/Radio/Radio.stories.tsx @@ -15,7 +15,7 @@ const StateWrapper = (props: RadioProps) => { } const meta: Meta = { - title: "ol-components/Radio", + title: "smoot-design/Radio", component: StateWrapper, argTypes: { onChange: { diff --git a/frontends/ol-components/src/components/RadioChoiceField/BooleanRadioChoiceField.stories.tsx b/frontends/ol-components/src/components/RadioChoiceField/BooleanRadioChoiceField.stories.tsx index 6a7d35653d..e948024152 100644 --- a/frontends/ol-components/src/components/RadioChoiceField/BooleanRadioChoiceField.stories.tsx +++ b/frontends/ol-components/src/components/RadioChoiceField/BooleanRadioChoiceField.stories.tsx @@ -18,7 +18,7 @@ function StateWrapper(props: BooleanRadioChoiceFieldProps) { } const meta: Meta = { - title: "ol-components/BooleanRadioChoiceField", + title: "~smoot-design/BooleanRadioChoiceField", component: StateWrapper, argTypes: { onChange: { diff --git a/frontends/ol-components/src/components/RadioChoiceField/RadioChoiceField.stories.tsx b/frontends/ol-components/src/components/RadioChoiceField/RadioChoiceField.stories.tsx index bb9f55855d..25e8903d09 100644 --- a/frontends/ol-components/src/components/RadioChoiceField/RadioChoiceField.stories.tsx +++ b/frontends/ol-components/src/components/RadioChoiceField/RadioChoiceField.stories.tsx @@ -4,7 +4,7 @@ import { useArgs } from "@storybook/preview-api" import { RadioChoiceField, BooleanRadioChoiceField } from "./RadioChoiceField" const meta: Meta = { - title: "ol-components/RadioChoiceField", + title: "smoot-design/RadioChoiceField", argTypes: { onChange: { action: "changed", diff --git a/frontends/ol-components/src/components/RoutedDrawer/RoutedDrawer.stories.tsx b/frontends/ol-components/src/components/RoutedDrawer/RoutedDrawer.stories.tsx index 861710f4b1..856b06aa7b 100644 --- a/frontends/ol-components/src/components/RoutedDrawer/RoutedDrawer.stories.tsx +++ b/frontends/ol-components/src/components/RoutedDrawer/RoutedDrawer.stories.tsx @@ -16,7 +16,7 @@ const Content = styled.div({ }) const meta: Meta = { - title: "ol-components/RoutedDrawer", + title: "smoot-design/RoutedDrawer", decorators: [withRouter], } diff --git a/frontends/ol-components/src/components/ShareTooltip/ShareTooltip.stories.tsx b/frontends/ol-components/src/components/ShareTooltip/ShareTooltip.stories.tsx index 767529c948..f847e0778d 100644 --- a/frontends/ol-components/src/components/ShareTooltip/ShareTooltip.stories.tsx +++ b/frontends/ol-components/src/components/ShareTooltip/ShareTooltip.stories.tsx @@ -3,7 +3,7 @@ import type { Meta, StoryObj } from "@storybook/react" import { ShareTooltip } from "./ShareTooltip" const meta: Meta = { - title: "ol-components/ShareTooltip", + title: "old/ShareTooltip", component: ShareTooltip, } diff --git a/frontends/ol-components/src/components/SimpleMenu/SimpleMenu.stories.tsx b/frontends/ol-components/src/components/SimpleMenu/SimpleMenu.stories.tsx index 41b85eb68c..7e2499280f 100644 --- a/frontends/ol-components/src/components/SimpleMenu/SimpleMenu.stories.tsx +++ b/frontends/ol-components/src/components/SimpleMenu/SimpleMenu.stories.tsx @@ -1,8 +1,7 @@ import React from "react" import type { Meta, StoryObj } from "@storybook/react" import { SimpleMenu } from "./SimpleMenu" -import EditIcon from "@mui/icons-material/Edit" -import DeleteIcon from "@mui/icons-material/Delete" +import { RiPencilFill, RiDeleteBin7Fill } from "@remixicon/react" import { withRouter } from "storybook-addon-react-router-v6" const meta: Meta = { @@ -45,11 +44,11 @@ export const PlainLinks: Story = { export const WithIcons: Story = { args: { items: [ - { key: "edit", label: "Edit", icon: , onClick: () => {} }, + { key: "edit", label: "Edit", icon: , onClick: () => {} }, { key: "delete", label: "Delete", - icon: , + icon: , onClick: () => {}, }, ], diff --git a/frontends/ol-components/src/components/TextField/TextField.stories.tsx b/frontends/ol-components/src/components/TextField/TextField.stories.tsx index 376c3affe1..4c484bdb16 100644 --- a/frontends/ol-components/src/components/TextField/TextField.stories.tsx +++ b/frontends/ol-components/src/components/TextField/TextField.stories.tsx @@ -5,9 +5,7 @@ import type { TextFieldProps } from "./TextField" import { AdornmentButton } from "../Input/Input" import Stack from "@mui/material/Stack" import Grid from "@mui/material/Grid" -import SearchIcon from "@mui/icons-material/Search" -import CalendarTodayIcon from "@mui/icons-material/CalendarToday" -import CloseIcon from "@mui/icons-material/Close" +import { RiSearchLine, RiCalendarLine, RiCloseLine } from "@remixicon/react" import { fn } from "@storybook/test" const SIZES = ["medium", "hero"] satisfies TextFieldProps["size"][] @@ -15,26 +13,26 @@ const ADORNMENTS = { None: undefined, SearchIcon: ( - + ), CalendarTodayIcon: ( - + ), CloseIcon: ( - + ), "Close and Calendar": ( <> - + - + ), diff --git a/frontends/ol-components/src/components/TruncateText/TruncateText.stories.tsx b/frontends/ol-components/src/components/TruncateText/TruncateText.stories.tsx index 65bbcf771e..40684dc769 100644 --- a/frontends/ol-components/src/components/TruncateText/TruncateText.stories.tsx +++ b/frontends/ol-components/src/components/TruncateText/TruncateText.stories.tsx @@ -2,7 +2,7 @@ import type { Meta, StoryObj } from "@storybook/react" import { TruncateText } from "./TruncateText" const meta: Meta = { - title: "ol-components/TruncateText", + title: "smoot-design/TruncateText", component: TruncateText, argTypes: { lineClamp: { diff --git a/frontends/ol-components/src/stories/Breakpoints.stories.tsx b/frontends/ol-components/src/stories/Breakpoints.stories.tsx index 696232d8cf..5f5fab95d1 100644 --- a/frontends/ol-components/src/stories/Breakpoints.stories.tsx +++ b/frontends/ol-components/src/stories/Breakpoints.stories.tsx @@ -23,7 +23,7 @@ const BreakpointsDemo = () => { } const meta: Meta = { - title: "ol-components/Breakpoints", + title: "smoot-design/Container Breakpoints", component: BreakpointsDemo, parameters: { viewports: {}, diff --git a/frontends/ol-widgets/package.json b/frontends/ol-widgets/package.json index 0ce9bc8566..a4e7356d1f 100644 --- a/frontends/ol-widgets/package.json +++ b/frontends/ol-widgets/package.json @@ -5,7 +5,7 @@ "main": "./src/index.ts", "dependencies": { "@faker-js/faker": "^8.0.0", - "@mui/icons-material": "^5.15.15", + "@remixicon/react": "^4.2.0", "classnames": "^2.3.2", "formik": "^2.4.5", "lodash": "^4.17.21", diff --git a/frontends/ol-widgets/src/components/Widget.tsx b/frontends/ol-widgets/src/components/Widget.tsx index 53b3af6f70..b75004e3df 100644 --- a/frontends/ol-widgets/src/components/Widget.tsx +++ b/frontends/ol-widgets/src/components/Widget.tsx @@ -1,11 +1,13 @@ import React, { useCallback } from "react" import classNames from "classnames" import { Divider, MuiCard, CardContent, CardActions } from "ol-components" -import IconEdit from "@mui/icons-material/Edit" -import IconDelete from "@mui/icons-material/Delete" -import IconDrag from "@mui/icons-material/DragHandle" -import IconExpand from "@mui/icons-material/ExpandMore" -import IconCollapse from "@mui/icons-material/ExpandLess" +import { + RiPencilFill, + RiDeleteBin7Fill, + RiArrowDownSLine, + RiArrowUpSLine, + RiDraggable, +} from "@remixicon/react" import type { AnonymousWidget, @@ -86,14 +88,14 @@ const WidgetTemplate: React.FC = ({ aria-label={btnLabel.collapse} onClick={handleVisibilityChange} > - + ) : ( ))} @@ -108,17 +110,17 @@ const WidgetTemplate: React.FC = ({ type="button" onClick={handleEdit} > - + diff --git a/frontends/ol-widgets/src/components/editing/WidgetsListEditable.tsx b/frontends/ol-widgets/src/components/editing/WidgetsListEditable.tsx index 8b393415c4..c914efff56 100644 --- a/frontends/ol-widgets/src/components/editing/WidgetsListEditable.tsx +++ b/frontends/ol-widgets/src/components/editing/WidgetsListEditable.tsx @@ -7,7 +7,7 @@ import { SortableList, SortEndEvent, } from "ol-components" -import AddIcon from "@mui/icons-material/Add" +import { RiAddLine } from "@remixicon/react" import { uniqueId, zip } from "lodash" import Widget from "../Widget" import type { WidgetListResponse, AnonymousWidget } from "../../interfaces" @@ -240,7 +240,7 @@ const WidgetsListEditable: React.FC = ({