Browse Source

Initial commit

master
Inex Code 5 months ago
commit
3c1bda5f1f
  1. 21
      .devcontainer/Dockerfile
  2. 53
      .devcontainer/devcontainer.json
  3. 43
      .devcontainer/docker-compose.yml
  4. 94
      .gitignore
  5. 0
      contacts/__init__.py
  6. 26
      contacts/app.py
  7. 225
      contacts/contacts.py
  8. 46
      contacts/db.py
  9. 37
      contacts/schema.sql
  10. 23
      frontend/.gitignore
  11. 70
      frontend/README.md
  12. 29172
      frontend/package-lock.json
  13. 46
      frontend/package.json
  14. BIN
      frontend/public/favicon.ico
  15. 43
      frontend/public/index.html
  16. BIN
      frontend/public/logo192.png
  17. BIN
      frontend/public/logo512.png
  18. 25
      frontend/public/manifest.json
  19. 3
      frontend/public/robots.txt
  20. 128
      frontend/src/App.jsx
  21. 175
      frontend/src/ContactDialog.jsx
  22. 33
      frontend/src/axios.js
  23. 27
      frontend/src/index.jsx
  24. 5
      requirements.txt

21
.devcontainer/Dockerfile

@ -0,0 +1,21 @@ @@ -0,0 +1,21 @@
# [Choice] Python version (use -bullseye variants on local arm64/Apple Silicon): 3, 3.10, 3.9, 3.8, 3.7, 3.6, 3-bullseye, 3.10-bullseye, 3.9-bullseye, 3.8-bullseye, 3.7-bullseye, 3.6-bullseye, 3-buster, 3.10-buster, 3.9-buster, 3.8-buster, 3.7-buster, 3.6-buster
ARG VARIANT=3-bullseye
FROM mcr.microsoft.com/vscode/devcontainers/python:0-${VARIANT}
ENV PYTHONUNBUFFERED 1
# [Choice] Node.js version: none, lts/*, 16, 14, 12, 10
ARG NODE_VERSION="none"
RUN if [ "${NODE_VERSION}" != "none" ]; then su vscode -c "umask 0002 && . /usr/local/share/nvm/nvm.sh && nvm install ${NODE_VERSION} 2>&1"; fi
# [Optional] If your requirements rarely change, uncomment this section to add them to the image.
# COPY requirements.txt /tmp/pip-tmp/
# RUN pip3 --disable-pip-version-check --no-cache-dir install -r /tmp/pip-tmp/requirements.txt \
# && rm -rf /tmp/pip-tmp
# [Optional] Uncomment this section to install additional OS packages.
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
# && apt-get -y install --no-install-recommends <your-package-list-here>

53
.devcontainer/devcontainer.json

@ -0,0 +1,53 @@ @@ -0,0 +1,53 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
// https://github.com/microsoft/vscode-dev-containers/tree/v0.205.2/containers/python-3-postgres
// Update the VARIANT arg in docker-compose.yml to pick a Python version
{
"name": "Python 3 & PostgreSQL",
"dockerComposeFile": "docker-compose.yml",
"service": "app",
"workspaceFolder": "/workspace",
// Set *default* container specific settings.json values on container create.
"settings": {
"sqltools.connections": [{
"name": "Container database",
"driver": "PostgreSQL",
"previewLimit": 50,
"server": "localhost",
"port": 5432,
"database": "postgres",
"username": "postgres",
"password": "postgres"
}],
"python.defaultInterpreterPath": "/usr/local/bin/python",
"python.linting.enabled": true,
"python.linting.pylintEnabled": true,
"python.formatting.autopep8Path": "/usr/local/py-utils/bin/autopep8",
"python.formatting.blackPath": "/usr/local/py-utils/bin/black",
"python.formatting.yapfPath": "/usr/local/py-utils/bin/yapf",
"python.linting.banditPath": "/usr/local/py-utils/bin/bandit",
"python.linting.flake8Path": "/usr/local/py-utils/bin/flake8",
"python.linting.mypyPath": "/usr/local/py-utils/bin/mypy",
"python.linting.pycodestylePath": "/usr/local/py-utils/bin/pycodestyle",
"python.linting.pydocstylePath": "/usr/local/py-utils/bin/pydocstyle",
"python.linting.pylintPath": "/usr/local/py-utils/bin/pylint",
"python.testing.pytestPath": "/usr/local/py-utils/bin/pytest"
},
// Add the IDs of extensions you want installed when the container is created.
"extensions": [
"ms-python.python",
"ms-python.vscode-pylance",
"mtxr.sqltools",
"mtxr.sqltools-driver-pg"
],
// Use 'forwardPorts' to make a list of ports inside the container available locally.
"forwardPorts": [5000, 5432],
// Use 'postCreateCommand' to run commands after the container is created.
// "postCreateCommand": "pip install --user -r requirements.txt",
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
"remoteUser": "vscode"
}

43
.devcontainer/docker-compose.yml

@ -0,0 +1,43 @@ @@ -0,0 +1,43 @@
version: '3.8'
services:
app:
build:
context: ..
dockerfile: .devcontainer/Dockerfile
args:
# Update 'VARIANT' to pick a version of Python: 3, 3.10, 3.9, 3.8, 3.7, 3.6
# Append -bullseye or -buster to pin to an OS version.
# Use -bullseye variants on local arm64/Apple Silicon.
VARIANT: "3.9"
# Optional Node.js version to install
NODE_VERSION: "lts/*"
volumes:
- ..:/workspace:cached
# Overrides default command so things don't shut down after the process ends.
command: sleep infinity
# Runs app on the same network as the database container, allows "forwardPorts" in devcontainer.json function.
network_mode: service:db
# Uncomment the next line to use a non-root user for all processes.
# user: vscode
# Use "forwardPorts" in **devcontainer.json** to forward an app port locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
db:
image: postgres:latest
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
POSTGRES_USER: postgres
POSTGRES_DB: postgres
POSTGRES_PASSWORD: postgres
# Add "forwardPorts": ["5432"] to **devcontainer.json** to forward PostgreSQL locally.
# (Adding the "ports" property to this file will not forward from a Codespace.)
volumes:
postgres-data: null

94
.gitignore vendored

@ -0,0 +1,94 @@ @@ -0,0 +1,94 @@
# Created by https://www.gitignore.io
### OSX ###
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
venv
# Thumbnails
._*
# Files that might appear on external disk
.Spotlight-V100
.Trashes
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
### Python ###
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
# C extensions
*.so
# Distribution / packaging
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
lib/
lib64/
parts/
sdist/
var/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.coverage
.cache
nosetests.xml
coverage.xml
# Translations
*.mo
*.pot
# Sphinx documentation
docs/_build/
# PyBuilder
target/
### Django ###
*.log
*.pot
*.pyc
__pycache__/
local_settings.py
.env
db.sqlite3
usermedia/
data/

0
contacts/__init__.py

26
contacts/app.py

@ -0,0 +1,26 @@ @@ -0,0 +1,26 @@
#!/usr/bin/env python3
"""Contacts API"""
import os
from . import db
from .contacts import Contacts, Contact
from flask import Flask, request, jsonify
from flask_restful import Api
from flask_cors import CORS
def create_app(test_config=None):
"""Initiate Flask app and bind routes"""
app = Flask(__name__)
db.init_app(app)
api = Api(app)
CORS(app, resources={r"/api/*": {"origins": "*"}})
api.add_resource(Contacts, '/api/contacts')
api.add_resource(Contact, '/api/contacts/<int:uid>')
return app
if __name__ == "__main__":
created_app = create_app()
created_app.run(port=5000, debug=False)

225
contacts/contacts.py

@ -0,0 +1,225 @@ @@ -0,0 +1,225 @@
import functools
from flask import (
Blueprint, flash, g, redirect, render_template, request, session, url_for
)
from flask import Blueprint
from flask_restful import Resource, Api, reqparse
from .db import get_db
class Contacts(Resource):
def get(self):
db = get_db()
cur = db.cursor()
cur.execute("SELECT uid, last_name_val, first_name_val, middle_name_val, street_val, build, build_k, apartment, telephone"
" FROM main"
" join last_name on main.last_name = last_name_id"
" join first_name on main.first_name = first_name_id"
" join middle_name on main.middle_name = middle_name_id"
" join street on main.street = street_id")
contacts = cur.fetchall()
def convert(data):
return {
'uid': data[0],
'last_name': data[1],
'first_name': data[2],
'middle_name': data[3],
'street': data[4],
'build': data[5],
'build_k': data[6],
'apartment': data[7],
'telephone': data[8]
}
contacts = [convert(contact) for contact in contacts]
return contacts
def post(self):
db = get_db()
cur = db.cursor()
parser = reqparse.RequestParser()
parser.add_argument('last_name')
parser.add_argument('first_name')
parser.add_argument('middle_name')
parser.add_argument('street')
parser.add_argument('build')
parser.add_argument('build_k')
parser.add_argument('apartment')
parser.add_argument('telephone', required=True)
args = parser.parse_args()
cur.execute("SELECT uid FROM main ORDER BY uid DESC LIMIT 1")
uid = cur.fetchone()
if uid is None:
uid = 0
else:
uid = uid[0] + 1
cur.execute("SELECT last_name_id FROM last_name WHERE last_name_val = %s", (args['last_name'],))
last_name = cur.fetchone()
if last_name is None:
cur.execute("INSERT INTO last_name (last_name_val) VALUES (%s)", (args['last_name'],))
cur.execute("SELECT last_name_id FROM last_name WHERE last_name_val = %s", (args['last_name'],))
last_name = cur.fetchone()
last_name = last_name[0]
else:
last_name = last_name[0]
cur.execute("SELECT first_name_id FROM first_name WHERE first_name_val = %s", (args['first_name'],))
first_name = cur.fetchone()
if first_name is None:
cur.execute("INSERT INTO first_name (first_name_val) VALUES (%s)", (args['first_name'],))
cur.execute("SELECT first_name_id FROM first_name WHERE first_name_val = %s", (args['first_name'],))
first_name = cur.fetchone()
first_name = first_name[0]
else:
first_name = first_name[0]
cur.execute("SELECT middle_name_id FROM middle_name WHERE middle_name_val = %s", (args['middle_name'],))
middle_name = cur.fetchone()
if middle_name is None:
cur.execute("INSERT INTO middle_name (middle_name_val) VALUES (%s)", (args['middle_name'],))
cur.execute("SELECT middle_name_id FROM middle_name WHERE middle_name_val = %s", (args['middle_name'],))
middle_name = cur.fetchone()
middle_name = middle_name[0]
else:
middle_name = middle_name[0]
cur.execute("SELECT street_id FROM street WHERE street_val = %s", (args['street'],))
street = cur.fetchone()
if street is None:
cur.execute("INSERT INTO street (street_val) VALUES (%s)", (args['street'],))
cur.execute("SELECT street_id FROM street WHERE street_val = %s", (args['street'],))
street = cur.fetchone()
street = street[0]
else:
street = street[0]
cur.execute("INSERT INTO main (uid, last_name, first_name, middle_name, street, build, build_k, apartment, telephone)"
" VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)",
(uid, last_name, first_name, middle_name, street, args['build'], args['build_k'], args['apartment'], args['telephone']))
db.commit()
return {'uid': uid}, 201
class Contact(Resource):
def get(self, uid):
db = get_db()
cur = db.cursor()
cur.execute("SELECT uid, last_name_val, first_name_val, middle_name_val, street_val, build, build_k, apartment, telephone"
" FROM main"
" join last_name on main.last_name = last_name_id"
" join first_name on main.first_name = first_name_id"
" join middle_name on main.middle_name = middle_name_id"
" join street on main.street = street_id"
" WHERE uid = %s", (uid,))
contact = cur.fetchone()
if contact is None:
return {'message': 'Contact not found'}, 404
def convert(data):
return {
'uid': data[0],
'last_name': data[1],
'first_name': data[2],
'middle_name': data[3],
'street': data[4],
'build': data[5],
'build_k': data[6],
'apartment': data[7],
'telephone': data[8]
}
contact = convert(contact)
return contact
def delete(self, uid):
db = get_db()
cur = db.cursor()
cur.execute("SELECT uid FROM main WHERE uid = %s", (uid,))
contact = cur.fetchone()
if contact is None:
return {'message': 'Contact not found'}, 404
cur.execute("DELETE FROM main WHERE uid = %s", (uid,))
db.commit()
return {'message': 'Contact deleted'}
def put(self, uid):
parser = reqparse.RequestParser()
parser.add_argument('last_name')
parser.add_argument('first_name')
parser.add_argument('middle_name')
parser.add_argument('street')
parser.add_argument('build')
parser.add_argument('build_k')
parser.add_argument('apartment')
parser.add_argument('telephone')
args = parser.parse_args()
db = get_db()
cur = db.cursor()
cur.execute("SELECT uid FROM main WHERE uid = %s", (uid,))
contact = cur.fetchone()
if contact is None:
return {'message': 'Contact not found'}, 404
if args['last_name'] is not None:
cur.execute("SELECT last_name_id FROM last_name WHERE last_name_val = %s", (args['last_name'],))
last_name = cur.fetchone()
if last_name is None:
cur.execute("INSERT INTO last_name (last_name_val) VALUES (%s)", (args['last_name'],))
cur.execute("SELECT last_name_id FROM last_name WHERE last_name_val = %s", (args['last_name'],))
last_name = cur.fetchone()
last_name = last_name[0]
else:
last_name = last_name[0]
cur.execute("UPDATE main SET last_name = %s WHERE uid = %s", (last_name, uid))
if args['first_name'] is not None:
cur.execute("SELECT first_name_id FROM first_name WHERE first_name_val = %s", (args['first_name'],))
first_name = cur.fetchone()
if first_name is None:
cur.execute("INSERT INTO first_name (first_name_val) VALUES (%s)", (args['first_name'],))
cur.execute("SELECT first_name_id FROM first_name WHERE first_name_val = %s", (args['first_name'],))
first_name = cur.fetchone()
first_name = first_name[0]
else:
first_name = first_name[0]
cur.execute("UPDATE main SET first_name = %s WHERE uid = %s", (first_name, uid))
if args['middle_name'] is not None:
cur.execute("SELECT middle_name_id FROM middle_name WHERE middle_name_val = %s", (args['middle_name'],))
middle_name = cur.fetchone()
if middle_name is None:
cur.execute("INSERT INTO middle_name (middle_name_val) VALUES (%s)", (args['middle_name'],))
cur.execute("SELECT middle_name_id FROM middle_name WHERE middle_name_val = %s", (args['middle_name'],))
middle_name = cur.fetchone()
middle_name = middle_name[0]
else:
middle_name = middle_name[0]
cur.execute("UPDATE main SET middle_name = %s WHERE uid = %s", (middle_name, uid))
if args['street'] is not None:
cur.execute("SELECT street_id FROM street WHERE street_val = %s", (args['street'],))
street = cur.fetchone()
if street is None:
cur.execute("INSERT INTO street (street_val) VALUES (%s)", (args['street'],))
cur.execute("SELECT street_id FROM street WHERE street_val = %s", (args['street'],))
street = cur.fetchone()
street = street[0]
else:
street = street[0]
cur.execute("UPDATE main SET street = %s WHERE uid = %s", (street, uid))
if args['build'] is not None:
cur.execute("UPDATE main SET build = %s WHERE uid = %s", (args['build'], uid))
if args['build_k'] is not None:
cur.execute("UPDATE main SET build_k = %s WHERE uid = %s", (args['build_k'], uid))
if args['apartment'] is not None:
cur.execute("UPDATE main SET apartment = %s WHERE uid = %s", (args['apartment'], uid))
if args['telephone'] is not None:
cur.execute("UPDATE main SET telephone = %s WHERE uid = %s", (args['telephone'], uid))
db.commit()
return {'message': 'Contact updated'}

46
contacts/db.py

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
import psycopg2
import click
from flask import current_app, g
from flask.cli import with_appcontext
def get_db():
"""Connect to the application's configured database. The connection
is unique for each request and will be reused if this is called
again.
"""
if 'db' not in g:
g.db = psycopg2.connect(dbname="postgres", user="postgres", password="postgres", host="db")
return g.db
def close_db(e=None):
"""If this request connected to the database, close the
connection.
"""
db = g.pop('db', None)
if db is not None:
db.close()
def init_db():
"""Clear existing data and create new tables."""
db = get_db()
with current_app.open_resource('schema.sql') as f:
db.cursor().execute(f.read().decode('utf8'))
db.commit()
@click.command('init-db')
@with_appcontext
def init_db_command():
"""Clear existing data and create new tables."""
init_db()
click.echo('Initialized the database.')
def init_app(app):
"""Register database functions with the Flask app. This is called by
the application factory.
"""
app.teardown_appcontext(close_db)
app.cli.add_command(init_db_command)

37
contacts/schema.sql

@ -0,0 +1,37 @@ @@ -0,0 +1,37 @@
DROP TABLE IF EXISTS main;
DROP TABLE IF EXISTS first_name;
DROP TABLE IF EXISTS last_name;
DROP TABLE IF EXISTS middle_name;
DROP TABLE IF EXISTS street;
CREATE TABLE first_name (
first_name_id SERIAL PRIMARY KEY,
first_name_val VARCHAR(255) NOT NULL
);
CREATE TABLE last_name (
last_name_id SERIAL PRIMARY KEY,
last_name_val VARCHAR(255) NOT NULL
);
CREATE TABLE middle_name (
middle_name_id SERIAL PRIMARY KEY,
middle_name_val VARCHAR(255) NOT NULL
);
CREATE TABLE street (
street_id SERIAL PRIMARY KEY,
street_val VARCHAR(255) NOT NULL
);
CREATE TABLE main (
uid serial,
last_name int,
first_name int,
middle_name int,
street int,
build VARCHAR(255),
build_k VARCHAR(255),
apartment VARCHAR(255),
telephone VARCHAR(255) NOT NULL,
CONSTRAINT fk_last_name FOREIGN KEY (last_name) REFERENCES last_name(last_name_id),
CONSTRAINT fk_first_name FOREIGN KEY (first_name) REFERENCES first_name(first_name_id),
CONSTRAINT fk_middle_name FOREIGN KEY (middle_name) REFERENCES middle_name(middle_name_id),
CONSTRAINT fk_street FOREIGN KEY (street) REFERENCES street(street_id),
PRIMARY KEY (uid)
);

23
frontend/.gitignore vendored

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
# 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*

70
frontend/README.md

@ -0,0 +1,70 @@ @@ -0,0 +1,70 @@
# Getting Started with Create React 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 your browser.
The page will reload when you make changes.\
You may 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)

29172
frontend/package-lock.json generated

File diff suppressed because it is too large Load Diff

46
frontend/package.json

@ -0,0 +1,46 @@ @@ -0,0 +1,46 @@
{
"name": "frontend",
"version": "0.1.0",
"private": true,
"dependencies": {
"@emotion/react": "^11.7.1",
"@emotion/styled": "^11.6.0",
"@mui/icons-material": "^5.2.4",
"@mui/material": "^5.2.4",
"@mui/styles": "^5.2.3",
"@testing-library/jest-dom": "^5.16.1",
"@testing-library/react": "^12.1.2",
"@testing-library/user-event": "^13.5.0",
"axios": "^0.24.0",
"mui-datatables": "^4.0.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-query": "^3.34.3",
"react-scripts": "5.0.0",
"web-vitals": "^2.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"
]
}
}

BIN
frontend/public/favicon.ico

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

43
frontend/public/index.html

@ -0,0 +1,43 @@ @@ -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
frontend/public/logo192.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
frontend/public/logo512.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

25
frontend/public/manifest.json

@ -0,0 +1,25 @@ @@ -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
frontend/public/robots.txt

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

128
frontend/src/App.jsx

@ -0,0 +1,128 @@ @@ -0,0 +1,128 @@
import * as React from 'react';
import Grid from '@mui/material/Grid';
import { Container, Dialog, Typography, IconButton, Tooltip } from '@mui/material';
import MUIDataTable from "mui-datatables";
import { useAxios } from './axios';
import { useMutation, useQuery, useQueryClient } from 'react-query';
import ContactDialog from './ContactDialog';
import AddIcon from '@mui/icons-material/Add';
function App() {
const client = useAxios();
const queryClient = useQueryClient()
const [open, setOpen] = React.useState(false);
const [openedContact, setOpenedContact] = React.useState({});
const { data, status } = useQuery('contacts', async () => {
const {data} = await client.get('/api/contacts');
return data.map(({
uid,
first_name,
last_name,
middle_name,
telephone,
street,
build,
build_k,
apartment
}) => {
return {
'first_name': first_name,
'last_name': last_name,
'middle_name': middle_name,
'telephone': telephone,
'street': street,
'build': build,
'build_k': build_k,
'apartment': apartment,
'display_name': `${first_name} ${last_name} ${middle_name}`,
'display_address': `${street} ${build} ${build_k} ${apartment}`,
'uid': uid
};
})
});
const columns = [
{
name: "uid", label: "ID", options: {
display: false
}
},
{
name: "display_name", label: "Имя"
},
{
name: "telephone", label: "Телефон"
},
{
name: "display_address", label: "Адрес"
},
];
const commitDelete = useMutation(async (uid) => {
await client.delete(`/api/contacts/${uid}`);
}, {
onSuccess: () => {
queryClient.invalidateQueries('contacts');
}
});
const handleRowClick = (rowData) => {
setOpenedContact(data.find(({uid}) => uid === rowData[0]));
setOpen(true);
console.log(data.find(({uid}) => uid === rowData[0]));
};
const handleRowsDelete = (rowsDeleted) => {
const uids = rowsDeleted.data.map(({dataIndex}) => data[dataIndex]['uid']);
for (const uid of uids) {
commitDelete.mutate(uid);
}
};
const handleClose = () => {
setOpen(false);
};
if (data === undefined || status === 'loading') {
return <div>Loading...</div>
}
return (
<Container maxWidth="md">
<MUIDataTable
title={"Контактная книга"}
data={data}
columns={columns}
options={{
filterType: "textField",
print: false,
rowsPerPage: 10,
rowsPerPageOptions: [10, 20, 50, 100],
onRowClick: handleRowClick,
customToolbar: () => {
return <CustomToolbar />
},
onRowsDelete: handleRowsDelete
}}
/>
<ContactDialog
open={open}
onClose={handleClose}
contact={openedContact}
/>
</Container>
);
function CustomToolbar() {
if (data !== undefined) return (
<Tooltip title='Новый контакт'>
<IconButton onClick={() => {
setOpenedContact({});
setOpen(true);
}}>
<AddIcon />
</IconButton>
</Tooltip>
)
return (<></>)
}
}
export default App;

175
frontend/src/ContactDialog.jsx

@ -0,0 +1,175 @@ @@ -0,0 +1,175 @@
import * as React from 'react';
import Grid from '@mui/material/Grid';
import { Button, Container, Dialog, DialogActions, DialogContent, DialogTitle, TextField, Typography } from '@mui/material';
import MUIDataTable from "mui-datatables";
import { useAxios } from './axios';
import { useMutation, useQuery, useQueryClient } from 'react-query';
function ContactDialog({
open,
onClose,
contact,
}) {
const client = useAxios();
const queryClient = useQueryClient()
const [uid, setUid] = React.useState(contact ? contact.uid : null);
const [firstName, setFirstName] = React.useState(contact['first_name'] ?? '');
const [lastName, setLastName] = React.useState(contact['last_name'] ?? '');
const [middleName, setMiddleName] = React.useState(contact['middle_name'] ?? '');
const [telephone, setTelephone] = React.useState(contact['telephone'] ?? '');
const [street, setStreet] = React.useState(contact['street'] ?? '');
const [build, setBuild] = React.useState(contact['build'] ?? '');
const [buildK, setBuildK] = React.useState(contact['build_k'] ?? '');
const [apartment, setApartment] = React.useState(contact['apartment'] ?? '');
const commitCreate = useMutation(async (payload) => client.post('/api/contacts', payload), {
onSuccess: () => {
queryClient.invalidateQueries('contacts');
onClose();
}
});
const commitUpdate = useMutation(async (payload) => client.put(`/api/contacts/${contact['uid']}`, payload), {
onSuccess: () => {
queryClient.invalidateQueries('contacts');
queryClient.invalidateQueries(['contact', contact['uid']]);
onClose();
}
});
const handleFormSubmit = (event) => {
event.preventDefault();
const data = {
first_name: firstName,
last_name: lastName,
middle_name: middleName,
telephone: telephone,
street: street,
build: build,
build_k: buildK,
apartment: apartment
};
if (contact['uid']) {
commitUpdate.mutate(data);
}
else {
commitCreate.mutate(data);
}
onClose();
};
const loadData = () => {
console.log(contact);
setUid(contact['uid'] ?? null);
setFirstName(contact['first_name'] ?? '');
setLastName(contact['last_name'] ?? '');
setMiddleName(contact['middle_name'] ?? '');
setTelephone(contact['telephone'] ?? '');
setStreet(contact['street'] ?? '');
setBuild(contact['build'] ?? '');
setBuildK(contact['build_k'] ?? '');
setApartment(contact['apartment'] ?? '');
};
return (
<Dialog
open={open}
onClose={onClose}
fullWidth
maxWidth="md"
TransitionProps={{
onEnter: loadData
}}
>
<DialogTitle>Контакт</DialogTitle>
<form onSubmit={handleFormSubmit}>
<DialogContent dividers>
<Grid container spacing={3}>
<Grid item xs={12}>
<TextField
id="first_name"
label="Имя"
value={firstName}
onChange={(event) => setFirstName(event.target.value)}
fullWidth
/>
</Grid>
<Grid item xs={12}>
<TextField
id="last_name"
label="Фамилия"
value={lastName}
onChange={(event) => setLastName(event.target.value)}
fullWidth
/>
</Grid>
<Grid item xs={12}>
<TextField
id="middle_name"
label="Отчество"
value={middleName}
onChange={(event) => setMiddleName(event.target.value)}
fullWidth
/>
</Grid>
<Grid item xs={12}>
<TextField
id="telephone"
label="Телефон"
value={telephone}
onChange={(event) => setTelephone(event.target.value)}
fullWidth
required
/>
</Grid>
<Grid item xs={6}>
<TextField
id="street"
label="Улица"
value={street}
onChange={(event) => setStreet(event.target.value)}
fullWidth
/>
</Grid>
<Grid item xs={2}>
<TextField
id="build"
label="Дом"
value={build}
onChange={(event) => setBuild(event.target.value)}
fullWidth
/>
</Grid>
<Grid item xs={2}>
<TextField
id="build_k"
label="Корпус"
value={buildK}
onChange={(event) => setBuildK(event.target.value)}
fullWidth
/>
</Grid>
<Grid item xs={2}>
<TextField
id="apartment"
label="Квартира"
value={apartment}
onChange={(event) => setApartment(event.target.value)}
fullWidth
/>
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={onClose}>Отмена</Button>
<Button type="submit" color="primary">Сохранить</Button>
</DialogActions>
</form>
</Dialog>
);
}
export default ContactDialog;

33
frontend/src/axios.js

@ -0,0 +1,33 @@ @@ -0,0 +1,33 @@
import { createContext, useContext, useMemo } from 'react'
import Axios, { AxiosInstance } from 'axios'
export const BASE_URL = 'http://localhost:5000'
export const AxiosContext = createContext(undefined)
export default function AxiosProvider ({
children
}) {
const axios = useMemo(() => {
const axios = Axios.create({
baseURL: BASE_URL,
headers: {
'Content-Type': 'application/json'
}
})
return axios
}, [])
return (
<AxiosContext.Provider value={axios}> {children} </AxiosContext.Provider>
)
}
export function useAxios () {
const context = useContext(AxiosContext)
if (context !== undefined) {
return context
}
throw Error('useAxios must be called within axios provider!')
}

27
frontend/src/index.jsx

@ -0,0 +1,27 @@ @@ -0,0 +1,27 @@
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import AxiosProvider from './axios';
import { createTheme, ThemeProvider } from '@mui/material/styles'
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from 'react-query/devtools'
const queryClient = new QueryClient();
const theme = createTheme({
});
ReactDOM.render(
<React.StrictMode>
<ThemeProvider theme={theme}>
<AxiosProvider>
<QueryClientProvider client={queryClient}>
<App />
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
</AxiosProvider>
</ThemeProvider>
</React.StrictMode>,
document.getElementById('root')
);

5
requirements.txt

@ -0,0 +1,5 @@ @@ -0,0 +1,5 @@
flask
flask_restful
flask_socketio
pytz
psycopg2
Loading…
Cancel
Save