1
0
Fork 0
mirror of https://github.com/dancojocaru2000/foxbank.git synced 2025-06-19 11:02:28 +03:00

Compare commits

...

12 commits

Author SHA1 Message Date
DariusTFox24
1c56bf2a60
Merge pull request #11 from dancojocaru2000/Frontend
Finished frontend implementation with api
2022-01-04 01:25:25 +02:00
DariusTFox24
57d9ff8ed0 Finished frontend implementation with api 2022-01-04 01:22:27 +02:00
649e2c729f
Merge pull request #10 from dancojocaru2000/Backend
Currency exchange in Backend
2022-01-03 21:57:28 +02:00
DariusTFox24
ab53e05287
Merge pull request #9 from dancojocaru2000/Frontend
Frontend updates
2022-01-03 14:28:47 +02:00
DariusTFox24
e8477db7b8 Implemented api stuff 2022-01-03 14:25:32 +02:00
1250b12049
Merge pull request #8 from dancojocaru2000/Backend
Hotfix: notification for received transfer has wrong id
2022-01-03 14:25:32 +02:00
DariusTFox24
5e19ab457b
Merge pull request #7 from dancojocaru2000/Backend
Backend updates
2022-01-03 09:53:40 +02:00
c429506bdf
Merge pull request #6 from dancojocaru2000/Frontend
Frontend Accounts functionality
2021-12-30 01:50:39 +02:00
a0a3fe774d
Merge pull request #5 from dancojocaru2000/Backend
Accounts functionality
2021-12-30 01:49:36 +02:00
DariusTFox24
e369315034 Updated state management and implemented new account functionality 2021-12-30 01:45:14 +02:00
DariusTFox
b76cbe77c7 Partial implementation of select instead of text for currency type 2021-12-08 16:21:06 +02:00
DariusTFox
0e27defbdc Partial API frontend implementation 2021-12-08 13:54:31 +02:00
14 changed files with 2867 additions and 150 deletions

15
client/.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "pwa-msedge",
"request": "launch",
"name": "Launch Edge against localhost",
"url": "http://localhost:5000",
"webRoot": "${workspaceFolder}/public"
}
]
}

2257
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -17,6 +17,7 @@
"rollup-plugin-svelte": "^7.0.0", "rollup-plugin-svelte": "^7.0.0",
"rollup-plugin-terser": "^7.0.0", "rollup-plugin-terser": "^7.0.0",
"svelte": "^3.0.0", "svelte": "^3.0.0",
"svelte-keydown": "^0.4.0",
"svelte-preprocess": "^4.9.8" "svelte-preprocess": "^4.9.8"
}, },
"dependencies": { "dependencies": {

View file

@ -5,20 +5,24 @@
import CardBG from "./CardBG.svelte"; import CardBG from "./CardBG.svelte";
import DetailField from './DetailField.svelte'; import DetailField from './DetailField.svelte';
import GreenButton from './GreenButton.svelte'; import GreenButton from './GreenButton.svelte';
import {createEventDispatcher} from 'svelte'; import {createEventDispatcher, onMount, getContext} from 'svelte';
import { fade, fly, slide } from 'svelte/transition'; import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { gettransactions } from './api';
import { amountToString } from './utils';
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const token = getContext("token");
export let type="RON Account"; export let name="RON Account";
export let currency="RON"; export let currency="RON";
export let balance="5425"; export let balance="5425";
export let iban="RONFOX62188921"; export let iban="RONFOX62188921";
export let isExpanded=false; export let isExpanded=false;
export let transactions=[]; export let transactions=[];
export let accountId;
let copied = false; let copied = false;
@ -42,20 +46,22 @@
dispatch("createPopup",{ dispatch("createPopup",{
type: 'send_money', type: 'send_money',
account: { account: {
type, id: accountId,
type: name,
currency, currency,
balance, balance,
iban, iban,
} }
}); });
} }
</script> </script>
<CardBG class="flex-shrink flex flex-col items-stretch md:self-start mt-16 mb-6 px-6 pt-6 pb-0 max-h-full overflow-clip min-h-0"> <CardBG class="flex-shrink flex flex-col items-stretch md:self-start mt-16 mb-6 px-6 pt-6 pb-0 max-h-full overflow-clip min-h-0">
<div class="flex flex-col flex-shrink"> <div class="flex flex-col flex-shrink">
<div class='font-sans mt-2 mx-2 border-b-1'> <div class='font-sans mt-2 mx-2 border-b-1'>
<h3 class="text-gray-50 inline mr-4">{type}</h3> <h3 class="text-gray-50 inline mr-4">{name}</h3>
<span class="text-gray-100">IBAN: {iban}</span> <span class="text-gray-100">IBAN: {iban}</span>
<button on:click={copyIban} class="inline {copied ? "cursor-default" : ""}"> <Icon icon={copied ? "akar-icons:check" : "akar-icons:copy"} color="rgba(249, 250, 251, 1)"/></button> <button on:click={copyIban} class="inline {copied ? "cursor-default" : ""}"> <Icon icon={copied ? "akar-icons:check" : "akar-icons:copy"} color="rgba(249, 250, 251, 1)"/></button>
</div> </div>
@ -68,7 +74,7 @@
<div> <div>
<DetailField class="p-1 flex-shrink"> <DetailField class="p-1 flex-shrink">
<h2 class='font-sans mt-3 mb-2 pl-2 text-4xl text-gray-50'>Balance: <span style="color: #264D59">{balance}</span>{currency}</h2> <h2 class='font-sans mt-3 mb-2 pl-2 text-4xl text-gray-50'>Balance: <span style="color: #264D59">{amountToString(balance)}</span>{currency}</h2>
</DetailField> </DetailField>
</div> </div>
@ -80,18 +86,27 @@
<DetailField class="my-3 py-1 flex-shrink min-w-transaction"> <DetailField class="my-3 py-1 flex-shrink min-w-transaction">
<div class='font-sans text-gray-50 mt-2 mx-4 border-b-1'> <div class='font-sans text-gray-50 mt-2 mx-4 border-b-1'>
<h3 class="inline mr-3">{transaction.title}: </h3> {#if transaction.transactionType == "send_transfer"}
<span class="text-4xl {transaction.type == "send" ? "text-red-c" : "text-lime-c"}">{transaction.amount}</span> <h3 class="inline mr-3">Sent money: </h3>
<span class="text-4xl">{currency}</span> {:else if transaction.transactionType == "receive_transfer"}
<h3 class="inline mr-3">Received money: </h3>
{:else if transaction.transactionType == "card_payment"}
<h3 class="inline mr-3">{transaction.otherParty.store}: </h3>
{:else if transaction.transactionType == "fee"}
<h3 class="inline mr-3">Fees: </h3>
{/if}
<span class="text-4xl {transaction.transactionType == "receive_transfer" ? "text-lime-c" : "text-red-c"}">{amountToString(transaction.extra.amount)}</span>
<span class="text-4xl">{transaction.extra.currency}</span>
</div> </div>
<div class='font-sans text-2xl text-gray-100 mt-2 mx-6 border-b-1'> <div class='font-sans text-2xl text-gray-100 mt-2 mx-6 border-b-1'>
<p class="inline">at {transaction.time} </p> <p class="inline">at {new Date(transaction.datetime).toLocaleString()} </p>
{#if transaction.status == "PROCESSED"} {#if transaction.status == "processed"}
<span> <span>
<Icon class="inline mb-1" icon="akar-icons:circle-check" color="#6DE25ACC"/> <Icon class="inline mb-1" icon="akar-icons:circle-check" color="#6DE25ACC"/>
</span> </span>
{:else if transaction.status == "PENDING"} {:else if transaction.status == "pending"}
<span> <span>
<Icon class="inline mb-1" icon="akar-icons:arrow-cycle" color="#F6AF43"/> <Icon class="inline mb-1" icon="akar-icons:arrow-cycle" color="#F6AF43"/>
</span> </span>
@ -114,18 +129,27 @@
{:else if transactions.length > 0} {:else if transactions.length > 0}
<DetailField class="my-3 py-2 flex-shrink min-w-transaction"> <DetailField class="my-3 py-2 flex-shrink min-w-transaction">
<div class='font-sans text-gray-50 mt-2 mx-4 border-b-1'> <div class='font-sans text-gray-50 mt-2 mx-4 border-b-1'>
<h3 class="inline mr-3">{transactions[0].title}: </h3> {#if transactions[0].transactionType == "send_transfer"}
<span class="text-4xl {transactions[0].type == "send" ? "text-red-c" : "text-lime-c"}">{transactions[0].amount}</span> <h3 class="inline mr-3">Sent money: </h3>
<span class="text-4xl">{currency}</span> {:else if transactions[0].transactionType == "receive_transfer"}
<h3 class="inline mr-3">Received money: </h3>
{:else if transactions[0].transactionType == "card_payment"}
<h3 class="inline mr-3">{transactions[0].otherParty.store}: </h3>
{:else if transactions[0].transactionType == "fee"}
<h3 class="inline mr-3">Fees: </h3>
{/if}
<span class="text-4xl {transactions[0].transactionType == "receive_transfer" ? "text-lime-c" : "text-red-c"}">{amountToString(transactions[0].extra.amount)}</span>
<span class="text-4xl">{transactions[0].extra.currency}</span>
</div> </div>
<div class='font-sans text-2xl text-gray-100 mt-2 mx-6 border-b-1'> <div class='font-sans text-2xl text-gray-100 mt-2 mx-6 border-b-1'>
<p class="inline">at {transactions[0].time} </p> <p class="inline">at {new Date(transactions[0].datetime).toLocaleString()} </p>
{#if transactions[0].status == "PROCESSED"} {#if transactions[0].status == "processed"}
<span> <span>
<Icon class="inline mb-1" icon="akar-icons:circle-check" color="#6DE25ACC"/> <Icon class="inline mb-1" icon="akar-icons:circle-check" color="#6DE25ACC"/>
</span> </span>
{:else if transactions[0].status == "PENDING"} {:else if transactions[0].status == "pending"}
<span> <span>
<Icon class="inline mb-1" icon="akar-icons:arrow-cycle" color="#F6AF43"/> <Icon class="inline mb-1" icon="akar-icons:arrow-cycle" color="#F6AF43"/>
</span> </span>

View file

@ -1,6 +1,7 @@
<script> <script>
import { onMount } from "svelte"; import { onMount, setContext } from "svelte";
import { whoami } from "./api"; import { writable, readable } from "svelte/store";
import { whoami, createnotification, getaccountlist, getnotificationlist } from "./api";
import BottomBorder from "./BottomBorder.svelte"; import BottomBorder from "./BottomBorder.svelte";
import CheckNotifications from "./CheckNotifications.svelte"; import CheckNotifications from "./CheckNotifications.svelte";
@ -12,16 +13,115 @@ import { whoami } from "./api";
import SendMoney from "./SendMoney.svelte"; import SendMoney from "./SendMoney.svelte";
import TopBorder from "./TopBorder.svelte"; import TopBorder from "./TopBorder.svelte";
let loggedin = false; const userToken = writable(sessionStorage.getItem("token"));
function toggleLoggedIn() { userToken.subscribe(newToken => {
loggedin = !loggedin; sessionStorage.setItem("token", newToken);
})
setContext("token", userToken);
const user = readable(null, set => {
const unsubscribe = userToken.subscribe(token => {
if(token == null) {
set(null);
}else{
whoami(token)
.then(result =>{
if(result.status != "success") {
$userToken = null;
}else {
set(result);
} }
})
}
})
return () => {
unsubscribe();
}
})
setContext("user", user);
const refreshAccounts = writable(null);
setContext("refreshAccounts", refreshAccounts);
const accounts = readable(null, set => {
function getAccounts(token){
if(token==null){
set(null);
}else{
getaccountlist(token)
.then(result => {
set(result);
})
}
}
let token = null;
refreshAccounts.set( () => {
getAccounts(token);
});
const unsubscribe = userToken.subscribe(newToken => {
token = newToken;
getAccounts(token);
})
const intervalId = setInterval(() => {
getAccounts(token);
}, 10000);
return () => {
unsubscribe();
clearInterval(intervalId);
}
})
setContext("accounts", accounts);
const refreshNotifications = writable(null);
setContext("refreshNotifications", refreshNotifications);
const notifications = readable(null, set=> {
function getNotifications(token){
if(token==null){
set(null);
}else{
getnotificationlist(token)
.then(result => {
set(result);
})
}
}
let token = null;
refreshNotifications.set( () => {
getNotifications(token);
});
const unsubscribe = userToken.subscribe(newToken => {
token = newToken;
getNotifications(token);
})
const intervalId = setInterval(() => {
getNotifications(token);
}, 10000);
return () => {
unsubscribe();
clearInterval(intervalId);
}
})
setContext("notifications", notifications);
let isCreatingAccount = false; let isCreatingAccount = false;
let isCheckingNotifications = false; let isCheckingNotifications = false;
let isSendingMoney = false; let isSendingMoney = false;
let sendingAccount = ""; let sendingAccount = "";
let notifications = [];
function onCreatePopup(event) { function onCreatePopup(event) {
const eventType = event.detail.type; const eventType = event.detail.type;
@ -34,12 +134,18 @@ import { whoami } from "./api";
var today = new Date(); var today = new Date();
var date = today.getDate()+'/'+(today.getMonth()+1)+'/'+today.getFullYear(); var date = today.getDate()+'/'+(today.getMonth()+1)+'/'+today.getFullYear();
var time = today.getHours() + ":" + today.getMinutes(); var time = today.getHours() + ":" + today.getMinutes();
var body = "The "+ eventType.currency + " account with the name " + eventType.type + " was created succesfully!";
//add notification about created account
createnotification(async function() {
const result = await createnotification($userToken, body, date+time);
if(result.status == "success") {
console.log("Succesfully created notification.");
}else{
console.log("Failed to create notification.");
}
})
notifications.push(
{
text: "The new account '" + event.detail.type+ "' was created successfully!",
time: time + " " + date,
});
isCreatingAccount = false; isCreatingAccount = false;
break; break;
@ -48,7 +154,7 @@ import { whoami } from "./api";
break; break;
case "create_acc_failed": case "create_acc_failed":
isCreatingAccount = false; // isCreatingAccount = false;
alert(`Account creation failed! Reason: ${event.detail.reason}`); alert(`Account creation failed! Reason: ${event.detail.reason}`);
break; break;
@ -66,7 +172,6 @@ import { whoami } from "./api";
break; break;
case "check_notifications": case "check_notifications":
notifications = event.detail.notifications;
isCheckingNotifications = true; isCheckingNotifications = true;
break; break;
@ -79,26 +184,13 @@ import { whoami } from "./api";
} }
} }
onMount(async function() {
const token = sessionStorage.getItem("token");
if(token == null){
loggedin = false;
}else {
const result = await whoami(token);
if (result.status == "success") {
loggedin = true;
}else {
loggedin = false;
}
}
})
</script> </script>
<main class="flex flex-col items-stretch bg-banner bg-cover bg-center bg-fixed h-screen font-sans"> <main class="flex flex-col items-stretch bg-banner bg-cover bg-center bg-fixed h-screen font-sans">
<TopBorder class="flex-shrink"></TopBorder> <TopBorder class="flex-shrink"></TopBorder>
<div class="flex-grow max-h-full overflow-hidden"> <div class="flex-grow max-h-full overflow-hidden">
{#if loggedin} {#if $user}
{#if isCreatingAccount} {#if isCreatingAccount}
<Overlay> <Overlay>
@ -109,7 +201,7 @@ import { whoami } from "./api";
{:else if isCheckingNotifications} {:else if isCheckingNotifications}
<Overlay> <Overlay>
<div class="flex items-center justify-center h-full"> <div class="flex items-center justify-center h-full">
<CheckNotifications on:createPopup={onCreatePopup} notifications={notifications}></CheckNotifications> <CheckNotifications on:createPopup={onCreatePopup}></CheckNotifications>
</div> </div>
</Overlay> </Overlay>
{:else if isSendingMoney} {:else if isSendingMoney}
@ -120,10 +212,10 @@ import { whoami } from "./api";
</Overlay> </Overlay>
{/if} {/if}
<MainPage on:createPopup={onCreatePopup} on:logOut={toggleLoggedIn}></MainPage> <MainPage on:createPopup={onCreatePopup} on:logOut={()=>$userToken=null}></MainPage>
{:else} {:else}
<Login on:loginSuccess={toggleLoggedIn}></Login> <Login on:loginSuccess={(e)=> $userToken = e.detail.token}></Login>
{/if} {/if}

View file

@ -1,21 +1,32 @@
<script> <script>
import CardBG from "./CardBG.svelte"; import CardBG from "./CardBG.svelte";
import {createEventDispatcher} from 'svelte'; import {createEventDispatcher, getContext} from 'svelte';
import Icon from "@iconify/svelte"; import Icon from "@iconify/svelte";
import { fade, fly, slide } from 'svelte/transition'; import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import DetailField from "./DetailField.svelte"; import DetailField from "./DetailField.svelte";
import { marknotificationread } from "./api";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
const notificationsStore = getContext("notifications");
const refreshNotifications = getContext("refreshNotifications");
const token = getContext("token");
export let notifications = [{time: "15:38 27/11/2021", text: "A notification's text."}]; $: notifications = $notificationsStore ? $notificationsStore.notifications : [];
function cancelCheckNotifications(){ function cancelCheckNotifications(){
dispatch("createPopup",{type:"check_notifications_cancelled"}); dispatch("createPopup",{type:"check_notifications_cancelled"});
} }
async function onNotificationClick(id){
await marknotificationread($token, id);
if($refreshNotifications){
$refreshNotifications();
}
}
</script> </script>
<div class="h-full self-center"> <div class="h-full self-center">
@ -28,23 +39,44 @@
<div class="w-full max-w-md self-start border-solid border mb-3"></div> <div class="w-full max-w-md self-start border-solid border mb-3"></div>
<div class="flex flex-col flex-grow pl-8 pr-10 relative scroller overflow-auto overflow-x-hidden max-h-full min-h-0"> <div class="flex flex-col flex-grow pl-8 pr-10 relative scroller overflow-auto overflow-x-hidden max-h-full min-h-0">
{#if notifications.length > 0}
{#each notifications as notification,i (i)} {#each notifications as notification,i (i)}
<div in:slide={{delay:100*i}} out:slide={{delay:50*(notifications.length-i)}}> <div on:click={() => onNotificationClick(notification.id)} in:slide={{delay:100*i}} out:slide={{delay:50*(notifications.length-i)}}>
<DetailField class="my-3 py-1 flex-shrink min-w-transaction max-w-4xl"> <DetailField class="relative my-3 py-1 flex-shrink min-w-transaction max-w-4xl">
<div class='font-sans text-gray-50 text-2xl mt-2 mx-4 border-b-1'> <div class='font-sans text-gray-50 text-2xl mt-2 mx-4 border-b-1'>
{notification.text} {notification.body}
</div> </div>
<div class="flex flex-row"> <div class="flex flex-row">
<div class='inline font-sans ml-auto mr-4 text-xl text-gray-100 mt-2 mx-6 border-b-1'> <div class='inline font-sans ml-auto mr-4 text-xl text-gray-100 mt-2 mx-6 border-b-1'>
<span> at {notification.time} </span> <span> at {new Date(notification.datetime).toLocaleString()} </span>
</div> </div>
</div> </div>
<div class='fixed font-sans text-6xl text-gray-100 left-2 bottom-2'>
{#if !notification.read}
<svg class="fill-current text-gray-100" viewBox="0 0 16 16" width="16" height="16">
<circle cx="8" cy="8" r="8"></circle>
</svg>
{:else}
<svg class="fill-current text-gray-100" viewBox="0 0 24 24" width="16" height="16">
<path d="M20.285 2l-11.285 11.567-5.286-5.011-3.714 3.716 9 8.728 15-15.285z"/>
</svg>
{/if}
</div>
</DetailField> </DetailField>
</div> </div>
{/each} {/each}
{:else}
<DetailField class="relative my-3 py-1 flex-shrink min-w-transaction max-w-4xl">
<div class='font-sans text-gray-50 text-2xl mt-2 mx-4 border-b-1'>
No notifications.
</div>
</DetailField>
{/if}
</div> </div>
<div class="m-2"></div> <div class="m-2"></div>

View file

@ -3,31 +3,42 @@
import CardBG from "./CardBG.svelte"; import CardBG from "./CardBG.svelte";
import InputField from "./InputField.svelte"; import InputField from "./InputField.svelte";
import {createEventDispatcher} from 'svelte'; import {createEventDispatcher, onMount} from 'svelte';
import Icon from "@iconify/svelte"; import Icon from "@iconify/svelte";
import Overlay from "./Overlay.svelte"; import Overlay from "./Overlay.svelte";
import { fade, fly, slide } from 'svelte/transition'; import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { createaccount, getcurrencies, getaccounttypes } from "./api";
import { getContext } from "svelte";
const token = getContext("token");
const refreshAccounts = getContext("refreshAccounts");
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let type = ""; let type = null;
let currencies = ["RON", "EUR"]; let name = "";
let currency = currencies[0]; let currency = null;
let termsAccepted = false; let termsAccepted = false;
$: placeholder = type==null ? "Checking Account" : `${type} Account`;
function create(){ async function create(){
if(type == "" || type == null) { if(type == null){
alert("Account Name field can not be empty!"); alert("Type is not selected!");
console.debug(`account name: ${type}`) }else if(currency == null){
}else if(!currencies.includes(currency)){ alert("Currency is not selected!");
alert("Currency is not supported!");
}else if (!termsAccepted){ }else if (!termsAccepted){
alert("Terms of Service not accepted!"); alert("Terms of Service not accepted!");
}else{ }else{
//TODO Create account with provided details on the server const result = await createaccount($token, name, currency, type);
dispatch("createPopup",{type:"create_acc_success", account:{type:type, currency:currency, transactions:[]}}); if(result.status == "success") {
dispatch("createPopup",{type:"create_acc_success"});
if($refreshAccounts){
$refreshAccounts();
}
}else{
dispatch("createPopup",{type:"create_acc_failed", reason:"Failed to create account. Error:"+result.status});
}
} }
} }
@ -43,6 +54,11 @@
termsAccepted = !termsAccepted; termsAccepted = !termsAccepted;
} }
onMount(() => {
getcurrencies().then(result => currency = result.currencies[0]);
getaccounttypes().then(result => type = result.accountTypes[0]);
})
</script> </script>
@ -58,12 +74,31 @@
<div class="mx-1 flex-shrink"> <div class="mx-1 flex-shrink">
<h2 class='font-sans text-2xl text-gray-50 mb-2 '>Account name:</h2> <h2 class='font-sans text-2xl text-gray-50 mb-2 '>Account name:</h2>
<InputField placeholder="New Account" isPassword={false} bind:value={type}></InputField> <InputField placeholder={placeholder} isPassword={false} bind:value={name}></InputField>
</div>
<div class="mx-1 flex-shrink">
<h2 class='font-sans text-2xl text-gray-50 mb-2 '>Type:</h2>
<select bind:value={type}>
{#await getaccounttypes() then result}
{#each result.accountTypes as option}
<option class="custom-option" value={option}>{option}</option>
{/each}
{/await}
</select>
</div> </div>
<div class="mx-1 flex-shrink"> <div class="mx-1 flex-shrink">
<h2 class='font-sans text-2xl text-gray-50 mb-2 '>Currency:</h2> <h2 class='font-sans text-2xl text-gray-50 mb-2 '>Currency:</h2>
<InputField placeholder="RON" isPassword={false} bind:value={currency}></InputField> <select bind:value={currency}>
{#await getcurrencies() then result}
{#each result.currencies as option}
<option class="custom-option" value={option}>{option}</option>
{/each}
{/await}
</select>
</div> </div>
<div class="mx-1 flex-shrink max-w-2xl"> <div class="mx-1 flex-shrink max-w-2xl">
@ -85,3 +120,28 @@
</div> </div>
</div> </div>
<style>
select{
min-width: 120px;
min-height: 32px;
color: rgba(233, 231, 231, 0.842);
background: linear-gradient(92.55deg, rgba(76, 172, 135, 0.95) -28.27%, rgba(249, 224, 127, 0.096) 115.79%);
filter: drop-shadow(0px 8px 4px rgba(0, 0, 0, 0.25));
border-radius: 3px;
}
select option{
min-width: 120px;
min-height: 32px;
color: rgba(233, 231, 231, 0.842);
background: linear-gradient(92.55deg, rgba(76, 172, 135, 0.95) -28.27%, rgba(249, 224, 127, 0.096) 115.79%);
filter: drop-shadow(0px 8px 4px rgba(0, 0, 0, 0.25));
border-radius: 3px;
}
</style>

View file

@ -9,7 +9,7 @@
</script> </script>
<main> <main>
<input type={isPassword ? "password" : "text"} placeholder={placeholder} value={value} on:input={handleInput} class="placeholder-gray-300 p-3 text-gray-50 w-full text-3xl"> <input on:keydown type={isPassword ? "password" : "text"} placeholder={placeholder} value={value} on:input={handleInput} class="placeholder-gray-300 p-3 text-gray-50 w-full text-3xl">
</main> </main>
<style> <style>

View file

@ -15,14 +15,28 @@
async function checkLogin(){ async function checkLogin(){
const result = await login(username, code); const result = await login(username, code);
if(result.status == "success") { if(result.status == "success") {
sessionStorage.setItem("token", result.token); dispatch("loginSuccess",{
dispatch("loginSuccess",null); token: result.token,
});
}else{ }else{
alert(result.code); alert(result.code);
} }
} }
async function enter(event) {
if (event.key === 'Enter') {
const result = await login(username, code);
if(result.status == "success") {
dispatch("loginSuccess",{
token: result.token,
});
}else{
alert(result.code);
}
}
}
</script> </script>
<main class="h-full"> <main class="h-full">
@ -34,7 +48,7 @@
</div> </div>
<div class="m-3 flex-shrink"> <div class="m-3 flex-shrink">
<InputField placeholder="Code" isPassword={true} bind:value={code}></InputField> <InputField on:keydown={enter} placeholder="Code" isPassword={true} bind:value={code}></InputField>
</div> </div>
<div class="m-3 flex-shrink"> <div class="m-3 flex-shrink">

View file

@ -1,66 +1,58 @@
<script> <script>
import Icon from '@iconify/svelte'; import Icon from '@iconify/svelte';
import CardBG from "./CardBG.svelte"; import CardBG from "./CardBG.svelte";
import {createEventDispatcher, onMount} from 'svelte'; import {createEventDispatcher, onMount, getContext} from 'svelte';
import AccountCard from './AccountCard.svelte'; import AccountCard from './AccountCard.svelte';
import GreenButton from './GreenButton.svelte'; import GreenButton from './GreenButton.svelte';
import { fade, fly, slide } from 'svelte/transition'; import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { logout, whoami } from './api'; import { logout, whoami, getaccountlist, getnotificationlist, getForex } from './api';
import { amountToString } from './utils';
const token = getContext("token");
const user = getContext("user");
const accountsStore = getContext("accounts");
const notificationsStore = getContext("notifications");
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
let fullname = ""; $: fullname = $user.user.fullname;
let username = ""; $: username = $user.user.username;
let email = ""; $: email = $user.user.email;
let code = ""; $: notifications = $notificationsStore ? $notificationsStore.notifications : [];
let totalbalance = "2455.22"; let totalbalance = "0.00";
let maincurrency = "RON"; let maincurrency = "RON";
let expandedAccount = null; let expandedAccount = null;
let showAllAccounts = true; let showAllAccounts = true;
let notifications = [ $: notifications_unread = notifications.filter(n => !n.read).length;
{time: "15:38 27/11/2021", text: "A notification's text."},
{time: "15:38 27/11/2021", text: "A notification's text but longer aaaaaaaaaaaa asddagagfabsdhubaiufbau bdauhsbabsdbayub badysabdyba ybbdbasbd bbdabsdb aybdbaysbdya bybdabs bdabsdbadbua."},
{time: "15:38 27/11/2021", text: "A notification's text but way longer absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd absdb aybdbaysbdya bybdabs bdabsd."},
{time: "15:38 27/11/2021", text: "A notification's text."},
{time: "15:38 27/11/2021", text: "A notification's text."},
]; $: accounts = $accountsStore ? $accountsStore.accounts.map(account => {
return {
name: account.customName ? account.customName : `${account.accountType} Account`,
currency: account.currency,
balance: account.balance,
iban: account.iban.replace(/(.{4})/g, "$1 "),
id: account.id,
transactions: account.transactions,
}
}) : [];
let accounts = [ $: {
{type:"RON Account", currency:"RON", balance:"420.42", iban:"RONFOX62188921", Promise.all(accounts.map(account => {
transactions: [ return getForex(account.currency, maincurrency, account.balance);
{title:"Transaction Name#1", status:"PROCESSED", amount:"-45.09", time:"15:38 27/11/2021", type:"send"}, })).then( balances => {
{title:"Transaction Name#2", status:"PENDING", amount:"+25.00", time:"15:38 27/11/2021", type:"received"}, const sum = balances.reduce((acc, current) => acc+current, 0);
{title:"Transaction Name#3", status:"CANCELLED", amount:"-469.09", time:"15:38 27/11/2021", type:"send"}, totalbalance = amountToString(Math.round(sum));
{title:"Transaction Name#1", status:"PROCESSED", amount:"-45.09", time:"15:38 27/11/2021", type:"send"}, })
{title:"Transaction Name#2", status:"PENDING", amount:"+25.00", time:"15:38 27/11/2021", type:"received"}, }
{title:"Transaction Name#3", status:"CANCELLED", amount:"-469.09", time:"15:38 27/11/2021", type:"send"},
{title:"Transaction Name#1", status:"PROCESSED", amount:"-45.09", time:"15:38 27/11/2021", type:"send"},
{title:"Transaction Name#2", status:"PENDING", amount:"+25.00", time:"15:38 27/11/2021", type:"received"},
{title:"Transaction Name#3", status:"CANCELLED", amount:"-469.09", time:"15:38 27/11/2021", type:"send"},
]
},
{type:"EUR Account", currency:"EUR", balance:"620,42", iban:"EURFOX62188921",
transactions: [
{title:"Transaction Name#2", status:"PENDING", amount:"+25.00", time:"15:38 27/11/2021", type:"received"},
{title:"Transaction Name#1", status:"PROCESSED", amount:"-45.09", time:"15:38 27/11/2021", type:"send"},
{title:"Transaction Name#3", status:"CANCELLED", amount:"-469.09", time:"15:38 27/11/2021", type:"send"},
]
},
];
function dispatchLogout(){ function dispatchLogout(){
//todo: CHeck here
if (confirm("Log out?")) { if (confirm("Log out?")) {
logout(sessionStorage.getItem("token")); logout($token);
sessionStorage.removeItem("token");
dispatch("logOut",null); dispatch("logOut",null);
} }
} }
function expanded(index) { function expanded(index) {
if (!expandedAccount && expandedAccount !== 0) { if (!expandedAccount && expandedAccount !== 0) {
expandedAccount = index; expandedAccount = index;
@ -92,34 +84,24 @@
}); });
} }
onMount( async function() {
const token = sessionStorage.getItem("token");
const result = await whoami(token);
if(result.status == "success") {
fullname = result.user.fullname;
email = result.user.email;
username = result.user.username;
}
})
</script> </script>
<main class="h-full flex flex-col items-stretch md:flex-row"> <main class="h-full flex flex-col items-stretch md:flex-row">
<div class="flex flex-col items-stretch max-h-full"> <div class="flex flex-col items-stretch max-h-full">
{#if expandedAccount || expandedAccount === 0} {#if expandedAccount || expandedAccount === 0}
<AccountCard type={accounts[expandedAccount].type} currency={accounts[expandedAccount].currency} balance={accounts[expandedAccount].balance} iban={accounts[expandedAccount].iban} transactions={accounts[expandedAccount].transactions} isExpanded={true} on:expanded={() => expanded(null)}></AccountCard> <AccountCard accountId={accounts[expandedAccount].id} name={accounts[expandedAccount].name} currency={accounts[expandedAccount].currency} balance={accounts[expandedAccount].balance} iban={accounts[expandedAccount].iban} transactions={accounts[expandedAccount].transactions} isExpanded={true} on:expanded={() => expanded(null)}></AccountCard>
{:else} {:else}
{#if showAllAccounts} {#if showAllAccounts}
{#each accounts as account,i} {#each accounts as account,i}
<div in:slide={{delay:500*i, duration:250*(i==0 ? 1 : i) }}> <div in:slide={{delay:500*i, duration:250*(i==0 ? 1 : i) }}>
<AccountCard type={account.type} currency={account.currency} balance={account.balance} iban={account.iban} transactions={account.transactions} isExpanded={false} on:expanded={() => expanded(i)} on:createPopup></AccountCard> <AccountCard accountId={account.id} name={account.name} currency={account.currency} balance={account.balance} iban={account.iban} transactions={account.transactions} isExpanded={false} on:expanded={() => expanded(i)} on:createPopup></AccountCard>
</div> </div>
{/each} {/each}
{/if} {/if}
{/if} {/if}
<div class="self-center m-0"> <div class="self-center m-0">
{#if showAllAccounts} {#if showAllAccounts && (accounts.length < 3) }
<div in:slide={{delay:500*accounts.length, duration:250*accounts.length}}> <div in:slide={{delay:500*accounts.length, duration:250*accounts.length}}>
<GreenButton on:click={createAccount}><Icon icon="akar-icons:plus" color="rgba(249, 250, 251, 1)" width="26" height="26" /></GreenButton> <GreenButton on:click={createAccount}><Icon icon="akar-icons:plus" color="rgba(249, 250, 251, 1)" width="26" height="26" /></GreenButton>
</div> </div>
@ -134,7 +116,7 @@
<CardBG class="flex-shrink flex flex-col min-w-transaction items-stretch md:self-start p-6"> <CardBG class="flex-shrink flex flex-col min-w-transaction items-stretch md:self-start p-6">
<div class="flex flex-row"> <div class="flex flex-row">
<h1 class='font-sans flex-grow text-5xl text-gray-50 m-6 border-b-2'>{fullname}</h1> <h1 class='font-sans flex-grow text-5xl text-gray-50 m-6 border-b-2'>{fullname}</h1>
<button on:click={checkNotifications} style=" filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));"> <Icon icon="akar-icons:envelope" color="#FB6666" width="36" height="36" /></button> <button on:click={checkNotifications} style=" filter: drop-shadow(0px 4px 4px rgba(0, 0, 0, 0.25));"> <Icon icon={notifications_unread==0 ? "akar-icons:envelope" : "akar-icons:open-envelope"} color="#FB6666" width="36" height="36" /></button>
</div> </div>
<div class="m-3 flex-shrink"> <div class="m-3 flex-shrink">

View file

@ -3,37 +3,46 @@
import CardBG from "./CardBG.svelte"; import CardBG from "./CardBG.svelte";
import InputField from "./InputField.svelte"; import InputField from "./InputField.svelte";
import {createEventDispatcher} from 'svelte'; import {createEventDispatcher, getContext} from 'svelte';
import Icon from "@iconify/svelte"; import Icon from "@iconify/svelte";
import Overlay from "./Overlay.svelte"; import Overlay from "./Overlay.svelte";
import TextareaField from "./TextareaField.svelte"; import TextareaField from "./TextareaField.svelte";
import { fade, fly, slide } from 'svelte/transition'; import { fade, fly, slide } from 'svelte/transition';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import { createnotification, createtransaction } from "./api";
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let account={type: "", currency:"", balance:0}; export let account={id: -1, type: "", currency:"", balance:0};
let receivername=""; let receivername="";
let receiveriban=""; let receiveriban="";
let amount=0.00; let amount=0.00;
let description=""; let description="";
const token = getContext("token");
const refreshAccounts = getContext("refreshAccounts");
let send_details={receivername:"", receiveriban:"", amount:0, description:""};
function create(){ async function create(){
if(receivername == "" || receivername == null) { if(receivername == "" || receivername == null) {
alert("Receiver's name field can not be empty!"); alert("Receiver's name field can not be empty!");
}else if(receiveriban == "" || receiveriban == null){ }else if(receiveriban == "" || receiveriban == null){
alert("Receiver's iBan field can not be empty!"); alert("Receiver's iBan field can not be empty!");
}else if (amount > parseFloat(account.balance) ){ }else if (parseFloat(amount) > parseFloat(account.balance) ){
alert("Not enough money in your account!"); alert("Not enough money in your account!");
}else if (amount <= 0.00 ){ }else if (amount <= 0.00 ){
alert("Insert a valid amount!"); alert("Insert a valid amount!");
}else{ }else{
//TODO Create account with provided details on the server //TODO Create account with provided details on the server
send_details={receivername:receivername, receiveriban:receiveriban, amount:amount, description:description} await createtransaction($token, receiveriban, Math.round(amount*100), account.id, description).then( result => {
dispatch("createPopup",{type:"send_money_success", send_details:{send_details}}); if(result.status == "success") {
dispatch("createPopup",{type:"send_money_success"});
if($refreshAccounts){
$refreshAccounts();
}
}
});
} }
} }
@ -63,7 +72,7 @@
<div class="mx-1 flex-shrink"> <div class="mx-1 flex-shrink">
<h2 class='font-sans text-2xl text-gray-50 mb-2 '>IBAN:</h2> <h2 class='font-sans text-2xl text-gray-50 mb-2 '>IBAN:</h2>
<InputField placeholder={account.currency +"-0000-0000-0000-0000"} isPassword={false} bind:value={receiveriban}></InputField> <InputField placeholder={"RO00 FOXB 0"+account.currency +" 0000 0000 0000"} isPassword={false} bind:value={receiveriban}></InputField>
</div> </div>
<div class="mx-1 flex-shrink"> <div class="mx-1 flex-shrink">

View file

@ -55,3 +55,233 @@ export async function logout(token) {
} }
} }
} }
export async function getaccountlist(token) {
try {
const result = await fetch(new URL("/accounts/", baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
const data = await result.json();
if(data.status == "success"){
for(let i=0; i < data.accounts.length; i++){
const transactionsreq = await gettransactions(token, data.accounts[i].id);
if (transactionsreq.status == "success") {
data.accounts[i].transactions = transactionsreq.transactions;
}
}
}
return data;
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function getcurrencies() {
try {
const result = await fetch(new URL("/accounts/meta/currencies", baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function getaccounttypes() {
try {
const result = await fetch(new URL("/accounts/meta/account_types", baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function createaccount(token, name, currency, type) {
try {
const result = await fetch(new URL("/accounts/", baseURL), {
method: "POST",
body: JSON.stringify({
customName: name,
currency: currency,
accountType: type,
}),
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function getnotificationlist(token) {
try {
const result = await fetch(new URL("/notifications/", baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
const data = await result.json();
if(data.status == "success") {
data.notifications.sort((e1, e2) => new Date(e2.datetime) - new Date(e1.datetime));
}
return data;
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function createnotification(token, body, read) {
try {
const result = await fetch(new URL("/notifications/", baseURL), {
method: "POST",
body: JSON.stringify({
body: body,
read: read,
}),
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function marknotificationread(token, id) {
try {
const result = await fetch(new URL("/notifications/"+id+"/mark_read", baseURL), {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function createtransaction(token, otherparty, amount, accountId, description) {
try {
const result = await fetch(new URL("/transactions/", baseURL), {
method: "POST",
body: JSON.stringify({
description: description,
account_id: accountId,
destination_iban: otherparty,
amount: amount,
}),
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
return (await result.json());
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function gettransactions(token, id) {
try {
const result = await fetch(new URL("/transactions/?account_id="+id, baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer " + token,
},
});
const data = await result.json();
if(data.status == "success") {
data.transactions.sort((e1, e2) => new Date(e2.datetime) - new Date(e1.datetime));
}
return data;
} catch (error) {
return {
status: "error",
code: "request/failure"
}
}
}
export async function getForex(from, to, amount) {
try {
const result = await fetch(new URL("/forex/"+from+"/"+to, baseURL), {
method: "GET",
headers: {
"Content-Type": "application/json",
},
});
const data = await result.json();
if(data.status == "success") {
const exchanged = amount * data.rate;
return exchanged;
}
return null;
} catch (error) {
return null;
}
}

4
client/src/utils.js Normal file
View file

@ -0,0 +1,4 @@
export function amountToString(amount) {
amount = amount.toString().padStart(3, "0");
return amount.replace(/(.{2})$/, ".$1");
}

View file

@ -4,6 +4,7 @@
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0", "version": "0.2.0",
"configurations": [ "configurations": [
{ {
"name": "Python: Flask", "name": "Python: Flask",
"type": "python", "type": "python",