feat(ui) [WIP] User profile page

This commit is contained in:
ArneBo 2025-01-05 15:47:43 +01:00 committed by upsiflu
parent 0d5d3bbba1
commit 601cc3a663
5 changed files with 193 additions and 151 deletions

View File

@ -261,10 +261,6 @@ const changeEmail = async () => {
isChangingEmail.value = false isChangingEmail.value = false
} }
onMounted(() => {
$('select.dropdown').dropdown()
})
fetchApps() fetchApps()
fetchOwnedApps() fetchOwnedApps()
</script> </script>

View File

@ -6,6 +6,9 @@ import { useI18n } from 'vue-i18n'
import useLogger from '~/composables/useLogger' import useLogger from '~/composables/useLogger'
import Textarea from '~/components/ui/Textarea.vue'
import Button from '~/components/ui/Button.vue'
interface Events { interface Events {
(e: 'update:modelValue', value: string): void (e: 'update:modelValue', value: string): void
} }
@ -83,22 +86,20 @@ onMounted(async () => {
</script> </script>
<template> <template>
<div class="content-form ui segments"> <Button
<div class="ui segment">
<div class="ui tiny secondary pointing menu">
<button
:class="[{active: !isPreviewing}, 'item']"
@click.prevent="isPreviewing = false" @click.prevent="isPreviewing = false"
:class="[{active: !isPreviewing}, 'item']"
title="write"
> >
{{ t('components.common.ContentForm.button.write') }} {{ t('components.common.ContentForm.button.write') }}
</button> </Button>
<button <Button
:class="[{active: isPreviewing}, 'item']"
@click.prevent="isPreviewing = true" @click.prevent="isPreviewing = true"
:class="[{active: isPreviewing}, 'item']"
title="preview"
> >
{{ t('components.common.ContentForm.button.preview') }} {{ t('components.common.ContentForm.button.preview') }}
</button> </Button>
</div>
<template v-if="isPreviewing"> <template v-if="isPreviewing">
<div <div
v-if="isLoadingPreview" v-if="isLoadingPreview"
@ -121,17 +122,14 @@ onMounted(async () => {
</template> </template>
<template v-else> <template v-else>
<div class="ui transparent input"> <div class="ui transparent input">
<textarea <Textarea
ref="textarea" ref="textarea"
v-model="value" v-model="value"
:required="required" :required="required"
:placeholder="labels.placeholder" :placeholder="labels.placeholder"
/> />
</div> </div>
<div class="ui very small hidden divider" />
</template> </template>
</div>
<div class="ui bottom attached segment">
<span <span
v-if="charLimit" v-if="charLimit"
:class="['right', 'floated', {'ui danger text': remainingChars < 0}]" :class="['right', 'floated', {'ui danger text': remainingChars < 0}]"
@ -141,6 +139,4 @@ onMounted(async () => {
<p> <p>
{{ t('components.common.ContentForm.help.markdown') }} {{ t('components.common.ContentForm.help.markdown') }}
</p> </p>
</div>
</div>
</template> </template>

View File

@ -8,6 +8,9 @@ import { useI18n } from 'vue-i18n'
import axios from 'axios' import axios from 'axios'
import clip from 'text-clipper' import clip from 'text-clipper'
import Button from '~/components/ui/Button.vue'
import Alert from '~/components/ui/Alert.vue'
interface Events { interface Events {
(e: 'updated', data: unknown): void (e: 'updated', data: unknown): void
} }
@ -88,9 +91,10 @@ const submit = async () => {
<template v-if="content && !isUpdating"> <template v-if="content && !isUpdating">
<sanitized-html :html="html" /> <sanitized-html :html="html" />
<template v-if="isTruncated"> <template v-if="isTruncated">
<div class="ui small hidden divider" />
<a <a
v-if="showMore === false" v-if="showMore === false"
class="more"
style="float: right; margin-top: -32px; color: var(--fw-primary);"
href="" href=""
@click.stop.prevent="showMore = true" @click.stop.prevent="showMore = true"
> >
@ -98,6 +102,8 @@ const submit = async () => {
</a> </a>
<a <a
v-else v-else
class="more"
style="float: right; margin-top: -32px; color: var(--fw-primary);"
href="" href=""
@click.stop.prevent="showMore = false" @click.stop.prevent="showMore = false"
> >
@ -109,28 +115,23 @@ const submit = async () => {
{{ t('components.common.RenderedDescription.empty.noDescription') }} {{ t('components.common.RenderedDescription.empty.noDescription') }}
</p> </p>
<template v-if="!isUpdating && canUpdate && updateUrl"> <template v-if="!isUpdating && canUpdate && updateUrl">
<div class="ui hidden divider" /> <Button
<span
role="button" role="button"
icon="bi-pencil"
@click="isUpdating = true" @click="isUpdating = true"
> >
<i class="pencil icon" />
{{ t('components.common.RenderedDescription.button.edit') }} {{ t('components.common.RenderedDescription.button.edit') }}
</span> </Button>
</template> </template>
<form <form
v-if="isUpdating" v-if="isUpdating"
class="ui form"
@submit.prevent="submit()" @submit.prevent="submit()"
> >
<div <Alert red
v-if="errors.length > 0" v-if="errors.length > 0"
title="{{ t('components.common.RenderedDescription.header.failure') }}"
role="alert" role="alert"
class="ui negative message"
> >
<h4 class="header">
{{ t('components.common.RenderedDescription.header.failure') }}
</h4>
<ul class="list"> <ul class="list">
<li <li
v-for="(error, key) in errors" v-for="(error, key) in errors"
@ -139,25 +140,28 @@ const submit = async () => {
{{ error }} {{ error }}
</li> </li>
</ul> </ul>
</div> </Alert>
<content-form <content-form
v-model="text" v-model="text"
:autofocus="true" :autofocus="true"
/> />
<a <Button
class="left floated" class="left floated"
@click.prevent="isUpdating = false" @click.prevent="isUpdating = false"
solid
secondary
> >
{{ t('components.common.RenderedDescription.button.cancel') }} {{ t('components.common.RenderedDescription.button.cancel') }}
</a> </Button>
<button <Button
:class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']" :class="['ui', {'loading': isLoading}, 'right', 'floated', 'button']"
type="submit" type="submit"
:disabled="isLoading" :disabled="isLoading"
solid
primary
> >
{{ t('components.common.RenderedDescription.button.update') }} {{ t('components.common.RenderedDescription.button.update') }}
</button> </Button>
<div class="ui clearing hidden divider" />
</form> </form>
</div> </div>
</template> </template>

View File

@ -64,6 +64,11 @@ onMounted(() => {
</template> </template>
<style module lang="scss"> <style module lang="scss">
.force-underline {
text-decoration: underline;
}
.link { .link {
// Layout // Layout

View File

@ -12,7 +12,11 @@ import useErrorHandler from '~/composables/useErrorHandler'
import useReport from '~/composables/moderation/useReport' import useReport from '~/composables/moderation/useReport'
import Layout from '~/components/ui/Layout.vue' import Layout from '~/components/ui/Layout.vue'
import Section from '~/components/ui/layout/Section.vue'
import Spacer from '~/components/ui/layout/Spacer.vue'
import Link from '~/components/ui/Link.vue' import Link from '~/components/ui/Link.vue'
import Tabs from '~/components/ui/Tabs.vue'
import Tab from '~/components/ui/Tab.vue'
interface Events { interface Events {
(e: 'updated', value: Actor): void (e: 'updated', value: Actor): void
@ -76,126 +80,163 @@ watch(props, fetchData, { immediate: true })
v-title="labels.usernameProfile" v-title="labels.usernameProfile"
class="page-profile" class="page-profile"
> >
<div <Layout flex>
v-if="isLoading" <img
class="ui vertical segment" src="https://images.unsplash.com/photo-1524650359799-842906ca1c06?ixlib=rb-1.2.1&dl=te-nguyen-Wt7XT1R6sjU-unsplash.jpg&w=640&q=80&fm=jpg&crop=entropy&cs=tinysrgb"
> alt="{{ displayName }}"
<div class="ui centered active inline loader" /> class="avatar"
</div> />
<div class="ui head vertical stripe segment container"> <!-- <div class="ui five wide column">
<div <button
v-if="object" ref="dropdown"
class="ui stackable grid" v-dropdown="{direction: 'downward'}"
> class="ui pointing dropdown icon small basic right floated button"
<div class="ui five wide column"> style="position: absolute; right: 1em; top: 1em;"
<button >
ref="dropdown" <i class="ellipsis vertical icon" />
v-dropdown="{direction: 'downward'}" <div class="menu">
class="ui pointing dropdown icon small basic right floated button" <a
style="position: absolute; right: 1em; top: 1em;" v-if="object.domain != store.getters['instance/domain']"
> :href="object.fid"
<i class="ellipsis vertical icon" /> target="_blank"
<div class="menu"> class="basic item"
<a
v-if="object.domain != store.getters['instance/domain']"
:href="object.fid"
target="_blank"
class="basic item"
>
<i class="external icon" />
{{ t('views.auth.ProfileBase.link.domainView', {domain: object.domain}) }}
</a>
<div
v-for="obj in getReportableObjects({account: object})"
:key="obj.target.type + obj.target.id"
role="button"
class="basic item"
@click.stop.prevent="report(obj)"
>
<i class="share icon" /> {{ obj.label }}
</div>
<div class="divider" />
<router-link
v-if="store.state.auth.availablePermissions['moderation']"
class="basic item"
:to="{name: 'manage.moderation.accounts.detail', params: {id: object.full_username}}"
>
<i class="wrench icon" />
{{ t('views.auth.ProfileBase.link.moderation') }}
</router-link>
</div>
</button>
<user-follow-button
v-if="$store.state.auth.authenticated && object && object.full_username !== $store.state.auth.fullUsername"
:actor="object"
/>
<h1 class="ui center aligned icon header">
<i
v-if="!object.icon"
class="circular inverted user success icon"
/>
<img
v-else
v-lazy="store.getters['instance/absoluteUrl'](object.icon.urls.medium_square_crop)"
alt=""
class="ui big circular image"
> >
<div class="ellispsis content"> <i class="external icon" />
<div class="ui very small hidden divider" /> {{ t('views.auth.ProfileBase.link.domainView', {domain: object.domain}) }}
<span>{{ displayName }}</span> </a>
<div class="ui very small hidden divider" /> <div
<div v-for="obj in getReportableObjects({account: object})"
class="sub header ellipsis" :key="obj.target.type + obj.target.id"
:title="object.full_username" role="button"
> class="basic item"
{{ object.full_username }} @click.stop.prevent="report(obj)"
</div> >
<i class="share icon" /> {{ obj.label }}
</div> </div>
<template v-if="object.full_username === store.state.auth.fullUsername">
<div class="ui very small hidden divider" /> <div class="divider" />
<div class="ui basic success label"> <router-link
{{ t('views.auth.ProfileBase.label.self') }} v-if="store.state.auth.availablePermissions['moderation']"
</div> class="basic item"
</template> :to="{name: 'manage.moderation.accounts.detail', params: {id: object.full_username}}"
</h1> >
<div class="ui small hidden divider" /> <i class="wrench icon" />
<div v-if="store.getters['ui/layoutVersion'] === 'large'"> {{ t('views.auth.ProfileBase.link.moderation') }}
<rendered-description </router-link>
:content="object.summary"
:field-name="'summary'"
:update-url="`users/${store.state.auth.username}/`"
:can-update="store.state.auth.authenticated && object.full_username === store.state.auth.fullUsername"
@updated="emit('updated', $event)"
/>
</div> </div>
</div> </button>
<div class="ui eleven wide column"> <user-follow-button
<div class="ui head vertical stripe segment"> v-if="$store.state.auth.authenticated && object && object.full_username !== $store.state.auth.fullUsername"
<div class="ui container"> :actor="object"
<div class="ui secondary pointing center aligned menu"> />
<router-link <h1 class="ui center aligned icon header">
class="item" <i
:to="{name: 'profile.overview', params: routerParams}" v-if="!object.icon"
> class="circular inverted user success icon"
{{ t('views.auth.ProfileBase.link.overview') }} />
</router-link> <img
<router-link v-else
class="item" v-lazy="store.getters['instance/absoluteUrl'](object.icon.urls.medium_square_crop)"
:to="{name: 'profile.activity', params: routerParams}" alt=""
> class="ui big circular image"
{{ t('views.auth.ProfileBase.link.activity') }} >
</router-link> <div class="ellispsis content">
</div> <div class="ui very small hidden divider" />
<div class="ui hidden divider" /> <span>{{ displayName }}</span>
<router-view <div class="ui very small hidden divider" />
:object="object" <div
@updated="fetchData" class="sub header ellipsis"
/> :title="object.full_username"
>
{{ object.full_username }}
</div> </div>
</div> </div>
<template v-if="object.full_username === store.state.auth.fullUsername">
<div class="ui very small hidden divider" />
<div class="ui basic success label">
{{ t('views.auth.ProfileBase.label.self') }}
</div>
</template>
</h1>
<div class="ui small hidden divider" />
<div v-if="store.getters['ui/layoutVersion'] === 'large'">
<rendered-description
:content="object.summary"
:field-name="'summary'"
:update-url="`users/${store.state.auth.username}/`"
:can-update="store.state.auth.authenticated && object.full_username === store.state.auth.fullUsername"
@updated="emit('updated', $event)"
/>
</div> </div>
</div> </div>
</div> <div class="ui eleven wide column">
<div class="ui head vertical stripe segment">
<div class="ui container">
<div class="ui secondary pointing center aligned menu">
<router-link
class="item"
:to="{name: 'profile.overview', params: routerParams}"
>
{{ t('views.auth.ProfileBase.link.overview') }}
</router-link>
<router-link
class="item"
:to="{name: 'profile.activity', params: routerParams}"
>
{{ t('views.auth.ProfileBase.link.activity') }}
</router-link>
</div>
<div class="ui hidden divider" />
<router-view
:object="object"
@updated="fetchData"
/>
</div>
</div>
</div> -->
</Layout>
<Layout stack noGap style="flex-grow: 2;">
<Layout flex style="justify-content: space-between;">
<!-- <Section h1="{{ displayName }}" :action="{ text: 'Edit profile', to:'/settings' }" /> -->
<h1>{{ displayName }}</h1>
<Link ghost alignSelf="center" minContent forceUnderline primary style="color: var(--fw-primary);" to="/settings">Edit profile</Link>
</Layout>
<span>{{ object?.full_username }}<i class="bi bi-copy" style="margin-left: 8px;"/></span>
<rendered-description
:content="object?.summary"
:field-name="'info'"
:update-url="`users/${store.state.auth.username}/`"
:can-update="store.state.auth.authenticated && object?.full_username === store.state.auth.fullUsername"
@updated="emit('updated', $event)"
/>
</Layout>
</Layout>
<Tabs>
<Tab title="Overview">{{ t('views.auth.ProfileBase.link.overview') }}</Tab>
<Tab title="Collections">{{ t('views.auth.ProfileBase.link.collections') }}</Tab>
<Tab title="Channels">{{ t('views.auth.ProfileBase.link.channels') }}</Tab>
<Tab title="Activity">{{ t('views.auth.ProfileBase.link.activity') }}</Tab>
</Tabs>
</Layout> </Layout>
</template> </template>
<style scoped lang="scss">
img.avatar {
width: 176px;
height: 176px;
border-radius: 50%;
}
h1 {
font-size: 48px;
margin-bottom: 8px;
}
a.edit span {
color: var(--fw-primary);
&:hover {
color: var(--color);
}
}
</style>