diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 73f69e0..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,8 +0,0 @@ -# Default ignored files -/shelf/ -/workspace.xml -# Datasource local storage ignored files -/dataSources/ -/dataSources.local.xml -# Editor-based HTTP Client requests -/httpRequests/ diff --git a/.idea/codeStyles/codeStyleConfig.xml b/.idea/codeStyles/codeStyleConfig.xml deleted file mode 100644 index a55e7a1..0000000 --- a/.idea/codeStyles/codeStyleConfig.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml deleted file mode 100644 index 615c24b..0000000 --- a/.idea/dataSources.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - postgresql - true - org.postgresql.Driver - jdbc:postgresql://dev.teamortix.com:5432/hamza - - - \ No newline at end of file diff --git a/.idea/dictionaries/dev.xml b/.idea/dictionaries/dev.xml deleted file mode 100644 index 283ade4..0000000 --- a/.idea/dictionaries/dev.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - navbar - - - \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml deleted file mode 100644 index d5cfc95..0000000 --- a/.idea/inspectionProfiles/Project_Default.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index e0a50ca..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 912b893..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 35eb1dd..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 8d09836..9bf51d4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3728,6 +3728,23 @@ } } }, + "compute-iqr": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/compute-iqr/-/compute-iqr-1.1.0.tgz", + "integrity": "sha1-Lqrmncpv+BCz/o8F8MZGwBmM7mc=", + "requires": { + "compute-quantile": "^1.0.0", + "validate.io-object": "^1.0.0" + } + }, + "compute-quantile": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/compute-quantile/-/compute-quantile-1.0.1.tgz", + "integrity": "sha1-MHI7rGFtnMXifA4HdanTVn37rWY=", + "requires": { + "validate.io-object": "^1.0.0" + } + }, "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -5477,6 +5494,11 @@ "strip-eof": "^1.0.0" } }, + "exenv": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz", + "integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50=" + }, "exit": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", @@ -5936,6 +5958,11 @@ "write": "1.0.3" } }, + "flatpickr": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.3.tgz", + "integrity": "sha512-007VucCkqNOMMb9ggRLNuJowwaJcyOh4sKAFcdGfahfGc7JQbf94zSzjdBq/wVyHWUEs5o3+idhFZ0wbZMRmVQ==" + }, "flatted": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", @@ -5980,22 +6007,9 @@ } }, "follow-redirects": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz", - "integrity": "sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==", - "requires": { - "debug": "^3.0.0" - }, - "dependencies": { - "debug": { - "version": "3.2.6", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz", - "integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==", - "requires": { - "ms": "^2.1.1" - } - } - } + "version": "1.12.1", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.12.1.tgz", + "integrity": "sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg==" }, "for-in": { "version": "1.0.2", @@ -6663,9 +6677,9 @@ "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" }, "http-proxy": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", - "integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", "requires": { "eventemitter3": "^4.0.0", "follow-redirects": "^1.0.0", @@ -8111,9 +8125,9 @@ } }, "lodash": { - "version": "4.17.15", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", - "integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" + "version": "4.17.19", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz", + "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ==" }, "lodash-es": { "version": "4.17.15", @@ -11402,11 +11416,28 @@ "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz", "integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==" }, + "react-flatpickr": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/react-flatpickr/-/react-flatpickr-3.10.0.tgz", + "integrity": "sha512-eeU5OlUZgWDyp2XvIeZ4+rI4T4VkqU1MdZUb+cXtTHfHYfLV+gpP0lAhRrij+QXlbPXTJFAbPerihohlOeTJ0A==", + "requires": { + "flatpickr": "^4.5.7", + "prop-types": "^15.5.10" + } + }, "react-hook-form": { "version": "5.7.2", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-5.7.2.tgz", "integrity": "sha512-bJvY348vayIvEUmSK7Fvea/NgqbT2racA2IbnJz/aPlQ3GBtaTeDITH6rtCa6y++obZzG6E3Q8VuoXPir7QYUg==" }, + "react-icons": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.10.0.tgz", + "integrity": "sha512-WsQ5n1JToG9VixWilSo1bHv842Cj5aZqTGiS3Ud47myF6aK7S/IUY2+dHcBdmkQcCFRuHsJ9OMUI0kTDfjyZXQ==", + "requires": { + "camelcase": "^5.0.0" + } + }, "react-is": { "version": "16.13.1", "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", @@ -11417,6 +11448,17 @@ "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" }, + "react-modal": { + "version": "3.11.2", + "resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.11.2.tgz", + "integrity": "sha512-o8gvvCOFaG1T7W6JUvsYjRjMVToLZgLIsi5kdhFIQCtHxDkA47LznX62j+l6YQkpXDbvQegsDyxe/+JJsFQN7w==", + "requires": { + "exenv": "^1.2.0", + "prop-types": "^15.5.10", + "react-lifecycles-compat": "^3.0.0", + "warning": "^4.0.3" + } + }, "react-redux": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz", @@ -13955,6 +13997,19 @@ "spdx-expression-parse": "^3.0.0" } }, + "validate.io-array": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz", + "integrity": "sha1-W1osr9j4uFq7L4hroVPy2Tond00=" + }, + "validate.io-object": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/validate.io-object/-/validate.io-object-1.0.4.tgz", + "integrity": "sha1-3KAezu45DhENvCr4Q8gfe/M6Qas=", + "requires": { + "validate.io-array": "^1.0.1" + } + }, "value-equal": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", @@ -14016,6 +14071,14 @@ "makeerror": "1.0.x" } }, + "warning": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz", + "integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==", + "requires": { + "loose-envify": "^1.0.0" + } + }, "watchpack": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", diff --git a/package.json b/package.json index bb9c213..b395369 100644 --- a/package.json +++ b/package.json @@ -6,13 +6,17 @@ "@testing-library/jest-dom": "^4.2.4", "@testing-library/react": "^9.5.0", "@testing-library/user-event": "^7.2.1", + "compute-iqr": "^1.1.0", "cross-env": "^7.0.2", "cssnano": "^4.1.10", "griddle-react": "^1.13.1", "mathjs": "^7.0.0", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-flatpickr": "^3.10.0", "react-hook-form": "^5.7.2", + "react-icons": "^3.10.0", + "react-modal": "^3.11.2", "react-router-dom": "^5.2.0", "react-scripts": "3.4.1", "react-toastify": "^6.0.4", diff --git a/postcss.config.js b/postcss.config.js index 43eaef7..bcc8f1a 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,30 +1,29 @@ -const tailwindcss = require('tailwindcss'); -const nano = require('cssnano') - -const purgecss = require('@fullhuman/postcss-purgecss')({ -// - content: [ - "./src/components/*.js", - "./node_modules/react-toastify/dist/ReactToastify.min.css", - "./public/index.html", - "./src/custom.js", - "./src/App.js", - ], - - defaultExtractor: content => { - const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || [] - const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || [] - - return broadMatches.concat(innerMatches) - } -}) - -module.exports = { - plugins: [ - tailwindcss('./tailwind.js'), - require('autoprefixer'), - ...process.env.NODE_ENV === 'production' - ? [purgecss, nano] - : [] - ] +const tailwindcss = require('tailwindcss'); +const nano = require('cssnano') + +const purgecss = require('@fullhuman/postcss-purgecss')({ +// + content: [ + "./src/components/*.js", + "./node_modules/react-toastify/dist/ReactToastify.min.css", + "./public/index.html", + "./src/custom.js", + "./src/App.js", + ], + + defaultExtractor: content => content.match(/[\w-:/]+(? - + Rain Track diff --git a/raintrack-ui.iml b/raintrack-ui.iml index 78aaf41..da07e02 100644 --- a/raintrack-ui.iml +++ b/raintrack-ui.iml @@ -1,10 +1,11 @@ - - - - - - - - - + + + + + + + + + + \ No newline at end of file diff --git a/src/App.js b/src/App.js index a23a51a..0901e9c 100644 --- a/src/App.js +++ b/src/App.js @@ -5,7 +5,6 @@ import 'react-toastify/dist/ReactToastify.min.css' import './styles/app.css' import {authenticate, defaultAuth, reducer} from "./auth"; - // pages import Home from "./components/Home"; import Login from "./components/Login"; @@ -14,6 +13,8 @@ import Register from "./components/Register"; import Upload from "./components/Upload"; import NotFound from "./components/NotFound"; import ViewAll from "./components/ViewAll"; +import Admin from "./components/Admin"; +import MyData from "./components/ViewOwn"; export const AuthContext = React.createContext(undefined) @@ -37,6 +38,9 @@ function App() { + + + diff --git a/src/auth.js b/src/auth.js index a8fcbf0..cc38d6e 100644 --- a/src/auth.js +++ b/src/auth.js @@ -1,74 +1,75 @@ -import {toast} from "react-toastify"; - -// export const SERVER_URL = "http://192.168.1.8:8080/api" -export const SERVER_URL = "http://dev.teamortix.com:4290/api" - -export const defaultAuth = () => ({ - loggedIn: window.localStorage.getItem("authtoken") !== null, - token: window.localStorage.getItem("authtoken"), - username: '', - email: '', - admin: false, -}) - - -export const reducer = (state, action) => { - return {...state, ...action.response} -} - -export async function authenticate(token) { - if (token == null) - return defaultAuth() - - const {status, json} = await query("/account/info", {token}).catch(() => { - toast.error("The authentication server is offline!") - return defaultAuth() - }) - - if (status !== 200) { - window.localStorage.removeItem("authtoken") - toast.warn("You have been logged out.") - return defaultAuth() - } - console.debug("Authentication Response:", json) - - const {email, username, admin} = json; - return {username, email, admin, loggedIn: true} -} - -export async function registration(username, email, password) { - const {status, json} = await query("/register", {username, email, password}).catch(() => { - return {status: 500, json: {reason: "The authentication server is offline!"}} - }) - - console.debug("Registration Response:", json) - return {status, json} -} - -export async function login(usernameOrEmail, password) { - const {status, json} = await query("/login", {usernameOrEmail, password}).catch(() => { - return {status: 500, json: {reason: "The authentication server is offline!"}} - }) - console.debug("Login Response:", json) - - return {status, json} -} - -export function logout(token) { - window.localStorage.removeItem("authtoken") - query("/logout", {token}).then(() => { - toast.warn("You have been logged out.") - }) - return defaultAuth() -} - -export async function query(url, data) { - console.debug("FETCH: " + SERVER_URL + url + " - " + JSON.stringify(data)) - let response = await fetch(SERVER_URL + url, { - method: "POST", - headers: {"Content-Type": "application/json"}, - body: JSON.stringify(data) - }) - - return {status: response.status, json: await response.json()} +import {toast} from "react-toastify"; + +// export const SERVER_URL = "http://192.168.1.8:8080/api" +export const SERVER_URL = "http://dev.teamortix.com:4290/api" + +export const defaultAuth = () => ({ + loggedIn: window.localStorage.getItem("authtoken") !== null, + token: window.localStorage.getItem("authtoken"), + username: '', + email: '', + admin: false, +}) + + +export const reducer = (state, action) => { + return {...state, ...action.response} +} + +export async function authenticate(token) { + if (token == null) + return defaultAuth() + + const {status, json} = await query("/account/info", {token}).catch(() => { + toast.error("The authentication server is offline!") + return defaultAuth() + }) + + if (status !== 200) { + window.localStorage.removeItem("authtoken") + toast.warn("You have been logged out.") + return defaultAuth() + } + console.debug("Authentication Response:", json) + + const {email, username, admin} = json; + return {username, email, admin, loggedIn: true} +} + +export async function registration(username, email, password) { + const {status, json} = await query("/register", {username, email, password}).catch(() => { + return {status: 500, json: {reason: "The authentication server is offline!"}} + }) + + console.debug("Registration Response:", json) + return {status, json} +} + +export async function login(usernameOrEmail, password) { + const {status, json} = await query("/login", {usernameOrEmail, password}).catch(() => { + return {status: 500, json: {reason: "The authentication server is offline!"}} + }) + console.debug("Login Response:", json) + + return {status, json} +} + +export function logout(token) { + window.localStorage.removeItem("authtoken") + query("/logout", {token}).then(() => { + toast.warn("You have been logged out.") + }) + return defaultAuth() +} + +export async function query(url, data) { + console.debug("FETCH: " + SERVER_URL + url + " - " + JSON.stringify(data)) + let response = await fetch(SERVER_URL + url, { + method: "POST", + headers: {"Content-Type": "application/json"}, + body: JSON.stringify(data) + }) + const json = await response.json() + console.debug(json) + return {status: response.status, json} } \ No newline at end of file diff --git a/src/components/Admin.js b/src/components/Admin.js new file mode 100644 index 0000000..da324c2 --- /dev/null +++ b/src/components/Admin.js @@ -0,0 +1,243 @@ +import React, {useCallback, useContext, useEffect, useReducer, useState} from 'react'; +import Navbar from "./Navbar"; + +import {AuthContext} from "../App"; +import Griddle, {ColumnDefinition, plugins, RowDefinition} from "griddle-react"; +import {query} from "../auth"; +import "flatpickr/dist/themes/material_blue.css"; +import "../styles/tables.css" +import Flatpickr from "react-flatpickr"; +import {Input} from "../custom"; +import {toast} from "react-toastify"; +import {AiFillDelete, AiFillEdit} from "react-icons/ai"; +import Modal from "react-modal" +import FileManualForm from "./FileManualForm"; + + +const background = require('../assets/wave.png') + +const defaultData = () => [{ + ID: "No Data Found", + Date: "", + Precipitation: "", + Latitude: "", + Longitude: "", + Remarks: "", + Edit: "", +}] + +function Admin({history, ...props}) { + const {authState} = useContext(AuthContext) + const [data, setData] = useState(defaultData()) + const [items, setItems] = useState(10) + const [status, setStatus] = useState("") + const initialDates = () => { + const now = new Date() + const lastYear = new Date(); + lastYear.setFullYear(lastYear.getFullYear() - 1) + return [lastYear, now] + } + const [dateRange, setDateRange] = useState([]); + const [editModal, setEditModal] = useState({open: false, data: {}}) + const [updateData, dispatchEdit] = useReducer((state, action) => action, []) + + useEffect(() => { + const params = new URLSearchParams(props.location.search) + let range = initialDates() + if (params.get("after")) range[0] = new Date(params.get("after")) + if (params.get("before")) range[1] = new Date(params.get("before")) + setDateRange(range) + }, []) + + useEffect(() => { + console.log(authState) + if (!authState.loggedIn) history.push("/login?return=admin") + if (!authState.admin) history.push("/") + }, [authState, history]) + + const deleteRow = async (data) => { + const {status, json} = await query("/data/delete", data).catch(e => { + console.debug(e) + return {status: 500, json: {reason: "The upload server is offline!"}} + }) + console.debug("Delete Row Response", json) + return {status, json} + } + + const initiateData = useCallback(async () => { + if (dateRange.length !== 2 || !authState.username) return + let after = dateRange[0] + let before = dateRange[1] + before.setDate(before.getDate() + 1) + after.setDate(after.getDate() - 1) + + + const now = new Date(); + now.setDate(now.getDate() + 5) + const hundred = new Date(1900, 0, 0) + + if (after > now || after < hundred || + before > now || before < hundred) + return toast.error("Invalid Date Range!") + + setStatus("Loading Data...") + let {json, status} = await query("/data/query", { + token: authState.token, + after_date: after, + before_date: before, + username: authState.username, + }).catch(() => ({status: 500, json: {reason: "The authentication server is offline!"}})) + setStatus("") + if (status !== 200) return toast.error(json.reason) + + let newData = defaultData() + + const formatDate = (date) => { + return date.getFullYear() + "-" + (date.getMonth() + 1).toString().padStart(2, "0") + "-" + date.getDate().toString().padStart(2, "0") + } + + json.entries.forEach((entry, index) => { + const date = new Date(entry.date); + + const group = json.groups.find(gr => gr.id === entry.group_id) + newData[index] = { + ID: entry.id, + Date: formatDate(date), + Precipitation: entry.precipitation, + Latitude: group.latitude, + Longitude: group.longitude, + Verified: group.validated ? "Yes" : "No", + Remarks: entry.remarks, + } + }) + setData(newData) + + before.setDate(before.getDate() - 1) + after.setDate(after.getDate() + 1) + window.history.pushState("Search", "Rain Track", "/mydata?after=" + formatDate(after) + "&before=" + formatDate(before)) + }, [setData, authState.token, dateRange, authState.username]) + + useEffect(() => { + (async () => { + initiateData() + })(); + }, [setData, initiateData, dateRange, authState.username]) + const styleConfig = { + classNames: { + Filter: "input h-8", + Table: "table table-bordered table-hover table-striped lg:overflow-x-auto overflow-y-scroll", + PreviousButton: "border border-gray-300 px-2 py-1 w-20 mr-2 rounded", + NextButton: "border border-gray-300 px-2 py-1 w-20 rounded ml-2" + } + } + useEffect(() => { + if (updateData.length !== 0) { + const row = data[updateData[1]] + if (!row) return + switch (updateData[0]) { + case 0: + setEditModal({open: true, data: row}) + break; + case 1: + window.confirm("Are you sure you would like to delete this row?") + && (async () => { + let {json, status} = await deleteRow({token: authState.token, id: row.ID}) + if (status !== 200) return toast.error(json.reason) + toast.success("Entry successfully deleted") + dispatchEdit([]) + setData(data.filter(r => r !== row)) + })() + break; + default: + break; + } + } + }, [updateData, data, authState]) + + + return ( +
+ +
+
+ setDateRange(date)} + options={{ + dateFormat: "m-d-Y", + mode: "range", + wrap: true, + position: "below" + }}> +
+ + Select Date Range + +
+
+
{status}
+
+ +
+ + + + + + + + + + }/> + + +
+ setEditModal({open: false, data: {ID: 0}})} + shouldCloseOnOverlayClick + isOpen={editModal.open}> +
+ +
+
+
+ ); +} + +export default Admin; + +const Layout = (setItems) => (({Table, Filter, Pagination}) => ( +
+
+ + +
+
+ + + + +)) + +const EditComponent = ({griddleKey, dispatch}) => { + + return
+ dispatch([0, griddleKey])}> + dispatch([1, griddleKey])}> +
+} \ No newline at end of file diff --git a/src/components/FileManualForm.js b/src/components/FileManualForm.js index eaa2854..39a5620 100644 --- a/src/components/FileManualForm.js +++ b/src/components/FileManualForm.js @@ -1,98 +1,116 @@ -import React, {useContext, useState} from 'react'; -import {toast} from "react-toastify"; -import {AuthContext} from "../App"; -import {useForm} from "react-hook-form"; -import {query} from "../auth"; -import {Input} from "../custom"; - -function FileManualForm() { - const {authState} = useContext(AuthContext) - const [manualError, setManualError] = useState("") - const {register, handleSubmit, errors, setError, reset} = useForm(); - - const manualUpload = async (data) => { - const {status, json} = await query("/data/add", data).catch(() => { - return {status: 500, json: {reason: "The upload server is offline!"}} - }) - console.debug("Manual Upload Response:", json) - return {status, json} - } - - function submitManual(data) { - let req = data - - let split = data.date.split(/[/-]/); - // noinspection JSCheckFunctionSignatures - let date = new Date(split[2], Number(split[0]) - 1, split[1]) - req.date = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() - if (date > new Date()) return setError("date", "time", "Your date must be in the past.") - if (date < new Date(1900, 0, 0)) return setError("date", "time", "Your date must be after 1900.") - if (date.getDay() === 0) return setError("date", "format", "Invalid Date format (MM-DD-YYYY)") - - if (isNaN(parseFloat(data["precipitation"]))) { - setError("precipitation", "format", "Invalid format. Precipitation must be a number. E.g 0.24") - } - req.precipitation = parseFloat(data["precipitation"]) - req.token = authState.token; - - (async () => { - let {json, status} = await manualUpload(data) - if (status !== 200) return setManualError(json.reason) - reset() - toast.success("Entry successfully uploaded") - })(); - } - - - return ( -
- -
Upload Individual Entry
- {manualError &&
- {manualError} -
- } - - - - - - - - - - - -
- -
- - - ); -} - +import React, {useContext, useEffect, useState} from 'react'; +import {toast} from "react-toastify"; +import {AuthContext} from "../App"; +import {useForm} from "react-hook-form"; +import {query} from "../auth"; +import {Input} from "../custom"; + +function FileManualForm({id, title = "Upload Individual Entry", defaultData = {}, editType: edit = false}) { + const {authState} = useContext(AuthContext) + const [manualError, setManualError] = useState("") + const {register, handleSubmit, errors, setError, reset, setValue} = useForm(); + + const manualUpload = async (data) => { + const {status, json} = await query("/data/add", data).catch(() => { + return {status: 500, json: {reason: "The upload server is offline!"}} + }) + console.debug("Manual Upload Response:", json) + return {status, json} + } + + function submitManual(data) { + let req = data + + let split = data.date.split(/[/-]/); + // noinspection JSCheckFunctionSignatures + let date = new Date(split[2], Number(split[0]) - 1, split[1]) + req.date = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() + if (date > new Date()) return setError("date", "time", "Your date must be in the past.") + if (date < new Date(1900, 0, 0)) return setError("date", "time", "Your date must be after 1900.") + if (date.getDate() === 0) return setError("date", "format", "Invalid Date format (MM-DD-YYYY)") + + if (isNaN(parseFloat(data["precipitation"]))) { + setError("precipitation", "format", "Invalid format. Precipitation must be a number. E.g 0.24") + } + req.precipitation = parseFloat(data["precipitation"]) + req.id = id + req.token = authState.token; + + (async () => { + let {json, status} = await manualUpload(data) + if (status !== 200) return setManualError(json.reason) + reset() + if (edit) window.location.reload() + toast.success("Entry successfully uploaded") + })(); + } + + const formClasses = edit + ? "bg-white shadow-lg p-4 overflow-hidden rounded-lg my-6 px-6 max-w-lg w-128" + : "bg-white shadow-lg p-4 overflow-hidden rounded-lg my-6 px-6 w-11/12 lg:mx-2 lg:my-5 lg:px-5 lg:w-5/12" + + useEffect(() => { + if (edit) { + const date = new Date(defaultData.Date) + const formattedDate = (date.getMonth() + 1).toString().padStart(2, "0") + "-" + date.getDate().toString().padStart(2, "0") + "-" + date.getFullYear() + setValue([ + {date: formattedDate}, + {precipitation: defaultData.Precipitation}, + {latitude: defaultData.Latitude}, + {longitude: defaultData.Longitude}, + {remarks: defaultData.Remarks}, + ]) + } + }, [defaultData, edit, setValue]) + + return ( +
+ +
{title}
+ {manualError &&
+ {manualError} +
+ } + + + + + + + + + + + +
+ +
+ + + ); +} + export default FileManualForm; \ No newline at end of file diff --git a/src/components/FileUploadForm.js b/src/components/FileUploadForm.js index 5fa0fe5..e5febf5 100644 --- a/src/components/FileUploadForm.js +++ b/src/components/FileUploadForm.js @@ -1,129 +1,129 @@ -import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; -import {toast} from "react-toastify"; -import {AuthContext} from "../App"; -import {SERVER_URL} from "../auth"; -import {Input} from "../custom"; - -const LIMIT = 2 * 1024 * 1024; // 2 mb - -const dispatch = (state, action) => { - switch (action.type) { - case "lat": - return {...state, lat: action.value} - case "long": - return {...state, long: action.value} - default: - return; - } -} - -function FileUploadForm() { - const {authState} = useContext(AuthContext) - const [files, setFiles] = useState(null); - const [load, setLoad] = useState(0) - const [status, setStatus] = useState("Upload a file") - const [latLong, setLatLong] = useReducer(dispatch, {lat: "", long: ""}, undefined) - let form = useRef(null); - - async function query(url, data) { - console.debug("FETCH: " + SERVER_URL + url + " - " + data) - return new Promise((resolve, reject) => { - let req = new XMLHttpRequest() - req.open('post', SERVER_URL + url) - req.upload.addEventListener('progress', e => { - if (e.loaded === e.total) setStatus("Processing...") - else { - let percent = (e.loaded / e.total * 100) - setLoad(percent) - setStatus("Uploading " + percent.toFixed(0) + "%") - } - }); - req.addEventListener('load', () => { - resolve({status: req.status, json: JSON.parse(req.response)}) - }); - req.addEventListener('error', e => { - resolve({status: req.status, json: JSON.parse(req.response)}) - }) - req.send(data) - }) - } - - const upload = useCallback(async () => { - if (!files || files.length === 0) return - const target = files[0] - if (target.size > LIMIT) return toast.error("File size too large! Max 2 mb.") - if (!target.name.includes(".xlsx")) return toast.error("Invalid file type!") - const {lat, long} = latLong; - - setStatus("Uploading") - - let file = new FormData() - file.append("file", target) - file.append("token", authState.token); - file.append("latitude", lat); - file.append("longitude", long); - - let {status, json} = await query("/data/upload", file) - form.current.reset(); - - setStatus("Upload a file") - setLoad(0) - - if (status !== 200) return toast.error(json.reason) - return toast.success("File successfully uploaded.") - }, [files, authState.token, form, latLong]) - - useEffect(() => { - (async () => upload())(); - }, [files, authState.token, upload, authState, setLoad, setStatus]) - - return
-
-
Upload File Entries
-
- Click the button to upload an .xlsx file -
-
-
-
-

Latitude and longitude is required if your data doesn't have it

- - setLatLong({type: "lat", value: e})}/> - setLatLong({type: "long", value: e})}/> - -
- -