Files
AstralRinth/apps/frontend/src/pages/settings/profile.vue
Cal H. 2aabcf36ee refactor: migrate to common eslint+prettier configs (#4168)
* refactor: migrate to common eslint+prettier configs

* fix: prettier frontend

* feat: config changes

* fix: lint issues

* fix: lint

* fix: type imports

* fix: cyclical import issue

* fix: lockfile

* fix: missing dep

* fix: switch to tabs

* fix: continue switch to tabs

* fix: rustfmt parity

* fix: moderation lint issue

* fix: lint issues

* fix: ui intl

* fix: lint issues

* Revert "fix: rustfmt parity"

This reverts commit cb99d2376c321d813d4b7fc7e2a213bb30a54711.

* feat: revert last rs
2025-08-14 20:48:38 +00:00

253 lines
6.3 KiB
Vue

<template>
<div>
<section class="card">
<h2 class="text-2xl">{{ formatMessage(messages.title) }}</h2>
<p class="mb-4">
<IntlFormatted :message-id="messages.description">
<template #docs-link="{ children }">
<a href="https://docs.modrinth.com/" target="_blank" class="text-link">
<component :is="() => children" />
</a>
</template>
</IntlFormatted>
</p>
<label>
<span class="label__title">{{ formatMessage(messages.profilePicture) }}</span>
</label>
<div class="avatar-changer">
<Avatar
:src="previewImage ? previewImage : avatarUrl"
size="md"
circle
:alt="auth.user.username"
/>
<div class="input-stack">
<FileInput
:max-size="262144"
:show-icon="true"
class="btn"
:prompt="formatMessage(commonMessages.uploadImageButton)"
accept="image/png,image/jpeg,image/gif,image/webp"
@change="showPreviewImage"
>
<UploadIcon />
</FileInput>
<Button v-if="avatarUrl !== null" :action="removePreviewImage">
<TrashIcon />
{{ formatMessage(commonMessages.removeImageButton) }}
</Button>
<Button
v-if="previewImage"
:action="
() => {
icon = null
previewImage = null
}
"
>
<UndoIcon />
{{ formatMessage(messages.profilePictureReset) }}
</Button>
</div>
</div>
<label for="username-field">
<span class="label__title">{{ formatMessage(messages.usernameTitle) }}</span>
<span class="label__description">
{{ formatMessage(messages.usernameDescription) }}
</span>
</label>
<input id="username-field" v-model="username" type="text" />
<label for="bio-field">
<span class="label__title">{{ formatMessage(messages.bioTitle) }}</span>
<span class="label__description">
{{ formatMessage(messages.bioDescription) }}
</span>
</label>
<textarea id="bio-field" v-model="bio" type="text" />
<div v-if="hasUnsavedChanges" class="input-group">
<Button color="primary" :action="() => saveChanges()">
<SaveIcon /> {{ formatMessage(commonMessages.saveChangesButton) }}
</Button>
<Button :action="() => cancel()">
<XIcon /> {{ formatMessage(commonMessages.cancelButton) }}
</Button>
</div>
<div v-else class="input-group">
<Button disabled color="primary" :action="() => saveChanges()">
<SaveIcon />
{{
saved
? formatMessage(commonMessages.changesSavedLabel)
: formatMessage(commonMessages.saveChangesButton)
}}
</Button>
<Button :link="`/user/${auth.user.username}`">
<UserIcon /> {{ formatMessage(commonMessages.visitYourProfile) }}
</Button>
</div>
</section>
</div>
</template>
<script setup>
import { SaveIcon, TrashIcon, UndoIcon, UploadIcon, UserIcon, XIcon } from '@modrinth/assets'
import { Avatar, Button, commonMessages, FileInput, injectNotificationManager } from '@modrinth/ui'
const { addNotification } = injectNotificationManager()
const { formatMessage } = useVIntl()
useHead({
title: 'Profile settings - Modrinth',
})
definePageMeta({
middleware: 'auth',
})
const messages = defineMessages({
title: {
id: 'settings.profile.profile-info',
defaultMessage: 'Profile information',
},
description: {
id: 'settings.profile.description',
defaultMessage:
'Your profile information is publicly viewable on Modrinth and through the <docs-link>Modrinth API</docs-link>.',
},
profilePicture: {
id: 'settings.profile.profile-picture.title',
defaultMessage: 'Profile picture',
},
profilePictureReset: {
id: 'settings.profile.profile-picture.reset',
defaultMessage: 'Reset',
},
usernameTitle: {
id: 'settings.profile.username.title',
defaultMessage: 'Username',
},
usernameDescription: {
id: 'settings.profile.username.description',
defaultMessage: 'A unique case-insensitive name to identify your profile.',
},
bioTitle: {
id: 'settings.profile.bio.title',
defaultMessage: 'Bio',
},
bioDescription: {
id: 'settings.profile.bio.description',
defaultMessage: 'A short description to tell everyone a little bit about you.',
},
})
const auth = await useAuth()
const username = ref(auth.value.user.username)
const bio = ref(auth.value.user.bio)
const avatarUrl = ref(auth.value.user.avatar_url)
const icon = shallowRef(null)
const previewImage = shallowRef(null)
const pendingAvatarDeletion = ref(false)
const saved = ref(false)
const hasUnsavedChanges = computed(
() =>
username.value !== auth.value.user.username ||
bio.value !== auth.value.user.bio ||
previewImage.value,
)
function showPreviewImage(files) {
const reader = new FileReader()
icon.value = files[0]
reader.readAsDataURL(icon.value)
reader.onload = (event) => {
previewImage.value = event.target.result
}
}
function removePreviewImage() {
pendingAvatarDeletion.value = true
previewImage.value = 'https://cdn.modrinth.com/placeholder.png'
}
function cancel() {
icon.value = null
previewImage.value = null
pendingAvatarDeletion.value = false
username.value = auth.value.user.username
bio.value = auth.value.user.bio
}
async function saveChanges() {
startLoading()
try {
if (pendingAvatarDeletion.value) {
await useBaseFetch(`user/${auth.value.user.id}/icon`, {
method: 'DELETE',
})
pendingAvatarDeletion.value = false
previewImage.value = null
}
if (icon.value) {
await useBaseFetch(
`user/${auth.value.user.id}/icon?ext=${
icon.value.type.split('/')[icon.value.type.split('/').length - 1]
}`,
{
method: 'PATCH',
body: icon.value,
},
)
icon.value = null
previewImage.value = null
}
const body = {}
if (auth.value.user.username !== username.value) {
body.username = username.value
}
if (auth.value.user.bio !== bio.value) {
body.bio = bio.value
}
await useBaseFetch(`user/${auth.value.user.id}`, {
method: 'PATCH',
body,
})
await useAuth(auth.value.token)
avatarUrl.value = auth.value.user.avatar_url
saved.value = true
} catch (err) {
addNotification({
title: 'An error occurred',
text: err
? err.data
? err.data.description
? err.data.description
: err.data
: err
: 'aaaaahhh',
type: 'error',
})
}
stopLoading()
}
</script>
<style lang="scss" scoped>
.avatar-changer {
display: flex;
gap: var(--gap-lg);
margin-top: var(--gap-md);
}
textarea {
height: 6rem;
width: 40rem;
margin-bottom: var(--gap-lg);
}
</style>