This commit is contained in:
louiscklaw
2025-06-05 13:51:25 +08:00
parent d909805283
commit d3ef280b20
23 changed files with 1900 additions and 0 deletions

View File

@@ -0,0 +1,96 @@
import {
IonButton,
IonButtons,
IonCol,
IonContent,
IonHeader,
IonIcon,
IonPage,
IonRow,
IonTitle,
IonToolbar,
useIonRouter,
} from '@ionic/react';
import { Geolocation } from '@capacitor/geolocation';
import { useEffect, useState } from 'react';
import { SkeletonDashboard } from '../components/SkeletonDashboard';
import { chevronBackOutline, refreshOutline } from 'ionicons/icons';
import { CurrentWeather } from '../components/CurrentWeather';
function Tab1() {
const router = useIonRouter();
const [currentWeather, setCurrentWeather] = useState(false);
useEffect(() => {
getCurrentPosition();
}, []);
const getCurrentPosition = async () => {
setCurrentWeather(false);
const coordinates = await Geolocation.getCurrentPosition();
getAddress(coordinates.coords);
};
const getAddress = async (coords) => {
const query = `${coords.latitude},${coords.longitude}`;
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?key=f93eb660b2424258bf5155016210712&q=${query}`
);
const data = await response.json();
console.log(data);
setCurrentWeather(data);
};
// const router = useIonRouter();
function handleBackClick() {
router.goBack();
}
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>My Weather</IonTitle>
<IonButtons slot="end">
<IonButton onClick={() => getCurrentPosition()}>
<IonIcon icon={refreshOutline} color="primary" />
</IonButton>
</IonButtons>
<IonButtons slot="start">
<IonButton onClick={() => handleBackClick()}>
<IonIcon icon={chevronBackOutline} color="primary" />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Dashboard</IonTitle>
</IonToolbar>
</IonHeader>
<IonRow className="ion-margin-start ion-margin-end ion-justify-content-center ion-text-center">
<IonCol size="12">
<h4>Here's your location based weather</h4>
</IonCol>
</IonRow>
<div style={{ marginTop: '-1.5rem' }}>
{currentWeather ? (
<CurrentWeather currentWeather={currentWeather} />
) : (
<SkeletonDashboard />
)}
</div>
</IonContent>
</IonPage>
);
}
export default Tab1;

View File

@@ -0,0 +1,81 @@
import {
IonButton,
IonCol,
IonContent,
IonHeader,
IonPage,
IonRow,
IonSearchbar,
IonTitle,
IonToolbar,
} from '@ionic/react';
import { useState } from 'react';
import { CurrentWeather } from '../components/CurrentWeather';
function Tab2() {
const [search, setSearch] = useState('');
const [currentWeather, setCurrentWeather] = useState(false);
const performSearch = async () => {
getAddress(search);
};
const getAddress = async (city) => {
const response = await fetch(
`https://api.weatherapi.com/v1/current.json?key=f93eb660b2424258bf5155016210712&q=${city}&aqi=no`
);
const data = await response.json();
if (data && data.current && data.location) {
setCurrentWeather(data);
}
};
return (
<IonPage>
<IonHeader>
<IonToolbar>
<IonTitle>Search</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonHeader collapse="condense">
<IonToolbar>
<IonTitle size="large">Search</IonTitle>
</IonToolbar>
</IonHeader>
<IonRow className="ion-justify-content-center ion-margin-top ion-align-items-center">
<IonCol size="7">
<IonSearchbar
placeholder="Try 'London'"
animated
value={search}
onIonChange={(e) => setSearch(e.target.value)}
/>
</IonCol>
<IonCol size="5">
<IonButton
expand="block"
className="ion-margin-start ion-margin-end"
onClick={performSearch}
>
Search
</IonButton>
</IonCol>
</IonRow>
<div style={{ marginTop: '-0.8rem' }}>
{currentWeather ? (
<CurrentWeather currentWeather={currentWeather} />
) : (
<h3 className="ion-text-center">Your search result will appear here</h3>
)}
</div>
</IonContent>
</IonPage>
);
}
export default Tab2;

View File

@@ -0,0 +1,75 @@
import { IonButton, IonCardSubtitle, IonCol, IonIcon, IonList, IonRow } from '@ionic/react';
import DebitCard from './DebitCard';
import styles from './CardSlide.module.css';
import TransactionItem from './TransactionItem';
import { addOutline, arrowRedoOutline } from 'ionicons/icons';
import { formatBalance } from '../data/Utils';
const CardSlide = (props) => {
const { index, card, profile } = props;
return (
<>
<IonRow className="ion-text-center">
<IonCol size="12">
<IonCardSubtitle color="medium">balance</IonCardSubtitle>
<IonCardSubtitle id={`slide_${index}_balance`} className={` ${styles.balance} animate__animated`}>
<span className={styles.poundSign}>£</span>
&nbsp;{formatBalance(card.balance)}
<IonButton
className={styles.addButton}
size="small"
style={{ '--background': card.color, '--background-focused': card.color, '--background-hover': card.color, '--background-activated': card.color }}
routerLink={`/add-transaction/${card.id}`}
>
<IonIcon icon={addOutline} />
</IonButton>
</IonCardSubtitle>
</IonCol>
</IonRow>
<IonRow id={`card_${index}_container`} className="animate__animated ion-text-center ion-justify-content-center">
<IonCol size="12">
<DebitCard key={index} {...card} profile={profile} />
</IonCol>
</IonRow>
<IonRow className={styles.heading}>
<IonCol size="12">
<h6>Transactions</h6>
</IonCol>
</IonRow>
{card.transactions.length > 0 && (
<IonRow id={`slide_${index}_transactions`} className="animate__animated">
<IonCol size="12">
<IonList className={styles.transactionList}>
{card.transactions.length > 0 &&
card.transactions
.slice(0)
.reverse()
.map((transaction, index) => <TransactionItem key={`card_transaction_${index}`} {...transaction} color={card.color} />)}
</IonList>
</IonCol>
</IonRow>
)}
{card.transactions.length === 0 && (
<IonRow id={`slide_${index}_transactions`} className="animate__animated">
<IonCol size="12">
<h5>No transactions found</h5>
<IonButton
style={{ '--background': card.color, '--background-focused': card.color, '--background-hover': card.color, '--background-activated': card.color }}
routerLink={`/add-transaction/${card.id}`}
>
<IonIcon icon={arrowRedoOutline} />
&nbsp;Transfer funds
</IonButton>
</IonCol>
</IonRow>
)}
</>
);
};
export default CardSlide;

View File

@@ -0,0 +1,51 @@
.customSlide {
display: flex;
flex-direction: column;
}
.transactionList {
overflow: scroll;
width: 100vw;
}
.balance {
font-weight: 300;
font-size: 1.5rem;
color: black;
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
align-items: center;
}
.poundSign {
font-weight: 800;
font-size: 1.2rem;
}
.heading h6 {
padding: 0;
margin: 0;
text-align: left !important;
float: left !important;
text-align: left !important;
color: rgb(124, 124, 124);
font-weight: 400;
}
.heading {
width: 83%;
padding: 0;
margin: 0;
margin-top: 0.75rem;
}
.addButton {
--border-radius: 500px !important;
width: fit-content !important;
margin-top: 0.45rem;
margin-left: 1rem;
opacity: 0.6;
}

View File

@@ -0,0 +1,62 @@
import { IonCardSubtitle, IonCol, IonIcon, IonNote, IonRow } from '@ionic/react';
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
import { useEffect, useState } from 'react';
export const WeatherProperty = ({ type, currentWeather }: { type: any; currentWeather: any }) => {
const [property, setProperty] = useState(false);
const properties = {
wind: {
isIcon: false,
icon: '/assets/WeatherDemo/wind.png',
alt: 'wind',
label: 'Wind',
value: `${currentWeather.current.wind_mph}mph`,
},
feelsLike: {
isIcon: true,
icon: thermometerOutline,
alt: 'feels like',
label: 'Feels like',
value: `${currentWeather.current.feelslike_c}°C`,
},
indexUV: {
isIcon: true,
icon: sunnyOutline,
alt: 'index uv',
label: 'Index UV',
value: currentWeather.current.uv,
},
pressure: {
isIcon: true,
icon: pulseOutline,
alt: 'pressure',
label: 'Pressure',
value: `${currentWeather.current.pressure_mb} mbar`,
},
};
useEffect(() => {
setProperty(properties[type]);
}, [type]);
return (
<IonCol size="6">
<IonRow className="ion-justify-content-center ion-align-items-center">
<IonCol size="3">
{!property.isIcon && (
<img alt={property.alt} src={property.icon} height="32" width="32" />
)}
{property.isIcon && (
<IonIcon icon={property.icon} color="medium" style={{ fontSize: '2rem' }} />
)}
</IonCol>
<IonCol size="9">
<IonCardSubtitle>{property.label}</IonCardSubtitle>
<IonNote>{property.value}</IonNote>
</IonCol>
</IonRow>
</IonCol>
);
};

View File

@@ -0,0 +1,48 @@
import { IonCard, IonCardContent, IonGrid, IonRow, IonText, IonCardTitle } from '@ionic/react';
import { WeatherProperty } from './WeatherProperty';
export const CurrentWeather = ({ currentWeather }: { currentWeather: any }) => (
<IonGrid>
<IonCard>
<IonCardContent className="ion-text-center">
<IonText color="primary">
<h1>
{currentWeather.location.region},{' '}
<span style={{ color: 'gray' }}>{currentWeather.location.country}</span>
</h1>
</IonText>
<div className="ion-margin-top">
<img
alt="condition"
src={currentWeather.current.condition.icon.replace('//', 'https://')}
/>
<IonText color="dark">
<h1 style={{ fontWeight: 'bold' }}>{currentWeather.current.condition.text}</h1>
</IonText>
<IonText color="medium">
<p>{new Date(currentWeather.location.localtime).toDateString()}</p>
</IonText>
</div>
<IonCardTitle style={{ fontSize: '3rem' }} className="ion-margin-top">
{currentWeather.current.temp_c}&#8451;
</IonCardTitle>
<IonGrid className="ion-margin-top">
<IonRow>
<WeatherProperty type="wind" currentWeather={currentWeather} />
<WeatherProperty type="feelsLike" currentWeather={currentWeather} />
</IonRow>
<IonRow className="ion-margin-top">
<WeatherProperty type="indexUV" currentWeather={currentWeather} />
<WeatherProperty type="pressure" currentWeather={currentWeather} />
</IonRow>
</IonGrid>
</IonCardContent>
</IonCard>
</IonGrid>
);

View File

@@ -0,0 +1,48 @@
import { useEffect, useState } from 'react';
import styles from './DebitCard.module.css';
const DebitCard = (props) => {
const { type, number, profile, expiry, secret, color } = props;
const [lastFourCardNumbers, setLastFourCardNumbers] = useState('****');
const cardClass = `card_${color}`;
const cardTypeLogo = type === 'visa' ? '/visa.png' : '/mastercard.png';
useEffect(() => {
var lastFourNumbers = number ? number.substr(number.length - 4) : '1234';
setLastFourCardNumbers(lastFourNumbers);
}, [number]);
return (
<div className={styles.card}>
<div className={`${styles.card__front} ${styles.card__part} ${styles[cardClass]}`}>
<img className={`${styles.card__front_chip} ${styles.card__square}`} src="/chip.png" alt="1" />
<img className={`${styles.card__front_square} ${styles.card__square}`} src="/ionicwhite.png" alt="1" />
<img className={`${styles.card__front_logo} ${styles.card__logo}`} src={cardTypeLogo} alt="2" />
<p className={styles.card_number}>**** **** **** {lastFourCardNumbers}</p>
<div className={styles.card__space_75}>
<span className={styles.card__label}>Card holder</span>
<p className={styles.card__info}>{`${profile.firstname} ${profile.surname}`}</p>
</div>
<div className={styles.card__space_25}>
<span className={styles.card__label}>Expires</span>
<p className={styles.card__info}>{expiry}</p>
</div>
</div>
<div className={`${styles.card__back} ${styles.card__part} ${styles[cardClass]}`}>
<div className={styles.card__black_line}></div>
<div className={styles.card__back_content}>
<div className={styles.card__secret}>
<p className={styles.card__secret__last}>{secret}</p>
</div>
<img className={`${styles.card__back_square} ${styles.card__square}`} src="/ionicwhite.png" alt="3" />
<img className={`${styles.card__back_logo} ${styles.card__logo}`} src={cardTypeLogo} alt="5" />
</div>
</div>
</div>
);
};
export default DebitCard;

View File

@@ -0,0 +1,188 @@
@import url('https://fonts.googleapis.com/css?family=Space+Mono:400,400i,700,700i');
.card {
box-sizing: border-box;
font-family: 'Space Mono', monospace;
margin: 0 auto;
}
.title {
margin-bottom: 30px;
color: #162969;
}
.card {
width: 320px;
height: 190px;
-webkit-perspective: 600px;
-moz-perspective: 600px;
perspective: 600px;
}
.card__part {
box-shadow: 1px 1px #aaa3a3;
top: 0;
position: absolute;
z-index: 1000;
left: 0;
display: inline-block;
width: 320px;
height: 190px;
background-repeat: no-repeat;
background-position: center;
background-size: cover;
border-radius: 8px;
-webkit-transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
-moz-transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
-ms-transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
-o-transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
-webkit-transform-style: preserve-3d;
-moz-transform-style: preserve-3d;
-webkit-backface-visibility: hidden;
-moz-backface-visibility: hidden;
}
.card_orange {
background: linear-gradient(to right bottom, #fd696b, #fa616e, #f65871, #c74261, #d62158);
}
.card_blue {
background: linear-gradient(to right bottom, #699dfd, #61b5fa, #58aff6, #4b86c9, #2151d6);
}
.card_black {
background: linear-gradient(to right bottom, #292929, #363636, #555555, #444444, #0f0f0f);
}
.card_purple {
background: linear-gradient(to right bottom, #7a43df, #644897, #8964cf, #633cac, #512c96);
}
.card__front {
padding: 18px;
-webkit-transform: rotateY(0);
-moz-transform: rotateY(0);
}
.card__back {
padding: 18px 0;
-webkit-transform: rotateY(-180deg);
-moz-transform: rotateY(-180deg);
}
.card__black_line {
margin-top: 5px;
height: 38px;
background-color: #303030;
}
.card__logo {
height: 16px !important;
}
.card__front_chip {
left: 1.2rem;
height: 1.5rem !important;
position: absolute;
}
.card__front_logo {
position: absolute;
top: 18px;
right: 18px;
}
.card__square {
border-radius: 5px;
height: 30px !important;
}
.card_number {
display: block;
width: 100%;
word-spacing: 4px;
font-size: 20px;
letter-spacing: 2px;
color: #fff;
text-align: center;
margin-bottom: 20px;
margin-top: 20px;
}
.card__space_75 {
width: 75%;
float: left;
}
.card__space_25 {
width: 25%;
float: left;
}
.card__label {
font-size: 10px;
text-transform: uppercase;
color: rgba(255, 255, 255, 0.8);
letter-spacing: 1px;
}
.card__info {
margin-bottom: 0;
margin-top: 5px;
font-size: 16px;
line-height: 18px;
color: #fff;
letter-spacing: 1px;
text-transform: uppercase;
}
.card__back_content {
padding: 15px 15px 0;
}
.card__secret__last {
color: #303030;
text-align: right;
margin: 0;
font-size: 14px;
}
.card__secret {
padding: 5px 12px;
background-color: #fff;
position: relative;
}
.card__secret:before {
content: '';
position: absolute;
top: -3px;
left: -3px;
height: calc(100% + 6px);
width: calc(100% - 42px);
border-radius: 4px;
background: repeating-linear-gradient(45deg, #ededed, #ededed 5px, #f9f9f9 5px, #f9f9f9 10px);
}
.card__back_logo {
position: absolute;
bottom: 15px;
right: 15px;
}
.card__back_square {
position: absolute;
bottom: 15px;
left: 15px;
}
.card:hover .card__front {
-webkit-transform: rotateY(180deg);
-moz-transform: rotateY(180deg);
}
.card:hover .card__back {
-webkit-transform: rotateY(0deg);
-moz-transform: rotateY(0deg);
}

View File

@@ -0,0 +1,117 @@
import {
IonCard,
IonCardContent,
IonCardSubtitle,
IonCardTitle,
IonCol,
IonGrid,
IonIcon,
IonNote,
IonRow,
IonSkeletonText,
IonText,
IonThumbnail,
} from '@ionic/react';
import { pulseOutline, sunnyOutline, thermometerOutline } from 'ionicons/icons';
export const SkeletonDashboard = () => (
<IonGrid>
<IonCard>
<IonCardContent className="ion-text-center">
<IonText color="primary">
<h1>
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
</h1>
</IonText>
<div className="ion-margin-top">
<IonThumbnail>
<IonSkeletonText animated style={{ width: '2rem', height: '2rem' }} />
</IonThumbnail>
<IonText color="dark">
<h1 style={{ fontWeight: 'bold' }}>
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
</h1>
</IonText>
<IonText color="medium">
<p>
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
</p>
</IonText>
</div>
<IonCardTitle style={{ fontSize: '3rem' }} className="ion-margin-top">
<IonSkeletonText animated style={{ height: '3rem', width: '30%', textAlign: 'center' }} />
</IonCardTitle>
<IonGrid className="ion-margin-top">
<IonRow>
<IonCol size="6">
<IonRow className="ion-justify-content-center ion-align-items-center">
<IonCol size="3">
<img alt="wind" src="/assets/WeatherDemo/wind.png" height="32" width="32" />
</IonCol>
<IonCol size="9">
<IonCardSubtitle>Wind</IonCardSubtitle>
<IonNote>
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
</IonNote>
</IonCol>
</IonRow>
</IonCol>
<IonCol size="6">
<IonRow className="ion-justify-content-center ion-align-items-center">
<IonCol size="3">
<IonIcon icon={thermometerOutline} color="medium" style={{ fontSize: '2rem' }} />
</IonCol>
<IonCol size="9">
<IonCardSubtitle>Feels like</IonCardSubtitle>
<IonNote>
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
</IonNote>
</IonCol>
</IonRow>
</IonCol>
</IonRow>
<IonRow className="ion-margin-top">
<IonCol size="6">
<IonRow className="ion-justify-content-center ion-align-items-center">
<IonCol size="3">
<IonIcon icon={sunnyOutline} color="medium" style={{ fontSize: '2rem' }} />
</IonCol>
<IonCol size="9">
<IonCardSubtitle>Index UV</IonCardSubtitle>
<IonNote>
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
</IonNote>
</IonCol>
</IonRow>
</IonCol>
<IonCol size="6">
<IonRow className="ion-justify-content-center ion-align-items-center">
<IonCol size="3">
<IonIcon icon={pulseOutline} color="medium" style={{ fontSize: '2rem' }} />
</IonCol>
<IonCol size="9">
<IonCardSubtitle>Pressure</IonCardSubtitle>
<IonNote>
<IonSkeletonText animated style={{ height: '2rem', width: '90%' }} />
</IonNote>
</IonCol>
</IonRow>
</IonCol>
</IonRow>
</IonGrid>
</IonCardContent>
</IonCard>
</IonGrid>
);

View File

@@ -0,0 +1,56 @@
import { IonAvatar, IonItem, IonLabel } from '@ionic/react';
import { formatBalance } from '../data/Utils';
import styles from './TransactionItem.module.css';
const TransactionItem = (props) => {
const { name, amount, deposit, color } = props;
const getContactNameInitials = (contactName) => {
var nameInitials = '';
if (contactName && contactName !== '' && contactName !== undefined) {
const nameParts = contactName && contactName.split(' ');
if (nameParts) {
if (nameParts[0].charAt(0).match(/^[a-z]+$/i)) {
nameInitials += nameParts[0].charAt(0).toUpperCase();
}
if (nameParts[1]) {
if (nameParts[1].charAt(0).match(/^[a-z]+$/i)) {
nameInitials += nameParts[1].charAt(0).toUpperCase();
}
} else {
nameInitials += nameParts[0].charAt(1).toUpperCase();
}
}
}
return nameInitials;
};
return (
<IonItem lines="full" detail={false} className={`item-text-wrap ion-text-wrap ${styles.transactionItem}`}>
<div className={styles.transactionItemContent}>
<IonAvatar slot="start">
<div style={{ borderColor: 'grey', color: 'grey' }} className={styles.avatarImage}>
{getContactNameInitials(name)}
</div>
</IonAvatar>
<IonLabel className={`ion-text-wrap ${styles.transactionContent}`}>
<h2>{name}</h2>
</IonLabel>
<IonLabel className={`ion-text-wrap ${styles.transactionContent}`}>
<h4 className={deposit ? styles.green : styles.red}>
{deposit ? '+' : '-'}
&pound;{formatBalance(amount)}
</h4>
</IonLabel>
</div>
</IonItem>
);
};
export default TransactionItem;

View File

@@ -0,0 +1,48 @@
.avatarImage {
/* background-color: var(--ion-color); */
width: 2.5rem;
height: 2.5rem;
border-radius: 500px;
color: black;
font-size: 1.3rem;
display: flex;
flex-direction: row;
justify-content: center;
align-content: center;
align-items: center;
padding: 0.5rem !important;
border: 2px solid rgb(44, 44, 44);
margin-top: 0.2rem;
}
.transactionItem {
flex-direction: row;
padding: 0;
margin: 0;
}
.transactionItemContent {
padding-left: 3rem;
padding-right: 2rem;
display: flex !important;
flex-direction: row !important;
justify-content: space-between;
width: 100%;
align-content: center;
align-items: center;
margin-top: -0.2rem;
margin-bottom: -0.2rem;
}
.transactionContent {
padding: 1rem;
text-align: left !important;
}
.green {
color: rgb(0, 165, 0);
}
.red {
color: red;
}

View File

@@ -0,0 +1,119 @@
import { Store } from 'pullstate';
export const AccountStore = new Store({
profile: {
firstname: 'Alan',
surname: 'Montgomery',
avatar: '/assets/DemoBankingUi/alan.jpg',
},
cards: [
{
id: 1,
type: 'visa',
description: 'Current Account',
number: '4859 2390 5635 7347',
expiry: '11/22',
secret: '483',
color: 'orange',
balance: '38.21',
transactions: [
{
name: 'Joe Bloggs',
amount: '2.50',
deposit: true,
},
{
name: 'Ocean Pratt',
amount: '12.99',
deposit: true,
},
{
name: 'Eugene Piper',
amount: '74.99',
deposit: false,
},
{
name: 'Emeli Potts',
amount: '4.20',
deposit: false,
},
{
name: 'Asia Wells',
amount: '12.73',
deposit: true,
},
{
name: 'Awais Brook',
amount: '17.10',
deposit: false,
},
{
name: 'Coen Haas',
amount: '9.99',
deposit: true,
},
],
},
{
id: 2,
type: 'visa',
description: 'Savings',
number: '7349 1284 6790 4587',
expiry: '05/23',
secret: '590',
color: 'blue',
balance: '120.90',
transactions: [
{
name: 'Joe Bloggs',
amount: '120.90',
deposit: true,
},
],
},
{
id: 3,
type: 'visa',
description: 'House Fund',
number: '6783 5692 4475 6682',
expiry: '01/24',
secret: '321',
color: 'purple',
balance: '0',
transactions: [],
},
],
});
export const addCardToAccount = (newCard) => {
AccountStore.update((s) => {
s.cards = [...s.cards, newCard];
});
};
export const addTransactionToCard = (newTransaction, cardID) => {
AccountStore.update((s) => {
s.cards.find((c, index) =>
parseInt(c.id) === parseInt(cardID) ? (s.cards[index].transactions = [...s.cards[index].transactions, newTransaction]) : false
);
});
if (newTransaction.deposit) {
AccountStore.update((s) => {
s.cards.find((c, index) =>
parseInt(c.id) === parseInt(cardID) ? (s.cards[index].balance = parseFloat(s.cards[index].balance) + parseFloat(newTransaction.amount)) : false
);
});
} else {
AccountStore.update((s) => {
s.cards.find((c, index) =>
parseInt(c.id) === parseInt(cardID) ? (s.cards[index].balance = parseFloat(s.cards[index].balance) - parseFloat(newTransaction.amount)) : false
);
});
}
};
// export const removeFromCart = productIndex => {
// AccountStore.update(s => { s.product_ids.splice(productIndex, 1) });
// }

View File

@@ -0,0 +1,6 @@
import { Store } from 'pullstate';
export const CardStore = new Store({
card_colors: ['orange', 'black', 'blue', 'purple'],
card_types: ['visa', 'mastercard'],
});

View File

@@ -0,0 +1,9 @@
export const formatBalance = (balance) => {
var formatter = new Intl.NumberFormat('en-GB', {
// style: 'currency',
currency: 'GBP',
minimumFractionDigits: 2,
});
return formatter.format(balance);
};

View File

@@ -0,0 +1,42 @@
import { IonIcon, IonLabel, IonRouterOutlet, IonTabBar, IonTabButton, IonTabs } from '@ionic/react';
import { cloudOutline, searchOutline } from 'ionicons/icons';
import { Route, Redirect } from 'react-router';
import Tab1 from './AppPages/Tab1';
import Tab2 from './AppPages/Tab2';
import Home from './pages/Home.jsx';
import Account from './pages/Account';
import AddCard from './pages/AddCard';
import AddTransaction from './pages/AddTransaction';
import './style.scss';
function DemoBankingUi() {
return (
<IonTabs className="demo-banking-ui">
<IonRouterOutlet>
<Route path="/demo-banking-ui/home" exact={true}>
<Home />
</Route>
<Route path="/demo-banking-ui/account" exact={true}>
<Account />
</Route>
<Route path="/demo-banking-ui/account/add-card" exact={true}>
<AddCard />
</Route>
<Route path="/demo-banking-ui/add-transaction/:card_id" exact={true}>
<AddTransaction />
</Route>
<Redirect exact path="/demo-banking-ui" to="/demo-banking-ui/home" />
</IonRouterOutlet>
</IonTabs>
);
}
export default DemoBankingUi;

View File

@@ -0,0 +1,95 @@
import {
IonBackButton,
IonButton,
IonButtons,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonIcon,
IonItem,
IonLabel,
IonPage,
IonRow,
IonTitle,
IonToolbar,
} from '@ionic/react';
import styles from './Account.module.css';
import { AccountStore } from '../data/AccountStore';
import { addOutline, logOutOutline } from 'ionicons/icons';
import { formatBalance } from '../data/Utils';
const Account = () => {
const cards = AccountStore.useState((s) => s.cards);
const profile = AccountStore.useState((s) => s.profile);
return (
<IonPage className={styles.accountPage}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton color="dark" />
</IonButtons>
<IonTitle>Account</IonTitle>
<IonButtons slot="end">
<IonButton>
<IonIcon color="dark" icon={logOutOutline} />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonGrid>
<IonRow className="ion-text-center ion-justify-content-center">
<IonCol size="4" className="animate__animated animate__fadeInTopLeft animate__faster">
<img src={profile.avatar} className={styles.avatar} alt="account avatar" />
</IonCol>
</IonRow>
<IonRow className={`ion-no-margin ion-text-center ion-justify-content-center ${styles.profileDetails}`}>
<IonCol size="12">
<h5>{`${profile.firstname} ${profile.surname}`}</h5>
<h6>{cards.length} current cards</h6>
</IonCol>
</IonRow>
<IonRow className="ion-text-center">
<IonCol size="12">
<IonButton color="primary" routerLink="/account/add-card" routerDirection="forward">
<IonIcon icon={addOutline} />
Add Card
</IonButton>
</IonCol>
</IonRow>
<div className="ion-margin-top">
{cards.map((card, index) => {
return (
<IonRow key={`smallCard_${index}`} className="animate__animated animate__fadeInLeft animate__faster">
<IonCol size="12">
<IonItem className={styles.cardItem} detail={false} lines="none">
<div className={styles.smallCard} style={{ backgroundColor: card.color }}></div>
<IonLabel className={`ion-text-left ${styles.cardDescription}`}>
<h4>{card.description}</h4>
</IonLabel>
<IonLabel className="ion-text-right">
<h4>&pound;{formatBalance(card.balance)}</h4>
</IonLabel>
</IonItem>
</IonCol>
</IonRow>
);
})}
</div>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default Account;

View File

@@ -0,0 +1,54 @@
.accountPage ion-toolbar {
--border-style: none;
--padding-top: 1rem;
--padding-bottom: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
.avatar {
border-radius: 500px;
border: 3px solid var(--ion-color-primary);
padding: 0.2rem;
}
.profileDetails {
}
.profileDetails h6,
.profileDetails h5 {
margin: 0;
padding: 0;
}
.profileDetails h6 {
color: var(--ion-color-medium);
}
.cards {
/* margin-top: */
}
.smallCard {
width: 15%;
height: 80%;
border-radius: 5px;
opacity: 0.7;
}
.cardDescription {
padding-left: 1.5rem;
}
.cardDescription h4 {
font-weight: 400;
}
.cardItem {
--background: rgb(246, 246, 246);
--border-radius: 5px;
--padding-start: 1rem;
--padding-end: 1rem;
--padding-top: 0.5rem;
--padding-bottom: 0.5rem;
}

View File

@@ -0,0 +1,209 @@
import { useState } from 'react';
import {
IonBackButton,
IonButton,
IonButtons,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonIcon,
IonInput,
IonItem,
IonLabel,
IonPage,
IonRow,
IonSelect,
IonSelectOption,
IonTitle,
IonToolbar,
} from '@ionic/react';
import styles from './Account.module.css';
import DebitCard from '../components/DebitCard';
import { AccountStore, addCardToAccount } from '../data/AccountStore';
import { CardStore } from '../data/CardStore';
import { addOutline, timerOutline } from 'ionicons/icons';
import { useHistory } from 'react-router';
const AddCard = () => {
const cards = AccountStore.useState((s) => s.cards);
const cardTypes = CardStore.useState((s) => s.card_types);
const cardColors = CardStore.useState((s) => s.card_colors);
const profile = AccountStore.useState((s) => s.profile);
const [cardType, setCardType] = useState(cardTypes[0]);
const [cardColor, setCardColor] = useState(cardColors[0]);
const [cardDescription, setCardDescription] = useState('');
const [cardNumber, setCardNumber] = useState('1234 1234 1234 1234');
const [cardSecret, setCardSecret] = useState('123');
const [cardExpiry, setCardExpiry] = useState('01/22');
const [cardBalance, setCardBalance] = useState(0);
const history = useHistory();
const [adding, setAdding] = useState(false);
const addCard = async () => {
setAdding(true);
const newCard = {
id: cards.length + 1,
type: cardType,
color: cardColor,
description: cardDescription,
number: cardNumber,
secret: cardSecret,
expiry: cardExpiry,
balance: cardBalance,
transactions: [
{
name: 'Starting Balance',
amount: cardBalance,
deposit: true,
},
],
};
await addCardToAccount(newCard);
setTimeout(() => {
setAdding(false);
history.goBack();
}, 500);
};
return (
<IonPage className={styles.accountPage}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton color="dark" />
</IonButtons>
<IonTitle>Add Card</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonGrid>
<IonRow className="animate__animated animate__fadeInTopLeft animate__faster ion-justify-content-center ion-text-center">
<IonCol size="12" className="ion-justify-content-center ion-text-center">
<DebitCard color={cardColor} type={cardType} expiry={cardExpiry} number={cardNumber} secret={cardSecret} profile={profile} />
</IonCol>
</IonRow>
<IonRow className="ion-padding-top">
<IonCol size="6">
<IonItem lines="full">
<IonLabel position="floating">Card Type</IonLabel>
<IonSelect placeholder="Select type" value={cardType} onIonChange={(e) => setCardType(e.currentTarget.value)}>
{cardTypes.map((option, index) => {
return (
<IonSelectOption key={index} value={option}>
{option.toUpperCase()}
</IonSelectOption>
);
})}
</IonSelect>
</IonItem>
</IonCol>
<IonCol size="6">
<IonItem lines="full">
<IonLabel position="floating">Card Color</IonLabel>
<IonSelect placeholder="Select color" value={cardColor} onIonChange={(e) => setCardColor(e.currentTarget.value)}>
{cardColors.map((option, index) => {
return (
<IonSelectOption key={index} value={option}>
{option.toUpperCase()}
</IonSelectOption>
);
})}
</IonSelect>
</IonItem>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="6">
<IonItem lines="full">
<IonLabel position="floating">Card Name</IonLabel>
<IonInput
type="text"
inputmode="text"
placeholder="Card name"
value={cardDescription}
onIonChange={(e) => setCardDescription(e.currentTarget.value)}
/>
</IonItem>
</IonCol>
<IonCol size="6">
<IonItem lines="full">
<IonLabel position="floating">Starting Balance</IonLabel>
<IonInput type="text" inputmode="text" placeholder="0" value={cardBalance} onIonChange={(e) => setCardBalance(e.currentTarget.value)} />
</IonItem>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="12">
<IonItem lines="full">
<IonLabel position="floating">Card Number</IonLabel>
<IonInput
type="text"
inputmode="text"
placeholder="**** **** **** ****"
value={cardNumber}
onIonChange={(e) => setCardNumber(e.currentTarget.value)}
/>
</IonItem>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="6">
<IonItem lines="full">
<IonLabel position="floating">Card Expiry</IonLabel>
<IonInput type="text" inputmode="text" placeholder="01/22" value={cardExpiry} onIonChange={(e) => setCardExpiry(e.currentTarget.value)} />
</IonItem>
</IonCol>
<IonCol size="6">
<IonItem lines="full">
<IonLabel position="floating">Card Secret</IonLabel>
<IonInput type="text" inputmode="text" placeholder="123" value={cardSecret} onIonChange={(e) => setCardSecret(e.currentTarget.value)} />
</IonItem>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="12">
<IonButton
style={{ '--background': cardColor, '--background-focused': cardColor, '--background-hover': cardColor, '--background-activated': cardColor }}
expand="block"
disabled={adding}
onClick={addCard}
>
{!adding && (
<>
<IonIcon icon={addOutline} />
&nbsp; Add Card
</>
)}
{adding && (
<>
<IonIcon icon={timerOutline} />
&nbsp; Adding...
</>
)}
</IonButton>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default AddCard;

View File

@@ -0,0 +1,165 @@
import { useState } from 'react';
import {
IonBackButton,
IonButton,
IonButtons,
IonCol,
IonContent,
IonGrid,
IonHeader,
IonIcon,
IonInput,
IonItem,
IonLabel,
IonPage,
IonRow,
IonSelect,
IonSelectOption,
IonTitle,
IonToggle,
IonToolbar,
useIonViewWillEnter,
} from '@ionic/react';
import styles from './Account.module.css';
import DebitCard from '../components/DebitCard';
import { AccountStore, addCardToAccount, addTransactionToCard } from '../data/AccountStore';
import { CardStore } from '../data/CardStore';
import { addOutline, timerOutline } from 'ionicons/icons';
import { useHistory, useParams } from 'react-router';
const AddTransaction = () => {
const cards = AccountStore.useState((s) => s.cards);
const profile = AccountStore.useState((s) => s.profile);
const [cardID, setCardID] = useState(false);
const [card, setCard] = useState({});
const [transactionName, setTransactionName] = useState('Test Transaction');
const [transactionAmount, setTransactionAmount] = useState(0);
const [transactionDeposit, setTransactionDeposit] = useState(false);
const history = useHistory();
const params = useParams();
const [adding, setAdding] = useState(false);
useIonViewWillEnter(() => {
const tempCardID = params.card_id;
const tempCard = cards.filter((c) => parseInt(c.id) === parseInt(tempCardID))[0];
setCardID(tempCardID);
setCard(tempCard);
});
const addTransaction = async () => {
setAdding(true);
const newTransaction = {
name: transactionName,
amount: transactionAmount,
deposit: transactionDeposit,
};
await addTransactionToCard(newTransaction, cardID);
setTimeout(() => {
setAdding(false);
history.goBack();
}, 500);
};
return (
<IonPage className={styles.accountPage}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
<IonBackButton color="dark" />
</IonButtons>
<IonTitle>Add Transaction</IonTitle>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonGrid>
<IonRow className="animate__animated animate__fadeInTopLeft animate__faster ion-justify-content-center ion-text-center">
<IonCol size="12" className="ion-justify-content-center ion-text-center">
<DebitCard {...card} profile={profile} />
</IonCol>
</IonRow>
<IonRow>
<IonCol size="6">
<IonItem lines="full">
<IonLabel position="floating">Name</IonLabel>
<IonInput
type="text"
inputmode="text"
placeholder="Transaction name"
value={transactionName}
onIonChange={(e) => setTransactionName(e.currentTarget.value)}
/>
</IonItem>
</IonCol>
<IonCol size="6">
<IonItem lines="full">
<IonLabel position="floating">Amount</IonLabel>
<IonInput
type="text"
inputmode="text"
placeholder="0"
value={transactionAmount}
onIonChange={(e) => setTransactionAmount(e.currentTarget.value)}
/>
</IonItem>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="12">
<IonItem lines="full">
<IonLabel>Deposit?</IonLabel>
<IonToggle
style={{ '--background-checked': card.color }}
slot="end"
value={transactionDeposit}
onIonChange={(e) => setTransactionDeposit(e.currentTarget.checked)}
/>
</IonItem>
</IonCol>
</IonRow>
<IonRow>
<IonCol size="12">
<IonButton
style={{
'--background': card.color,
'--background-focused': card.color,
'--background-hover': card.color,
'--background-activated': card.color,
}}
expand="block"
disabled={adding}
onClick={addTransaction}
>
{!adding && (
<>
<IonIcon icon={addOutline} />
&nbsp; Add Transaction
</>
)}
{adding && (
<>
<IonIcon icon={timerOutline} />
&nbsp; Adding...
</>
)}
</IonButton>
</IonCol>
</IonRow>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default AddTransaction;

View File

@@ -0,0 +1,116 @@
import { useRef, useState } from 'react';
import { IonButton, IonButtons, IonContent, IonGrid, IonHeader, IonIcon, IonPage, IonTitle, IonToolbar, useIonRouter, useIonViewDidEnter } from '@ionic/react';
import styles from './Home.module.css';
import { AccountStore } from '../data/AccountStore';
import CardSlide from '../components/CardSlide';
import { chevronBackOutline, searchOutline } from 'ionicons/icons';
// Import Swiper React components
import { Swiper, SwiperSlide } from 'swiper/react';
// Import Swiper styles
// import 'swiper/swiper.scss';
import 'swiper/css';
import stylesS from './Home.module.scss';
const Home = () => {
const cards = AccountStore.useState((s) => s.cards);
const profile = AccountStore.useState((s) => s.profile);
const [pageTitle, setPageTitle] = useState(cards[0].description);
const [mainColor, setMainColor] = useState(cards[0].color);
const [slideSpace, setSlideSpace] = useState(10);
const slidesRef = useRef();
useIonViewDidEnter(() => {
setSlideSpace(0);
});
const changeSlide = async (e) => {
const swiper = e;
const swiperIndex = swiper.activeIndex;
setPageTitle(cards[swiperIndex].description);
setMainColor(cards[swiperIndex].color);
document.getElementById(`slide_${swiperIndex}_balance`).classList.add('animate__headShake');
setTimeout(() => {
document.getElementById(`slide_${swiperIndex}_balance`).classList.remove('animate__headShake');
}, 1000);
};
const manageTouch = async (touched, e) => {
const swiper = e;
const swiperIndex = swiper.activeIndex;
if (touched) {
document.getElementById(`slide_${swiperIndex}_transactions`).classList.add('animate__fadeOut');
} else {
document.getElementById(`slide_${swiperIndex}_transactions`).classList.remove('animate__fadeOut');
document.getElementById(`slide_${swiperIndex}_transactions`).classList.add('animate__fadeIn');
}
};
const router = useIonRouter();
function handleBackClick() {
router.goBack();
}
return (
<IonPage className={stylesS.homePage}>
<IonHeader>
<IonToolbar>
<IonButtons slot="start">
{/* */}
<IonButton
//
routerLink="/demo-banking-ui/account"
className={stylesS.toolbarAvatar}
>
<img alt="toolbar avatar" className={stylesS.toolbarAvatarImage} src={profile.avatar} />
</IonButton>
</IonButtons>
<IonTitle>{pageTitle}</IonTitle>
<IonButtons slot="end">
<IonButton onClick={() => handleBackClick()}>
<IonIcon icon={chevronBackOutline} color="primary" />
</IonButton>
{/* */}
<IonButton>
<IonIcon color="light" icon={searchOutline} style={{ backgroundColor: mainColor, borderRadius: '500px', padding: '0.2rem' }} />
</IonButton>
</IonButtons>
</IonToolbar>
</IonHeader>
<IonContent fullscreen>
<IonGrid>
<Swiper
spaceBetween={slideSpace}
ref={slidesRef}
slidesPerView={1}
className={stylesS.cardsContainer}
onTouchStart={(e) => manageTouch(true, e)}
onTouchEnd={(e) => manageTouch(false, e)}
onSlideChange={(e) => changeSlide(e)}
>
{cards.map((card, index) => {
return (
<SwiperSlide key={`slide_${index}`} id={`slide_${index}`} className={stylesS.customSlide}>
<CardSlide key={index} card={card} profile={profile} index={index} />
</SwiperSlide>
);
})}
</Swiper>
</IonGrid>
</IonContent>
</IonPage>
);
};
export default Home;

View File

@@ -0,0 +1,38 @@
.homePage ion-toolbar {
--border-style: none;
--padding-top: 1rem;
--padding-bottom: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
.customSlide {
display: flex;
flex-direction: column;
}
.transactionList {
overflow: scroll;
width: 100vw;
}
.balance {
font-weight: 300;
font-size: 1.5rem;
color: black;
}
.poundSign {
font-weight: 800;
font-size: 1.2rem;
}
.toolbarAvatarImage {
border-radius: 500px;
height: 100%;
width: auto;
}
.helloworld {
background-color: gold;
}

View File

@@ -0,0 +1,38 @@
.homePage ion-toolbar {
--border-style: none;
--padding-top: 1rem;
--padding-bottom: 1rem;
padding-left: 1rem;
padding-right: 1rem;
}
.customSlide {
display: flex;
flex-direction: column;
}
.transactionList {
overflow: scroll;
width: 100vw;
}
.balance {
font-weight: 300;
font-size: 1.5rem;
color: black;
}
.poundSign {
font-weight: 800;
font-size: 1.2rem;
}
.toolbarAvatarImage {
border-radius: 500px;
height: 32px;
width: auto;
}
.helloworld {
background-color: cyan;
}

View File

@@ -0,0 +1,139 @@
.demo-banking-ui {
/* Ionic Variables and Theming. For more info, please see:
http://ionicframework.com/docs/theming/ */
/** Ionic CSS Variables **/
* {
/** primary **/
--ion-color-primary: #3880ff;
--ion-color-primary-rgb: 56, 128, 255;
--ion-color-primary-contrast: #ffffff;
--ion-color-primary-contrast-rgb: 255, 255, 255;
--ion-color-primary-shade: #3171e0;
--ion-color-primary-tint: #4c8dff;
/** secondary **/
--ion-color-secondary: #3dc2ff;
--ion-color-secondary-rgb: 61, 194, 255;
--ion-color-secondary-contrast: #ffffff;
--ion-color-secondary-contrast-rgb: 255, 255, 255;
--ion-color-secondary-shade: #36abe0;
--ion-color-secondary-tint: #50c8ff;
/** tertiary **/
--ion-color-tertiary: #5260ff;
--ion-color-tertiary-rgb: 82, 96, 255;
--ion-color-tertiary-contrast: #ffffff;
--ion-color-tertiary-contrast-rgb: 255, 255, 255;
--ion-color-tertiary-shade: #4854e0;
--ion-color-tertiary-tint: #6370ff;
/** success **/
--ion-color-success: #2dd36f;
--ion-color-success-rgb: 45, 211, 111;
--ion-color-success-contrast: #ffffff;
--ion-color-success-contrast-rgb: 255, 255, 255;
--ion-color-success-shade: #28ba62;
--ion-color-success-tint: #42d77d;
/** warning **/
--ion-color-warning: #ffc409;
--ion-color-warning-rgb: 255, 196, 9;
--ion-color-warning-contrast: #000000;
--ion-color-warning-contrast-rgb: 0, 0, 0;
--ion-color-warning-shade: #e0ac08;
--ion-color-warning-tint: #ffca22;
/** danger **/
--ion-color-danger: #eb445a;
--ion-color-danger-rgb: 235, 68, 90;
--ion-color-danger-contrast: #ffffff;
--ion-color-danger-contrast-rgb: 255, 255, 255;
--ion-color-danger-shade: #cf3c4f;
--ion-color-danger-tint: #ed576b;
/** dark **/
--ion-color-dark: #222428;
--ion-color-dark-rgb: 34, 36, 40;
--ion-color-dark-contrast: #ffffff;
--ion-color-dark-contrast-rgb: 255, 255, 255;
--ion-color-dark-shade: #1e2023;
--ion-color-dark-tint: #383a3e;
/** medium **/
--ion-color-medium: #92949c;
--ion-color-medium-rgb: 146, 148, 156;
--ion-color-medium-contrast: #ffffff;
--ion-color-medium-contrast-rgb: 255, 255, 255;
--ion-color-medium-shade: #808289;
--ion-color-medium-tint: #9d9fa6;
/** light **/
--ion-color-light: #f4f5f8;
--ion-color-light-rgb: 244, 245, 248;
--ion-color-light-contrast: #000000;
--ion-color-light-contrast-rgb: 0, 0, 0;
--ion-color-light-shade: #d7d8da;
--ion-color-light-tint: #f5f6f9;
}
ion-item {
--padding-start: 0;
--inner-padding-end: 0;
}
ion-label {
margin-top: 12px;
margin-bottom: 12px;
}
ion-item h2 {
font-weight: 600;
margin: 0;
}
ion-item p {
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
width: 95%;
}
ion-item .date {
float: right;
align-items: center;
display: flex;
}
ion-item ion-icon {
color: #c9c9ca;
}
ion-item ion-note {
font-size: 15px;
margin-right: 8px;
font-weight: normal;
}
ion-item ion-note.md {
margin-right: 14px;
}
.dot {
display: block;
height: 12px;
width: 12px;
border-radius: 50%;
align-self: start;
margin: 16px 10px 16px 16px;
}
.dot-unread {
background: var(--ion-color-primary);
}
ion-footer ion-title {
font-size: 11px;
font-weight: normal;
}
}