mirror of
https://github.com/modrinth/code.git
synced 2026-08-26 17:44:50 +00:00
* feat: refactor button components
* fix: qa
* fix: storybook
* a11y: pass
* fix: build
* fix: rename default ->md
* fix: lint
* docs: buttons.md
* refactor part 1
* refactor part 2
* fix: undo refactor for fresh restart
* refactor
* Revert "refactor"
This reverts commit 96d65902d7.
* refactor: part 1
* fix: qa
* fix: qa
* fix: qa
* fix: qa
* fix: qa
* fix: qa
* fix: qa
* fix: qa
* fix: remove text-contrast
* fix: qa
* fix: v-tooltip
* fix: lint
* fix: split broken
* fix: passkey qa
* fix: qa
* fix: qa
* fix: prepr
* fix: splitbutton
* improve button group seam
---------
Signed-off-by: Calum H. <calum@modrinth.com>
Co-authored-by: Prospector <6166773+Prospector@users.noreply.github.com>
318 lines
8.9 KiB
Vue
318 lines
8.9 KiB
Vue
<template>
|
|
<div v-if="subtleLauncherRedirectUri">
|
|
<iframe
|
|
:src="subtleLauncherRedirectUri"
|
|
class="fixed left-0 top-0 z-[9999] m-0 h-full w-full border-0 p-0"
|
|
></iframe>
|
|
</div>
|
|
<div
|
|
v-else
|
|
class="universal-card mx-auto flex w-full max-w-[27rem] flex-col gap-6 border border-solid border-surface-5 !p-6"
|
|
>
|
|
<template v-if="flow && !subtleLauncherRedirectUri">
|
|
<div class="flex flex-col items-end gap-4">
|
|
<div class="flex flex-col gap-1.5">
|
|
<label for="two-factor-code">
|
|
<span class="label__title">{{ formatMessage(messages.twoFactorCodeLabel) }}</span>
|
|
<span class="label__description">
|
|
{{ formatMessage(messages.twoFactorCodeLabelDescription) }}
|
|
</span>
|
|
</label>
|
|
<StyledInput
|
|
id="two-factor-code"
|
|
v-model="twoFactorCodeModel"
|
|
:maxlength="11"
|
|
inputmode="numeric"
|
|
:placeholder="formatMessage(messages.twoFactorCodeInputPlaceholder)"
|
|
autocomplete="one-time-code"
|
|
@keyup.enter="onTwoFactorSignIn()"
|
|
/>
|
|
</div>
|
|
<Button type="colored" color="brand" @click="onTwoFactorSignIn()">
|
|
{{ formatMessage(commonMessages.signInButton) }} <RightArrowIcon />
|
|
</Button>
|
|
</div>
|
|
</template>
|
|
<template v-else>
|
|
<div class="flex flex-col gap-5">
|
|
<div class="text-center text-2xl font-semibold text-contrast">
|
|
{{ formatMessage(messages.signInWithLabel) }}
|
|
</div>
|
|
|
|
<section class="grid grid-cols-1 gap-2.5 sm:grid-cols-2">
|
|
<ButtonLink
|
|
v-for="provider in oauthProviders"
|
|
:key="provider.id"
|
|
class="relative w-full !justify-center overflow-visible"
|
|
:class="{
|
|
'!border !border-[var(--color-green)]': lastSignInProvider === provider.id,
|
|
}"
|
|
:href="getAuthUrl(provider.id, redirectTarget)"
|
|
:aria-label="formatMessage(messages.continueWithProvider, { provider: provider.name })"
|
|
@click="onOAuthProviderClick(provider.id)"
|
|
>
|
|
<component :is="provider.icon" />
|
|
<span>{{ provider.name }}</span>
|
|
<span
|
|
v-if="lastSignInProvider === provider.id"
|
|
class="oauth-provider-last-sign-in-badge"
|
|
>
|
|
{{ formatMessage(messages.lastSignInLabel) }}
|
|
</span>
|
|
</ButtonLink>
|
|
<Button
|
|
class="relative !w-full !justify-center overflow-visible sm:col-span-2"
|
|
:class="{ '!border !border-[var(--color-green)]': lastSignInProvider === 'passkey' }"
|
|
role="button"
|
|
tabindex="0"
|
|
@click="onPasskeySignIn"
|
|
@keydown.enter="onPasskeySignIn"
|
|
>
|
|
<UserKeyIcon />
|
|
<span class="ml-1">{{ formatMessage(messages.continueWithPasskey) }}</span>
|
|
<span v-if="lastSignInProvider === 'passkey'" class="oauth-provider-last-sign-in-badge">
|
|
{{ formatMessage(messages.lastSignInLabel) }}
|
|
</span>
|
|
</Button>
|
|
</section>
|
|
|
|
<div class="h-px w-full bg-surface-5"></div>
|
|
|
|
<section class="mx-auto flex w-full flex-col gap-2.5">
|
|
<label for="email" hidden>{{ formatMessage(commonMessages.emailUsernameLabel) }}</label>
|
|
<StyledInput
|
|
id="email"
|
|
v-model="emailModel"
|
|
:icon="MailIcon"
|
|
type="text"
|
|
inputmode="email"
|
|
autocomplete="username"
|
|
:placeholder="formatMessage(commonMessages.emailUsernameLabel)"
|
|
wrapper-class="w-full"
|
|
/>
|
|
|
|
<label for="password" hidden>{{ formatMessage(commonMessages.passwordLabel) }}</label>
|
|
<StyledInput
|
|
id="password"
|
|
v-model="passwordModel"
|
|
:icon="KeyIcon"
|
|
type="password"
|
|
autocomplete="current-password"
|
|
:placeholder="formatMessage(commonMessages.passwordLabel)"
|
|
wrapper-class="w-full"
|
|
/>
|
|
|
|
<HCaptcha
|
|
v-if="globals?.captcha_enabled && emailModel && passwordModel"
|
|
:ref="onSetCaptchaRef"
|
|
v-model="tokenModel"
|
|
/>
|
|
|
|
<Button
|
|
type="colored"
|
|
color="brand"
|
|
class="!w-full"
|
|
:disabled="globals?.captcha_enabled ? !tokenModel : false"
|
|
@click="onPasswordSignIn()"
|
|
>
|
|
{{ formatMessage(messages.continueWithEmail) }} <RightArrowIcon />
|
|
</Button>
|
|
|
|
<div class="flex flex-wrap items-center justify-center gap-2.5 !text-base">
|
|
<NuxtLink
|
|
class="text-link"
|
|
:to="{
|
|
path: '/auth/reset-password',
|
|
query: routeQuery,
|
|
}"
|
|
>
|
|
{{ formatMessage(messages.forgotPasswordLabel) }}
|
|
</NuxtLink>
|
|
<div class="h-1.5 w-1.5 rounded-full bg-surface-5" />
|
|
<NuxtLink
|
|
class="inline text-link"
|
|
:to="{
|
|
path: '/auth/sign-up',
|
|
query: routeQuery,
|
|
}"
|
|
>
|
|
{{ formatMessage(messages.createAccountLabel) }}
|
|
</NuxtLink>
|
|
</div>
|
|
</section>
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import {
|
|
DiscordColorIcon,
|
|
GitHubColorIcon,
|
|
GitLabColorIcon,
|
|
GoogleColorIcon,
|
|
KeyIcon,
|
|
MailIcon,
|
|
MicrosoftColorIcon,
|
|
RightArrowIcon,
|
|
SteamColorIcon,
|
|
UserKeyIcon,
|
|
} from '@modrinth/assets'
|
|
import {
|
|
Button,
|
|
ButtonLink,
|
|
commonMessages,
|
|
defineMessages,
|
|
StyledInput,
|
|
useVIntl,
|
|
} from '@modrinth/ui'
|
|
import { useStorage } from '@vueuse/core'
|
|
import { computed } from 'vue'
|
|
import type { LocationQuery } from 'vue-router'
|
|
|
|
import HCaptcha from '@/components/ui/auth/HCaptcha.vue'
|
|
import {
|
|
getAuthUrl,
|
|
LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
|
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
|
} from '@/composables/auth.ts'
|
|
|
|
const oauthProviders = [
|
|
{ id: 'discord', name: 'Discord', icon: DiscordColorIcon },
|
|
{ id: 'github', name: 'GitHub', icon: GitHubColorIcon },
|
|
{ id: 'microsoft', name: 'Microsoft', icon: MicrosoftColorIcon },
|
|
{ id: 'google', name: 'Google', icon: GoogleColorIcon },
|
|
{ id: 'steam', name: 'Steam', icon: SteamColorIcon },
|
|
{ id: 'gitlab', name: 'GitLab', icon: GitLabColorIcon },
|
|
] as const
|
|
|
|
type AuthProvider = (typeof oauthProviders)[number]['id'] | 'passkey'
|
|
|
|
interface AuthGlobals {
|
|
captcha_enabled?: boolean
|
|
[key: string]: unknown
|
|
}
|
|
|
|
interface Props {
|
|
subtleLauncherRedirectUri?: string
|
|
flow?: string
|
|
redirectTarget?: string
|
|
routeQuery?: LocationQuery
|
|
globals?: AuthGlobals | null
|
|
onPasswordSignIn?: () => void
|
|
onTwoFactorSignIn?: () => void
|
|
onPasskeySignIn?: () => void
|
|
onSetCaptchaRef?: ((captchaRef: unknown) => void) | undefined
|
|
}
|
|
|
|
const {
|
|
subtleLauncherRedirectUri = '',
|
|
flow = '',
|
|
redirectTarget = '',
|
|
routeQuery = {},
|
|
globals = null,
|
|
onPasswordSignIn = () => {},
|
|
onTwoFactorSignIn = () => {},
|
|
onPasskeySignIn = () => {},
|
|
onSetCaptchaRef = undefined,
|
|
} = defineProps<Props>()
|
|
|
|
const emailModel = defineModel<string>('email', { default: '' })
|
|
const passwordModel = defineModel<string>('password', { default: '' })
|
|
const tokenModel = defineModel<string>('token', { default: '' })
|
|
const twoFactorCodeModel = defineModel<string>('twoFactorCode', { default: '' })
|
|
|
|
const lastSignInOAuthProvider = useStorage<AuthProvider | null>(
|
|
LAST_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
|
null,
|
|
undefined,
|
|
{ initOnMounted: true },
|
|
)
|
|
const pendingSignInOAuthProvider = useStorage<AuthProvider | null>(
|
|
PENDING_SIGN_IN_OAUTH_PROVIDER_STORAGE_KEY,
|
|
null,
|
|
undefined,
|
|
{ initOnMounted: true },
|
|
)
|
|
const lastSignInProvider = computed(() => lastSignInOAuthProvider.value)
|
|
const onOAuthProviderClick = (provider: AuthProvider) => {
|
|
pendingSignInOAuthProvider.value = provider
|
|
}
|
|
|
|
const { formatMessage } = useVIntl()
|
|
|
|
const messages = defineMessages({
|
|
forgotPasswordLabel: {
|
|
id: 'auth.sign-in.forgot-password',
|
|
defaultMessage: 'Forgot password',
|
|
},
|
|
noAccountLabel: {
|
|
id: 'auth.sign-in.no-account',
|
|
defaultMessage: "Don't have an account?",
|
|
},
|
|
createAccountLabel: {
|
|
id: 'auth.sign-in.create-account',
|
|
defaultMessage: 'Sign up',
|
|
},
|
|
signInWithLabel: {
|
|
id: 'auth.sign-in.sign-in-with',
|
|
defaultMessage: 'Sign into Modrinth',
|
|
},
|
|
twoFactorCodeInputPlaceholder: {
|
|
id: 'auth.sign-in.2fa.placeholder',
|
|
defaultMessage: 'Enter code...',
|
|
},
|
|
twoFactorCodeLabel: {
|
|
id: 'auth.sign-in.2fa.label',
|
|
defaultMessage: 'Two-factor authentication',
|
|
},
|
|
twoFactorCodeLabelDescription: {
|
|
id: 'auth.sign-in.2fa.description',
|
|
defaultMessage:
|
|
'Enter the 6-digit code from your authenticator app, or one of your backup codes.',
|
|
},
|
|
continueWithProvider: {
|
|
id: 'auth.continue-with-provider',
|
|
defaultMessage: 'Continue with {provider}',
|
|
},
|
|
continueWithEmail: {
|
|
id: 'auth.sign-in.continue-with-email',
|
|
defaultMessage: 'Continue with Email',
|
|
},
|
|
lastSignInLabel: {
|
|
id: 'auth.sign-in.last-sign-in',
|
|
defaultMessage: 'Last used',
|
|
},
|
|
continueWithPasskey: {
|
|
id: 'auth.sign-in.continue-with-passkey',
|
|
defaultMessage: 'Continue with passkey',
|
|
},
|
|
})
|
|
</script>
|
|
|
|
<style scoped lang="scss">
|
|
.oauth-provider-last-sign-in-badge {
|
|
position: absolute;
|
|
top: -0.75rem;
|
|
right: 0.25rem;
|
|
z-index: 1;
|
|
border-radius: 9999px;
|
|
background-color: var(--surface-3);
|
|
color: var(--color-green);
|
|
border: 1px solid var(--color-green);
|
|
padding: 0.125rem 0.375rem;
|
|
font-size: 0.75rem;
|
|
font-weight: 600;
|
|
line-height: 1;
|
|
pointer-events: none;
|
|
}
|
|
.oauth-provider-last-sign-in-badge::before {
|
|
content: '';
|
|
inset: 0;
|
|
border-radius: inherit;
|
|
background-color: var(--color-green-highlight);
|
|
position: absolute;
|
|
z-index: 0;
|
|
}
|
|
</style>
|