Implement top menu user popover (#3631)

Co-authored-by: github-actions <github-actions@github.com>
This commit is contained in:
Chenlei Hu
2025-04-25 15:26:41 -04:00
committed by GitHub
parent 25359575db
commit 45eb4701d2
9 changed files with 133 additions and 34 deletions

View File

@@ -1,45 +1,50 @@
<!-- A button that shows current authenticated user's avatar -->
<template>
<Button
v-if="isAuthenticated"
v-tooltip="{ value: $t('userSettings.title'), showDelay: 300 }"
class="user-profile-button p-1"
severity="secondary"
text
:aria-label="$t('userSettings.title')"
@click="openUserSettings"
>
<div
class="flex items-center rounded-full bg-[var(--p-content-background)]"
<div>
<Button
v-if="isAuthenticated"
v-tooltip="{ value: $t('userSettings.title'), showDelay: 300 }"
class="user-profile-button p-1"
severity="secondary"
text
:aria-label="$t('userSettings.title')"
@click="popover?.toggle($event)"
>
<Avatar
:image="photoURL"
:icon="photoURL ? undefined : 'pi pi-user'"
shape="circle"
aria-label="User Avatar"
/>
<div
class="flex items-center rounded-full bg-[var(--p-content-background)]"
>
<Avatar
:image="photoURL"
:icon="photoURL ? undefined : 'pi pi-user'"
shape="circle"
aria-label="User Avatar"
/>
<i class="pi pi-chevron-down px-1" :style="{ fontSize: '0.5rem' }" />
</div>
</Button>
<i class="pi pi-chevron-down px-1" :style="{ fontSize: '0.5rem' }" />
</div>
</Button>
<Popover ref="popover" :show-arrow="false">
<CurrentUserPopover />
</Popover>
</div>
</template>
<script setup lang="ts">
import Avatar from 'primevue/avatar'
import Button from 'primevue/button'
import { computed } from 'vue'
import Popover from 'primevue/popover'
import { computed, ref } from 'vue'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
import CurrentUserPopover from './CurrentUserPopover.vue'
const authStore = useFirebaseAuthStore()
const dialogService = useDialogService()
const popover = ref<InstanceType<typeof Popover> | null>(null)
const isAuthenticated = computed(() => authStore.isAuthenticated)
const photoURL = computed<string | undefined>(
() => authStore.currentUser?.photoURL ?? undefined
)
const openUserSettings = () => {
dialogService.showSettingsDialog('user')
}
</script>

View File

@@ -0,0 +1,80 @@
<!-- A popover that shows current user information and actions -->
<template>
<div class="current-user-popover w-72">
<!-- User Info Section -->
<div class="p-3">
<div class="flex flex-col items-center">
<Avatar
class="mb-3"
:image="user?.photoURL ?? undefined"
:icon="user?.photoURL ? undefined : 'pi pi-user'"
shape="circle"
size="large"
aria-label="User Avatar"
/>
<!-- User Details -->
<h3 class="text-lg font-semibold truncate my-0 mb-1">
{{ user?.displayName || $t('g.user') }}
</h3>
<p v-if="user?.email" class="text-sm text-muted truncate my-0">
{{ user.email }}
</p>
</div>
</div>
<Divider class="my-2" />
<Button
class="justify-start"
:label="$t('userSettings.title')"
icon="pi pi-cog"
text
fluid
severity="secondary"
@click="handleOpenUserSettings"
/>
<Divider class="my-2" />
<div class="w-full flex flex-col gap-2 p-2">
<div class="text-muted text-sm">
{{ $t('credits.yourCreditBalance') }}
</div>
<div class="flex justify-between items-center">
<UserCredit text-class="text-2xl" />
<Button :label="$t('credits.topUp.topUp')" @click="handleTopUp" />
</div>
</div>
</div>
</template>
<script setup lang="ts">
import Avatar from 'primevue/avatar'
import Button from 'primevue/button'
import Divider from 'primevue/divider'
import { computed, onMounted } from 'vue'
import UserCredit from '@/components/common/UserCredit.vue'
import { useDialogService } from '@/services/dialogService'
import { useFirebaseAuthService } from '@/services/firebaseAuthService'
import { useFirebaseAuthStore } from '@/stores/firebaseAuthStore'
const authStore = useFirebaseAuthStore()
const authService = useFirebaseAuthService()
const dialogService = useDialogService()
const user = computed(() => authStore.currentUser)
const handleOpenUserSettings = () => {
dialogService.showSettingsDialog('user')
}
const handleTopUp = () => {
dialogService.showTopUpCreditsDialog()
}
onMounted(() => {
void authService.fetchBalance()
})
</script>

View File

@@ -1,5 +1,6 @@
{
"g": {
"user": "User",
"currentUser": "Current user",
"empty": "Empty",
"noWorkflowsFound": "No workflows found.",
@@ -1161,7 +1162,8 @@
"quickPurchase": "Quick Purchase",
"maxAmount": "(Max. $1,000 USD)",
"buyNow": "Buy now",
"seeDetails": "See details"
"seeDetails": "See details",
"topUp": "Top Up"
}
},
"userSettings": {

View File

@@ -114,7 +114,8 @@
"insufficientTitle": "Créditos insuficientes",
"maxAmount": "(Máx. $1,000 USD)",
"quickPurchase": "Compra rápida",
"seeDetails": "Ver detalles"
"seeDetails": "Ver detalles",
"topUp": "Recargar"
},
"yourCreditBalance": "Tu saldo de créditos"
},
@@ -302,6 +303,7 @@
"updated": "Actualizado",
"updating": "Actualizando",
"upload": "Subir",
"user": "Usuario",
"videoFailedToLoad": "Falló la carga del video",
"workflow": "Flujo de trabajo"
},

View File

@@ -114,7 +114,8 @@
"insufficientTitle": "Crédits insuffisants",
"maxAmount": "(Max. 1 000 $ US)",
"quickPurchase": "Achat rapide",
"seeDetails": "Voir les détails"
"seeDetails": "Voir les détails",
"topUp": "Recharger"
},
"yourCreditBalance": "Votre solde de crédits"
},
@@ -302,6 +303,7 @@
"updated": "Mis à jour",
"updating": "Mise à jour",
"upload": "Téléverser",
"user": "Utilisateur",
"videoFailedToLoad": "Échec du chargement de la vidéo",
"workflow": "Flux de travail"
},

View File

@@ -114,7 +114,8 @@
"insufficientTitle": "クレジット不足",
"maxAmount": "(最大 $1,000 USD",
"quickPurchase": "クイック購入",
"seeDetails": "詳細を見る"
"seeDetails": "詳細を見る",
"topUp": "チャージ"
},
"yourCreditBalance": "あなたのクレジット残高"
},
@@ -302,6 +303,7 @@
"updated": "更新済み",
"updating": "更新中",
"upload": "アップロード",
"user": "ユーザー",
"videoFailedToLoad": "ビデオの読み込みに失敗しました",
"workflow": "ワークフロー"
},

View File

@@ -114,7 +114,8 @@
"insufficientTitle": "크레딧 부족",
"maxAmount": "(최대 $1,000 USD)",
"quickPurchase": "빠른 구매",
"seeDetails": "자세히 보기"
"seeDetails": "자세히 보기",
"topUp": "충전하기"
},
"yourCreditBalance": "보유 크레딧 잔액"
},
@@ -302,6 +303,7 @@
"updated": "업데이트 됨",
"updating": "업데이트 중",
"upload": "업로드",
"user": "사용자",
"videoFailedToLoad": "비디오를 로드하지 못했습니다.",
"workflow": "워크플로"
},

View File

@@ -114,7 +114,8 @@
"insufficientTitle": "Недостаточно кредитов",
"maxAmount": "(Макс. $1,000 USD)",
"quickPurchase": "Быстрая покупка",
"seeDetails": "Смотреть детали"
"seeDetails": "Смотреть детали",
"topUp": "Пополнить"
},
"yourCreditBalance": "Ваш баланс кредитов"
},
@@ -302,6 +303,7 @@
"updated": "Обновлено",
"updating": "Обновление",
"upload": "Загрузить",
"user": "Пользователь",
"videoFailedToLoad": "Не удалось загрузить видео",
"workflow": "Рабочий процесс"
},

View File

@@ -114,7 +114,8 @@
"insufficientTitle": "积分不足",
"maxAmount": "(最高 $1,000 美元)",
"quickPurchase": "快速购买",
"seeDetails": "查看详情"
"seeDetails": "查看详情",
"topUp": "充值"
},
"yourCreditBalance": "您的积分余额"
},
@@ -302,6 +303,7 @@
"updated": "已更新",
"updating": "更新中",
"upload": "上传",
"user": "用户",
"videoFailedToLoad": "视频加载失败",
"workflow": "工作流"
},