mirror of
https://github.com/siwa-net/my-finance-pal.git
synced 2024-11-10 00:51:56 +01:00
Adding schema validation, detail page init setup.
This commit is contained in:
parent
3b5e82d31c
commit
58f8194e5f
24 changed files with 295 additions and 64 deletions
11
package-lock.json
generated
11
package-lock.json
generated
|
@ -26,7 +26,8 @@
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-sweet-progress": "^1.1.2",
|
"react-sweet-progress": "^1.1.2",
|
||||||
"sass": "^1.59.3",
|
"sass": "^1.59.3",
|
||||||
"typescript": "5.0.2"
|
"typescript": "5.0.2",
|
||||||
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
@ -3856,6 +3857,14 @@
|
||||||
"funding": {
|
"funding": {
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"url": "https://github.com/sponsors/sindresorhus"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"node_modules/zod": {
|
||||||
|
"version": "3.21.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/zod/-/zod-3.21.4.tgz",
|
||||||
|
"integrity": "sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==",
|
||||||
|
"funding": {
|
||||||
|
"url": "https://github.com/sponsors/colinhacks"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,7 +29,8 @@
|
||||||
"react-dom": "18.2.0",
|
"react-dom": "18.2.0",
|
||||||
"react-sweet-progress": "^1.1.2",
|
"react-sweet-progress": "^1.1.2",
|
||||||
"sass": "^1.59.3",
|
"sass": "^1.59.3",
|
||||||
"typescript": "5.0.2"
|
"typescript": "5.0.2",
|
||||||
|
"zod": "^3.21.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"eslint-config-prettier": "^8.8.0",
|
"eslint-config-prettier": "^8.8.0",
|
||||||
|
|
|
@ -1,3 +1,43 @@
|
||||||
.Container {
|
.Headline1 {
|
||||||
|
position: relative;
|
||||||
}
|
text-align: center;
|
||||||
|
margin-bottom: 32px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ArrowBack {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
position: absolute;
|
||||||
|
left: 0px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.Details {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.DetailsContainer {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--grid-gap);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DetailsContainerLeft {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--grid-gap);
|
||||||
|
flex-basis: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.DetailsContainerRight {
|
||||||
|
flex: 0;
|
||||||
|
flex-basis: 300px;
|
||||||
|
align-self: stretch;
|
||||||
|
|
||||||
|
& > *:first-child:last-child {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,9 +1,15 @@
|
||||||
|
import { faArrowLeft } from '@fortawesome/free-solid-svg-icons/faArrowLeft';
|
||||||
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import is from '@sindresorhus/is';
|
import is from '@sindresorhus/is';
|
||||||
|
import Link from 'next/link';
|
||||||
import { FunctionComponent } from 'react';
|
import { FunctionComponent } from 'react';
|
||||||
|
|
||||||
import styles from './BudgetDetail.module.scss';
|
import styles from './BudgetDetail.module.scss';
|
||||||
|
import { datesToDayRange, dateToFormattedDay } from '../../helpers/date';
|
||||||
import { useBudgetSummaryQuery } from '../../hooks/useBudgetSummaryQuery';
|
import { useBudgetSummaryQuery } from '../../hooks/useBudgetSummaryQuery';
|
||||||
|
import { Button } from '../_design/Button/Button';
|
||||||
import { Card } from '../_design/Card/Card';
|
import { Card } from '../_design/Card/Card';
|
||||||
|
import { DetailWithTitle } from '../_design/DetailWithTitle/DetailWithTitle';
|
||||||
|
|
||||||
type BudgetDetailProps = {
|
type BudgetDetailProps = {
|
||||||
id: string;
|
id: string;
|
||||||
|
@ -13,19 +19,48 @@ export const BudgetDetail: FunctionComponent<BudgetDetailProps> = ({ id }) => {
|
||||||
const budgetQuery = useBudgetSummaryQuery(id);
|
const budgetQuery = useBudgetSummaryQuery(id);
|
||||||
const budget = budgetQuery.data;
|
const budget = budgetQuery.data;
|
||||||
|
|
||||||
if (is.undefined(budget) || budgetQuery.isFetching) {
|
if (is.nullOrUndefined(budget) || budgetQuery.isFetching) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { startDate, endDate, name, limit, transactions } = budget;
|
||||||
|
|
||||||
|
const isValidDateRange = is.date(startDate) && is.date(endDate) && startDate < endDate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<section className={styles.DetailsContainer}>
|
||||||
<h1>{budget.name} </h1>
|
<div className={styles.DetailsContainerLeft}>
|
||||||
<div className={styles.ProgressBar}>
|
<Card>
|
||||||
<div className={styles.ProgressPercentage}></div>
|
<h1 className={styles.Headline1}>
|
||||||
|
<Link href={'/'}>
|
||||||
|
<FontAwesomeIcon icon={faArrowLeft} className={styles.ArrowBack} />
|
||||||
|
</Link>
|
||||||
|
{name}
|
||||||
|
</h1>
|
||||||
|
<div className={styles.Details}>
|
||||||
|
<DetailWithTitle title={'Limit'}>{limit}</DetailWithTitle>
|
||||||
|
{isValidDateRange && (
|
||||||
|
<DetailWithTitle title={'Period'}>{datesToDayRange(startDate, endDate)}</DetailWithTitle>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
<Card>
|
||||||
|
<h3>Transactions</h3>
|
||||||
|
<ul>
|
||||||
|
{transactions.map((transaction) => (
|
||||||
|
<li key={transaction.id}>
|
||||||
|
{dateToFormattedDay(transaction.date)} - {transaction.description}
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
<span>Amount</span>
|
<div className={styles.DetailsContainerRight}>
|
||||||
<span>Date- start -end</span>
|
<Card>
|
||||||
<button onClick={() => null}>View transactions</button>
|
<h3>New Transaction</h3>
|
||||||
</Card>
|
<Button onClick={() => {}}>Add Transaction</Button>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -1,25 +1,25 @@
|
||||||
import { faCoins } from '@fortawesome/free-solid-svg-icons/faCoins';
|
import { faCoins } from '@fortawesome/free-solid-svg-icons/faCoins';
|
||||||
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
|
||||||
import format from 'date-fns/format';
|
import is from '@sindresorhus/is';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { FC } from 'react';
|
import { FC } from 'react';
|
||||||
import { Progress } from 'react-sweet-progress';
|
|
||||||
import 'react-sweet-progress/lib/style.css';
|
import 'react-sweet-progress/lib/style.css';
|
||||||
|
|
||||||
import styles from './BudgetItem.module.scss';
|
import styles from './BudgetItem.module.scss';
|
||||||
import { Budget } from '../../generated/openapi';
|
import { datesToDayRange } from '../../helpers/date';
|
||||||
|
import { BudgetModel } from '../../models/budget';
|
||||||
import { Card } from '../_design/Card/Card';
|
import { Card } from '../_design/Card/Card';
|
||||||
|
import { DetailWithTitle } from '../_design/DetailWithTitle/DetailWithTitle';
|
||||||
|
import { ProgressBar } from '../_design/ProgressBar/ProgressBar';
|
||||||
|
|
||||||
type BudgetItemProps = { budget: Budget };
|
type BudgetItemProps = { budget: BudgetModel };
|
||||||
|
|
||||||
const DATE_FORMAT = 'dd.MM.yy';
|
|
||||||
|
|
||||||
export const BudgetItem: FC<BudgetItemProps> = ({ budget }) => {
|
export const BudgetItem: FC<BudgetItemProps> = ({ budget }) => {
|
||||||
const { id, spent, name, limit, startDate, endDate } = budget;
|
const { id, spent, name, limit, startDate, endDate } = budget;
|
||||||
|
|
||||||
const spentPercentage = Math.trunc((100 * spent) / limit);
|
const spentPercentage = Math.trunc((100 * spent) / limit);
|
||||||
const startDateFormatted = format(new Date(startDate ?? ''), DATE_FORMAT);
|
|
||||||
const endDateFormatted = format(new Date(endDate ?? ''), DATE_FORMAT);
|
const isValidDateRange = is.date(startDate) && is.date(endDate) && endDate > startDate;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Card>
|
<Card>
|
||||||
|
@ -27,17 +27,16 @@ export const BudgetItem: FC<BudgetItemProps> = ({ budget }) => {
|
||||||
<h3 className={styles.Title}>
|
<h3 className={styles.Title}>
|
||||||
{name} <FontAwesomeIcon icon={faCoins} />
|
{name} <FontAwesomeIcon icon={faCoins} />
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
|
||||||
<small>Remaining Budget</small>
|
|
||||||
<Progress percent={spentPercentage} theme={{ active: { color: '#84844c' } }} />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div>
|
<DetailWithTitle title={'Remaining Budget'}>
|
||||||
<small>Period</small>
|
<ProgressBar percentage={spentPercentage} />
|
||||||
<p>
|
</DetailWithTitle>
|
||||||
{startDateFormatted} - {endDateFormatted}
|
|
||||||
</p>
|
{isValidDateRange && (
|
||||||
</div>
|
<DetailWithTitle title={'Period'}>
|
||||||
|
<p>{datesToDayRange(startDate, endDate)}</p>
|
||||||
|
</DetailWithTitle>
|
||||||
|
)}
|
||||||
|
|
||||||
<Link className={styles.Details} href={`/budget/${id}`}>
|
<Link className={styles.Details} href={`/budget/${id}`}>
|
||||||
Show Details
|
Show Details
|
||||||
|
|
|
@ -6,6 +6,6 @@
|
||||||
|
|
||||||
.ListItems {
|
.ListItems {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 16px;
|
gap: var(--grid-gap);
|
||||||
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,6 @@
|
||||||
|
.Detail {
|
||||||
|
display: flex;
|
||||||
|
align-items: stretch;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
14
src/components/_design/DetailWithTitle/DetailWithTitle.tsx
Normal file
14
src/components/_design/DetailWithTitle/DetailWithTitle.tsx
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
import { FunctionComponent, PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import styles from './DetailWithTitle.module.scss';
|
||||||
|
|
||||||
|
export interface DetailWithTitleProps extends PropsWithChildren {
|
||||||
|
readonly title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DetailWithTitle: FunctionComponent<DetailWithTitleProps> = ({ children, title }) => (
|
||||||
|
<div className={styles.Detail}>
|
||||||
|
<small>{title}</small>
|
||||||
|
<div>{children}</div>
|
||||||
|
</div>
|
||||||
|
);
|
10
src/components/_design/ProgressBar/ProgressBar.tsx
Normal file
10
src/components/_design/ProgressBar/ProgressBar.tsx
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { FunctionComponent } from 'react';
|
||||||
|
import { Progress } from 'react-sweet-progress';
|
||||||
|
|
||||||
|
export interface ProgressBarProps {
|
||||||
|
readonly percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ProgressBar: FunctionComponent<ProgressBarProps> = ({ percentage }) => (
|
||||||
|
<Progress percent={percentage} theme={{ active: { color: '#84844c' } }} />
|
||||||
|
);
|
7
src/helpers/date.ts
Normal file
7
src/helpers/date.ts
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
import format from 'date-fns/format';
|
||||||
|
|
||||||
|
const DAY_FORMAT = 'dd.MM.yy';
|
||||||
|
|
||||||
|
export const dateToFormattedDay = (date: Date): string => format(date, DAY_FORMAT);
|
||||||
|
|
||||||
|
export const datesToDayRange = (start: Date, end: Date) => dateToFormattedDay(start) + ' – ' + dateToFormattedDay(end);
|
|
@ -1,13 +1,14 @@
|
||||||
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { BudgetsService, BudgetSummary } from '../generated/openapi';
|
import { BudgetsService } from '../generated/openapi';
|
||||||
|
import { BudgetSummaryModel, mapToBudgetSummaryModel } from '../models/budget-summary';
|
||||||
|
|
||||||
const queryKey = Symbol('getBudgetSummary');
|
const queryKey = Symbol('getBudgetSummary');
|
||||||
|
|
||||||
export const useBudgetSummaryQuery = (id: string): UseQueryResult<BudgetSummary> => {
|
export const useBudgetSummaryQuery = (id: string): UseQueryResult<BudgetSummaryModel | null> =>
|
||||||
return useQuery({
|
useQuery({
|
||||||
queryKey: [queryKey, id],
|
queryKey: [queryKey, id],
|
||||||
queryFn: () => BudgetsService.getBudgetSummary(id),
|
queryFn: () => BudgetsService.getBudgetSummary(id),
|
||||||
keepPreviousData: true,
|
keepPreviousData: true,
|
||||||
|
select: mapToBudgetSummaryModel,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
|
@ -1,11 +1,20 @@
|
||||||
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||||
|
|
||||||
import { Budget, BudgetsService } from '../generated/openapi';
|
import { Budget, BudgetsService } from '../generated/openapi';
|
||||||
|
import { BudgetModel, mapToBudgetModel } from '../models/budget';
|
||||||
|
|
||||||
const budgetSymbol = Symbol('getBudgets');
|
const budgetSymbol = Symbol('getBudgets');
|
||||||
export const useBudgets = (): UseQueryResult<Budget[]> => {
|
|
||||||
return useQuery({
|
const selectValidBudgetModelsFromBudgetDtos = (budgetDtos: Budget[]) =>
|
||||||
|
budgetDtos.reduce<BudgetModel[]>((budgetModels, budgetDto) => {
|
||||||
|
const mappedBudget = mapToBudgetModel(budgetDto);
|
||||||
|
|
||||||
|
return mappedBudget ? [...budgetModels, mappedBudget] : budgetModels;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
export const useBudgets = (): UseQueryResult<BudgetModel[]> =>
|
||||||
|
useQuery({
|
||||||
queryKey: [budgetSymbol],
|
queryKey: [budgetSymbol],
|
||||||
queryFn: () => BudgetsService.getBudgets(),
|
queryFn: () => BudgetsService.getBudgets(),
|
||||||
|
select: selectValidBudgetModelsFromBudgetDtos,
|
||||||
});
|
});
|
||||||
};
|
|
||||||
|
|
8
src/models/_generic-schemas.ts
Normal file
8
src/models/_generic-schemas.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import is from '@sindresorhus/is';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const optionalDateSchema = z
|
||||||
|
.string()
|
||||||
|
.datetime()
|
||||||
|
.optional()
|
||||||
|
.transform((dataString) => (!is.undefined(dataString) ? new Date(dataString) : dataString));
|
24
src/models/budget-summary.ts
Normal file
24
src/models/budget-summary.ts
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { optionalDateSchema } from './_generic-schemas';
|
||||||
|
import { transactionSchema } from './transaction';
|
||||||
|
|
||||||
|
export const budgetSummaryModelSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
limit: z.number().positive(),
|
||||||
|
transactions: z.array(transactionSchema),
|
||||||
|
startDate: optionalDateSchema,
|
||||||
|
endDate: optionalDateSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BudgetSummaryModel = z.infer<typeof budgetSummaryModelSchema>;
|
||||||
|
|
||||||
|
export const mapToBudgetSummaryModel = (data: unknown): BudgetSummaryModel | null => {
|
||||||
|
try {
|
||||||
|
return budgetSummaryModelSchema.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
23
src/models/budget.ts
Normal file
23
src/models/budget.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { optionalDateSchema } from './_generic-schemas';
|
||||||
|
|
||||||
|
export const budgetModelSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
name: z.string(),
|
||||||
|
limit: z.number().positive(),
|
||||||
|
spent: z.number().positive(),
|
||||||
|
startDate: optionalDateSchema,
|
||||||
|
endDate: optionalDateSchema,
|
||||||
|
});
|
||||||
|
|
||||||
|
export type BudgetModel = z.infer<typeof budgetModelSchema>;
|
||||||
|
|
||||||
|
export const mapToBudgetModel = (data: unknown): BudgetModel | null => {
|
||||||
|
try {
|
||||||
|
return budgetModelSchema.parse(data);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
13
src/models/transaction.ts
Normal file
13
src/models/transaction.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const transactionSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
amount: z.number().positive(),
|
||||||
|
date: z
|
||||||
|
.string()
|
||||||
|
.datetime()
|
||||||
|
.transform((dateTime) => new Date(dateTime)),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type TransactionModel = z.infer<typeof transactionSchema>;
|
|
@ -15,7 +15,7 @@ import type { AppProps } from 'next/app';
|
||||||
config.autoAddCss = false;
|
config.autoAddCss = false;
|
||||||
const inter = Inter({ subsets: ['latin'] });
|
const inter = Inter({ subsets: ['latin'] });
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient({ defaultOptions: { queries: { refetchOnWindowFocus: false } } });
|
||||||
|
|
||||||
export default function App({ Component, pageProps }: AppProps) {
|
export default function App({ Component, pageProps }: AppProps) {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -1,19 +1,19 @@
|
||||||
import is from '@sindresorhus/is';
|
import is from '@sindresorhus/is';
|
||||||
|
|
||||||
import { Budget } from '../../../../generated/openapi';
|
import { BudgetSummary } from '../../../../generated/openapi';
|
||||||
import { budgetList } from '../index';
|
import { budgetSummaryFactory } from '../../factories/budget.factories';
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
export default function handler(req: NextApiRequest, res: NextApiResponse<Budget>) {
|
const budgetSummaryMock = budgetSummaryFactory.buildList(8);
|
||||||
|
|
||||||
|
export default function handler(req: NextApiRequest, res: NextApiResponse<BudgetSummary>) {
|
||||||
const { query, method } = req;
|
const { query, method } = req;
|
||||||
const id = query.id as string;
|
const id = query.id as string;
|
||||||
|
|
||||||
switch (method) {
|
switch (method) {
|
||||||
case 'GET':
|
case 'GET':
|
||||||
const targetBudget = budgetList.find(({ id: budgetId }) => budgetId === id);
|
const targetBudget = budgetSummaryMock.find(({ id: budgetId }) => budgetId === id);
|
||||||
|
|
||||||
console.log(budgetList.map(({ id }) => id));
|
|
||||||
|
|
||||||
if (is.undefined(targetBudget)) {
|
if (is.undefined(targetBudget)) {
|
||||||
res.status(404).end();
|
res.status(404).end();
|
||||||
|
|
|
@ -4,7 +4,6 @@ import { budgetFactory } from '../factories/budget.factories';
|
||||||
|
|
||||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||||
|
|
||||||
// TODO fix this setup as on HMR this is created as well again which results in invalid data on detail pages (404)
|
|
||||||
export const budgetList = budgetFactory.buildList(8);
|
export const budgetList = budgetFactory.buildList(8);
|
||||||
budgetFactory.rewindSequence();
|
budgetFactory.rewindSequence();
|
||||||
|
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
import { randomUUID } from 'crypto';
|
|
||||||
|
|
||||||
import { Factory } from 'fishery';
|
import { Factory } from 'fishery';
|
||||||
|
|
||||||
import { Budget } from '../../../generated/openapi';
|
import { transactionFactory } from './transaction.factories';
|
||||||
|
import { Budget, BudgetSummary } from '../../../generated/openapi';
|
||||||
|
|
||||||
export const budgetFactory = Factory.define<Budget>(({ sequence }) => {
|
export const budgetFactory = Factory.define<Budget>(({ sequence }) => {
|
||||||
const limit = 200 + (sequence - 1) * 50;
|
const limit = 200 + (sequence - 1) * 50;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: randomUUID(),
|
id: `budget-mock-id-${sequence}`,
|
||||||
startDate: '2023-02-21T18:49:13.620Z',
|
startDate: '2023-02-21T18:49:13.620Z',
|
||||||
endDate: '2023-03-21T18:49:13.620Z',
|
endDate: '2023-03-21T18:49:13.620Z',
|
||||||
name: 'Budget-' + sequence,
|
name: 'Budget-' + sequence,
|
||||||
|
@ -16,3 +15,19 @@ export const budgetFactory = Factory.define<Budget>(({ sequence }) => {
|
||||||
spent: limit - Math.min(limit, sequence * 50),
|
spent: limit - Math.min(limit, sequence * 50),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const budgetSummaryFactory = Factory.define<BudgetSummary>(({ sequence }) => {
|
||||||
|
const limit = 200 + (sequence - 1) * 50;
|
||||||
|
|
||||||
|
const transactions = transactionFactory.buildList(5);
|
||||||
|
transactionFactory.rewindSequence();
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: `budget-mock-id-${sequence}`,
|
||||||
|
startDate: '2023-02-21T18:49:13.620Z',
|
||||||
|
endDate: '2023-03-21T18:49:13.620Z',
|
||||||
|
name: 'Budget-' + sequence,
|
||||||
|
limit,
|
||||||
|
transactions: transactions,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
10
src/pages/api/factories/transaction.factories.ts
Normal file
10
src/pages/api/factories/transaction.factories.ts
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
import { Factory } from 'fishery';
|
||||||
|
|
||||||
|
import { Transaction } from '../../../generated/openapi';
|
||||||
|
|
||||||
|
export const transactionFactory = Factory.define<Transaction>(({ sequence }) => ({
|
||||||
|
id: `transaction-id-${sequence}`,
|
||||||
|
amount: 5 * sequence,
|
||||||
|
date: '2023-03-21T18:49:13.620Z',
|
||||||
|
description: `Transaction description for transaction ${sequence}`,
|
||||||
|
}));
|
|
@ -5,16 +5,7 @@ import { BudgetDetail } from '../../components/BudgetDetail/BudgetDetail';
|
||||||
|
|
||||||
export default function BudgetPage() {
|
export default function BudgetPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
const { id } = router.query;
|
const { id } = router.query;
|
||||||
|
|
||||||
if (is.nonEmptyString(id)) {
|
return !is.string(id) ? null : <BudgetDetail id={id} />;
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<BudgetDetail id={id} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
--padding-inline: var(--padding-container);
|
--padding-inline: var(--padding-container);
|
||||||
|
|
||||||
@media (min-width: 980px) {
|
@media (min-width: 980px) {
|
||||||
--content-width: clamp(980px, 60vw, var(--max-width));
|
--content-width: clamp(980px, 70vw, var(--max-width));
|
||||||
--padding-inline: 0px;
|
--padding-inline: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
:root {
|
:root {
|
||||||
--max-width: 1100px;
|
--max-width: 1080px;
|
||||||
--border-radius: 4px;
|
--border-radius: 4px;
|
||||||
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
|
--font-mono: ui-monospace, Menlo, Monaco, 'Cascadia Mono', 'Segoe UI Mono',
|
||||||
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
|
'Roboto Mono', 'Oxygen Mono', 'Ubuntu Monospace', 'Source Code Pro',
|
||||||
|
@ -49,6 +49,7 @@
|
||||||
|
|
||||||
/* Custom Dimensions */
|
/* Custom Dimensions */
|
||||||
--container-padding: 32px;
|
--container-padding: 32px;
|
||||||
|
--grid-gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (prefers-color-scheme: dark) {
|
@media (prefers-color-scheme: dark) {
|
||||||
|
@ -107,6 +108,22 @@ body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h1, h2, h3, h4, h4 {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 1.6rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: var(--primary-color);
|
color: var(--primary-color);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
|
Loading…
Reference in a new issue