Initial commit
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
<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'
|
||||
|
||||
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 formatUrl(url) {
|
||||
if (!url) return ''
|
||||
return url.startsWith('http') ? url : `http://localhost:3001${url}`
|
||||
}
|
||||
|
||||
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>
|
||||
Reference in New Issue
Block a user