Files
noctune-client/src/views/ProfilePage.vue
T

155 lines
7.0 KiB
Vue

<template>
<div class="p-8">
<div class="bg-white rounded-2xl border-4 border-black p-6 mb-6 flex items-center gap-8">
<div class="w-40 h-40 rounded-full border-4 border-black overflow-hidden flex-shrink-0 bg-gray-300">
<img v-if="authStore.user?.avatarUrl" :src="formatUrl(authStore.user.avatarUrl)" alt="Profile" class="w-full h-full object-cover" />
<div v-else class="w-full h-full flex items-center justify-center">
<svg class="w-16 h-16 text-gray-400" fill="currentColor" viewBox="0 0 24 24"><path d="M12 12c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm0 2c-2.67 0-8 1.34-8 4v2h16v-2c0-2.66-5.33-4-8-4z"/></svg>
</div>
</div>
<div class="flex-1">
<h1 class="text-3xl font-black text-black mb-1">{{ userDisplayName }}</h1>
<p class="text-gray-600 mb-3">@{{ authStore.user?.username || 'username' }}</p>
<div class="inline-flex items-center gap-2 border-2 border-noctune-orange text-noctune-orange px-3 py-1 rounded-full mb-4">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11.049 2.927c.3-.921 1.603-.921 1.902 0l1.519 4.674a1 1 0 00.95.69h4.915c.969 0 1.371 1.24.588 1.81l-3.976 2.888a1 1 0 00-.363 1.118l1.518 4.674c.3.922-.755 1.688-1.538 1.118l-3.976-2.888a1 1 0 00-1.176 0l-3.976 2.888c-.783.57-1.838-.197-1.538-1.118l1.518-4.674a1 1 0 00-.363-1.118l-3.976-2.888c-.784-.57-.38-1.81.588-1.81h4.914a1 1 0 00.951-.69l1.519-4.674z"/></svg>
<span class="text-sm font-medium">{{ authStore.user?.role === 'admin' ? 'Admin' : 'Premium Member' }}</span>
</div>
<div class="flex gap-4">
<button @click="toggleEdit" class="border-2 border-black px-4 py-2 rounded-lg font-medium flex items-center gap-2 hover:bg-gray-100 transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/></svg>
{{ editing ? 'Cancel' : 'Edit Profile' }}
</button>
</div>
</div>
</div>
<div v-if="editing" class="bg-white rounded-2xl border-4 border-black p-6 mb-6">
<h3 class="text-xl font-black text-black mb-4">Edit Profile</h3>
<p v-if="editError" class="text-red-500 text-sm mb-4">{{ editError }}</p>
<p v-if="editSuccess" class="text-green-600 text-sm mb-4">{{ editSuccess }}</p>
<div class="space-y-4 max-w-md">
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">NAME</label>
<input type="text" v-model="editName" class="w-full border-2 border-black rounded-lg px-4 py-2 focus:outline-none text-black" />
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">USERNAME</label>
<input type="text" v-model="editUsername" class="w-full border-2 border-black rounded-lg px-4 py-2 focus:outline-none text-black" />
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">EMAIL</label>
<input type="email" v-model="editEmail" class="w-full border-2 border-black rounded-lg px-4 py-2 focus:outline-none text-black" />
</div>
<div>
<label class="block text-xs font-bold text-gray-600 mb-1">AVATAR</label>
<input type="file" accept="image/*" @change="onAvatarChange" class="w-full text-sm" />
</div>
<button @click="handleUpdate" :disabled="updating" class="bg-noctune-teal border-2 border-black px-6 py-2 font-bold rounded-lg hover:bg-teal-600 transition-colors disabled:opacity-50">
<span v-if="updating" class="inline-block w-4 h-4 border-2 border-black border-t-transparent rounded-full animate-spin mr-1"></span>
Save Changes
</button>
</div>
</div>
<div class="bg-white rounded-2xl border-4 border-black p-6 mb-6 flex justify-center gap-12">
<div class="flex items-center gap-4">
<div class="w-14 h-14 bg-noctune-orange rounded-xl flex items-center justify-center border-2 border-black">
<svg class="w-7 h-7 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M12 3v10.55c-.59-.34-1.27-.55-2-.55-2.21 0-4 1.79-4 4s1.79 4 4 4 4-1.79 4-4V7h4V3h-6z"/></svg>
</div>
<div>
<p class="text-2xl font-black text-black">{{ likedCount }}</p>
<p class="text-sm text-gray-500">Liked Songs</p>
</div>
</div>
</div>
<button @click="handleLogout" class="w-full bg-noctune-orange border-4 border-black rounded-2xl py-4 font-bold text-black flex items-center justify-center gap-3 hover:bg-orange-600 transition-colors">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/></svg>
Logout
</button>
</div>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/authStore.js'
import api from '../lib/api.js'
import { formatUrl } from '../lib/utils.js'
const router = useRouter()
const authStore = useAuthStore()
const editing = ref(false)
const editName = ref('')
const editUsername = ref('')
const editEmail = ref('')
const avatarFile = ref(null)
const updating = ref(false)
const editError = ref('')
const editSuccess = ref('')
const likedCount = ref(0)
const userDisplayName = computed(() => {
return authStore.user?.name || authStore.user?.username || 'User'
})
function toggleEdit() {
editing.value = !editing.value
if (editing.value) {
editName.value = authStore.user?.name || ''
editUsername.value = authStore.user?.username || ''
editEmail.value = authStore.user?.email || ''
avatarFile.value = null
editError.value = ''
editSuccess.value = ''
}
}
function onAvatarChange(e) {
avatarFile.value = e.target.files[0] || null
}
async function handleUpdate() {
editError.value = ''
editSuccess.value = ''
updating.value = true
try {
const formData = new FormData()
formData.append('name', editName.value)
formData.append('username', editUsername.value)
formData.append('email', editEmail.value)
if (avatarFile.value) formData.append('avatar', avatarFile.value)
const result = await authStore.updateProfile(formData)
if (result.success) {
editSuccess.value = 'Profile updated!'
setTimeout(() => { editing.value = false }, 1500)
} else {
editError.value = result.message
}
} catch {
editError.value = 'Update failed'
} finally {
updating.value = false
}
}
async function handleLogout() {
await authStore.logout()
router.push('/')
}
onMounted(async () => {
try {
const { data } = await api.get('/tracks/liked')
if (data.success) {
likedCount.value = data.data.likedTracks?.length || 0
}
} catch {
// ignore
}
})
</script>