diff --git a/apps/app-frontend/package.json b/apps/app-frontend/package.json index d892df69f6..e44564ae98 100644 --- a/apps/app-frontend/package.json +++ b/apps/app-frontend/package.json @@ -40,7 +40,6 @@ "vite-svg-loader": "^5.1.0", "vue": "^3.5.13", "vue-i18n": "^10.0.0", - "vue-multiselect": "3.0.0", "vue-router": "^4.6.0", "vue-virtual-scroller": "v2.0.0-beta.8" }, diff --git a/apps/app-frontend/src/App.vue b/apps/app-frontend/src/App.vue index f7a029b5c3..c28a7b0d0e 100644 --- a/apps/app-frontend/src/App.vue +++ b/apps/app-frontend/src/App.vue @@ -1597,4 +1597,3 @@ provideAppUpdateDownloadProgress(appUpdateDownload) } } - diff --git a/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue b/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue index 7e8c6f7457..24f493ee03 100644 --- a/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue +++ b/apps/app-frontend/src/components/ui/install_flow/IncompatibilityWarningModal.vue @@ -17,31 +17,17 @@ {{ instance?.loader }} {{ instance?.game_version }} - - - {{ selectedVersion?.name }} ({{ - selectedVersion?.loaders - .map((name) => formatLoader(formatMessage, name)) - .join(', ') - }} - - {{ selectedVersion?.game_versions.join(', ') }}) - + {{ selectedVersionLabel }} @@ -59,9 +45,8 @@ + + diff --git a/packages/ui/src/components/base/index.ts b/packages/ui/src/components/base/index.ts index 85251b0e15..2b3506366a 100644 --- a/packages/ui/src/components/base/index.ts +++ b/packages/ui/src/components/base/index.ts @@ -41,6 +41,8 @@ export { default as JoinedButtons } from './JoinedButtons.vue' export { default as LoadingIndicator } from './LoadingIndicator.vue' export { default as ManySelect } from './ManySelect.vue' export { default as MarkdownEditor } from './MarkdownEditor.vue' +export type { MultiSelectOption } from './MultiSelect.vue' +export { default as MultiSelect } from './MultiSelect.vue' export type { MaybeCtxFn, StageButtonConfig, StageConfigInput } from './MultiStageModal.vue' export { default as MultiStageModal, resolveCtxFn } from './MultiStageModal.vue' export { default as OptionGroup } from './OptionGroup.vue' diff --git a/packages/ui/src/components/billing/PurchaseModal.vue b/packages/ui/src/components/billing/PurchaseModal.vue index 95093b863d..d1753d10a0 100644 --- a/packages/ui/src/components/billing/PurchaseModal.vue +++ b/packages/ui/src/components/billing/PurchaseModal.vue @@ -323,92 +323,15 @@

Pay for it with

- - - - - - + />

By clicking "Subscribe", you are purchasing a recurring subscription. @@ -546,13 +469,13 @@ import { import { calculateSavings, createStripeElements, getCurrency } from '@modrinth/utils' import dayjs from 'dayjs' import { computed, nextTick, reactive, ref, watch } from 'vue' -import { Multiselect } from 'vue-multiselect' import { useVIntl } from '../../composables/i18n' import { useFormatDateTime, useFormatPrice } from '../../composables/index.ts' import { paymentMethodMessages } from '../../utils/common-messages' import Admonition from '../base/Admonition.vue' import Checkbox from '../base/Checkbox.vue' +import Combobox from '../base/Combobox.vue' import Slider from '../base/Slider.vue' import StyledInput from '../base/StyledInput.vue' import AnimatedLogo from '../brand/AnimatedLogo.vue' @@ -784,6 +707,67 @@ const selectablePaymentMethods = computed(() => { return values }) +function formatPaymentMethodLabel(paymentMethod) { + if (!paymentMethod) { + return formatMessage(paymentMethodMessages.unknown) + } + + if (paymentMethod.id === 'new') { + return 'Add payment method' + } + + if (paymentMethod.type === 'card') { + return formatMessage(paymentMethodMessages.paymentMethodCardDisplay, { + card_brand: + formatMessage(paymentMethodMessages[paymentMethod.card?.brand]) ?? + formatMessage(paymentMethodMessages.unknown), + last_four: paymentMethod.card?.last4 ?? '****', + }) + } + + const typeLabel = + formatMessage(paymentMethodMessages[paymentMethod.type]) ?? + formatMessage(paymentMethodMessages.unknown) + let suffix = '' + + if (paymentMethod.type === 'cashapp' && paymentMethod.cashapp?.cashtag) { + suffix = ` (${paymentMethod.cashapp.cashtag})` + } else if (paymentMethod.type === 'paypal' && paymentMethod.paypal?.payer_email) { + suffix = ` (${paymentMethod.paypal.payer_email})` + } + + return `${typeLabel}${suffix}` +} + +function getPaymentMethodIcon(paymentMethod) { + if (paymentMethod.id === 'new') return PlusIcon + if (paymentMethod.type === 'card') return CardIcon + if (paymentMethod.type === 'cashapp') return CurrencyIcon + if (paymentMethod.type === 'paypal') return PayPalIcon + return undefined +} + +const selectablePaymentMethodOptions = computed(() => + selectablePaymentMethods.value.map((paymentMethod) => ({ + value: paymentMethod.id, + label: formatPaymentMethodLabel(paymentMethod), + icon: getPaymentMethodIcon(paymentMethod), + })), +) + +const selectedPaymentMethodId = computed({ + get: () => selectedPaymentMethod.value?.id ?? null, + set: (value) => { + if (!value) return + + const paymentMethod = selectablePaymentMethods.value.find((method) => method.id === value) + if (paymentMethod) { + selectedPaymentMethod.value = paymentMethod + void selectPaymentMethod(paymentMethod) + } + }, +}) + const primaryPaymentMethodId = computed(() => { if ( props.customer && diff --git a/packages/ui/src/locales/en-US/index.json b/packages/ui/src/locales/en-US/index.json index 4200211520..73658b9390 100644 --- a/packages/ui/src/locales/en-US/index.json +++ b/packages/ui/src/locales/en-US/index.json @@ -978,7 +978,7 @@ "defaultMessage": "Singleplayer" }, "label.sort-by": { - "defaultMessage": "Sort by" + "defaultMessage": "Sort by: " }, "label.success": { "defaultMessage": "Success" diff --git a/packages/ui/src/stories/base/MultiSelect.stories.ts b/packages/ui/src/stories/base/MultiSelect.stories.ts new file mode 100644 index 0000000000..6d7b59f4b0 --- /dev/null +++ b/packages/ui/src/stories/base/MultiSelect.stories.ts @@ -0,0 +1,95 @@ +import type { Meta, StoryObj } from '@storybook/vue3-vite' +import { ref } from 'vue' + +import MultiSelect from '../../components/base/MultiSelect.vue' + +const meta = { + title: 'Base/MultiSelect', + // @ts-ignore - error comes from generically typed component + component: MultiSelect, + render: (args) => ({ + components: { MultiSelect }, + setup() { + const selected = ref(args.modelValue) + return { args, selected } + }, + template: /*html*/ ` +

+ +
+ `, + }), +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + options: [ + { value: 'en', label: 'English' }, + { value: 'es', label: 'Spanish' }, + { value: 'fr', label: 'French' }, + { value: 'de', label: 'German' }, + { value: 'zh-CN', label: 'Chinese (Simplified)' }, + { value: 'ko', label: 'Korean' }, + { value: 'ja', label: 'Japanese' }, + { value: 'pt', label: 'Portuguese' }, + { value: 'ru', label: 'Russian' }, + { value: 'it', label: 'Italian' }, + { value: 'ar', label: 'Arabic' }, + ], + modelValue: ['en', 'es', 'fr', 'zh-CN'], + placeholder: 'Select languages', + }, +} + +export const WithSearch: Story = { + args: { + ...Default.args, + searchable: true, + searchPlaceholder: 'Search versions', + }, +} + +export const WithSelectAll: Story = { + args: { + ...Default.args, + searchable: true, + includeSelectAllOption: true, + searchPlaceholder: 'Search versions', + }, +} + +export const ManySelected: Story = { + args: { + ...Default.args, + modelValue: ['en', 'es', 'fr', 'zh-CN', 'ko', 'ja', 'pt', 'ru', 'it', 'ar', 'de'], + searchable: true, + includeSelectAllOption: true, + }, +} + +export const TwoTagRows: Story = { + args: { + ...ManySelected.args, + maxTagRows: 2, + }, +} + +export const NoOptions: Story = { + args: { + ...Default.args, + options: [], + modelValue: [], + searchable: true, + noOptionsMessage: 'No options available', + }, +} + +export const Empty: Story = { + args: { + ...Default.args, + modelValue: [], + }, +} diff --git a/packages/ui/src/utils/common-messages.ts b/packages/ui/src/utils/common-messages.ts index a4d052db1c..a98c3275ef 100644 --- a/packages/ui/src/utils/common-messages.ts +++ b/packages/ui/src/utils/common-messages.ts @@ -291,7 +291,7 @@ export const commonMessages = defineMessages({ }, sortByLabel: { id: 'label.sort-by', - defaultMessage: 'Sort by', + defaultMessage: 'Sort by: ', }, stopButton: { id: 'button.stop', diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7a10e78768..887a2cc10e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -143,9 +143,6 @@ importers: vue-i18n: specifier: ^10.0.0 version: 10.0.8(vue@3.5.27(typescript@5.9.3)) - vue-multiselect: - specifier: 3.0.0 - version: 3.0.0 vue-router: specifier: ^4.6.0 version: 4.6.4(vue@3.5.27(typescript@5.9.3)) @@ -353,9 +350,6 @@ importers: vue-confetti-explosion: specifier: ^1.0.2 version: 1.0.2(vue@3.5.27(typescript@5.9.3)) - vue-multiselect: - specifier: 3.0.0-alpha.2 - version: 3.0.0-alpha.2 vue-typed-virtual-list: specifier: ^1.0.10 version: 1.0.10(vue@3.5.27(typescript@5.9.3)) @@ -676,9 +670,6 @@ importers: vue-i18n: specifier: ^10.0.0 version: 10.0.8(vue@3.5.27(typescript@5.9.3)) - vue-multiselect: - specifier: 3.0.0 - version: 3.0.0 vue-select: specifier: 4.0.0-beta.6 version: 4.0.0-beta.6(vue@3.5.27(typescript@5.9.3)) @@ -9467,14 +9458,6 @@ packages: peerDependencies: vue: '>=2' - vue-multiselect@3.0.0: - resolution: {integrity: sha512-uupKdINgz7j83lQToCL7KkgQQxvG43el++hsR39YT9pCe1DwzUGmKzPxjVP6rqskXed5P6DtUASYAlCliW740Q==} - engines: {node: '>= 14.18.1', npm: '>= 6.14.15'} - - vue-multiselect@3.0.0-alpha.2: - resolution: {integrity: sha512-Xp9fGJECns45v+v8jXbCIsAkCybYkEg0lNwr7Z6HDUSMyx2TEIK2giipPE+qXiShEc1Ipn+ZtttH2iq9hwXP4Q==} - engines: {node: '>= 4.0.0', npm: '>= 3.0.0'} - vue-observe-visibility@2.0.0-alpha.1: resolution: {integrity: sha512-flFbp/gs9pZniXR6fans8smv1kDScJ8RS7rEpMjhVabiKeq7Qz3D9+eGsypncjfIyyU84saU88XZ0zjbD6Gq/g==} peerDependencies: @@ -19605,10 +19588,6 @@ snapshots: dependencies: vue: 3.5.27(typescript@5.9.3) - vue-multiselect@3.0.0: {} - - vue-multiselect@3.0.0-alpha.2: {} - vue-observe-visibility@2.0.0-alpha.1(vue@3.5.27(typescript@5.9.3)): dependencies: vue: 3.5.27(typescript@5.9.3)