diff --git a/package-lock.json b/package-lock.json index f9a952e5230487372e33df21b45f2738c2457af3..531c3213c1bbce25c61ab9721471562475c8d51b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@vuepic/vue-datepicker": "^8.5.0", "chart.js": "^4.4.2", + "js-confetti": "^0.12.0", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "vue": "^3.4.21", @@ -5004,6 +5005,11 @@ "node": ">=14" } }, + "node_modules/js-confetti": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/js-confetti/-/js-confetti-0.12.0.tgz", + "integrity": "sha512-1R0Akxn3Zn82pMqW65N1V2NwKkZJ75bvBN/VAb36Ya0YHwbaSiAJZVRr/19HBxH/O8x2x01UFAbYI18VqlDN6g==" + }, "node_modules/js-cookie": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz", diff --git a/package.json b/package.json index cecc1638d69308e212ada1ced917a4b254ded18d..6f66083185f9528e0545e2de9ee07b78dbb7a1b0 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,7 @@ "dependencies": { "@vuepic/vue-datepicker": "^8.5.0", "chart.js": "^4.4.2", + "js-confetti": "^0.12.0", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", "vue": "^3.4.21", diff --git a/src/components/challenge/ActiveChallengeDisplay.vue b/src/components/challenge/ActiveChallengeDisplay.vue index fbacb0668a0e17c4cda05fadb4151653d10d61b8..f1fb206228b5f021f77978f684b13974d2dfeb97 100644 --- a/src/components/challenge/ActiveChallengeDisplay.vue +++ b/src/components/challenge/ActiveChallengeDisplay.vue @@ -1,27 +1,25 @@ <script setup lang="ts"> -import { completeChallenge } from '@/utils/challengeutils' -import { useTokenStore } from '@/stores/token' -import { ref } from 'vue' - -const token:string = useTokenStore().jwtToken; const emits = defineEmits(['challengeCompleted']); -const milestoneId = ref(1) + +interface Challenge{ + 'challengeId':number, + 'challengeTitle':string, + 'challengeDescription':string, + 'goalSum':number, + 'expirationDate':string +} const props = defineProps({ - challengeId: Number, - challengeTitle: String, - challengeDescription: String + challenge: { + type: Object as () => Challenge, + required: true + } }); -const completeTheChallenge = async () => { - if(props.challengeId){ - try{ - await completeChallenge(token,props.challengeId, milestoneId.value) - emits('challengeCompleted', props.challengeId); - } catch (error){ - alert('Noe gikk galt! Venligst prøv på nytt!') - } +const completeTheChallenge = () => { + if(props.challenge.challengeId){ + emits('challengeCompleted', props.challenge.challengeId); } } @@ -29,8 +27,12 @@ const completeTheChallenge = async () => { <template> <div class="potential-challenge-display"> - <h3 class="title">{{ props.challengeTitle }}</h3> - <h4 class="description">{{ props.challengeDescription }}</h4> + <h3 class="title">{{ props.challenge.challengeTitle }}</h3> + <h4 class="description">{{ props.challenge.challengeDescription }}</h4> + <div class="on-object-hover"> + <h4>Utløpsdato: {{props.challenge.expirationDate}} |</h4> + <h4 class="sum"> Sparesum: {{props.challenge.goalSum}} kr,-</h4> + </div> <div class="button-container"> <button class="complete-button" @click="completeTheChallenge()"> <h3 class="complete-button-text">Fullfør</h3> @@ -47,6 +49,7 @@ const completeTheChallenge = async () => { padding: 1.5%; place-content: space-between; } + .title{ text-align: center; color: var(--color-text-black); @@ -57,6 +60,23 @@ const completeTheChallenge = async () => { color: var(--color-text-black); } +.on-object-hover{ + display: none; +} + + +.potential-challenge-display:hover{ + .on-object-hover{ + display: flex; + flex-direction: row; + place-content: center; + gap: 1.0%; + } + .description{ + display: none; + } +} + .button-container{ display: flex; width: 100%; diff --git a/src/components/challenge/CompleteChallengePopUp.vue b/src/components/challenge/CompleteChallengePopUp.vue new file mode 100644 index 0000000000000000000000000000000000000000..1ac824b6a1ef55b7cf71d93d0be8e82f5bae7056 --- /dev/null +++ b/src/components/challenge/CompleteChallengePopUp.vue @@ -0,0 +1,173 @@ +<script setup lang="ts"> + +import { onMounted, ref } from 'vue' +import { useTokenStore } from '@/stores/token' +import { getAllMilestones } from '@/utils/MilestoneUtils' +import { completeChallenge } from '@/utils/challengeutils' + +interface Milestone{ + 'milestoneId': number, + 'milestoneTitle': string +} + +const props = defineProps({ + challengeId: Number, +}); + +const token:string = useTokenStore().jwtToken; +const emit = defineEmits(['closePopUp', 'challengeCompleted']); + +const milestones = ref<Milestone[]>([]); +const chosenMilestone = ref<number|null>(null); +const chosenMileStoneError = ref<string|null>(null) + + +onMounted(async () => { + try { + await fetchAllMilestones(); + } catch (error) { + console.error('Error fetching user info:', error); + } +}) +const fetchAllMilestones = async () =>{ + try{ + milestones.value = await getAllMilestones(token) + chosenMilestone.value = milestones.value[0].milestoneId; + + } catch (error){ + console.error('Error fetching user info:', error); + } +} + +const cancelCompleteThisChallenge = () => { + emit('closePopUp'); +} + +const completeThisChallenge = async () => { + if(chosenMilestone.value && props.challengeId) + try{ + console.log(props.challengeId) + console.log(chosenMilestone.value) + await completeChallenge(token, props.challengeId, chosenMilestone.value); + emit('challengeCompleted'); + + }catch (error){ + console.log(error) + chosenMileStoneError.value = 'Noe gikk galt! Venligst prøv på nytt!' + } +} + +</script> + +<template> + <div class="popup-content"> + <!-- Pop-up content goes here --> + <h2>Venligst velg et sparemål!</h2> + <h3>Velg sparemålet som skal motta sparebeløpet fra utforderingen 🎉</h3> + + <select class="milestones" :class="{'error': chosenMileStoneError}" v-model="chosenMilestone"> + <option v-for="(milestone, index) in milestones" + :key="index" :value="milestone.milestoneId">{{ milestone.milestoneTitle }}</option> + </select> + <div class="alert-box"> + <h3 class="error-message" v-if="chosenMileStoneError">{{chosenMileStoneError}}</h3> + </div> + + <div class="option-buttons"> + <button class="option-button" id="cancel-button" @click="cancelCompleteThisChallenge()"> + <h2 class="option-button-title">Avbryt</h2> + </button> + <button class="option-button" id="complete-button" @click="completeThisChallenge()"> + <h2 class="option-button-title">Fullfør</h2> + </button> + </div> + </div> + +</template> + +<style scoped> +.popup-content { + display: flex; + flex-direction: column; + + width: 50%; + height: 50%; + background-color: var(--color-background); + + padding: 20px; + border-radius: 10px; + border: 2px solid var(--color-border); + + place-content: space-between; + +} + +.milestones{ + height: 15%; + width: 100%; + border-radius: 20px; + border: 2px solid var(--color-border) +} + +.milestones:hover{ + transform: scale(1.02); +} + +.option-buttons{ + display: flex; + flex-direction: row; + + width: 100%; + place-content: space-between; +} + +.option-button{ + border: none; + border-radius: 20px; + width: 35%; + +} + +.option-button-title{ + color: var(--color-headerText); + font-weight: bold; +} + + +.alert-box{ + display: flex; + flex-direction: column; + place-items: center; + min-height: 20px; +} + +.error-message{ + color: var(--color-text-error); +} +#cancel-button{ + background-color: var(--color-cancel-button); +} +#cancel-button:active{ + background-color: var(--color-cancel-button-click); +} + +#complete-button{ + background-color: var(--color-confirm-button); +} +#complete-button:active{ + background-color: var(--color-confirm-button-click); +} + +#complete-button:hover, #cancel-button:hover{ + transform: scale(1.02); +} + +@media only screen and (max-width: 1000px){ + .popup-content { + width: 90%; + height: 60%; + } +} + + +</style> \ No newline at end of file diff --git a/src/components/challenge/PotentialChallengeDisplay.vue b/src/components/challenge/PotentialChallengeDisplay.vue index 8ba724bf6666eeeb37a3154e4d49f8fc9cffeb02..18ce8749dc51df55cece04104eb3c4a78e629baf 100644 --- a/src/components/challenge/PotentialChallengeDisplay.vue +++ b/src/components/challenge/PotentialChallengeDisplay.vue @@ -6,18 +6,27 @@ import { useTokenStore } from '@/stores/token' const token:string = useTokenStore().jwtToken; const emits = defineEmits(['challengeAccepted', 'challengeDeclined']); +interface Challenge{ + 'challengeId':number, + 'challengeTitle':string, + 'challengeDescription':string, + 'goalSum':number, + 'expirationDate':string +} + const props = defineProps({ - challengeId: Number, - challengeTitle: String, - challengeDescription: String + challenge: { + type: Object as () => Challenge, + required: true + } }); const declineChallenge = async () => { console.log('decline-button clicked') - if(props.challengeId){ + if(props.challenge.challengeId){ try{ - await deleteChallenge(token, props.challengeId); - emits('challengeDeclined', props.challengeId); + await deleteChallenge(token, props.challenge.challengeId); + emits('challengeDeclined', props.challenge.challengeId); } catch (error){ alert('Noe gikk galt! Venligst prøv på nytt.') } @@ -27,10 +36,10 @@ const declineChallenge = async () => { } const acceptChallenge = async () => { - if(props.challengeId){ + if(props.challenge.challengeId){ try{ - await activateChallenge(token, props.challengeId); - emits('challengeAccepted', props.challengeId); + await activateChallenge(token, props.challenge.challengeId); + emits('challengeAccepted', props.challenge.challengeId); } catch (error){ alert('Noe gikk galt! Venligst prøv på nytt.') } @@ -40,8 +49,12 @@ const acceptChallenge = async () => { <template> <div class="potential-challenge-display"> - <h2 class="title">{{ props.challengeTitle }}</h2> - <h4 class="description">{{ props.challengeDescription }}</h4> + <h2 class="title">{{ props.challenge.challengeTitle }}</h2> + <h4 class="description">{{ props.challenge.challengeDescription }}</h4> + <div class="info"> + <h4>Utløpsdato: {{props.challenge.expirationDate}} |</h4> + <h4 class="sum"> Sparesum: {{props.challenge.goalSum}} kr,-</h4> + </div> <div class="options"> <button class="option-button" id="decline-button" @click="declineChallenge"> <h3 class="button-text">Avslå</h3> @@ -69,6 +82,13 @@ const acceptChallenge = async () => { .description{ text-align: center; } +.info{ + display: flex; + flex-direction: row; + place-content: center; + gap: 1.0%; +} + .options{ display: flex; diff --git a/src/utils/MilestoneUtils.ts b/src/utils/MilestoneUtils.ts index 9c141556e17818b5086a6a8fd643b21312203dc8..8897b5f3d0c5445d09300aefaf06d5a3e722a536 100644 --- a/src/utils/MilestoneUtils.ts +++ b/src/utils/MilestoneUtils.ts @@ -1,5 +1,4 @@ import axios from 'axios'; -import {useTokenStore} from "@/stores/token"; export const getAllMilestones = async(token: string) => { const config = { diff --git a/src/utils/challengeutils.ts b/src/utils/challengeutils.ts index d2340ef1d08e5a9157dfcd889752d9fc9a5920ce..840377301f4ec67512a26faa5decd62efa467fbe 100644 --- a/src/utils/challengeutils.ts +++ b/src/utils/challengeutils.ts @@ -130,7 +130,7 @@ export const getChallenge = async (token:string, challengeId: number):Promise<an } -export const getActiveChallenges = async (token:string):Promise<any>=>{ +export const getActiveChallenges = async (token:string, page:number, size:number):Promise<any>=>{ console.log(token) try{ const config = { @@ -139,8 +139,8 @@ export const getActiveChallenges = async (token:string):Promise<any>=>{ 'Authorization': `Bearer ${token}` }, params: { - 'page': 0, - 'size': 10 + 'page': page, + 'size': size } }; diff --git a/src/utils/profileutils.ts b/src/utils/profileutils.ts index 82f84680f8cc4882b9caadf9ae2cd49d1108e12d..a9e8349021ff11dae876072d9d337c6f6cb8722f 100644 --- a/src/utils/profileutils.ts +++ b/src/utils/profileutils.ts @@ -142,8 +142,8 @@ export const getUserInfo = async (token:string): Promise<any> => { const result = await axios.get('http://localhost:8080/users/get', config); return result.data; } catch (error){ - console.log('sending mock data') - return testDataUser; + console.log(error); + throw error; } } diff --git a/src/views/HomePage/ChallengeView.vue b/src/views/HomePage/ChallengeView.vue index 8ec7d54d62b4cb54e7ce18a70b2db831f5821ed6..9f2345318c5e63e423c29606c83863d47652d08b 100644 --- a/src/views/HomePage/ChallengeView.vue +++ b/src/views/HomePage/ChallengeView.vue @@ -1,16 +1,23 @@ <script setup lang="ts"> import PotentialChallengeDisplay from '@/components/challenge/PotentialChallengeDisplay.vue' -import { onMounted, ref } from 'vue' +import { onMounted, ref, watch } from 'vue' import ActiveChallengeDisplay from '@/components/challenge/ActiveChallengeDisplay.vue' import router from '@/router' import { useTokenStore } from '@/stores/token' import { getActiveChallenges, getInactiveChallenges } from '@/utils/challengeutils' +import CompleteChallengePopUp from '@/components/challenge/CompleteChallengePopUp.vue' + +import JSConfetti from 'js-confetti' + +const jsConfetti = new JSConfetti() interface Challenge{ - challengeId: number; - challengeTitle: string; - challengeDescription: string + challengeId: number, + challengeTitle: string, + challengeDescription: string, + goalSum:number, + expirationDate:string } const token:string = useTokenStore().jwtToken; @@ -18,12 +25,21 @@ const token:string = useTokenStore().jwtToken; const activeChallenges = ref<Challenge[]>([]) const inactiveChallenges = ref<Challenge[]>([]) +const SIZE = 4 + const pages = ref<number>(1) const currentPage = ref<number>(0) +const displayType = ref<boolean>(true); + +const displayPopUp = ref<boolean>(false); +const completedChallenge = ref<number|any>(null) + onMounted(async () => { try { + currentPage.value = 0; + await fetchInactiveChallenges(); await fetchActiveChallenges(); } catch (error) { @@ -33,16 +49,28 @@ onMounted(async () => { const fetchInactiveChallenges = async () => { try { - const response = await getInactiveChallenges(token) + const { content } = await getInactiveChallenges(token) + inactiveChallenges.value = [] - for (let i = 0; i < response.length; i++) { + + for (let i = 0; i < content.length; i++) { + + const date = new Date(content[i].expirationDate); + + const formattedDate = date.toLocaleDateString('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); + inactiveChallenges.value.push({ - challengeId: response[i].challengeId, - challengeTitle: response[i].challengeTitle, - challengeDescription: response[i].challengeDescription + challengeId: content[i].challengeId, + challengeTitle: content[i].challengeTitle, + challengeDescription: content[i].challengeDescription, + goalSum: content[i].goalSum, + expirationDate: formattedDate }) } - console.log(inactiveChallenges.value) } catch (error) { console.error('Error fetching active challenges:', error); @@ -51,15 +79,31 @@ const fetchInactiveChallenges = async () => { const fetchActiveChallenges = async () => { try{ - const response = await getActiveChallenges(token) - console.log(response) + const { content, totalPages, number } = + await getActiveChallenges(token, currentPage.value,SIZE) + + pages.value = totalPages; + currentPage.value = number; + activeChallenges.value = []; - for(let i = 0; i < response.length; i ++){ - console.log(response.data) + + for(let i = 0; i < content.length; i ++){ + console.log(content.data) + + const date = new Date(content[i].expirationDate); + + const formattedDate = date.toLocaleDateString('en-US', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); + activeChallenges.value.push({ - challengeId:response[i].challengeId, - challengeTitle:response[i].challengeTitle, - challengeDescription:response[i].challengeDescription + challengeId:content[i].challengeId, + challengeTitle:content[i].challengeTitle, + challengeDescription:content[i].challengeDescription, + goalSum:content[i].goalSum, + expirationDate:formattedDate }) } console.log(activeChallenges.value) @@ -80,58 +124,114 @@ const handleChallengeDeclined = async () => { await fetchInactiveChallenges(); } +const handleRequestToCompleteChallenge = (challengeId: number) => { + displayPopUp.value = true; + completedChallenge.value = challengeId; +} + const handleChallengeCompleted = async () => { + await closePopUp(); + await jsConfetti.addConfetti(); +} + +const closePopUp = async () => { + displayPopUp.value = false; await fetchActiveChallenges(); } -const previousPage = () => {} -const goToPage = (pageNumber:number) => { - currentPage.value = pageNumber; +const displayNewChallenges = () => { + displayType.value = false; } -const nextPage = () =>{} +const displayActiveChallenges = () => { + displayType.value = true; + +} const navigateTo = (path: string) => { router.push(path) } +const previousPage = () => { + currentPage.value -- +} +const goToPage = (pageNumber:number) => { + currentPage.value = pageNumber; +} + +const nextPage = () =>{ + currentPage.value ++; +} + +watch(currentPage, fetchActiveChallenges); </script> <template> - <div class="challenge-view"> + <div class="challenge-view" :aria-disabled="displayPopUp"> + + <div v-if="displayPopUp" class="popup-container"> + <CompleteChallengePopUp + :challenge-id="completedChallenge" + @closePopUp="closePopUp" + @challengeCompleted="handleChallengeCompleted" + ></CompleteChallengePopUp> + </div> + + <h2 class="title">Dine utfordringer</h2> + <div class="toggle-buttons"> + <button class="toggle-button" @click="displayActiveChallenges" :class="{ 'active-button': displayType}"> + <h3 class="toggle-button-title">Nye utfordringer</h3> + </button> + + <button class="toggle-button" @click="displayNewChallenges" :class="{ 'active-button': !displayType}"> + <h3 class="toggle-button-title">Aktive utfordringer</h3> + </button> + + </div> <div class="main"> - <div class="left"> + <div class="left" :class="{ 'mobile-hide': !displayType }"> + <button class="create-challenge-button" @click="navigateTo('/homepage/create-challenge')"> - <h2 class="create-challenge-button-title">Personlig utfordring + </h2> + <h2 class="create-challenge-button-title">Ny personlig utfordring + </h2> </button> <div class="challenge-recommendations"> <PotentialChallengeDisplay class="potential-challenge" v-for="(potentialChallenge, index) in inactiveChallenges" :key="index" - :challengeId="potentialChallenge.challengeId" - :challengeTitle="potentialChallenge.challengeTitle" - :challengeDescription="potentialChallenge.challengeDescription" + :challenge="potentialChallenge" @challengeAccepted="handleChallengeAccepted" @challengeDeclined="handleChallengeDeclined" ></PotentialChallengeDisplay> + + <h4 class="challenge-placeholder" v-if="inactiveChallenges.length == 0"> + Ojda, her gikk det unna.<br> + Vi har for øyeblikket ingen flere forslag til utfordringer. <br> + Lag din egen personlige utfordring eller kom tilbake senere! <br> + Nye utfordringer blir generert med gjevne mellomrom. + </h4> </div> - </div> - <div class="right"> + </div> + <div class="right" :class="{ 'mobile-hide': displayType }"> <h2 class="active-challenges-title">Aktive utfordringer</h2> - <div class="active-challenges"> - <ActiveChallengeDisplay - class="active-challenge" - v-for="(activeChallenge, index) in activeChallenges" - :key="index" - :challengeId="activeChallenge.challengeId" - :challengeTitle="activeChallenge.challengeTitle" - :challengeDescription="activeChallenge.challengeDescription" - @challengeCompleted="handleChallengeCompleted" - ></ActiveChallengeDisplay> + <div class="active-challenge-box"> + <div class="active-challenges"> + <ActiveChallengeDisplay + class="active-challenge" + v-for="(activeChallenge, index) in activeChallenges" + :key="index" + :challenge="activeChallenge" + @challengeCompleted="handleRequestToCompleteChallenge(activeChallenge.challengeId)" + ></ActiveChallengeDisplay> + <h4 class="challenge-placeholder" id="active-challenge-placeholder" v-if="activeChallenges.length == 0"> + Du har ingen aktive utfordringer.<br> + Lag din egen utfordring eller aksepter våre tilpassede forslag! + Aktive utfordringer vil vises i denne boksen. + </h4> + </div> <div class="pagination"> <button @click="previousPage" :disabled="currentPage === 0">Forige side</button> <div v-if="pages>0" class="page-numbers"> @@ -148,6 +248,7 @@ const navigateTo = (path: string) => { </div> </div> </div> + </template> <style scoped> @@ -155,6 +256,8 @@ const navigateTo = (path: string) => { display: flex; flex-direction: column; + gap: 2.5%; + height: 100%; width: 100%; } @@ -163,11 +266,15 @@ const navigateTo = (path: string) => { color: var(--color-heading); } +.toggle-buttons { + display: none; +} + .main{ display: flex; flex-direction: row; - height: 100%; + min-height: 100%; width: 100%; gap: 2.5%; } @@ -178,7 +285,6 @@ const navigateTo = (path: string) => { width: 60%; gap: 2.5%; } - .create-challenge-button{ border-radius: 20px; background-color: var(--color-confirm-button); @@ -202,19 +308,25 @@ const navigateTo = (path: string) => { .challenge-recommendations{ display: flex; flex-direction: column; - place-content: space-between; height: 100%; width: 100%; gap: 2.5%; } +.challenge-placeholder{ + text-align: center; +} + +#active-challenge-placeholder{ + color: var(--color-headerText); +} .potential-challenge{ border-radius: 20px; border: 2px solid var(--color-border); box-shadow: 0 4px 4px var(--color-shadow); - min-height: 30%; + height: calc(100%/3); width: 100%; } @@ -225,6 +337,8 @@ const navigateTo = (path: string) => { .right{ display: flex; flex-direction: column; + place-content: space-evenly; + border: 2px solid var(--color-border); border-radius: 20px; box-shadow: 0 4px 4px var(--color-shadow); @@ -239,12 +353,23 @@ const navigateTo = (path: string) => { font-weight: bold; } -.active-challenges{ +.active-challenge-box{ display: flex; flex-direction: column; + height: 100%; width: 100%; + padding: 5.0%; + + place-content: space-between; +} + +.active-challenges{ + display: flex; + flex-direction: column; + height: 100%; + width: 100%; gap:2.5% } @@ -253,7 +378,7 @@ const navigateTo = (path: string) => { border: 2px solid var(--color-border); background-color: var(--color-background-white); - min-height: calc(100%/4.8); + min-height: calc(calc(100% - 2.5*4%)/4); width: 100%; } @@ -311,7 +436,83 @@ const navigateTo = (path: string) => { } .chosen{ - background-color: black; + color: var(--color-heading); + font-weight: bold; +} + +.popup-container { + position: fixed; /* Change to fixed to cover the entire viewport */ + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + background-color: rgba(64, 64, 64, 0.5); + + align-items: center; + z-index: 1000; /* Adjust z-index as needed */ +} + + +@media only screen and (max-width: 1000px){ + .main{ + display: flex; + flex-direction: column; + + min-height: 100%; + width: 100%; + + padding-top: 1.0%; + padding-bottom: 1.0%; + } + + .toggle-buttons{ + display: flex; + flex-direction: row; + width: 100%; + min-height: 7.5%; + place-content: space-between; + } + + .toggle-button{ + width: 49.5%; + border-radius: 20px; + border: none; + background-color: var(--color-confirm-button); + } + + .toggle-button:hover{ + transform: scale(1.02); + } + + .toggle-button-title{ + font-weight: bold; + color: var(--color-headerText); + } + + .active-button{ + background-color: var(--color-confirm-button-click); + } + + .mobile-hide{ + display: none; + } + + .left{ + width: 100%; + height: 100%; + } + + .right{ + min-height: 110%; + width: 100%; + } + + .challenge-recommendations{ + min-height: 100%; + } + } </style> \ No newline at end of file