mirror of
https://github.com/jcreek/BankStatementAnalyser.git
synced 2026-07-13 02:53:49 +00:00
restart fresh
This commit is contained in:
@@ -1,14 +0,0 @@
|
|||||||
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
|
|
||||||
ARG VARIANT=16-bullseye
|
|
||||||
FROM mcr.microsoft.com/vscode/devcontainers/typescript-node:0-${VARIANT}
|
|
||||||
|
|
||||||
# [Optional] Uncomment this section to install additional OS packages.
|
|
||||||
RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
&& apt-get -y install --no-install-recommends inotify-tools
|
|
||||||
|
|
||||||
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
|
||||||
# ARG EXTRA_NODE_VERSION=10
|
|
||||||
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
|
||||||
|
|
||||||
# [Optional] Uncomment if you want to install more global node packages
|
|
||||||
# RUN su node -c "npm install -g <your-package-list -here>"
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
# [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster
|
|
||||||
ARG VARIANT=16-bullseye
|
|
||||||
FROM mcr.microsoft.com/vscode/devcontainers/javascript-node:0-${VARIANT}
|
|
||||||
|
|
||||||
# Install tslint, typescript. eslint is installed by javascript image
|
|
||||||
ARG NODE_MODULES="tslint-to-eslint-config typescript"
|
|
||||||
COPY library-scripts/meta.env /usr/local/etc/vscode-dev-containers
|
|
||||||
RUN su node -c "umask 0002 && npm install -g ${NODE_MODULES}" \
|
|
||||||
&& npm cache clean --force > /dev/null 2>&1
|
|
||||||
|
|
||||||
# [Optional] Uncomment this section to install additional OS packages.
|
|
||||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
|
||||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
|
||||||
|
|
||||||
# [Optional] Uncomment if you want to install an additional version of node using nvm
|
|
||||||
# ARG EXTRA_NODE_VERSION=10
|
|
||||||
# RUN su node -c "source /usr/local/share/nvm/nvm.sh && nvm install ${EXTRA_NODE_VERSION}"
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
|
||||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.238.0/containers/typescript-node
|
|
||||||
{
|
|
||||||
"name": "Node.js & TypeScript",
|
|
||||||
"build": {
|
|
||||||
"dockerfile": "Dockerfile",
|
|
||||||
// Update 'VARIANT' to pick a Node version: 18, 16, 14.
|
|
||||||
// Append -bullseye or -buster to pin to an OS version.
|
|
||||||
// Use -bullseye variants on local on arm64/Apple Silicon.
|
|
||||||
"args": {
|
|
||||||
"VARIANT": "16-bullseye"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Configure tool-specific properties.
|
|
||||||
"customizations": {
|
|
||||||
// Configure properties specific to VS Code.
|
|
||||||
"vscode": {
|
|
||||||
// Add the IDs of extensions you want installed when the container is created.
|
|
||||||
"extensions": [
|
|
||||||
"formulahendry.auto-rename-tag",
|
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"bierner.github-markdown-preview",
|
|
||||||
"eamodio.gitlens",
|
|
||||||
"xabikos.JavaScriptSnippets",
|
|
||||||
"ritwickdey.LiveServer",
|
|
||||||
"yzhang.markdown-all-in-one",
|
|
||||||
"bierner.markdown-emoji",
|
|
||||||
"bierner.markdown-footnotes",
|
|
||||||
"bierner.markdown-preview-github-styles",
|
|
||||||
"bierner.markdown-yaml-preamble",
|
|
||||||
"DavidAnson.vscode-markdownlint",
|
|
||||||
"PKief.material-icon-theme",
|
|
||||||
"christian-kohler.npm-intellisense",
|
|
||||||
"christian-kohler.path-intellisense",
|
|
||||||
"esbenp.prettier-vscode",
|
|
||||||
"gencer.html-slim-scss-css-class-completion"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
|
||||||
"forwardPorts": [3000],
|
|
||||||
|
|
||||||
// Use 'postCreateCommand' to run commands after the container is created.
|
|
||||||
"postCreateCommand": "npm install",
|
|
||||||
|
|
||||||
// Comment out to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
|
||||||
"remoteUser": "node"
|
|
||||||
}
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
dist
|
|
||||||
node_modules
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
{
|
|
||||||
"env": {
|
|
||||||
"browser": true,
|
|
||||||
"es2021": true
|
|
||||||
},
|
|
||||||
"extends": [
|
|
||||||
"airbnb-base",
|
|
||||||
"plugin:import/errors",
|
|
||||||
"plugin:import/warnings",
|
|
||||||
"plugin:import/typescript"
|
|
||||||
],
|
|
||||||
"parser": "@typescript-eslint/parser",
|
|
||||||
"parserOptions": {
|
|
||||||
"ecmaVersion": "latest",
|
|
||||||
"sourceType": "module"
|
|
||||||
},
|
|
||||||
"plugins": [
|
|
||||||
"@typescript-eslint"
|
|
||||||
],
|
|
||||||
"rules": {
|
|
||||||
"import/extensions": [
|
|
||||||
"error",
|
|
||||||
"ignorePackages",
|
|
||||||
{
|
|
||||||
"js": "never",
|
|
||||||
"jsx": "never",
|
|
||||||
"ts": "never",
|
|
||||||
"tsx": "never"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"quotes": [2, "single", { "avoidEscape": true }]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
* text=auto eol=lf
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
dist
|
|
||||||
node_modules
|
|
||||||
Vendored
-19
@@ -1,19 +0,0 @@
|
|||||||
{
|
|
||||||
"version": "0.2.0",
|
|
||||||
"configurations": [
|
|
||||||
{
|
|
||||||
"name": "Launch via NPM",
|
|
||||||
"request": "launch",
|
|
||||||
"runtimeArgs": [
|
|
||||||
"run",
|
|
||||||
"dev"
|
|
||||||
],
|
|
||||||
"runtimeExecutable": "npm",
|
|
||||||
"skipFiles": [
|
|
||||||
"<node_internals>/**"
|
|
||||||
],
|
|
||||||
"type": "node",
|
|
||||||
"stopOnEntry": true,
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
Generated
-19172
File diff suppressed because it is too large
Load Diff
@@ -1,54 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "BankStatementAnalyser",
|
|
||||||
"version": "1.5.0",
|
|
||||||
"description": "A tiny web app to display graphs and stats from a bank statement/csv/google sheet.",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"build": "webpack --config webpack.prod.js",
|
|
||||||
"dev": "webpack serve --hot --config webpack.dev.js",
|
|
||||||
"lint": "eslint --ignore-path .eslintignore --ext .js,.ts ."
|
|
||||||
},
|
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/jcreek/BankStatementAnalyser"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "jcreek",
|
|
||||||
"license": "GNU General Public License v3.0",
|
|
||||||
"devDependencies": {
|
|
||||||
"@babel/core": "^7.18.5",
|
|
||||||
"@babel/preset-env": "^7.18.2",
|
|
||||||
"@babel/preset-typescript": "^7.17.12",
|
|
||||||
"@typescript-eslint/eslint-plugin": "^5.28.0",
|
|
||||||
"@typescript-eslint/parser": "^5.28.0",
|
|
||||||
"babel-loader": "^8.2.5",
|
|
||||||
"css-loader": "^6.7.1",
|
|
||||||
"eslint": "^8.17.0",
|
|
||||||
"eslint-config-airbnb-base": "^15.0.0",
|
|
||||||
"eslint-plugin-import": "^2.26.0",
|
|
||||||
"file-loader": "^6.2.0",
|
|
||||||
"sass": "^1.52.3",
|
|
||||||
"sass-loader": "^13.0.0",
|
|
||||||
"style-loader": "^3.3.1",
|
|
||||||
"typescript": "^4.7.3",
|
|
||||||
"webpack": "^5.73.0",
|
|
||||||
"webpack-cli": "^4.10.0",
|
|
||||||
"webpack-dev-server": "^4.9.2",
|
|
||||||
"webpack-webmanifest-loader": "^2.0.2",
|
|
||||||
"workbox-cli": "^6.5.4"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@types/chart.js": "^2.9.37",
|
|
||||||
"chart.js": "^4.1.0",
|
|
||||||
"chroma-js": "^2.4.2",
|
|
||||||
"html-webpack-plugin": "^5.5.0",
|
|
||||||
"react-dev-utils": "^12.0.1",
|
|
||||||
"webpack-manifest-plugin": "^5.0.0",
|
|
||||||
"webpack-merge": "^5.8.0",
|
|
||||||
"workbox-navigation-preload": "^6.5.4",
|
|
||||||
"workbox-precaching": "^6.5.4",
|
|
||||||
"workbox-routing": "^6.5.4",
|
|
||||||
"workbox-strategies": "^6.5.4",
|
|
||||||
"workbox-webpack-plugin": "^6.5.4"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 11 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 30 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 10 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 540 B |
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -1,20 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "BankStatementAnalyser",
|
|
||||||
"short_name": "BSA",
|
|
||||||
"icons": [
|
|
||||||
{
|
|
||||||
"src": "android-chrome-192x192.png",
|
|
||||||
"sizes": "192x192",
|
|
||||||
"type": "image/png"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"src": "android-chrome-512x512.png",
|
|
||||||
"sizes": "512x512",
|
|
||||||
"type": "image/png"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"theme_color": "#ffffff",
|
|
||||||
"background_color": "#ffffff",
|
|
||||||
"display": "standalone",
|
|
||||||
"start_url": "/"
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
export const transactions: Array<BankTransaction> = [];
|
|
||||||
Vendored
-4
@@ -1,4 +0,0 @@
|
|||||||
declare module '*.svg' {
|
|
||||||
const content: any;
|
|
||||||
export default content;
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
import { transactions } from "./bankData";
|
|
||||||
import Chart from 'chart.js/auto';
|
|
||||||
import * as chroma from 'chroma-js';
|
|
||||||
|
|
||||||
export default function generateCharts(): void {
|
|
||||||
// const filter: Filter = { year: 2022, category: 'Eating out', excludeIncome: true};
|
|
||||||
const filter: Filter = { year: 2022, excludeIncome: true};
|
|
||||||
// const filter: Filter = { year: 2022, month: 1, excludeIncome: true};
|
|
||||||
const chartData = getChartData(transactions, filter);
|
|
||||||
console.log('chartData', chartData);
|
|
||||||
|
|
||||||
const ctx = document.getElementById('myChart') as HTMLCanvasElement;
|
|
||||||
const myChart = new Chart(ctx, {
|
|
||||||
type: 'bar',
|
|
||||||
data: chartData,
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
stacked: true,
|
|
||||||
},
|
|
||||||
y: {
|
|
||||||
stacked: true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRandomColor(): string {
|
|
||||||
// Generate a random color in hexadecimal format
|
|
||||||
const color = Math.floor(Math.random() * 0x1000000).toString(16);
|
|
||||||
return '#' + ('000000' + color).slice(-6);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getChartData(transactions: BankTransaction[], filters: { year?: number, month?: number, category?: string, excludeIncome?: boolean } | null | undefined): ChartData {
|
|
||||||
// Return an empty chart data object if no filters are provided
|
|
||||||
if (!filters) {
|
|
||||||
return {
|
|
||||||
labels: [],
|
|
||||||
datasets: []
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Generate a color scale using the chroma-js library, with the number of colors equal to the number of categories
|
|
||||||
const categories = new Set<string>();
|
|
||||||
for (const transaction of transactions) {
|
|
||||||
categories.add(transaction.category);
|
|
||||||
}
|
|
||||||
const colorScale = chroma.scale('Spectral').mode('hsl').colors(categories.size);
|
|
||||||
|
|
||||||
// Filter the transactions based on the specified filters
|
|
||||||
const filteredTransactions = transactions.filter(transaction => {
|
|
||||||
if (filters.year && transaction.dateYear !== filters.year) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (filters.month && transaction.dateMonth !== filters.month) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (filters.category && transaction.category !== filters.category) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Group the transactions by year or month, depending on the filters
|
|
||||||
const groupedTransactions: { [key: string]: BankTransaction[] } = {};
|
|
||||||
if (filters.year && !filters.month) {
|
|
||||||
// Group the transactions by month
|
|
||||||
for (const transaction of filteredTransactions) {
|
|
||||||
if (filters.excludeIncome && transaction.amount < 0) {
|
|
||||||
continue; // Skip transactions with a positive amount if excludeIncome is true
|
|
||||||
}
|
|
||||||
const month = `${transaction.dateYear}-${transaction.dateMonth}`;
|
|
||||||
if (!groupedTransactions[month]) {
|
|
||||||
groupedTransactions[month] = [];
|
|
||||||
}
|
|
||||||
groupedTransactions[month].push(transaction);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Group the transactions by year
|
|
||||||
for (const transaction of filteredTransactions) {
|
|
||||||
if (filters.excludeIncome && transaction.amount < 0) {
|
|
||||||
continue; // Skip transactions with a positive amount if excludeIncome is true
|
|
||||||
}
|
|
||||||
const year = transaction.dateYear.toString();
|
|
||||||
if (!groupedTransactions[year]) {
|
|
||||||
groupedTransactions[year] = [];
|
|
||||||
}
|
|
||||||
groupedTransactions[year].push(transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the chart data object
|
|
||||||
const chartData: ChartData = {
|
|
||||||
labels: [],
|
|
||||||
datasets: []
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add a dataset for each category
|
|
||||||
let i = 0;
|
|
||||||
categories.forEach((category: string) => {
|
|
||||||
chartData.datasets.push({
|
|
||||||
label: category,
|
|
||||||
data: [],
|
|
||||||
backgroundColor: colorScale[i % colorScale.length], // Use a color from the color scale,
|
|
||||||
borderColor: 'black',
|
|
||||||
borderWidth: 2
|
|
||||||
});
|
|
||||||
i++;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Populate the chart data with the transaction amounts
|
|
||||||
for (const key of Object.keys(groupedTransactions)) {
|
|
||||||
chartData.labels.push(key);
|
|
||||||
const transactions = groupedTransactions[key];
|
|
||||||
for (let i = 0; i < chartData.datasets.length; i++) {
|
|
||||||
chartData.datasets[i].data.push(0);
|
|
||||||
for (const transaction of transactions) {
|
|
||||||
if (transaction.category === chartData.datasets[i].label) {
|
|
||||||
chartData.datasets[i].data[chartData.labels.length - 1] += transaction.amount;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return chartData;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
import loadServiceWorker from './loadServiceWorker';
|
|
||||||
import loadBankData from './loadBankData';
|
|
||||||
import generateCharts from './generateCharts';
|
|
||||||
|
|
||||||
require('./assets/favicon.ico');
|
|
||||||
require('./assets/android-chrome-192x192.png');
|
|
||||||
require('./assets/android-chrome-512x512.png');
|
|
||||||
require('./assets/apple-touch-icon.png');
|
|
||||||
require('./assets/favicon-16x16.png');
|
|
||||||
require('./assets/favicon-32x32.png');
|
|
||||||
require('./styles/main.scss');
|
|
||||||
require('./styles/buttons.scss');
|
|
||||||
require('./styles/modal.scss');
|
|
||||||
require('./styles/spinner.scss');
|
|
||||||
|
|
||||||
if (localStorage.hasOwnProperty('bankData')) {
|
|
||||||
loadBankData(JSON.parse(localStorage.getItem('bankData')).values);
|
|
||||||
generateCharts();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'production') {
|
|
||||||
loadServiceWorker();
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { transactions } from "./bankData";
|
|
||||||
|
|
||||||
export default function loadBankData(data: Array<any>):void {
|
|
||||||
// Set bankData array to be whatever this new data is
|
|
||||||
for (let index = 0; index < data.length; index++) {
|
|
||||||
const dateParts: Array<string> = data[index][0].split("/");
|
|
||||||
const date: Date = new Date(Number(dateParts[2]), Number(dateParts[1])-1, Number(dateParts[0])); // JS months are zero-indexed
|
|
||||||
const year: number = date.getFullYear();
|
|
||||||
const month: number = date.getMonth();
|
|
||||||
const day: number = date.getDay();
|
|
||||||
|
|
||||||
const transaction: BankTransaction = {
|
|
||||||
date: data[index][0],
|
|
||||||
dateYear: year,
|
|
||||||
dateMonth: month + 1, // JS months are zero-indexed
|
|
||||||
dateDay: day,
|
|
||||||
time: data[index][1],
|
|
||||||
type: data[index][2],
|
|
||||||
name: data[index][3],
|
|
||||||
category: data[index][5],
|
|
||||||
amount: Number(data[index][6]) * -1, // Invert money out being negative and money in being positive
|
|
||||||
currency: data[index][7],
|
|
||||||
localAmount: Number(data[index][8]) * -1, // Invert money out being negative and money in being positive
|
|
||||||
localCurrency: data[index][9],
|
|
||||||
};
|
|
||||||
|
|
||||||
transactions.push(transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
export default function loadServiceWorker() {
|
|
||||||
let deferredPrompt;
|
|
||||||
|
|
||||||
window.addEventListener('beforeinstallprompt', (e) => {
|
|
||||||
const installApp = document.getElementById('install-app-btn-container');
|
|
||||||
|
|
||||||
installApp.addEventListener('click', async () => {
|
|
||||||
if (deferredPrompt !== null) {
|
|
||||||
deferredPrompt.prompt();
|
|
||||||
const { outcome } = await deferredPrompt.userChoice;
|
|
||||||
if (outcome === 'accepted') {
|
|
||||||
deferredPrompt = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('install-app-btn-container').style.display = 'block';
|
|
||||||
deferredPrompt = e;
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('load', () => {
|
|
||||||
navigator.serviceWorker.register('/service-worker.js');
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.matchMedia('(display-mode: standalone)').matches) {
|
|
||||||
document.getElementById('install-app-btn-container').style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
import { precacheAndRoute } from 'workbox-precaching';
|
|
||||||
import * as navigationPreload from 'workbox-navigation-preload';
|
|
||||||
import { NetworkFirst, CacheFirst, StaleWhileRevalidate } from 'workbox-strategies';
|
|
||||||
import { registerRoute, NavigationRoute, Route } from 'workbox-routing';
|
|
||||||
|
|
||||||
// Precache the manifest
|
|
||||||
// eslint-disable-next-line no-underscore-dangle, no-restricted-globals
|
|
||||||
precacheAndRoute(self.__WB_MANIFEST);
|
|
||||||
|
|
||||||
const navigationRoute = new NavigationRoute(new NetworkFirst({
|
|
||||||
cacheName: 'navigations',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const imageAssetRoute = new Route(({ request }) => request.destination === 'image', new CacheFirst({
|
|
||||||
cacheName: 'image-assets',
|
|
||||||
}));
|
|
||||||
|
|
||||||
registerRoute(navigationRoute);
|
|
||||||
registerRoute(imageAssetRoute);
|
|
||||||
|
|
||||||
registerRoute(
|
|
||||||
({ url }) => url.origin === 'https://fonts.googleapis.com',
|
|
||||||
new StaleWhileRevalidate({
|
|
||||||
cacheName: 'google-fonts-stylesheets',
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<!-- Google tag (gtag.js) -->
|
|
||||||
<!-- <script async src="https://www.googletagmanager.com/gtag/js?id=G-EZ2WT7NQMK"></script>
|
|
||||||
<script>
|
|
||||||
window.dataLayer = window.dataLayer || [];
|
|
||||||
function gtag(){dataLayer.push(arguments);}
|
|
||||||
gtag('js', new Date());
|
|
||||||
|
|
||||||
gtag('config', 'G-EZ2WT7NQMK');
|
|
||||||
</script> -->
|
|
||||||
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<meta name="description" content="A tiny web app to display graphs and stats from a bank statement/csv/google sheet.">
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
|
||||||
<link rel="manifest" href="<%= require('../src/assets/manifest.webmanifest') %>" />
|
|
||||||
<link rel="shortcut icon" href="favicon.ico">
|
|
||||||
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,400;0,700;1,400;1,700&display=swap" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<header class="topbar-container">
|
|
||||||
<div class="topbar-items"><h1><a href="/">Bank Statement Analyser</a></h1></div>
|
|
||||||
<p id="install-app-btn-container" class="topbar-items">
|
|
||||||
<button id="install-offline-app" class="button button-black" role="button">
|
|
||||||
<span>Install the app (you can even use it offline!)</span>
|
|
||||||
</button>
|
|
||||||
</p>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<p>Sheets API Quickstart</p>
|
|
||||||
|
|
||||||
<!--Add buttons to initiate auth sequence and sign out-->
|
|
||||||
<button id="authorize_button" onclick="handleAuthClick()">Authorize</button>
|
|
||||||
<button id="signout_button" onclick="handleSignoutClick()">Sign Out</button>
|
|
||||||
|
|
||||||
<pre id="content" style="white-space: pre-wrap;"></pre>
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<canvas id="myChart"></canvas>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<footer class="footer-container">
|
|
||||||
<hr>
|
|
||||||
<span class="footer-items">Copyright © 2022 Josh Creek</span>
|
|
||||||
<!-- <span> | </span> -->
|
|
||||||
<!-- <span class="footer-items"><a href="/privacy-policy.html">Privacy Policy</a></span> -->
|
|
||||||
</footer>
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
/* exported gapiLoaded */
|
|
||||||
/* exported gisLoaded */
|
|
||||||
/* exported handleAuthClick */
|
|
||||||
/* exported handleSignoutClick */
|
|
||||||
|
|
||||||
// TODO(developer): Set to client ID and API key from the Developer Console
|
|
||||||
const CLIENT_ID = '<YOUR_CLIENT_ID>';
|
|
||||||
const API_KEY = '<YOUR_API_KEY>';
|
|
||||||
|
|
||||||
// Discovery doc URL for APIs used by the quickstart
|
|
||||||
const DISCOVERY_DOC = 'https://sheets.googleapis.com/$discovery/rest?version=v4';
|
|
||||||
|
|
||||||
// Authorization scopes required by the API; multiple scopes can be
|
|
||||||
// included, separated by spaces.
|
|
||||||
const SCOPES = 'https://www.googleapis.com/auth/spreadsheets.readonly';
|
|
||||||
|
|
||||||
let tokenClient;
|
|
||||||
let gapiInited = false;
|
|
||||||
let gisInited = false;
|
|
||||||
|
|
||||||
document.getElementById('authorize_button').style.visibility = 'hidden';
|
|
||||||
document.getElementById('signout_button').style.visibility = 'hidden';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback after api.js is loaded.
|
|
||||||
*/
|
|
||||||
function gapiLoaded() {
|
|
||||||
gapi.load('client', intializeGapiClient);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback after the API client is loaded. Loads the
|
|
||||||
* discovery doc to initialize the API.
|
|
||||||
*/
|
|
||||||
async function intializeGapiClient() {
|
|
||||||
await gapi.client.init({
|
|
||||||
apiKey: API_KEY,
|
|
||||||
discoveryDocs: [DISCOVERY_DOC],
|
|
||||||
});
|
|
||||||
gapiInited = true;
|
|
||||||
maybeEnableButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Callback after Google Identity Services are loaded.
|
|
||||||
*/
|
|
||||||
function gisLoaded() {
|
|
||||||
tokenClient = google.accounts.oauth2.initTokenClient({
|
|
||||||
client_id: CLIENT_ID,
|
|
||||||
scope: SCOPES,
|
|
||||||
callback: '', // defined later
|
|
||||||
});
|
|
||||||
gisInited = true;
|
|
||||||
maybeEnableButtons();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enables user interaction after all libraries are loaded.
|
|
||||||
*/
|
|
||||||
function maybeEnableButtons() {
|
|
||||||
if (gapiInited && gisInited && !localStorage.hasOwnProperty('bankData')) {
|
|
||||||
document.getElementById('authorize_button').style.visibility = 'visible';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign in the user upon button click.
|
|
||||||
*/
|
|
||||||
function handleAuthClick() {
|
|
||||||
tokenClient.callback = async (resp) => {
|
|
||||||
if (resp.error !== undefined) {
|
|
||||||
throw (resp);
|
|
||||||
}
|
|
||||||
document.getElementById('signout_button').style.visibility = 'visible';
|
|
||||||
document.getElementById('authorize_button').innerText = 'Refresh Google Sheets data';
|
|
||||||
await listMajors();
|
|
||||||
};
|
|
||||||
|
|
||||||
if (gapi.client.getToken() === null) {
|
|
||||||
// Prompt the user to select a Google Account and ask for consent to share their data
|
|
||||||
// when establishing a new session.
|
|
||||||
tokenClient.requestAccessToken({prompt: 'consent'});
|
|
||||||
} else {
|
|
||||||
// Skip display of account chooser and consent dialog for an existing session.
|
|
||||||
tokenClient.requestAccessToken({prompt: ''});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Sign out the user upon button click.
|
|
||||||
*/
|
|
||||||
function handleSignoutClick() {
|
|
||||||
const token = gapi.client.getToken();
|
|
||||||
if (token !== null) {
|
|
||||||
google.accounts.oauth2.revoke(token.access_token);
|
|
||||||
gapi.client.setToken('');
|
|
||||||
document.getElementById('content').innerText = '';
|
|
||||||
document.getElementById('authorize_button').innerText = 'Authorize';
|
|
||||||
document.getElementById('signout_button').style.visibility = 'hidden';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Print the names and majors of students in a sample spreadsheet:
|
|
||||||
* https://docs.google.com/spreadsheets/d/1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms/edit
|
|
||||||
*/
|
|
||||||
async function listMajors() {
|
|
||||||
let response;
|
|
||||||
try {
|
|
||||||
// Fetch first 10 files
|
|
||||||
response = await gapi.client.sheets.spreadsheets.values.get({
|
|
||||||
spreadsheetId: '1BxiMVs0XRA5nFMdKvBdBZjgmUUqptlbs74OgvE2upms',
|
|
||||||
range: 'Class Data!A2:E',
|
|
||||||
});
|
|
||||||
} catch (err) {
|
|
||||||
document.getElementById('content').innerText = err.message;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const range = response.result;
|
|
||||||
localStorage.setItem('bankData', JSON.stringify(range));
|
|
||||||
if (!range || !range.values || range.values.length == 0) {
|
|
||||||
document.getElementById('content').innerText = 'No values found.';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
<script async defer src="https://apis.google.com/js/api.js" onload="gapiLoaded()"></script>
|
|
||||||
<script async defer src="https://accounts.google.com/gsi/client" onload="gisLoaded()"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
type BankTransaction = {
|
|
||||||
date: string;
|
|
||||||
dateYear: number;
|
|
||||||
dateMonth: number;
|
|
||||||
dateDay: number;
|
|
||||||
time: string;
|
|
||||||
type: string;
|
|
||||||
name: string;
|
|
||||||
// emoji here
|
|
||||||
category: string;
|
|
||||||
amount: number;
|
|
||||||
currency: string;
|
|
||||||
localAmount: number;
|
|
||||||
localCurrency: string; // Actually the foreign currency
|
|
||||||
};
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
type ChartData = {
|
|
||||||
labels: string[];
|
|
||||||
datasets: {
|
|
||||||
label: string;
|
|
||||||
backgroundColor: string;
|
|
||||||
borderColor: string;
|
|
||||||
borderWidth: number;
|
|
||||||
data: number[];
|
|
||||||
}[];
|
|
||||||
};
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
type Filter = {
|
|
||||||
year?: number;
|
|
||||||
month?: number;
|
|
||||||
category?: string;
|
|
||||||
excludeIncome?: boolean;
|
|
||||||
};
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"preserveConstEnums": true,
|
|
||||||
"sourceMap": true
|
|
||||||
},
|
|
||||||
"include": ["src/**/*", "src/custom.d.ts", "src/audio.d.ts"],
|
|
||||||
"exclude": ["node_modules", "**/*.spec.ts"]
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const HtmlWebpackPlugin = require('html-webpack-plugin');
|
|
||||||
const InlineChunkHtmlPlugin = require('react-dev-utils/InlineChunkHtmlPlugin');
|
|
||||||
const { WebpackManifestPlugin } = require('webpack-manifest-plugin');
|
|
||||||
|
|
||||||
const options = {};
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
target: 'web',
|
|
||||||
entry: {
|
|
||||||
bundle: path.resolve(__dirname, 'src/index.ts'),
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
path: path.resolve(__dirname, 'dist'),
|
|
||||||
filename: '[name].[contenthash].js',
|
|
||||||
clean: true,
|
|
||||||
assetModuleFilename: '[name].[contenthash][ext]',
|
|
||||||
publicPath: '',
|
|
||||||
},
|
|
||||||
watchOptions: { poll: true },
|
|
||||||
module: {
|
|
||||||
rules: [
|
|
||||||
{
|
|
||||||
// Use these loaders for any matching scss file types
|
|
||||||
test: /\.scss$/,
|
|
||||||
use: [
|
|
||||||
'style-loader',
|
|
||||||
'css-loader',
|
|
||||||
'sass-loader',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Add backwards compatibility
|
|
||||||
test: /\.(js|ts)$/,
|
|
||||||
exclude: /node_modules/,
|
|
||||||
use: {
|
|
||||||
loader: 'babel-loader',
|
|
||||||
options: {
|
|
||||||
presets: ['@babel/preset-env', '@babel/preset-typescript'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Add support for images
|
|
||||||
test: /\.(png|jpg|jpeg|gif)$/i,
|
|
||||||
type: 'asset/resource',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Add support for favicon
|
|
||||||
test: /\.ico$/,
|
|
||||||
use: [
|
|
||||||
{
|
|
||||||
loader: 'file-loader',
|
|
||||||
options: {
|
|
||||||
name: '[name].[ext]', // <-- retain original file name
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Add support for manifest
|
|
||||||
test: /\.webmanifest$/i,
|
|
||||||
use: 'webpack-webmanifest-loader',
|
|
||||||
type: 'asset/resource',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Add support for audio
|
|
||||||
test: /\.(mp3)$/i,
|
|
||||||
type: 'asset/resource',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// Add inlining of SVG
|
|
||||||
test: /\.svg$/i,
|
|
||||||
type: 'asset/inline',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
resolve: {
|
|
||||||
// Specify the order in which to resolve files by their extension
|
|
||||||
extensions: ['*', '.js', '.ts'],
|
|
||||||
},
|
|
||||||
plugins: [
|
|
||||||
new HtmlWebpackPlugin({
|
|
||||||
inject: 'body',
|
|
||||||
title: 'Bank Statement Analyser',
|
|
||||||
filename: 'index.html',
|
|
||||||
template: 'src/template.html',
|
|
||||||
}),
|
|
||||||
new InlineChunkHtmlPlugin(HtmlWebpackPlugin, [/.*/]),
|
|
||||||
new WebpackManifestPlugin(options),
|
|
||||||
],
|
|
||||||
};
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
const path = require('path');
|
|
||||||
const { merge } = require('webpack-merge');
|
|
||||||
const common = require('./webpack.common');
|
|
||||||
|
|
||||||
module.exports = merge(common, {
|
|
||||||
mode: 'development',
|
|
||||||
devtool: 'inline-source-map',
|
|
||||||
devServer: {
|
|
||||||
static: {
|
|
||||||
directory: path.resolve(__dirname, 'dist'),
|
|
||||||
},
|
|
||||||
port: 3000,
|
|
||||||
open: true,
|
|
||||||
hot: true,
|
|
||||||
compress: true,
|
|
||||||
historyApiFallback: true,
|
|
||||||
devMiddleware: {
|
|
||||||
index: true,
|
|
||||||
mimeTypes: { phtml: 'text/html' },
|
|
||||||
writeToDisk: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
const { merge } = require('webpack-merge');
|
|
||||||
const { InjectManifest } = require('workbox-webpack-plugin');
|
|
||||||
const common = require('./webpack.common');
|
|
||||||
|
|
||||||
common.plugins.push(new InjectManifest({
|
|
||||||
swSrc: './src/serviceWorker.js',
|
|
||||||
swDest: 'service-worker.js',
|
|
||||||
maximumFileSizeToCacheInBytes: 50000000,
|
|
||||||
}));
|
|
||||||
|
|
||||||
module.exports = merge(common, {
|
|
||||||
mode: 'production',
|
|
||||||
devtool: 'source-map',
|
|
||||||
performance: {
|
|
||||||
hints: false,
|
|
||||||
maxEntrypointSize: 512000,
|
|
||||||
maxAssetSize: 512000,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
Reference in New Issue
Block a user