Initial commit.

This commit is contained in:
Steve Schafer 2025-07-13 10:50:17 -06:00
parent 1ce1e4ccfc
commit 63f36a7ab0
21 changed files with 31615 additions and 2 deletions

27
.gitignore vendored Normal file
View file

@ -0,0 +1,27 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
*.sublime-workspace
*.log
pid

View file

@ -1,3 +1,70 @@
# com.stephenschafer.budget.ui
# Getting Started with Create React App
React budget app
This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
## Available Scripts
In the project directory, you can run:
### `npm start`
Runs the app in the development mode.\
Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
The page will reload if you make edits.\
You will also see any lint errors in the console.
### `npm test`
Launches the test runner in the interactive watch mode.\
See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information.
### `npm run build`
Builds the app for production to the `build` folder.\
It correctly bundles React in production mode and optimizes the build for the best performance.
The build is minified and the filenames include the hashes.\
Your app is ready to be deployed!
See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information.
### `npm run eject`
**Note: this is a one-way operation. Once you `eject`, you cant go back!**
If you arent satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project.
Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point youre on your own.
You dont have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldnt feel obligated to use this feature. However we understand that this tool wouldnt be useful if you couldnt customize it when you are ready for it.
## Learn More
You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started).
To learn React, check out the [React documentation](https://reactjs.org/).
### Code Splitting
This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting)
### Analyzing the Bundle Size
This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size)
### Making a Progressive Web App
This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app)
### Advanced Configuration
This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration)
### Deployment
This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment)
### `npm run build` fails to minify
This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify)

5
deploy Executable file
View file

@ -0,0 +1,5 @@
#!/bin/sh
npm run build > build.log 2> build.err.log
zip -r build.zip build
mv build.zip /tmp
echo "build.zip is in /tmp. Switch to elephant to deploy it."

425
install_nvm.sh Normal file
View file

@ -0,0 +1,425 @@
#!/usr/bin/env bash
{ # this ensures the entire script is downloaded #
nvm_has() {
type "$1" > /dev/null 2>&1
}
nvm_default_install_dir() {
[ -z "${XDG_CONFIG_HOME-}" ] && printf %s "${HOME}/.nvm" || printf %s "${XDG_CONFIG_HOME}/nvm"
}
nvm_install_dir() {
if [ -n "$NVM_DIR" ]; then
printf %s "${NVM_DIR}"
else
nvm_default_install_dir
fi
}
nvm_latest_version() {
echo "v0.35.0"
}
nvm_profile_is_bash_or_zsh() {
local TEST_PROFILE
TEST_PROFILE="${1-}"
case "${TEST_PROFILE-}" in
*"/.bashrc" | *"/.bash_profile" | *"/.zshrc")
return
;;
*)
return 1
;;
esac
}
#
# Outputs the location to NVM depending on:
# * The availability of $NVM_SOURCE
# * The method used ("script" or "git" in the script, defaults to "git")
# NVM_SOURCE always takes precedence unless the method is "script-nvm-exec"
#
nvm_source() {
local NVM_METHOD
NVM_METHOD="$1"
local NVM_SOURCE_URL
NVM_SOURCE_URL="$NVM_SOURCE"
if [ "_$NVM_METHOD" = "_script-nvm-exec" ]; then
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/nvm-exec"
elif [ "_$NVM_METHOD" = "_script-nvm-bash-completion" ]; then
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/bash_completion"
elif [ -z "$NVM_SOURCE_URL" ]; then
if [ "_$NVM_METHOD" = "_script" ]; then
NVM_SOURCE_URL="https://raw.githubusercontent.com/nvm-sh/nvm/$(nvm_latest_version)/nvm.sh"
elif [ "_$NVM_METHOD" = "_git" ] || [ -z "$NVM_METHOD" ]; then
NVM_SOURCE_URL="https://github.com/nvm-sh/nvm.git"
else
echo >&2 "Unexpected value \"$NVM_METHOD\" for \$NVM_METHOD"
return 1
fi
fi
echo "$NVM_SOURCE_URL"
}
#
# Node.js version to install
#
nvm_node_version() {
echo "$NODE_VERSION"
}
nvm_download() {
if nvm_has "curl"; then
curl --compressed -q "$@"
elif nvm_has "wget"; then
# Emulate curl with wget
ARGS=$(echo "$*" | command sed -e 's/--progress-bar /--progress=bar /' \
-e 's/-L //' \
-e 's/--compressed //' \
-e 's/-I /--server-response /' \
-e 's/-s /-q /' \
-e 's/-o /-O /' \
-e 's/-C - /-c /')
# shellcheck disable=SC2086
eval wget $ARGS
fi
}
install_nvm_from_git() {
local INSTALL_DIR
INSTALL_DIR="$(nvm_install_dir)"
if [ -d "$INSTALL_DIR/.git" ]; then
echo "=> nvm is already installed in $INSTALL_DIR, trying to update using git"
command printf '\r=> '
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" fetch origin tag "$(nvm_latest_version)" --depth=1 2> /dev/null || {
echo >&2 "Failed to update nvm, run 'git fetch' in $INSTALL_DIR yourself."
exit 1
}
else
# Cloning to $INSTALL_DIR
echo "=> Downloading nvm from git to '$INSTALL_DIR'"
command printf '\r=> '
mkdir -p "${INSTALL_DIR}"
if [ "$(ls -A "${INSTALL_DIR}")" ]; then
command git init "${INSTALL_DIR}" || {
echo >&2 'Failed to initialize nvm repo. Please report this!'
exit 2
}
command git --git-dir="${INSTALL_DIR}/.git" remote add origin "$(nvm_source)" 2> /dev/null \
|| command git --git-dir="${INSTALL_DIR}/.git" remote set-url origin "$(nvm_source)" || {
echo >&2 'Failed to add remote "origin" (or set the URL). Please report this!'
exit 2
}
command git --git-dir="${INSTALL_DIR}/.git" fetch origin tag "$(nvm_latest_version)" --depth=1 || {
echo >&2 'Failed to fetch origin with tags. Please report this!'
exit 2
}
else
command git -c advice.detachedHead=false clone "$(nvm_source)" -b "$(nvm_latest_version)" --depth=1 "${INSTALL_DIR}" || {
echo >&2 'Failed to clone nvm repo. Please report this!'
exit 2
}
fi
fi
command git -c advice.detachedHead=false --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" checkout -f --quiet "$(nvm_latest_version)"
if [ -n "$(command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" show-ref refs/heads/master)" ]; then
if command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet 2>/dev/null; then
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch --quiet -D master >/dev/null 2>&1
else
echo >&2 "Your version of git is out of date. Please update it!"
command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" branch -D master >/dev/null 2>&1
fi
fi
echo "=> Compressing and cleaning up git repository"
if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" reflog expire --expire=now --all; then
echo >&2 "Your version of git is out of date. Please update it!"
fi
if ! command git --git-dir="$INSTALL_DIR"/.git --work-tree="$INSTALL_DIR" gc --auto --aggressive --prune=now ; then
echo >&2 "Your version of git is out of date. Please update it!"
fi
return
}
#
# Automatically install Node.js
#
nvm_install_node() {
local NODE_VERSION_LOCAL
NODE_VERSION_LOCAL="$(nvm_node_version)"
if [ -z "$NODE_VERSION_LOCAL" ]; then
return 0
fi
echo "=> Installing Node.js version $NODE_VERSION_LOCAL"
nvm install "$NODE_VERSION_LOCAL"
local CURRENT_NVM_NODE
CURRENT_NVM_NODE="$(nvm_version current)"
if [ "$(nvm_version "$NODE_VERSION_LOCAL")" == "$CURRENT_NVM_NODE" ]; then
echo "=> Node.js version $NODE_VERSION_LOCAL has been successfully installed"
else
echo >&2 "Failed to install Node.js $NODE_VERSION_LOCAL"
fi
}
install_nvm_as_script() {
local INSTALL_DIR
INSTALL_DIR="$(nvm_install_dir)"
local NVM_SOURCE_LOCAL
NVM_SOURCE_LOCAL="$(nvm_source script)"
local NVM_EXEC_SOURCE
NVM_EXEC_SOURCE="$(nvm_source script-nvm-exec)"
local NVM_BASH_COMPLETION_SOURCE
NVM_BASH_COMPLETION_SOURCE="$(nvm_source script-nvm-bash-completion)"
# Downloading to $INSTALL_DIR
mkdir -p "$INSTALL_DIR"
if [ -f "$INSTALL_DIR/nvm.sh" ]; then
echo "=> nvm is already installed in $INSTALL_DIR, trying to update the script"
else
echo "=> Downloading nvm as script to '$INSTALL_DIR'"
fi
nvm_download -s "$NVM_SOURCE_LOCAL" -o "$INSTALL_DIR/nvm.sh" || {
echo >&2 "Failed to download '$NVM_SOURCE_LOCAL'"
return 1
} &
nvm_download -s "$NVM_EXEC_SOURCE" -o "$INSTALL_DIR/nvm-exec" || {
echo >&2 "Failed to download '$NVM_EXEC_SOURCE'"
return 2
} &
nvm_download -s "$NVM_BASH_COMPLETION_SOURCE" -o "$INSTALL_DIR/bash_completion" || {
echo >&2 "Failed to download '$NVM_BASH_COMPLETION_SOURCE'"
return 2
} &
for job in $(jobs -p | command sort)
do
wait "$job" || return $?
done
chmod a+x "$INSTALL_DIR/nvm-exec" || {
echo >&2 "Failed to mark '$INSTALL_DIR/nvm-exec' as executable"
return 3
}
}
nvm_try_profile() {
if [ -z "${1-}" ] || [ ! -f "${1}" ]; then
return 1
fi
echo "${1}"
}
#
# Detect profile file if not specified as environment variable
# (eg: PROFILE=~/.myprofile)
# The echo'ed path is guaranteed to be an existing file
# Otherwise, an empty string is returned
#
nvm_detect_profile() {
if [ "${PROFILE-}" = '/dev/null' ]; then
# the user has specifically requested NOT to have nvm touch their profile
return
fi
if [ -n "${PROFILE}" ] && [ -f "${PROFILE}" ]; then
echo "${PROFILE}"
return
fi
local DETECTED_PROFILE
DETECTED_PROFILE=''
if [ -n "${BASH_VERSION-}" ]; then
if [ -f "$HOME/.bashrc" ]; then
DETECTED_PROFILE="$HOME/.bashrc"
elif [ -f "$HOME/.bash_profile" ]; then
DETECTED_PROFILE="$HOME/.bash_profile"
fi
elif [ -n "${ZSH_VERSION-}" ]; then
DETECTED_PROFILE="$HOME/.zshrc"
fi
if [ -z "$DETECTED_PROFILE" ]; then
for EACH_PROFILE in ".profile" ".bashrc" ".bash_profile" ".zshrc"
do
if DETECTED_PROFILE="$(nvm_try_profile "${HOME}/${EACH_PROFILE}")"; then
break
fi
done
fi
if [ -n "$DETECTED_PROFILE" ]; then
echo "$DETECTED_PROFILE"
fi
}
#
# Check whether the user has any globally-installed npm modules in their system
# Node, and warn them if so.
#
nvm_check_global_modules() {
command -v npm >/dev/null 2>&1 || return 0
local NPM_VERSION
NPM_VERSION="$(npm --version)"
NPM_VERSION="${NPM_VERSION:--1}"
[ "${NPM_VERSION%%[!-0-9]*}" -gt 0 ] || return 0
local NPM_GLOBAL_MODULES
NPM_GLOBAL_MODULES="$(
npm list -g --depth=0 |
command sed -e '/ npm@/d' -e '/ (empty)$/d'
)"
local MODULE_COUNT
MODULE_COUNT="$(
command printf %s\\n "$NPM_GLOBAL_MODULES" |
command sed -ne '1!p' | # Remove the first line
wc -l | command tr -d ' ' # Count entries
)"
if [ "${MODULE_COUNT}" != '0' ]; then
# shellcheck disable=SC2016
echo '=> You currently have modules installed globally with `npm`. These will no'
# shellcheck disable=SC2016
echo '=> longer be linked to the active version of Node when you install a new node'
# shellcheck disable=SC2016
echo '=> with `nvm`; and they may (depending on how you construct your `$PATH`)'
# shellcheck disable=SC2016
echo '=> override the binaries of modules installed with `nvm`:'
echo
command printf %s\\n "$NPM_GLOBAL_MODULES"
echo '=> If you wish to uninstall them at a later point (or re-install them under your'
# shellcheck disable=SC2016
echo '=> `nvm` Nodes), you can remove them from the system Node as follows:'
echo
echo ' $ nvm use system'
echo ' $ npm uninstall -g a_module'
echo
fi
}
nvm_do_install() {
if [ -n "${NVM_DIR-}" ] && ! [ -d "${NVM_DIR}" ]; then
if [ -e "${NVM_DIR}" ]; then
echo >&2 "File \"${NVM_DIR}\" has the same name as installation directory."
exit 1
fi
if [ "${NVM_DIR}" = "$(nvm_default_install_dir)" ]; then
mkdir "${NVM_DIR}"
else
echo >&2 "You have \$NVM_DIR set to \"${NVM_DIR}\", but that directory does not exist. Check your profile files and environment."
exit 1
fi
fi
if [ -z "${METHOD}" ]; then
# Autodetect install method
if nvm_has git; then
install_nvm_from_git
elif nvm_has nvm_download; then
install_nvm_as_script
else
echo >&2 'You need git, curl, or wget to install nvm'
exit 1
fi
elif [ "${METHOD}" = 'git' ]; then
if ! nvm_has git; then
echo >&2 "You need git to install nvm"
exit 1
fi
install_nvm_from_git
elif [ "${METHOD}" = 'script' ]; then
if ! nvm_has nvm_download; then
echo >&2 "You need curl or wget to install nvm"
exit 1
fi
install_nvm_as_script
else
echo >&2 "The environment variable \$METHOD is set to \"${METHOD}\", which is not recognized as a valid installation method."
exit 1
fi
echo
local NVM_PROFILE
NVM_PROFILE="$(nvm_detect_profile)"
local PROFILE_INSTALL_DIR
PROFILE_INSTALL_DIR="$(nvm_install_dir | command sed "s:^$HOME:\$HOME:")"
SOURCE_STR="\\nexport NVM_DIR=\"${PROFILE_INSTALL_DIR}\"\\n[ -s \"\$NVM_DIR/nvm.sh\" ] && \\. \"\$NVM_DIR/nvm.sh\" # This loads nvm\\n"
# shellcheck disable=SC2016
COMPLETION_STR='[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion\n'
BASH_OR_ZSH=false
if [ -z "${NVM_PROFILE-}" ] ; then
local TRIED_PROFILE
if [ -n "${PROFILE}" ]; then
TRIED_PROFILE="${NVM_PROFILE} (as defined in \$PROFILE), "
fi
echo "=> Profile not found. Tried ${TRIED_PROFILE-}~/.bashrc, ~/.bash_profile, ~/.zshrc, and ~/.profile."
echo "=> Create one of them and run this script again"
echo " OR"
echo "=> Append the following lines to the correct file yourself:"
command printf "${SOURCE_STR}"
echo
else
if nvm_profile_is_bash_or_zsh "${NVM_PROFILE-}"; then
BASH_OR_ZSH=true
fi
if ! command grep -qc '/nvm.sh' "$NVM_PROFILE"; then
echo "=> Appending nvm source string to $NVM_PROFILE"
command printf "${SOURCE_STR}" >> "$NVM_PROFILE"
else
echo "=> nvm source string already in ${NVM_PROFILE}"
fi
# shellcheck disable=SC2016
if ${BASH_OR_ZSH} && ! command grep -qc '$NVM_DIR/bash_completion' "$NVM_PROFILE"; then
echo "=> Appending bash_completion source string to $NVM_PROFILE"
command printf "$COMPLETION_STR" >> "$NVM_PROFILE"
else
echo "=> bash_completion source string already in ${NVM_PROFILE}"
fi
fi
if ${BASH_OR_ZSH} && [ -z "${NVM_PROFILE-}" ] ; then
echo "=> Please also append the following lines to the if you are using bash/zsh shell:"
command printf "${COMPLETION_STR}"
fi
# Source nvm
# shellcheck source=/dev/null
\. "$(nvm_install_dir)/nvm.sh"
nvm_check_global_modules
nvm_install_node
nvm_reset
echo "=> Close and reopen your terminal to start using nvm or run the following to use it now:"
command printf "${SOURCE_STR}"
if ${BASH_OR_ZSH} ; then
command printf "${COMPLETION_STR}"
fi
}
#
# Unsets the various functions defined
# during the execution of the install script
#
nvm_reset() {
unset -f nvm_has nvm_install_dir nvm_latest_version nvm_profile_is_bash_or_zsh \
nvm_source nvm_node_version nvm_download install_nvm_from_git nvm_install_node \
install_nvm_as_script nvm_try_profile nvm_detect_profile nvm_check_global_modules \
nvm_do_install nvm_reset nvm_default_install_dir
}
[ "_$NVM_ENV" = "_testing" ] || nvm_do_install
} # this ensures the entire script is downloaded #

29330
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

54
package.json Normal file
View file

@ -0,0 +1,54 @@
{
"name": "budget-ui",
"version": "0.1.0",
"private": true,
"dependencies": {
"@ckeditor/ckeditor5-react": "^9.5.0",
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.0",
"@mui/material": "^7.1.0",
"@mui/x-tree-view": "^8.3.1",
"@testing-library/jest-dom": "^5.15.1",
"@testing-library/react": "^11.2.7",
"@testing-library/user-event": "^12.8.3",
"ckeditor5": "^45.1.0",
"date-fns": "^2.29.3",
"history": "^5.1.0",
"intl": "^1.2.5",
"moment": "^2.29.4",
"react": "^17.0.2",
"react-date-picker": "^8.3.6",
"react-dom": "^17.0.2",
"react-scripts": "^5.0.1",
"react-time-picker": "^4.4.4",
"web-vitals": "^1.1.2"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
},
"eslintConfig": {
"extends": [
"react-app",
"react-app/jest"
]
},
"browserslist": {
"production": [
">0.2%",
"not dead",
"not op_mini all"
],
"development": [
"last 1 chrome version",
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"react-router": "^6.0.2",
"react-router-dom": "^6.0.2"
}
}

BIN
public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

43
public/index.html Normal file
View file

@ -0,0 +1,43 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>

BIN
public/logo192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
public/logo512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
public/manifest.json Normal file
View file

@ -0,0 +1,25 @@
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
},
{
"src": "logo192.png",
"type": "image/png",
"sizes": "192x192"
},
{
"src": "logo512.png",
"type": "image/png",
"sizes": "512x512"
}
],
"start_url": ".",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}

3
public/robots.txt Normal file
View file

@ -0,0 +1,3 @@
# https://www.robotstxt.org/robotstxt.html
User-agent: *
Disallow:

715
src/EditRegex.jsx Normal file
View file

@ -0,0 +1,715 @@
import React, {useState, useEffect} from "react";
import {useParams, useNavigate} from 'react-router-dom';
import {
getRegex,
saveRegex,
getSources,
getCategories,
getAllCategories,
createCategory,
getCategoryAncestry,
saveCategory,
cloneCategories,
getTransactionsByRegexId,
getAllTransactions
} from "./apiService";
const EditRegex = (props) => {
console.log("EditRegex", props);
const [error, setError] = useState(null);
const [regexIsLoaded, setRegexIsLoaded] = useState(false);
const [categoryAncestryIsLoaded, setCategoryAncestryIsLoaded] = useState(false);
const [regexObj, setRegexObj] = useState(null);
const [originalRegexObj, setOriginalRegexObj] = useState(null);
const {regexIdParam} = useParams();
const [sources, setSources] = useState([]);
const [categoryAncestry, setCategoryAncestry] = useState([]);
const [categoryMap, setCategoryMap] = useState({});
const [originalCategoryAncestry, setOriginalCategoryAncestry] = useState(null);
const [addingCategory, setAddingCategory] = useState(null);
const [addCategoryOkButtonDisabled, setAddCategoryOkButtonDisabled] = useState(true);
const [dirty, setDirty] = useState(false);
const [attachedTransactions, setAttachedTransactions] = useState(null);
const [allTransactions, setAllTransactions] = useState(null);
const navigate = useNavigate();
useEffect(() => {
console.log('start useEffect 1', regexIdParam);
let ignore = false;
let regexId = regexIdParam;
setRegexObj(null);
if(regexId == null) {
setRegexIsLoaded(true);
let regexObj = {categoryId: null, regex: "", flags: 0, source: "", priority: 0, description: "", year: null};
setRegexObj(regexObj);
setOriginalRegexObj(regexObj);
}
else getRegex(regexId).then(
data => {
// console.log('EditRegex.useEffect getRegex data', data);
if(data.status === 401) {
navigate('/login', {replace: true});
return;
}
if(data.status !== 200) {
setRegexIsLoaded(true);
setError(data);
return;
}
if(!ignore) {
setRegexObj(data.result);
setOriginalRegexObj(data.result);
setRegexIsLoaded(true);
}
},
error => {
if(error.status === 401) {
navigate('/login', {replace: true});
return;
}
setRegexIsLoaded(true);
setError(error);
console.error('error object', error);
}
);
getSources("2025").then(
data => {
// console.log('EditRegex.useEffect getSources data', data);
setSources(data);
},
error => {
if(error.status === 401) {
navigate('/login', {replace: true});
return;
}
setRegexIsLoaded(true);
setError(error);
console.error('error object', error);
}
)
getAllCategories().then(
data => {
// console.log("getCategories", data);
let categoryMap = {};
for(let category of data) {
categoryMap[category.id] = category;
}
setCategoryMap(categoryMap);
},
error => {
if(error.status === 401) {
navigate('/login', {replace: true});
return;
}
setRegexIsLoaded(true);
setError(error);
console.error('error object', error);
}
)
return () => {
ignore = true;
};
}, [regexIdParam, navigate]);
useEffect(() => {
console.log('start useEffect 2', regexObj);
let ignore = false;
if(regexObj && regexObj.categoryId) {
getCategoryAncestry(regexObj.categoryId).then(
data => {
// console.log('useEffect 2 getCategoryAncestry data', data);
if(!ignore) {
setCategoryAncestry(data);
setOriginalCategoryAncestry(data);
setCategoryAncestryIsLoaded(true);
}
}
);
getTransactionsByRegexId("2025", regexObj.id).then(
data => {
if(!ignore) {
setAttachedTransactions(data);
}
}
);
getAllTransactions("2025").then(
data => {
if(!ignore) {
setAllTransactions(data);
}
}
)
}
return () => {
ignore = true;
};
}, [regexObj]);
console.log('render', error, regexIsLoaded, categoryAncestryIsLoaded, regexObj, categoryAncestry);
if(error) {
return <div>Error: {error.message}</div>;
}
if(!regexIsLoaded || !categoryAncestryIsLoaded) {
console.log("returning loading message");
return <div>Loading...</div>;
}
const handleCancel = e => {
navigate(`/regexes`);
};
const handleSubmit = e => {
// console.log('EditRegex.handleSubmit', e);
e.preventDefault();
let regexId = regexObj.id;
let submitRegex = {
categoryId: regexObj.categoryId,
regex: regexObj.regex,
flags: regexObj.flags,
source: regexObj.source,
priority: regexObj.priority,
description: regexObj.description,
year: regexObj.year};
if(regexId) {
submitRegex.id = regexId;
}
// console.log('EditRegex.handleSubmit submitRegex', submitRegex);
saveRegex(submitRegex).then(
data => {
// console.log('saveRegexes data', data);
if(data.status !== 200) {
alert(data.message);
return;
}
navigate(`/regexes`);
},
error => {
alert(error.message);
console.error('error object', error);
}
);
};
const getCopyOfRegexObj = () => {
return {
id: regexObj.id,
categoryId: regexObj.categoryId,
regex: regexObj.regex,
flags: regexObj.flags,
source: regexObj.source,
priority: regexObj.priority,
description: regexObj.description,
year: regexObj.year
}
}
const handleRegexChange = e => {
// console.log('EditRegex.handleRegexChange', e);
const name = e.target.name;
const value = e.target.value;
if(name !== 'regex') {
console.error('EditRegex.handleRegexChange unexpected target name: ' + name);
return;
}
let newRegexObj = getCopyOfRegexObj();
newRegexObj.regex = value;
setRegexObj(newRegexObj);
setDirty(newRegexObj.regex !== originalRegexObj.regex);
}
const handleSourceChange = e => {
// console.log('EditRegex.handleSourceChange', e);
const name = e.target.name;
const value = e.target.value;
if(name !== 'source') {
console.error('EditRegex.handleRegexChange unexpected target name: ' + name);
return;
}
let newRegexObj = getCopyOfRegexObj();
newRegexObj.source = value;
setRegexObj(newRegexObj);
setDirty(newRegexObj.source !== originalRegexObj.source);
}
const handlePriorityChange = e => {
// console.log('EditRegex.handlePriorityChange', e);
const name = e.target.name;
const value = e.target.value;
if(name !== 'priority') {
console.error('EditRegex.handleRegexChange unexpected target name: ' + name);
return;
}
let newRegexObj = getCopyOfRegexObj();
newRegexObj.priority = value;
setRegexObj(newRegexObj);
setDirty(newRegexObj.priority !== originalRegexObj.priority);
}
const handleDescriptionChange = e => {
// console.log('EditRegex.handleDescriptionChange', e);
const name = e.target.name;
const value = e.target.value;
if(name !== 'description') {
console.error('EditRegex.handleRegexChange unexpected target name: ' + name);
return;
}
let newRegexObj = getCopyOfRegexObj();
newRegexObj.description = value;
setRegexObj(newRegexObj);
setDirty(newRegexObj.description !== originalRegexObj.description);
}
const handleYearChange = e => {
// console.log('EditRegex.handleYearChange', e);
const name = e.target.name;
const value = e.target.value;
if(name !== 'year') {
console.error('EditRegex.handleRegexChange unexpected target name: ' + name);
return;
}
let newRegexObj = getCopyOfRegexObj();
newRegexObj.year = value;
setRegexObj(newRegexObj);
setDirty(newRegexObj.year !== originalRegexObj.year);
}
const handleFlagChange = e => {
// console.log("handleFlagChange e", e);
let newRegexObj = getCopyOfRegexObj();
if(e.target.checked) {
newRegexObj.flags |= e.target.value;
}
else {
newRegexObj.flags &= ~e.target.value;
}
// console.log("handleFlagChange newRegexObj.flags", newRegexObj.flags);
setRegexObj(newRegexObj);
setDirty(newRegexObj.flags !== originalRegexObj.flags);
}
const getFlagChecked = flag => {
let result = (regexObj.flags & flag) !== 0 ? "checked" : "";
return result;
};
if(regexObj == null) {
console.log("return empty page because regexObj is null");
return (<></>);
}
let idRows = null;
if(regexObj.id != null) {
idRows = <tr>
<td>ID:</td>
<td>{regexObj.id}</td>
</tr>;
}
const flagInput = function(flag, id, name) {
return <>
<label>
<input type="checkbox" id={id} name={id} value={flag}
checked={getFlagChecked(flag)}
onChange={handleFlagChange}/>
{name}
</label>
</>
}
const sourceOptions = [];
sourceOptions.push(<option key={"none"} value="">None</option>);
for(let source of sources) {
sourceOptions.push(<option key={source} value={source}>{source}</option>)
}
const handleCategoryChange = e => {
// console.log('EditRegex.handleCategoryChange', e);
const name = e.target.name;
const value = e.target.value;
let categoryLevel = +name;
let categoryId = +value;
chooseCategory(categoryLevel, categoryId).then(
categories => {
setCategoryAncestry(categories);
setDirty(categoryAncestryWasChanged(categories));
},
error => {
alert(error.message);
console.error('error object', error);
});
}
const categoryAncestryWasChanged = (categoryAncestry) => {
if(categoryAncestry.length !== originalCategoryAncestry.length) {
return true;
}
for(let i = 0; i < categoryAncestry.length; i++) {
let cat1 = categoryAncestry[i];
let cat2 = originalCategoryAncestry[i];
let cat1Id = cat1.id;
if(cat1Id == null) {
cat1Id = 0;
}
let cat2Id = cat2.id;
if(cat2Id == null) {
cat2Id = 0;
}
if(cat1Id !== cat2Id) {
return true;
}
}
return false;
};
const chooseCategory = (categoryLevel, categoryId) => {
// console.log("chooseCatgory categoryLevel, categoryId", categoryLevel, categoryId);
let clonedAncestry = cloneCategories(categoryAncestry);
if(categoryId === -2 /* select one */) {
clonedAncestry.length = categoryLevel + 1;
let dummyCat = clonedAncestry[categoryLevel];
dummyCat.id = null;
dummyCat.name = null;
return new Promise((resolve, reject) => {
resolve(clonedAncestry);
});
}
else if(categoryId === -1 /* create */) {
// add
// console.log('setAddingCategory', categoryLevel);
setAddingCategory({
level: categoryLevel
});
return new Promise((resolve, reject) => {
resolve(clonedAncestry);
});
}
else {
let category = clonedAncestry[categoryLevel];
if(category == null) {
let message = `No category found at level ${categoryLevel}`;
console.error(message);
return new Promise((resolve, reject) => {
reject({message: message});
});
}
if(category.id == null) {
if(categoryLevel !== clonedAncestry.length - 1) {
let message = `categoryLevel should be ${clonedAncestry.length - 1} but is ${categoryLevel}`;
console.error(message);
return new Promise((resolve, reject) => {
reject({message: message});
});
}
}
else {
if(categoryLevel > clonedAncestry.length + 1) {
let message = `categoryLevel should be less than or equal to ${clonedAncestry.length + 1} but is ${categoryLevel}`;
console.error(message);
return new Promise((resolve, reject) => {
reject({message: message});
});
}
}
if(categoryId === 0) {
clonedAncestry.length = categoryLevel + 1;
category = {siblings: []};
clonedAncestry[categoryLevel] = category;
categoryId = null;
}
else {
category.id = categoryId;
let foundCategory = categoryMap[categoryId];
if(foundCategory) {
category.name = foundCategory.name;
}
clonedAncestry.length = categoryLevel + 2;
category = {siblings: []};
clonedAncestry[categoryLevel + 1] = category;
}
return getCategories(categoryId).then(
dataResults => {
// console.log('getCategories', categoryId, dataResults);
for(let dataResult of dataResults) {
let sibling = createCategory(dataResult, categoryId);
category.siblings.push(sibling);
}
category.siblings.sort((p1, p2) => {
if(p1.name < p2.name) {
return -1;
}
if(p1.name > p2.name) {
return 1;
}
return 0;
});
// console.log('clonedAncestry', clonedAncestry);
return new Promise((resolve, reject) => {
resolve(clonedAncestry);
});
},
error => {
console.error('getCategories', error);
return new Promise((resolve, reject) => {
reject(error);
});
}
);
}
};
const handleAddCategory = e => {
// console.log('handleAddCategory', e);
// e.preventDefault();
let input = document.getElementById('add_category_name');
let data = {
name: input.value
};
if(addingCategory.level > 0) {
let parentCategory = categoryAncestry[addingCategory.level - 1];
data.parentCategoryId = parentCategory.id;
}
// console.log('saveCategory', data);
saveCategory(data).then(
data => {
// console.log('saveCategory response', data);
if(data.status !== 200) {
alert(data.message);
return;
}
let clonedCategorys = cloneCategories(categoryAncestry);
// add category to selection list and select it
let category = clonedCategorys[addingCategory.level];
let sibling = data.result;
category.siblings.push(sibling);
category.siblings.sort((p1, p2) => {
if(p1.name < p2.name) {
return -1;
}
if(p1.name > p2.name) {
return 1;
}
return 0;
});
category.id = sibling.id;
setCategoryAncestry(clonedCategorys);
setAddingCategory(null);
},
error => {
alert(error.message);
console.error('error object', error);
}
);
};
const categorySelects = [];
// console.log("render categoryAncestry", categoryAncestry);
if(categoryAncestry) {
for(let category of categoryAncestry) {
// console.log("category", category);
let key = categorySelects.length;
if(addingCategory != null &&
categorySelects.length === addingCategory.level) {
categorySelects.push((
<span key={key}>
<input type="text" id="add_category_name" onChange={e => {
let value = e.target.value;
setAddCategoryOkButtonDisabled(value === '');
}}/>
<button type="button"
onClick={handleAddCategory}
disabled={addCategoryOkButtonDisabled}
id="add_category_ok">OK</button>
<button type="button"
onClick={e => {
// console.log('addCategory cancel', e);
setAddingCategory(null);
}}>Cancel</button>
</span>
))
continue;
}
let options = [];
options.push(<option key={-2} value={"-2"}>?Select one</option>)
options.push(<option key={-1} value={"-1"}>+Create</option>);
if(category.siblings) {
for(let child of category.siblings) {
options.push(<option key={child.id} value={child.id}>{child.name}</option>)
}
}
// console.log("options", options);
let select = <select key={key} value={category.id || -2}
name={categorySelects.length}
onChange={handleCategoryChange}>
{options}
</select>;
// console.log("select", select);
categorySelects.push(select);
}
}
let attachedTransactionRows = [];
let i = 0;
for(let transaction of attachedTransactions) {
attachedTransactionRows.push(<tr key={i++}>
<td>{transaction.id}</td>
<td>{transaction.source}</td>
<td>{transaction.type}</td>
<td>{transaction.description}</td>
<td>{transaction.extraDescription}</td>
<td>{transaction.date}</td>
<td>{transaction.amount}</td>
<td>{transaction.optional}</td>
</tr>);
}
let flagsStr = "";
if(regexObj.flags & 2) {
flagsStr += "i";
}
let re = new RegExp(regexObj.regex, flagsStr);
let matchedTransactionRows = [];
i = 0;
for(let transaction of allTransactions) {
let regexMatches = re.test(transaction.description);
let sourceMatches = regexObj.source == null || regexObj.source === "" ||
transaction.source === regexObj.source;
// console.log(regexMatches, sourceMatches, transaction.description, transaction.source);
if(regexMatches && sourceMatches) {
matchedTransactionRows.push(<tr key={i++}>
<td>{transaction.id}</td>
<td>{transaction.source}</td>
<td>{transaction.type}</td>
<td>{transaction.description}</td>
<td>{transaction.extraDescription}</td>
<td>{transaction.date}</td>
<td>{transaction.amount}</td>
<td>{transaction.optional}</td>
<td>{transaction.regexId}</td>
</tr>);
}
}
console.log("returning page");
return (
<>
<h1>{regexObj.id == null ? 'Add Regex' : `Edit Regex ${regexObj.id}`}</h1>
<form onSubmit={handleSubmit}>
<table>
<tbody>
{idRows}
<tr>
<td>Regular Expression:</td>
<td>
<input type="text" name="regex" id="regex"
style={{width: "10in"}}
value={regexObj.regex || ""}
onChange={handleRegexChange} />
</td>
</tr>
<tr>
<td>Flags:</td>
<td>
{flagInput(0x01, "unix-lines", "Unix Lines")}
{flagInput(0x02, "case-insensitive", "Case Insensitive")}
{flagInput(0x04, "comments", "Comments")}
{flagInput(0x08, "multiline", "Multiline")}
{flagInput(0x10, "literal", "Literal")}
{flagInput(0x20, "dotall", "Dotall")};
{flagInput(0x40, "unicode-case", "Unicode Case")}
{flagInput(0x80, "canon-eq", "Canon EQ")}
{flagInput(0x100, "unicode-character-class", "Unicode Character Class")}
</td>
</tr>
<tr>
<td>Source:</td>
<td>
<select name="source" id="source" value={regexObj.source == null ? "" : regexObj.source}
onChange={handleSourceChange}>
{sourceOptions}
</select>
</td>
</tr>
<tr>
<td>Priority:</td>
<td>
<input type="text" name="priority" id="priority"
style={{width: "1in"}}
value={regexObj.priority || ""}
onChange={handlePriorityChange} />
</td>
</tr>
<tr>
<td>Description:</td>
<td>
<input type="text" name="description" id="description"
style={{width: "10in"}}
value={regexObj.description || ""}
onChange={handleDescriptionChange} />
</td>
</tr>
<tr>
<td>Year:</td>
<td>
<input type="text" name="year" id="year"
style={{width: "1in"}}
value={regexObj.year || ""}
onChange={handleYearChange} />
</td>
</tr>
<tr>
<td>Category:</td>
<td>
{categorySelects}
</td>
</tr>
</tbody>
</table>
<button type="submit" disabled={!dirty}>Submit</button>
<button type="button" onClick={handleCancel}>Cancel</button>
</form>
<h2>Attached Transactions</h2>
<table>
<thead>
<tr>
<th>Id</th>
<th>Source</th>
<th>Type</th>
<th>Description</th>
<th>Extra Description</th>
<th>Date</th>
<th>Amount</th>
<th>Optional</th>
</tr>
</thead>
<tbody>
{attachedTransactionRows}
</tbody>
</table>
<h2>Matched Transactions {matchedTransactionRows.length} out of {allTransactions.length}</h2>
<table>
<thead>
<tr>
<th>Id</th>
<th>Source</th>
<th>Type</th>
<th>Description</th>
<th>Extra Description</th>
<th>Date</th>
<th>Amount</th>
<th>Optional</th>
<th>Regex ID</th>
</tr>
</thead>
<tbody>
{matchedTransactionRows}
</tbody>
</table>
</>
);
};
export default EditRegex;

66
src/LoginForm.jsx Normal file
View file

@ -0,0 +1,66 @@
import React, {useState} from "react";
import {useNavigate} from 'react-router-dom';
import {login} from "./apiService";
// import {createHashHistory} from 'history';
const LoginForm = props => {
const [inputs, setInputs] = useState({});
// const history = createHashHistory();
const navigate = useNavigate();
const handleChange = (event) => {
const name = event.target.name;
const value = event.target.value;
setInputs(values => ({...values, [name]: value}));
}
const handleSubmit = (event) => {
event.preventDefault();
console.log(inputs);
login(inputs.username, inputs.password).then(
data => {
console.log('data', data);
if(data.status !== 200) {
alert(data.message);
return;
}
window.localStorage.setItem('token', data.result.token);
navigate('/regexes', {replace: true});
// history.back();
},
error => {
console.error('error object', error);
}
);
}
window.localStorage.removeItem('token');
return (
<form onSubmit={handleSubmit}>
<h1>Budget Login</h1>
<table>
<tbody>
<tr>
<td><label htmlFor="username">Name:</label></td>
<td>
<input type="text" name="username" id="username"
value={inputs.username || ""}
onChange={handleChange} />
</td>
</tr>
<tr>
<td><label htmlFor="password">Password:</label></td>
<td>
<input type="password" name="password" id="password"
value={inputs.password || ""}
onChange={handleChange} />
</td>
</tr>
</tbody>
</table>
<input type="submit"/>
</form>
)
}
export default LoginForm;

215
src/Regexes.jsx Normal file
View file

@ -0,0 +1,215 @@
import React from 'react';
import {useState, useEffect} from "react";
import {useParams, Link, useNavigate} from 'react-router-dom';
import {
getRegexes,
deleteRegex
} from "./apiService";
const Regexes = (props) => {
const [error, setError] = useState(null);
const [isLoaded, setIsLoaded] = useState(false);
const [regexes, setRegexes] = useState([]);
const navigate = useNavigate();
const params = useParams();
console.log("Regexes: calling useEffect");
useEffect(() => {
console.log("Regexes: useEffect");
console.log("params", params);
let categoryParam = params.categoryName;
console.log("categoryParam", categoryParam);
let sourceParam = params.source;
console.log("sourceParam", sourceParam);
const search = window.location.search;
const searchParams = new URLSearchParams(search);
console.log("searchParams", searchParams);
let sortParam = searchParams.get('sort');
console.log("sortParam", sortParam);
const filter = function(regexes) {
let newRegexes = [];
for(let regex of regexes) {
if(categoryParam != null && categoryParam !== regex.fqCategoryName) {
continue;
}
if(sourceParam != null && sourceParam !== regex.source) {
continue;
}
newRegexes.push(regex);
}
if(sortParam != null) {
let sorts = sortParam.split(/, */);
newRegexes.sort((r1, r2) => {
for(let sort of sorts) {
let f1 = r1[sort];
let f2 = r2[sort];
if(f1 == null) {
if(f2 != null) {
return -1;
}
}
else {
if(f2 == null) {
return 1;
}
else {
if(f1 < f2) {
return -1;
}
if(f1 > f2) {
return 1;
}
}
}
}
return 0;
});
}
return newRegexes;
}
getRegexes().then(
data => {
console.log("Regexes.useEffect getRegexes data", data);
if(data.status === 401) {
navigate('/login', {replace: true});
return;
}
if(data.status !== 200) {
setIsLoaded(true);
setError(data);
return;
}
setIsLoaded(true);
setRegexes(filter(data.result));
},
error => {
if(error.status === 401) {
navigate('/login', {replace: true});
return;
}
console.log('error object', error);
setIsLoaded(true);
setError(error);
}
);
}, [navigate, params]);
if(error) {
return <div>Error: {error.message}</div>;
}
if(!isLoaded) {
return <div>Loading...</div>;
}
const getFlagNames = function(flags) {
let names = [];
if(flags & 0x02) {
names.push("Case Insensitive");
}
if ((flags & 0x08) !== 0) {
names.push("Multiline");
}
if ((flags & 0x20) !== 0) {
names.push("Dotall");
}
if ((flags & 0x40) !== 0) {
names.push("Unicode Case");
}
if ((flags & 0x80) !== 0) {
names.push("Canon EQ");
}
if ((flags & 0x01) !== 0) {
names.push("Unix Linex");
}
if ((flags & 0x10) !== 0) {
names.push("Literal");
}
if ((flags & 0x100) !== 0) {
names.push("Unicode Character Class");
}
if ((flags & 0x04) !== 0) {
names.push("Comments");
}
return names.join(", ");
}
console.log("regexes render", regexes);
return (
<>
<div className={"header"}><span className={"header"}>Regular Expressions</span> &nbsp;
<Link to={`/regex`}>Add</Link>
</div>
<table className={'regexes'}>
<thead>
<tr>
<th><Link to={`/regexes?sort=fqCategoryName`}>Category</Link></th>
<th><Link to={`/regexes?sort=source`}>Source</Link></th>
<th><Link to={`/regexes?sort=regex`}>Regular Expression</Link></th>
<th>Flags</th>
<th><Link to={`/regexes?sort=priority`}>Priority</Link></th>
<th><Link to={`/regexes?sort=description`}>Extra Description</Link></th>
<th><Link to={'/regexes?sort=year'}>Year</Link></th>
</tr>
</thead>
<tbody>
{
regexes.map(regex => {
return (
<tr key={regex.id}>
<td>
{regex.fqCategoryName}
</td>
<td>
{regex.source}
</td>
<td>
{regex.regex}
</td>
<td>
{getFlagNames(regex.flags)}
</td>
<td style={{textAlign: "right"}}>
{regex.priority}
</td>
<td>
{regex.description}
</td>
<td>
{regex.year}
</td>
<td>
<button type="button" onClick={
e => {
deleteRegex(regex.id).then(
data => {
console.log('deleteRegex data', data);
},
error => {
console.error('error object', error);
setError(error);
}
);
}
}>Delete</button>
</td>
<td>
<button type="button" onClick={
e => {
window.location.href = `/regex/${regex.id}`;
}
}>Edit</button>
</td>
</tr>
);
})
}
</tbody>
</table>
</>
);
}
export default Regexes;

521
src/apiService.jsx Normal file
View file

@ -0,0 +1,521 @@
const baseUrl = 'http://localhost:9090';
const fetchOptions = (method, data) => {
let token = window.localStorage.getItem('token');
let headers = {
};
if(method === 'POST' || method === 'PUT') {
headers['Content-Type'] = 'application/json';
}
if(token != null) {
headers['Authorization'] = `Bearer ${token}`;
}
let options = {
method: method,
mode: 'cors',
cache: 'no-cache',
credentials: 'omit',
headers: headers,
redirect: 'follow',
referrerPolicy: 'no-referrer'
};
if(data != null) {
options.body = JSON.stringify(data);
}
// console.log("fetchOptions", options);
return options;
};
export const login = (username, password) => {
let url = `${baseUrl}/token/generate-token`;
let data = {username: username, password: password};
return fetch(url, fetchOptions('POST', data)).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
});
};
export const getRegexesByCategory = categoryName => {
if(categoryName == null) {
return new Promise((resolve, reject) => {
reject('categoryName is null');
});
}
let url = `${baseUrl}/regexes/category/${categoryName}`;
return fetch(url, fetchOptions('GET')).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
});
};
export const getRegexesBySource = source => {
if(source == null) {
return new Promise((resolve, reject) => {
reject('source is null');
});
}
let url = `${baseUrl}/regexes/source/${source}`;
return fetch(url, fetchOptions('GET')).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
});
};
export const getRegexes = () => {
let url = `${baseUrl}/regexes`;
return fetch(url, fetchOptions('GET')).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
});
};
export const getRegex = regexId => {
if(regexId == null) {
return new Promise((resolve, reject) => {
reject('regexId is null');
});
}
let url = `${baseUrl}/regex/${regexId}`;
return fetch(url, fetchOptions('GET')).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
});
}
export const deleteRegex = regexId => {
if(regexId == null) {
return new Promise((resolve, reject) => {
reject('regexId is null');
});
}
let url = `${baseUrl}/regex/${regexId}`;
return fetch(url, fetchOptions('DELETE')).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
});
};
export const saveRegex = regex => {
if(regex == null) {
return new Promise((resolve, reject) => {
reject('regex is null');
});
}
// console.log("saveRegex", regex);
let id = regex.id;
let data = {
categoryId: regex.categoryId,
regex: regex.regex,
flags: regex.flags,
source: regex.source,
priority: regex.priority,
description: regex.description,
year: regex.year
};
let url = `${baseUrl}/regexes`;
let method = 'POST';
if(+id > 0) {
data.id = id;
method = 'PUT';
}
// console.log('saveRegex.data', data);
return fetch(url, fetchOptions(method, data)).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
});
};
export const getCategories = parentCategoryId => {
let url = parentCategoryId == null || parentCategoryId === 0
? `${baseUrl}/categories/root`
: `${baseUrl}/categories/parent/${parentCategoryId}`;
return fetch(url, fetchOptions('GET')).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
}).then(data => {
// console.log('getCategories data', parentCategoryId, data);
if(data.status !== 200) {
return new Promise((resolve, reject) => {
reject(data);
});
}
return new Promise((resolve, reject) => {
resolve(data.result);
});
}, error => {
return new Promise((resolve, reject) => {
reject(error);
});
});
};
export const getAllCategories = () => {
let url = `${baseUrl}/categories`;
return fetch(url, fetchOptions('GET')).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
}).then(data => {
// console.log('getAllCategories data', data);
if(data.status !== 200) {
return new Promise((resolve, reject) => {
reject(data);
});
}
return new Promise((resolve, reject) => {
resolve(data.result);
});
}, error => {
return new Promise((resolve, reject) => {
reject(error);
});
});
};
export const createCategory = (dataResult, parentCategoryId) => ({
id: dataResult.id,
parentCategoryId: parentCategoryId,
name: dataResult.name
});
export const getCategoryAncestry = categoryId => {
let url = `${baseUrl}/categories/ancestry/${categoryId}`;
return fetch(url, fetchOptions('GET')).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
}).then(data => {
// console.log('getCategoryAncestry data', categoryId, data);
if(data.status !== 200) {
return new Promise((resolve, reject) => {
reject(data);
});
}
return new Promise((resolve, reject) => {
let ancestry = data.result;
let getSelectionList = index => {
if(index >= ancestry.length) {
let lastCategoryId = ancestry.length > 0 ? ancestry[ancestry.length - 1].id : null;
let category = {parentCategoryId: lastCategoryId};
getCategories(category.parentCategoryId).then(siblings => {
// console.log("siblings", siblings);
category.siblings = siblings;
ancestry.push(category);
resolve(ancestry);
});
return;
}
let category = ancestry[index];
category.siblings = [];
getCategories(category.parentCategoryId).then(siblings => {
// console.log("siblings", siblings);
category.siblings = siblings;
getSelectionList(index + 1);
});
};
getSelectionList(0);
});
}, error => {
return new Promise((resolve, reject) => {
reject(error);
});
});
};
export const saveCategory = category => {
if(category == null) {
return new Promise((resolve, reject) => {
reject('category is null');
});
}
// console.log("saveCategory", category);
let id = category.id;
let data = {
parentCategoryId: category.parentCategoryId,
name: category.name
};
let url = `${baseUrl}/categories`;
let method = 'POST';
if(+id > 0) {
data.id = id;
method = 'PUT';
}
// console.log('saveCategory.data', data);
return fetch(url, fetchOptions(method, data)).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
});
};
export const cloneCategories = categories => {
let newCategories = [];
for(let category of categories) {
// selectionList not cloned, but it doesn't need to be
let newCategory = {
id: category.id,
parentCategoryId: category.parentCategoryId,
name: category.name,
siblings: category.siblings
};
newCategories.push(newCategory);
}
return newCategories;
};
export const getSources = year => {
let url = `${baseUrl}/sources/${year}`;
return fetch(url, fetchOptions('GET')).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
}).then(data => {
// console.log("getSources data", year, data);
if(data.status !== 200) {
return new Promise((resolve, reject) => {
reject(data);
});
}
return new Promise((resolve, reject) => {
resolve(data.result);
});
}, error => {
return new Promise((resolve, reject) => {
reject(error);
});
})
}
export const getAllTransactions = year => {
let url = `${baseUrl}/transactions/${year}`;
return fetch(url, fetchOptions('GET')).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
}).then(data => {
// console.log('getAllCategories data', data);
if(data.status !== 200) {
return new Promise((resolve, reject) => {
reject(data);
});
}
return new Promise((resolve, reject) => {
resolve(data.result);
});
}, error => {
return new Promise((resolve, reject) => {
reject(error);
});
});
};
export const getTransactionsByRegexId = (year, regexId) => {
let url = `${baseUrl}/transactionsByRegexId/${year}/${regexId}`;
return fetch(url, fetchOptions('GET')).then(response => {
// console.log(url, "response", response);
if(response.status !== 200) {
return new Promise((resolve, reject) => {
reject({status: response.status});
});
}
return response.json(); // promise
}).then(data => {
// console.log('getAllCategories data', data);
if(data.status !== 200) {
return new Promise((resolve, reject) => {
reject(data);
});
}
return new Promise((resolve, reject) => {
resolve(data.result);
});
}, error => {
return new Promise((resolve, reject) => {
reject(error);
});
});
};
export const parseDateISOString = s => {
if(s == null) {
return null;
}
let ds = s.split(/\D+/).map(s => parseInt(s));
ds[1] = ds[1] - 1; // adjust month
return new Date(ds[0], ds[1], ds[2]);
};
export const formatDate = date => {
if(date == null) {
return null;
}
let pad = num => {
return (num < 10 ? '0' : '') + num;
};
// return date.toISOString().split('T')[0];
return date.getFullYear() +
'-' + pad(date.getMonth() + 1) +
'-' + pad(date.getDate());
}
export const formatTime = date => {
let pad = num => {
return (num < 10 ? '0' : '') + num;
};
// return date.toISOString().split('T')[1];
return pad(date.getHours()) +
':' + pad(date.getMinutes()) +
':' + pad(date.getSeconds());
}
export const formatDuration = value => {
let pad = (n, width, z = "0") => {
let ns = n + '';
return ns.length >= width ? ns : new Array(width - ns.length + 1).join(z) + n;
};
let s = Math.floor(value / 1000);
let m = Math.floor(s / 60);
s -= m * 60;
let h = Math.floor(m / 60);
m -= h * 60;
return "" + pad(h, 2) + ":" + pad(m, 2) + ":" + pad(s, 2);
};
export const incrementDate = (date, delta) => {
if(date == null) {
return null;
}
let newDate = new Date(date);
newDate.setDate(newDate.getDate() + delta);
return newDate;
};
export const isToday = (date) => {
if(date == null) {
return false;
}
if(typeof date == 'string') {
date = parseDateISOString(date);
}
let today = new Date();
if(date.getDate() === today.getDate() &&
date.getMonth() === today.getMonth() &&
date.getFullYear() === today.getFullYear()) {
return true;
}
return false;
};
export const isYesterday = (date) => {
if(date == null) {
return false;
}
if(typeof date == 'string') {
date = parseDateISOString(date);
}
let yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
if(date.getDate() === yesterday.getDate() &&
date.getMonth() === yesterday.getMonth() &&
date.getFullYear() === yesterday.getFullYear()) {
return true;
}
return false;
};
export const isTomorrow = (date) => {
if(date == null) {
return false;
}
if(typeof date == 'string') {
date = parseDateISOString(date);
}
let tomorrow = new Date();
tomorrow.setDate(tomorrow.getDate() + 1);
if(date.getDate() === tomorrow.getDate() &&
date.getMonth() === tomorrow.getMonth() &&
date.getFullYear() === tomorrow.getFullYear()) {
return true;
}
return false;
};
export const getDateCaption = (date) => {
if(date == null) {
return null;
}
if(typeof date == 'string') {
date = parseDateISOString(date);
}
if(isToday(date)) {
return 'Today';
}
if(isYesterday(date)) {
return 'Yesterday';
}
if(isTomorrow(date)) {
return 'Tomorrow';
}
return formatDate(date);
};

36
src/index.jsx Normal file
View file

@ -0,0 +1,36 @@
import React from 'react';
import ReactDOM from 'react-dom';
import LoginForm from './LoginForm';
import {BrowserRouter, Routes, Route, Link} from "react-router-dom";
import Regexes from "./Regexes";
import EditRegex from "./EditRegex";
import './main.css';
export default function App() {
let token = window.localStorage.getItem('token');
console.log('token', token);
return (
<BrowserRouter>
<Link to="/">Regexes</Link> &nbsp;
<Link to="/week">Week</Link> &nbsp;
<Link to="/login">Log out</Link> &nbsp;
<hr />
<Routes>
<Route exact path="/regexes/:categoryParam" element={<Regexes/>}/>
<Route exact path="/regexes" element={<Regexes/>}/>
<Route exact path="/regex/:regexIdParam" element={<EditRegex/>}/>
<Route exact path="/regex" element={<EditRegex/>}/>
<Route exact path="/login" element={<LoginForm />}/>
{
token == null
? <Route exact path="/" element={<LoginForm/>}/>
: <Route exact path="/" element={<Regexes/>}/>
}
</Routes>
</BrowserRouter>
);
}
ReactDOM.render(<App/>, document.getElementById('root'));

73
src/main.css Normal file
View file

@ -0,0 +1,73 @@
body {
padding: 40px;
font-family: sans-serif;
}
table.week tr td.duration {
text-align: right;
}
table.week a {
text-decoration: none;
}
div.header {
margin-bottom: 6px;
}
span.header {
font-weight: bold;
font-size: larger;
}
table.compact tr {
height: 15px;
}
div.header-date, div.header-links {
display: inline;
}
div.header-date {
margin-right: 5px;
}
@media only screen and (max-width: 640px) {
body {
padding: 0px;
}
div>a {
line-height: 32px;
white-space: nowrap;
}
div>span>a {
line-height: 32px;
white-space: nowrap;
}
div.header-date, div.header-links {
display: block;
height: 30px;
}
div.header-date {
height: 20px;
}
table.regexes, table.regexes thead, table.regexes tbody, table.regexes th, table.regexes td, table.regexes tr {
display: block;
}
table.regexes thead tr {
position: absolute;
top: -9999px;
left: -9999px;
}
table.regexes tr { border: 1px solid #ccc; }
table.regexes td {
border: none;
border-bottom: 1px solid #eee;
position: relative;
min-height: 30px;
}
a {
border: 1px solid;
padding: 5px;
text-decoration: none;
}
table.projects tr {
height: 32px;
}
span.button {
border: 1px solid;
padding: 5px 10px;
}
}

4
start Executable file
View file

@ -0,0 +1,4 @@
#!/bin/sh
npm run build > build.log 2> build.err.log
serve -s --no-port-switching -l 3000 build > start.log 2> start.err.log &
echo "$!" > pid

3
stop Executable file
View file

@ -0,0 +1,3 @@
#!/bin/sh
kill $(cat pid)
rm pid

1
target/npmlist.json Normal file
View file

@ -0,0 +1 @@
{"version":"0.1.0","name":"react-timesheet","dependencies":{"@testing-library/jest-dom":{"version":"5.16.4"},"@testing-library/react":{"version":"11.2.7"},"@testing-library/user-event":{"version":"12.8.3"},"bindings":{"version":"1.5.0"},"file-uri-to-path":{"version":"1.0.0"},"history":{"version":"5.3.0"},"moment":{"version":"2.29.3"},"nan":{"version":"2.16.0"},"react-date-picker":{"version":"8.4.0"},"react-dom":{"version":"17.0.2"},"react-scripts":{"version":"3.4.4"},"react-time-picker":{"version":"4.5.0"},"react":{"version":"17.0.2"},"web-vitals":{"version":"1.1.2"}}}