From dc6733367264f1132a7e553c44400198d12efe3a Mon Sep 17 00:00:00 2001 From: Olga Bulat Date: Mon, 15 Jan 2024 19:09:51 +0300 Subject: [PATCH] Update to Nuxt 3 Use har for audio tests to prevent provider requests and flakiness Convert ui cookies for ease of use Move analytics tests from unit tests to e2e tests Disable buttons until hydrated Make audio tapes smaller Update tests Use har for audio tests to prevent provider requests and flakiness Convert ui cookies for ease of use Move analytics tests from unit tests to e2e tests Disable buttons until hydrated Remove everything but the homepage and content pages Vendor in SVG sprites Remove patch Add search, media and related-media stores Add search pages with search functionality Add single result pages Re-add tests; fix unit tests; disable buttons until hydrated Fix invalid locale keys Refactor search page and fetching Fix localePath warning Add additional search views Get localePath from useNuxtApp Add healthcheck and robots server routes Add photos redirect Add sitemap Add simple sentry integration Proxy API calls Tests Add a missing tapes and add peaks=true to related audio tapes Add peaks=true to related tapes Reuse the same audio for seo Add server-prepare for initial provider loading Fix e2e route mocking Fix collection test Fix provider unit test Handle case when there are no providers Fix ui cookie overwriting Fix Tab border when focused Fix Waveform on box layout Fix Sources table sorting Fix content report POST request Fix global audio player on navigation Add link to source page to the sources table Update nuxt prepare script and add logging Remove aria-pressed from buttons when not set Refactor media store to make easier to test Add coverage to unit tests Add more media store unit tests Extract api-token from the api route Add api token tests Remove duplicate tags Use script setup for pages fetching data Improve VSkipToContentButton and add it to VCollectionPage Error component: add modal target and script setup Unify error handling Update sentry and env settings Remove unnecessary await from VSvg Remove provider-init plugin Set lazy to true in useAsyncData Remove error watch from index pages Remove VFetchingError Use script setup and remove useHead from pages Remove head and localeHead from app, error and single result Remove sentry Use script setup; simplify VLoadMore; remove fetchState Remove async from watchers Convert headers to script setup Replace store getters with actions Remove page title Update @nuxtjs/i18n to edge version Update dependencies Move search handling to search.vue Remove single-result middleware Use script setup and t from useNuxtApp.$i18n layout Replace axios with ofetch Signed-off-by: Olga Bulat Update prettier; types; analytics plugin Signed-off-by: Olga Bulat Use axios for fetching search requests Signed-off-by: Olga Bulat Fix photos redirect Signed-off-by: Olga Bulat Remove extra param Signed-off-by: Olga Bulat Fix single result Signed-off-by: Olga Bulat Fix back to search link for all view Signed-off-by: Olga Bulat Try fixing error display Signed-off-by: Olga Bulat --- .eslintignore | 2 + .github/actions/build-docs/action.yml | 11 - .github/workflows/ci_cd.yml | 4 - frontend/.env.template | 16 +- frontend/.gitignore | 2 + frontend/.storybook/decorators/with-rtl.js | 37 - .../decorators/with-screenshot-area.js | 12 - .../.storybook/decorators/with-ui-store.js | 18 - frontend/.storybook/main.js | 23 - frontend/.storybook/middleware.js | 1 - frontend/.storybook/preview.js | 30 - frontend/Dockerfile | 11 +- frontend/Dockerfile.playwright | 7 + frontend/docker-compose.playwright.yml | 11 +- 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 | 375 +- frontend/package.json | 136 +- frontend/server/routes/healthcheck.ts | 8 + .../robots.js => server/routes/robots.ts} | 11 +- frontend/src/app.html | 10 - frontend/src/app.vue | 48 + 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 | 29 +- .../VAudioTrack/VGlobalAudioTrack.vue | 14 +- .../src/components/VAudioTrack/VWaveform.vue | 2 +- .../VAudioTrack/layouts/VBoxLayout.vue | 14 +- .../VAudioTrack/layouts/VGlobalLayout.vue | 5 +- .../VAudioTrack/layouts/VRowLayout.vue | 16 +- .../components/VBackToSearchResultsLink.vue | 1 - .../components/VBanner/VAnalyticsNotice.vue | 13 +- frontend/src/components/VBanner/VBanners.vue | 24 +- .../VBanner/VTranslationStatusBanner.vue | 15 +- frontend/src/components/VButton.vue | 10 +- .../src/components/VCheckbox/VCheckbox.vue | 6 +- .../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 | 69 +- .../VErrorSection/VErrorSection.vue | 72 +- .../components/VErrorSection/VNoResults.vue | 65 +- .../VExternalSearch/VExternalSearchForm.vue | 102 +- .../VExternalSearch/VExternalSourceList.vue | 56 +- .../components/VFilters/VFilterChecklist.vue | 11 +- .../VFilters/VLicenseExplanation.vue | 7 +- .../components/VFilters/VSearchGridFilter.vue | 3 +- frontend/src/components/VFourOhFour.vue | 16 +- .../VGlobalAudioSection.vue | 30 +- .../src/components/VHeader/VFilterButton.vue | 7 +- .../VHeader/VFilterIconOrCounter.vue | 2 +- .../src/components/VHeader/VHeaderDesktop.vue | 20 +- .../components/VHeader/VHeaderInternal.vue | 7 +- .../VHeaderMobile/VContentSettingsButton.vue | 5 +- .../VContentSettingsModalContent.vue | 2 +- .../VHeader/VHeaderMobile/VHeaderMobile.vue | 19 +- .../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 | 2 +- .../VSearchBar/VStandaloneSearchBar.vue | 13 +- .../src/components/VHeader/VWordPressLink.vue | 11 +- frontend/src/components/VHideButton.vue | 4 +- .../components/VHomeGallery/VHomeGallery.vue | 12 +- frontend/src/components/VHomepageContent.vue | 27 +- frontend/src/components/VIcon/VIcon.vue | 94 +- .../components/VIconButton/VIconButton.vue | 1 - .../src/components/VImageCell/VImageCell.vue | 19 +- .../components/VInputField/VInputField.vue | 31 +- frontend/src/components/VItemGroup/VItem.vue | 37 +- .../src/components/VItemGroup/VItemGroup.vue | 8 +- .../VLanguageSelect/VLanguageSelect.vue | 49 +- frontend/src/components/VLicense/VLicense.vue | 7 +- .../components/VLicense/VLicenseElements.vue | 5 +- frontend/src/components/VLink.vue | 196 +- frontend/src/components/VLoadMore.vue | 16 +- .../components/VLogoLoader/VLogoLoader.vue | 2 +- .../VMediaInfo/VByLine/VScroller.vue | 5 +- .../components/VMediaInfo/VCopyLicense.vue | 5 +- .../components/VMediaInfo/VMediaDetails.vue | 5 +- .../components/VMediaInfo/VMediaLicense.vue | 18 +- .../src/components/VMediaInfo/VMediaReuse.vue | 5 +- .../src/components/VMediaInfo/VMediaTags.vue | 10 +- .../src/components/VMediaInfo/VMetadata.vue | 17 +- .../components/VMediaInfo/VRelatedMedia.vue | 17 +- .../VMediaInfo/VSourceProviderTooltip.vue | 3 +- .../src/components/VModal/VInputModal.vue | 48 +- frontend/src/components/VModal/VModal.vue | 8 +- .../src/components/VModal/VModalContent.vue | 323 +- .../src/components/VModal/VModalTarget.vue | 13 - .../components/VModal/meta/VModal.stories.js | 5 +- frontend/src/components/VRadio/VRadio.vue | 15 +- .../VRecentSearches/VRecentSearches.vue | 4 +- .../VSafeBrowsing/VSafeBrowsing.vue | 16 +- .../components/VSafetyWall/VSafetyWall.vue | 11 +- frontend/src/components/VScrollButton.vue | 2 +- frontend/src/components/VScrollableLine.vue | 6 +- .../VSearchResultsGrid/VAudioInstructions.vue | 14 +- .../VSearchResultsGrid/VAudioResult.vue | 15 +- .../VSearchResultsGrid/VCollectionResults.vue | 4 +- .../VSearchResultsGrid/VImageCollection.vue | 4 +- .../VSearchResultsGrid/VSearchResults.vue | 4 +- .../src/components/VSearchResultsTitle.vue | 15 +- .../components/VSelectField/VSelectField.vue | 119 +- .../src/components/VSingleResultControls.vue | 2 +- .../components/VSkeleton/VGridSkeleton.vue | 11 +- frontend/src/components/VSketchFabViewer.vue | 18 +- 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 | 6 +- .../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 | 20 +- frontend/src/composables/use-i18n.ts | 9 - .../src/composables/use-image-cell-size.ts | 2 +- frontend/src/composables/use-match-routes.ts | 7 +- frontend/src/composables/use-pages.ts | 17 +- frontend/src/composables/use-search-type.ts | 12 +- frontend/src/composables/use-search.ts | 6 +- frontend/src/composables/use-seekable.ts | 13 +- .../use-single-result-page-meta.ts | 11 +- frontend/src/composables/use-sprite.ts | 42 + frontend/src/composables/use-window-scroll.ts | 62 - frontend/src/constants/media.ts | 2 +- frontend/src/constants/meta.ts | 10 +- frontend/src/data/api-service.ts | 301 +- 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 | 38 + 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 +- frontend/src/locales/scripts/utils.js | 29 +- frontend/src/middleware/collection.ts | 32 +- .../src/middleware/feature-flags.global.ts | 11 + frontend/src/middleware/feature-flags.ts | 11 - frontend/src/middleware/search.ts | 54 +- frontend/src/middleware/single-result.ts | 68 +- frontend/src/modules/prometheus.ts | 98 - frontend/src/pages/about.vue | 65 +- .../src/pages/audio/{_id => [id]}/index.vue | 181 +- frontend/src/pages/audio/collection.vue | 143 +- frontend/src/pages/feedback.vue | 35 +- frontend/src/pages/image/[id]/index.vue | 273 + .../src/pages/image/{_id => [id]}/report.vue | 61 +- frontend/src/pages/image/_id/index.vue | 303 - frontend/src/pages/image/collection.vue | 138 +- frontend/src/pages/index.vue | 130 +- frontend/src/pages/preferences.vue | 128 +- frontend/src/pages/privacy.vue | 55 +- frontend/src/pages/search-help.vue | 60 +- frontend/src/pages/search.vue | 294 +- frontend/src/pages/sensitive-content.vue | 63 +- frontend/src/pages/sources.vue | 48 +- frontend/src/plugins/analytics.ts | 33 +- frontend/src/plugins/api-token.server.ts | 60 +- frontend/src/plugins/errors.ts | 48 +- frontend/src/plugins/focus-visible.client.ts | 1 - frontend/src/plugins/init-stores.ts | 24 +- frontend/src/plugins/polyfills.client.ts | 5 - frontend/src/plugins/sentry.ts | 19 +- 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 .../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/static/ai.txt | 60 - frontend/src/static/favicon.svg | 28 - frontend/src/stores/active-media.ts | 5 +- frontend/src/stores/feature-flag.ts | 47 +- frontend/src/stores/media/index.ts | 39 +- frontend/src/stores/media/related-media.ts | 46 +- frontend/src/stores/media/services.ts | 11 - frontend/src/stores/media/single-result.ts | 45 +- frontend/src/stores/provider.ts | 33 +- frontend/src/stores/search.ts | 20 +- frontend/src/stores/ui.ts | 60 +- frontend/src/stories/Introduction.mdx | 8 - frontend/src/types/analytics.ts | 8 +- frontend/src/types/cookies.ts | 18 +- frontend/src/types/fetch-state.ts | 7 +- frontend/src/types/media.ts | 1 + frontend/src/utils/attribution-html.ts | 9 +- frontend/src/utils/console.ts | 4 +- frontend/src/utils/cookies.ts | 10 - frontend/src/utils/decode-media-data.ts | 7 +- frontend/src/utils/license.ts | 10 +- frontend/src/utils/metadata.ts | 18 +- frontend/src/utils/node-env.ts | 4 +- frontend/src/utils/og.ts | 23 +- frontend/src/utils/query-utils.ts | 24 + 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 | 12 +- .../test/playwright/e2e/audio-detail.spec.ts | 4 +- .../e2e/filters-sidebar-keyboard.spec.ts | 8 +- frontend/test/playwright/e2e/filters.spec.ts | 16 +- .../test/playwright/e2e/image-detail.spec.ts | 4 +- .../test/playwright/e2e/load-more.spec.ts | 12 +- .../test/playwright/e2e/report-media.spec.ts | 2 +- .../playwright/e2e/search-navigation.spec.ts | 2 +- .../e2e/search-query-client.spec.ts | 2 +- .../e2e/search-query-server.spec.ts | 2 +- .../test/playwright/e2e/search-types.spec.ts | 4 +- .../playwright/e2e/skip-to-content.spec.ts | 3 +- frontend/test/playwright/playwright.config.ts | 1 + frontend/test/playwright/utils/navigation.ts | 1 + .../components/filters.spec.ts | 4 +- .../visual-regression/pages/errors.spec.ts | 8 +- ...h-result-image-no-results-ltr-sm-linux.png | Bin 39799 -> 37764 bytes .../search-result-timeout-ltr-sm-linux.png | Bin 27347 -> 25569 bytes .../ltr-full-page-report-xl-linux.png | Bin 268450 -> 268213 bytes .../visual-regression/pages/pages.spec.ts | 8 +- .../pages/search-with-banners.spec.ts | 3 +- ...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 | 127 +- .../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 | 22 +- .../VMediaInfo/v-media-details.spec.js | 59 +- .../VSafetyWall/v-safety-wall.spec.js | 37 +- .../unit/specs/components/VTag/v-tag.spec.js | 13 +- .../v-search-results-title.spec.js.snap | 758 +- .../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 | 68 +- .../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 | 2 + .../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 | 48 +- .../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 | 99 +- .../specs/stores/active-media-store.spec.js | 4 +- .../specs/stores/feature-flag-store.spec.js | 52 +- .../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 | 82 +- .../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 | 8 +- .../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 | 185 + frontend/test/unit/test-utils/render.js | 31 +- frontend/test/unit/vitest-setup.ts | 4 + frontend/tsconfig.json | 46 +- 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 + package.json | 2 +- packages/js/eslint-plugin/package.json | 1 + .../js/eslint-plugin/src/configs/import.ts | 15 +- .../js/eslint-plugin/src/configs/index.ts | 18 +- packages/js/eslint-plugin/src/configs/vue.ts | 33 +- pnpm-lock.yaml | 19085 +++++++--------- 385 files changed, 13620 insertions(+), 19079 deletions(-) delete mode 100644 frontend/.storybook/decorators/with-rtl.js delete mode 100644 frontend/.storybook/decorators/with-screenshot-area.js delete mode 100644 frontend/.storybook/decorators/with-ui-store.js 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/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/routes/healthcheck.ts rename frontend/{src/server-middleware/robots.js => server/routes/robots.ts} (83%) 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/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/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 (51%) create mode 100644 frontend/src/pages/image/[id]/index.vue rename frontend/src/pages/image/{_id => [id]}/report.vue (64%) delete mode 100644 frontend/src/pages/image/_id/index.vue delete mode 100644 frontend/src/plugins/focus-visible.client.ts delete mode 100644 frontend/src/plugins/polyfills.client.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}/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/static/ai.txt delete mode 100644 frontend/src/static/favicon.svg delete mode 100644 frontend/src/stores/media/services.ts delete mode 100644 frontend/src/stories/Introduction.mdx delete mode 100644 frontend/src/utils/cookies.ts create mode 100644 frontend/src/utils/query-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%) 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 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 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 diff --git a/.eslintignore b/.eslintignore index d95ecf4d139..5a1daedf208 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,5 +1,7 @@ coverage +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 4b713a96c30..d1d38b71385 100644 --- a/.github/actions/build-docs/action.yml +++ b/.github/actions/build-docs/action.yml @@ -17,19 +17,8 @@ runs: # Pass -W to fail CI if warnings exist just documentation/build -W - - name: Install translations - shell: bash - run: | - just frontend/run i18n - - - name: Build Storybook - shell: bash - run: | - just frontend/run storybook:build-for-docs - # 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 e148ae73361..a88ad171477 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -750,14 +750,11 @@ jobs: name: - playwright_vr - playwright_e2e - - storybook include: - name: playwright_vr script: "test:playwright visual-regression" - name: playwright_e2e script: "test:playwright e2e" - - name: storybook - script: "test:storybook" steps: - name: Checkout repository @@ -794,7 +791,6 @@ jobs: name: - playwright_vr - playwright_e2e - - storybook steps: - name: Pass diff --git a/frontend/.env.template b/frontend/.env.template index 57bfac3a2c4..87800330475 100644 --- a/frontend/.env.template +++ b/frontend/.env.template @@ -1,3 +1,13 @@ -#API_URL=http://127.17.0.1:8000/ -#API_CLIENT_ID="" -#API_CLIENT_SECRET="" +#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.engineering/ # prod API +#NUXT_API_CLIENT_ID="" +#NUXT_API_CLIENT_SECRET="" +NUXT_PUBLIC_SAVED_SEARCH_COUNT=4 +NUXT_PUBLIC_PROVIDER_UPDATE_FREQUENCY=3600000# 1 hour +NUXT_PUBLIC_PLAUSIBLE_DOMAIN="openverse.org" #localhost +NUXT_PUBLIC_PLAUSIBLE_API_HOST="https://openverse.org" #http://localhost:50290 +NUXT_PUBLIC_SENTRY_DSN="https://b6466b74788a4a2f8a7912eea912beb7@o787041.ingest.sentry.io/5799642" +NUXT_PUBLIC_SENTRY_ENVIRONMENT="local" +NUXT_PUBLIC_SENTRY_RELEASE="" +PORT=8443 diff --git a/frontend/.gitignore b/frontend/.gitignore index 99b7e598eec..70ccfce63f2 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -1,5 +1,6 @@ # Nuxt .nuxt +.output # Storybook .nuxt-storybook @@ -11,6 +12,7 @@ test/Default # Coverage test/unit/coverage/ +coverage # Generated translation files *.pot diff --git a/frontend/.storybook/decorators/with-rtl.js b/frontend/.storybook/decorators/with-rtl.js deleted file mode 100644 index 384a001b174..00000000000 --- a/frontend/.storybook/decorators/with-rtl.js +++ /dev/null @@ -1,37 +0,0 @@ -import Vue from "vue" - -import { ref, watch, useContext, onMounted } from "@nuxtjs/composition-api" -import { useEffect } from "@storybook/client-api" - -const languageDirection = Vue.observable({ value: "ltr" }) - -export const WithRTL = (story, context) => { - useEffect(() => { - languageDirection.value = context.globals.languageDirection - }, [context.globals.languageDirection]) - - return { - template: `
`, - components: { story }, - setup() { - const element = ref() - const { i18n } = useContext() - onMounted(() => { - watch( - languageDirection, - (direction) => { - i18n.localeProperties.dir = direction.value - if (element.value) { - element.value.ownerDocument.documentElement.setAttribute( - "dir", - direction?.value ?? "ltr" - ) - } - }, - { immediate: true } - ) - }) - return { element } - }, - } -} diff --git a/frontend/.storybook/decorators/with-screenshot-area.js b/frontend/.storybook/decorators/with-screenshot-area.js deleted file mode 100644 index a6bc128177e..00000000000 --- a/frontend/.storybook/decorators/with-screenshot-area.js +++ /dev/null @@ -1,12 +0,0 @@ -export const WithScreenshotArea = (story) => { - return { - template: ` -
- -
`, - components: { story }, - } -} diff --git a/frontend/.storybook/decorators/with-ui-store.js b/frontend/.storybook/decorators/with-ui-store.js deleted file mode 100644 index d57f28fa746..00000000000 --- a/frontend/.storybook/decorators/with-ui-store.js +++ /dev/null @@ -1,18 +0,0 @@ -import { ref, onMounted } from "@nuxtjs/composition-api" - -import { useLayout } from "~/composables/use-layout" - -export const WithUiStore = (story) => { - return { - template: `
`, - components: { story }, - setup() { - const element = ref() - const { updateBreakpoint } = useLayout() - onMounted(() => { - updateBreakpoint() - }) - return { element } - }, - } -} 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 0bcecf68561..aab158362e0 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -22,7 +22,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 -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. @@ -41,9 +41,10 @@ RUN pnpm install # 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.engineering/ +ENV NUXT_PUBLIC_API_URL=$API_URL RUN pnpm build:only @@ -68,7 +69,9 @@ 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 +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 615097955d1..e665bac8f91 100644 --- a/frontend/Dockerfile.playwright +++ b/frontend/Dockerfile.playwright @@ -6,7 +6,14 @@ FROM mcr.microsoft.com/playwright:v${PLAYWRIGHT_VERSION}-jammy ARG PACKAGE_MANAGER +WORKDIR /frontend + COPY package.json . # Requires `packageManager` field to be present in `frontend/package.json`. RUN npm install -g $PACKAGE_MANAGER + +# 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 93ad9383adb..7f229b679e0 100644 --- a/frontend/docker-compose.playwright.yml +++ b/frontend/docker-compose.playwright.yml @@ -8,12 +8,15 @@ services: - PACKAGE_MANAGER=${PACKAGE_MANAGER} - 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/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..2e8f1a761c3 100644 --- a/frontend/nuxt.config.ts +++ b/frontend/nuxt.config.ts @@ -1,29 +1,10 @@ -import path from "path" -import fs from "fs" - -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 { defineNuxtConfig } from "nuxt/config" 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,50 +29,6 @@ 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 baseProdName = process.env.CI ? "[name]" : "[contenthash:7]" - -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 openverseLocales = [ { /* Nuxt i18n fields */ @@ -109,77 +46,112 @@ const openverseLocales = [ ...(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: + process.env.NUXT_PUBLIC_API_URL || + "https://api.openverse.engineering/", + }, + { + rel: "preconnect", + href: + process.env.NUXT_PUBLIC_API_URL || + "https://api.openverse.engineering/", + 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, - }, - router: { - middleware: "feature-flags", - }, - 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, + serverDir: "server/", + devServer: { + port: 8443, + host: "0.0.0.0", + }, + imports: { + autoImport: false, + }, + css: ["~/assets/fonts.css", "~/styles/accent.css"], + runtimeConfig: { + apiClientId: "", + apiClientSecret: "", + public: { + // Can be overwritten by NUXT_PUBLIC_API_URL env variable + deploymentEnv: process.env.DEPLOYMENT_ENV ?? "local", + apiUrl: "https://api.openverse.engineering/", + providerUpdateFrequency: 3600000, + savedSearchCount: 4, + sentry: { + dsn: "https://b6466b74788a4a2f8a7912eea912beb7@o787041.ingest.sentry.io/5799642", + environment: "local", + release: "", + }, + plausible: { + trackLocalhost: true, // TODO: Replace with isProdNotPw + // 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"), + }, + }, }, dev: !isProd, - buildModules: [ - "@nuxt/typescript-build", - "@nuxtjs/composition-api/module", - "@nuxtjs/svg-sprite", - "@pinia/nuxt", - ], + /** + * 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, + }, 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", ], - 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 } }, + }, + tailwindcss: { + cssPath: "~/styles/tailwind.css", }, i18n: { baseUrl: "https://openverse.org", @@ -199,152 +171,11 @@ const config: NuxtConfig = { * - [Browser language detection info](https://i18n.nuxtjs.org/browser-language-detection) * */ detectBrowserLanguage: false, - vueI18n: "~/plugins/vue-i18n", + vueI18n: "./src/vue-i18n", }, - sitemap: { - hostname: "https://openverse.org", - i18n: { - locales: openverseLocales.map((l) => l.iso), - routesNameSeparator: "___", + nitro: { + prerender: { + routes: ["/sitemap.xml"], }, }, - /** - * 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, - }, - }, - }, - plausible: { - trackLocalhost: !isProdNotPlaywright, - }, - 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 650ce1bd13b..aa9d0b1f55b 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -6,26 +6,30 @@ "scripts": { "predev": "pnpm install && pnpm i18n:no-get", "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", "prebuild": "pnpm install && pnpm i18n", "build": "pnpm build:only", - "build:only": "nuxt build", + "build:only": "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:playwright": "pnpm i18n:copy-test-locales && pnpm start", "prod": "pnpm build:only && 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": "nuxt storybook build", "storybook:build-for-docs": "NODE_OPTIONS=--openssl-legacy-provider nuxt storybook build", "talkback": "node ./test/proxy.js", "pretest": "pnpm install", + "prepare-nuxt": "pnpm run i18n:copy-test-locales && nuxt prepare", + "postinstall:nuxt": "pnpm run prepare-nuxt", "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", @@ -41,7 +45,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", @@ -54,96 +58,68 @@ "create:component-unit-test": "remake component-unit-test", "create:component-scaffolding": "npm-run-all \"create:component-sfc --output=src/components --name={1}\" \"create:story --output=src/components/{1}/meta --name={1}\" \"create:component-unit-test --output=test/unit/specs/components/{1} --name={1} --fileName={2}\" \"create:component-storybook-test --output=test/storybook/visual-regression --fileName={2} --name={1}\" --", "create:component": "./bin/create-component.sh", - "doc:media-props": "node ./scripts/document-media.js" + "doc:media-props": "node ./scripts/document-media.js", + "build-storybook": "storybook build" }, "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.4", + "@intlify/core-base": "^9.13.1", + "@intlify/message-compiler": "^9.9.1", + "@nuxtjs/plausible": "^1.0.0", + "@nuxtjs/sitemap": "^5.1.4", + "@nuxtjs/tailwindcss": "^6.12.0", + "@pinia/nuxt": "^0.5.1", + "@sentry/vue": "^7.112.2", + "@tailwindcss/typography": "^0.5.13", + "@vueuse/core": "^10.9.0", + "@wordpress/is-shallow-equal": "^4.56.0", "async-mutex": "^0.5.0", - "axios": "^1.0.0", - "axios-mock-adapter": "^1.20.0", + "axios": "^1.6.8", + "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.3.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" + "uuid": "^9.0.1" }, "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", + "@nuxt/test-utils": "^3.9.0", + "@nuxtjs/i18n": "^8.3.1", "@playwright/test": "1.43.1", - "@tailwindcss/typography": "^0.5.10", - "@testing-library/dom": "^9.3.4", - "@testing-library/jest-dom": "^6.4.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", - "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", + "@testing-library/vue": "^8.0.3", + "@vitest/coverage-v8": "^1.5.3", + "@vitest/ui": "^1.5.3", + "@vue/test-utils": "^2.4.3", + "adm-zip": "^0.5.12", + "eslint-plugin-jsonc": "^2.15.1", + "jsdom": "^24.0.0", + "node-html-parser": "^6.0.0", "npm-run-all2": "^6.1.2", - "postcss": "^8.4.31", - "qs": "^6.11.0", + "nuxt": "^3.11.2", "rimraf": "^5.0.0", - "tailwindcss": "^3.3.5", "talkback": "^4.0.0", - "ts-node": "^10.9.1", - "typescript": "^5.4.5", - "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": "1.8.27", - "webpack": "^4.46.0" + "typescript": "5.4.5", + "vitest": "^1.5.3", + "vitest-dom": "^0.1.1", + "vue": "3.4.26", + "vue-router": "^4.3.2", + "vue-tsc": "^2.0.16", + "storybook": "8.0.8", + "@types/node": "^18.17.5", + "@storybook/vue3": "8.0.8", + "@storybook-vue/nuxt": "0.2.6", + "@nuxtjs/storybook": "7.0.2", + "@storybook/addon-links": "8.0.8", + "@storybook/builder-vite": "8.0.8", + "@storybook/addon-essentials": "8.0.8", + "@storybook/addon-interactions": "8.0.8", + "@storybook/test": "8.0.8", + "@storybook/blocks": "8.0.8" }, "browserslist": [ "> 1%", 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/src/server-middleware/robots.js b/frontend/server/routes/robots.ts similarity index 83% rename from frontend/src/server-middleware/robots.js rename to frontend/server/routes/robots.ts index fd38e9cc8e8..757b572828e 100644 --- a/frontend/src/server-middleware/robots.js +++ b/frontend/server/routes/robots.ts @@ -1,4 +1,6 @@ -const { LOCAL, PRODUCTION } = require("../constants/deploy-env") +import { defineEventHandler } from "h3" + +import { LOCAL, PRODUCTION } from "~/constants/deploy-env" const deniedUserAgents = [ "GPTBot", @@ -22,7 +24,7 @@ const aiDisallowRules = deniedUserAgents /** * Send the correct robots.txt information per-environment. */ -export default function robots(_, res) { +export default defineEventHandler(() => { const deployEnv = process.env.DEPLOYMENT_ENV ?? LOCAL const contents = @@ -49,6 +51,5 @@ User-agent: * Disallow: / ` - res.setHeader("Content-Type", "text/plain") - res.end(contents) -} + return contents.replaceAll("\n", "
") +}) 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..b18b33993d4 --- /dev/null +++ b/frontend/src/app.vue @@ -0,0 +1,48 @@ + + + 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..41d83caf7b7 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 64% rename from frontend/src/pages/image/_id/report.vue rename to frontend/src/pages/image/[id]/report.vue index dfb7bef5645..a6ddc6a2d96 100644 --- a/frontend/src/pages/image/_id/report.vue +++ b/frontend/src/pages/image/[id]/report.vue @@ -27,7 +27,7 @@ size="medium" class="label-bold" > - {{ $t("report.imageDetails") }} + {{ t("report.imageDetails") }} @@ -41,52 +41,45 @@ - diff --git a/frontend/src/pages/image/_id/index.vue b/frontend/src/pages/image/_id/index.vue deleted file mode 100644 index 275e3d99762..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..7403006f368 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..201fb3c773b 100644 --- a/frontend/src/pages/index.vue +++ b/frontend/src/pages/index.vue @@ -14,106 +14,80 @@ -