From 1db86d404f2c37ffa3b7f443f04f682847497f50 Mon Sep 17 00:00:00 2001 From: Flupsi Date: Mon, 4 Aug 2025 21:32:51 +0000 Subject: [PATCH] feat (front): new ui on start page #2477 NOCHANGELOG --- .gitignore | 3 +- .gitlab-ci.yml | 8 +- front/.browserslistrc | 24 + front/.yarnrc.yml | 1 + front/Dockerfile | 2 +- front/package.json | 10 +- front/postcss.config.js | 20 + front/src/components/Home.vue | 498 +- front/src/components/album/Widget.vue | 2 +- .../src/components/audio/ChannelEntryCard.vue | 2 +- front/src/components/audio/ChannelForm.vue | 4 +- front/src/components/audio/PlayButton.vue | 3 +- front/src/components/audio/SearchBar.vue | 106 +- front/src/components/audio/podcast/Table.vue | 2 +- front/src/components/audio/track/Widget.vue | 2 +- front/src/components/auth/LoginForm.vue | 26 +- front/src/components/common/ActorLink.vue | 5 +- front/src/components/common/UserLink.vue | 11 +- front/src/components/library/AlbumBase.vue | 35 +- front/src/components/library/Albums.vue | 13 +- front/src/components/library/Artists.vue | 9 +- front/src/components/library/EditForm.vue | 35 +- front/src/components/library/Library.vue | 11 +- front/src/components/library/Podcasts.vue | 14 +- front/src/components/playlists/Widget.vue | 2 +- front/src/components/radios/Card.vue | 8 +- front/src/components/ui/Activity.vue | 2 + front/src/components/ui/Button.vue | 2 + front/src/components/ui/Card.vue | 16 +- front/src/components/ui/Header.vue | 7 +- front/src/components/ui/Input.vue | 18 +- front/src/components/ui/Modal.vue | 8 +- front/src/components/ui/Nav.vue | 14 +- front/src/components/ui/Pill.vue | 283 +- front/src/components/ui/Pills.vue | 114 +- front/src/components/ui/Popover.vue | 23 +- front/src/components/ui/Section.vue | 7 +- front/src/components/ui/Select.vue | 256 + front/src/components/ui/Textarea.vue | 12 +- front/src/components/ui/input.scss | 4 + .../components/ui/popover/PopoverCheckbox.vue | 1 + .../src/components/ui/popover/PopoverItem.vue | 139 +- .../components/ui/popover/PopoverRadio.vue | 8 +- .../ui/popover/PopoverRadioItem.vue | 3 + .../components/ui/popover/PopoverSubmenu.vue | 4 +- front/src/components/vui/Pagination.vue | 16 +- .../src/composables/locale/useSharedLabels.ts | 2 + .../src/composables/navigation/useOrdering.ts | 27 +- front/src/composables/navigation/usePage.ts | 4 + .../composables/navigation/useSmartSearch.ts | 45 + front/src/composables/width.ts | 2 + front/src/injection-keys.ts | 1 + front/src/locales/en_US.json | 37 +- front/src/main.ts | 2 - front/src/style/colors.scss | 51 +- front/src/style/components/_button.scss | 8 +- front/src/ui/components/UploadModal.vue | 2 +- front/src/ui/layouts/constrained.vue | 7 - front/src/ui/modals/Language.vue | 2 +- front/src/ui/stores/data.ts | 106 + front/src/views/auth/ManageUploads.vue | 400 +- front/src/views/auth/ProfileBase.vue | 5 - front/src/views/auth/ProfileOverview.vue | 5 - front/src/views/channels/DetailEpisodes.vue | 2 +- front/src/views/channels/List.vue | 4 +- front/tsconfig.json | 17 +- front/ui-docs/.vitepress/config.ts | 1 + front/ui-docs/components/ui/button.md | 28 +- front/ui-docs/components/ui/card.md | 5 +- front/ui-docs/components/ui/modal.md | 13 +- front/ui-docs/components/ui/pill.md | 8 + front/ui-docs/components/ui/popover.md | 37 +- front/ui-docs/components/ui/select.md | 254 + front/ui-docs/components/ui/toggle.md | 2 + front/ui-docs/contributing.md | 16 +- front/ui-docs/designing-pages.md | 8 +- front/ui-docs/index.md | 57 +- front/ui-docs/navigation.md | 18 +- front/ui-docs/using-color.md | 11 - front/vite.config.ts | 39 +- front/yarn.lock | 8157 ++++++++--------- 81 files changed, 5628 insertions(+), 5548 deletions(-) create mode 100644 front/.browserslistrc create mode 100644 front/.yarnrc.yml create mode 100644 front/postcss.config.js create mode 100644 front/src/components/ui/Select.vue create mode 100644 front/src/ui/stores/data.ts create mode 100644 front/ui-docs/components/ui/select.md diff --git a/.gitignore b/.gitignore index 1841c3ba9..17539d041 100644 --- a/.gitignore +++ b/.gitignore @@ -135,8 +135,9 @@ flake.lock # Zed .zed/ -# Node version (asdf) +# Node version (asdf, mise) .tool-versions +mise.toml # Lychee link checker .lycheecache diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 70dad9243..14e209a51 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -95,7 +95,7 @@ review_front: - if: $CI_PIPELINE_SOURCE == "merge_request_event" when: manual - image: $CI_REGISTRY/funkwhale/ci/node-python:18 + image: $CI_REGISTRY/funkwhale/ci/node-python:22 variables: BASE_URL: /-/$CI_PROJECT_NAME/-/jobs/$CI_JOB_ID/artifacts/front-review/ VUE_APP_ROUTER_BASE_URL: /-/$CI_PROJECT_NAME/-/jobs/$CI_JOB_ID/artifacts/front-review/ @@ -202,7 +202,7 @@ lint_front: - if: $CI_COMMIT_BRANCH =~ /(stable|develop)/ - changes: [front/**/*] - image: $CI_REGISTRY/funkwhale/ci/node-python:18 + image: $CI_REGISTRY/funkwhale/ci/node-python:22 cache: - *yarn_cache - *node_cache @@ -290,7 +290,7 @@ test_front: - if: $CI_COMMIT_BRANCH =~ /(stable|develop)/ - changes: [front/**/*] - image: $CI_REGISTRY/funkwhale/ci/node-python:18 + image: $CI_REGISTRY/funkwhale/ci/node-python:22 cache: - *yarn_cache - *node_cache @@ -417,7 +417,7 @@ build_front: - if: $CI_COMMIT_BRANCH =~ /(stable|develop)/ - changes: [front/**/*] - image: $CI_REGISTRY/funkwhale/ci/node-python:18 + image: $CI_REGISTRY/funkwhale/ci/node-python:22 variables: <<: *keep_git_files_permissions NODE_OPTIONS: --max-old-space-size=4096 diff --git a/front/.browserslistrc b/front/.browserslistrc new file mode 100644 index 000000000..2de50cd6e --- /dev/null +++ b/front/.browserslistrc @@ -0,0 +1,24 @@ +# Browser support targeting 95% coverage while enabling modern features +# This targets browsers that support ES2020+ and modern CSS features + +# Cover 95% of global usage +> 1% +last 2 versions +not dead + +# Exclude problematic browsers +not ie 11 +not op_mini all +not android <= 4.4 +not samsung <= 4 + +# Ensure modern browser support for ES2020+ features +chrome >= 87 +firefox >= 78 +safari >= 14 +edge >= 88 + +# Mobile browsers +ios >= 14 +and_chr >= 87 +and_ff >= 78 diff --git a/front/.yarnrc.yml b/front/.yarnrc.yml new file mode 100644 index 000000000..3186f3f07 --- /dev/null +++ b/front/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/front/Dockerfile b/front/Dockerfile index 659c25cf7..e16fabb37 100644 --- a/front/Dockerfile +++ b/front/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM node:18-alpine AS builder +FROM --platform=$BUILDPLATFORM node:22-alpine AS builder RUN apk add --no-cache jq bash coreutils python3 build-base diff --git a/front/package.json b/front/package.json index 4b5ce2a36..0cb921920 100644 --- a/front/package.json +++ b/front/package.json @@ -43,7 +43,6 @@ "dompurify": "3.2.4", "focus-trap": "7.2.0", "idb-keyval": "6.2.1", - "jquery": "3.7.1", "jsmediatags": "3.9.7", "lodash-es": "4.17.21", "lru-cache": "10.2.0", @@ -55,6 +54,7 @@ "showdown": "2.1.0", "stacktrace-js": "2.0.2", "standardized-audio-context": "25.3.60", + "string-similarity-js": "2.1.4", "text-clipper": "2.2.0", "transliteration": "2.3.5", "type-fest": "4.30.1", @@ -82,7 +82,6 @@ "@tauri-apps/cli": "^2.0.2", "@types/diff": "5.0.9", "@types/dompurify": "3.0.5", - "@types/jquery": "3.5.29", "@types/lodash-es": "4.17.12", "@types/moxios": "0.4.17", "@types/qs": "6.9.10", @@ -99,6 +98,7 @@ "@vue/eslint-config-typescript": "12.0.0", "@vue/test-utils": "2.4.1", "@vue/tsconfig": "0.6.0", + "autoprefixer": "10.4.21", "cypress": "13.6.4", "eslint": "8.57.0", "eslint-config-standard": "17.1.0", @@ -114,13 +114,13 @@ "msw-auto-mock": "0.18.0", "openapi-typescript": "7.6.0", "patch-package": "8.0.0", + "postcss": "8.5.6", "rollup-plugin-visualizer": "5.9.0", "sass": "1.68.0", "sinon": "15.0.2", "standardized-audio-context-mock": "9.6.32", "typescript": "5.3.3", - "unocss": "0.58.0", - "unplugin-vue-macros": "2.6.2", + "unplugin-vue-macros": "2.14.5", "utility-types": "3.10.0", "vite": "5.2.12", "vite-plugin-node-polyfills": "0.17.0", @@ -128,7 +128,7 @@ "vite-plugin-vue-devtools": "^7.5.2", "vitepress": "1.5.0", "vitest": "1.3.1", - "vue-tsc": "1.8.27", + "vue-tsc": "3.0.5", "workbox-core": "6.5.4", "workbox-precaching": "6.5.4", "workbox-routing": "6.5.4", diff --git a/front/postcss.config.js b/front/postcss.config.js new file mode 100644 index 000000000..c88c5b619 --- /dev/null +++ b/front/postcss.config.js @@ -0,0 +1,20 @@ +export default { + plugins: process.env.NODE_ENV === 'development' ? { + // Skip autoprefixer in development - modern dev browsers don't need prefixes + } : { + autoprefixer: { + overrideBrowserslist: [ + '> 1%', + 'last 2 versions', + 'not dead', + 'not ie 11', + 'not op_mini all', + 'chrome >= 87', + 'firefox >= 78', + 'safari >= 14', + 'edge >= 88', + 'ios >= 14' + ] + } + } +} diff --git a/front/src/components/Home.vue b/front/src/components/Home.vue index 4e10082a9..96fef6fdb 100644 --- a/front/src/components/Home.vue +++ b/front/src/components/Home.vue @@ -13,6 +13,13 @@ import { whenever } from '@vueuse/core' import { useI18n } from 'vue-i18n' import { useRouter } from 'vue-router' +import Header from '~/components/ui/Header.vue' +import Layout from '~/components/ui/Layout.vue' +import Card from '~/components/ui/Card.vue' +import Spacer from '~/components/ui/Spacer.vue' +import Section from '~/components/ui/Section.vue' +import Link from '~/components/ui/Link.vue' + const { t } = useI18n() const labels = computed(() => ({ title: t('components.Home.title') @@ -43,15 +50,11 @@ const stats = computed(() => { return { users, hours } }) -const headerStyle = computed(() => { - if (!banner.value) { - return '' - } - - return { - backgroundImage: `url(${store.getters['instance/absoluteUrl'](banner.value)})` - } -}) +const backgroundImage = computed(() => + banner.value + ? `url(${store.getters['instance/absoluteUrl'](banner.value)})` + : 'radial-gradient(circle at 80%, rgb(55, 122, 170), transparent), linear-gradient(135deg, rgb(40, 88, 125) 0%, rgb(64, 190, 220) 100%)' +) // TODO (wvffle): Check if needed const router = useRouter() @@ -62,243 +65,207 @@ whenever(() => store.state.auth.authenticated, () => { + + diff --git a/front/src/components/album/Widget.vue b/front/src/components/album/Widget.vue index e059df6e6..5f99fa4dd 100644 --- a/front/src/components/album/Widget.vue +++ b/front/src/components/album/Widget.vue @@ -67,7 +67,7 @@ const performSearch = () => { } watch( - [() => store.state.moderation.lastUpdate, page], + () => [store.state.moderation.lastUpdate, page.value], () => fetchData(), { immediate: true } ) diff --git a/front/src/components/audio/ChannelEntryCard.vue b/front/src/components/audio/ChannelEntryCard.vue index e2102cbaf..aec92994f 100644 --- a/front/src/components/audio/ChannelEntryCard.vue +++ b/front/src/components/audio/ChannelEntryCard.vue @@ -33,7 +33,7 @@ const duration = computed(() => props.entry.uploads.find(upload => upload.durati
(), { }) const { t } = useI18n() +const dataStore = useDataStore() const newValues = reactive({ name: props.object?.artist?.name ?? '', @@ -261,7 +263,7 @@ defineExpose({ :get="model => { newValues.tags = model.currents.map(({ label }) => label) }" :set="model => ({ currents: newValues.tags.map(tag => ({ type: 'custom' as const, label: tag })), - others: [].map(tag => ({ type: 'custom' as const, label: tag })) + others: dataStore.tags().value.map(({ name }) => ({ type: 'custom' as const, label: name })) })" :label="t('components.audio.ChannelForm.label.tags')" /> diff --git a/front/src/components/audio/PlayButton.vue b/front/src/components/audio/PlayButton.vue index b1debb5d3..79372c2cf 100644 --- a/front/src/components/audio/PlayButton.vue +++ b/front/src/components/audio/PlayButton.vue @@ -20,7 +20,7 @@ interface Props extends PlayOptionsProps { dropdownIconClasses?: string[] playIconClass?: string buttonClasses?: string[] - discrete?: boolean + discrete?: true dropdownOnly?: boolean iconOnly?: boolean playing?: boolean @@ -52,7 +52,6 @@ const props = withDefaults(defineProps(), { dropdownIconClasses: () => ['bi-caret-down-fill'], playIconClass: () => 'bi-play-fill', buttonClasses: () => ['button'], - discrete: () => false, dropdownOnly: () => false, iconOnly: () => false, isPlayable: () => false, diff --git a/front/src/components/audio/SearchBar.vue b/front/src/components/audio/SearchBar.vue index 4976a20ad..2ab1bf4fc 100644 --- a/front/src/components/audio/SearchBar.vue +++ b/front/src/components/audio/SearchBar.vue @@ -26,9 +26,6 @@ const router = useRouter() const query = ref() const enter = () => { - // TODO: Find out what jQuery version supports `search` - // jQuery(el.value).search('cancel query') - // Cancel any API search request to backend… return router.push(`/search?q=${query.value}&type=artists`) } @@ -38,108 +35,7 @@ const blur = () => { } onMounted(() => { - // TODO: Find out what jQuery version supports `search` - // jQuery(el.value).search({ - // type: 'category', - // minCharacters: 3, - // showNoResults: true, - // error: { - // // @ts-expect-error Semantic is broken - // noResultsHeader: t('components.audio.SearchBar.header.noResults'), - // noResults: t('components.audio.SearchBar.empty.noResults') - // }, - - // onSelect (result, response) { - // jQuery(el.value).search('set value', query.value) - // router.push(result.routerUrl) - // jQuery(el.value).search('hide results') - // return false - // }, - // onSearchQuery (value) { - // // query.value = value - // emit('search') - // }, - // apiSettings: { - // url: store.getters['instance/absoluteUrl']('api/v1/search?query={query}'), - // beforeXHR: function (xhrObject) { - // if (!store.state.auth.authenticated) { - // return xhrObject - // } - - // if (store.state.auth.oauth.accessToken) { - // xhrObject.setRequestHeader('Authorization', store.getters['auth/header']) - // } - - // return xhrObject - // }, - // onResponse: function (initialResponse) { - // const id = objectId.value - // const results: Partial> = {} - - // let resultsEmpty = true - // for (const category of categories.value) { - // results[category.code] = { - // name: category.name, - // results: [] - // } - - // if (category.code === 'federation' && id) { - // resultsEmpty = false - // results[category.code]?.results.push({ - // title: t('components.audio.SearchBar.link.fediverse'), - // routerUrl: { - // name: 'search', - // query: { id } - // } - // }) - // } - - // if (category.code === 'podcasts' && id) { - // resultsEmpty = false - // results[category.code]?.results.push({ - // title: t('components.audio.SearchBar.link.rss'), - // routerUrl: { - // name: 'search', - // query: { id, type: 'rss' } - // } - // }) - // } - - // if (category.code === 'more') { - // results[category.code]?.results.push({ - // title: t('components.audio.SearchBar.link.more'), - // routerUrl: { - // name: 'search', - // query: { type: 'artists', q: query.value } - // } - // }) - // } - - // if (isCategoryGuard(category)) { - // for (const result of initialResponse[category.code]) { - // resultsEmpty = false - // const id = category.getId(result) - // results[category.code]?.results.push({ - // title: category.getTitle(result), - // id, - // routerUrl: { - // name: category.route, - // params: { id } - // }, - // description: category.getDescription(result) - // }) - // } - // } - // } - - // return { - // results: resultsEmpty - // ? {} - // : results - // } - // } - // } - // }) + // Search functionality could be implemented here if needed }) diff --git a/front/src/components/audio/podcast/Table.vue b/front/src/components/audio/podcast/Table.vue index 6082e2c8b..fc4bab5a8 100644 --- a/front/src/components/audio/podcast/Table.vue +++ b/front/src/components/audio/podcast/Table.vue @@ -32,7 +32,7 @@ withDefaults(defineProps(), { total: 0 }) -const { page } = defineModels<{ page: number, }>() +const page = defineModel('page', { required: true })