Skip to content
Snippets Groups Projects
Commit 28e21b03 authored by Henrik's avatar Henrik
Browse files

Merge branch 'main' into feat/profile-and-settings-improvements

parents 043399a3 4ce33258
No related branches found
No related tags found
1 merge request!106Feat/profile and settings improvements
Showing
with 143 additions and 79 deletions
# SpareSti
## Description
The frontend of sparesti.app. SpareSti is designed to make saving fun. The app is integrated with your online bank, therefore it has an overview of what your money is being spent on and can provide you with personalized saving tips based on this information. The app is suitable for all saving goals and offers motivation and tips tailored to your desires. Since we know that saving money can be difficult, SpareSti automatically deposits money into your savings account when you complete challenges. Based on your saved funds, the feed will give you personalized tips on how your money can be invested, and you will be able to set up a budget that provides you with the overview you need to make informed choices.
The frontend of sparesti.app. SpareSti is designed to make saving fun. The app is suitable for all saving goals and offers motivation and tips tailored to your desires.
Since we know that saving money can be difficult, SpareSti automatically transfers money into your savings account when you complete a challenge.
We provide a set purchasable tires, either Ad-Free or Premium. By purchasing Ad-Free you remove all ads present ont the site. Premium lets you create saving goals with groups and gives you access to budgeting tools
## Links
......@@ -108,18 +112,32 @@ The current application uses mocked bank data to transfer money between savings
```sh
12073650567
```
Account 1 balance: ``100 kr``
- **Account 2**
```sh
12097256355
```
Account 2 balance: ``500000 kr``
- **Account 3**
```sh
12032202452
```
Account 3 balance: ``13000 kr``
- **Account 4**
```sh
12041281683
```
Account 4 balance: ``19372 kr``
## Notes
#### Website limitations
The [sparesti.org](https://sparesti.org/login) website has certain limitations.
We use the free plan provided by [News API](https://newsapi.org/) for accessing the current finacial news that are displayed on our news page. This plan cannot be used on published websites, only localhost. The result of this is an empty news page at [sparesti.org](https://sparesti.org/login).
---
## Contributors
The individuals who contributed to the project:
......
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M840-320 464-614 305-395 120-540v-140l160 120 200-280 200 160h160v360ZM120-160v-280l200 160 160-220 360 281v59H120Z"/></svg>
\ No newline at end of file
<svg xmlns="http://www.w3.org/2000/svg" fill="#FFF" height="24" viewBox="0 -960 960 960" width="24"><path d="M840-320 464-614 305-395 120-540v-140l160 120 200-280 200 160h160v360ZM120-160v-280l200 160 160-220 360 281v59H120Z"/></svg>
\ No newline at end of file
......@@ -30,6 +30,15 @@
<img src="@/assets/icons/newsletter.svg">Nyheter
</router-link>
</li>
<li class="nav-item" v-if="useUserInfoStore().isPremium">
<router-link data-cy="budget"
class="nav-link text-white"
:to="toBudget()"
exact-active-class="active-nav"
@click="toggleDropdown">
<img src="@/assets/icons/budget.svg">Budjsett
</router-link>
</li>
<li class="nav-item">
<router-link data-cy="store" class="nav-link text-white"
:to="toStore()" exact-active-class="active-nav">
......@@ -83,15 +92,6 @@
<img src="@/assets/icons/black_person.svg">Min profil
</router-link>
</li>
<li v-if="useUserInfoStore().isPremium">
<router-link data-cy="budget"
class="dropdown-item dropdown-username-link"
:to="toBudget()"
exact-active-class="active-link"
@click="toggleDropdown">
<img src="@/assets/icons/budget.svg">Budjsett
</router-link>
</li>
<li>
<router-link data-cy="friends"
class="dropdown-item dropdown-username-link"
......@@ -181,7 +181,7 @@ let notificationListRef = ref<NotificationDTO[]>([]);
* @returns {boolean} True if the current route is one of the active pages, otherwise false.
*/
function isAnyActivePage(): boolean {
const activeRoutes = ['/roadmap', '/leaderboard', '/news', '/shop'];
const activeRoutes = ['/roadmap', '/leaderboard', '/news', '/budget-overview', '/shop'];
return activeRoutes.includes(route.path);
}
......
......@@ -57,7 +57,6 @@ const onBudgetContainerPressed = () => {
const onBudgetDeleted = () => {
emit('deletedBudgetEvent');
}
</script>
<template>
......@@ -115,7 +114,7 @@ const onBudgetDeleted = () => {
}
.container-fluid {
border: 4px solid #5959ea;
border: 4px solid #003A58;
min-height: 90px;
border-radius: 15px;
transition: transform 150ms ease-in-out, border 200ms ease-in-out;
......@@ -123,7 +122,7 @@ const onBudgetDeleted = () => {
}
.container-fluid:hover {
border: 4px solid #0000f1;
border: 4px solid #01476b;
transform: scale(1.03);
}
......@@ -175,4 +174,15 @@ div.col-12 p {
align-self: center;
justify-self: right;
}
div.container-fluid.row {
display: flex;
}
@media (max-width: 405px) {
.col-4 {
width: 100%; /* Make each column take up full width */
margin-bottom: 10px; /* Add some spacing between columns */
}
}
</style>
\ No newline at end of file
......@@ -38,7 +38,7 @@ const deleteBudget = async () => {
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h3>Er du sikker på at du vil slette dette budgettet <i>{{ budgetTitle }}?</i></h3>
<h3>Er du sikker på at du vil slette dette budgettet {{ budgetTitle }}?</h3>
<button class="btn btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
......
......@@ -34,15 +34,18 @@ const emitImportBudgetEvent = (budgetId: number) => {
<button class="btn btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<MiniBudgetBox v-for="(item, index) in listOfBudgetResponseDTO"
:key="index"
:budget-id="Number(item.id) || 0"
:budget-title="item.budgetName"
:budget-amount="Number(item.budgetAmount)"
:expense-amount="Number(item.expenseAmount)"
@importBudgetEvent="emitImportBudgetEvent"
data-bs-dismiss="modal">
</MiniBudgetBox>
<h6 v-if="listOfBudgetResponseDTO.length === 0" class="text-center">Du har ingen budsjetter du kan importere</h6>
<div v-else>
<MiniBudgetBox v-for="(item, index) in listOfBudgetResponseDTO"
:key="index"
:budget-id="Number(item.id) || 0"
:budget-title="item.budgetName"
:budget-amount="Number(item.budgetAmount)"
:expense-amount="Number(item.expenseAmount)"
@importBudgetEvent="emitImportBudgetEvent"
data-bs-dismiss="modal">
</MiniBudgetBox>
</div>
</div>
</div>
</div>
......
......@@ -18,7 +18,7 @@ const props = defineProps({
aria-valuemin="0"
aria-valuemax="100"/>
</div>
<label class="row text-info font-bold display-5">{{ Math.round(props.percentage*100) + '%' }} Completed</label>
<label class="row text-info font-bold display-5">{{ Math.round(props.percentage*100) + '%' }} Fullført</label>
</div>
</template>
......
......@@ -46,15 +46,22 @@ const handleSubmit = async () => {
formRef.value.classList.add("was-validated")
const form = formRef.value;
if (form.checkValidity()) {
errorMsg.value = '';
try {
await AccountControllerService.getAccountsByBban({bban: Number(checkingAccount.value)})
} catch (error) {
errorMsg.value = "Fant ikke forbrukskonto"
return
}
try {
await AccountControllerService.getAccountsByBban({bban: Number(savingsAccount.value)})
useConfigurationStore().setChekingAccountBBAN(Number(checkingAccount.value))
useConfigurationStore().setSavingsAccountBBAN(Number(savingsAccount.value))
await router.push("/commitment")
} catch (error) {
errorMsg.value = handleUnknownError(error)
errorMsg.value = "Fant ikke sparekonto"
return
}
useConfigurationStore().setChekingAccountBBAN(Number(checkingAccount.value))
useConfigurationStore().setSavingsAccountBBAN(Number(savingsAccount.value))
await router.push("/commitment")
}
}
</script>
......@@ -73,7 +80,7 @@ const handleSubmit = async () => {
type="number"
min="10000000000"
max="99999999999"
label="Brukskonto"
label="Forbrukskonto"
placeholder="Skriv inn din brukskonto"
invalid-message="Vennligst skriv inn din brukskonto (11 siffer)"/>
......
......@@ -44,24 +44,24 @@ const handleSubmit = () => {
<div class="container">
<div>
<h3 class="d-flex align-items-center justify-content-center">
How much experience do you have with saving money?
Hvor mye erfaring har du med å spare penger?
</h3>
</div>
<form class="btn-group-vertical" ref="formRef">
<input ref="beginnerRef" type="radio" class="btn-check" name="experience" id="btn-check-outlined" autocomplete="off" required>
<label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check-outlined">Beginner</label>
<label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check-outlined">Lite</label>
<input ref="someExperienceRef" type="radio" class="btn-check" name="experience" id="btn-check2-outlined" autocomplete="off" required>
<label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check2-outlined">Some experience</label>
<label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check2-outlined">Noe</label>
<input ref="expertRef" type="radio" class="btn-check" name="experience" id="btn-check3-outlined" autocomplete="off" required>
<label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check3-outlined">Expert</label>
<label class="btn btn-outline-primary d-flex align-items-center justify-content-center" for="btn-check3-outlined">Ekspert</label>
</form>
<p class="text-danger">{{ errorMsg }}</p>
<div class="confirm-button-container">
<BaseButton id="confirmButton" @click="handleSubmit" button-text="Continue"/>
<BaseButton id="confirmButton" @click="handleSubmit" button-text="Fortsett"/>
</div>
</div>
</template>
......
......@@ -101,10 +101,10 @@ const handleSumInputEvent = (newSum: number) => {
<div class="container">
<div>
<h3 class="d-flex align-items-center justify-content-center">
Now it remains only one step
Nå gjenstår det kun ett steg
</h3>
<h5 class="d-flex align-items-center justify-content-center">
Create your first saving goal
Lag ditt første sparemål
</h5>
</div>
......@@ -113,15 +113,15 @@ const handleSumInputEvent = (newSum: number) => {
@input-change-event="handleTitleInputEvent"
id="titleInput"
input-id="title"
label="Title"
placeholder="Enter the title of the saving goal"/>
label="Navn"
placeholder="Oppgi navnet på sparemålet"/>
<div>
<label for="description">Description</label>
<textarea v-model="descriptionRef"
type="text"
maxlength="150"
class="form-control"
placeholder="Enter description of the saving goal here (optional)"
placeholder="Oppgi en beskrivelse på sparemålet her (valgfritt)"
id="description"/>
</div>
<BaseInput :model-value="dateRef"
......@@ -130,19 +130,19 @@ const handleSumInputEvent = (newSum: number) => {
input-id="dueDate"
type="date"
:min="getTodayDate()"
label="Due date"/>
label="Utløpsdato"/>
<BaseInput :model-value="sumRef"
@input-change-event="handleSumInputEvent"
id="sumToSaveInput"
input-id="sumToSpareInput"
type="number"
label="Sum to save"
label="Sum"
min="0"
placeholder="Enter the sum you would like to spare (kr)"/>
placeholder="Oppgi summen du ønsker å spare (kr)"/>
</form>
<div class="confirm-button-container">
<BaseButton id="confirmButton" @click="handleSubmit" button-text="Continue"></BaseButton>
<BaseButton id="confirmButton" @click="handleSubmit" button-text="Fortsett"></BaseButton>
</div>
<div style="color: red">
{{ errorMessage }}
......@@ -153,6 +153,10 @@ const handleSumInputEvent = (newSum: number) => {
<style scoped>
#titleInput, #description, #dueDateInput, #sumToSaveInput {
margin-top: 5px;
}
#description {
resize: none;
height: auto;
......
......@@ -21,6 +21,28 @@ let errorMsg = ref('')
// Represents a list of available challenges.
const challenges: string[] = ['NO_COFFEE' , 'NO_CAR' , 'SHORTER_SHOWER' , 'SPEND_LESS_ON_FOOD' , 'BUY_USED_CLOTHES' , 'LESS_SHOPPING' , 'DROP_SUBSCRIPTION' , 'SELL_SOMETHING' , 'BUY_USED' , 'EAT_PACKED_LUNCH' , 'STOP_SHOPPING' , 'ZERO_SPENDING' , 'RENT_YOUR_STUFF' , 'MEATLESS' , 'SCREEN_TIME_LIMIT' , 'UNPLUGGED_ENTERTAINMENT']
/**
* Mapping between challenge enum and norwegian translation.
*/
const challengeMapper: any = {
"NO_COFFEE": "Droppe kaffe",
"NO_CAR": "Droppe bil",
"SHORTER_SHOWER": "Ta kortere dusjer",
"SPEND_LESS_ON_FOOD": "Bruk mindre penger på mat",
"BUY_USED_CLOTHES": "Kjøp brukte klær",
"LESS_SHOPPING": "Handle mindre",
"DROP_SUBSCRIPTION": "Si opp abonnement",
"SELL_SOMETHING": "Selg noe",
"BUY_USED": "Kjøp brukt",
"EAT_PACKED_LUNCH": "Lag niste",
"STOP_SHOPPING": "Shoppestopp",
"ZERO_SPENDING": "Null-forbruk",
"RENT_YOUR_STUFF": "Lei ut ting",
"MEATLESS": "Kjøttfritt",
"SCREEN_TIME_LIMIT": "Skjerm tidsgrense",
"UNPLUGGED_ENTERTAINMENT": "Strømløs underholdning"
}
/**
* Handles the event when a challenge is selected or deselected.
* @param {Array} value - An array containing the challenge value and its checked status.
......@@ -39,16 +61,6 @@ const onChangedChallengeEvent = (value: never) => {
console.log(chosenChallenges.value)
}
/**
* Converts the given enum value to a formatted text representation.
*
* @param {string} enumValue the enum value to be converted
* @return {string} The formatted text representation of the enum value
*/
const convertEnumToText = (enumValue: String): string => {
return enumValue.charAt(0).toUpperCase() + enumValue.slice(1).replace(/_/g, ' ').toLowerCase();
}
/**
* Retrieves user configuration and signup information, sends a signup request to the backend.
*
......@@ -121,7 +133,7 @@ const handleSubmit = async () => {
<div class="challenge-container row justify-content-center">
<ChallangeCheckBox v-for="(item, index) in challenges"
:id="String(index)"
:text="convertEnumToText(item)"
:text="challengeMapper[item]"
:enum-value="item"
@challengeChangedEvent="onChangedChallengeEvent"/>
</div>
......@@ -129,7 +141,7 @@ const handleSubmit = async () => {
<p class="text-danger">{{ errorMsg }}</p>
<div class="confirm-button-container">
<BaseButton id="confirmButton" @click="handleSubmit" button-text="Continue"/>
<BaseButton id="confirmButton" @click="handleSubmit" button-text="Fortsett"/>
</div>
</div>
</template>
......
......@@ -82,7 +82,9 @@ const goToBudget = (id: number) => {
</div>
</div>
<p class="text-danger">{{ errorMsg }}</p>
<ul class="budgetContainer" :key="budgetListKey">
<hr>
<h5 v-if="budgetList.length === 0" class="text-center">Du har ingen budsjetter</h5>
<ul v-else class="budgetContainer" :key="budgetListKey">
<li v-for="(item, index) in budgetList">
<budget-box
:key="index"
......@@ -96,23 +98,7 @@ const goToBudget = (id: number) => {
></budget-box>
</li>
</ul>
<nav id="navbar" aria-label="Sidenavigasjon eksempel">
<ul class="pagination">
<li class="page-item">
<a class="page-link" href="#" aria-label="Forrige">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
<li class="page-item"><a class="page-link" href="#">1</a></li>
<li class="page-item"><a class="page-link" href="#">2</a></li>
<li class="page-item"><a class="page-link" href="#">3</a></li>
<li class="page-item">
<a class="page-link" href="#" aria-label="Neste">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
</ul>
</nav>
</div>
</template>
......
......@@ -225,7 +225,7 @@ const importBudget = async (budgetId: number) => {
<h1 class="text-center">{{ title }}</h1>
<div class="button-container">
<BaseButton id="goBack" @click="router.push('/budsjett-oversikt')" button-text="Gå tilbake"/>
<BaseButton id="goBack" @click="router.push('/budget-overview')" button-text="Gå tilbake"/>
<BaseButton id="optionButton" button-text="Alternativer" data-bs-toggle="modal" data-bs-target="#modal"/>
</div>
......@@ -260,7 +260,7 @@ const importBudget = async (budgetId: number) => {
<confirm-delete-modal :budget-id="useBudgetStore().getActiveBudgetId"
modal-id="confirm-modal"
:budgetTitle="title"
@deletedEvent="router.push('/budsjett-oversikt')"/>
@deletedEvent="router.push('/budget-overview')"/>
<import-budget-modal modal-id="import-modal"
:listOfBudgetResponseDTO="budgetDTOList"
......@@ -298,7 +298,7 @@ const importBudget = async (budgetId: number) => {
<div class="input-group">
<span class="input-group-text">Ditt budsjett </span>
<input type="text" class="form-control" placeholder="Skriv inn ditt budsjett" required v-model="budgetValue">
<button type="submit" class="btn btn-primary">Beregn</button>
<BaseButton id="calculate-budget" type="submit" class="btn" button-text="Beregn"></BaseButton>
</div>
</form>
......@@ -307,7 +307,7 @@ const importBudget = async (budgetId: number) => {
<span class="input-group-text">Legg til ny utgift </span>
<input type="text" class="form-control" placeholder="Navn på utgift" required v-model="expenseDescription">
<input type="number" min="0" class="form-control" placeholder="Beløp (kr)" required v-model="expenseAmount">
<button type="submit" class="btn btn-primary">Beregn</button>
<BaseButton id="calculate-expense" type="submit" class="btn" button-text="Beregn"></BaseButton>
</div>
</form>
</div>
......@@ -325,6 +325,7 @@ const importBudget = async (budgetId: number) => {
@editEvent="editExpense"/>
</div>
</div>
<h5 v-else class="text-center">Du har ingen utgifter</h5>
</div>
</template>
......@@ -422,4 +423,27 @@ div.info:hover {
max-height: 100vh;
}
@media (max-width: 550px) {
div.budget-info-container {
display: flex;
flex-direction: column;
}
}
@media (max-width: 400px) {
div.budget-info-container {
display: flex;
flex-direction: column;
}
.input-group {
display: block; /* Change display to block to stack vertically */
margin-bottom: 10px; /* Add some spacing between stacked input groups */
gap: 5px;
}
.input-group input {
min-width: 100%;
}
}
</style>
\ No newline at end of file
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment