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/ButtonDisplayStreak.vue b/src/components/ButtonDisplayStreak.vue index 1949a487de56cf33670aa12ad603b459b0ac81a7..883fcea10530af3c8288adea0220652b26ce7f10 100644 --- a/src/components/ButtonDisplayStreak.vue +++ b/src/components/ButtonDisplayStreak.vue @@ -1,6 +1,5 @@ <template> - <div class="flex flex-col items-center absolute"> - <span class="text-sm text-bold">STREAK</span> + <div class="flex flex-col items-center relative"> <button @mouseover="display" @mouseleave="hide" @@ -15,7 +14,7 @@ <div v-if="displayStreakCard" - class="w-[30vh] h-[20vh] md:w-auto md:h-auto group z-50 bg-opacity-50 overflow-hidden absolute left-0 top-14 md:top-20 flex flex-col justify-evenly text-wrap" + class="w-[30vh] h-[20vh] md:w-auto md:h-auto group z-50 bg-opacity-50 overflow-hidden absolute right-[-4rem] top-14 md:top-20 flex flex-col justify-evenly text-wrap" > <div class="flex flex-col justify-evenly w-full h-full py-2 px-4 md:py-0 bg-white rounded-2xl border-4 border-green-300" @@ -60,7 +59,7 @@ class="flex flex-row items-center mx-auto h-20 w-4/5 md:w-full bg-black-400 gap-4" > <div class="flex flex-1 overflow-x-auto"> - <div v-for="index in 7" :key="index" class="min-w-max mx-auto"> + <div v-for="index in 6" :key="index" class="min-w-max mx-auto"> <div class="flex flex-col justify-around items-center"> <!-- Display the current streak day number adjusted by index --> <span class="text-black text-xs md:text-1xl font-bold"> diff --git a/src/components/CardChallengeSavingsPath.vue b/src/components/CardChallengeSavingsPath.vue index aca6c5cb08c3119cc62ddae0d1fcd4613082f2d7..3acd26ccedfea9407170c929ca5f8e7733adae52 100644 --- a/src/components/CardChallengeSavingsPath.vue +++ b/src/components/CardChallengeSavingsPath.vue @@ -84,10 +84,6 @@ const challengeStore = useChallengeStore() const challengeImageUrl = ref('/src/assets/star.png') // Default or placeholder image const props = defineProps<{ challenge: Challenge }>() -interface Props { - challenge: Challenge -} - const emit = defineEmits(['update-challenge', 'complete-challenge']) // Increment saved amount diff --git a/src/components/ModalEditAvatar.vue b/src/components/ModalEditAvatar.vue index 5474b34e6b346b1206405db3681641638b9f74ff..459190f7713707ab23d9ee66ecd9df46b45c016f 100644 --- a/src/components/ModalEditAvatar.vue +++ b/src/components/ModalEditAvatar.vue @@ -1,65 +1,161 @@ <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' +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 avatars = [ - 'src/assets/coffee.png', - 'src/assets/head.png', - 'src/assets/nose.png', - 'src/assets/penger.png', - 'src/assets/pig.png' -] -let currentAvatarIndex = 0 +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 nextAvatar = ref(avatars[(currentAvatarIndex + 1) % avatars.length]) -const currentAvatar = ref(avatars[currentAvatarIndex]) -const previousAvatar = ref(avatars[(currentAvatarIndex - 1 + avatars.length) % avatars.length]) +const closeModal = () => { + isModalOpen.value = false + //Remove the uploaded file if there is one. + state.avatars = [] + + 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] + state.currentAvatarIndex = + (state.currentAvatarIndex - 1 + state.avatars.length) % state.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] + state.currentAvatarIndex = (state.currentAvatarIndex + 1) % state.avatars.length } } -const saveAvatar = () => { - localStorage.setItem('avatar', currentAvatar.value) - isModalOpen.value = false +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 && currentAvatar.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/components/NavBarComponent.vue b/src/components/NavBarComponent.vue index 924816689e21f4e077342f8c2a448454838c8a5d..951c4981cc34faaed0e6586fd69b4ed08c69aba5 100644 --- a/src/components/NavBarComponent.vue +++ b/src/components/NavBarComponent.vue @@ -1,6 +1,6 @@ <template> <nav class="flex justify-between items-center min-h-32 text-xl w-full px-3 my-0"> - <div> + <div class="order-first basis-1/5"> <router-link to="/hjem" @click="hamburgerOpen = false"> <img alt="logo" @@ -8,12 +8,8 @@ src="@/assets/spareSti.png" /> </router-link> - - <div class="flex flex-row justify-center"> - <ButtonDisplayStreak /> - </div> </div> - <div v-if="!isHamburger" class="flex flex-row gap-10"> + <div v-if="!isHamburger" class="flex flex-row justify-center gap-10 mx-auto basis-3/5"> <router-link active-class="border-b-2" to="/hjem">ðŸ Hjem</router-link> <router-link active-class="border-b-2" to="/sparemaal">🎯SparemÃ¥l</router-link> <router-link active-class="border-b-2" to="/spareutfordringer" @@ -22,15 +18,19 @@ <router-link active-class="border-b-2" to="/profil">ðŸ¤Profil</router-link> </div> - <div v-if="!isHamburger" class="flex justify-center w-40"> + <div v-if="!isHamburger" class="flex-row flex gap-2 justify-end w-auto h-14 basis-1/5"> + <ButtonDisplayStreak /> <button - class="primary bg-[#95e35d] logout focus:ring focus:ring-black-300" + class="primary basis-1/2 bg-[#95e35d] logout focus:ring focus:ring-black-300 text-nowrap" @click="openModal" > Logg ut </button> </div> - <button class="primary logout" v-if="isHamburger" @click="toggleMenu">☰</button> + <div class="flex flex-row gap-2"> + <ButtonDisplayStreak v-if="isHamburger" /> + <button class="primary logout" v-if="isHamburger" @click="toggleMenu">☰</button> + </div> </nav> <div v-if="hamburgerOpen" class="flex flex-col bg-white border border-slate-300 z-50"> diff --git a/src/stores/userStore.ts b/src/stores/userStore.ts index 3bdb2eb0cf9532f9e598879ee43ac1df7b0580c4..4f640e3e4fe2d21397328d0fa6dda1fab9890d23 100644 --- a/src/stores/userStore.ts +++ b/src/stores/userStore.ts @@ -19,6 +19,7 @@ export const useUserStore = defineStore('user', () => { }) const errorMessage = ref<string>('') const streak = ref<Streak>() + const profilePicture = ref<string>('') const register = async ( firstName: string, @@ -244,6 +245,28 @@ 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, @@ -255,6 +278,9 @@ export const useUserStore = defineStore('user', () => { bioRegister, errorMessage, getUserStreak, - streak + streak, + uploadProfilePicture, + getProfilePicture, + profilePicture } }) diff --git a/src/views/ManageProfileView.vue b/src/views/ManageProfileView.vue index adc8b26a59ce9dabf1a9d7743bc760151bebf558..470edfffaae114078d0ae427ee241ebddb223575 100644 --- a/src/views/ManageProfileView.vue +++ b/src/views/ManageProfileView.vue @@ -28,6 +28,7 @@ const updatePassword = ref<boolean>(false) const confirmPassword = ref<string>('') const errorMessage = ref<string>('') const isModalOpen = ref(false) +const image = ref<File>() const nameRegex = /^[æÆøØåÅa-zA-Z,.'-][æÆøØåÅa-zA-Z ,.'-]{0,29}$/ const emailRegex = @@ -89,6 +90,24 @@ onMounted(async () => { }) }) +const selectImage = async () => { + const fileInput = document.getElementById('fileInput')! as HTMLInputElement + if (!fileInput) { + // Error handling + + console.log('Vi klarte ikke Ã¥ hente bildene dine. Prøv igjen!') + } + if (!fileInput.files) { + return + } + image.value = fileInput.files[0] +} +const uploadImage = async () => { + // bildet mÃ¥ lastes opp som en form. altsÃ¥ en body med form + // const formData = new FormData() + // authInterceptor.post("/profile/picture", formData) +} + const saveChanges = async () => { if (isFormInvalid.value) { errorMessage.value = 'Vennligst fyll ut alle feltene riktig' @@ -124,7 +143,17 @@ const saveChanges = async () => { <button class="h-min bg-transparent text-4xl" v-text="'âž¡ï¸'" /> </div> </div> + <div class="flex flex-row justify-center"> + <input + id="fileInput" + type="file" + style="display: none" + accept=".jpg, .jpeg, .png, .gif, .img" + /> + <button v-text="'Velg eget bilde!'" @click="selectImage()" /> + <button v-text="'Send bilde'" @click="uploadImage()" /> + </div> <div class="flex flex-col"> <div class="flex flex-row justify-between mx-4"> <p>Fornavn*</p> diff --git a/src/views/ViewProfileView.vue b/src/views/ViewProfileView.vue index 86647f4720092860ade5c8cd7c5dcb8a1fec9197..d166da3316e4e1bdbe270c177ccfe95a12a783df 100644 --- a/src/views/ViewProfileView.vue +++ b/src/views/ViewProfileView.vue @@ -9,11 +9,15 @@ import CardGoal from '@/components/CardGoal.vue' import router from '@/router' import SpareComponent from '@/components/SpareComponent.vue' import { useUserStore } from '@/stores/userStore' +import ModalEditAvatar from '@/components/ModalEditAvatar.vue' 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') @@ -44,6 +48,8 @@ onMounted(async () => { return console.log(error) }) + await userStore.getProfilePicture() + profilePicture.value = userStore.profilePicture openSpare() }) @@ -52,6 +58,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}! 🤠`, @@ -67,7 +79,14 @@ const openSpare = () => { <div class="flex flex-col max-w-96 w-full gap-5"> <h1>Profil</h1> <div class="flex flex-row gap-5"> - <div class="w-32 h-32 border-slate-200 border-2 rounded-full shrink-0" /> + <div class="flex flex-col gap-1"> + <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 md:text-xl text-lg">{{ profile?.username }}</h3> <h3 class="font-thin my-0 md:text-xl text-lg">