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,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;
}