Initial commit.
This commit is contained in:
parent
1ce1e4ccfc
commit
63f36a7ab0
21 changed files with 31615 additions and 2 deletions
27
.gitignore
vendored
Normal file
27
.gitignore
vendored
Normal 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
|
||||||
71
README.md
71
README.md
|
|
@ -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 can’t go back!**
|
||||||
|
|
||||||
|
If you aren’t 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 you’re on your own.
|
||||||
|
|
||||||
|
You don’t have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn’t feel obligated to use this feature. However we understand that this tool wouldn’t be useful if you couldn’t 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
5
deploy
Executable 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
425
install_nvm.sh
Normal 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
29330
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
54
package.json
Normal file
54
package.json
Normal 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
BIN
public/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
43
public/index.html
Normal file
43
public/index.html
Normal 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
BIN
public/logo192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
BIN
public/logo512.png
Normal file
BIN
public/logo512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
25
public/manifest.json
Normal file
25
public/manifest.json
Normal 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
3
public/robots.txt
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
# https://www.robotstxt.org/robotstxt.html
|
||||||
|
User-agent: *
|
||||||
|
Disallow:
|
||||||
715
src/EditRegex.jsx
Normal file
715
src/EditRegex.jsx
Normal 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
66
src/LoginForm.jsx
Normal 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
215
src/Regexes.jsx
Normal 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>
|
||||||
|
<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
521
src/apiService.jsx
Normal 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
36
src/index.jsx
Normal 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>
|
||||||
|
<Link to="/week">Week</Link>
|
||||||
|
<Link to="/login">Log out</Link>
|
||||||
|
|
||||||
|
<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
73
src/main.css
Normal 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
4
start
Executable 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
3
stop
Executable file
|
|
@ -0,0 +1,3 @@
|
||||||
|
#!/bin/sh
|
||||||
|
kill $(cat pid)
|
||||||
|
rm pid
|
||||||
1
target/npmlist.json
Normal file
1
target/npmlist.json
Normal 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"}}}
|
||||||
Loading…
Reference in a new issue