From faf599ba7d1c1973e8e062a19f0b5989588ee13e Mon Sep 17 00:00:00 2001 From: zack <6351754+zackkrida@users.noreply.github.com> Date: Tue, 30 Jul 2024 08:38:42 -0400 Subject: [PATCH] Migrate to Nuxt 3 (#4257) Co-authored-by: Olga Bulat Co-authored-by: sarayourfriend --- .codespell/ignore_words.txt | 2 +- .eslintignore | 3 + .github/actions/build-docs/action.yml | 8 - .github/workflows/ci_cd.yml | 12 +- .pre-commit-config.yaml | 6 +- automations/data/labels.yml | 4 +- .../meta/documentation/guidelines.md | 4 +- frontend/.env.template | 61 +- frontend/.storybook/decorators/with-rtl.js | 9 +- .../.storybook/decorators/with-ui-store.js | 2 +- frontend/.storybook/main.js | 23 - frontend/.storybook/middleware.js | 1 - frontend/.storybook/preview.js | 30 - frontend/Dockerfile | 17 +- frontend/Dockerfile.playwright | 9 +- frontend/docker-compose.playwright.yml | 11 +- frontend/jest.config.ts | 38 - frontend/justfile | 1 + frontend/nuxt-template-overrides/App.js | 356 - frontend/nuxt-template-overrides/README.md | 13 - frontend/nuxt-template-overrides/index.js | 306 - frontend/nuxt.config.ts | 396 +- frontend/package.json | 142 +- frontend/server/plugins/sentry.ts | 29 + frontend/server/routes/healthcheck.ts | 8 + frontend/server/utils/logger.ts | 18 + frontend/src/app.html | 10 - frontend/src/app.vue | 68 + frontend/src/assets/fonts.css | 40 +- frontend/src/assets/svg/sprite/icons.svg | 38 + frontend/src/assets/svg/sprite/images.svg | 10 + frontend/src/assets/svg/sprite/licenses.svg | 14 + .../VAudioThumbnail/VAudioThumbnail.vue | 19 +- .../components/VAudioTrack/VAudioTrack.vue | 35 +- .../VAudioTrack/VGlobalAudioTrack.vue | 18 +- .../src/components/VAudioTrack/VWaveform.vue | 2 +- .../VAudioTrack/layouts/VBoxLayout.vue | 11 +- .../VAudioTrack/layouts/VGlobalLayout.vue | 5 +- .../VAudioTrack/layouts/VRowLayout.vue | 17 +- .../components/VBackToSearchResultsLink.vue | 1 - .../components/VBanner/VAnalyticsNotice.vue | 13 +- frontend/src/components/VBanner/VBanners.vue | 24 +- .../VBanner/VTranslationStatusBanner.vue | 16 +- frontend/src/components/VButton.vue | 8 +- .../src/components/VCheckbox/VCheckbox.vue | 10 +- .../components/VContentLink/VContentLink.vue | 2 +- .../VContentReport/VContentReportForm.vue | 41 +- .../VContentReport/VContentReportPopover.vue | 2 +- .../components/VContentReport/VDmcaNotice.vue | 9 +- .../VContentReport/VReportDescForm.vue | 10 +- .../VContentSwitcher/VSearchTypeButton.vue | 9 +- .../VContentSwitcher/VSearchTypeItem.vue | 11 +- .../VContentSwitcher/VSearchTypePopover.vue | 2 +- .../VContentSwitcher/VSearchTypes.vue | 3 +- .../components/VErrorSection/VErrorImage.vue | 70 +- .../VErrorSection/VErrorSection.vue | 62 +- .../components/VErrorSection/VNoResults.vue | 55 +- .../VExternalSearch/VExternalSearchForm.vue | 95 +- .../VExternalSearch/VExternalSourceList.vue | 50 +- .../components/VFilters/VFilterChecklist.vue | 21 +- .../VFilters/VLicenseExplanation.vue | 7 +- .../components/VFilters/VSearchGridFilter.vue | 7 +- frontend/src/components/VFourOhFour.vue | 16 +- .../VGlobalAudioSection.vue | 32 +- .../src/components/VHeader/VFilterButton.vue | 9 +- .../VHeader/VFilterIconOrCounter.vue | 2 +- .../src/components/VHeader/VHeaderDesktop.vue | 8 +- .../components/VHeader/VHeaderInternal.vue | 7 +- .../VHeaderMobile/VContentSettingsButton.vue | 7 +- .../VContentSettingsModalContent.vue | 6 +- .../VHeader/VHeaderMobile/VFilterTab.vue | 4 +- .../VHeader/VHeaderMobile/VHeaderMobile.vue | 260 +- .../VHeaderMobile/VSearchBarButton.vue | 8 +- frontend/src/components/VHeader/VHomeLink.vue | 2 +- .../src/components/VHeader/VLogoButton.vue | 4 +- .../src/components/VHeader/VPageLinks.vue | 2 +- .../VHeader/VSearchBar/VSearchBar.vue | 28 +- .../VHeader/VSearchBar/VSearchButton.vue | 3 +- .../VSearchBar/VStandaloneSearchBar.vue | 13 +- .../src/components/VHeader/VWordPressLink.vue | 11 +- .../meta/VContentSettingsModal.stories.js | 5 +- .../VHeader/meta/VHeaderInternal.stories.js | 5 +- frontend/src/components/VHideButton.vue | 4 +- .../components/VHomeGallery/VHomeGallery.vue | 14 +- frontend/src/components/VHomepageContent.vue | 30 +- frontend/src/components/VIcon/VIcon.vue | 100 +- .../components/VIconButton/VIconButton.vue | 1 - .../src/components/VImageCell/VImageCell.vue | 27 +- .../components/VInputField/VInputField.vue | 31 +- frontend/src/components/VItemGroup/VItem.vue | 37 +- .../src/components/VItemGroup/VItemGroup.vue | 8 +- .../VLanguageSelect/VLanguageSelect.vue | 48 +- frontend/src/components/VLicense/VLicense.vue | 7 +- .../components/VLicense/VLicenseElements.vue | 5 +- frontend/src/components/VLink.vue | 188 +- frontend/src/components/VLoadMore.vue | 24 +- .../components/VLogoLoader/VLogoLoader.vue | 2 +- .../VMediaInfo/VByLine/VScroller.vue | 7 +- .../VMediaInfo/VCollapsibleTagSection.vue | 10 +- .../components/VMediaInfo/VCopyLicense.vue | 5 +- .../components/VMediaInfo/VMediaDetails.vue | 29 +- .../components/VMediaInfo/VMediaLicense.vue | 19 +- .../src/components/VMediaInfo/VMediaReuse.vue | 5 +- .../src/components/VMediaInfo/VMediaTags.vue | 7 +- .../src/components/VMediaInfo/VMetadata.vue | 17 +- .../components/VMediaInfo/VRelatedMedia.vue | 21 +- .../VMediaInfo/VSourceProviderTooltip.vue | 7 +- .../VMediaInfo/meta/VMetadata.stories.js | 5 +- .../src/components/VModal/VInputModal.vue | 117 - frontend/src/components/VModal/VModal.vue | 12 +- .../src/components/VModal/VModalContent.vue | 309 +- .../src/components/VModal/VModalTarget.vue | 13 - .../components/VModal/meta/VModal.stories.js | 5 +- .../components/VPopover/VPopoverContent.vue | 2 +- frontend/src/components/VRadio/VRadio.vue | 15 +- .../VRecentSearches/VRecentSearches.vue | 6 +- .../VSafeBrowsing/VSafeBrowsing.vue | 24 +- .../components/VSafetyWall/VSafetyWall.vue | 11 +- frontend/src/components/VScrollButton.vue | 2 +- frontend/src/components/VScrollableLine.vue | 8 +- .../VSearchResultsGrid/VAudioInstructions.vue | 14 +- .../VSearchResultsGrid/VAudioResult.vue | 15 +- .../VSearchResultsGrid/VCollectionResults.vue | 4 +- .../VSearchResultsGrid/VImageCollection.vue | 4 +- .../VSearchResultsGrid/VSearchResults.vue | 14 +- .../src/components/VSearchResultsTitle.vue | 29 +- .../components/VSelectField/VSelectField.vue | 119 +- .../src/components/VSingleResultControls.vue | 2 +- .../components/VSkeleton/VGridSkeleton.vue | 11 +- frontend/src/components/VSketchFabViewer.vue | 12 +- frontend/src/components/VSnackbar.vue | 2 +- frontend/src/components/VSourcesTable.vue | 2 +- frontend/src/components/VSvg/VSvg.vue | 59 +- frontend/src/components/VTabs/VTab.vue | 4 +- frontend/src/components/VTag/VTag.vue | 1 - .../src/components/VWarningSuppressor.vue | 17 - frontend/src/composables/use-analytics.ts | 4 +- .../src/composables/use-collection-meta.ts | 3 +- .../src/composables/use-component-name.ts | 2 +- .../use-get-locale-formatted-number.ts | 10 +- frontend/src/composables/use-i18n-sync.ts | 33 - .../src/composables/use-i18n-utilities.ts | 22 +- frontend/src/composables/use-i18n.ts | 9 - .../src/composables/use-image-cell-size.ts | 2 +- frontend/src/composables/use-match-routes.ts | 8 +- frontend/src/composables/use-pages.ts | 19 +- frontend/src/composables/use-search-type.ts | 12 +- frontend/src/composables/use-search.ts | 6 +- frontend/src/composables/use-seekable.ts | 13 +- .../src/composables/use-sensitive-media.ts | 13 +- .../use-single-result-page-meta.ts | 15 +- frontend/src/composables/use-sprite.ts | 46 + frontend/src/composables/use-window-scroll.ts | 62 - frontend/src/constants/media.ts | 5 +- frontend/src/constants/meta.ts | 11 +- frontend/src/constants/window.ts | 3 - frontend/src/data/api-service.ts | 293 +- frontend/src/data/media-provider-service.ts | 48 - frontend/src/data/media-service.ts | 171 - frontend/src/data/report-service.ts | 23 - frontend/src/error.vue | 70 + frontend/src/layouts/content-layout.vue | 83 +- frontend/src/layouts/default.vue | 63 +- frontend/src/layouts/error.vue | 15 - frontend/src/layouts/search-layout.vue | 222 +- .../src/locales/scripts/get-translations.js | 4 +- frontend/src/locales/scripts/utils.js | 46 +- frontend/src/middleware/collection.ts | 40 +- .../src/middleware/feature-flags.global.ts | 11 + frontend/src/middleware/feature-flags.ts | 11 - frontend/src/middleware/search.ts | 57 +- frontend/src/middleware/single-result.ts | 91 +- frontend/src/modules/prometheus.ts | 98 - frontend/src/pages/about.vue | 62 +- .../src/pages/audio/{_id => [id]}/index.vue | 191 +- frontend/src/pages/audio/collection.vue | 159 +- frontend/src/pages/feedback.vue | 35 +- frontend/src/pages/image/[id]/index.vue | 303 + .../src/pages/image/{_id => [id]}/report.vue | 58 +- frontend/src/pages/image/_id/index.vue | 303 - frontend/src/pages/image/collection.vue | 136 +- frontend/src/pages/index.vue | 127 +- frontend/src/pages/preferences.vue | 79 +- frontend/src/pages/privacy.vue | 52 +- frontend/src/pages/search-help.vue | 57 +- frontend/src/pages/search.vue | 283 +- frontend/src/pages/sensitive-content.vue | 60 +- frontend/src/pages/sources.vue | 47 +- frontend/src/pages/tags.vue | 12 +- frontend/src/plugins/01.sentry.client.ts | 26 + frontend/src/plugins/analytics.ts | 38 +- frontend/src/plugins/api-token.server.ts | 87 +- frontend/src/plugins/errors.ts | 52 +- frontend/src/plugins/focus-visible.client.ts | 3 + frontend/src/plugins/init-stores.ts | 24 +- frontend/src/plugins/polyfills.client.ts | 3 + frontend/src/plugins/sentry.ts | 9 - frontend/src/plugins/ua-parse.ts | 24 - .../error_images/depressed_musician.jpg | Bin .../error_images/waiting_for_a_bite.jpg | Bin frontend/src/{static => public}/favicon.ico | Bin .../{static => public}/favicon_storybook.ico | Bin .../homepage_images/olympics/1.png | Bin .../homepage_images/olympics/10.png | Bin .../homepage_images/olympics/11.png | Bin .../homepage_images/olympics/12.png | Bin .../homepage_images/olympics/13.png | Bin .../homepage_images/olympics/14.png | Bin .../homepage_images/olympics/15.png | Bin .../homepage_images/olympics/2.png | Bin .../homepage_images/olympics/3.png | Bin .../homepage_images/olympics/4.png | Bin .../homepage_images/olympics/5.png | Bin .../homepage_images/olympics/6.png | Bin .../homepage_images/olympics/7.png | Bin .../homepage_images/olympics/8.png | Bin .../homepage_images/olympics/9.png | Bin .../homepage_images/pottery/1.png | Bin .../homepage_images/pottery/10.png | Bin .../homepage_images/pottery/11.png | Bin .../homepage_images/pottery/12.png | Bin .../homepage_images/pottery/13.png | Bin .../homepage_images/pottery/14.png | Bin .../homepage_images/pottery/15.png | Bin .../homepage_images/pottery/2.png | Bin .../homepage_images/pottery/3.png | Bin .../homepage_images/pottery/4.png | Bin .../homepage_images/pottery/5.png | Bin .../homepage_images/pottery/6.png | Bin .../homepage_images/pottery/7.png | Bin .../homepage_images/pottery/8.png | Bin .../homepage_images/pottery/9.png | Bin .../homepage_images/universe/1.png | Bin .../homepage_images/universe/10.png | Bin .../homepage_images/universe/11.png | Bin .../homepage_images/universe/12.png | Bin .../homepage_images/universe/13.png | Bin .../homepage_images/universe/14.png | Bin .../homepage_images/universe/15.png | Bin .../homepage_images/universe/2.png | Bin .../homepage_images/universe/3.png | Bin .../homepage_images/universe/4.png | Bin .../homepage_images/universe/5.png | Bin .../homepage_images/universe/6.png | Bin .../homepage_images/universe/7.png | Bin .../homepage_images/universe/8.png | Bin .../homepage_images/universe/9.png | Bin .../src/{static => public}/opensearch.xml | 0 .../{static => public}/openverse-default.jpg | Bin .../{static => public}/openverse-logo-180.png | Bin .../src/{static => public}/openverse-logo.svg | 0 ...NOT_MOVE_THIS_DIRECTORY_OR_ITS_CONTENTS.md | 0 .../static/img/cc-by_icon.svg | 0 .../static/img/cc-cc0_icon.svg | 0 .../static/img/cc-nc_icon.svg | 0 .../static/img/cc-nd_icon.svg | 0 .../static/img/cc-pdm_icon.svg | 0 .../static/img/cc-sa_icon.svg | 0 .../{static => public}/static/img/cc_icon.svg | 0 frontend/src/server-middleware/healthcheck.js | 11 - frontend/src/server-middleware/robots.js | 50 - frontend/src/static/favicon.svg | 28 - frontend/src/stores/active-media.ts | 5 +- frontend/src/stores/feature-flag.ts | 33 +- frontend/src/stores/media/index.ts | 47 +- frontend/src/stores/media/related-media.ts | 33 +- frontend/src/stores/media/services.ts | 11 - frontend/src/stores/media/single-result.ts | 60 +- frontend/src/stores/provider.ts | 30 +- frontend/src/stores/search.ts | 22 +- frontend/src/stores/ui.ts | 64 +- frontend/src/types/analytics.ts | 8 +- frontend/src/types/cookies.ts | 25 +- frontend/src/types/fetch-state.ts | 7 +- frontend/src/types/media.ts | 1 + frontend/src/types/modal.ts | 1 + frontend/src/utils/attribution-html.ts | 9 +- frontend/src/utils/console.ts | 12 +- frontend/src/utils/cookies.ts | 10 - frontend/src/utils/decode-media-data.ts | 37 +- frontend/src/utils/env.ts | 12 - frontend/src/utils/focus-management.ts | 2 +- frontend/src/utils/license.ts | 10 +- frontend/src/utils/metadata.ts | 18 +- frontend/src/utils/node-env.ts | 5 - frontend/src/utils/og.ts | 25 +- frontend/src/utils/query-utils.ts | 24 + frontend/src/utils/route-utils.ts | 13 + frontend/src/utils/search-query-transform.ts | 45 +- frontend/src/utils/sentry-config.ts | 36 - frontend/src/utils/string-to-boolean.ts | 39 - frontend/src/{plugins => }/vue-i18n.ts | 12 +- .../locales/{scripts => }/valid-locales.json | 0 .../e2e/all-results-analytics.spec.ts | 33 +- .../test/playwright/e2e/audio-detail.spec.ts | 4 +- .../test/playwright/e2e/collections.spec.ts | 7 +- .../e2e/filters-sidebar-keyboard.spec.ts | 2 - frontend/test/playwright/e2e/filters.spec.ts | 14 +- .../playwright/e2e/header-internal.spec.ts | 5 +- .../test/playwright/e2e/image-detail.spec.ts | 4 +- .../test/playwright/e2e/load-more.spec.ts | 12 +- .../test/playwright/e2e/preferences.spec.ts | 33 +- .../playwright/e2e/recent-searches.spec.ts | 29 +- .../playwright/e2e/search-navigation.spec.ts | 2 +- .../e2e/search-query-client.spec.ts | 2 +- .../e2e/search-query-server.spec.ts | 2 +- frontend/test/playwright/e2e/search.spec.ts | 3 + frontend/test/playwright/e2e/seo.spec.ts | 40 +- .../playwright/e2e/skip-to-content.spec.ts | 1 + frontend/test/playwright/playwright.config.ts | 4 + frontend/test/playwright/utils/components.ts | 5 +- frontend/test/playwright/utils/navigation.ts | 26 +- .../components/filters.spec.ts | 4 +- .../visual-regression/pages/errors.spec.ts | 13 +- ...h-result-image-no-results-ltr-sm-linux.png | Bin 39799 -> 39794 bytes .../search-result-timeout-ltr-sm-linux.png | Bin 27347 -> 27348 bytes .../ltr-full-page-report-xl-linux.png | Bin 268450 -> 268451 bytes .../visual-regression/pages/pages.spec.ts | 11 +- .../pages/search-with-banners.spec.ts | 6 +- ...93-4d62-b721-cda18797e3fd_keep-alive.json5 | 87 - ...93-4d62-b721-cda18797e3fe_keep-alive.json5 | 6 +- ...19-7232-40c2-b9ea-8d6c47e677f9_close.json5 | 431 + ...4d-4217-a4ea-aaa4e2aef873_keep-alive.json5 | 2 +- ...76-fdbd-4919-8b8f-b498ef246ee8_close.json5 | 2 +- ...49-41af-9d7c-9fb17d59553d_keep-alive.json5 | 2 +- ...ce-406b-ad87-9b17250331dd_keep-alive.json5 | 2 +- frontend/test/unit/setup-after-env.js | 19 - frontend/test/unit/setup.js | 43 - .../AudioTrack/v-audio-track.spec.js | 130 +- .../AudioTrack/v-box-layout.spec.js | 12 +- .../AudioTrack/v-full-layout.spec.js | 17 +- .../components/AudioTrack/waveform.spec.js | 23 +- .../v-external-source-list.spec.js | 40 - .../ImageDetails/v-copy-license.spec.js | 8 +- .../components/InputField/input-field.spec.js | 26 +- .../VHeader/SearchBar/search-bar.spec.js | 23 +- .../VMediaInfo/v-media-details.spec.js | 59 +- .../VSafetyWall/v-safety-wall.spec.js | 38 +- .../unit/specs/components/VTag/v-tag.spec.js | 13 +- .../v-search-results-title.spec.js.snap | 1616 +- .../specs/components/loading-icon.spec.js | 4 +- .../specs/components/scroll-button.spec.js | 8 +- .../unit/specs/components/v-button.spec.js | 39 +- .../unit/specs/components/v-checkbox.spec.js | 20 +- .../specs/components/v-content-link.spec.js | 48 + .../components/v-content-report-form.spec.js | 168 - .../specs/components/v-copy-button.spec.js | 9 +- .../components/v-filter-checklist.spec.js | 42 - .../specs/components/v-image-cell.spec.js | 35 +- .../specs/components/v-item-group.spec.js | 84 +- .../unit/specs/components/v-license.spec.js | 21 +- .../test/unit/specs/components/v-link.spec.js | 65 +- .../specs/components/v-logo-loader.spec.js | 28 +- .../unit/specs/components/v-modal.spec.js | 39 +- .../unit/specs/components/v-popover.spec.js | 61 +- .../components/v-search-grid-filter.spec.js | 67 - .../components/v-search-results-title.spec.js | 10 +- .../specs/components/v-sources-table.spec.js | 85 +- .../test/unit/specs/components/v-tabs.spec.js | 47 +- .../specs/composables/default-ref.spec.ts | 6 +- .../use-get-locale-formatted-number.spec.js | 15 +- .../composables/use-i18n-utilities.spec.js | 21 +- .../composables/use-image-cell-size.spec.js | 22 +- .../specs/composables/use-search-type.spec.js | 27 +- .../composables/use-sensitive-media.spec.ts | 56 +- .../composables/use-window-scroll.spec.js | 64 - .../specs/data/media-provider-service.spec.js | 28 - .../unit/specs/data/media-service.spec.ts | 128 - .../unit/specs/locales/json-to-pot.spec.js | 42 - .../specs/plugins/api-token.server.spec.ts | 308 - .../test/unit/specs/plugins/errors.spec.ts | 191 - .../specs/stores/active-media-store.spec.js | 4 +- .../specs/stores/feature-flag-store.spec.js | 13 +- .../specs/stores/media-store-fetching.spec.js | 217 + .../unit/specs/stores/media-store.spec.js | 314 +- .../test/unit/specs/stores/provider.spec.js | 95 +- .../unit/specs/stores/search-store.spec.js | 84 +- .../specs/stores/single-result-store.spec.js | 135 +- .../test/unit/specs/stores/ui-store.spec.js | 60 +- .../api-token-missing-credentials.spec.js | 26 + .../api-token/api-token-parallel.spec.js | 94 + .../api-token/api-token-successful.spec.js | 111 + .../api-token/api-token-unsuccessful.spec.js | 91 + .../test/unit/specs/utils/api-token/setup.js | 23 + .../unit/specs/utils/attribution-html.spec.ts | 60 +- frontend/test/unit/specs/utils/case.spec.js | 2 + .../test/unit/specs/utils/decode-data.spec.js | 20 +- .../specs/utils/decode-image-data.spec.js | 11 +- .../test/unit/specs/utils/deep-freeze.spec.js | 12 +- .../utils/get-additional-sources.spec.ts | 2 + .../test/unit/specs/utils/license.spec.ts | 11 +- .../test/unit/specs/utils/resampling.spec.js | 12 +- .../specs/utils/string-to-boolean.spec.js | 30 - .../test/unit/specs/utils/time-fmt.spec.ts | 34 +- .../test/unit/test-utils/api-service-mock.ts | 79 - frontend/test/unit/test-utils/i18n.js | 28 +- frontend/test/unit/test-utils/pinia.js | 36 +- .../test/unit/test-utils/render-suspended.ts | 184 + frontend/test/unit/test-utils/render.js | 31 +- frontend/test/unit/vitest-setup.ts | 4 + frontend/tsconfig.json | 46 +- frontend/typings/extend.d.ts | 7 + frontend/typings/nuxt__types/index.d.ts | 57 +- frontend/typings/svg/index.d.ts | 16 - frontend/typings/vue-router/index.d.ts | 7 - frontend/typings/vue/index.d.ts | 5 - frontend/vitest.config.mts | 16 + justfile | 1 + package.json | 15 +- packages/js/eslint-plugin/README.md | 2 +- packages/js/eslint-plugin/jest.config.js | 8 - packages/js/eslint-plugin/package.json | 9 +- .../js/eslint-plugin/src/configs/import.ts | 15 +- .../js/eslint-plugin/src/configs/index.ts | 24 +- packages/js/eslint-plugin/src/configs/vue.ts | 33 +- packages/js/eslint-plugin/vitest.config.mts | 9 + pnpm-lock.yaml | 27672 +++++----------- prettier.config.js | 3 +- utilities/load_testing/k6/frontend.js | 43 +- 419 files changed, 15520 insertions(+), 27806 deletions(-) delete mode 100644 frontend/.storybook/main.js delete mode 100644 frontend/.storybook/middleware.js delete mode 100644 frontend/.storybook/preview.js delete mode 100644 frontend/jest.config.ts delete mode 100644 frontend/nuxt-template-overrides/App.js delete mode 100644 frontend/nuxt-template-overrides/README.md delete mode 100644 frontend/nuxt-template-overrides/index.js create mode 100644 frontend/server/plugins/sentry.ts create mode 100644 frontend/server/routes/healthcheck.ts create mode 100644 frontend/server/utils/logger.ts delete mode 100644 frontend/src/app.html create mode 100644 frontend/src/app.vue create mode 100644 frontend/src/assets/svg/sprite/icons.svg create mode 100644 frontend/src/assets/svg/sprite/images.svg create mode 100644 frontend/src/assets/svg/sprite/licenses.svg delete mode 100644 frontend/src/components/VModal/VInputModal.vue delete mode 100644 frontend/src/components/VModal/VModalTarget.vue delete mode 100644 frontend/src/components/VWarningSuppressor.vue delete mode 100644 frontend/src/composables/use-i18n-sync.ts delete mode 100644 frontend/src/composables/use-i18n.ts create mode 100644 frontend/src/composables/use-sprite.ts delete mode 100644 frontend/src/composables/use-window-scroll.ts delete mode 100644 frontend/src/data/media-provider-service.ts delete mode 100644 frontend/src/data/media-service.ts delete mode 100644 frontend/src/data/report-service.ts create mode 100644 frontend/src/error.vue delete mode 100644 frontend/src/layouts/error.vue create mode 100644 frontend/src/middleware/feature-flags.global.ts delete mode 100644 frontend/src/middleware/feature-flags.ts delete mode 100644 frontend/src/modules/prometheus.ts rename frontend/src/pages/audio/{_id => [id]}/index.vue (50%) create mode 100644 frontend/src/pages/image/[id]/index.vue rename frontend/src/pages/image/{_id => [id]}/report.vue (66%) delete mode 100644 frontend/src/pages/image/_id/index.vue create mode 100644 frontend/src/plugins/01.sentry.client.ts delete mode 100644 frontend/src/plugins/sentry.ts delete mode 100644 frontend/src/plugins/ua-parse.ts rename frontend/src/{static => public}/error_images/depressed_musician.jpg (100%) rename frontend/src/{static => public}/error_images/waiting_for_a_bite.jpg (100%) rename frontend/src/{static => public}/favicon.ico (100%) rename frontend/src/{static => public}/favicon_storybook.ico (100%) rename frontend/src/{static => public}/homepage_images/olympics/1.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/10.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/11.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/12.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/13.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/14.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/15.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/2.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/3.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/4.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/5.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/6.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/7.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/8.png (100%) rename frontend/src/{static => public}/homepage_images/olympics/9.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/1.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/10.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/11.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/12.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/13.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/14.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/15.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/2.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/3.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/4.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/5.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/6.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/7.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/8.png (100%) rename frontend/src/{static => public}/homepage_images/pottery/9.png (100%) rename frontend/src/{static => public}/homepage_images/universe/1.png (100%) rename frontend/src/{static => public}/homepage_images/universe/10.png (100%) rename frontend/src/{static => public}/homepage_images/universe/11.png (100%) rename frontend/src/{static => public}/homepage_images/universe/12.png (100%) rename frontend/src/{static => public}/homepage_images/universe/13.png (100%) rename frontend/src/{static => public}/homepage_images/universe/14.png (100%) rename frontend/src/{static => public}/homepage_images/universe/15.png (100%) rename frontend/src/{static => public}/homepage_images/universe/2.png (100%) rename frontend/src/{static => public}/homepage_images/universe/3.png (100%) rename frontend/src/{static => public}/homepage_images/universe/4.png (100%) rename frontend/src/{static => public}/homepage_images/universe/5.png (100%) rename frontend/src/{static => public}/homepage_images/universe/6.png (100%) rename frontend/src/{static => public}/homepage_images/universe/7.png (100%) rename frontend/src/{static => public}/homepage_images/universe/8.png (100%) rename frontend/src/{static => public}/homepage_images/universe/9.png (100%) rename frontend/src/{static => public}/opensearch.xml (100%) rename frontend/src/{static => public}/openverse-default.jpg (100%) rename frontend/src/{static => public}/openverse-logo-180.png (100%) rename frontend/src/{static => public}/openverse-logo.svg (100%) rename frontend/src/{static => public}/static/img/.DO_NOT_MOVE_THIS_DIRECTORY_OR_ITS_CONTENTS.md (100%) rename frontend/src/{static => public}/static/img/cc-by_icon.svg (100%) rename frontend/src/{static => public}/static/img/cc-cc0_icon.svg (100%) rename frontend/src/{static => public}/static/img/cc-nc_icon.svg (100%) rename frontend/src/{static => public}/static/img/cc-nd_icon.svg (100%) rename frontend/src/{static => public}/static/img/cc-pdm_icon.svg (100%) rename frontend/src/{static => public}/static/img/cc-sa_icon.svg (100%) rename frontend/src/{static => public}/static/img/cc_icon.svg (100%) delete mode 100644 frontend/src/server-middleware/healthcheck.js delete mode 100644 frontend/src/server-middleware/robots.js delete mode 100644 frontend/src/static/favicon.svg delete mode 100644 frontend/src/stores/media/services.ts delete mode 100644 frontend/src/utils/cookies.ts delete mode 100644 frontend/src/utils/env.ts delete mode 100644 frontend/src/utils/node-env.ts create mode 100644 frontend/src/utils/query-utils.ts create mode 100644 frontend/src/utils/route-utils.ts delete mode 100644 frontend/src/utils/sentry-config.ts delete mode 100644 frontend/src/utils/string-to-boolean.ts rename frontend/src/{plugins => }/vue-i18n.ts (78%) rename frontend/test/locales/{scripts => }/valid-locales.json (100%) delete mode 100644 frontend/test/tapes/detail/images/da5cb478-c093-4d62-b721-cda18797e3fd_keep-alive.json5 create mode 100644 frontend/test/tapes/related/audio/1cb1af19-7232-40c2-b9ea-8d6c47e677f9_close.json5 delete mode 100644 frontend/test/unit/setup-after-env.js delete mode 100644 frontend/test/unit/setup.js delete mode 100644 frontend/test/unit/specs/components/ExternalSources/v-external-source-list.spec.js create mode 100644 frontend/test/unit/specs/components/v-content-link.spec.js delete mode 100644 frontend/test/unit/specs/components/v-content-report-form.spec.js delete mode 100644 frontend/test/unit/specs/components/v-filter-checklist.spec.js delete mode 100644 frontend/test/unit/specs/components/v-search-grid-filter.spec.js delete mode 100644 frontend/test/unit/specs/composables/use-window-scroll.spec.js delete mode 100644 frontend/test/unit/specs/data/media-provider-service.spec.js delete mode 100644 frontend/test/unit/specs/data/media-service.spec.ts delete mode 100644 frontend/test/unit/specs/locales/json-to-pot.spec.js delete mode 100644 frontend/test/unit/specs/plugins/api-token.server.spec.ts delete mode 100644 frontend/test/unit/specs/plugins/errors.spec.ts create mode 100644 frontend/test/unit/specs/stores/media-store-fetching.spec.js create mode 100644 frontend/test/unit/specs/utils/api-token/api-token-missing-credentials.spec.js create mode 100644 frontend/test/unit/specs/utils/api-token/api-token-parallel.spec.js create mode 100644 frontend/test/unit/specs/utils/api-token/api-token-successful.spec.js create mode 100644 frontend/test/unit/specs/utils/api-token/api-token-unsuccessful.spec.js create mode 100644 frontend/test/unit/specs/utils/api-token/setup.js delete mode 100644 frontend/test/unit/specs/utils/string-to-boolean.spec.js delete mode 100644 frontend/test/unit/test-utils/api-service-mock.ts create mode 100644 frontend/test/unit/test-utils/render-suspended.ts create mode 100644 frontend/test/unit/vitest-setup.ts create mode 100644 frontend/typings/extend.d.ts delete mode 100644 frontend/typings/svg/index.d.ts delete mode 100644 frontend/typings/vue-router/index.d.ts delete mode 100644 frontend/typings/vue/index.d.ts create mode 100644 frontend/vitest.config.mts delete mode 100644 packages/js/eslint-plugin/jest.config.js create mode 100644 packages/js/eslint-plugin/vitest.config.mts diff --git a/.codespell/ignore_words.txt b/.codespell/ignore_words.txt index 6d336421248..8ad2a60ae42 100644 --- a/.codespell/ignore_words.txt +++ b/.codespell/ignore_words.txt @@ -1,7 +1,7 @@ ;; Please include explanations for each ignored word. ;; See https://docs.openverse.org/meta/codespell.html for docs. -;; Jest's `afterAll` hook incorrectly matched +;; JS test library's `afterAll` hook incorrectly matched afterall ;; `nd` references licence part diff --git a/.eslintignore b/.eslintignore index d95ecf4d139..043e8c52369 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,8 @@ coverage +# Vendored module. See explanation in file +frontend/test/unit/test-utils/render-suspended.ts + frontend/test/tapes frontend/nuxt-template-overrides frontend/storybook-static diff --git a/.github/actions/build-docs/action.yml b/.github/actions/build-docs/action.yml index 8d3b39ec90c..654642d1131 100644 --- a/.github/actions/build-docs/action.yml +++ b/.github/actions/build-docs/action.yml @@ -17,15 +17,7 @@ runs: # Pass -W to fail CI if warnings exist just documentation/build -W - - name: Build Storybook - shell: bash - run: | - just frontend/run storybook:build-for-docs - mv frontend/storybook-static/favicon_storybook.ico frontend/storybook-static/favicon.ico - - # Storybook will be available at `/storybook` - name: Merge all docs shell: bash run: | mv documentation/_build /tmp/docs - mv frontend/storybook-static /tmp/docs/storybook diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index a4373a98236..bcb55952a9a 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -294,10 +294,10 @@ jobs: - name: Setup CI env uses: ./.github/actions/setup-env with: - # Python is not needed to run the tests. - setup_python: false - # Node.js is needed by lint actions. - install_recipe: "node-install" + setup_python: true + # Node.js is not needed to run the tests. + setup_nodejs: false + install_recipe: "" - name: Load Docker images uses: ./.github/actions/load-img @@ -796,14 +796,11 @@ jobs: name: - playwright_vr - playwright_e2e - - storybook include: - name: playwright_vr script: "test:playwright visual-regression -u" - name: playwright_e2e script: "test:playwright e2e" - - name: storybook - script: "test:storybook -u" steps: - name: Checkout repository @@ -866,7 +863,6 @@ jobs: name: - playwright_vr - playwright_e2e - - storybook steps: - name: Pass diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e373d77370a..4e7504aa123 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,8 +61,8 @@ repos: # Must include any plugins defined in prettier.config.js, along with TypeScript and Prettier themselves # Versions must be manually kept in sync with those in the pnpm-lock.yaml file to prevent drift. additional_dependencies: - - prettier@3.2.5 - - prettier-plugin-tailwindcss@0.5.14 + - prettier@3.3.2 + - prettier-plugin-tailwindcss@0.6.5 - typescript@5.5.2 - repo: https://github.com/koalaman/shellcheck-precommit @@ -119,7 +119,7 @@ repos: - id: types name: types files: ^(frontend|packages/js)/.*$ - entry: bash -c 'pnpm run -r types' + entry: bash -c 'pnpm run prepare:nuxt && pnpm run -r types' language: system pass_filenames: false diff --git a/automations/data/labels.yml b/automations/data/labels.yml index e376b8c703b..a3a11e36030 100644 --- a/automations/data/labels.yml +++ b/automations/data/labels.yml @@ -189,8 +189,8 @@ groups: - name: pytest description: Involves pytest emoji: "🐍" - - name: jest - description: Involves Jest + - name: vitest + description: Involves vitest emoji: "🟨" - name: playwright description: Involves Playwright diff --git a/documentation/meta/documentation/guidelines.md b/documentation/meta/documentation/guidelines.md index 80b4485622e..e8bdf37154b 100644 --- a/documentation/meta/documentation/guidelines.md +++ b/documentation/meta/documentation/guidelines.md @@ -12,8 +12,8 @@ maintainers which occurred after reading These are some aspects to consider when starting the document. **Type**: _(README, "Getting Started", Conceptual, Procedural -[tutorial/how-to/guide], Reference [API reference, Glossary, Troubleshooting, -Changelog])_\ +[tutorial/how-to/guide], Reference [API reference, Glossary, +Troubleshooting, Changelog])_\ **Audience**:\ **Purpose**:\ **Title**:\ diff --git a/frontend/.env.template b/frontend/.env.template index 57bfac3a2c4..4c33a86dc76 100644 --- a/frontend/.env.template +++ b/frontend/.env.template @@ -1,3 +1,58 @@ -#API_URL=http://127.17.0.1:8000/ -#API_CLIENT_ID="" -#API_CLIENT_SECRET="" +PORT=8443 + +# ---------------------------- # +# REQUIRED LOCAL ENV VARIABLES # +# ---------------------------- # +# DEPLOYMENT_ENV=local +# SITE_DOMAIN=localhost +# SITE_URL=http://localhost:8443 +#/** +#* We rely on the Nginx container running as `frontend_nginx` +#* in the local compose stack to proxy requests. Therefore, the +#* URL here is not for the Plausible container in the local stack, +#* but the Nginx service, which then itself forwards the requests +#* to the local Plausible instance. +#* +#* In production, the Nginx container is handling all requests +#* made to the root URL (openverse.org), and is configured to +#* forward Plausible requests to upstream Plausible. +#*/ +# PLAUSIBLE_SITE_URL = http://localhost:50290 + +# ---------------------------- # +# OPTIONAL LOCAL ENV VARIABLES +# ---------------------------- # +### API settings + +# NUXT_PUBLIC_API_URL=http://127.17.0.1:8000/ # local dev API +# NUXT_PUBLIC_API_URL=http://localhost:49153/ # talkback proxy +# NUXT_PUBLIC_API_URL=https://api.openverse.org/# prod API + +# NUXT_API_CLIENT_ID="" +# NUXT_API_CLIENT_SECRET="" + +# -------------------- # +# PRODUCTION VARIABLES # +# -------------------- # +# DEPLOYMENT_ENV=production +# NUXT_API_CLIENT_ID="" +# NUXT_API_CLIENT_SECRET="" +# NUXT_PUBLIC_API_URL=https://api.openverse.org/ +# NUXT_PUBLIC_SENTRY_DSN="" +# NUXT_PUBLIC_SENTRY_RELEASE="" +# SITE_URL=https://openverse.org +# SITE_DOMAIN=openverse.org +# PLAUSIBLE_SITE_URL=https://openverse.org + +# ----------------- # +# STAGING VARIABLES # +# ----------------- # +# DEPLOYMENT_ENV=staging +# NUXT_API_CLIENT_ID="" +# NUXT_API_CLIENT_SECRET="" +# NUXT_PUBLIC_API_URL=https://api-staging.openverse.org/ +# NUXT_PUBLIC_SENTRY_DSN="" +# NUXT_PUBLIC_SENTRY_RELEASE="" +# SITE_URL=https://staging.openverse.org +# SITE_DOMAIN=staging.openverse.org +# PLAUSIBLE_SITE_URL=https://staging.openverse.org diff --git a/frontend/.storybook/decorators/with-rtl.js b/frontend/.storybook/decorators/with-rtl.js index 384a001b174..112765a9271 100644 --- a/frontend/.storybook/decorators/with-rtl.js +++ b/frontend/.storybook/decorators/with-rtl.js @@ -1,9 +1,10 @@ -import Vue from "vue" +import { useI18n } from "#imports" + +import { ref, watch, onMounted, reactive } from "vue" -import { ref, watch, useContext, onMounted } from "@nuxtjs/composition-api" import { useEffect } from "@storybook/client-api" -const languageDirection = Vue.observable({ value: "ltr" }) +const languageDirection = reactive({ value: "ltr" }) export const WithRTL = (story, context) => { useEffect(() => { @@ -15,7 +16,7 @@ export const WithRTL = (story, context) => { components: { story }, setup() { const element = ref() - const { i18n } = useContext() + const { i18n } = useI18n({ useScope: "global" }) onMounted(() => { watch( languageDirection, diff --git a/frontend/.storybook/decorators/with-ui-store.js b/frontend/.storybook/decorators/with-ui-store.js index d57f28fa746..e47cb729815 100644 --- a/frontend/.storybook/decorators/with-ui-store.js +++ b/frontend/.storybook/decorators/with-ui-store.js @@ -1,4 +1,4 @@ -import { ref, onMounted } from "@nuxtjs/composition-api" +import { ref, onMounted } from "vue" import { useLayout } from "~/composables/use-layout" diff --git a/frontend/.storybook/main.js b/frontend/.storybook/main.js deleted file mode 100644 index e72ad6527b6..00000000000 --- a/frontend/.storybook/main.js +++ /dev/null @@ -1,23 +0,0 @@ -const { nuxifyStorybook } = require("../.nuxt-storybook/storybook/main") - -const storybook = nuxifyStorybook({ - webpackFinal(config) { - // extend config here - - return config - }, - stories: [ - // Add your stories here - ], - addons: [ - // Add your addons here - ], -}) - -const generatedIconsStory = storybook.stories.indexOf( - "@nuxtjs/svg-sprite/stories/*.stories.js" -) -storybook.stories[generatedIconsStory] = - "../node_modules/@nuxtjs/svg-sprite/stories/*.stories.js" - -module.exports = storybook diff --git a/frontend/.storybook/middleware.js b/frontend/.storybook/middleware.js deleted file mode 100644 index 29cf1ee3f4f..00000000000 --- a/frontend/.storybook/middleware.js +++ /dev/null @@ -1 +0,0 @@ -module.exports = require("../.nuxt-storybook/storybook/middleware") diff --git a/frontend/.storybook/preview.js b/frontend/.storybook/preview.js deleted file mode 100644 index 5e6b9d1c163..00000000000 --- a/frontend/.storybook/preview.js +++ /dev/null @@ -1,30 +0,0 @@ -/* eslint-disable import/order */ - -export * from "~~/.nuxt-storybook/storybook/preview" -import { - globalTypes as nuxtGlobalTypes, - decorators as nuxtDecorators, -} from "~~/.nuxt-storybook/storybook/preview" - -import { WithRTL } from "./decorators/with-rtl" -import { WithUiStore } from "./decorators/with-ui-store" - -/* eslint-enable import/order */ - -export const globalTypes = { - ...nuxtGlobalTypes, - languageDirection: { - name: "RTL", - description: "Simulate an RTL language.", - defaultValue: "ltr", - toolbar: { - icon: "globe", - items: [ - { value: "ltr", title: "LTR" }, - { value: "rtl", title: "RTL" }, - ], - }, - }, -} - -export const decorators = [...nuxtDecorators, WithRTL, WithUiStore] diff --git a/frontend/Dockerfile b/frontend/Dockerfile index 6520bde2422..19baf6a29cd 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -20,8 +20,7 @@ WORKDIR /home/node/ # Copy monorepo mocking files into `/home/node`, which pretends to be the monorepo root. # Note: these files must be manually un-ignored in the root .dockerignore # hadolint ignore=DL3022 -COPY --from=repo_root --chown=node:node .npmrc .pnpmfile.cjs pnpm-lock.yaml tsconfig.base.json ./ - +COPY --from=repo_root --chown=node:node .npmrc .pnpmfile.cjs pnpm-lock.yaml ./ RUN echo '{"packages":["frontend/"]}' > pnpm-workspace.yaml # Copy the `frontend/` directory into `/home/node/frontend`, as a package in the monorepo. @@ -40,9 +39,12 @@ RUN pnpm install && pnpm i18n # disable telemetry when building the app ENV NUXT_TELEMETRY_DISABLED=1 ENV NODE_ENV=production -ENV SENTRY_DSN=https://b6466b74788a4a2f8a7912eea912beb7@o787041.ingest.sentry.io/5799642 +ENV NUXT_PUBLIC_SENTRY_DSN=https://b6466b74788a4a2f8a7912eea912beb7@o787041.ingest.sentry.io/5799642 -ARG API_URL +ARG API_URL=https://api.openverse.org/ +ENV NUXT_PUBLIC_API_URL=$API_URL +# Increase memory limit for the build process (necessary for i18n routes) +ENV NODE_OPTIONS="--max_old_space_size=4096" RUN pnpm build @@ -67,7 +69,12 @@ COPY --from=builder --chown=node:node /home/node/frontend ./frontend/ WORKDIR /home/node/frontend/ ARG SEMANTIC_VERSION -ENV SENTRY_RELEASE=$SEMANTIC_VERSION +ARG DEPLOYMENT_ENV=production +ARG API_URL=https://api.openverse.org/ + +ENV NUXT_PUBLIC_API_URL=$API_URL +ENV NUXT_PUBLIC_SENTRY_RELEASE=$SEMANTIC_VERSION +ENV NUXT_PUBLIC_SENTRY_ENVIRONMENT=$DEPLOYMENT_ENV # set app serving to permissive / assigned ENV NUXT_HOST=0.0.0.0 diff --git a/frontend/Dockerfile.playwright b/frontend/Dockerfile.playwright index 2ec05a03305..6c1439cc69b 100644 --- a/frontend/Dockerfile.playwright +++ b/frontend/Dockerfile.playwright @@ -4,10 +4,17 @@ ARG PLAYWRIGHT_VERSION FROM mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-jammy -COPY package.json / +WORKDIR /frontend + +COPY package.json . # Ensure the Playwright container's pnpm cache folder exists and is writable RUN mkdir -p /.cache/node/corepack/ && chmod -R 777 /.cache/node/corepack/ # Requires `packageManager` field to be present in `frontend/package.json`. RUN corepack enable pnpm + +# DO NOT actually run `pnpm install` here. Doing so requires us to copy the the source into the container. +# However, that's a waste of time because we mount the source in the compose file anyway. +# Instead, we run `pnpm install` in the entrypoint script defined in the compose file. +# ENTRYPOINT ["pnpm", "install", "&&"] diff --git a/frontend/docker-compose.playwright.yml b/frontend/docker-compose.playwright.yml index 7de7814a1b6..f89e81ec20a 100644 --- a/frontend/docker-compose.playwright.yml +++ b/frontend/docker-compose.playwright.yml @@ -6,12 +6,15 @@ services: args: - PLAYWRIGHT_VERSION=${PLAYWRIGHT_VERSION} volumes: - - ../node_modules:/node_modules - - .:/frontend - - ${PWD}/../tsconfig.base.json:/tsconfig.base.json + - ../node_modules:/node_modules:rw,Z + - .:/frontend:rw,Z user: ${USER_ID} working_dir: /frontend - command: pnpm ${TEST_COMMAND} ${PLAYWRIGHT_ARGS:-} + entrypoint: > + /bin/sh -c ' + pnpm install; + pnpm ${TEST_COMMAND} ${PLAYWRIGHT_ARGS:-}; + ' environment: # This makes the webserver that Playwright runs show the build - DEBUG=pw:webserver diff --git a/frontend/jest.config.ts b/frontend/jest.config.ts deleted file mode 100644 index f1be2c90c6d..00000000000 --- a/frontend/jest.config.ts +++ /dev/null @@ -1,38 +0,0 @@ -import type { Config } from "jest" - -const config: Config = { - globals: { - "vue-jest": { - experimentalCSSCompile: false, - templateCompiler: { - prettify: false, - }, - }, - }, - testEnvironment: "jsdom", - moduleFileExtensions: ["ts", "js", "vue", "json"], - moduleNameMapper: { - "^@/(.*)$": "/src/$1", - "^~/(.*)$": "/src/$1", - "^~~/(.*)$": "/$1", - "^@nuxtjs/composition-api$": - "/node_modules/@nuxtjs/composition-api/dist/runtime/index.js", - }, - setupFiles: ["/test/unit/setup.js"], - setupFilesAfterEnv: ["/test/unit/setup-after-env.js"], - transform: { - "^.+\\.(j|t)s$": "babel-jest", - ".*\\.(vue)$": "@vue/vue2-jest", - ".+\\.(css|png|jpg|ttf|woff|woff2)$": "jest-transform-stub", - }, - transformIgnorePatterns: ["/node_modules/(?!@nuxtjs/composition-api)"], - testPathIgnorePatterns: ["/playwright/", "/storybook/", ".remake"], - collectCoverage: false, - coverageDirectory: "/test/unit/coverage", - collectCoverageFrom: [ - "/src/**/*.vue", - "/src/**/*.js", - "/src/**/*.ts", - ], -} -export default config diff --git a/frontend/justfile b/frontend/justfile index bd8868ad7f0..eeb402bb24f 100644 --- a/frontend/justfile +++ b/frontend/justfile @@ -35,6 +35,7 @@ export FRONTEND_PNPM_VERSION := `grep 'packageManager' ../package.json | awk -F' # Build the frontend Docker image build-img tag="openverse-frontend:local" target="app": docker build \ + --load \ --target {{ target }} \ --build-context 'repo_root=..' \ --build-arg FRONTEND_NODE_VERSION=$(just node-version) \ diff --git a/frontend/nuxt-template-overrides/App.js b/frontend/nuxt-template-overrides/App.js deleted file mode 100644 index 132c5fbc144..00000000000 --- a/frontend/nuxt-template-overrides/App.js +++ /dev/null @@ -1,356 +0,0 @@ -import Vue from 'vue' -import { decode, parsePath, withoutBase, withoutTrailingSlash, normalizeURL } from 'ufo' -<% utilsImports = [ - ...(features.asyncData || features.fetch) ? [ - 'getMatchedComponentsInstances', - 'getChildrenComponentInstancesUsingFetch', - 'promisify', - 'globalHandleError', - 'urlJoin' - ] : [], - ...features.layouts ? [ - 'sanitizeComponent' - ]: [] -] %> -<% if (utilsImports.length) { %>import { <%= utilsImports.join(', ') %> } from './utils'<% } %> -<% css.forEach((c) => { %> -import '<%= relativeToBuild(resolvePath(c.src || c, { isStyle: true })) %>' -<% }) %> -import NuxtError from '<%= components.ErrorPage ? components.ErrorPage : "./components/nuxt-error.vue" %>' -<% if (loading) { %>import NuxtLoading from '<%= (typeof loading === "string" ? loading : "./components/nuxt-loading.vue") %>'<% } %> -<% if (buildIndicator) { %>import NuxtBuildIndicator from './components/nuxt-build-indicator'<% } %> - -<% if (features.layouts) { %> -<%= Object.keys(layouts).map((key) => { - if (splitChunks.layouts) { - return `const _${hash(key)} = () => import('${layouts[key]}' /* webpackChunkName: "${wChunk('layouts/' + key)}" */).then(m => sanitizeComponent(m.default || m))` - } else { - return `import _${hash(key)} from '${layouts[key]}'` - } -}).join('\n') %> - -<% if (splitChunks.layouts) { %> -let resolvedLayouts = {} -const layouts = { <%= Object.keys(layouts).map(key => `"_${key}": _${hash(key)}`).join(',') %> }<%= isTest ? '// eslint-disable-line' : '' %> -<% } else { %> -const layouts = { <%= Object.keys(layouts).map(key => `"_${key}": sanitizeComponent(_${hash(key)})`).join(',') %> }<%= isTest ? '// eslint-disable-line' : '' %> -<% } %> - -<% } %> - -export default { - render (h, props) { - <% if (loading) { %>const loadingEl = h('NuxtLoading', { ref: 'loading' })<% } %> - <% if (features.layouts) { %> - const layoutEl = h(this.layout || 'nuxt') - const templateEl = h('div', { - domProps: { - id: '__layout' - }, - key: this.layoutName - }, [layoutEl]) - <% } else { %> - const templateEl = h('nuxt') - <% } %> - - <% if (features.transitions) { %> - const transitionEl = h('transition', { - props: { - name: '<%= layoutTransition.name %>', - mode: '<%= layoutTransition.mode %>' - }, - on: { - beforeEnter (el) { - // Ensure to trigger scroll event after calling scrollBehavior - window.<%= globals.nuxt %>.$nextTick(() => { - window.<%= globals.nuxt %>.$emit('triggerScroll') - }) - } - } - }, [templateEl]) - <% } %> - - return h('div', { - domProps: { - id: '<%= globals.id %>' - } - }, [ - <% if (loading) { %>loadingEl, <% } %> - <% if (buildIndicator) { %>h(NuxtBuildIndicator), <% } %> - <% if (features.transitions) { %>transitionEl<% } else { %>templateEl<% } %> - ]) - }, - <% if (features.clientOnline || features.layouts) { %> - data: () => ({ - <% if (features.clientOnline) { %> - isOnline: true, - <% } %> - <% if (features.layouts) { %> - layout: null, - layoutName: '', - <% } %> - <% if (features.fetch) { %> - nbFetching: 0 - <% } %> - }), - <% } %> - beforeCreate () { - Vue.util.defineReactive(this, 'nuxt', this.$options.nuxt) - }, - created () { - // Add this.$nuxt in child instances - this.$root.$options.<%= globals.nuxt %> = this - - if (process.client) { - // add to window so we can listen when ready - window.<%= globals.nuxt %> = <%= (globals.nuxt !== '$nuxt' ? 'window.$nuxt = ' : '') %>this - <% if (features.clientOnline) { %> - this.refreshOnlineStatus() - // Setup the listeners - window.addEventListener('online', this.refreshOnlineStatus) - window.addEventListener('offline', this.refreshOnlineStatus) - <% } %> - } - // Add $nuxt.error() - this.error = this.nuxt.error - // Add $nuxt.context - this.context = this.$options.context - }, - <% if (loading || isFullStatic) { %> - async mounted () { - <% if (loading) { %>this.$loading = this.$refs.loading<% } %> - <% if (isFullStatic) {%> - if (this.isPreview) { - if (this.$store && this.$store._actions.nuxtServerInit) { - <% if (loading) { %>this.$loading.start()<% } %> - await this.$store.dispatch('nuxtServerInit', this.context) - } - await this.refresh() - <% if (loading) { %>this.$loading.finish()<% } %> - } - <% } %> - }, - <% } %> - watch: { - 'nuxt.err': 'errorChanged' - }, - <% if (features.clientOnline) { %> - computed: { - isOffline () { - return !this.isOnline - }, - <% if (features.fetch) { %> - isFetching () { - return this.nbFetching > 0 - },<% } %> - <% if (nuxtOptions.target === 'static') { %> - isPreview () { - return Boolean(this.$options.previewData) - },<% } %> - }, - <% } %> - methods: { - <%= isTest ? '/* eslint-disable comma-dangle */' : '' %> - <% if (features.clientOnline) { %> - refreshOnlineStatus () { - if (process.client) { - if (typeof window.navigator.onLine === 'undefined') { - // If the browser doesn't support connection status reports - // assume that we are online because most apps' only react - // when they now that the connection has been interrupted - this.isOnline = true - } else { - this.isOnline = window.navigator.onLine - } - } - }, - <% } %> - async refresh () { - <% if (features.asyncData || features.fetch) { %> - const pages = getMatchedComponentsInstances(this.$route) - - if (!pages.length) { - return - } - <% if (loading) { %>this.$loading.start()<% } %> - - const promises = pages.map(async (page) => { - let p = [] - - <% if (features.fetch) { %> - // Old fetch - if (page.$options.fetch && page.$options.fetch.length) { - p.push(promisify(page.$options.fetch, this.context)) - } - <% } %> - - <% if (features.asyncData) { %> - if (page.$options.asyncData) { - p.push( - promisify(page.$options.asyncData, this.context) - .then((newData) => { - for (const key in newData) { - Vue.set(page.$data, key, newData[key]) - } - }) - ) - } - <% } %> - - <% if (features.fetch) { %> - // Wait for asyncData & old fetch to finish - await Promise.all(p) - // Cleanup refs - p = [] - - if (page.$fetch) { - p.push(page.$fetch()) - } - // Get all component instance to call $fetch - for (const component of getChildrenComponentInstancesUsingFetch(page.$vnode.componentInstance)) { - p.push(component.$fetch()) - } - <% } %> - - return Promise.all(p) - }) - try { - await Promise.all(promises) - } catch (error) { - <% if (loading) { %>this.$loading.fail(error)<% } %> - globalHandleError(error) - this.error(error) - } - <% if (loading) { %>this.$loading.finish()<% } %> - <% } %> - }, - <% if (splitChunks.layouts) { %>async <% } %>errorChanged () { - if (this.nuxt.err) { - <% if (loading) { %> - if (this.$loading) { - if (this.$loading.fail) { - this.$loading.fail(this.nuxt.err) - } - if (this.$loading.finish) { - this.$loading.finish() - } - } - <% } %> - let errorLayout = (NuxtError.options || NuxtError).layout; - - if (typeof errorLayout === 'function') { - errorLayout = errorLayout(this.context) - } - <% if (splitChunks.layouts) { %> - await this.loadLayout(errorLayout) - <% } %> - this.setLayout(errorLayout) - } - }, - <% if (features.layouts) { %> - <% if (splitChunks.layouts) { %> - setLayout (layout) { - <% if (debug) { %> - if(layout && typeof layout !== 'string') { - throw new Error('[nuxt] Avoid using non-string value as layout property.') - } - <% } %> - if (!layout || !resolvedLayouts['_' + layout]) { - layout = 'default' - } - this.layoutName = layout - let _layout = '_' + layout - this.layout = resolvedLayouts[_layout] - return this.layout - }, - loadLayout (layout) { - const undef = !layout - const nonexistent = !(layouts['_' + layout] || resolvedLayouts['_' + layout]) - let _layout = '_' + ((undef || nonexistent) ? 'default' : layout) - if (resolvedLayouts[_layout]) { - return Promise.resolve(resolvedLayouts[_layout]) - } - return layouts[_layout]() - .then((Component) => { - resolvedLayouts[_layout] = Component - delete layouts[_layout] - return resolvedLayouts[_layout] - }) - .catch((e) => { - if (this.<%= globals.nuxt %>) { - return this.<%= globals.nuxt %>.error({ statusCode: 500, message: e.message }) - } - }) - }, - <% } else { %> - setLayout (layout) { - <% if (debug) { %> - if(layout && typeof layout !== 'string') { - throw new Error('[nuxt] Avoid using non-string value as layout property.') - } - <% } %> - if (!layout || !layouts['_' + layout]) { - layout = 'default' - } - this.layoutName = layout - this.layout = layouts['_' + layout] - return this.layout - }, - loadLayout (layout) { - if (!layout || !layouts['_' + layout]) { - layout = 'default' - } - return Promise.resolve(layouts['_' + layout]) - }, - <% } /* splitChunks.layouts */ %> - <% } /* features.layouts */ %> - <% if (isFullStatic) { %> - getRouterBase() { - return withoutTrailingSlash(this.$router.options.base) - }, - getRoutePath(route = '/') { - const base = this.getRouterBase() - return withoutTrailingSlash(withoutBase(parsePath(route).pathname, base)) - }, - getStaticAssetsPath(route = '/') { - const { staticAssetsBase } = window.<%= globals.context %> - - return urlJoin(staticAssetsBase, this.getRoutePath(route)) - }, - <% if (nuxtOptions.generate.manifest) { %> - async fetchStaticManifest() { - return window.__NUXT_IMPORT__('manifest.js', normalizeURL(urlJoin(this.getStaticAssetsPath(), 'manifest.js'))) - }, - <% } %> - setPagePayload(payload) { - this._pagePayload = payload - this._fetchCounters = {} - }, - async fetchPayload(route, prefetch) { - const path = decode(this.getRoutePath(route)) - <% if (nuxtOptions.generate.manifest) { %> - const manifest = await this.fetchStaticManifest() - if (!manifest.routes.includes(path)) { - if (!prefetch) { this.setPagePayload(false) } - throw new Error(`Route ${path} is not pre-rendered`) - } - <% } %> - const src = urlJoin(this.getStaticAssetsPath(route), 'payload.js') - try { - const payload = await window.__NUXT_IMPORT__(path, normalizeURL(src)) - if (!prefetch) { this.setPagePayload(payload) } - return payload - } catch (err) { - if (!prefetch) { this.setPagePayload(false) } - throw err - } - } - <% } %> - }, - <% if (loading) { %> - components: { - NuxtLoading - } - <% } %> - <%= isTest ? '/* eslint-enable comma-dangle */' : '' %> -} diff --git a/frontend/nuxt-template-overrides/README.md b/frontend/nuxt-template-overrides/README.md deleted file mode 100644 index d3a0ad0ad4d..00000000000 --- a/frontend/nuxt-template-overrides/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# Nuxt vue-app template overrides - -Due to a CSS ordering bug that we haven't been able to find any other solutions for, we've had to override the Nuxt templates for the `App.js` and `index.js` files to prevent any Vue components from being imported before the static CSS assets in the `nuxt.config.ts`. - -## Described changes - -There are two basic changes we're making all with a single goal: stop the importing of our custom `error.vue` component coming before our static CSS assets. The generated page apparently depends on this specific ordering of file imports, which is far from ideal. Alas, there doesn't appear to be another clear solution to this problem at the moment, so we're stuck with this hack. - -To update these files, it's probably easiest to just copy `App.js` and `index.js` from `node_modules/@nuxt/vue-app/template` into this directory and then apply the following transformations. - -In `index.js`, move the import of `NuxtError` and `Nuxt` below importing `App`. `NuxtError` imports our custom error component and `Nuxt` imports `NuxtError`. - -In `App.js`, move the `css.forEach` loop above importing `NuxtError`. diff --git a/frontend/nuxt-template-overrides/index.js b/frontend/nuxt-template-overrides/index.js deleted file mode 100644 index 088509c72d4..00000000000 --- a/frontend/nuxt-template-overrides/index.js +++ /dev/null @@ -1,306 +0,0 @@ -import Vue from 'vue' -<% if (store) { %>import Vuex from 'vuex'<% } %> -<% if (features.meta) { %>import Meta from 'vue-meta'<% } %> -<% if (features.componentClientOnly) { %>import ClientOnly from 'vue-client-only'<% } %> -<% if (features.deprecations) { %>import NoSsr from 'vue-no-ssr'<% } %> -import { createRouter } from './router.js' -import NuxtChild from './components/nuxt-child.js' -import App from '<%= appPath %>' -import Nuxt from './components/nuxt.js' -import NuxtError from '<%= components.ErrorPage ? components.ErrorPage : "./components/nuxt-error.vue" %>' -import { setContext, getLocation, getRouteData, normalizeError } from './utils' -<% if (store) { %>import { createStore } from './store.js'<% } %> - -/* Plugins */ -<%= isTest ? '/* eslint-disable camelcase */' : '' %> -<% plugins.forEach((plugin) => { %>import <%= plugin.name %> from '<%= plugin.name %>' // Source: <%= relativeToBuild(plugin.src) %> (mode: '<%= plugin.mode %>') -<% }) %> -<%= isTest ? '/* eslint-enable camelcase */' : '' %> - -<% if (features.componentClientOnly) { %> -// Component: -Vue.component(ClientOnly.name, ClientOnly) -<% } %> -<% if (features.deprecations) { %> -// TODO: Remove in Nuxt 3: -Vue.component(NoSsr.name, { - ...NoSsr, - render (h, ctx) { - if (process.client && !NoSsr._warned) { - NoSsr._warned = true - <%= isTest ? '// eslint-disable-next-line no-console' : '' %> - console.warn(' has been deprecated and will be removed in Nuxt 3, please use instead') - } - return NoSsr.render(h, ctx) - } -}) -<% } %> -// Component: -Vue.component(NuxtChild.name, NuxtChild) -<% if (features.componentAliases) { %>Vue.component('NChild', NuxtChild)<% } %> - -// Component NuxtLink is imported in server.js or client.js - -// Component: -Vue.component(Nuxt.name, Nuxt) - -Object.defineProperty(Vue.prototype, '<%= globals.nuxt %>', { - get() { - const globalNuxt = this.$root ? this.$root.$options.<%= globals.nuxt %> : null - if (process.client && !globalNuxt && typeof window !== 'undefined') { - return window.<%= globals.nuxt %> - } - return globalNuxt - }, - configurable: true -}) - -<% if (features.meta) { -// vue-meta configuration -const vueMetaOptions = { - ...nuxtOptions.vueMeta, - keyName: 'head', // the component option name that vue-meta looks for meta info on. - attribute: 'data-n-head', // the attribute name vue-meta adds to the tags it observes - ssrAttribute: 'data-n-head-ssr', // the attribute name that lets vue-meta know that meta info has already been server-rendered - tagIDKeyName: 'hid' // the property name that vue-meta uses to determine whether to overwrite or append a tag -} -%> -Vue.use(Meta, <%= JSON.stringify(vueMetaOptions) %>)<%= isTest ? '// eslint-disable-line' : '' %> -<% } %> - -<% if (features.transitions) { %> -const defaultTransition = <%= - serialize(pageTransition) - .replace('beforeEnter(', 'function(').replace('enter(', 'function(').replace('afterEnter(', 'function(') - .replace('enterCancelled(', 'function(').replace('beforeLeave(', 'function(').replace('leave(', 'function(') - .replace('afterLeave(', 'function(').replace('leaveCancelled(', 'function(').replace('beforeAppear(', 'function(') - .replace('appear(', 'function(').replace('afterAppear(', 'function(').replace('appearCancelled(', 'function(') -%><%= isTest ? '// eslint-disable-line' : '' %> -<% } %> - -<% if (store) { %> -const originalRegisterModule = Vuex.Store.prototype.registerModule - -function registerModule (path, rawModule, options = {}) { - const preserveState = process.client && ( - Array.isArray(path) - ? !!path.reduce((namespacedState, path) => namespacedState && namespacedState[path], this.state) - : path in this.state - ) - return originalRegisterModule.call(this, path, rawModule, { preserveState, ...options }) -} -<% } %> - -async function createApp(ssrContext, config = {}) { - const store = <%= store ? 'createStore(ssrContext)' : 'null' %> - const router = await createRouter(ssrContext, config, { store }) - - <% if (store) { %> - // Add this.$router into store actions/mutations - store.$router = router - <% if (mode === 'universal') { %> - // Fix SSR caveat https://github.com/nuxt/nuxt.js/issues/3757#issuecomment-414689141 - store.registerModule = registerModule - <% } %> - <% } %> - - // Create Root instance - - // here we inject the router and store to all child components, - // making them available everywhere as `this.$router` and `this.$store`. - const app = { - <% if (features.meta) { %> - <%= isTest ? '/* eslint-disable array-bracket-spacing, quotes, quote-props, semi, indent, comma-spacing, key-spacing, object-curly-spacing, space-before-function-paren, object-shorthand */' : '' %> - head: <%= serializeFunction(head) %>, - <%= isTest ? '/* eslint-enable array-bracket-spacing, quotes, quote-props, semi, indent, comma-spacing, key-spacing, object-curly-spacing, space-before-function-paren, object-shorthand */' : '' %> - <% } %> - <% if (store) { %>store,<% } %> - router, - nuxt: { - <% if (features.transitions) { %> - defaultTransition, - transitions: [defaultTransition], - setTransitions (transitions) { - if (!Array.isArray(transitions)) { - transitions = [transitions] - } - transitions = transitions.map((transition) => { - if (!transition) { - transition = defaultTransition - } else if (typeof transition === 'string') { - transition = Object.assign({}, defaultTransition, { name: transition }) - } else { - transition = Object.assign({}, defaultTransition, transition) - } - return transition - }) - this.$options.nuxt.transitions = transitions - return transitions - }, - <% } %> - err: null, - dateErr: null, - error (err) { - err = err || null - app.context._errored = Boolean(err) - err = err ? normalizeError(err) : null - let nuxt = app.nuxt // to work with @vue/composition-api, see https://github.com/nuxt/nuxt.js/issues/6517#issuecomment-573280207 - if (this) { - nuxt = this.nuxt || this.$options.nuxt - } - nuxt.dateErr = Date.now() - nuxt.err = err - // Used in src/server.js - if (ssrContext) { - ssrContext.nuxt.error = err - } - return err - } - }, - ...App - } - <% if (store) { %> - // Make app available into store via this.app - store.app = app - <% } %> - const next = ssrContext ? ssrContext.next : location => app.router.push(location) - // Resolve route - let route - if (ssrContext) { - route = router.resolve(ssrContext.url).route - } else { - const path = getLocation(router.options.base, router.options.mode) - route = router.resolve(path).route - } - - // Set context to app.context - await setContext(app, { - <% if (store) { %>store,<% } %> - route, - next, - error: app.nuxt.error.bind(app), - payload: ssrContext ? ssrContext.payload : undefined, - req: ssrContext ? ssrContext.req : undefined, - res: ssrContext ? ssrContext.res : undefined, - beforeRenderFns: ssrContext ? ssrContext.beforeRenderFns : undefined, - beforeSerializeFns: ssrContext ? ssrContext.beforeSerializeFns : undefined, - ssrContext - }) - - function inject(key, value) { - if (!key) { - throw new Error('inject(key, value) has no key provided') - } - if (value === undefined) { - throw new Error(`inject('${key}', value) has no value provided`) - } - - key = '$' + key - // Add into app - app[key] = value - // Add into context - if (!app.context[key]) { - app.context[key] = value - } - <% if (store) { %> - // Add into store - store[key] = app[key] - <% } %> - // Check if plugin not already installed - const installKey = '__<%= globals.pluginPrefix %>_' + key + '_installed__' - if (Vue[installKey]) { - return - } - Vue[installKey] = true - // Call Vue.use() to install the plugin into vm - Vue.use(() => { - if (!Object.prototype.hasOwnProperty.call(Vue.prototype, key)) { - Object.defineProperty(Vue.prototype, key, { - get () { - return this.$root.$options[key] - } - }) - } - }) - } - - // Inject runtime config as $config - inject('config', config) - - <% if (store) { %> - if (process.client) { - // Replace store state before plugins execution - if (window.<%= globals.context %> && window.<%= globals.context %>.state) { - store.replaceState(window.<%= globals.context %>.state) - } - } - <% } %> - - // Add enablePreview(previewData = {}) in context for plugins - if (process.static && process.client) { - app.context.enablePreview = function (previewData = {}) { - app.previewData = Object.assign({}, previewData) - inject('preview', previewData) - } - } - // Plugin execution - <%= isTest ? '/* eslint-disable camelcase */' : '' %> - <% plugins.forEach((plugin) => { %> - <% if (plugin.mode == 'client') { %> - if (process.client && typeof <%= plugin.name %> === 'function') { - await <%= plugin.name %>(app.context, inject) - } - <% } else if (plugin.mode == 'server') { %> - if (process.server && typeof <%= plugin.name %> === 'function') { - await <%= plugin.name %>(app.context, inject) - } - <% } else { %> - if (typeof <%= plugin.name %> === 'function') { - await <%= plugin.name %>(app.context, inject) - } - <% } %> - <% }) %> - <%= isTest ? '/* eslint-enable camelcase */' : '' %> - // Lock enablePreview in context - if (process.static && process.client) { - app.context.enablePreview = function () { - console.warn('You cannot call enablePreview() outside a plugin.') - } - } - - // Wait for async component to be resolved first - await new Promise((resolve, reject) => { - // Ignore 404s rather than blindly replacing URL in browser - if (process.client) { - const { route } = router.resolve(app.context.route.fullPath) - if (!route.matched.length) { - return resolve() - } - } - router.replace(app.context.route.fullPath, resolve, (err) => { - // https://github.com/vuejs/vue-router/blob/v3.4.3/src/util/errors.js - if (!err._isRouter) return reject(err) - if (err.type !== 2 /* NavigationFailureType.redirected */) return resolve() - - // navigated to a different route in router guard - const unregister = router.afterEach(async (to, from) => { - if (process.server && ssrContext && ssrContext.url) { - ssrContext.url = to.fullPath - } - app.context.route = await getRouteData(to) - app.context.params = to.params || {} - app.context.query = to.query || {} - unregister() - resolve() - }) - }) - }) - - return { - <% if(store) { %>store,<% } %> - app, - router - } -} - -export { createApp, NuxtError } diff --git a/frontend/nuxt.config.ts b/frontend/nuxt.config.ts index 75a87f94a32..50fa9d59ee9 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -1,29 +1,11 @@ -import path from "path" -import fs from "fs" +import { defineNuxtConfig } from "nuxt/config" -import pkg from "./package.json" -import locales from "./src/locales/scripts/valid-locales.json" - -import { meta } from "./src/constants/meta" -import { VIEWPORTS } from "./src/constants/screens" +import { LOCAL, PRODUCTION } from "./src/constants/deploy-env" -import { isProd } from "./src/utils/node-env" -import { sentryConfig } from "./src/utils/sentry-config" -import { env } from "./src/utils/env" - -import type http from "http" +import locales from "./src/locales/scripts/valid-locales.json" +import { meta as commonMeta } from "./src/constants/meta" -import type { NuxtConfig } from "@nuxt/types" import type { LocaleObject } from "@nuxtjs/i18n" -import type { IncomingMessage, NextFunction } from "connect" - -if (process.env.NODE_ENV === "production") { - meta.push({ - // @ts-expect-error: 'http-equiv' isn't allowed here by Nuxt - "http-equiv": "Content-Security-Policy", - content: "upgrade-insecure-requests", - }) -} const favicons = [ // SVG favicon @@ -48,49 +30,44 @@ const favicons = [ }, ] -// Default html head -const head = { - title: "Openly Licensed Images, Audio and More | Openverse", - meta, - link: [ - ...favicons, - { - rel: "preconnect", - href: env.apiUrl, - crossorigin: "", - }, - { - rel: "dns-prefetch", - href: env.apiUrl, - }, - { - rel: "search", - type: "application/opensearchdescription+xml", - title: "Openverse", - href: "/opensearch.xml", - }, +const disallowedBots = [ + "GPTBot", + "CCBot", + "ChatGPT-User", + "Google-Extended", + "anthropic-ai", + "Omgilibot", + "Omgili", + "FacebookBot", + "Diffbot", + "Bytespider", + "ImagesiftBot", + "cohere-ai", +] + +/** + * Robots.txt rules are configured here via the \@nuxtjs/robots package. + * @see {@link https://nuxtseo.com/robots/guides/nuxt-config|Robots Config Rules} + */ +const robots = { + userAgent: "*", + disallow: ["/search", "/search/audio", "/search/image"], + groups: [ + ...disallowedBots.map((bot) => ({ + userAgent: [bot], + disallow: ["/"], // block bots from all routes + })), ], } -const baseProdName = process.env.CI ? "[name]" : "[contenthash:7]" +const isProductionBuild = import.meta.env.NODE_ENV === "production" +const isPlaywright = import.meta.env.PW === "true" +const isProdNotPlaywright = isProductionBuild && !isPlaywright +const isTest = import.meta.env.TEST === "true" +const deploymentEnv = import.meta.env.DEPLOYMENT_ENV || LOCAL -const filenames: NonNullable["filenames"] = { - app: ({ isDev, isModern }) => - isDev - ? `[name]${isModern ? ".modern" : ""}.js` - : `${baseProdName}${isModern ? ".modern" : ""}.js`, - chunk: ({ isDev, isModern }) => - isDev - ? `[name]${isModern ? ".modern" : ""}.js` - : `${baseProdName}${isModern ? ".modern" : ""}.js`, - css: ({ isDev }) => (isDev ? "[name].css" : `css/${baseProdName}.css`), - img: ({ isDev }) => - isDev ? "[path][name].[ext]" : `img/${baseProdName}.[ext]`, - font: ({ isDev }) => - isDev ? "[path][name].[ext]" : `fonts/${baseProdName}.[ext]`, - video: ({ isDev }) => - isDev ? "[path][name].[ext]" : `videos/${baseProdName}.[ext]`, -} +const apiUrl = + import.meta.env.NUXT_PUBLIC_API_URL || "https://api.openverse.org/" const openverseLocales = [ { @@ -106,83 +83,96 @@ const openverseLocales = [ name: "English", nativeName: "English", }, - ...(locales ?? []), + ...locales, ].filter((l) => Boolean(l.iso)) as LocaleObject[] -const port = process.env.PORT || 8443 -const isProdNotPlaywright = isProd && !(process.env.PW === "true") - -const config: NuxtConfig = { - // eslint-disable-next-line no-undef - version: pkg.version, // used to purge cache :) - cache: { - pages: ["/"], - store: { - type: "memory", // 'redis' would be nice - max: 100, - ttl: process.env.MICROCACHE_DURATION || 60, +export default defineNuxtConfig({ + app: { + head: { + title: "Openly Licensed Images, Audio and More | Openverse", + meta: commonMeta, + link: [ + ...favicons, + { + rel: "search", + type: "application/opensearchdescription+xml", + title: "Openverse", + href: "/opensearch.xml", + }, + { + rel: "dns-prefetch", + href: apiUrl, + }, + { + rel: "preconnect", + href: apiUrl, + crossorigin: "", + }, + ], }, }, srcDir: "src/", - modern: "client", - server: { - port, - https: process.env.LOCAL_SSL - ? { - key: fs.readFileSync(path.resolve(__dirname, "localhost+1-key.pem")), - cert: fs.readFileSync(path.resolve(__dirname, "localhost+1.pem")), - } - : undefined, + serverDir: "server/", + devServer: { + port: 8443, + host: "0.0.0.0", + }, + imports: { + autoImport: false, + }, + compatibilityDate: "2024-07-23", + css: ["~/assets/fonts.css", "~/styles/accent.css"], + runtimeConfig: { + apiClientId: "", + apiClientSecret: "", + public: { + // These values can be overridden by the NUXT_PUBLIC_* env variables + deploymentEnv, + apiUrl, + providerUpdateFrequency: 3600000, + savedSearchCount: 4, + sentry: { + dsn: "", + environment: deploymentEnv, + release: import.meta.env.SEMANTIC_VERSION, + }, + isPlaywright, + }, }, - router: { - middleware: "feature-flags", + site: { + indexable: deploymentEnv === PRODUCTION, + trailingSlash: false, }, - plugins: [ - "~/plugins/ua-parse.ts", - "~/plugins/focus-visible.client.ts", - "~/plugins/api-token.server.ts", - "~/plugins/polyfills.client.ts", - "~/plugins/sentry.ts", - "~/plugins/analytics.ts", - "~/plugins/errors.ts", - "~/plugins/init-stores.ts", - ], - css: ["~/assets/fonts.css", "~/styles/tailwind.css", "~/styles/accent.css"], - head, - env, // TODO: Replace with `publicRuntimeConfig` - privateRuntimeConfig: { - apiClientId: process.env.API_CLIENT_ID, - apiClientSecret: process.env.API_CLIENT_SECRET, + /** + * Disable debug mode to prevent excessive timing logs. + */ + debug: false, + experimental: { + /** + * Improve router performance, see https://nuxt.com/blog/v3-10#%EF%B8%8F-build-time-route-metadata + */ + scanPageMeta: true, }, - dev: !isProd, - buildModules: [ - "@nuxt/typescript-build", - "@nuxtjs/composition-api/module", - "@nuxtjs/svg-sprite", - "@pinia/nuxt", - ], modules: [ - "portal-vue/nuxt", + "@pinia/nuxt", "@nuxtjs/i18n", - "@nuxtjs/proxy", - "@nuxtjs/redirect-module", - "@nuxtjs/sentry", - "cookie-universal-nuxt", - "vue-plausible", - "~/modules/prometheus.ts", - // Sitemap must be last to ensure that even routes created by other modules are added + "@nuxtjs/tailwindcss", + "@nuxtjs/plausible", + "@nuxt/test-utils/module", "@nuxtjs/sitemap", + "@nuxtjs/robots", ], - serverMiddleware: [ - { path: "/healthcheck", handler: "~/server-middleware/healthcheck.js" }, - { path: "/robots.txt", handler: "~/server-middleware/robots.js" }, - ], - svgSprite: { - input: "~/assets/svg/raw", - output: "~/assets/svg/sprite", + routeRules: { + "/photos/**": { redirect: { to: "/image/**", statusCode: 301 } }, + "/meta-search": { redirect: { to: "/about", statusCode: 301 } }, + "/external-sources": { redirect: { to: "/about", statusCode: 301 } }, + }, + robots, + tailwindcss: { + cssPath: "~/styles/tailwind.css", }, i18n: { - baseUrl: "https://openverse.org", + baseUrl: import.meta.env.SITE_URL, locales: openverseLocales, lazy: true, langDir: "locales", @@ -191,160 +181,20 @@ const config: NuxtConfig = { * `detectBrowserLanguage` must be false to prevent nuxt/i18n from automatically * setting the locale based on headers or the client-side `navigator` object. * - * Such detection is handled at the parent level in WP.org. - * * More info about the Nuxt i18n: * - * - [detectBrowserLanguage](https://i18n.nuxtjs.org/options-reference/#detectbrowserlanguage) - * - [Browser language detection info](https://i18n.nuxtjs.org/browser-language-detection) + * - [Browser language detection info](https://i18n.nuxtjs.org/docs/guide/browser-language-detection) * */ detectBrowserLanguage: false, - vueI18n: "~/plugins/vue-i18n", - }, - sitemap: { - hostname: "https://openverse.org", - i18n: { - locales: openverseLocales.map((l) => l.iso), - routesNameSeparator: "___", - }, - }, - /** - * Map the old route for /photos/_id page to /image/_id permanently to keep links working. - * See the redirect module for more info. - * {@link https://github.com/nuxt-community/redirect-module#usage} - */ - redirect: { - rules: [ - { from: "^/photos/(.*)$", to: "/image/$1", statusCode: 301 }, - { from: "/meta-search", to: "/about", statusCode: 301 }, - { from: "/external-sources", to: "/about", statusCode: 301 }, - ], - // If the URL cannot be decoded, we call next() to show the client-side error page. - onDecodeError: ( - _error: Error, - _req: IncomingMessage, - _res: http.ServerResponse, - next: NextFunction - ) => { - return next() - }, - }, - sentry: sentryConfig, - build: { - templates: [ - { - src: "./nuxt-template-overrides/App.js", - dst: "App.js", - }, - { - src: "./nuxt-template-overrides/index.js", - dst: "index.js", - }, - ], - filenames, - friendlyErrors: false, - postcss: { - postcssOptions: { - preset: { - features: { - // Disable conversion of logical properties to physical properties - // e.g.: `margin-inline-start` is NOT converted to `margin-left` - // Necessary for RTL support. - "logical-properties-and-values": false, - }, - }, - plugins: { - tailwindcss: { - config: path.resolve(__dirname, "tailwind.config.ts"), - }, - "postcss-focus-visible": {}, - }, - }, - }, - extend(config, ctx) { - // Enables use of IDE debuggers - config.devtool = ctx.isClient ? "source-map" : "inline-source-map" - }, - transpile: [({ isLegacy }) => (isLegacy ? "axios" : undefined)], - }, - typescript: { - typeCheck: { - typescript: { - configFile: "./tsconfig.json", - extensions: { - vue: true, - }, - }, - }, - }, - storybook: { - port: 6006, - addons: [ - { - name: "@storybook/addon-essentials", - options: { - backgrounds: true, - viewport: true, - toolbars: true, - }, - }, - ], - parameters: { - backgrounds: { - default: "White", - values: [ - { name: "White", value: "#ffffff" }, - { name: "Dark charcoal", value: "#30272e" }, - ], - }, - options: { - storySort: { - order: ["Introduction", ["Openverse UI"], "Meta"], - }, - }, - viewport: { - viewports: VIEWPORTS, - }, - }, + trailingSlash: false, + vueI18n: "./src/vue-i18n", }, plausible: { + enabled: !isTest, + logIgnoredEvents: !isProductionBuild, trackLocalhost: !isProdNotPlaywright, + autoPageviews: isProdNotPlaywright, + domain: import.meta.env.SITE_DOMAIN, + apiHost: import.meta.env.PLAUSIBLE_SITE_URL, }, - publicRuntimeConfig: { - deploymentEnv: process.env.DEPLOYMENT_ENV ?? "local", - plausible: { - // This is the current domain of the site. - domain: - process.env.SITE_DOMAIN ?? - (isProdNotPlaywright ? "openverse.org" : "localhost"), - apiHost: - process.env.SITE_DOMAIN ?? - (isProdNotPlaywright - ? "https://openverse.org" - : /** - * We rely on the Nginx container running as `frontend_nginx` - * in the local compose stack to proxy requests. Therefore, the - * URL here is not for the Plausible container in the local stack, - * but the Nginx service, which then itself forwards the requests - * to the local Plausible instance. - * - * In production, the Nginx container is handling all requests - * made to the root URL (openverse.org), and is configured to - * forward Plausible requests to upstream Plausible. - */ - "http://localhost:50290"), - }, - sentry: { - config: { - // We need to explicitly configure this for the frontend to have - // access to it at runtime. On the server side it would be picked - // up from the environment; the client-side doesn't have that - // luxury of a configured runtime environment, so we need to - // tell it what environment it is in. - environment: process.env.SENTRY_ENVIRONMENT, - }, - }, - }, -} - -export default config +}) diff --git a/frontend/package.json b/frontend/package.json index 5b90f98491a..144e8a1a200 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -5,26 +5,29 @@ "version": "2.2.1", "packageManager": "pnpm@9.5.0", "scripts": { - "predev": "pnpm install && pnpm i18n:no-get", + "predev": "pnpm install && pnpm i18n:en", "dev": "run-p dev:only 'i18n:en --watch'", - "dev:only": "nuxt --hostname 0.0.0.0", + "dev:only": "nuxt dev --host 0.0.0.0", "dev:secure": "LOCAL_SSL=enabled pnpm dev", - "build": "nuxt build", + "build": "NODE_ENV=production nuxt build", "build:clean": "rm -rf .nuxt", "docker:build": "docker build . -t openverse-frontend:latest", "docker:run": "docker run --rm -it -p 127.0.0.1:8443:8443/tcp openverse-frontend:latest", "generate": "nuxt generate", - "start": "nuxt start", + "start": "PORT=\"${PORT:-8443}\" CONSOLA_LEVEL=\"${CONSOLA_LEVEL:-1}\" node .output/server/index.mjs", + "start:mem": "PORT=\"${PORT:-8443}\" CONSOLA_LEVEL=\"${CONSOLA_LEVEL:-1}\" node --inspect .output/server/index.mjs", "start:playwright": "pnpm i18n:copy-test-locales && pnpm start", "prod": "pnpm build && pnpm start", "prod:playwright": "pnpm i18n:copy-test-locales && pnpm prod", - "storybook": "NODE_OPTIONS=--openssl-legacy-provider nuxt storybook --port=54000", + "storybook": "TEST=true storybook dev --port 54000", "storybook:build": "pnpm i18n:copy-test-locales && NODE_OPTIONS=--openssl-legacy-provider nuxt storybook build", "storybook:build-for-docs": "pnpm install & pnpm i18n:en && NODE_OPTIONS=--openssl-legacy-provider nuxt storybook build", "talkback": "node ./test/proxy.js", - "pretest": "pnpm install", + "prepare:nuxt": "pnpm i18n:en && npx nuxi prepare", "test": "pnpm test:unit && pnpm test:playwright", - "test:unit": "pnpm run i18n:en && jest", + "test:unit": "pnpm run i18n:en && vitest run", + "test:unit:ui": "vitest run --ui", + "test:unit:coverage": "vitest run --coverage", "test:unit:watch": "pnpm test:unit --collectCoverage=false --watch", "test:playwright": "./bin/playwright.sh", "test:playwright:local": "playwright test -c test/playwright", @@ -40,7 +43,7 @@ "types": "vue-tsc -p .", "i18n": "pnpm i18n:create-locales-list && pnpm i18n:get-translations && pnpm i18n:update-locales", "i18n:en": "pnpm i18n:get-translations --en-only", - "i18n:copy-test-locales": "cp -r test/locales/* src/locales/", + "i18n:copy-test-locales": "cp test/locales/**.json src/locales/ && mv src/locales/valid-locales.json src/locales/scripts/valid-locales.json", "i18n:no-get": "pnpm i18n:create-locales-list && pnpm i18n:update-locales", "i18n:create-locales-list": "node src/locales/scripts/create-wp-locale-list", "i18n:get-translations": "node src/locales/scripts/get-translations", @@ -56,94 +59,59 @@ "doc:media-props": "node ./scripts/document-media.js" }, "dependencies": { - "@floating-ui/dom": "^1.5.3", - "@nuxt/vue-app": "^2.17.3", - "@nuxtjs/composition-api": "^0.34.0", - "@nuxtjs/i18n": "^7.3.1", - "@nuxtjs/proxy": "^2.1.0", - "@nuxtjs/redirect-module": "^0.3.1", - "@nuxtjs/sentry": "^7.5.0", - "@nuxtjs/sitemap": "^2.4.0", - "@nuxtjs/svg-sprite": "0.5.2", - "@pinia/nuxt": "0.2.1", - "@vueuse/core": "^10.2.1", - "@wordpress/is-shallow-equal": "^4.6.0", + "@floating-ui/dom": "^1.6.8", + "@intlify/core-base": "^9.13.1", + "@intlify/message-compiler": "^9.13.1", + "@nuxtjs/plausible": "^1.0.0", + "@nuxtjs/robots": "^4.0.2", + "@nuxtjs/sitemap": "^5.3.5", + "@nuxtjs/tailwindcss": "^6.12.1", + "@pinia/nuxt": "^0.5.1", + "@sentry/node": "^8.20.0", + "@sentry/vue": "^8.20.0", + "@tailwindcss/typography": "^0.5.13", + "@vueuse/core": "^10.11.0", + "@wordpress/is-shallow-equal": "^5.3.0", "async-mutex": "^0.5.0", - "axios": "^1.0.0", - "axios-mock-adapter": "^1.20.0", + "axios": "^1.7.2", + "axios-mock-adapter": "^1.22.0", "clipboard": "^2.0.11", - "cookie-universal-nuxt": "^2.1.5", - "core-js": "^3.36.1", - "express-prom-bundle": "^6.4.1", - "express-useragent": "^1.0.15", "focus-trap": "^7.5.4", "focus-visible": "^5.2.0", - "glob": "^8.0.1", - "node-html-parser": "^6.0.0", - "nuxt": "^2.17.3", "pinia": "^2.1.7", - "portal-vue": "^2.1.7", - "postcss-focus-visible": "^9.0.0", - "prom-client": "^14.0.1", - "rfdc": "^1.3.0", + "postcss-focus-visible": "^9.0.1", + "rfdc": "^1.4.1", "seeded-rand": "^2.0.1", - "throttle-debounce": "^5.0.0", - "uuid": "^9.0.1", - "vue": "^2.7.16", - "vue-i18n": "^8.26.7", - "vue-plausible": "^1.3.2" + "throttle-debounce": "^5.0.2", + "uuid": "^10.0.0" }, "devDependencies": { - "@babel/core": "^7.24.4", - "@babel/parser": "^7.24.4", - "@babel/preset-env": "^7.24.4", - "@babel/preset-typescript": "^7.24.1", - "@itsjonq/remake": "^2.0.0", - "@nuxt/types": "^2.17.3", - "@nuxt/typescript-build": "^3.0.2", - "@nuxtjs/storybook": "^4.3.2", - "@pinia/testing": "^0.1.0", - "@playwright/test": "1.45.0", - "@tailwindcss/typography": "^0.5.10", - "@testing-library/dom": "^9.3.4", - "@testing-library/jest-dom": "^6.4.2", + "@babel/parser": "^7.24.8", + "@nuxt/test-utils": "^3.13.1", + "@nuxtjs/i18n": "^8.3.1", + "@playwright/test": "1.45.2", "@testing-library/user-event": "^14.5.2", - "@testing-library/vue": "^5.9.0", - "@types/express-useragent": "^1.0.2", - "@types/jest": "^29.5.12", - "@types/node": "^20.12.11", - "@types/throttle-debounce": "^5.0.0", - "@types/uuid": "^9.0.6", - "@vue/test-utils": "^1.1.3", - "@vue/vue2-jest": "^29.2.6", - "adm-zip": "^0.5.10", - "autoprefixer": "^10.4.16", - "babel-core": "^7.0.0-bridge.0", - "babel-jest": "^29.7.0", - "babel-loader": "8.3.0", - "caniuse-lite": "^1.0.30001620", - "chokidar": "^3.5.3", - "comment-json": "^4.2.3", - "css-loader": "^5.2.7", - "jest": "^29.7.0", - "jest-environment-jsdom": "^29.7.0", - "jest-fail-on-console": "^3.2.0", - "jest-transform-stub": "^2.0.0", - "npm-run-all2": "^6.1.2", - "postcss": "^8.4.31", - "qs": "^6.11.0", - "rimraf": "^5.0.0", - "tailwindcss": "^3.3.5", - "talkback": "^4.0.0", - "ts-node": "^10.9.1", - "typescript": "^5.5.2", - "vue-demi": "^0.14.7", - "vue-i18n-extract": "^2.0.7", - "vue-loader": "^15.11.1", - "vue-server-renderer": "^2.7.16", - "vue-template-compiler": "^2.7.16", - "vue-tsc": "2.0.24", - "webpack": "^4.46.0" + "@testing-library/vue": "^8.1.0", + "@types/node": "^20.14.11", + "@vitest/coverage-v8": "^2.0.4", + "@vitest/ui": "^2.0.4", + "@vue/test-utils": "^2.4.6", + "adm-zip": "^0.5.14", + "chokidar": "^3.6.0", + "core-js": "^3.37.1", + "eslint-plugin-jsonc": "^2.16.0", + "jsdom": "^24.1.0", + "node-html-parser": "^6.1.13", + "npm-run-all2": "^6.2.2", + "nuxt": "3.12.4", + "rimraf": "^6.0.1", + "talkback": "^4.2.0", + "typescript": "5.5.3", + "vitest": "^2.0.4", + "vitest-dom": "^0.1.1", + "vue": "3.4.33", + "vue-router": "^4.4.0", + "vue-tsc": "2.0.28" }, "browserslist": [ "> 1%", diff --git a/frontend/server/plugins/sentry.ts b/frontend/server/plugins/sentry.ts new file mode 100644 index 00000000000..dc467a10b8d --- /dev/null +++ b/frontend/server/plugins/sentry.ts @@ -0,0 +1,29 @@ +import { useRuntimeConfig } from "#imports" + +import { defineNitroPlugin } from "nitropack/runtime" +import * as Sentry from "@sentry/node" + +import { logger } from "~~/server/utils/logger" + +export default defineNitroPlugin((nitroApp) => { + const { + public: { sentry }, + } = useRuntimeConfig() + + Sentry.init({ + dsn: sentry.dsn, + environment: sentry.environment, + release: sentry.release, + }) + Sentry.setContext("render context", { platform: "server" }) + logger.success("Initialized sentry on the server with config\n", sentry) + + nitroApp.hooks.hook("request", (event) => { + event.context.$sentry = Sentry + }) + + nitroApp.hooks.hookOnce("close", async () => { + logger.log("Closing Sentry") + await Sentry.close() + }) +}) diff --git a/frontend/server/routes/healthcheck.ts b/frontend/server/routes/healthcheck.ts new file mode 100644 index 00000000000..3e2d185d81f --- /dev/null +++ b/frontend/server/routes/healthcheck.ts @@ -0,0 +1,8 @@ +import { defineEventHandler } from "h3" + +/** + * A simple healthcheck that is always true and confirms the server is running. + */ +export default defineEventHandler(() => { + return "OK" +}) diff --git a/frontend/server/utils/logger.ts b/frontend/server/utils/logger.ts new file mode 100644 index 00000000000..88b4352afb7 --- /dev/null +++ b/frontend/server/utils/logger.ts @@ -0,0 +1,18 @@ +import { consola } from "consola" + +import { LOCAL, PRODUCTION, STAGING } from "~/constants/deploy-env" + +/** + * This logger is used only in the Nitro server. + * Without the Nuxt context here, we cannot determine the deploymentEnv on the client, + * which is necessary to silence logging on client in production. + */ +const logger = consola.withTag("Openverse") +// In production, `info`, in other environments - `debug`. +logger.level = [PRODUCTION, STAGING].includes( + import.meta.env.DEPLOYMENT_ENV ?? LOCAL +) + ? 3 + : 4 + +export { logger } diff --git a/frontend/src/app.html b/frontend/src/app.html deleted file mode 100644 index 92d3de5b15e..00000000000 --- a/frontend/src/app.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - {{ HEAD }} - - - - {{ APP }} - - diff --git a/frontend/src/app.vue b/frontend/src/app.vue new file mode 100644 index 00000000000..bd5358698e3 --- /dev/null +++ b/frontend/src/app.vue @@ -0,0 +1,68 @@ + + + diff --git a/frontend/src/assets/fonts.css b/frontend/src/assets/fonts.css index e1fe30d1518..561162e78de 100644 --- a/frontend/src/assets/fonts.css +++ b/frontend/src/assets/fonts.css @@ -5,7 +5,7 @@ font-style: normal; font-weight: 100 900; font-display: swap; - src: url("~assets/fonts/Inter-Roman.var.woff2") format("woff2"); + src: url("fonts/Inter-Roman.var.woff2") format("woff2"); } @font-face { @@ -13,7 +13,7 @@ font-style: italic; font-weight: 100 900; font-display: swap; - src: url("~assets/fonts/Inter-Italic.var.woff2") format("woff2"); + src: url("fonts/Inter-Italic.var.woff2") format("woff2"); } } @@ -27,8 +27,8 @@ src: local("Inter-Regular"), local("Inter Regular"), - url("~assets/fonts/Inter-Regular.woff2") format("woff2"), - url("~assets/fonts/Inter-Regular.woff") format("woff"); + url("fonts/Inter-Regular.woff2") format("woff2"), + url("fonts/Inter-Regular.woff") format("woff"); } @font-face { @@ -39,8 +39,8 @@ src: local("Inter-Italic"), local("Inter Italic"), - url("~assets/fonts/Inter-Italic.woff2") format("woff2"), - url("~assets/fonts/Inter-Italic.woff") format("woff"); + url("fonts/Inter-Italic.woff2") format("woff2"), + url("fonts/Inter-Italic.woff") format("woff"); } @font-face { @@ -51,8 +51,8 @@ src: local("Inter-SemiBold"), local("Inter SemiBold"), - url("~assets/fonts/Inter-SemiBold.woff2") format("woff2"), - url("~assets/fonts/Inter-SemiBold.woff") format("woff"); + url("fonts/Inter-SemiBold.woff2") format("woff2"), + url("fonts/Inter-SemiBold.woff") format("woff"); } @font-face { @@ -63,8 +63,8 @@ src: local("Inter-SemiBoldItalic"), local("Inter SemiBoldItalic"), - url("~assets/fonts/Inter-SemiBoldItalic.woff2") format("woff2"), - url("~assets/fonts/Inter-SemiBoldItalic.woff") format("woff"); + url("fonts/Inter-SemiBoldItalic.woff2") format("woff2"), + url("fonts/Inter-SemiBoldItalic.woff") format("woff"); } @font-face { @@ -75,8 +75,8 @@ src: local("Inter-Bold"), local("Inter Bold"), - url("~assets/fonts/Inter-Bold.woff2") format("woff2"), - url("~assets/fonts/Inter-Bold.woff") format("woff"); + url("fonts/Inter-Bold.woff2") format("woff2"), + url("fonts/Inter-Bold.woff") format("woff"); } @font-face { @@ -87,8 +87,8 @@ src: local("Inter-BoldItalic"), local("Inter BoldItalic"), - url("~assets/fonts/Inter-BoldItalic.woff2") format("woff2"), - url("~assets/fonts/Inter-BoldItalic.woff") format("woff"); + url("fonts/Inter-BoldItalic.woff2") format("woff2"), + url("fonts/Inter-BoldItalic.woff") format("woff"); } } @@ -100,9 +100,9 @@ src: local("JetBrainsMono-Regular"), local("JetBrains Mono Regular"), - url("~assets/fonts/JetBrainsMono-Regular.woff2") format("woff2"), - url("~assets/fonts/JetBrainsMono-Regular.woff") format("woff"), - url("~assets/fonts/JetBrainsMono-Regular.otf") format("opentype"); + url("fonts/JetBrainsMono-Regular.woff2") format("woff2"), + url("fonts/JetBrainsMono-Regular.woff") format("woff"), + url("fonts/JetBrainsMono-Regular.otf") format("opentype"); } @font-face { font-family: "JetBrains Mono"; @@ -112,7 +112,7 @@ src: local("JetBrainsMono-Bold"), local("JetBrains Mono Bold"), - url("~assets/fonts/JetBrainsMono-Bold.woff2") format("woff2"), - url("~assets/fonts/JetBrainsMono-Bold.woff") format("woff"), - url("~assets/fonts/JetBrainsMono-Bold.otf") format("opentype"); + url("fonts/JetBrainsMono-Bold.woff2") format("woff2"), + url("fonts/JetBrainsMono-Bold.woff") format("woff"), + url("fonts/JetBrainsMono-Bold.otf") format("opentype"); } diff --git a/frontend/src/assets/svg/sprite/icons.svg b/frontend/src/assets/svg/sprite/icons.svg new file mode 100644 index 00000000000..54d9127acc8 --- /dev/null +++ b/frontend/src/assets/svg/sprite/icons.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/frontend/src/assets/svg/sprite/images.svg b/frontend/src/assets/svg/sprite/images.svg new file mode 100644 index 00000000000..a2a807b57c9 --- /dev/null +++ b/frontend/src/assets/svg/sprite/images.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/frontend/src/assets/svg/sprite/licenses.svg b/frontend/src/assets/svg/sprite/licenses.svg new file mode 100644 index 00000000000..3c8d6c6cb98 --- /dev/null +++ b/frontend/src/assets/svg/sprite/licenses.svg @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/frontend/src/components/VAudioThumbnail/VAudioThumbnail.vue b/frontend/src/components/VAudioThumbnail/VAudioThumbnail.vue index 5c4d6dd2067..06f492ee9c9 100644 --- a/frontend/src/components/VAudioThumbnail/VAudioThumbnail.vue +++ b/frontend/src/components/VAudioThumbnail/VAudioThumbnail.vue @@ -36,12 +36,13 @@ diff --git a/frontend/src/pages/image/_id/report.vue b/frontend/src/pages/image/[id]/report.vue similarity index 66% rename from frontend/src/pages/image/_id/report.vue rename to frontend/src/pages/image/[id]/report.vue index dfb7bef5645..53246380d53 100644 --- a/frontend/src/pages/image/_id/report.vue +++ b/frontend/src/pages/image/[id]/report.vue @@ -41,52 +41,44 @@ - diff --git a/frontend/src/pages/image/_id/index.vue b/frontend/src/pages/image/_id/index.vue deleted file mode 100644 index d88bf9f1af5..00000000000 --- a/frontend/src/pages/image/_id/index.vue +++ /dev/null @@ -1,303 +0,0 @@ - - - - - diff --git a/frontend/src/pages/image/collection.vue b/frontend/src/pages/image/collection.vue index c823700a778..fa2f485f7a7 100644 --- a/frontend/src/pages/image/collection.vue +++ b/frontend/src/pages/image/collection.vue @@ -16,13 +16,15 @@ - diff --git a/frontend/src/pages/index.vue b/frontend/src/pages/index.vue index 0d57279fb6d..33a6b76f27a 100644 --- a/frontend/src/pages/index.vue +++ b/frontend/src/pages/index.vue @@ -14,106 +14,77 @@ -