diff --git a/package-lock.json b/package-lock.json index 857fc779d547280a54648f9c8e8da6fbe00d12d8..f9a952e5230487372e33df21b45f2738c2457af3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,7 @@ "name": "sparesti_frontend", "version": "0.0.0", "dependencies": { + "@vuepic/vue-datepicker": "^8.5.0", "chart.js": "^4.4.2", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", @@ -2307,6 +2308,20 @@ "integrity": "sha512-VcZK7MvpjuTPx2w6blwnwZAu5/LgBUtejFOi3pPGQFXQN5Ela03FUtd2Qtg4yWGGissVL0dr6Ro1LfOFh+PCuQ==", "dev": true }, + "node_modules/@vuepic/vue-datepicker": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/@vuepic/vue-datepicker/-/vue-datepicker-8.5.0.tgz", + "integrity": "sha512-p8CHPJYJ1nQgrKzVBaDi1ZO9G9syuvOacPDNMF4uViHsXGdUyGLZbgrvvcmDTd0xYtyCUswiH6S27gb1E7qQ2Q==", + "dependencies": { + "date-fns": "^3.6.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "vue": ">=3.2.0" + } + }, "node_modules/abbrev": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", @@ -3264,6 +3279,15 @@ "node": ">=18" } }, + "node_modules/date-fns": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.6.0.tgz", + "integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/kossnocorp" + } + }, "node_modules/dayjs": { "version": "1.11.10", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", diff --git a/package.json b/package.json index 40577120b87af6b3a8a21208a17daa7741a914e1..cecc1638d69308e212ada1ced917a4b254ded18d 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,7 @@ "format": "prettier --write src/" }, "dependencies": { + "@vuepic/vue-datepicker": "^8.5.0", "chart.js": "^4.4.2", "pinia": "^2.1.7", "pinia-plugin-persistedstate": "^3.2.1", diff --git a/src/assets/base.css b/src/assets/base.css index 442cf4ed65f18f74431c4abf239b36102501bc5e..4a1cdbca58382bea3f2b33eef6a774cbbc8b8cb2 100644 --- a/src/assets/base.css +++ b/src/assets/base.css @@ -48,6 +48,9 @@ --color-inactive-button: var(--vt-c-borderGrey); --color-inactive-button-text: var(--vt-c-borderGreyDark); + --color-logout-button: var(--vt-c-DarkBlue-Light); + --color-logout-button-click: var(--vt-c-DarkBlue-Dark); + --color-PathYetToTakeLight: var(--vt-c-Grey-Light); --color-PathYetToTakeDark: var(--vt-c-Grey); --color-PathCurrentLight: var(--vt-c-kellyGreen-Light); diff --git a/src/components/MilestonePath/DirectTransfer.vue b/src/components/MilestonePath/DirectTransfer.vue new file mode 100644 index 0000000000000000000000000000000000000000..619ec9abb233bf0ace6065088e970522279ad02e --- /dev/null +++ b/src/components/MilestonePath/DirectTransfer.vue @@ -0,0 +1,63 @@ +<script setup lang="ts"> + +import { ref, defineEmits } from 'vue'; + +const transferValue = ref<number>(0); +const emits = defineEmits(['transfer-value']); + +function transfer() { + emits('transfer-value', transferValue.value); +} + +</script> + +<template> + <div id = DirectTransfer> + <h2 id = Title>Direkte overføring:</h2> + <div id = Transfer> + <input type="number" min = "1" id = TransferInput v-model="transferValue"/> + <button id = TransferButton @click = "transfer()">Overfør</button> + </div> + </div> +</template> + +<style scoped> + #DirectTransfer{ + border: 3px solid var(--color-border); + border-radius: 20px; + width: 100%; + padding: 2%; + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.6); + } + + #Title{ + font-size: 1.5em; + margin-left: 5%; + font-weight: bold; + } + + #Transfer{ + display: flex; + justify-content: space-evenly; + } + + #TransferInput{ + font-size: 1.5em; + border-radius: 20px; + border-color: var(--color-border); + } + + #TransferButton{ + border: 0; + border-radius: 20px; + padding: 3% 5% 3% 5%; + font-size: 1.5em; + background-color: var(--color-confirm-button); + color: var(--color-buttonText); + } + + + #TransferButton:hover{ + transform: scale(1.05); + } +</style> \ No newline at end of file diff --git a/src/components/MilestonePath/MilestoneDescription.vue b/src/components/MilestonePath/MilestoneDescription.vue new file mode 100644 index 0000000000000000000000000000000000000000..dd4b7d0a548ae10eecb048d5cb52fe4d363b5531 --- /dev/null +++ b/src/components/MilestonePath/MilestoneDescription.vue @@ -0,0 +1,46 @@ +<script setup lang="ts"> + +import { defineProps } from 'vue' + +const props = defineProps({ + pathDescription: String +}); + +</script> + +<template> + <div id = MilestoneDescription> + <h2 id = Title>SparemÃ¥ls beskrivelse:</h2> + <h3 id = Description>{{props.pathDescription}}</h3> + </div> +</template> + +<style scoped> + + #MilestoneDescription{ + border: 3px solid var(--color-border); + border-radius: 20px; + width: 100%; + min-height: 25vh; + max-height: 25vh; + padding: 2%; + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.6); + overflow: auto; + scrollbar-width: none; + -ms-overflow-style: none; + } + + #MilestoneDescription::-webkit-scrollbar { + display: none; + } + + #Title, #Description{ + font-size: 1.5em; + margin-left: 5%; + } + + #Title{ + font-weight: bold; + } + +</style> \ No newline at end of file diff --git a/src/components/MilestonePath/MilestonePath.vue b/src/components/MilestonePath/MilestonePath.vue new file mode 100644 index 0000000000000000000000000000000000000000..8cc39d4acc90884d9c8930a626288de706d4eb30 --- /dev/null +++ b/src/components/MilestonePath/MilestonePath.vue @@ -0,0 +1,59 @@ +<script setup lang="ts"> +import PathNode from '@/components/MilestonePath/PathNode.vue' +import { ref } from 'vue' +import { defineProps } from 'vue'; + + +const props = defineProps({ + totalToSave: Number, + totalSaved: Number +}); + +const totalNodes = ref(props.totalToSave ? Math.ceil(props.totalToSave / 250) : 0); +const remainingNodes = ref( + totalNodes.value && props.totalSaved && props.totalToSave + ? Math.ceil(totalNodes.value - (totalNodes.value * (props.totalSaved / props.totalToSave))) + : 0 +); + +if(props.totalSaved === 0){ + remainingNodes.value = totalNodes.value; +} + +const nodes = Array.from({ length: totalNodes.value }, (_, index) => ({ + offset: `${Math.random() > 0.5 ? Math.random() * 2 * 100 + 'px' : '-' + (Math.random() * 2 * 100 + 'px')}`, + colorIndex: index < remainingNodes.value - 1 ? 0 : (index === remainingNodes.value - 1 ? 1 : 2) +})); + + + +const nodeForegroundColors = ref(['#CCCCCF', '#A4ED45', '#FCBD47']) +const nodeBackgroundColors = ref(['#A4A4A6', '#6AB40A', '#FFA600']) + +</script> + +<template> + <div class="learning-path"> + <PathNode v-for="(node, index) in nodes" :key="index" :style="{ marginLeft: node.offset }" + :node-background-color="nodeBackgroundColors[node.colorIndex]" + :top-background-color="nodeForegroundColors[node.colorIndex]" + :bottom-background-color="nodeBackgroundColors[node.colorIndex]"/> + </div> +</template> + +<style scoped> +.learning-path { + display: flex; + flex-direction: column; + align-items: center; + width: 100%; + height: 100%; + overflow: scroll; + scrollbar-width: none; + -ms-overflow-style: none; +} + +.learning-path::-webkit-scrollbar { + display: none; +} +</style> diff --git a/src/components/MilestonePath/MilestoneProgress.vue b/src/components/MilestonePath/MilestoneProgress.vue new file mode 100644 index 0000000000000000000000000000000000000000..33ed1b441952dab3fda123d54412b888406ccea9 --- /dev/null +++ b/src/components/MilestonePath/MilestoneProgress.vue @@ -0,0 +1,50 @@ +<script setup lang="ts"> + +import ProgressBar from '@/components/ProgressBar.vue' + +import { defineProps } from 'vue'; + +const props = defineProps({ + totalToSave: Number, + totalSaved: Number +}); + + +</script> + +<template> + <div id = MilestoneProgress> + <h3 id = TotalSavings>Totalt oppspart:</h3> + <h3 id = Savings>{{props.totalSaved + " / " + props.totalToSave + " NOK"}}</h3> + <ProgressBar :Max="props.totalToSave || 0" :Current="props.totalSaved || 0" id = progress /> + </div> +</template> + +<style scoped> + + #TotalSavings, #Savings{ + font-size: 1.5em; + margin-left: 5%; + } + + #TotalSavings{ + font-weight: bold; + } + + #MilestoneProgress{ + border: 3px solid var(--color-border); + border-radius: 20px; + width: 100%; + height: 30%; + padding-left: 1%; + padding-right: 1%; + box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.6); + } + + #progress{ + display: flex; + justify-content: center; + align-items: center; + } + +</style> \ No newline at end of file diff --git a/src/components/MilestonePath/PathNode.vue b/src/components/MilestonePath/PathNode.vue new file mode 100644 index 0000000000000000000000000000000000000000..a9fa3f022f2730cfe80a5225697afab78a5cc17a --- /dev/null +++ b/src/components/MilestonePath/PathNode.vue @@ -0,0 +1,39 @@ +<script setup lang="ts"> +import { defineProps } from 'vue'; + + +const props = defineProps({ + nodeBackgroundColor: String, + topBackgroundColor: String, + bottomBackgroundColor: String +}); +</script> + +<template> + <div class="Node" :style="{ backgroundColor : nodeBackgroundColor}"> + <div id = top :style="{backgroundColor : topBackgroundColor}"></div> + <div id = bottom :style="{backgroundColor: bottomBackgroundColor}"></div> + </div> +</template> + +<style scoped> + + .Node{ + border-radius: 50%; + margin: 3%; + } + + #top, #bottom{ + min-width: 100px; + min-height: 30px; + border-radius: 50%; + } + + #top{ + } + + #bottom{ + } + + +</style> \ No newline at end of file diff --git a/src/components/ProgressBar.vue b/src/components/ProgressBar.vue index c5fddd34a16633a5649a2fda004e16a2a4ce3c79..ccc5b5391dc7d82760fe44d8f41dcb442c1a99e1 100644 --- a/src/components/ProgressBar.vue +++ b/src/components/ProgressBar.vue @@ -25,9 +25,8 @@ <style scoped> #QuestionProgress{ - width: 80vw; + width: 90%; height: 10vh; - margin-top: 1%; } @media only screen and (max-width: 1000px) { diff --git a/src/components/__tests__/DirectTransfer.spec.ts b/src/components/__tests__/DirectTransfer.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..3713f1c9a1efa7a5833b605bc6826ceb8c6d7e09 --- /dev/null +++ b/src/components/__tests__/DirectTransfer.spec.ts @@ -0,0 +1,32 @@ +import { mount } from '@vue/test-utils'; +import DirectTransfer from '@/components/MilestonePath/DirectTransfer.vue'; +import { describe, it, expect } from 'vitest' + +describe('DirectTransfer', () => { + it('updates transferValue when input changes', async () => { + const wrapper = mount(DirectTransfer); + const input = wrapper.find('input[type="number"]'); + + await input.setValue(50); + const actualValue = (wrapper.vm as any).transferValue; + + expect(actualValue).toBe(50); + }); + + it('emits "transfer-value" event with correct value when button is clicked', async () => { + const wrapper = mount(DirectTransfer); + const input = wrapper.find('input[type="number"]'); + const button = wrapper.find('button'); + + await input.setValue(50); + await button.trigger('click'); + + const emittedValue = wrapper.emitted('transfer-value'); + expect(emittedValue).toBeTruthy(); + + if (emittedValue) { + expect(emittedValue[0]).toEqual([50]); + } + }); + +}); diff --git a/src/components/__tests__/MilestoneDescription.spec.ts b/src/components/__tests__/MilestoneDescription.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..09bb6bc70586452294fd5e54bcebd0af2f930fde --- /dev/null +++ b/src/components/__tests__/MilestoneDescription.spec.ts @@ -0,0 +1,27 @@ +import { mount } from '@vue/test-utils' +import MilestoneDescription from '@/components/MilestonePath/MilestoneDescription.vue'; +import { describe, it, expect } from 'vitest' + +describe('MilestoneDescription', () => { + it('renders path description correctly', () => { + const props = { + pathDescription: 'This is a sample path description.' + } + const wrapper = mount(MilestoneDescription, { + props + }) + const description = wrapper.find('#Description') + expect(description.text()).toBe(props.pathDescription) + }) + + it('renders title correctly', () => { + const props = { + pathDescription: 'This is a sample path description.' + } + const wrapper = mount(MilestoneDescription, { + props + }) + const title = wrapper.find('#Title') + expect(title.text()).toBe('SparemÃ¥ls beskrivelse:') + }) +}) diff --git a/src/components/__tests__/MilestonePath.spec.ts b/src/components/__tests__/MilestonePath.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..f383ca97471670acec2443db2ce801dc875411ff --- /dev/null +++ b/src/components/__tests__/MilestonePath.spec.ts @@ -0,0 +1,75 @@ +import { shallowMount } from '@vue/test-utils' +import MilestonePath from '@/components/MilestonePath/MilestonePath.vue' +import { describe, it, expect } from 'vitest' + +describe('PathNode.vue', () => { + describe('computed', () => { + it('calculates total nodes correctly', () => { + const totalToSave = 2000 + const wrapper = shallowMount(MilestonePath, { + props: { totalToSave, totalSaved: 0 } + }) + + const actualValue = (wrapper.vm as any).totalNodes; + + expect(actualValue).toBe(Math.ceil(totalToSave / 250)) + }) + + it('calculates remaining nodes correctly', () => { + const totalToSave = 2000 + const totalSaved = 500 + const wrapper = shallowMount(MilestonePath, { + props: { totalToSave, totalSaved } + }) + + const actualValue = (wrapper.vm as any).remainingNodes; + + expect(actualValue).toBe(Math.ceil((wrapper.vm as any).totalNodes - ((wrapper.vm as any).totalNodes * (totalSaved / totalToSave)))) + }) + + it('sets remaining nodes to total nodes if total saved is 0', () => { + const totalToSave = 2000 + const wrapper = shallowMount(MilestonePath, { + props: { totalToSave, totalSaved: 0 } + }) + + const actualValue = (wrapper.vm as any).remainingNodes; + + expect(actualValue).toBe((wrapper.vm as any).totalNodes) + }) + }) + + describe('node generation and styling', () => { + it('generates nodes correctly', () => { + const totalNodes = 8 // For example + const totalToSave = 2000 + const totalSaved = 1000 + const wrapper = shallowMount(MilestonePath, { + props: { totalToSave, totalSaved } + }) + + const actualValue = (wrapper.vm as any).nodes; + + expect(actualValue).toHaveLength(totalNodes) + }) + + it('applies correct styles to nodes', () => { + const totalToSave = 2000 + const totalSaved = 1000 + const wrapper = shallowMount(MilestonePath, { + props: { totalToSave, totalSaved } + }) + const nodes = wrapper.findAll('.node') + nodes.forEach((node, index) => { + + const colorIndex = index < (wrapper.vm as any).remainingNodes - 1 ? 0 : (index === (wrapper.vm as any).remainingNodes - 1 ? 1 : 2) + const foregroundColor = (wrapper.vm as any).nodeForegroundColors[colorIndex] + const backgroundColor = (wrapper.vm as any).nodeBackgroundColors[colorIndex] + const nodeStyle = (node.element as HTMLElement).style + expect(nodeStyle.backgroundColor).toBe(`${backgroundColor}`) + expect(nodeStyle.top).toBe(`${foregroundColor}`) + expect(nodeStyle.bottom).toBe(`${backgroundColor}`) + }) + }) + }) +}) diff --git a/src/components/__tests__/MilestoneProgress.spec.ts b/src/components/__tests__/MilestoneProgress.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..b4177e987f29857f91224897724fa50d2aa0c623 --- /dev/null +++ b/src/components/__tests__/MilestoneProgress.spec.ts @@ -0,0 +1,15 @@ +import { mount } from '@vue/test-utils' +import MilestoneProgress from '@/components/MilestonePath/MilestoneProgress.vue' +import { describe, it, expect } from 'vitest' + +describe('ProgressBar.vue', () => { + it('renders props.totalSaved and props.totalToSave when passed', () => { + const totalSaved = 50 + const totalToSave = 100 + const wrapper = mount(MilestoneProgress, { + props: { totalSaved, totalToSave } + }) + + expect(wrapper.text()).toMatch(`${totalSaved} / ${totalToSave} NOK`) + }) +}) diff --git a/src/components/__tests__/PathNode.spec.ts b/src/components/__tests__/PathNode.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..bf459b22563f320e6f5d20f87da7c07c893eb5dc --- /dev/null +++ b/src/components/__tests__/PathNode.spec.ts @@ -0,0 +1,25 @@ +import { shallowMount } from '@vue/test-utils'; +import PathNode from '@/components/MilestonePath/PathNode.vue' +import { describe, it, expect } from 'vitest' + +describe('NodeComponent', () => { + it('renders with default props', () => { + const wrapper = shallowMount(PathNode); + expect(wrapper.find('.Node').exists()).toBe(true); + expect(wrapper.find('#top').exists()).toBe(true); + expect(wrapper.find('#bottom').exists()).toBe(true); + }); + + it('renders with custom props', () => { + const wrapper = shallowMount(PathNode, { + props: { + nodeBackgroundColor: 'red', + topBackgroundColor: 'blue', + bottomBackgroundColor: 'green' + } + }); + expect(wrapper.find('.Node').attributes('style')).toContain('background-color: red;'); + expect(wrapper.find('#top').attributes('style')).toContain('background-color: blue;'); + expect(wrapper.find('#bottom').attributes('style')).toContain('background-color: green;'); + }); +}); diff --git a/src/components/challenge/ActiveChallengeDisplay.vue b/src/components/challenge/ActiveChallengeDisplay.vue index 6ab8201d979daf9845dc287470dad00b6757209d..fbacb0668a0e17c4cda05fadb4151653d10d61b8 100644 --- a/src/components/challenge/ActiveChallengeDisplay.vue +++ b/src/components/challenge/ActiveChallengeDisplay.vue @@ -1,18 +1,38 @@ <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) + const props = defineProps({ - id: Number, - title: String, - description: String + challengeId: Number, + challengeTitle: String, + challengeDescription: String }); +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!') + } + } +} + </script> <template> <div class="potential-challenge-display"> - <h3 class="title">{{props.title}}</h3> - <h4 class="description">{{props.description}}</h4> + <h3 class="title">{{ props.challengeTitle }}</h3> + <h4 class="description">{{ props.challengeDescription }}</h4> <div class="button-container"> - <button class="complete-button"> + <button class="complete-button" @click="completeTheChallenge()"> <h3 class="complete-button-text">Fullfør</h3> </button> </div> diff --git a/src/components/challenge/PotentialChallengeDisplay.vue b/src/components/challenge/PotentialChallengeDisplay.vue index 01d0e7e88673895a5453f40e580a85df2881f9e2..8ba724bf6666eeeb37a3154e4d49f8fc9cffeb02 100644 --- a/src/components/challenge/PotentialChallengeDisplay.vue +++ b/src/components/challenge/PotentialChallengeDisplay.vue @@ -1,22 +1,52 @@ <script setup lang="ts"> +import { activateChallenge, deleteChallenge } from '@/utils/challengeutils' +import { useTokenStore } from '@/stores/token' + +const token:string = useTokenStore().jwtToken; +const emits = defineEmits(['challengeAccepted', 'challengeDeclined']); + const props = defineProps({ - id: Number, - title: String, - description: String + challengeId: Number, + challengeTitle: String, + challengeDescription: String }); +const declineChallenge = async () => { + console.log('decline-button clicked') + if(props.challengeId){ + try{ + await deleteChallenge(token, props.challengeId); + emits('challengeDeclined', props.challengeId); + } catch (error){ + alert('Noe gikk galt! Venligst prøv pÃ¥ nytt.') + } + } else { + alert('challengeId not defined') + } +} + +const acceptChallenge = async () => { + if(props.challengeId){ + try{ + await activateChallenge(token, props.challengeId); + emits('challengeAccepted', props.challengeId); + } catch (error){ + alert('Noe gikk galt! Venligst prøv pÃ¥ nytt.') + } + } +} </script> <template> <div class="potential-challenge-display"> - <h2 class="title">{{props.title}}</h2> - <h4 class="description">{{props.description}}</h4> + <h2 class="title">{{ props.challengeTitle }}</h2> + <h4 class="description">{{ props.challengeDescription }}</h4> <div class="options"> - <button class="option-button" id="decline-button"> + <button class="option-button" id="decline-button" @click="declineChallenge"> <h3 class="button-text">AvslÃ¥</h3> </button> - <button class="option-button" id="accept-button"> + <button class="option-button" id="accept-button" @click="acceptChallenge"> <h3 class="button-text">Godta</h3> </button> diff --git a/src/components/create-challenge/BaseInput.vue b/src/components/create-challenge/BaseInput.vue new file mode 100644 index 0000000000000000000000000000000000000000..a3e2d429b761c50ace5a2b692288af6464223495 --- /dev/null +++ b/src/components/create-challenge/BaseInput.vue @@ -0,0 +1,53 @@ +<script setup lang="ts"> +const props = defineProps({ + label: { + type: String, + default: '' + }, + modelValue: { + type: [String, Number], + default: '' + }, + placeHolder: { + type: String, + default: '' + }, + type: { + type: String, + default: '' + } + } +) + +</script> + +<template> + <label>{{ label }}</label> + <input + :placeholder="placeHolder" + class="field" + :value="modelValue" + @input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement)?.value)" + :type="type" + > +</template> + +<style scoped> +input { + width: 100%; + padding: 12px 20px; + margin: 8px 0; + box-sizing: border-box; + border-radius: 20px; +} + +label { + font-size: 1.5em; +} + +@media screen and (max-width: 1200px){ + label{ + font-size: 1.2em; + } +} +</style> diff --git a/src/components/create-challenge/BaseTextArea.vue b/src/components/create-challenge/BaseTextArea.vue new file mode 100644 index 0000000000000000000000000000000000000000..d5544abf92dbdd46503a951f9ba01ad2dec13cb9 --- /dev/null +++ b/src/components/create-challenge/BaseTextArea.vue @@ -0,0 +1,49 @@ +<script setup lang="ts"> +const props = defineProps({ + label: { + type: String, + default: '' + }, + modelValue: { + type: [String, Number], + default: '' + }, + placeHolder: { + type: String, + default: '' + } + } +) +</script> + +<template> + <label>{{ label }}</label> + <textarea + :placeholder=placeHolder + class="field" + :value="modelValue" + @input="$emit('update:modelValue', ($event.target as HTMLTextAreaElement)?.value)" + /> +</template> + +<style scoped> +textarea { + height: 18vh; + width: 100%; + border-radius: 20px; + resize: none; + padding: 12px 20px; + margin: 8px 0; + box-sizing: border-box; +} + +label{ + font-size: 1.5em; +} + +@media screen and (max-width: 1200px){ + label{ + font-size: 1.2em; + } +} +</style> diff --git a/src/components/economy/TransactionComponent.vue b/src/components/economy/TransactionComponent.vue index 0563852af5898a750eb73dcaf77bf30faba6712d..ea0deda37024d70891a6742f7c7fc356b5be6bc8 100644 --- a/src/components/economy/TransactionComponent.vue +++ b/src/components/economy/TransactionComponent.vue @@ -53,7 +53,7 @@ const toggleExpand = () => { display:flex; overflow: hidden; transition: height 0.3s ease; - background-color: var(--vt-c-kellyGreen-Light) + background-color: var(--vt-c-Orange) } .component.expanded { @@ -80,7 +80,7 @@ const toggleExpand = () => { padding-left: 20px; font-size:1.4em; word-break: break-word; - color: black; + color: white; } @@ -90,10 +90,11 @@ const toggleExpand = () => { width: 95%; justify-content: right; font-size: 1em; + color: white; } -@media screen and (max-width: 600px) { +@media screen and (max-width: 1300px) { .component { width: 90% ; height: 10%; @@ -119,5 +120,10 @@ const toggleExpand = () => { } +@media (prefers-color-scheme: dark) { + .component{ + background-color: var(--vt-c-Orange-Dark); + } +} </style> diff --git a/src/components/icons/image/AddImage.png b/src/components/icons/image/AddImage.png new file mode 100644 index 0000000000000000000000000000000000000000..22dba638edadae42ce0b61eaef911a4b5b326643 Binary files /dev/null and b/src/components/icons/image/AddImage.png differ diff --git a/src/components/milestone/ActiveMilestoneDisplay.vue b/src/components/milestone/ActiveMilestoneDisplay.vue index 4b27ca594ca6ad31ffb42751c1c752e1d4e5da85..f654cb64d50d70b103a46a7a95ded450a69273ae 100644 --- a/src/components/milestone/ActiveMilestoneDisplay.vue +++ b/src/components/milestone/ActiveMilestoneDisplay.vue @@ -18,7 +18,7 @@ const imageUrl = "src/assets/pig.png" const openMilestone = () => { if (props.id !== undefined) { useMilestoneStore().setMilestoneId(props.id) - router.push("/path") + router.push("/homepage/path") } console.log("Milestone id is not defined") } @@ -34,7 +34,6 @@ const openMilestone = () => { }"> <h2 class="title">{{props.title}}</h2> <div class="progress"> - <h4 class="description">{{props.description}}</h4> <h4 class="description" v-if="goalSum&¤tSum">{{props.currentSum}}kr av {{props.goalSum}}kr</h4> <ProgressBar class="progress-bar" :Max="goalSum || 0" :Current="currentSum || 0"/> </div> diff --git a/src/components/milestone/CompletedMilestoneDisplay.vue b/src/components/milestone/CompletedMilestoneDisplay.vue index 74cc4e51c49afaa23c5a83f2e7c68e79b4ad0d52..98d494322a9b05c4f9c48589d1efc70bafa52c0b 100644 --- a/src/components/milestone/CompletedMilestoneDisplay.vue +++ b/src/components/milestone/CompletedMilestoneDisplay.vue @@ -26,8 +26,8 @@ const displayDescription = ref(false) }"> <h2 class="title">{{props.title}}</h2> <div class="info" v-if="!displayDescription"> - <h4 v-if="currentSum">Du sparte {{props.currentSum}}kr</h4> - <h4 v-if="props.deadline">{{props.deadline.getDate()}}/{{props.deadline.getMonth()}}-{{props.deadline.getFullYear()}}</h4> + <h4 v-if="currentSum">Du sparte {{props.currentSum}}kr</h4> + <h4 v-if="deadline">{{new Date(deadline).getDate()}}/{{new Date(deadline).getMonth()}}-{{new Date(deadline).getFullYear()}}</h4> </div> <div class="info" v-if="displayDescription"> <h4>{{props.description}}</h4> diff --git a/src/components/profile/BankAccountInfo.vue b/src/components/profile/BankAccountInfo.vue index 0ff7f8f90ea6963505d2cf9fb46bc514654f897c..5d7e7c03d0490a791191d895e1b1b0d2933eaf3d 100644 --- a/src/components/profile/BankAccountInfo.vue +++ b/src/components/profile/BankAccountInfo.vue @@ -36,6 +36,7 @@ const fetchUserInfo = async () =>{ const fetchAccountInfo = async () => { const response = await getUserAccountInfo(token); + console.log('account info') console.log(response) for(let i = 0; i < response.length; i++){ console.log(response[i].accountNumber) diff --git a/src/components/profile/UserInfo.vue b/src/components/profile/UserInfo.vue index 62558198b4efd8fc196839ce587472dd16296a4c..22c8f7eb7c9859c6ec2bbc12f12522d9d49b8508 100644 --- a/src/components/profile/UserInfo.vue +++ b/src/components/profile/UserInfo.vue @@ -24,7 +24,6 @@ onMounted(async () => { const fetchUserInfo = async () =>{ try{ const response = await getUserInfo(token) - console.log(response) username.value = response.username; email.value = response.email; profilePictureBase64.value = response.profilePictureBase64; diff --git a/src/main.ts b/src/main.ts index dbbaa1a871d3d2560909c1af0625679fea09986a..ec5d68a20f64cf1fc4aa911fa9780ce3bd3768c2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -2,9 +2,12 @@ import './assets/main.css' import { createApp } from 'vue' import { createPinia } from 'pinia' -import piniaPluginPersistedState from "pinia-plugin-persistedstate" +import VueDatePicker from '@vuepic/vue-datepicker'; +import '@vuepic/vue-datepicker/dist/main.css' +import piniaPluginPersistedState from "pinia-plugin-persistedstate" import App from './App.vue' + import router from './router' const app = createApp(App) @@ -14,5 +17,5 @@ const pinia = createPinia() pinia.use(piniaPluginPersistedState) app.use(pinia) app.use(router) - +app.component('VueDatePicker', VueDatePicker); app.mount('#app') diff --git a/src/router/index.ts b/src/router/index.ts index c500155b858c5dffeb5e61515c324714f0327172..d08cd62b3d1e3544074ad42548b6942d785d2837 100644 --- a/src/router/index.ts +++ b/src/router/index.ts @@ -11,7 +11,12 @@ import LoginView from '@/views/FrontPage/LoginView.vue' import SignupView from '@/views/FrontPage/SignupView.vue' import RegisterPageView from '@/views/RegisterPageView.vue' import StartView from '@/views/FrontPage/StartView.vue' + +import MilestonePathView from '@/views/HomePage/MilestonePathView.vue' + import CreateChallengeView from '@/views/HomePage/CreateChallengeView.vue' +import CreateMilestoneView from '@/views/HomePage/CreateMilestoneView.vue' + const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -61,6 +66,10 @@ const router = createRouter({ path: 'milestone', component: MilestoneView }, + { + path: 'path', + component: MilestonePathView + }, { path: 'challenge', component: ChallengeView @@ -76,6 +85,10 @@ const router = createRouter({ { path: 'profile', component: ProfileView + }, + { + path: 'create-milestone', + component: CreateMilestoneView } ] } diff --git a/src/stores/token.ts b/src/stores/token.ts index 133edc47ee821943f099db855a43645e194ed003..14e6dee0b15ab78d13d2914522171fb43d12bce6 100644 --- a/src/stores/token.ts +++ b/src/stores/token.ts @@ -1,16 +1,19 @@ import { defineStore } from "pinia"; import { getJwtToken, - getUserInfo + getUserInfo, + refreshToken } from "@/utils/frontPageUtils"; +import router from "@/router"; -export const useTokenStore = defineStore('token', { +export const useTokenStore = defineStore({ + id: 'token', state: () => ({ - timer: null, + timer: null as ReturnType<typeof setTimeout> | null, + tokenTimer: null as ReturnType<typeof setTimeout> | null, jwtToken: "", - username: null, - isConnectedToBank: false - + username: null as string | null, + isConnectedToBank: null as boolean | null }), persist: { @@ -19,10 +22,10 @@ export const useTokenStore = defineStore('token', { actions: { async getTokenAndSaveInStore(username: string, password: string) { + let response; try { - const response = await getJwtToken(username, password); - if (response !== undefined) { - console.log(response) + response = await getJwtToken(username, password); + if (response !== undefined && response.data.useername !== "") { const data = response.data; if (data !== "" && data !== undefined) { this.jwtToken = data; @@ -31,15 +34,54 @@ export const useTokenStore = defineStore('token', { this.username = response.data.username this.isConnectedToBank = response.data.isConnectedToBank console.log(this.isConnectedToBank) - } - }) } } + this.startTimer(); + } catch (error) { + if (error instanceof Error) { + this.jwtToken = error.message; + } else { + throw error; + } + } + }, + + async refreshToken() { + try { + const response = await refreshToken(this.jwtToken); + if (response !== undefined) { + this.jwtToken = response.data; + this.startTimer(); + } } catch (error) { console.error(error); } + }, + + logout() { + this.jwtToken = ""; + this.username = null; + this.isConnectedToBank = null; + router.push("/login").then(r => r); + }, + + startTimer() { + this.timer = setTimeout(() => { + if (window.confirm("Your session is about to expire. Do you want to extend it?")) { + this.refreshToken().then(r => r); + this.actualTokenTimer(); + } else { + this.logout(); + } + }, 300000); + }, + + actualTokenTimer() { + this.tokenTimer = setTimeout(() => { + this.logout(); + }, 3600000); } }, @@ -56,4 +98,4 @@ export const useTokenStore = defineStore('token', { return state.username; } } -}); +}); \ No newline at end of file diff --git a/src/utils/MilestonePathUtils.ts b/src/utils/MilestonePathUtils.ts new file mode 100644 index 0000000000000000000000000000000000000000..e71a818a78a60500401d440be74f18211fffb3a2 --- /dev/null +++ b/src/utils/MilestonePathUtils.ts @@ -0,0 +1,15 @@ +import axios from 'axios' + +export async function getMilestoneDetails(id: number){ + + console.log("Method is called") + + const config = { + headers: { + 'Content-Type': 'Application/json', + //'Authorization': 'Bearer ' + TokenStore().getToken() + } + } + + return await axios.post("http://Localhost:8080/milestone/id", id, config); +} \ No newline at end of file diff --git a/src/utils/MilestoneUtils.ts b/src/utils/MilestoneUtils.ts new file mode 100644 index 0000000000000000000000000000000000000000..9c141556e17818b5086a6a8fd643b21312203dc8 --- /dev/null +++ b/src/utils/MilestoneUtils.ts @@ -0,0 +1,32 @@ +import axios from 'axios'; +import {useTokenStore} from "@/stores/token"; + +export const getAllMilestones = async(token: string) => { + const config = { + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token + } + }; + try { + const response = await axios.get("http://localhost:8080/milestone/user", config) + return response.data + } catch (error) { + console.log(error) + } +} + +export const getAllMilestoneLogs = async(token: string) => { + const config = { + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token + } + }; + try { + const response = await axios.get("http://localhost:8080/milestoneLog/user", config) + return response.data + } catch (error) { + console.log(error) + } +} \ No newline at end of file diff --git a/src/utils/TransactionUtils.ts b/src/utils/TransactionUtils.ts new file mode 100644 index 0000000000000000000000000000000000000000..e565330d805791f94e150c48347a79965feac0c0 --- /dev/null +++ b/src/utils/TransactionUtils.ts @@ -0,0 +1,95 @@ +import axios from 'axios' + +const transactionsList = [ + { + transactionId: 1, + transactionTitle: "Kiwi", + date: "15-05-2003", + debtorAccount: 12312312312, + debtorName: "Kiwi AS", + creditorAccount: 32131232132, + amount: 250, + currency: "NOK", + TransactionCategory: { + category: "Groceries" + } + }, + { + transactionId: 2, + transactionTitle: "Apple", + date: "16-05-2003", + debtorAccount: 12312312312, + debtorName: "Apple Inc.", + creditorAccount: 32131232132, + amount: 300, + currency: "NOK", + TransactionCategory: { + category: "Electronics" + } + }, + { + transactionId: 3, + transactionTitle: "Banana", + date: "17-05-2003", + debtorAccount: 12312312312, + debtorName: "Banana Corp.", + creditorAccount: 32131232132, + amount: 350, + currency: "NOK", + TransactionCategory: { + category: "Banana criminality" + } + }, + { + transactionId: 4, + transactionTitle: "Orange", + date: "18-05-2003", + debtorAccount: 12312312312, + debtorName: "Orange Ltd.", + creditorAccount: 32131232132, + amount: 400, + currency: "NOK", + TransactionCategory: { + category: "Annoying" + } + }, + { + transactionId: 4, + transactionTitle: "Crab", + date: "18-05-2003", + debtorAccount: 12312312312, + debtorName: "Crab Narcotics.", + creditorAccount: 32131232132, + amount: 400, + currency: "NOK", + TransactionCategory: { + category: "Drugs hehe" + } + } +]; + +export const getTransactions = async (token:string, pageNumber: Number, pageSize: Number): Promise<any> => { + + const config = { + headers: { + "Content-type": "application/json", + 'Authorization': `Bearer ${token}` + }, + params:{ + "page": pageNumber, + "size": pageSize + } + } + try { + console.log(token) + console.log('trying to get transactions') + console.log(config) + const result = await axios.get("http://localhost:8080/user/transaction/latest/expense", config) + + console.log(result.data); + return result.data; + } catch (e: any) { + console.log(e) + throw e; + } +} diff --git a/src/utils/challengeutils.ts b/src/utils/challengeutils.ts new file mode 100644 index 0000000000000000000000000000000000000000..d2340ef1d08e5a9157dfcd889752d9fc9a5920ce --- /dev/null +++ b/src/utils/challengeutils.ts @@ -0,0 +1,182 @@ +import axios from "axios"; + +const challengeRecomendationsTestData = [ + { + challengeId:1, + challengeTitle:'GÃ¥ til skolen!', + challengeDescription:'Spar 46kr hver dag du gÃ¥r isteden for Ã¥ ta buss til skolen denne uken.' + }, + { challengeId: 2, + challengeTitle:'UnngÃ¥ kjøp av kaffe!', + challengeDescription:'Spar 59kr for Ã¥ unngÃ¥ kjøp av kaffe i dag.' + }, + { challengeId: 3, + challengeTitle:'Bruk handlenett pÃ¥ butikken!', + challengeDescription:'Spar 5kr for Ã¥ burke handlenett pÃ¥ butikken.' + }, +]; + +const activeChallengesTestData = [ + { + challengeId: 4, + challengeTitle:'Spis middag hjemme!', + challengeDescription:'Spar 200kr for Ã¥ spise middag hjemme i dag.' + }, + { challengeId: 5, + challengeTitle:'Kjøp brukt isteden for nytt!', + challengeDescription:'Spar 250kr for Ã¥ kun kjøpe brukt denne uken.' + }, + { challengeId: 6, + challengeTitle: 'Ta med lunsj hjemmefra!', + challengeDescription:'Spar 100kr for Ã¥ ta med lunsj hjemmefra i dag.' + }, + { challengeId: 7, + challengeTitle: 'UnngÃ¥ netthandel!', + challengeDescription:'Spar 500kr for Ã¥ unngÃ¥ netthandel denne mÃ¥neden.' + }, +] + +export const createChallenge = async ( + token:string, + challengeTitle: string, + challengeDescription: string, + goalSum: number, + expirationData:string, + recurring: number ):Promise<any>=>{ + try{ + const config = { + headers:{ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }; + const data = { + 'challengeTitle': challengeTitle, + 'challengeDescription':challengeDescription, + 'goalSum': goalSum, + 'expirationDate': expirationData, + 'recurring': recurring, + } + return await axios.post(`http://localhost:8080/user/challenge/create/`,data,config); + } catch (error){ + console.error(error); + } +} + +export const deleteChallenge = async (token:string, challengeId: number):Promise<any>=>{ + try{ + const config = { + headers:{ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }; + return await axios.delete(`http://localhost:8080/user/challenge/delete/${challengeId}`,config); + } catch (error){ + console.error(error); + throw error; + } +} + +export const completeChallenge= async (token:string, challengeId:number, milestoneId:number):Promise<any>=>{ + try{ + const config = { + headers:{ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + params: { + challengeId: challengeId, + milestoneId: milestoneId + } + }; + console.log(config) + return await axios.post(`http://localhost:8080/user/challenge/complete`,{},config); + } catch (error){ + console.error(error); + throw error; + } +} + +export const activateChallenge= async (token:string, challengeId: number):Promise<any>=>{ + console.log(challengeId) + console.log(token) + try{ + const config = { + headers:{ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }; + return await axios.post(`http://localhost:8080/user/challenge/activate/${challengeId}`,{},config); + } catch (error){ + console.error(error); + throw error; + } +} + +export const getChallenge = async (token:string, challengeId: number):Promise<any>=>{ + try{ + const config = { + headers:{ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + }; + return await axios.get(`http://localhost:8080/user/challenge/${challengeId}`,config); + } catch (error){ + console.error(error); + } +} + + +export const getActiveChallenges = async (token:string):Promise<any>=>{ + console.log(token) + try{ + const config = { + headers:{ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + params: { + 'page': 0, + 'size': 10 + } + + }; + const result = await axios.get('http://localhost:8080/user/challenge/paginated/active',config); + console.log('result') + console.log(result) + return result.data; + } catch (error){ + console.error(error); + return activeChallengesTestData + } +} + +export const getInactiveChallenges = async (token:string):Promise<any>=>{ + try{ + const config = { + headers:{ + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + }, + params: { + 'page': 0, + 'size': 10 + } + }; + const result = await axios.get('http://localhost:8080/user/challenge/paginated/inactive',config); + console.log('interactive') + console.log(result); + return result.data; + } catch (error){ + console.error(error); + return challengeRecomendationsTestData + } +} + + + + + diff --git a/src/utils/createMilestoneUtils.ts b/src/utils/createMilestoneUtils.ts new file mode 100644 index 0000000000000000000000000000000000000000..50c4bf06d17fdbe6ec6bdd849d50cb7c17a28532 --- /dev/null +++ b/src/utils/createMilestoneUtils.ts @@ -0,0 +1,14 @@ +import axios from 'axios' +export const createMilestone = async (milestoneData: any, token: string) => { + const config = { + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${token}` + } + } + try{ + return await axios.post('http://localhost:8080/milestone/create', milestoneData, config) + } catch (e) { + console.log(e) + } +} diff --git a/src/utils/frontPageUtils.ts b/src/utils/frontPageUtils.ts index b895aadfc05b18e8965e8009203e8a0cc2d3cb90..d7813b0e223f6dc5cd6164b49f3eec55cbdaa06f 100644 --- a/src/utils/frontPageUtils.ts +++ b/src/utils/frontPageUtils.ts @@ -1,5 +1,6 @@ import axios from "axios"; import { useTokenStore } from '../stores/token'; +import router from "@/router"; export const getJwtToken = async (username: string, password: string) => { const config = { @@ -51,4 +52,23 @@ export const getUserInfo = async(username: string, token: string) => { } catch (error) { console.log(error) } +} + +export const refreshToken = async (token: string) => { + const config = { + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token + } + }; + try { + return await axios.get("http://localhost:8080/auth/refresh", config) + } catch (error) { + console.log(error) + } +} + +export const logout = async () => { + useTokenStore().logout(); + router.push("/login").then(r => r); } \ No newline at end of file diff --git a/src/utils/profileutils.ts b/src/utils/profileutils.ts index 2ea224995cee97e54a233cefcda90d407cb2c6a4..469d6ddb446100a518d2863e0412cb33baa9afb3 100644 --- a/src/utils/profileutils.ts +++ b/src/utils/profileutils.ts @@ -112,6 +112,7 @@ export const deleteUser = async (token:string):Promise<any>=>{ return await axios.delete('http://localhost:8080/users/delete',config); } catch (error){ console.error(error); + throw error; } } @@ -123,7 +124,8 @@ export const getUserAccountInfo = async (token:string):Promise<any> => { 'Authorization': `Bearer ${token}` }, }; - return await axios.get('http://localhost:8080/users/get', config); + const result = await axios.get('http://localhost:8080/user/account', config); + return result.data; } catch (error){ console.log('sending mock data') return testDataUserAccounts; @@ -137,7 +139,8 @@ export const getUserInfo = async (token:string): Promise<any> => { 'Authorization': `Bearer ${token}` }, }; - return await axios.get('http://localhost:8080/user/account', config); + const result = await axios.get('http://localhost:8080/users/get', config); + return result.data; } catch (error){ console.log('sending mock data') return testDataUser; @@ -159,7 +162,7 @@ export const updateUserInfo = async ( 'email': email, 'profilePictureBase64': profilePictureBase64 }; - return await axios.put('http://localhost:8080/users/update',data,config); + return await axios.post('http://localhost:8080/users/update',data,config); } catch (error){ console.error(error) throw error; @@ -181,7 +184,7 @@ export const updatePasswordInfo = async ( 'password': currentPassword, 'newPassword': newPassword }; - return await axios.put('http://localhost:8080/usersCredentials/updatePassword',data,config); + return await axios.post('http://localhost:8080/userCredentials/updatePassword',data,config); } catch (error){ console.error(error) throw error; @@ -202,7 +205,7 @@ export const updateBankAccountInfo = async ( 'currentAccount': checkingAccount, 'savingAccount': savingAccount }; - return await axios.put('http://localhost:8080/users/update',data,config); + return await axios.post('http://localhost:8080/users/update',data,config); } catch (error){ console.error(error) throw error; @@ -226,9 +229,23 @@ export const updateIncomeInfo = async ( 'monthlyFixedExpenses': monthlyFixedExpenses, 'monthlySavings': monthlySavings }; - return await axios.put('http://localhost:8080/users/update',data,config); + return await axios.post('http://localhost:8080/users/update',data,config); } catch (error){ console.error(error) throw error; } +} + +export const deleteAccount = async (token: string) => { + const config = { + headers: { + "Content-Type": "application/json", + Authorization: "Bearer " + token + } + }; + try { + await axios.delete("http://localhost:8080/users/delete", config) + } catch (error) { + console.log(error) + } } \ No newline at end of file diff --git a/src/views/FrontPage/LoginView.vue b/src/views/FrontPage/LoginView.vue index f0c44ad68a47c1caf34e731843b4a00e666b9c9a..e34714f380c8ff4ab821abd7ed659ac7ad180f4b 100644 --- a/src/views/FrontPage/LoginView.vue +++ b/src/views/FrontPage/LoginView.vue @@ -13,30 +13,20 @@ function navigateToNewUser() { } async function login() { - if (password.value.length < 8) { - alert('Password must be at least 8 characters'); - return; + await useTokenStore().getTokenAndSaveInStore(username.value, password.value); + if (useTokenStore().$state.jwtToken !== '' && !useTokenStore().$state.jwtToken.includes('Request')) { + await router.push('/homepage') } - - const tokenStore = useTokenStore(); - if (!tokenStore) { - alert('Token store is not available'); - return; + else if (useTokenStore().$state.jwtToken === 'Request failed with status code 401'){ + alert('Feil brukernavn eller passord') } - - await tokenStore.getTokenAndSaveInStore(username.value, password.value); - - if (tokenStore.$state.isConnectedToBank) { - await router.push('/homepage'); - } else if (!tokenStore.$state.isConnectedToBank) { - alert('Not complete user, routing to registration page'); - await router.push('/register'); + else if (useTokenStore().$state.jwtToken === 'Request failed with status code 403') { + alert('Du kommer fra et utrygt nettverk, vennligst prøv igjen senere') } - else { - alert('Login failed'); + else if (useTokenStore().$state.jwtToken === 'Request failed with status code 500') { + alert('Serveren er nede, vennligst prøv igjen senere') } } - </script> <template> diff --git a/src/views/HomePage/ChallengeView.vue b/src/views/HomePage/ChallengeView.vue index 95ac4c9b0ddb4504479387931e19b6d01ba7bb95..8ec7d54d62b4cb54e7ce18a70b2db831f5821ed6 100644 --- a/src/views/HomePage/ChallengeView.vue +++ b/src/views/HomePage/ChallengeView.vue @@ -1,65 +1,98 @@ <script setup lang="ts"> import PotentialChallengeDisplay from '@/components/challenge/PotentialChallengeDisplay.vue' -import { ref } from 'vue' +import { onMounted, ref } from 'vue' import ActiveChallengeDisplay from '@/components/challenge/ActiveChallengeDisplay.vue' import router from '@/router' +import { useTokenStore } from '@/stores/token' +import { getActiveChallenges, getInactiveChallenges } from '@/utils/challengeutils' -const challengeRecomendationsTestData = [ - { - id:1, - title:'GÃ¥ til skolen!', - description:'Spar 46kr hver dag du gÃ¥r isteden for Ã¥ ta buss til skolen denne uken.' - }, - { id: 2, - title:'UnngÃ¥ kjøp av kaffe!', - description:'Spar 59kr for Ã¥ unngÃ¥ kjøp av kaffe i dag.' - }, - { id: 3, - title:'Bruk handlenett pÃ¥ butikken!', - description:'Spar 5kr for Ã¥ burke handlenett pÃ¥ butikken.' - }, -]; - -const activeChallengesTestData = [ - { - id: 4, - title:'Spis middag hjemme!', - description:'Spar 200kr for Ã¥ spise middag hjemme i dag.' - }, - { id: 5, - title:'Kjøp brukt isteden for nytt!', - description:'Spar 250kr for Ã¥ kun kjøpe brukt denne uken.' - }, - { id: 6, - title: 'Ta med lunsj hjemmefra!', - description:'Spar 100kr for Ã¥ ta med lunsj hjemmefra i dag.' - }, - { id: 7, - title: 'UnngÃ¥ netthandel!', - description:'Spar 500kr for Ã¥ unngÃ¥ netthandel denne mÃ¥neden.' - }, -] - -const activeChallenges = ref<Challenge[] | null>(activeChallengesTestData) - -const challengeRecommendations = ref<Challenge[] | null>(challengeRecomendationsTestData) +interface Challenge{ + challengeId: number; + challengeTitle: string; + challengeDescription: string +} + +const token:string = useTokenStore().jwtToken; + +const activeChallenges = ref<Challenge[]>([]) +const inactiveChallenges = ref<Challenge[]>([]) const pages = ref<number>(1) const currentPage = ref<number>(0) -const navigateTo = (path: string) => { - router.push(path) + + +onMounted(async () => { + try { + await fetchInactiveChallenges(); + await fetchActiveChallenges(); + } catch (error) { + console.error('Error fetching user info:', error); + } +}) + +const fetchInactiveChallenges = async () => { + try { + const response = await getInactiveChallenges(token) + inactiveChallenges.value = [] + for (let i = 0; i < response.length; i++) { + inactiveChallenges.value.push({ + challengeId: response[i].challengeId, + challengeTitle: response[i].challengeTitle, + challengeDescription: response[i].challengeDescription + }) + } + + console.log(inactiveChallenges.value) + } catch (error) { + console.error('Error fetching active challenges:', error); + } +} + +const fetchActiveChallenges = async () => { + try{ + const response = await getActiveChallenges(token) + console.log(response) + activeChallenges.value = []; + for(let i = 0; i < response.length; i ++){ + console.log(response.data) + activeChallenges.value.push({ + challengeId:response[i].challengeId, + challengeTitle:response[i].challengeTitle, + challengeDescription:response[i].challengeDescription + }) + } + console.log(activeChallenges.value) + } catch (error){ + console.error('Error fetching inactive challenges:', error); + } +} + +// Function to handle the emitted challengeAccepted event +const handleChallengeAccepted = async () => { + console.log('handling it') + await fetchActiveChallenges(); + await fetchInactiveChallenges(); +} + +// Function to handle the emitted challengeDeclined event +const handleChallengeDeclined = async () => { + await fetchInactiveChallenges(); +} + +const handleChallengeCompleted = async () => { + await fetchActiveChallenges(); } const previousPage = () => {} -const goToPage = (pageNumber:number) => {} +const goToPage = (pageNumber:number) => { + currentPage.value = pageNumber; +} const nextPage = () =>{} -interface Challenge{ - id: number; - title: string; - description: string +const navigateTo = (path: string) => { + router.push(path) } </script> @@ -75,11 +108,13 @@ interface Challenge{ <div class="challenge-recommendations"> <PotentialChallengeDisplay class="potential-challenge" - v-for="(potentialChallenge, index) in challengeRecommendations" + v-for="(potentialChallenge, index) in inactiveChallenges" :key="index" - :id="potentialChallenge.id" - :title="potentialChallenge.title" - :description="potentialChallenge.description" + :challengeId="potentialChallenge.challengeId" + :challengeTitle="potentialChallenge.challengeTitle" + :challengeDescription="potentialChallenge.challengeDescription" + @challengeAccepted="handleChallengeAccepted" + @challengeDeclined="handleChallengeDeclined" ></PotentialChallengeDisplay> </div> </div> @@ -92,9 +127,10 @@ interface Challenge{ class="active-challenge" v-for="(activeChallenge, index) in activeChallenges" :key="index" - :id="activeChallenge.id" - :title="activeChallenge.title" - :description="activeChallenge.description" + :challengeId="activeChallenge.challengeId" + :challengeTitle="activeChallenge.challengeTitle" + :challengeDescription="activeChallenge.challengeDescription" + @challengeCompleted="handleChallengeCompleted" ></ActiveChallengeDisplay> <div class="pagination"> <button @click="previousPage" :disabled="currentPage === 0">Forige side</button> @@ -102,8 +138,8 @@ interface Challenge{ <button v-for="pageNumber in pages" :key="pageNumber-2" - @click="goToPage(pageNumber-1)" :class="{ chosen: pageNumber-1 === currentPage }" + @click="goToPage(pageNumber-1)" >{{ pageNumber}}</button> </div> <button @click="nextPage" :disabled="currentPage === pages - 1 || pages === 0">Neste side</button> diff --git a/src/views/HomePage/CreateMilestoneView.vue b/src/views/HomePage/CreateMilestoneView.vue new file mode 100644 index 0000000000000000000000000000000000000000..cc65a1743d39172a9f00422001f7b6ac18cb3221 --- /dev/null +++ b/src/views/HomePage/CreateMilestoneView.vue @@ -0,0 +1,347 @@ +<script setup lang="ts"> + +import BaseInput from '@/components/create-challenge/BaseInput.vue' +import BaseTextArea from '@/components/create-challenge/BaseTextArea.vue' +import VueDatePicker from '@vuepic/vue-datepicker' +import '@vuepic/vue-datepicker/dist/main.css' +import { ref, computed } from 'vue' +import { createMilestone } from '@/utils/createMilestoneUtils' +import { useTokenStore } from '@/stores/token' +import { useRouter } from 'vue-router' + +const title = ref('') +const description = ref('') +const end_date = ref() +const current_sum = ref<number>() +const goal_sum = ref<number>() +const start_date = ref() +const titleError = ref() +const descriptionError = ref() +const dateError = ref() +const amountError = ref() +const image = ref() +const tokenStore = useTokenStore() +const router = useRouter() +const validate = () => { + let isValid = true + titleError.value = '' + descriptionError.value = '' + dateError.value = '' + amountError.value = '' + + if (!title.value.trim()) { + titleError.value = 'Vennligst sett inn tittel til sparemÃ¥l' + isValid = false + } + if (!description.value.trim()) { + descriptionError.value = 'Vennligst skriv hva du ønsker Ã¥ spare til' + isValid = false + } + if (!start_date.value || !end_date.value) { + dateError.value = 'Vennligst velg bÃ¥de start- og sluttdato' + isValid = false + } + if (isNaN(<number>current_sum.value) || isNaN(<number>goal_sum.value)) { + amountError.value = 'Vennligst bruk bare tall' + isValid = false + } + + if (<number>goal_sum.value < <number>current_sum.value) { + amountError.value = 'MÃ¥let kan ikke være større enn det nÃ¥værende beløpet'; + isValid = false; + } + + return isValid +} + +const milestoneData = computed(() => ({ + milestoneTitle: title.value, + milestoneDescription: description.value, + milestoneGoalSum: goal_sum.value, + milestoneCurrentSum: current_sum.value, + milestoneImage: image.value, + deadlineDate: end_date.value ? end_date.value : null, + startDate: start_date.value ? start_date.value : null +})); + +const saveInput = () => { + if (validate()) { + console.log(milestoneData.value) + createMilestone(milestoneData.value, tokenStore.jwtToken) + router.push('/homepage/home') + } else { + console.log('fail') + } +} + +const handleFileChange = (event: any) => { + const file = event.target.files[0] + const reader = new FileReader() + reader.onload = (e) => { + image.value = e.target?.result + } + reader.readAsDataURL(file) +} + +</script> + +<template> + <div id="createContainer"> + <div class="input-container"> + <div class="input"> + <BaseInput + v-model="title" + label="Tittel pÃ¥ sparesti" + place-holder="Navnet pÃ¥ sparestien" + type="email" + ></BaseInput> + <label class="error" + v-if="titleError">{{ titleError }}</label> + </div> + <div class="input"> + <BaseTextArea + v-model="description" + label="Beskrivelse" + place-holder="Vennligst beskriv sparestien"> + </BaseTextArea> + <label class="error" v-if="descriptionError">{{ descriptionError }}</label> + </div> + <div class="smaller-inputs"> + <div class="input"> + <base-input + v-model="goal_sum" + label="Hvor mye vil du spare?" + place-holder="Sett inn hvor mye du vil spare" + id="test"> + </base-input> + <label class="error" v-if="amountError">{{ amountError }}</label> + </div> + <div class="input"> + <base-input + v-model="current_sum" + place-holder="Sett inn hvor mye du har" + label="Hvor mye har du nÃ¥?"> + </base-input> + <label class="error" v-if="amountError">{{ amountError }}</label> + </div> + </div> + <div class="smaller-inputs"> + <div class="input"> + <label>Sett start dato</label> + <VueDatePicker + :enable-time-picker="false" + placeholder="Velg start dato" + v-model="start_date" + :max-date="end_date" + ></VueDatePicker> + <label class="error" v-if="dateError">{{ dateError }}</label> + </div> + <div class="input"> + <label>Sett slutt dato</label> + <VueDatePicker + :enable-time-picker="false" + placeholder="Velg slutt dato" + v-model="end_date" + :min-date="start_date" + ></VueDatePicker> + <label class="error" v-if="dateError">{{ dateError }}</label> + </div> + </div> + <div class="submit-button"> + <button class="save-button" @click="saveInput"> + <h3 class="save-button-title">Lagre</h3> + </button> + </div> + </div> + <div class="image-container"> + <label>Legg til et bilde</label> + <div class="add-image-box"> + <label> + <input type="file" style="display: none" ref="fileInput" accept="image/png, image/jpeg" + @change="handleFileChange"> + <img v-if="image" :src="image" id="literal-image" alt="Selected Image" width="150px" height="150px"> + <img v-else src="../../components/icons/image/AddImage.png" alt="Add Image" width="50px" height="50px"> + </label> + </div> + <!-- <div class="existing-pictures">--> + <!-- <div class="existing-image-box"></div>--> + <!-- <div class="existing-image-box"></div>--> + <!-- </div>--> + <div class="submit-button-mobile"> + <button class="save-button" @click="saveInput"> + <h3 class="save-button-title">Lagre</h3> + </button> + </div> + </div> + </div> +</template> + +<style scoped> + +.input-container { + display: flex; + flex-direction: column; + align-items: flex-start; + text-align: left; + width: 50%; + height: 100%; +} + +.smaller-inputs { + display: flex; + flex-direction: row; + gap: 2%; + height: 100%; + width: 100%; +} + +.input { + display: flex; + flex-direction: column; + align-items: flex-start; + text-align: left; + width: 100%; + height: 100%; +} + +#createContainer { + display: flex; + flex-direction: row; + row-gap: 3%; + margin-left: 1%; +} + +label { + font-size: 1.5em; +} + +.add-image-box { + display: flex; + flex-direction: column; + justify-content: center; /* Center horizontally */ + align-items: center; /* Center vertically */ + + width: 75%; + height: 340px; +} + +.existing-image-box { + display: flex; + width: 50%; + height: auto; + min-height: 180px; +} + +.add-image-box, +.existing-image-box { + border: 2px solid darkgray; + border-radius: 20px; + box-sizing: border-box; + + font-size: 12px; + color: darkgray; + padding: 10px; + gap: 10px; + background-color: var(--color-background); +} + +.image-container { + width: 50%; + height: 100%; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; +} + +.existing-pictures { + height: 100%; + width: 100%; + display: flex; +} + +.save-button { + border-radius: 20px; + padding-right: 5.0%; + padding-left: 5.0%; + margin-top: 5%; + color: var(--color-headerText); + background-color: var(--color-save-button); + border-color: var(--color-border); + width: 100%; + height: 20%; + min-height: 100px; +} + +.submit-button-mobile, +.submit-button { + height: 100%; + width: 100%; + display: flex; +} + +.save-button:hover { + transform: scale(1.02); +} + +.save-button:active { + background-color: var(--color-save-button-click); +} + +.save-button-title { + font-weight: bold; +} + +.submit-button-mobile { + display: none; +} + +.error { + color: rgb(189, 0, 0); + font-size: 15px; +} +#literal-image{ + height: 100%; + width: 100%; +} + +@media screen and (max-width: 1200px) { + .input-container { + width: 90%; + margin: 0 auto; + } + + .smaller-inputs { + flex-direction: column; + } + + #createContainer { + flex-direction: column; + } + + .submit-button { + display: none; + } + + .submit-button-mobile { + display: block; + } + + .image-container { + margin-top: 5%; + width: 100%; + } + + label { + font-size: 1.2em; + } + + .add-image-box { + height: 170px; + } + + #literal-image{ + max-height: 170px; + } +} +</style> diff --git a/src/views/HomePage/EconomyView.vue b/src/views/HomePage/EconomyView.vue index 8f40f988c6cb8230cbbf54bcc90d0942a49b2c65..af02ae6d1bf0d1e1283759aad5a56763ec6f90ec 100644 --- a/src/views/HomePage/EconomyView.vue +++ b/src/views/HomePage/EconomyView.vue @@ -1,50 +1,48 @@ <script setup lang="ts"> -import { computed, ref } from 'vue' -import {Pie} from 'vue-chartjs' -import { Chart as ChartJS, ArcElement, Tooltip, Legend } from 'chart.js' -ChartJS.register(ArcElement,Tooltip,Legend) +import { computed, onMounted, ref } from 'vue' +import { Pie } from 'vue-chartjs' +import { ArcElement, Chart as ChartJS, Colors, Legend, Tooltip } from 'chart.js' import TransactionComponent from '@/components/economy/TransactionComponent.vue' import ToggleButton from '@/components/economy/ToggleButton.vue' +import { getTransactions } from '@/utils/TransactionUtils' +import { useTokenStore } from '@/stores/token' -const selectedOption = ref<string | null>("") +ChartJS.register(ArcElement,Tooltip,Legend, Colors) + +interface Transaction { + "transactionCategory": string, + "transactionTitle": string, + "amount": number, + "transactionId": number, + "date": string, +} + +const token:string = useTokenStore().jwtToken; + +const selectedOption = ref<string | null>("") //let page = 0; let pages = 0; let currentPage = 0; -const transactions = ref([ - { id: 1, - title: 'Rema 1000', - date: '2022-05-10', - amount: 100, - category: 'Dagligvare' - }, - { id: 2, - title: 'Trondheim Kino', - date: '2022-05-15', - amount: 500, - category: 'Underholdning' - }, - { id: 3, - title: 'SIT', - date: '2022-05-15', - amount: 4450, - category: 'regninger' - }, - { id: 4, - title: 'Superhero Burger', - date: '2022-05-15', - amount: 1500, - category: 'Mat & Restaurant' - }, - { id: 6, - title: 'Kiwi', - date: '2022-05-20', - amount: 100, - category: 'Dagligvare' - }, -]) +const transactions = ref<Transaction[]>([]) +const fetchTransactions = async() => { + try{ + const response = await getTransactions(token,0,6) + transactions.value = response + console.log(transactions.value) + } catch (e) { + console.log(e) + } + +} +onMounted(() => { + fetchTransactions() +}) + + + const chartVisible = ref(false) const toggleChart = (value: boolean) => { @@ -58,7 +56,9 @@ const handleSelectionChange = (value: string | null) => { const distinctCategories = computed(() => { const categories = new Set<string>() transactions.value.forEach(transaction => { - categories.add(transaction.category) + console.log(transaction.transactionCategory) + categories.add(transaction.transactionCategory) + console.log(categories) }) return Array.from(categories) }) @@ -69,11 +69,13 @@ const dropdownOptions = computed(() => { const filteredTransactions = computed(() => { if (selectedOption.value === 'Alle' || !selectedOption.value) { + console.log(transactions.value) return transactions.value } else { - return transactions.value.filter(transaction => transaction.category === selectedOption.value) + return transactions.value.filter(transaction => transaction.transactionCategory === selectedOption.value) } }) + const chartData = computed(() => { const data: { labels: string[], datasets: { data: number[], label:string ,backgroundColor: string[] }[] } = { labels: [], @@ -87,7 +89,9 @@ const chartData = computed(() => { const categoryAmounts: { [key: string]: number } = {}; transactions.value.forEach(transaction => { - const { category, amount } = transaction; + const { transactionCategory, amount } = transaction; + console.log(transactionCategory) + const category = transactionCategory if (category in categoryAmounts) { categoryAmounts[category] += amount; } else { @@ -101,7 +105,6 @@ const chartData = computed(() => { data.labels.forEach(label => { data.datasets[0].data.push(categoryAmounts[label]); }); - console.log(data) return data; }) @@ -114,6 +117,40 @@ const getRandomColor = () => { } return color } + +// +// const transactions1 = ref([ +// { id: 1, +// title: 'Rema 1000', +// date: '2022-05-10', +// amount: 100, +// category: 'Dagligvare' +// }, +// { id: 2, +// title: 'Trondheim Kino', +// date: '2022-05-15', +// amount: 500, +// category: 'Underholdning' +// }, +// { id: 3, +// title: 'SIT', +// date: '2022-05-15', +// amount: 4450, +// category: 'regninger' +// }, +// { id: 4, +// title: 'Superhero Burger', +// date: '2022-05-15', +// amount: 1500, +// category: 'Mat & Restaurant' +// }, +// { id: 6, +// title: 'Kiwi', +// date: '2022-05-20', +// amount: 100, +// category: 'Dagligvare' +// }, +// ]) </script> <template> @@ -133,12 +170,12 @@ const getRandomColor = () => { <option v-for="option in dropdownOptions" :key="option" :value="option">{{ option }}</option> </select> </div> - <div class="component-container"> + <div class="component-container" v-if="filteredTransactions"> <transaction-component v-for="transaction in filteredTransactions" - :key="transaction.id" - :title="transaction.title" - :category="transaction.category" + :key="transaction.transactionId" + :title="transaction.transactionTitle" + :category="transaction.transactionCategory" :amount="transaction.amount" :date="transaction.date" ></transaction-component> @@ -286,10 +323,10 @@ h2 { } -@media screen and (max-width: 600px) { +@media screen and (max-width: 1300px) { .box { width: 100%; - min-height: 510px; + min-height: 580px; margin:10px /* Adjust margin for smaller screens */ } @@ -332,4 +369,9 @@ h2 { } } +@media (prefers-color-scheme: dark){ + h2{ + color: var(--vt-c-kellyGreen-Light); + } +} </style> diff --git a/src/views/HomePage/MilestonePathView.vue b/src/views/HomePage/MilestonePathView.vue new file mode 100644 index 0000000000000000000000000000000000000000..a83a5c587bb6ebbedb987912d504e148c7b41219 --- /dev/null +++ b/src/views/HomePage/MilestonePathView.vue @@ -0,0 +1,164 @@ +<script setup lang="ts"> + +import MilestoneProgress from '@/components/MilestonePath/MilestoneProgress.vue' +import { ref } from 'vue' +import DirectTransfer from '@/components/MilestonePath/DirectTransfer.vue' +import MilestoneDescription from '@/components/MilestonePath/MilestoneDescription.vue' +import MilestonePath from '@/components/MilestonePath/MilestonePath.vue' +import { getMilestoneDetails } from '@/utils/MilestonePathUtils' + +const pathName = ref("PathNameHere") +const pathDescription = ref("PathDescriptionHere") + +const totalToSave = ref(2000) +const totalSaved = ref(0) + +const showPath = ref(true); +const showInfo = ref(false); + +const milestonePathKey = ref(0); + +function checkScreenWidth() { + if(window.innerWidth >= 1000){ + showInfo.value = true; + showPath.value = true; + } +} + +function showInfoFields(){ + showInfo.value = true; + showPath.value = false; +} + +function showPathField(){ + showInfo.value = false + showPath.value = true +} + +checkScreenWidth() + +//Re-add when token store is up and running +//const response = getMilestoneDetails(1) +//pathName.value = response//data//milestoneTitle +//pathDescription.value = response//data//milestoneDescription +//totalToSave.value = response//data//milestoneGoalSum +//totalSaved.value = response//data//milestoneCurrentSum + +function updateTotalSaved(value: number) { + totalSaved.value += value; + milestonePathKey.value++; +} +</script> + +<template> + <div id = milestonePathView> + <h1 id = title>{{pathName}}</h1> + <div id = mobileButtons> + <button class = mobileButton @click="showPathField()">Sti</button> + <button class = mobileButton @click="showInfoFields()">Oversikt</button> + </div> + <div id = MilestonePath> + <div id = Path v-show="showPath"> + <MilestonePath :total-to-save="totalToSave" :total-saved="totalSaved" :key="milestonePathKey"/> + </div> + <div id = Info v-show="showInfo"> + <div id = Progress> + <MilestoneProgress :total-to-save="totalToSave" :total-saved="totalSaved"/> + </div> + <div id = Transfer> + <DirectTransfer @transfer-value="updateTotalSaved"/> + </div> + <div id = Description> + <MilestoneDescription :path-description="pathDescription"/> + </div> + </div> + </div> + </div> + + + +</template> + +<style scoped> + + #milestonePathView{ + height: 100%; + width: 100%; + } + + #title{ + margin-left: 9%; + } + + #MilestonePath{ + width: 100%; + height: 80%; + display: flex; + } + + #Path{ + width: 60%; + height: 70vh; + } + + #Info{ + width: 40%; + height: 100%; + display: block; + } + + #Progress{ + width: 100%; + } + + #Progress, #Transfer, #Description{ + margin-bottom: 5%; + } + + @media only screen and (max-width: 1000px) { + #Path{ + height: 60vh; + } + + #title{ + margin: 0; + text-align: center; + } + + #Path{ + width: 100%; + } + + #Info{ + width: 100%; + } + + #mobileButtons{ + width: 100%; + height: 8%; + margin-bottom: 5%; + display: flex; + justify-content: space-evenly; + align-items: center; + } + + .mobileButton{ + width: 40%; + height: 100%; + background-color: var(--color-confirm-button); + color: var(--color-buttonText); + border: 0; + border-radius: 20px; + padding: 1%; + min-width: 30%; + font-size: 250%; + } + + } + + @media only screen and (min-width: 900px){ + #mobileButtons{ + display: none; + } + } +</style> \ No newline at end of file diff --git a/src/views/HomePage/MilestoneView.vue b/src/views/HomePage/MilestoneView.vue index 9767888fa72b1fdb6c5f3632f68b8dc014e1a8ba..62d67cfbc0d408705c6a0eb60bfaf05980c6eead 100644 --- a/src/views/HomePage/MilestoneView.vue +++ b/src/views/HomePage/MilestoneView.vue @@ -1,94 +1,21 @@ <script setup lang="ts"> -import {ref} from "vue"; +import {onMounted, ref} from "vue"; import router from "@/router"; import CompletedMilestoneDisplay from '@/components/milestone/CompletedMilestoneDisplay.vue' import ActiveMilestoneDisplay from '@/components/milestone/ActiveMilestoneDisplay.vue' +import {getAllMilestoneLogs, getAllMilestones} from "@/utils/MilestoneUtils"; +import {useTokenStore} from "@/stores/token"; +const activeMilestones = ref(<Milestone[]>[]) +const completedMilestones = ref<Milestone[]>([]) -const activeMilestonesTestData = [ - { - id: 1, - title: 'Spar 1000kr!', - description: 'Spar 1000kr i løpet av denne mÃ¥neden.', - goalSum: 1000, - currentSum: 500, - deadline: new Date('2021-12-24'), - startDate: new Date('2021-12-01'), - image: 'https://www.tlctranslation.com/wp-content/uploads/2016/05/translation-makes-you-money-1024x602-1.jpg' - }, - { - id: 2, - title: 'Spar 500kr!', - description: 'Spar 500kr i løpet av denne mÃ¥neden.', - goalSum: 500, - currentSum: 500, - deadline: new Date('2021-12-24'), - startDate: new Date('2021-12-01'), - image: 'https://www.tlctranslation.com/wp-content/uploads/2016/05/translation-makes-you-money-1024x602-1.jpg' - - }, - { - id: 3, - title: 'Spar 200kr!', - description: 'Spar 200kr i løpet av denne mÃ¥neden.', - goalSum: 200, - currentSum: 200, - deadline: new Date('2021-12-24'), - startDate: new Date('2021-12-01'), - image: 'https://www.tlctranslation.com/wp-content/uploads/2016/05/translation-makes-you-money-1024x602-1.jpg' - - }, - { - id: 4, - title: 'Spar 100kr!', - description: 'Spar 100kr i løpet av denne mÃ¥neden.', - goalSum: 100, - currentSum: 100, - deadline: new Date('2021-12-24'), - startDate: new Date('2021-12-01'), - image: 'https://www.tlctranslation.com/wp-content/uploads/2016/05/translation-makes-you-money-1024x602-1.jpg' - - } -] - -const completedMilestonesTestData = [ - { - id: 4, - title: 'Spar 100kr!', - description: 'Spar 100kr i løpet av denne mÃ¥neden.', - goalSum: 100, - currentSum: 100, - deadline: new Date('2021-12-24'), - startDate: new Date('2021-12-01'), - image: 'https://www.tlctranslation.com/wp-content/uploads/2016/05/translation-makes-you-money-1024x602-1.jpg' - - }, - - { - id: 8, - title: 'Spar 5kr!', - description: 'Spar 5kr i løpet av denne mÃ¥neden.', - goalSum: 5, - currentSum: 5, - deadline: new Date('2021-12-24'), - startDate: new Date('2021-12-01'), - image: 'https://www.tlctranslation.com/wp-content/uploads/2016/05/translation-makes-you-money-1024x602-1.jpg' - }, - { - id: 9, - title: 'Spar 5kr!', - description: 'Spar 5kr i løpet av denne mÃ¥neden.', - goalSum: 5, - currentSum: 5, - deadline: new Date('2021-12-24'), - startDate: new Date('2021-12-01'), - image: 'https://www.tlctranslation.com/wp-content/uploads/2016/05/translation-makes-you-money-1024x602-1.jpg' - } -] - -const activeMilestones = ref<Milestone[] | null>(activeMilestonesTestData) -const completedMilestones = ref<Milestone[] | null>(completedMilestonesTestData) +onMounted(async () => { + const token = useTokenStore().$state.jwtToken + activeMilestones.value = await getAllMilestones(token); + completedMilestones.value = await getAllMilestoneLogs(token); + console.log(completedMilestones.value); +}) const pages = ref<number>(1) const currentPage = ref<number>(0) @@ -102,18 +29,16 @@ const goToPage = (pageNumber:number) => {} const nextPage = () =>{} interface Milestone{ - id: number; - title: string; - description: string - goalSum: number; - currentSum: number; - deadline: Date; + milestoneId: number; + milestoneTitle: string; + milestoneDescription: string + milestoneGoalSum: number; + milestoneCurrentSum: number; + deadlineDate: Date; startDate: Date; - image: string; + milestoneImage: string; + username: string; } - - - </script> <template> @@ -125,19 +50,24 @@ interface Milestone{ <h2 class="create-milestone-button-title">Lag nytt sparemÃ¥l + </h2> </button> <div class="active-milestones"> + <template v-if="activeMilestones.length === 0"> + <h4>Opps, her var det tomt.<br>Lag ditt første sparemÃ¥l for Ã¥ komme i gang!</h4> + </template> + <template v-else> <ActiveMilestoneDisplay class="active-milestone" v-for="(activeMilestone, index) in activeMilestones" :key="index" - :id="activeMilestone.id" - :title="activeMilestone.title" - :description="activeMilestone.description" - :goalSum="activeMilestone.goalSum" - :currentSum="activeMilestone.currentSum" - :deadline="activeMilestone.deadline" + :id="activeMilestone.milestoneId" + :title="activeMilestone.milestoneTitle" + :description="activeMilestone.milestoneDescription" + :goalSum="activeMilestone.milestoneGoalSum" + :currentSum="activeMilestone.milestoneCurrentSum" + :deadline="activeMilestone.deadlineDate" :startDate="activeMilestone.startDate" - :image="activeMilestone.image" + :image="activeMilestone.milestoneImage" ></ActiveMilestoneDisplay> + </template> </div> </div> @@ -145,18 +75,22 @@ interface Milestone{ <h2 class="completed-milestones-title">SparemÃ¥l historikk</h2> <div class="completed-milestones"> + <template v-if="activeMilestones.length === 0"> + <h4>Du har ingen fullførte sparemÃ¥l<br>Avsluttede sparemÃ¥l ender opp her sÃ¥nn at du fÃ¥r full oversikt.</h4> + </template> + <template v-else> <CompletedMilestoneDisplay class="completed-milestone" v-for="(completedMilestone, index) in completedMilestones" :key="index" - :id="completedMilestone.id" - :title="completedMilestone.title" - :description="completedMilestone.description" - :current-sum="completedMilestone.currentSum" - :goal-sum="completedMilestone.goalSum" - :deadline="completedMilestone.deadline" + :id="completedMilestone.milestoneId" + :title="completedMilestone.milestoneTitle" + :description="completedMilestone.milestoneDescription" + :current-sum="completedMilestone.milestoneCurrentSum" + :goal-sum="completedMilestone.milestoneGoalSum" + :deadline="completedMilestone.deadlineDate" :start-date="completedMilestone.startDate" - :image="completedMilestone.image" + :image="completedMilestone.milestoneImage" ></CompletedMilestoneDisplay> <div class="pagination"> <button @click="previousPage" :disabled="currentPage === 0">Forige side</button> @@ -170,7 +104,9 @@ interface Milestone{ </div> <button @click="nextPage" :disabled="currentPage === pages - 1 || pages === 0">Neste side</button> </div> + </template> </div> + </div> </div> </div> @@ -228,7 +164,7 @@ flex-direction: column; .active-milestones{ display: flex; flex-direction: column; - place-content: space-between; + text-align: center; height: 100%; width: 100%; @@ -269,6 +205,7 @@ flex-direction: column; .completed-milestones{ display: flex; flex-direction: column; + text-align: center; height: 100%; width: 100%; padding: 5.0%; diff --git a/src/views/HomePage/ProfileView.vue b/src/views/HomePage/ProfileView.vue index de9be17ab38d823734c7cb01643d1a841b825326..a8a8abd770597595862f2eadbec57b6aae6f6a87 100644 --- a/src/views/HomePage/ProfileView.vue +++ b/src/views/HomePage/ProfileView.vue @@ -6,7 +6,7 @@ import BankAccountInfo from '@/components/profile/BankAccountInfo.vue' import IncomeInfo from '@/components/profile/IncomeInfo.vue' import BadgeInfo from '@/components/profile/BadgeInfo.vue' import PasswordInfo from '@/components/profile/PasswordInfo.vue' -import { getUserInfo } from '@/utils/profileutils' +import { deleteUser, getUserInfo } from '@/utils/profileutils' import { useTokenStore } from '@/stores/token' const token:string = useTokenStore().jwtToken; @@ -33,11 +33,30 @@ const fetchUserInfo = async () =>{ } } +const logout = () => { + useTokenStore().logout(); +} + +const deleteUserAccount = async () => { + try{ + const response = await deleteUser(token); + console.log(response); + useTokenStore().logout(); + } catch (error){ + console.error('Error deleting user:', error); + } +} + </script> <template> <div class="profile-view"> - <h2 class="view-title">{{firstName}} {{lastName}}</h2> + <div class="header"> + <h2 class="view-title">{{firstName}} {{lastName}}</h2> + <button class="user-button" id="logout-button" @click="logout"> + <h3 class="user-button-title">Logg ut</h3> + </button> + </div> <div id="top"> <div class="component" id="user-info" > <UserInfo></UserInfo> @@ -58,6 +77,11 @@ const fetchUserInfo = async () =>{ <div class="component" id="badges"> <BadgeInfo title="Mynter"></BadgeInfo> </div> + <div class="delete-user-button-box"> + <button class="user-button" id="delete-user-button" @click="deleteUserAccount"> + <h3 class="user-button-title">Slett bruker</h3> + </button> + </div> </div> </div> </template> @@ -74,10 +98,40 @@ const fetchUserInfo = async () =>{ gap: 2.5%; } +.header{ + display: flex; + flex-direction: row; + width: 100%; + place-content: space-between; +} + .view-title{ color: var(--color-heading); } +.user-button{ + border-radius: 20px; + width: 20%; + border: none; +} + +.user-button:hover{ + transform: scale(1.02); +} + +#logout-button{ + background-color: var(--color-logout-button); +} + +#logout-button:active{ + background-color: var(--color-logout-button-click); +} + +.user-button-title{ + color: var(--color-headerText); + font-weight: bold; +} + #top, #middle{ display: flex; flex-direction: row; @@ -94,8 +148,8 @@ const fetchUserInfo = async () =>{ } #bottom{ - padding-bottom: 100px; - min-height: 35%; + height: fit-content; + min-height: 40%; } .component{ border: 2px solid var(--color-border); @@ -117,6 +171,26 @@ const fetchUserInfo = async () =>{ #badges{ width: 100%; + min-height: 80%; +} + +.delete-user-button-box{ + display: flex; + width: 100%; + height: 20%; + margin-top: 1.5%; + margin-bottom: 1.5%; + padding: 0.02%; + place-content: center; +} + +#delete-user-button{ + width: 25%; + background-color: var(--color-cancel-button); +} + +#delete-user-button:active{ + background-color: var(--color-cancel-button-click); } @media only screen and (max-width: 1000px){ @@ -141,6 +215,12 @@ const fetchUserInfo = async () =>{ #user-info, #password-info{ height: 50%; } + + #delete-user-button{ + width: 35%; + height: 75%; + } + } </style> \ No newline at end of file diff --git a/src/views/RegisterPageView.vue b/src/views/RegisterPageView.vue index db4c05725bb6eb65f2a75368ddc9bf987c2df27c..882af700b6bd9cefb610a1a60f0956098dac3ae2 100644 --- a/src/views/RegisterPageView.vue +++ b/src/views/RegisterPageView.vue @@ -16,6 +16,7 @@ const questions = ["Hva er fødselsdatoen din?", "Hva er ditt etternavn?", "Hvor stor intekt har du hver mÃ¥ned?", "Hvor mye har du i faste utgifter hver mÃ¥ned?", + "Hvor mye har du lyst stil Ã¥ spare hver mÃ¥ned?", "Velg din brukskonto", "Velg din sparekonto", ] @@ -34,7 +35,7 @@ const accounts = ref(<Account[]>[]) let index = 0; let currentQuestion = ref(questions[index]) -const questionType = ["date", "text", "text", "number", "number", "selection", "selection"] +const questionType = ["date", "text", "text", "number", "number", "number", "selection", "selection"] let currentQuestionType = ref(questionType[index]) const answer = ref(FirstTimeAnswersStore().userResponses[index]); @@ -106,15 +107,14 @@ const nextButtonText = ref("Neste") function convertToJsonObject(responses: any[]): Record<string, any> { return { - username: useTokenStore().getUsername, birthDate: responses[0], firstName: responses[1], lastName: responses[2], monthlyIncome: responses[3], monthlyFixedExpenses: responses[4], - currentAccount: responses[5], - savingsAccount: responses[6], - isConnectedToBank: useTokenStore().getUserRole + monthlySavings: responses[5], + currentAccount: responses[6], + savingsAccount: responses[7], }; }