diff --git a/public/avatar1.png b/public/avatar1.png new file mode 100644 index 0000000000000000000000000000000000000000..0f9f57b5faea8f110ee1e080c9eec6627425c1cc Binary files /dev/null and b/public/avatar1.png differ diff --git a/public/avatar2.png b/public/avatar2.png new file mode 100644 index 0000000000000000000000000000000000000000..911b457e282e6a0fc3254b3c75d11268a99549a0 Binary files /dev/null and b/public/avatar2.png differ diff --git a/public/avatar3.png b/public/avatar3.png new file mode 100644 index 0000000000000000000000000000000000000000..9d3cc3ede8d28baa1f2ca2f7dff2a6b0280b619b Binary files /dev/null and b/public/avatar3.png differ diff --git a/public/avatar4.png b/public/avatar4.png new file mode 100644 index 0000000000000000000000000000000000000000..6cd29cefa858ba8b4fa190eda94460b0ed8758d6 Binary files /dev/null and b/public/avatar4.png differ diff --git a/public/avatar5.png b/public/avatar5.png new file mode 100644 index 0000000000000000000000000000000000000000..dfd4a3e4bb5cc039d5c7761a5ed10554eaeea685 Binary files /dev/null and b/public/avatar5.png differ diff --git a/public/avatar6.png b/public/avatar6.png new file mode 100644 index 0000000000000000000000000000000000000000..f6d7e19307e16f04afeda356e6833e8eebeab4a0 Binary files /dev/null and b/public/avatar6.png differ diff --git a/public/avatar7.png b/public/avatar7.png new file mode 100644 index 0000000000000000000000000000000000000000..a58f2ac0a551498b617991602a24d31fc92408b6 Binary files /dev/null and b/public/avatar7.png differ diff --git a/public/avatar8.png b/public/avatar8.png new file mode 100644 index 0000000000000000000000000000000000000000..d7b0f8fb11fdfdd8061380a02ae70df30c0de379 Binary files /dev/null and b/public/avatar8.png differ diff --git a/public/avatar9.png b/public/avatar9.png new file mode 100644 index 0000000000000000000000000000000000000000..1268037d1bfe19964d14ad784a951d1bca3fda34 Binary files /dev/null and b/public/avatar9.png differ diff --git a/src/components/CardChallengeSavingsPath.vue b/src/components/CardChallengeSavingsPath.vue index cb34ba2490ebcaa4e16a54ec2ac4823e210e4a0e..8ecfdfae70c584b2d6da61e1b27e5eb45b535761 100644 --- a/src/components/CardChallengeSavingsPath.vue +++ b/src/components/CardChallengeSavingsPath.vue @@ -107,7 +107,6 @@ const editChallenge = (challenge: Challenge) => { } // Helper methods to get icons const getChallengeIcon = (challenge: Challenge): string => { - //TODO change to challenge.icon return `src/assets/coffee.png` } </script> diff --git a/src/components/ModalEditAvatar.vue b/src/components/ModalEditAvatar.vue index 5474b34e6b346b1206405db3681641638b9f74ff..9233440acb54809f5e28ebdca602c87b81a5d8d0 100644 --- a/src/components/ModalEditAvatar.vue +++ b/src/components/ModalEditAvatar.vue @@ -1,65 +1,155 @@ <template> - <button @click="openModal" class="text-nowrap">Endre avatar</button> + <button @click="openModal" class="primary text-nowrap">Endre avatar</button> <div v-if="isModalOpen" class="fixed inset-0 bg-black bg-opacity-50 flex justify-center items-center z-50" > <div class="bg-white p-6 rounded-lg shadow-lg max-w-[80vh] h-auto w-full text-center"> + <div class="flex flex-row justify-end"> + <button @click="closeModal" class="primary ">X</button> + </div> <h2 class="title">Endre avatar</h2> - <div class="avatar-container flex flex-row justify-between items-center my-8"> + <div class="avatar-container flex flex-row justify-between gap-2 items-center my-8"> <button @click="cycleArray('prev')">â—€</button> <div class="flex flex-row items-center justify-around"> <img :src="previousAvatar" alt="avatar" class="avatar h-16 w-16" /> - <div class="border-4 rounded-full border-green-600 p-8 mx-4"> - <img :src="currentAvatar" alt="avatar" class="avatar h-40 w-40" /> - </div> + <img :src="currentAvatar" alt="avatar" class="avatar block mx-auto h-32 w-32 rounded-full border-green-600 border-2 sm:mx-0 sm:shrink-0" /> <img :src="nextAvatar" alt="avatar" class="avatar h-16 w-16" /> </div> <button @click="cycleArray('next')">â–¶</button> </div> - <button @click="saveAvatar" class="save-button">Lagre</button> + <div class="flex flex-row items-center gap-4 mx-auto"> + <button @click="saveAvatar" class=" primary save-button basis-1/2">Lagre</button> + <button @click="openFileExplorer" class="primary basis-1/2">Upload New Avatar</button> + </div> + <input type="file" ref="fileInput" @change="handleFileUpload" hidden /> + </div> </div> </template> <script setup lang="ts"> -import { ref } from 'vue' - -const isModalOpen = ref(false) -const avatars = [ - 'src/assets/coffee.png', - 'src/assets/head.png', - 'src/assets/nose.png', - 'src/assets/penger.png', - 'src/assets/pig.png' -] -let currentAvatarIndex = 0 +import { ref, reactive, computed } from 'vue'; +import { useUserStore } from "@/stores/userStore"; + +const userStore = useUserStore(); + +const state = reactive({ + avatars: [ + '/avatar1.png', + '/avatar2.png', + '/avatar3.png', + '/avatar4.png', + '/avatar5.png', + '/avatar6.png', + '/avatar7.png', + '/avatar8.png', + '/avatar9.png', + ], + currentAvatarIndex: 0, + newFile: null, // To hold the new file object + selectedPublicImg: '' // Track blob URLs created for uploaded files +}); + +const isModalOpen = ref(false); +const fileInput = ref<HTMLElement |null >(null); + +const emit = defineEmits(['update-profile-picture']); + const openModal = () => { - isModalOpen.value = !isModalOpen.value -} + state.avatars = [ + '/avatar1.png', + '/avatar2.png', + '/avatar3.png', + '/avatar4.png', + '/avatar5.png', + '/avatar6.png', + '/avatar7.png', + '/avatar8.png', + '/avatar9.png', + ]; + userStore.getProfilePicture(); + const urlProfilePicture = userStore.profilePicture; + // Check if a profile picture URL exists and append it to the avatars list + const img = localStorage.getItem('profilePicture') as string; + console.log(state.avatars) + console.log(img) + if (state.avatars.includes(state.selectedPublicImg) || state.avatars.includes(img)) { + // Remove the public asset from the list if it's already selected + state.avatars = state.avatars.filter(avatar => avatar !== state.selectedPublicImg); + console.log(state.avatars, 'state.avatars') + } + // Clear + console.log(state.avatars) + localStorage.removeItem('profilePicture'); + state.selectedPublicImg = ''; + + if (urlProfilePicture) { + state.avatars.push(urlProfilePicture); + state.currentAvatarIndex = state.avatars.length - 1; // Set the current avatar to the profile picture + } + isModalOpen.value = true; +}; + +const closeModal = () => { + isModalOpen.value = false; + //Remove the uploaded file if there is one. + state.avatars = [] -const nextAvatar = ref(avatars[(currentAvatarIndex + 1) % avatars.length]) -const currentAvatar = ref(avatars[currentAvatarIndex]) -const previousAvatar = ref(avatars[(currentAvatarIndex - 1 + avatars.length) % avatars.length]) + state.newFile = null; // Clear the new file reference +}; const cycleArray = (direction: string) => { - if (direction === 'prev') { - currentAvatarIndex = (currentAvatarIndex - 1 + avatars.length) % avatars.length - console.log(currentAvatarIndex) - currentAvatar.value = avatars[currentAvatarIndex] - previousAvatar.value = avatars[(currentAvatarIndex - 1 + avatars.length) % avatars.length] - nextAvatar.value = avatars[(currentAvatarIndex + 1) % avatars.length] - } else { - currentAvatarIndex = (currentAvatarIndex + 1) % avatars.length - currentAvatar.value = avatars[currentAvatarIndex] - previousAvatar.value = avatars[(currentAvatarIndex - 1 + avatars.length) % avatars.length] - nextAvatar.value = avatars[(currentAvatarIndex + 1) % avatars.length] - } -} - -const saveAvatar = () => { - localStorage.setItem('avatar', currentAvatar.value) - isModalOpen.value = false -} + if (direction === 'prev') { + state.currentAvatarIndex = (state.currentAvatarIndex - 1 + state.avatars.length) % state.avatars.length; + } else { + state.currentAvatarIndex = (state.currentAvatarIndex + 1) % state.avatars.length; + } +}; + +const handleFileUpload = async (event: any) => { + const input = event.target; + if (input.files && input.files[0]) { + const file = input.files[0]; + // Clear any existing temporary blob URLs + state.avatars = state.avatars.filter(avatar => !avatar.startsWith('blob:')); + state.newFile = file; // Save the new file object for later upload + state.avatars.push(URL.createObjectURL(file)); // Add the blob URL for preview + state.currentAvatarIndex = state.avatars.length - 1; // Set this new avatar as current + } +}; + +const saveAvatar = async () => { + if (state.newFile &¤tAvatar.value.startsWith('blob:')) { + // If there's a new file selected, upload it + const formData = new FormData(); + formData.append('file', state.newFile); + await userStore.uploadProfilePicture(formData); + } else if (currentAvatar.value.startsWith('/')) { + // If it's a public asset, fetch it as a blob and upload + state.selectedPublicImg = currentAvatar.value; + const response = await fetch(currentAvatar.value); + const blob = await response.blob(); + const file = new File([blob], 'public-avatar.png', { type: blob.type }); + const formData = new FormData(); + formData.append('file', file); + await userStore.uploadProfilePicture(formData); + localStorage.setItem('profilePicture', currentAvatar.value) + } + closeModal(); + emit('update-profile-picture', currentAvatar.value); +}; + +const openFileExplorer = () => { + fileInput.value?.click(); +}; + +const currentAvatar = computed(() => state.avatars[state.currentAvatarIndex]); +const nextAvatar = computed(() => state.avatars[(state.currentAvatarIndex + 1) % state.avatars.length]); +const previousAvatar = computed(() => state.avatars[(state.currentAvatarIndex - 1 + state.avatars.length) % state.avatars.length]); </script> + + + + diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts index 35d6374e397c0c66a70dbe5abe36c30c5803fa9c..734efa6e5c81281c3d26dcb5e44f021a9c38ced6 100644 --- a/src/stores/userStore.ts +++ b/src/stores/userStore.ts @@ -21,6 +21,7 @@ export const useUserStore = defineStore('user', () => { const user = ref<User>(defaultUser) const errorMessage = ref<string>('') const streak = ref<Streak>() + const profilePicture = ref<string>('') const register = async ( firstname: string, @@ -246,6 +247,27 @@ export const useUserStore = defineStore('user', () => { user.value.isConfigured = false }) } + // Inside your store or component methods + const uploadProfilePicture = async (formData: FormData) => { + try { + const response = await authInterceptor.post('/profile/picture', formData, { + headers: { 'Content-Type': 'multipart/form-data' }, + }); + console.log('Upload successful:', response.data); + } catch (error: any) { + console.error('Failed to upload profile picture:', error.response.data); + } + }; + + const getProfilePicture = async () => { + try { + const imageResponse = await authInterceptor.get('/profile/picture', { responseType: 'blob' }); + profilePicture.value = URL.createObjectURL(imageResponse.data); + } catch (error: any) { + console.error('Failed to retrieve profile picture:', error.response.data); + } + }; + return { user, @@ -257,6 +279,9 @@ export const useUserStore = defineStore('user', () => { bioRegister, errorMessage, getUserStreak, - streak + streak, + uploadProfilePicture, + getProfilePicture, + profilePicture, } }) diff --git a/src/views/ViewProfileView.vue b/src/views/ViewProfileView.vue index 4670d24cea15ba6a7f07ee238d21150844e8599f..7acdde05c7b19a8fdcec41cab63ea177ea992eff 100644 --- a/src/views/ViewProfileView.vue +++ b/src/views/ViewProfileView.vue @@ -15,6 +15,9 @@ const profile = ref<Profile>() const completedGoals = ref<Goal[]>([]) const completedChallenges = ref<Challenge[]>([]) const speech = ref<string[]>([]) +const profilePicture = ref<string>() + +const userStore = useUserStore(); const updateUser = async () => { authInterceptor('/profile') @@ -30,6 +33,7 @@ const updateUser = async () => { onMounted(async () => { await updateUser() + await authInterceptor(`/goals/completed?page=0&size=3`) .then((response) => { completedGoals.value = response.data.content @@ -46,6 +50,8 @@ onMounted(async () => { return console.log(error) }) + await userStore.getProfilePicture() + profilePicture.value = userStore.profilePicture; openSpare() }) const updateBiometrics = async () => { @@ -53,6 +59,12 @@ const updateBiometrics = async () => { await updateUser() } +const updateProfilePicture =async () => { + await updateUser() + await userStore.getProfilePicture() + profilePicture.value = userStore.profilePicture; +} + const openSpare = () => { speech.value = [ `Velkommen, ${profile.value?.firstName} ${profile.value?.lastName} !`, @@ -68,10 +80,11 @@ const openSpare = () => { <div class="flex flex-col max-w-96 w-full gap-5"> <h1>Profile</h1> <div class="flex flex-row gap-5"> - <div class="flex flex-col"> + <div class="flex flex-col gap-1"> + - <div class="w-32 h-32 border-black border-2 rounded-full shrink-0" /> - <ModalEditAvatar /> + <img :src="profilePicture" alt="could not load" class="block mx-auto h-32 rounded-full border-green-600 border-2 sm:mx-0 sm:shrink-0"/> + <ModalEditAvatar @update-profile-picture="updateProfilePicture" /> </div> <div class="w-full flex flex-col justify-between"> <h3 class="font-thin my-0">{{ profile?.username }}</h3>