Dev #11
@@ -147,7 +147,7 @@
|
|||||||
--gradient-coin-chip-hover: linear-gradient(135deg, var(--color-primary-a40) 0%, var(--color-aqua-a40) 100%);
|
--gradient-coin-chip-hover: linear-gradient(135deg, var(--color-primary-a40) 0%, var(--color-aqua-a40) 100%);
|
||||||
--color-coin-chip-border: var(--color-primary-a45);
|
--color-coin-chip-border: var(--color-primary-a45);
|
||||||
--color-coin-chip-text: var(--color-primary-border);
|
--color-coin-chip-text: var(--color-primary-border);
|
||||||
--color-coin-chip-label: var(--color-aqua-dark);
|
--color-coin-chip-label: var(--color-primary-border);
|
||||||
|
|
||||||
--radius-sm: 8px;
|
--radius-sm: 8px;
|
||||||
--radius-md: 12px;
|
--radius-md: 12px;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { useAuthStore } from '@/stores/auth'
|
|||||||
import { useUserStore } from '@/stores/user'
|
import { useUserStore } from '@/stores/user'
|
||||||
import CoinChip from '@/components/ui/CoinChip.vue'
|
import CoinChip from '@/components/ui/CoinChip.vue'
|
||||||
import AppIcon from '@/components/ui/AppIcon.vue'
|
import AppIcon from '@/components/ui/AppIcon.vue'
|
||||||
|
import ModalDialog from '@/components/ui/ModalDialog.vue'
|
||||||
import { formatUserName } from '@/utils/formatUserName'
|
import { formatUserName } from '@/utils/formatUserName'
|
||||||
import type { UserRole } from '@/types'
|
import type { UserRole } from '@/types'
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ const auth = useAuthStore()
|
|||||||
const userStore = useUserStore()
|
const userStore = useUserStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const isProfileMenuOpen = ref(false)
|
const isProfileMenuOpen = ref(false)
|
||||||
|
const isCoinDialogOpen = ref(false)
|
||||||
const profileMenuRef = ref<HTMLElement | null>(null)
|
const profileMenuRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
const unreadCount = computed(() => userStore.unreadCount())
|
const unreadCount = computed(() => userStore.unreadCount())
|
||||||
@@ -42,10 +44,20 @@ function closeProfileMenu() {
|
|||||||
isProfileMenuOpen.value = false
|
isProfileMenuOpen.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function closeCoinDialog() {
|
||||||
|
isCoinDialogOpen.value = false
|
||||||
|
}
|
||||||
|
|
||||||
function toggleProfileMenu() {
|
function toggleProfileMenu() {
|
||||||
|
closeCoinDialog()
|
||||||
isProfileMenuOpen.value = !isProfileMenuOpen.value
|
isProfileMenuOpen.value = !isProfileMenuOpen.value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openCoinDialog() {
|
||||||
|
closeProfileMenu()
|
||||||
|
isCoinDialogOpen.value = true
|
||||||
|
}
|
||||||
|
|
||||||
function openProfile() {
|
function openProfile() {
|
||||||
closeProfileMenu()
|
closeProfileMenu()
|
||||||
router.push('/profile')
|
router.push('/profile')
|
||||||
@@ -65,9 +77,9 @@ async function logout() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleDocumentPointerDown(event: PointerEvent) {
|
function handleDocumentPointerDown(event: PointerEvent) {
|
||||||
if (!isProfileMenuOpen.value) return
|
|
||||||
const target = event.target
|
const target = event.target
|
||||||
if (!(target instanceof Node)) return
|
if (!(target instanceof Node)) return
|
||||||
|
|
||||||
if (!profileMenuRef.value?.contains(target)) closeProfileMenu()
|
if (!profileMenuRef.value?.contains(target)) closeProfileMenu()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +102,20 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="topbar-right">
|
<div class="topbar-right">
|
||||||
<CoinChip v-if="auth.user" :amount="auth.user.coins" />
|
<CoinChip
|
||||||
|
v-if="auth.user"
|
||||||
|
:amount="auth.user.coins"
|
||||||
|
aria-haspopup="dialog"
|
||||||
|
@click="openCoinDialog"
|
||||||
|
/>
|
||||||
|
|
||||||
<button class="notif-btn" @click="$router.push('/notifications')">
|
<div v-if="auth.user" class="level-chip">
|
||||||
|
<AppIcon class="level-icon" icon="star" :size="15" />
|
||||||
|
<span class="level-label">Ур.</span>
|
||||||
|
<span class="level-value">{{ auth.user.level }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button class="notif-btn" type="button" aria-label="Уведомления" @click="$router.push('/notifications')">
|
||||||
<AppIcon icon="bell" :size="18" />
|
<AppIcon icon="bell" :size="18" />
|
||||||
<span class="notif-dot" v-if="auth.user && unreadCount > 0">
|
<span class="notif-dot" v-if="auth.user && unreadCount > 0">
|
||||||
{{ unreadCount }}
|
{{ unreadCount }}
|
||||||
@@ -140,6 +163,19 @@ onBeforeUnmount(() => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ModalDialog v-model="isCoinDialogOpen">
|
||||||
|
<div class="coin-dialog-content">
|
||||||
|
<div class="coin-dialog-title">
|
||||||
|
<AppIcon class="coin-dialog-icon" icon="coin" :size="22" />
|
||||||
|
<span>Монеты UniVerse</span>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
В будущем монеты можно будет использовать во внутреннем магазине. Сейчас магазин еще не запущен, поэтому монеты просто копятся на вашем балансе.
|
||||||
|
</p>
|
||||||
|
<button class="btn-primary coin-dialog-ok" type="button" @click="closeCoinDialog">ОК</button>
|
||||||
|
</div>
|
||||||
|
</ModalDialog>
|
||||||
</header>
|
</header>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -185,13 +221,74 @@ onBeforeUnmount(() => {
|
|||||||
gap: 12px;
|
gap: 12px;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
.coin-dialog-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
.coin-dialog-title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 9px;
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
.coin-dialog-icon {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
color: var(--color-coin-chip-text);
|
||||||
|
}
|
||||||
|
.coin-dialog-content p {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
.coin-dialog-ok {
|
||||||
|
min-width: 92px;
|
||||||
|
align-self: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
.level-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
height: 28px;
|
||||||
|
padding: 0 11px;
|
||||||
|
border: 1px solid var(--color-primary-a20);
|
||||||
|
border-radius: 20px;
|
||||||
|
background: var(--color-primary-a10);
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
cursor: default;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.level-icon {
|
||||||
|
color: var(--color-primary);
|
||||||
|
}
|
||||||
|
.level-label {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
.level-value {
|
||||||
|
color: var(--color-primary-dark);
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
.notif-btn {
|
.notif-btn {
|
||||||
position: relative;
|
position: relative;
|
||||||
background: none;
|
display: inline-flex;
|
||||||
border: none;
|
align-items: center;
|
||||||
font-size: 20px;
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
background: transparent;
|
||||||
|
border: 0;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
padding: 4px;
|
padding: 0;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
transition: background 0.2s;
|
transition: background 0.2s;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,14 +2,15 @@
|
|||||||
import AppIcon from '@/components/ui/AppIcon.vue'
|
import AppIcon from '@/components/ui/AppIcon.vue'
|
||||||
|
|
||||||
defineProps<{ amount: number }>()
|
defineProps<{ amount: number }>()
|
||||||
|
defineEmits<{ click: [] }>()
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="coin-chip">
|
<button class="coin-chip" type="button" @click="$emit('click')">
|
||||||
<AppIcon class="coin-icon" icon="coin" :size="16" />
|
<AppIcon class="coin-icon" icon="coin" :size="16" />
|
||||||
<span class="coin-amount">{{ amount }}</span>
|
<span class="coin-amount">{{ amount }}</span>
|
||||||
<span class="coin-label">монет</span>
|
<span class="coin-label">монет</span>
|
||||||
</div>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@@ -17,16 +18,23 @@ defineProps<{ amount: number }>()
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 5px;
|
gap: 5px;
|
||||||
background: var(--gradient-coin-chip);
|
background: var(--color-primary-a10);
|
||||||
border: 1px solid var(--color-coin-chip-border);
|
border: 1px solid var(--color-coin-chip-border);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
padding: 5px 12px;
|
padding: 5px 12px;
|
||||||
cursor: default;
|
color: inherit;
|
||||||
transition: all 0.2s;
|
cursor: pointer;
|
||||||
|
font: inherit;
|
||||||
|
transition: background 0.2s, border-color 0.2s, box-shadow 0.2s;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
.coin-chip:hover {
|
.coin-chip:hover {
|
||||||
background: var(--gradient-coin-chip-hover);
|
background: var(--color-primary-a18);
|
||||||
|
border-color: var(--color-primary-a50);
|
||||||
|
}
|
||||||
|
.coin-chip:focus-visible {
|
||||||
|
outline: 2px solid var(--color-primary-a45);
|
||||||
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
.coin-icon { color: var(--color-coin-chip-text); }
|
.coin-icon { color: var(--color-coin-chip-text); }
|
||||||
.coin-amount { font-weight: 800; font-size: 14px; color: var(--color-coin-chip-text); }
|
.coin-amount { font-weight: 800; font-size: 14px; color: var(--color-coin-chip-text); }
|
||||||
|
|||||||
Reference in New Issue
Block a user