209 lines
8.0 KiB
Vue
209 lines
8.0 KiB
Vue
<template>
|
||
<div class="max-w-xl mx-auto p-6">
|
||
<h1 class="text-2xl font-bold mb-6">Настройки</h1>
|
||
|
||
<div class="space-y-6">
|
||
<!-- Profile Settings -->
|
||
<div class="bg-slate-800 border border-slate-700 rounded-xl p-6">
|
||
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||
<span>👤</span> Личный профиль
|
||
</h2>
|
||
<div class="space-y-4">
|
||
<div class="flex items-center gap-4 mb-4">
|
||
<img :src="profileForm.avatar" class="w-16 h-16 rounded-full bg-slate-700 object-cover border-2 border-blue-500/30" />
|
||
<div class="flex-1">
|
||
<label class="text-xs text-slate-500 uppercase font-bold">Ссылка на аватар</label>
|
||
<input v-model="profileForm.avatar" class="w-full mt-1 bg-slate-900 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-200 outline-none focus:border-blue-500" />
|
||
</div>
|
||
</div>
|
||
|
||
<div class="grid grid-cols-1 gap-4">
|
||
<div>
|
||
<label class="text-xs text-slate-500 uppercase font-bold">Имя (Display Name)</label>
|
||
<input v-model="profileForm.displayName" class="w-full mt-1 bg-slate-900 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-200 outline-none focus:border-blue-500" />
|
||
</div>
|
||
</div>
|
||
|
||
<div>
|
||
<label class="text-xs text-slate-500 uppercase font-bold">О себе (Bio)</label>
|
||
<textarea v-model="profileForm.bio" rows="3" class="w-full mt-1 bg-slate-900 border border-slate-700 rounded px-3 py-1.5 text-sm text-slate-200 outline-none focus:border-blue-500 resize-none"></textarea>
|
||
</div>
|
||
|
||
<button
|
||
@click="saveProfile"
|
||
:disabled="saving"
|
||
class="w-full py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition disabled:opacity-50"
|
||
>
|
||
{{ saving ? 'Сохранение...' : 'Сохранить изменения' }}
|
||
</button>
|
||
<p v-if="saveMessage" :class="saveMessage.type === 'error' ? 'text-red-400' : 'text-emerald-400'" class="text-center text-xs mt-2">
|
||
{{ saveMessage.text }}
|
||
</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- My Groups -->
|
||
<div class="bg-slate-800 border border-slate-700 rounded-xl p-6">
|
||
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||
<span>👥</span> Мои Группы
|
||
</h2>
|
||
<div v-if="userGroups.length === 0" class="text-slate-500 text-sm py-4">
|
||
Вы не состоите ни в одной группе.
|
||
</div>
|
||
<div v-else class="space-y-3">
|
||
<div v-for="g in userGroups" :key="g.id" class="flex items-center justify-between p-3 bg-slate-900/50 rounded-lg border border-slate-700">
|
||
<div>
|
||
<div class="font-medium text-slate-200">{{ g.name }}</div>
|
||
<div class="text-xs text-slate-500">{{ g.description }}</div>
|
||
</div>
|
||
<span :class="g.membershipRole === 'admin' ? 'text-purple-400 bg-purple-900/20' : 'text-blue-400 bg-blue-900/20'" class="text-[10px] uppercase font-bold px-2 py-0.5 rounded border border-current/20">
|
||
{{ g.membershipRole }}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Push Notifications -->
|
||
<div class="bg-slate-800 border border-slate-700 rounded-xl p-6">
|
||
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||
<span>🔔</span> Push-уведомления
|
||
</h2>
|
||
<div class="space-y-3">
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-slate-300">Статус поддержки</span>
|
||
<span :class="isSupported ? 'text-emerald-400' : 'text-red-400'">
|
||
{{ isSupported ? 'Доступно' : 'Недоступно' }}
|
||
</span>
|
||
</div>
|
||
<div class="flex items-center justify-between">
|
||
<span class="text-slate-300">Разрешение</span>
|
||
<span :class="permissionGranted ? 'text-emerald-400' : 'text-yellow-400'">
|
||
{{ permissionGranted ? 'Разрешено' : 'Не запрошено' }}
|
||
</span>
|
||
</div>
|
||
<button
|
||
v-if="isSupported && !permissionGranted"
|
||
@click="enablePush"
|
||
class="w-full py-2 bg-blue-600 hover:bg-blue-500 text-white rounded-lg font-medium transition"
|
||
>
|
||
Включить уведомления
|
||
</button>
|
||
<button
|
||
v-if="isSupported && permissionGranted && !subscription"
|
||
@click="subscribe"
|
||
class="w-full py-2 bg-emerald-600 hover:bg-emerald-500 text-white rounded-lg font-medium transition"
|
||
>
|
||
Подписаться на пуши
|
||
</button>
|
||
<div v-if="subscription" class="text-sm text-emerald-400">
|
||
✅ Подписка активна
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Achievements -->
|
||
<div class="bg-slate-800 border border-slate-700 rounded-xl p-6">
|
||
<h2 class="text-lg font-semibold mb-4 flex items-center gap-2">
|
||
<span>🏆</span> Ачивки
|
||
</h2>
|
||
<div v-if="achievements.length === 0" class="text-slate-400 text-sm">
|
||
Пока нет разблокированных ачивок. Завершите задачи в Canvas!
|
||
</div>
|
||
<div v-else class="space-y-2">
|
||
<div
|
||
v-for="ach in achievements"
|
||
:key="ach.id"
|
||
class="flex items-center gap-3 p-3 bg-slate-700 rounded-lg"
|
||
>
|
||
<span class="text-xl">{{ ach.icon || '🏆' }}</span>
|
||
<div>
|
||
<div class="font-medium">{{ ach.title }}</div>
|
||
<div class="text-xs text-slate-400">{{ ach.description }}</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script setup>
|
||
definePageMeta({
|
||
middleware: 'auth'
|
||
})
|
||
|
||
const { user } = useAuth()
|
||
const { isSupported, subscription, requestPermission, subscribe: pushSubscribe } = usePush()
|
||
const { achievements, loadAchievements } = useAchievements()
|
||
|
||
const permissionGranted = ref(false)
|
||
const saving = ref(false)
|
||
const saveMessage = ref(null)
|
||
const userGroups = ref([])
|
||
|
||
const profileForm = reactive({
|
||
displayName: '',
|
||
username: '',
|
||
bio: '',
|
||
avatar: ''
|
||
})
|
||
|
||
const loadProfile = async () => {
|
||
try {
|
||
const data = await $fetch(`/api/users/${user.value.id}/profile`, {
|
||
headers: { Authorization: `Bearer ${useAuth().token.value}` }
|
||
})
|
||
const groupsData = await $fetch('/api/user/groups', {
|
||
headers: { Authorization: `Bearer ${useAuth().token.value}` }
|
||
})
|
||
userGroups.value = groupsData.groups || []
|
||
|
||
profileForm.displayName = data.profile.displayName || ''
|
||
profileForm.username = data.profile.user.username || ''
|
||
profileForm.bio = data.profile.bio || ''
|
||
profileForm.avatar = data.profile.avatar || ''
|
||
} catch (e) {
|
||
console.error('Failed to load profile', e)
|
||
}
|
||
}
|
||
|
||
const saveProfile = async () => {
|
||
saving.value = true
|
||
saveMessage.value = null
|
||
try {
|
||
await $fetch('/api/user/profile', {
|
||
method: 'PUT',
|
||
body: profileForm,
|
||
headers: { Authorization: `Bearer ${useAuth().token.value}` }
|
||
})
|
||
saveMessage.value = { type: 'success', text: 'Профиль обновлен успешно!' }
|
||
// Update local user data if needed
|
||
user.value.username = profileForm.username
|
||
} catch (e) {
|
||
saveMessage.value = { type: 'error', text: e.data?.message || 'Ошибка при сохранении' }
|
||
} finally {
|
||
saving.value = false
|
||
}
|
||
}
|
||
|
||
const enablePush = async () => {
|
||
const granted = await requestPermission()
|
||
permissionGranted.value = granted
|
||
if (granted) {
|
||
await pushSubscribe()
|
||
}
|
||
}
|
||
|
||
const subscribe = async () => {
|
||
await pushSubscribe()
|
||
}
|
||
|
||
onMounted(async () => {
|
||
permissionGranted.value = Notification.permission === 'granted'
|
||
await Promise.all([
|
||
loadAchievements(),
|
||
loadProfile()
|
||
])
|
||
})
|
||
</script>
|