diff --git a/client/components/app/Appbar.vue b/client/components/app/Appbar.vue index cbd0a96f3..04ea3da23 100644 --- a/client/components/app/Appbar.vue +++ b/client/components/app/Appbar.vue @@ -42,7 +42,7 @@ - + diff --git a/client/pages/user/_id/index.vue b/client/pages/user/_id/index.vue index 3c08ad186..03ef67e57 100644 --- a/client/pages/user/_id/index.vue +++ b/client/pages/user/_id/index.vue @@ -1,42 +1,149 @@ @@ -45,7 +152,20 @@ export default { data() { return { user: null, - reviews: [] + reviews: [], + activeTab: 'account', + loading: true, + // Account tab data + password: null, + newPassword: null, + confirmPassword: null, + changingPassword: false, + selectedLanguage: '', + displayNameInput: '', + ereaderDevices: [], + deletingDeviceName: null, + selectedEReaderDevice: null, + showEReaderDeviceModal: false } }, computed: { @@ -54,6 +174,28 @@ export default { }, bookCoverAspectRatio() { return this.$store.getters['libraries/getBookCoverAspectRatio'] + }, + isCurrentUser() { + return this.user?.id === this.$store.state.user.user?.id + }, + isRoot() { + return this.user?.type === 'root' + }, + isGuest() { + return this.user?.type === 'guest' + }, + isPasswordAuthEnabled() { + const activeAuthMethods = this.$store.getters['getServerSetting']('authActiveAuthMethods') || [] + return activeAuthMethods.includes('local') + }, + showChangePasswordForm() { + return !this.isGuest && this.isPasswordAuthEnabled + }, + showEreaderTable() { + return this.user?.type !== 'root' && this.user?.type !== 'admin' && this.user?.permissions?.createEreader + }, + revisedEreaderDevices() { + return this.ereaderDevices.filter((device) => device.users?.length === 1) } }, methods: { @@ -63,6 +205,11 @@ export default { async loadUser() { try { this.user = await this.$axios.$get('/api/users/' + this.$route.params.id) + if (this.isCurrentUser) { + this.displayNameInput = this.user.displayName || '' + this.selectedLanguage = this.$languageCodes.current + await this.loadEreaderDevices() + } } catch (error) { console.error('Error loading user:', error) this.$toast.error('Error loading user profile') @@ -71,17 +218,122 @@ export default { async loadReviews() { try { const reviews = await this.$axios.$get('/api/users/' + this.$route.params.id + '/reviews') - console.log('Raw reviews data:', JSON.stringify(reviews, null, 2)) this.reviews = reviews } catch (error) { console.error('Error loading reviews:', error) this.$toast.error('Error loading reviews') } + }, + async loadEreaderDevices() { + try { + const response = await this.$axios.$get('/api/users/' + this.user.id + '/ereader-devices') + this.ereaderDevices = response.devices || [] + } catch (error) { + console.error('Error loading e-reader devices:', error) + } + }, + updateLocalLanguage(lang) { + this.$setLanguageCode(lang) + }, + async submitDisplayName() { + try { + await this.$axios.$patch('/api/users/' + this.user.id, { + displayName: this.displayNameInput + }) + this.$toast.success('Display name updated') + await this.loadUser() + } catch (error) { + console.error('Error updating display name:', error) + this.$toast.error('Error updating display name') + } + }, + async submitChangePassword() { + if (this.newPassword !== this.confirmPassword) { + return this.$toast.error('Passwords do not match') + } + if (this.password === this.newPassword) { + return this.$toast.error('New password must be different') + } + this.changingPassword = true + try { + await this.$axios.$patch('/api/me/password', { + password: this.password, + newPassword: this.newPassword + }) + this.$toast.success('Password updated successfully') + this.password = null + this.newPassword = null + this.confirmPassword = null + } catch (error) { + console.error('Error updating password:', error) + this.$toast.error('Error updating password') + } finally { + this.changingPassword = false + } + }, + addNewDeviceClick() { + this.selectedEReaderDevice = null + this.showEReaderDeviceModal = true + }, + editDeviceClick(device) { + this.selectedEReaderDevice = device + this.showEReaderDeviceModal = true + }, + async deleteDeviceClick(device) { + try { + this.deletingDeviceName = device.name + await this.$axios.$delete(`/api/users/${this.user.id}/ereader-devices/${device.name}`) + this.$toast.success('Device removed') + await this.loadEreaderDevices() + } catch (error) { + console.error('Error deleting device:', error) + this.$toast.error('Error removing device') + } finally { + this.deletingDeviceName = null + } + }, + async ereaderDevicesUpdated() { + await this.loadEreaderDevices() + }, + logout() { + if (this.$root.socket) { + console.log('Disconnecting from socket', this.$root.socket.id) + this.$root.socket.removeAllListeners() + this.$root.socket.disconnect() + } + + if (localStorage.getItem('token')) { + localStorage.removeItem('token') + } + this.$store.commit('libraries/setUserPlaylists', []) + this.$store.commit('libraries/setCollections', []) + + this.$axios + .$post('/logout') + .then((logoutPayload) => { + const redirect_url = logoutPayload.redirect_url + if (redirect_url) { + window.location.href = redirect_url + } else { + this.$router.push('/login') + } + }) + .catch((error) => { + console.error(error) + }) + }, + async init() { + this.loading = true + try { + await this.loadUser() + await this.loadReviews() + } finally { + this.loading = false + } } }, async mounted() { - await this.loadUser() - await this.loadReviews() + this.init() } } @@ -107,4 +359,22 @@ export default { -webkit-box-orient: vertical; overflow: hidden; } + +.tracksTable { + width: 100%; + border-collapse: collapse; +} + +.tracksTable th, +.tracksTable td { + padding: 8px; + text-align: left; + border-bottom: 1px solid rgba(255, 255, 255, 0.1); +} + +.tracksTable th { + font-weight: 600; + font-size: 0.875rem; + color: rgba(255, 255, 255, 0.7); +} \ No newline at end of file