mirror of
https://github.com/siwa-net/my-finance-pal.git
synced 2024-11-10 00:51:56 +01:00
Initial setup/pages/linters
This commit is contained in:
parent
fbc062fefc
commit
fe6ed2b158
43 changed files with 1828 additions and 2309 deletions
33
.eslintrc.json
Normal file
33
.eslintrc.json
Normal file
|
@ -0,0 +1,33 @@
|
|||
{
|
||||
"extends": [
|
||||
"next",
|
||||
"prettier",
|
||||
"plugin:import/recommended",
|
||||
"plugin:import/typescript"
|
||||
],
|
||||
"parserOptions": {
|
||||
"sourceType": "module",
|
||||
"ecmaVersion": 2020
|
||||
},
|
||||
"plugins": [
|
||||
"prettier",
|
||||
"import"
|
||||
],
|
||||
"rules": {
|
||||
"import/order": [
|
||||
"error",
|
||||
{
|
||||
"groups": ["builtin", "external", ["parent", "sibling", "index"], "type"],
|
||||
"newlines-between": "always",
|
||||
"pathGroupsExcludedImportTypes": ["builtin"],
|
||||
"alphabetize": {
|
||||
"order": "asc",
|
||||
"caseInsensitive": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"prettier/prettier": [
|
||||
"error"
|
||||
]
|
||||
}
|
||||
}
|
10
.prettierrc
Normal file
10
.prettierrc
Normal file
|
@ -0,0 +1,10 @@
|
|||
{
|
||||
"arrowParens": "always",
|
||||
"bracketSpacing": true,
|
||||
"printWidth": 120,
|
||||
"semi": true,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 4,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false
|
||||
}
|
271
api.yaml
Normal file
271
api.yaml
Normal file
|
@ -0,0 +1,271 @@
|
|||
openapi: 3.0.1
|
||||
info:
|
||||
version: 1.0.0
|
||||
title: My Finance Pal
|
||||
description: API of the personal finance budgeting app My Finance Pal
|
||||
tags:
|
||||
- name: transactions
|
||||
description: Transaction of budgets
|
||||
- name: budgets
|
||||
description: Managing budgets
|
||||
paths:
|
||||
/budgets:
|
||||
post:
|
||||
tags:
|
||||
- budgets
|
||||
description: Create a new budget
|
||||
operationId: createBudget
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NewBudget"
|
||||
responses:
|
||||
201:
|
||||
description: Budget created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Budget"
|
||||
400:
|
||||
description: Invalid budget
|
||||
get:
|
||||
tags:
|
||||
- budgets
|
||||
description: Get all budgets
|
||||
operationId: getBudgets
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/Budget"
|
||||
/budgets/{budgetId}/summary:
|
||||
get:
|
||||
tags:
|
||||
- budgets
|
||||
description: Get a budget summary including all transactions for a given ID
|
||||
operationId: getBudgetSummary
|
||||
parameters:
|
||||
- in: path
|
||||
name: budgetId
|
||||
required: true
|
||||
description: The UUID of the budget
|
||||
schema:
|
||||
$ref: "#/components/schemas/BudgetId"
|
||||
responses:
|
||||
200:
|
||||
description: OK
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BudgetSummary"
|
||||
404:
|
||||
description: Budget not found
|
||||
/budgets/{budgetId}:
|
||||
delete:
|
||||
tags:
|
||||
- budgets
|
||||
description: Delete a budget including all transactions
|
||||
operationId: deleteBudget
|
||||
parameters:
|
||||
- in: path
|
||||
name: budgetId
|
||||
required: true
|
||||
description: The UUID of the budget
|
||||
schema:
|
||||
$ref: "#/components/schemas/BudgetId"
|
||||
responses:
|
||||
204:
|
||||
description: Budget successfully deleted
|
||||
404:
|
||||
description: Budget not found
|
||||
/budgets/{budgetId}/transactions:
|
||||
post:
|
||||
tags:
|
||||
- transactions
|
||||
description: Register a new transaction for a budget
|
||||
operationId: createTransaction
|
||||
parameters:
|
||||
- $ref: "#/components/parameters/BudgetIdParam"
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/NewTransaction"
|
||||
responses:
|
||||
201:
|
||||
description: Transaction created
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Transaction"
|
||||
|
||||
components:
|
||||
parameters:
|
||||
BudgetIdParam:
|
||||
in: path
|
||||
name: budgetId
|
||||
required: true
|
||||
description: The UUID of the budget
|
||||
schema:
|
||||
$ref: "#/components/schemas/BudgetId"
|
||||
TransactionIdParam:
|
||||
in: path
|
||||
name: transactionId
|
||||
required: true
|
||||
description: The UUID of the transaction
|
||||
schema:
|
||||
$ref: "#/components/schemas/TransactionId"
|
||||
schemas:
|
||||
BudgetId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique identifier of one budget
|
||||
example: 850963b9-a04d-4698-b767-c0a0096b37c5
|
||||
Budget:
|
||||
type: object
|
||||
description: Planned money to be available for tracking expenses related to a certain purpose over a period of time
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- limit
|
||||
- spent
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/components/schemas/BudgetId"
|
||||
name:
|
||||
type: string
|
||||
description: The name of the budget
|
||||
example: Takeout
|
||||
limit:
|
||||
type: number
|
||||
minimum: 0
|
||||
description: The total amount available for the budget
|
||||
example: 250
|
||||
spent:
|
||||
type: number
|
||||
minimum: 0
|
||||
description: The summed up spent amount of all transaction of that budget
|
||||
startDate:
|
||||
type: string
|
||||
format: date
|
||||
description: Date marking the start of the budget period
|
||||
example: 2023-04-01
|
||||
endDate:
|
||||
type: string
|
||||
format: date
|
||||
description: Date marking the end of the budget period
|
||||
example: 2023-04-30
|
||||
BudgetSummary:
|
||||
type: object
|
||||
description: Summary of the whole budget including all transactions
|
||||
required:
|
||||
- id
|
||||
- name
|
||||
- limit
|
||||
- transactions
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/components/schemas/BudgetId"
|
||||
name:
|
||||
type: string
|
||||
description: The name of the budget
|
||||
example: Takeout
|
||||
limit:
|
||||
type: number
|
||||
minimum: 0
|
||||
description: The total amount available for the budget
|
||||
example: 250
|
||||
startDate:
|
||||
type: string
|
||||
format: date
|
||||
description: Date marking the start of the budget period
|
||||
example: 2023-04-01
|
||||
endDate:
|
||||
type: string
|
||||
format: date
|
||||
description: Date marking the end of the budget period
|
||||
example: 2023-04-30
|
||||
transactions:
|
||||
type: array
|
||||
description: All transactions of the budget
|
||||
items:
|
||||
$ref: "#/components/schemas/Transaction"
|
||||
NewBudget:
|
||||
type: object
|
||||
description: Budget to be created
|
||||
required:
|
||||
- name
|
||||
- limit
|
||||
properties:
|
||||
name:
|
||||
type: string
|
||||
description: The name of the budget
|
||||
example: Takeout
|
||||
limit:
|
||||
type: number
|
||||
minimum: 0
|
||||
description: The total amount available for the budget
|
||||
example: 250
|
||||
startDate:
|
||||
type: string
|
||||
format: date
|
||||
description: Date marking the start of the budget period
|
||||
example: 2023-04-01
|
||||
endDate:
|
||||
type: string
|
||||
format: date
|
||||
description: Date marking the end of the budget period
|
||||
example: 2023-04-30
|
||||
TransactionId:
|
||||
type: string
|
||||
format: uuid
|
||||
description: Unique identifier of a transaction
|
||||
example: cfd3b0b7-bf5e-43d4-8341-940d5e07487d
|
||||
Transaction:
|
||||
type: object
|
||||
description: An expense or income related to a single budget
|
||||
required:
|
||||
- id
|
||||
- description
|
||||
- amount
|
||||
- date
|
||||
properties:
|
||||
id:
|
||||
$ref: "#/components/schemas/TransactionId"
|
||||
description:
|
||||
type: string
|
||||
description: Description of the transaction
|
||||
example: Healthy breakfast at Five Guys
|
||||
amount:
|
||||
type: number
|
||||
description: Amount of money contained in the transaction
|
||||
example: 23.94
|
||||
date:
|
||||
type: string
|
||||
format: date
|
||||
NewTransaction:
|
||||
type: object
|
||||
description: A new transaction to be created
|
||||
required:
|
||||
- description
|
||||
- amount
|
||||
- date
|
||||
properties:
|
||||
description:
|
||||
type: string
|
||||
description: Description of the transaction
|
||||
example: Healthy breakfast at Five Guys
|
||||
amount:
|
||||
type: number
|
||||
description: Amount of money contained in the transaction
|
||||
example: 23.94
|
||||
date:
|
||||
type: string
|
||||
format: date
|
7
openapitools.json
Normal file
7
openapitools.json
Normal file
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json",
|
||||
"spaces": 2,
|
||||
"generator-cli": {
|
||||
"version": "6.4.0"
|
||||
}
|
||||
}
|
2698
package-lock.json
generated
2698
package-lock.json
generated
File diff suppressed because it is too large
Load diff
15
package.json
15
package.json
|
@ -6,17 +6,30 @@
|
|||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "next lint"
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint -- --fix",
|
||||
"codegen": "rm -rf ./src/generated/openapi; openapi --input ./api.yaml --output ./src/generated/openapi"
|
||||
},
|
||||
"dependencies": {
|
||||
"@sindresorhus/is": "^5.3.0",
|
||||
"@tanstack/react-query": "^4.28.0",
|
||||
"@types/node": "18.15.5",
|
||||
"@types/react": "18.0.28",
|
||||
"@types/react-dom": "18.0.11",
|
||||
"eslint": "8.36.0",
|
||||
"eslint-config-next": "13.2.4",
|
||||
"fishery": "^2.2.2",
|
||||
"next": "13.2.4",
|
||||
"react": "18.2.0",
|
||||
"react-dom": "18.2.0",
|
||||
"sass": "^1.59.3",
|
||||
"typescript": "5.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint-config-prettier": "^8.8.0",
|
||||
"eslint-plugin-import": "^2.27.5",
|
||||
"eslint-plugin-prettier": "^4.2.1",
|
||||
"openapi-typescript-codegen": "^0.23.0",
|
||||
"prettier": "2.8.6"
|
||||
}
|
||||
}
|
||||
|
|
3
src/components/BudgetDetail/BudgetDetail.module.scss
Normal file
3
src/components/BudgetDetail/BudgetDetail.module.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
.Container {
|
||||
|
||||
}
|
19
src/components/BudgetDetail/BudgetDetail.tsx
Normal file
19
src/components/BudgetDetail/BudgetDetail.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import styles from './BudgetDetail.module.scss';
|
||||
|
||||
type BudgetDetailProps = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const BudgetDetail: React.FC<BudgetDetailProps> = ({ id }) => {
|
||||
return (
|
||||
<>
|
||||
<h1>{id} </h1>
|
||||
<div className={styles.ProgressBar}>
|
||||
<div className={styles.ProgressPercentage}></div>
|
||||
</div>
|
||||
<span>Amount</span>
|
||||
<span>Date- start -end</span>
|
||||
<button onClick={() => null}>View transactions</button>
|
||||
</>
|
||||
);
|
||||
};
|
8
src/components/BudgetItem/BudgetItem.module.scss
Normal file
8
src/components/BudgetItem/BudgetItem.module.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
.Container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
width: 250px;
|
||||
height: 250px;
|
||||
background: skyblue;
|
||||
}
|
23
src/components/BudgetItem/BudgetItem.tsx
Normal file
23
src/components/BudgetItem/BudgetItem.tsx
Normal file
|
@ -0,0 +1,23 @@
|
|||
import Link from 'next/link';
|
||||
import { FC } from 'react';
|
||||
|
||||
import styles from './BudgetItem.module.scss';
|
||||
import { Budget } from '../../generated/openapi';
|
||||
|
||||
type BudgetItemProps = { budget: Budget };
|
||||
|
||||
export const BudgetItem: FC<BudgetItemProps> = ({ budget: { id, spent, name, limit, startDate, endDate } }) => {
|
||||
return (
|
||||
<div className={styles.Container}>
|
||||
<span> {name}</span>
|
||||
<span>
|
||||
{spent} / {limit}
|
||||
</span>
|
||||
<span>
|
||||
{startDate} - {endDate}
|
||||
</span>
|
||||
|
||||
<Link href={`/budgets/${id}`}>More info </Link>
|
||||
</div>
|
||||
);
|
||||
};
|
11
src/components/BudgetList/BudgetList.module.scss
Normal file
11
src/components/BudgetList/BudgetList.module.scss
Normal file
|
@ -0,0 +1,11 @@
|
|||
.Container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
|
||||
.ListItems {
|
||||
display: flex;
|
||||
gap: 1rem;
|
||||
border: 1px solid green;
|
||||
}
|
19
src/components/BudgetList/BudgetList.tsx
Normal file
19
src/components/BudgetList/BudgetList.tsx
Normal file
|
@ -0,0 +1,19 @@
|
|||
import { FC } from 'react';
|
||||
|
||||
import styles from './BudgetList.module.scss';
|
||||
import { useBudgets } from '../../hooks/useBudgets';
|
||||
import { BudgetItem } from '../BudgetItem/BudgetItem';
|
||||
|
||||
export const BudgetList: FC = () => {
|
||||
const { data: budgets = [] } = useBudgets();
|
||||
return (
|
||||
<div className={styles.Container}>
|
||||
<button onClick={() => null}>Add a budget</button>
|
||||
<div className={styles.ListItems}>
|
||||
{budgets.map((budget) => (
|
||||
<BudgetItem key={budget.id} budget={budget} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
14
src/components/BudgetList/__mocks__/budgets.mock.ts
Normal file
14
src/components/BudgetList/__mocks__/budgets.mock.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
import { Factory } from 'fishery';
|
||||
|
||||
import { Budget } from '../../../generated/openapi';
|
||||
|
||||
const budgetFactory = Factory.define<Budget>(({ sequence }) => ({
|
||||
id: sequence.toString(),
|
||||
name: `Budget-${sequence}`,
|
||||
limit: 500,
|
||||
spent: 250,
|
||||
startDate: '2022-03-21T18:49:13.620Z',
|
||||
endDate: '2023-03-21T18:49:13.620Z',
|
||||
}));
|
||||
|
||||
export const budgetsMock = budgetFactory.buildList(5);
|
|
@ -1,5 +0,0 @@
|
|||
import {FunctionComponent} from 'react';
|
||||
|
||||
export const Navigation: FunctionComponent = () => (
|
||||
<nav>Navigation</nav>
|
||||
);
|
8
src/components/Navigation/Navigation.module.scss
Normal file
8
src/components/Navigation/Navigation.module.scss
Normal file
|
@ -0,0 +1,8 @@
|
|||
.Container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
box-shadow: 0px -8px 24px 0px #555;
|
||||
justify-content: space-between;
|
||||
}
|
10
src/components/Navigation/Navigation.tsx
Normal file
10
src/components/Navigation/Navigation.tsx
Normal file
|
@ -0,0 +1,10 @@
|
|||
import { FunctionComponent } from 'react';
|
||||
|
||||
import styles from './Navigation.module.scss';
|
||||
|
||||
export const Navigation: FunctionComponent = () => (
|
||||
<nav className={styles.Container}>
|
||||
<span>LOGO</span>
|
||||
<span>Account</span>
|
||||
</nav>
|
||||
);
|
24
src/generated/openapi/core/ApiError.ts
Normal file
24
src/generated/openapi/core/ApiError.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
|
||||
export class ApiError extends Error {
|
||||
public readonly url: string;
|
||||
public readonly status: number;
|
||||
public readonly statusText: string;
|
||||
public readonly body: any;
|
||||
public readonly request: ApiRequestOptions;
|
||||
|
||||
constructor(request: ApiRequestOptions, response: ApiResult, message: string) {
|
||||
super(message);
|
||||
|
||||
this.name = 'ApiError';
|
||||
this.url = response.url;
|
||||
this.status = response.status;
|
||||
this.statusText = response.statusText;
|
||||
this.body = response.body;
|
||||
this.request = request;
|
||||
}
|
||||
}
|
16
src/generated/openapi/core/ApiRequestOptions.ts
Normal file
16
src/generated/openapi/core/ApiRequestOptions.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ApiRequestOptions = {
|
||||
readonly method: 'GET' | 'PUT' | 'POST' | 'DELETE' | 'OPTIONS' | 'HEAD' | 'PATCH';
|
||||
readonly url: string;
|
||||
readonly path?: Record<string, any>;
|
||||
readonly cookies?: Record<string, any>;
|
||||
readonly headers?: Record<string, any>;
|
||||
readonly query?: Record<string, any>;
|
||||
readonly formData?: Record<string, any>;
|
||||
readonly body?: any;
|
||||
readonly mediaType?: string;
|
||||
readonly responseHeader?: string;
|
||||
readonly errors?: Record<number, string>;
|
||||
};
|
10
src/generated/openapi/core/ApiResult.ts
Normal file
10
src/generated/openapi/core/ApiResult.ts
Normal file
|
@ -0,0 +1,10 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export type ApiResult = {
|
||||
readonly url: string;
|
||||
readonly ok: boolean;
|
||||
readonly status: number;
|
||||
readonly statusText: string;
|
||||
readonly body: any;
|
||||
};
|
128
src/generated/openapi/core/CancelablePromise.ts
Normal file
128
src/generated/openapi/core/CancelablePromise.ts
Normal file
|
@ -0,0 +1,128 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export class CancelError extends Error {
|
||||
|
||||
constructor(message: string) {
|
||||
super(message);
|
||||
this.name = 'CancelError';
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export interface OnCancel {
|
||||
readonly isResolved: boolean;
|
||||
readonly isRejected: boolean;
|
||||
readonly isCancelled: boolean;
|
||||
|
||||
(cancelHandler: () => void): void;
|
||||
}
|
||||
|
||||
export class CancelablePromise<T> implements Promise<T> {
|
||||
readonly [Symbol.toStringTag]!: string;
|
||||
|
||||
private _isResolved: boolean;
|
||||
private _isRejected: boolean;
|
||||
private _isCancelled: boolean;
|
||||
private readonly _cancelHandlers: (() => void)[];
|
||||
private readonly _promise: Promise<T>;
|
||||
private _resolve?: (value: T | PromiseLike<T>) => void;
|
||||
private _reject?: (reason?: any) => void;
|
||||
|
||||
constructor(
|
||||
executor: (
|
||||
resolve: (value: T | PromiseLike<T>) => void,
|
||||
reject: (reason?: any) => void,
|
||||
onCancel: OnCancel
|
||||
) => void
|
||||
) {
|
||||
this._isResolved = false;
|
||||
this._isRejected = false;
|
||||
this._isCancelled = false;
|
||||
this._cancelHandlers = [];
|
||||
this._promise = new Promise<T>((resolve, reject) => {
|
||||
this._resolve = resolve;
|
||||
this._reject = reject;
|
||||
|
||||
const onResolve = (value: T | PromiseLike<T>): void => {
|
||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||
return;
|
||||
}
|
||||
this._isResolved = true;
|
||||
this._resolve?.(value);
|
||||
};
|
||||
|
||||
const onReject = (reason?: any): void => {
|
||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||
return;
|
||||
}
|
||||
this._isRejected = true;
|
||||
this._reject?.(reason);
|
||||
};
|
||||
|
||||
const onCancel = (cancelHandler: () => void): void => {
|
||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||
return;
|
||||
}
|
||||
this._cancelHandlers.push(cancelHandler);
|
||||
};
|
||||
|
||||
Object.defineProperty(onCancel, 'isResolved', {
|
||||
get: (): boolean => this._isResolved,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, 'isRejected', {
|
||||
get: (): boolean => this._isRejected,
|
||||
});
|
||||
|
||||
Object.defineProperty(onCancel, 'isCancelled', {
|
||||
get: (): boolean => this._isCancelled,
|
||||
});
|
||||
|
||||
return executor(onResolve, onReject, onCancel as OnCancel);
|
||||
});
|
||||
}
|
||||
|
||||
public then<TResult1 = T, TResult2 = never>(
|
||||
onFulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
|
||||
onRejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
|
||||
): Promise<TResult1 | TResult2> {
|
||||
return this._promise.then(onFulfilled, onRejected);
|
||||
}
|
||||
|
||||
public catch<TResult = never>(
|
||||
onRejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
|
||||
): Promise<T | TResult> {
|
||||
return this._promise.catch(onRejected);
|
||||
}
|
||||
|
||||
public finally(onFinally?: (() => void) | null): Promise<T> {
|
||||
return this._promise.finally(onFinally);
|
||||
}
|
||||
|
||||
public cancel(): void {
|
||||
if (this._isResolved || this._isRejected || this._isCancelled) {
|
||||
return;
|
||||
}
|
||||
this._isCancelled = true;
|
||||
if (this._cancelHandlers.length) {
|
||||
try {
|
||||
for (const cancelHandler of this._cancelHandlers) {
|
||||
cancelHandler();
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('Cancellation threw an error', error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._cancelHandlers.length = 0;
|
||||
this._reject?.(new CancelError('Request aborted'));
|
||||
}
|
||||
|
||||
public get isCancelled(): boolean {
|
||||
return this._isCancelled;
|
||||
}
|
||||
}
|
31
src/generated/openapi/core/OpenAPI.ts
Normal file
31
src/generated/openapi/core/OpenAPI.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||
type Headers = Record<string, string>;
|
||||
|
||||
export type OpenAPIConfig = {
|
||||
BASE: string;
|
||||
VERSION: string;
|
||||
WITH_CREDENTIALS: boolean;
|
||||
CREDENTIALS: 'include' | 'omit' | 'same-origin';
|
||||
TOKEN?: string | Resolver<string>;
|
||||
USERNAME?: string | Resolver<string>;
|
||||
PASSWORD?: string | Resolver<string>;
|
||||
HEADERS?: Headers | Resolver<Headers>;
|
||||
ENCODE_PATH?: (path: string) => string;
|
||||
};
|
||||
|
||||
export const OpenAPI: OpenAPIConfig = {
|
||||
BASE: '/api', // TODO: Make dependent from ENV
|
||||
VERSION: '1.0.0',
|
||||
WITH_CREDENTIALS: false,
|
||||
CREDENTIALS: 'include',
|
||||
TOKEN: undefined,
|
||||
USERNAME: undefined,
|
||||
PASSWORD: undefined,
|
||||
HEADERS: undefined,
|
||||
ENCODE_PATH: undefined,
|
||||
};
|
306
src/generated/openapi/core/request.ts
Normal file
306
src/generated/openapi/core/request.ts
Normal file
|
@ -0,0 +1,306 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import { ApiError } from './ApiError';
|
||||
import type { ApiRequestOptions } from './ApiRequestOptions';
|
||||
import type { ApiResult } from './ApiResult';
|
||||
import { CancelablePromise } from './CancelablePromise';
|
||||
import type { OnCancel } from './CancelablePromise';
|
||||
import type { OpenAPIConfig } from './OpenAPI';
|
||||
|
||||
const isDefined = <T>(value: T | null | undefined): value is Exclude<T, null | undefined> => {
|
||||
return value !== undefined && value !== null;
|
||||
};
|
||||
|
||||
const isString = (value: any): value is string => {
|
||||
return typeof value === 'string';
|
||||
};
|
||||
|
||||
const isStringWithValue = (value: any): value is string => {
|
||||
return isString(value) && value !== '';
|
||||
};
|
||||
|
||||
const isBlob = (value: any): value is Blob => {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
typeof value.type === 'string' &&
|
||||
typeof value.stream === 'function' &&
|
||||
typeof value.arrayBuffer === 'function' &&
|
||||
typeof value.constructor === 'function' &&
|
||||
typeof value.constructor.name === 'string' &&
|
||||
/^(Blob|File)$/.test(value.constructor.name) &&
|
||||
/^(Blob|File)$/.test(value[Symbol.toStringTag])
|
||||
);
|
||||
};
|
||||
|
||||
const isFormData = (value: any): value is FormData => {
|
||||
return value instanceof FormData;
|
||||
};
|
||||
|
||||
const base64 = (str: string): string => {
|
||||
try {
|
||||
return btoa(str);
|
||||
} catch (err) {
|
||||
// @ts-ignore
|
||||
return Buffer.from(str).toString('base64');
|
||||
}
|
||||
};
|
||||
|
||||
const getQueryString = (params: Record<string, any>): string => {
|
||||
const qs: string[] = [];
|
||||
|
||||
const append = (key: string, value: any) => {
|
||||
qs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
};
|
||||
|
||||
const process = (key: string, value: any) => {
|
||||
if (isDefined(value)) {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => {
|
||||
process(key, v);
|
||||
});
|
||||
} else if (typeof value === 'object') {
|
||||
Object.entries(value).forEach(([k, v]) => {
|
||||
process(`${key}[${k}]`, v);
|
||||
});
|
||||
} else {
|
||||
append(key, value);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
process(key, value);
|
||||
});
|
||||
|
||||
if (qs.length > 0) {
|
||||
return `?${qs.join('&')}`;
|
||||
}
|
||||
|
||||
return '';
|
||||
};
|
||||
|
||||
const getUrl = (config: OpenAPIConfig, options: ApiRequestOptions): string => {
|
||||
const encoder = config.ENCODE_PATH || encodeURI;
|
||||
|
||||
const path = options.url
|
||||
.replace('{api-version}', config.VERSION)
|
||||
.replace(/{(.*?)}/g, (substring: string, group: string) => {
|
||||
if (options.path?.hasOwnProperty(group)) {
|
||||
return encoder(String(options.path[group]));
|
||||
}
|
||||
return substring;
|
||||
});
|
||||
|
||||
const url = `${config.BASE}${path}`;
|
||||
if (options.query) {
|
||||
return `${url}${getQueryString(options.query)}`;
|
||||
}
|
||||
return url;
|
||||
};
|
||||
|
||||
const getFormData = (options: ApiRequestOptions): FormData | undefined => {
|
||||
if (options.formData) {
|
||||
const formData = new FormData();
|
||||
|
||||
const process = (key: string, value: any) => {
|
||||
if (isString(value) || isBlob(value)) {
|
||||
formData.append(key, value);
|
||||
} else {
|
||||
formData.append(key, JSON.stringify(value));
|
||||
}
|
||||
};
|
||||
|
||||
Object.entries(options.formData)
|
||||
.filter(([_, value]) => isDefined(value))
|
||||
.forEach(([key, value]) => {
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach(v => process(key, v));
|
||||
} else {
|
||||
process(key, value);
|
||||
}
|
||||
});
|
||||
|
||||
return formData;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
type Resolver<T> = (options: ApiRequestOptions) => Promise<T>;
|
||||
|
||||
const resolve = async <T>(options: ApiRequestOptions, resolver?: T | Resolver<T>): Promise<T | undefined> => {
|
||||
if (typeof resolver === 'function') {
|
||||
return (resolver as Resolver<T>)(options);
|
||||
}
|
||||
return resolver;
|
||||
};
|
||||
|
||||
const getHeaders = async (config: OpenAPIConfig, options: ApiRequestOptions): Promise<Headers> => {
|
||||
const token = await resolve(options, config.TOKEN);
|
||||
const username = await resolve(options, config.USERNAME);
|
||||
const password = await resolve(options, config.PASSWORD);
|
||||
const additionalHeaders = await resolve(options, config.HEADERS);
|
||||
|
||||
const headers = Object.entries({
|
||||
Accept: 'application/json',
|
||||
...additionalHeaders,
|
||||
...options.headers,
|
||||
})
|
||||
.filter(([_, value]) => isDefined(value))
|
||||
.reduce((headers, [key, value]) => ({
|
||||
...headers,
|
||||
[key]: String(value),
|
||||
}), {} as Record<string, string>);
|
||||
|
||||
if (isStringWithValue(token)) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
if (isStringWithValue(username) && isStringWithValue(password)) {
|
||||
const credentials = base64(`${username}:${password}`);
|
||||
headers['Authorization'] = `Basic ${credentials}`;
|
||||
}
|
||||
|
||||
if (options.body) {
|
||||
if (options.mediaType) {
|
||||
headers['Content-Type'] = options.mediaType;
|
||||
} else if (isBlob(options.body)) {
|
||||
headers['Content-Type'] = options.body.type || 'application/octet-stream';
|
||||
} else if (isString(options.body)) {
|
||||
headers['Content-Type'] = 'text/plain';
|
||||
} else if (!isFormData(options.body)) {
|
||||
headers['Content-Type'] = 'application/json';
|
||||
}
|
||||
}
|
||||
|
||||
return new Headers(headers);
|
||||
};
|
||||
|
||||
const getRequestBody = (options: ApiRequestOptions): any => {
|
||||
if (options.body) {
|
||||
if (options.mediaType?.includes('/json')) {
|
||||
return JSON.stringify(options.body)
|
||||
} else if (isString(options.body) || isBlob(options.body) || isFormData(options.body)) {
|
||||
return options.body;
|
||||
} else {
|
||||
return JSON.stringify(options.body);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
export const sendRequest = async (
|
||||
config: OpenAPIConfig,
|
||||
options: ApiRequestOptions,
|
||||
url: string,
|
||||
body: any,
|
||||
formData: FormData | undefined,
|
||||
headers: Headers,
|
||||
onCancel: OnCancel
|
||||
): Promise<Response> => {
|
||||
const controller = new AbortController();
|
||||
|
||||
const request: RequestInit = {
|
||||
headers,
|
||||
body: body ?? formData,
|
||||
method: options.method,
|
||||
signal: controller.signal,
|
||||
};
|
||||
|
||||
if (config.WITH_CREDENTIALS) {
|
||||
request.credentials = config.CREDENTIALS;
|
||||
}
|
||||
|
||||
onCancel(() => controller.abort());
|
||||
|
||||
return await fetch(url, request);
|
||||
};
|
||||
|
||||
const getResponseHeader = (response: Response, responseHeader?: string): string | undefined => {
|
||||
if (responseHeader) {
|
||||
const content = response.headers.get(responseHeader);
|
||||
if (isString(content)) {
|
||||
return content;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const getResponseBody = async (response: Response): Promise<any> => {
|
||||
if (response.status !== 204) {
|
||||
try {
|
||||
const contentType = response.headers.get('Content-Type');
|
||||
if (contentType) {
|
||||
const isJSON = contentType.toLowerCase().startsWith('application/json');
|
||||
if (isJSON) {
|
||||
return await response.json();
|
||||
} else {
|
||||
return await response.text();
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const catchErrorCodes = (options: ApiRequestOptions, result: ApiResult): void => {
|
||||
const errors: Record<number, string> = {
|
||||
400: 'Bad Request',
|
||||
401: 'Unauthorized',
|
||||
403: 'Forbidden',
|
||||
404: 'Not Found',
|
||||
500: 'Internal Server Error',
|
||||
502: 'Bad Gateway',
|
||||
503: 'Service Unavailable',
|
||||
...options.errors,
|
||||
}
|
||||
|
||||
const error = errors[result.status];
|
||||
if (error) {
|
||||
throw new ApiError(options, result, error);
|
||||
}
|
||||
|
||||
if (!result.ok) {
|
||||
throw new ApiError(options, result, 'Generic Error');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Request method
|
||||
* @param config The OpenAPI configuration object
|
||||
* @param options The request options from the service
|
||||
* @returns CancelablePromise<T>
|
||||
* @throws ApiError
|
||||
*/
|
||||
export const request = <T>(config: OpenAPIConfig, options: ApiRequestOptions): CancelablePromise<T> => {
|
||||
return new CancelablePromise(async (resolve, reject, onCancel) => {
|
||||
try {
|
||||
const url = getUrl(config, options);
|
||||
const formData = getFormData(options);
|
||||
const body = getRequestBody(options);
|
||||
const headers = await getHeaders(config, options);
|
||||
|
||||
if (!onCancel.isCancelled) {
|
||||
const response = await sendRequest(config, options, url, body, formData, headers, onCancel);
|
||||
const responseBody = await getResponseBody(response);
|
||||
const responseHeader = getResponseHeader(response, options.responseHeader);
|
||||
|
||||
const result: ApiResult = {
|
||||
url,
|
||||
ok: response.ok,
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
body: responseHeader ?? responseBody,
|
||||
};
|
||||
|
||||
catchErrorCodes(options, result);
|
||||
|
||||
resolve(result.body);
|
||||
}
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
};
|
18
src/generated/openapi/index.ts
Normal file
18
src/generated/openapi/index.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
export { ApiError } from './core/ApiError';
|
||||
export { CancelablePromise, CancelError } from './core/CancelablePromise';
|
||||
export { OpenAPI } from './core/OpenAPI';
|
||||
export type { OpenAPIConfig } from './core/OpenAPI';
|
||||
|
||||
export type { Budget } from './models/Budget';
|
||||
export type { BudgetId } from './models/BudgetId';
|
||||
export type { BudgetSummary } from './models/BudgetSummary';
|
||||
export type { NewBudget } from './models/NewBudget';
|
||||
export type { NewTransaction } from './models/NewTransaction';
|
||||
export type { Transaction } from './models/Transaction';
|
||||
export type { TransactionId } from './models/TransactionId';
|
||||
|
||||
export { BudgetsService } from './services/BudgetsService';
|
||||
export { TransactionsService } from './services/TransactionsService';
|
33
src/generated/openapi/models/Budget.ts
Normal file
33
src/generated/openapi/models/Budget.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
import type { BudgetId } from './BudgetId';
|
||||
|
||||
/**
|
||||
* Planned money to be available for tracking expenses related to a certain purpose over a period of time
|
||||
*/
|
||||
export type Budget = {
|
||||
id: BudgetId;
|
||||
/**
|
||||
* The name of the budget
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The total amount available for the budget
|
||||
*/
|
||||
limit: number;
|
||||
/**
|
||||
* The summed up spent amount of all transaction of that budget
|
||||
*/
|
||||
spent: number;
|
||||
/**
|
||||
* Date marking the start of the budget period
|
||||
*/
|
||||
startDate?: string;
|
||||
/**
|
||||
* Date marking the end of the budget period
|
||||
*/
|
||||
endDate?: string;
|
||||
};
|
||||
|
8
src/generated/openapi/models/BudgetId.ts
Normal file
8
src/generated/openapi/models/BudgetId.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* Unique identifier of one budget
|
||||
*/
|
||||
export type BudgetId = string;
|
34
src/generated/openapi/models/BudgetSummary.ts
Normal file
34
src/generated/openapi/models/BudgetSummary.ts
Normal file
|
@ -0,0 +1,34 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
import type { BudgetId } from './BudgetId';
|
||||
import type { Transaction } from './Transaction';
|
||||
|
||||
/**
|
||||
* Summary of the whole budget including all transactions
|
||||
*/
|
||||
export type BudgetSummary = {
|
||||
id: BudgetId;
|
||||
/**
|
||||
* The name of the budget
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The total amount available for the budget
|
||||
*/
|
||||
limit: number;
|
||||
/**
|
||||
* Date marking the start of the budget period
|
||||
*/
|
||||
startDate?: string;
|
||||
/**
|
||||
* Date marking the end of the budget period
|
||||
*/
|
||||
endDate?: string;
|
||||
/**
|
||||
* All transactions of the budget
|
||||
*/
|
||||
transactions: Array<Transaction>;
|
||||
};
|
||||
|
26
src/generated/openapi/models/NewBudget.ts
Normal file
26
src/generated/openapi/models/NewBudget.ts
Normal file
|
@ -0,0 +1,26 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* Budget to be created
|
||||
*/
|
||||
export type NewBudget = {
|
||||
/**
|
||||
* The name of the budget
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* The total amount available for the budget
|
||||
*/
|
||||
limit: number;
|
||||
/**
|
||||
* Date marking the start of the budget period
|
||||
*/
|
||||
startDate?: string;
|
||||
/**
|
||||
* Date marking the end of the budget period
|
||||
*/
|
||||
endDate?: string;
|
||||
};
|
||||
|
19
src/generated/openapi/models/NewTransaction.ts
Normal file
19
src/generated/openapi/models/NewTransaction.ts
Normal file
|
@ -0,0 +1,19 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* A new transaction to be created
|
||||
*/
|
||||
export type NewTransaction = {
|
||||
/**
|
||||
* Description of the transaction
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Amount of money contained in the transaction
|
||||
*/
|
||||
amount: number;
|
||||
date: string;
|
||||
};
|
||||
|
22
src/generated/openapi/models/Transaction.ts
Normal file
22
src/generated/openapi/models/Transaction.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
import type { TransactionId } from './TransactionId';
|
||||
|
||||
/**
|
||||
* An expense or income related to a single budget
|
||||
*/
|
||||
export type Transaction = {
|
||||
id: TransactionId;
|
||||
/**
|
||||
* Description of the transaction
|
||||
*/
|
||||
description: string;
|
||||
/**
|
||||
* Amount of money contained in the transaction
|
||||
*/
|
||||
amount: number;
|
||||
date: string;
|
||||
};
|
||||
|
8
src/generated/openapi/models/TransactionId.ts
Normal file
8
src/generated/openapi/models/TransactionId.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
|
||||
/**
|
||||
* Unique identifier of a transaction
|
||||
*/
|
||||
export type TransactionId = string;
|
89
src/generated/openapi/services/BudgetsService.ts
Normal file
89
src/generated/openapi/services/BudgetsService.ts
Normal file
|
@ -0,0 +1,89 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { Budget } from '../models/Budget';
|
||||
import type { BudgetId } from '../models/BudgetId';
|
||||
import type { BudgetSummary } from '../models/BudgetSummary';
|
||||
import type { NewBudget } from '../models/NewBudget';
|
||||
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
|
||||
export class BudgetsService {
|
||||
|
||||
/**
|
||||
* Create a new budget
|
||||
* @param requestBody
|
||||
* @returns Budget Budget created
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createBudget(
|
||||
requestBody: NewBudget,
|
||||
): CancelablePromise<Budget> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/budgets',
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
errors: {
|
||||
400: `Invalid budget`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all budgets
|
||||
* @returns Budget OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getBudgets(): CancelablePromise<Array<Budget>> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/budgets',
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a budget summary including all transactions for a given ID
|
||||
* @param budgetId The UUID of the budget
|
||||
* @returns BudgetSummary OK
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static getBudgetSummary(
|
||||
budgetId: BudgetId,
|
||||
): CancelablePromise<BudgetSummary> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'GET',
|
||||
url: '/budgets/{budgetId}/summary',
|
||||
path: {
|
||||
'budgetId': budgetId,
|
||||
},
|
||||
errors: {
|
||||
404: `Budget not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a budget including all transactions
|
||||
* @param budgetId The UUID of the budget
|
||||
* @returns void
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static deleteBudget(
|
||||
budgetId: BudgetId,
|
||||
): CancelablePromise<void> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'DELETE',
|
||||
url: '/budgets/{budgetId}',
|
||||
path: {
|
||||
'budgetId': budgetId,
|
||||
},
|
||||
errors: {
|
||||
404: `Budget not found`,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
}
|
36
src/generated/openapi/services/TransactionsService.ts
Normal file
36
src/generated/openapi/services/TransactionsService.ts
Normal file
|
@ -0,0 +1,36 @@
|
|||
/* istanbul ignore file */
|
||||
/* tslint:disable */
|
||||
/* eslint-disable */
|
||||
import type { BudgetId } from '../models/BudgetId';
|
||||
import type { NewTransaction } from '../models/NewTransaction';
|
||||
import type { Transaction } from '../models/Transaction';
|
||||
|
||||
import type { CancelablePromise } from '../core/CancelablePromise';
|
||||
import { OpenAPI } from '../core/OpenAPI';
|
||||
import { request as __request } from '../core/request';
|
||||
|
||||
export class TransactionsService {
|
||||
|
||||
/**
|
||||
* Register a new transaction for a budget
|
||||
* @param budgetId The UUID of the budget
|
||||
* @param requestBody
|
||||
* @returns Transaction Transaction created
|
||||
* @throws ApiError
|
||||
*/
|
||||
public static createTransaction(
|
||||
budgetId: BudgetId,
|
||||
requestBody: NewTransaction,
|
||||
): CancelablePromise<Transaction> {
|
||||
return __request(OpenAPI, {
|
||||
method: 'POST',
|
||||
url: '/budgets/{budgetId}/transactions',
|
||||
path: {
|
||||
'budgetId': budgetId,
|
||||
},
|
||||
body: requestBody,
|
||||
mediaType: 'application/json',
|
||||
});
|
||||
}
|
||||
|
||||
}
|
12
src/hooks/useBudget.ts
Normal file
12
src/hooks/useBudget.ts
Normal file
|
@ -0,0 +1,12 @@
|
|||
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||
|
||||
import { BudgetsService, BudgetSummary } from '../generated/openapi';
|
||||
|
||||
const queryKey = Symbol('getBudget');
|
||||
|
||||
export const useBudget = (id: string): UseQueryResult<BudgetSummary> => {
|
||||
return useQuery({
|
||||
queryKey: [queryKey, id],
|
||||
queryFn: () => BudgetsService.getBudgetSummary(id),
|
||||
});
|
||||
};
|
11
src/hooks/useBudgets.ts
Normal file
11
src/hooks/useBudgets.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
import { useQuery, UseQueryResult } from '@tanstack/react-query';
|
||||
|
||||
import { Budget, BudgetsService } from '../generated/openapi';
|
||||
|
||||
const budgetSymbol = Symbol('getBudgets');
|
||||
export const useBudgets = (): UseQueryResult<Budget[]> => {
|
||||
return useQuery({
|
||||
queryKey: [budgetSymbol],
|
||||
queryFn: () => BudgetsService.getBudgets(),
|
||||
});
|
||||
};
|
|
@ -1,6 +1,23 @@
|
|||
import '@/styles/globals.css'
|
||||
import type { AppProps } from 'next/app'
|
||||
import '../styles/globals.css';
|
||||
import { QueryClient } from '@tanstack/query-core';
|
||||
import { QueryClientProvider } from '@tanstack/react-query';
|
||||
import { Inter } from 'next/font/google';
|
||||
import React from 'react';
|
||||
|
||||
import { Navigation } from '../components/Navigation/Navigation';
|
||||
|
||||
import type { AppProps } from 'next/app';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] });
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
export default function App({ Component, pageProps }: AppProps) {
|
||||
return <Component {...pageProps} />
|
||||
return (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<main className={inter.className}>
|
||||
<Navigation />
|
||||
<Component {...pageProps} />
|
||||
</main>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
import { Html, Head, Main, NextScript } from 'next/document'
|
||||
import { Html, Head, Main, NextScript } from 'next/document';
|
||||
import React from 'react';
|
||||
|
||||
export default function Document() {
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
)
|
||||
return (
|
||||
<Html lang="en">
|
||||
<Head />
|
||||
<body>
|
||||
<Main />
|
||||
<NextScript />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
|
|
11
src/pages/api/budgets.ts
Normal file
11
src/pages/api/budgets.ts
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import { budgetFactory } from './factories/buget.factories';
|
||||
import { Budget } from '../../generated/openapi';
|
||||
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
const budgetList = budgetFactory.buildList(5);
|
||||
|
||||
export default function handler(_req: NextApiRequest, res: NextApiResponse<Budget[]>) {
|
||||
res.status(200).json(budgetList);
|
||||
}
|
18
src/pages/api/factories/buget.factories.ts
Normal file
18
src/pages/api/factories/buget.factories.ts
Normal file
|
@ -0,0 +1,18 @@
|
|||
import { randomUUID } from 'crypto';
|
||||
|
||||
import { Factory } from 'fishery';
|
||||
|
||||
import { Budget } from '../../../generated/openapi';
|
||||
|
||||
export const budgetFactory = Factory.define<Budget>(({ sequence }) => {
|
||||
const limit = 200 + (sequence - 1) * 50;
|
||||
|
||||
return {
|
||||
id: randomUUID(),
|
||||
startDate: '2023-02-21T18:49:13.620Z',
|
||||
endDate: '2023-03-21T18:49:13.620Z',
|
||||
name: 'Budget-' + sequence,
|
||||
limit,
|
||||
spent: limit * Math.random(),
|
||||
};
|
||||
});
|
|
@ -1,13 +1,10 @@
|
|||
// Next.js API route support: https://nextjs.org/docs/api-routes/introduction
|
||||
import type { NextApiRequest, NextApiResponse } from 'next'
|
||||
import type { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
type Data = {
|
||||
name: string
|
||||
}
|
||||
name: string;
|
||||
};
|
||||
|
||||
export default function handler(
|
||||
req: NextApiRequest,
|
||||
res: NextApiResponse<Data>
|
||||
) {
|
||||
res.status(200).json({ name: 'John Doe' })
|
||||
export default function handler(_req: NextApiRequest, res: NextApiResponse<Data>) {
|
||||
res.status(200).json({ name: 'John Doe' });
|
||||
}
|
||||
|
|
20
src/pages/budgets/[id].tsx
Normal file
20
src/pages/budgets/[id].tsx
Normal file
|
@ -0,0 +1,20 @@
|
|||
import is from '@sindresorhus/is';
|
||||
import { useRouter } from 'next/router';
|
||||
|
||||
import { BudgetDetail } from '../../components/BudgetDetail/BudgetDetail';
|
||||
|
||||
export default function BudgetPage() {
|
||||
const router = useRouter();
|
||||
|
||||
const { id } = router.query;
|
||||
|
||||
if (is.nonEmptyString(id)) {
|
||||
return (
|
||||
<>
|
||||
<BudgetDetail id={id} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
|
@ -1,19 +1,20 @@
|
|||
import Head from 'next/head'
|
||||
import { Inter } from 'next/font/google'
|
||||
import {Navigation} from "@/components/Navigation";
|
||||
import Head from 'next/head';
|
||||
import React from 'react';
|
||||
|
||||
const inter = Inter({ subsets: ['latin'] })
|
||||
import { BudgetList } from '../components/BudgetList/BudgetList';
|
||||
|
||||
export default function Home() {
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<Navigation />
|
||||
</>
|
||||
)
|
||||
return (
|
||||
<>
|
||||
<Head>
|
||||
<title>Create Next App</title>
|
||||
<meta name="description" content="Generated by create next app" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
<div style={{ padding: '0 80px' }}>
|
||||
<BudgetList />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -95,6 +95,12 @@ a {
|
|||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
color-scheme: dark;
|
||||
/* color-scheme: dark; */
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 32px;
|
||||
}
|
||||
|
|
|
@ -7,16 +7,15 @@
|
|||
"strict": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"noEmit": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "node",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "preserve",
|
||||
"incremental": true,
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
"incremental": true
|
||||
},
|
||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
|
||||
"exclude": ["node_modules"]
|
||||
|
|
Loading…
Reference in a new issue