last changed

master
ALI Hamza 2021-08-10 15:14:26 +07:00
parent 9d6f948c51
commit 71b88f91d8
Signed by: hamza
GPG Key ID: 22473A32291F8CB6
33 changed files with 1602 additions and 1225 deletions

8
.idea/.gitignore vendored

@ -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/

@ -1,5 +0,0 @@
<component name="ProjectCodeStyleConfiguration">
<state>
<option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
</state>
</component>

@ -1,11 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="hamza@dev.teamortix.com" uuid="84aa8c1d-dc47-44b7-8255-6c8c16c0c001">
<driver-ref>postgresql</driver-ref>
<synchronize>true</synchronize>
<jdbc-driver>org.postgresql.Driver</jdbc-driver>
<jdbc-url>jdbc:postgresql://dev.teamortix.com:5432/hamza</jdbc-url>
</data-source>
</component>
</project>

@ -1,7 +0,0 @@
<component name="ProjectDictionaryState">
<dictionary name="dev">
<words>
<w>navbar</w>
</words>
</dictionary>
</component>

@ -1,11 +0,0 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="CssInvalidAtRule" enabled="false" level="ERROR" enabled_by_default="false" />
<inspection_tool class="ES6CheckImport" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="JSCheckFunctionSignatures" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="JSUnresolvedFunction" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="JSUnresolvedVariable" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
<inspection_tool class="JSUnusedGlobalSymbols" enabled="false" level="WARNING" enabled_by_default="false" />
</profile>
</component>

@ -1,9 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptSettings">
<option name="languageLevel" value="JSX" />
</component>
<component name="ProjectRootManager">
<output url="file://$PROJECT_DIR$/out" />
</component>
</project>

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/raintrack-ui.iml" filepath="$PROJECT_DIR$/raintrack-ui.iml" />
</modules>
</component>
</project>

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>

107
package-lock.json generated

@ -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": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -5477,6 +5494,11 @@
"strip-eof": "^1.0.0" "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": { "exit": {
"version": "0.1.2", "version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@ -5936,6 +5958,11 @@
"write": "1.0.3" "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": { "flatted": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
@ -5980,22 +6007,9 @@
} }
}, },
"follow-redirects": { "follow-redirects": {
"version": "1.11.0", "version": "1.12.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.12.1.tgz",
"integrity": "sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==", "integrity": "sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg=="
"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"
}
}
}
}, },
"for-in": { "for-in": {
"version": "1.0.2", "version": "1.0.2",
@ -6663,9 +6677,9 @@
"integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q=" "integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q="
}, },
"http-proxy": { "http-proxy": {
"version": "1.18.0", "version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz", "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==", "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"requires": { "requires": {
"eventemitter3": "^4.0.0", "eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0", "follow-redirects": "^1.0.0",
@ -8111,9 +8125,9 @@
} }
}, },
"lodash": { "lodash": {
"version": "4.17.15", "version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==" "integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
}, },
"lodash-es": { "lodash-es": {
"version": "4.17.15", "version": "4.17.15",
@ -11402,11 +11416,28 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
"integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA==" "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": { "react-hook-form": {
"version": "5.7.2", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-5.7.2.tgz", "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-5.7.2.tgz",
"integrity": "sha512-bJvY348vayIvEUmSK7Fvea/NgqbT2racA2IbnJz/aPlQ3GBtaTeDITH6rtCa6y++obZzG6E3Q8VuoXPir7QYUg==" "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": { "react-is": {
"version": "16.13.1", "version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", "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", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" "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": { "react-redux": {
"version": "5.1.2", "version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz", "resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz",
@ -13955,6 +13997,19 @@
"spdx-expression-parse": "^3.0.0" "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": { "value-equal": {
"version": "1.0.1", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
@ -14016,6 +14071,14 @@
"makeerror": "1.0.x" "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": { "watchpack": {
"version": "1.6.1", "version": "1.6.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz",

@ -6,13 +6,17 @@
"@testing-library/jest-dom": "^4.2.4", "@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0", "@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1", "@testing-library/user-event": "^7.2.1",
"compute-iqr": "^1.1.0",
"cross-env": "^7.0.2", "cross-env": "^7.0.2",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"griddle-react": "^1.13.1", "griddle-react": "^1.13.1",
"mathjs": "^7.0.0", "mathjs": "^7.0.0",
"react": "^16.13.1", "react": "^16.13.1",
"react-dom": "^16.13.1", "react-dom": "^16.13.1",
"react-flatpickr": "^3.10.0",
"react-hook-form": "^5.7.2", "react-hook-form": "^5.7.2",
"react-icons": "^3.10.0",
"react-modal": "^3.11.2",
"react-router-dom": "^5.2.0", "react-router-dom": "^5.2.0",
"react-scripts": "3.4.1", "react-scripts": "3.4.1",
"react-toastify": "^6.0.4", "react-toastify": "^6.0.4",

@ -1,30 +1,29 @@
const tailwindcss = require('tailwindcss'); const tailwindcss = require('tailwindcss');
const nano = require('cssnano') const nano = require('cssnano')
const purgecss = require('@fullhuman/postcss-purgecss')({ const purgecss = require('@fullhuman/postcss-purgecss')({
// //
content: [ content: [
"./src/components/*.js", "./src/components/*.js",
"./node_modules/react-toastify/dist/ReactToastify.min.css", "./node_modules/react-toastify/dist/ReactToastify.min.css",
"./public/index.html", "./public/index.html",
"./src/custom.js", "./src/custom.js",
"./src/App.js", "./src/App.js",
], ],
defaultExtractor: content => { defaultExtractor: content => content.match(/[\w-:/]+(?<!:)/g) || []
const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []
const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || [] })
return broadMatches.concat(innerMatches) const tailwind = tailwindcss('./tailwind.js')
} console.log(tailwind)
})
module.exports = {
module.exports = { plugins: [
plugins: [ tailwind,
tailwindcss('./tailwind.js'), require('autoprefixer'),
require('autoprefixer'), ...process.env.NODE_ENV === 'production'
...process.env.NODE_ENV === 'production' ? [purgecss, nano]
? [purgecss, nano] : []
: [] ]
]
} }

@ -5,10 +5,8 @@
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/> <link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/> <meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="theme-color" content="#4670de"/> <meta name="theme-color" content="#4670de"/>
<meta <meta name="description"
name="description" content="RainTrack manages your rainfall data. Start uploading now."/>
content="RainTrack manages your rainfall data. Start uploading now."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/> <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/> <link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
<title>Rain Track</title> <title>Rain Track</title>

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<module type="WEB_MODULE" version="4"> <module type="WEB_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true"> <component name="Go" enabled="true" />
<exclude-output /> <component name="NewModuleRootManager" inherit-compiler-output="true">
<content url="file://$MODULE_DIR$"> <exclude-output />
<excludeFolder url="file://$MODULE_DIR$/build" /> <content url="file://$MODULE_DIR$">
</content> <excludeFolder url="file://$MODULE_DIR$/build" />
<orderEntry type="sourceFolder" forTests="false" /> </content>
</component> <orderEntry type="sourceFolder" forTests="false" />
</component>
</module> </module>

@ -5,7 +5,6 @@ import 'react-toastify/dist/ReactToastify.min.css'
import './styles/app.css' import './styles/app.css'
import {authenticate, defaultAuth, reducer} from "./auth"; import {authenticate, defaultAuth, reducer} from "./auth";
// pages // pages
import Home from "./components/Home"; import Home from "./components/Home";
import Login from "./components/Login"; import Login from "./components/Login";
@ -14,6 +13,8 @@ import Register from "./components/Register";
import Upload from "./components/Upload"; import Upload from "./components/Upload";
import NotFound from "./components/NotFound"; import NotFound from "./components/NotFound";
import ViewAll from "./components/ViewAll"; import ViewAll from "./components/ViewAll";
import Admin from "./components/Admin";
import MyData from "./components/ViewOwn";
export const AuthContext = React.createContext(undefined) export const AuthContext = React.createContext(undefined)
@ -37,6 +38,9 @@ function App() {
<Route path="/upload" exact component={Upload}/> <Route path="/upload" exact component={Upload}/>
<Route path="/all" exact component={ViewAll}/> <Route path="/all" exact component={ViewAll}/>
<Route path="/mydata" exact component={MyData}/>
<Route path="/admin" exact component={Admin}/>
<Route path="/" exact component={Home}/> <Route path="/" exact component={Home}/>
<Route path="/" component={NotFound}/> <Route path="/" component={NotFound}/>

@ -1,74 +1,75 @@
import {toast} from "react-toastify"; import {toast} from "react-toastify";
// export const SERVER_URL = "http://192.168.1.8:8080/api" // export const SERVER_URL = "http://192.168.1.8:8080/api"
export const SERVER_URL = "http://dev.teamortix.com:4290/api" export const SERVER_URL = "http://dev.teamortix.com:4290/api"
export const defaultAuth = () => ({ export const defaultAuth = () => ({
loggedIn: window.localStorage.getItem("authtoken") !== null, loggedIn: window.localStorage.getItem("authtoken") !== null,
token: window.localStorage.getItem("authtoken"), token: window.localStorage.getItem("authtoken"),
username: '', username: '',
email: '', email: '',
admin: false, admin: false,
}) })
export const reducer = (state, action) => { export const reducer = (state, action) => {
return {...state, ...action.response} return {...state, ...action.response}
} }
export async function authenticate(token) { export async function authenticate(token) {
if (token == null) if (token == null)
return defaultAuth() return defaultAuth()
const {status, json} = await query("/account/info", {token}).catch(() => { const {status, json} = await query("/account/info", {token}).catch(() => {
toast.error("The authentication server is offline!") toast.error("The authentication server is offline!")
return defaultAuth() return defaultAuth()
}) })
if (status !== 200) { if (status !== 200) {
window.localStorage.removeItem("authtoken") window.localStorage.removeItem("authtoken")
toast.warn("You have been logged out.") toast.warn("You have been logged out.")
return defaultAuth() return defaultAuth()
} }
console.debug("Authentication Response:", json) console.debug("Authentication Response:", json)
const {email, username, admin} = json; const {email, username, admin} = json;
return {username, email, admin, loggedIn: true} return {username, email, admin, loggedIn: true}
} }
export async function registration(username, email, password) { export async function registration(username, email, password) {
const {status, json} = await query("/register", {username, email, password}).catch(() => { const {status, json} = await query("/register", {username, email, password}).catch(() => {
return {status: 500, json: {reason: "The authentication server is offline!"}} return {status: 500, json: {reason: "The authentication server is offline!"}}
}) })
console.debug("Registration Response:", json) console.debug("Registration Response:", json)
return {status, json} return {status, json}
} }
export async function login(usernameOrEmail, password) { export async function login(usernameOrEmail, password) {
const {status, json} = await query("/login", {usernameOrEmail, password}).catch(() => { const {status, json} = await query("/login", {usernameOrEmail, password}).catch(() => {
return {status: 500, json: {reason: "The authentication server is offline!"}} return {status: 500, json: {reason: "The authentication server is offline!"}}
}) })
console.debug("Login Response:", json) console.debug("Login Response:", json)
return {status, json} return {status, json}
} }
export function logout(token) { export function logout(token) {
window.localStorage.removeItem("authtoken") window.localStorage.removeItem("authtoken")
query("/logout", {token}).then(() => { query("/logout", {token}).then(() => {
toast.warn("You have been logged out.") toast.warn("You have been logged out.")
}) })
return defaultAuth() return defaultAuth()
} }
export async function query(url, data) { export async function query(url, data) {
console.debug("FETCH: " + SERVER_URL + url + " - " + JSON.stringify(data)) console.debug("FETCH: " + SERVER_URL + url + " - " + JSON.stringify(data))
let response = await fetch(SERVER_URL + url, { let response = await fetch(SERVER_URL + url, {
method: "POST", method: "POST",
headers: {"Content-Type": "application/json"}, headers: {"Content-Type": "application/json"},
body: JSON.stringify(data) body: JSON.stringify(data)
}) })
const json = await response.json()
return {status: response.status, json: await response.json()} console.debug(json)
return {status: response.status, json}
} }

@ -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 (
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
style={{backgroundImage: `url(${background})`}}>
<Navbar history={history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
<div className="container text-gray-700 bg-white p-6 mt-12 mb-32 rounded">
<div className="flex">
<Flatpickr
value={dateRange}
onChange={date => setDateRange(date)}
options={{
dateFormat: "m-d-Y",
mode: "range",
wrap: true,
position: "below"
}}>
<div className="w-72">
<Input name="date" type='text' others={{"data-input": true}} className="input h-8"
placeholder="Select Date Range">
Select Date Range
</Input>
</div>
</Flatpickr>
<div className="italic text-sm ml-4 w-full flex items-center">{status}</div>
</div>
<hr className="bg-gray-400 mb-4"/>
<Griddle data={data} plugins={[plugins.LocalPlugin]} styleConfig={styleConfig}
pageProperties={{
currentPage: 1,
pageSize: items
}}
components={{
Layout: Layout(setItems)
}}>
<RowDefinition>
<ColumnDefinition id="ID"/>
<ColumnDefinition id="Date" title="Date"/>
<ColumnDefinition id="Precipitation"/>
<ColumnDefinition id="Latitude"/>
<ColumnDefinition id="Longitude"/>
<ColumnDefinition id="Remarks"/>
<ColumnDefinition id="Verified"/>
<ColumnDefinition id=" "
customComponent={(items) => <EditComponent {...items} dispatch={dispatchEdit}/>}/>
</RowDefinition>
</Griddle>
</div>
<Modal
onRequestClose={() => setEditModal({open: false, data: {ID: 0}})}
shouldCloseOnOverlayClick
isOpen={editModal.open}>
<div className="flex justify-center">
<FileManualForm title={"Edit Data"} id={editModal.data.ID} editType defaultData={editModal.data}/>
</div>
</Modal>
</div>
);
}
export default Admin;
const Layout = (setItems) => (({Table, Filter, Pagination}) => (
<div>
<div className="flex w-96 pb-4 h-12 justify-between">
<Filter/>
<select onChange={e => setItems(Number(e.target.value))}>
<option value="10">Show 10 items</option>
<option value="20">Show 20 items</option>
<option value="50">Show 50 items</option>
<option value="100">Show 100 items</option>
</select>
</div>
<div className="lg:overflow-x-auto overflow-x-scroll">
<Table/>
</div>
<Pagination/>
</div>
))
const EditComponent = ({griddleKey, dispatch}) => {
return <div className="flex justify-around">
<span onClick={() => dispatch([0, griddleKey])}><AiFillEdit/></span>
<span onClick={() => dispatch([1, griddleKey])}><AiFillDelete/></span>
</div>
}

@ -1,98 +1,116 @@
import React, {useContext, useState} from 'react'; import React, {useContext, useEffect, useState} from 'react';
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {AuthContext} from "../App"; import {AuthContext} from "../App";
import {useForm} from "react-hook-form"; import {useForm} from "react-hook-form";
import {query} from "../auth"; import {query} from "../auth";
import {Input} from "../custom"; import {Input} from "../custom";
function FileManualForm() { function FileManualForm({id, title = "Upload Individual Entry", defaultData = {}, editType: edit = false}) {
const {authState} = useContext(AuthContext) const {authState} = useContext(AuthContext)
const [manualError, setManualError] = useState("") const [manualError, setManualError] = useState("")
const {register, handleSubmit, errors, setError, reset} = useForm(); const {register, handleSubmit, errors, setError, reset, setValue} = useForm();
const manualUpload = async (data) => { const manualUpload = async (data) => {
const {status, json} = await query("/data/add", data).catch(() => { const {status, json} = await query("/data/add", data).catch(() => {
return {status: 500, json: {reason: "The upload server is offline!"}} return {status: 500, json: {reason: "The upload server is offline!"}}
}) })
console.debug("Manual Upload Response:", json) console.debug("Manual Upload Response:", json)
return {status, json} return {status, json}
} }
function submitManual(data) { function submitManual(data) {
let req = data let req = data
let split = data.date.split(/[/-]/); let split = data.date.split(/[/-]/);
// noinspection JSCheckFunctionSignatures // noinspection JSCheckFunctionSignatures
let date = new Date(split[2], Number(split[0]) - 1, split[1]) let date = new Date(split[2], Number(split[0]) - 1, split[1])
req.date = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate() 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()) 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 < 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 (date.getDate() === 0) return setError("date", "format", "Invalid Date format (MM-DD-YYYY)")
if (isNaN(parseFloat(data["precipitation"]))) { if (isNaN(parseFloat(data["precipitation"]))) {
setError("precipitation", "format", "Invalid format. Precipitation must be a number. E.g 0.24") setError("precipitation", "format", "Invalid format. Precipitation must be a number. E.g 0.24")
} }
req.precipitation = parseFloat(data["precipitation"]) req.precipitation = parseFloat(data["precipitation"])
req.token = authState.token; req.id = id
req.token = authState.token;
(async () => {
let {json, status} = await manualUpload(data) (async () => {
if (status !== 200) return setManualError(json.reason) let {json, status} = await manualUpload(data)
reset() if (status !== 200) return setManualError(json.reason)
toast.success("Entry successfully uploaded") reset()
})(); if (edit) window.location.reload()
} toast.success("Entry successfully uploaded")
})();
}
return (
<form className="bg-white shadow p-4 overflow-hidden rounded-lg const formClasses = edit
my-6 px-6 w-11/12 lg:mx-2 lg:my-5 lg:px-5 lg:w-5/12" onSubmit={handleSubmit(submitManual)}> ? "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"
<div className="text-lg font-medium mb-4">Upload Individual Entry</div>
{manualError && <div useEffect(() => {
className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded"> if (edit) {
{manualError} const date = new Date(defaultData.Date)
</div> const formattedDate = (date.getMonth() + 1).toString().padStart(2, "0") + "-" + date.getDate().toString().padStart(2, "0") + "-" + date.getFullYear()
} setValue([
{date: formattedDate},
<Input name="date" type="text" placeholder="Date (MM-DD-YYYY)" errors={errors} {precipitation: defaultData.Precipitation},
register={register({ {latitude: defaultData.Latitude},
required: {value: true, message: "You must provide a date."}, {longitude: defaultData.Longitude},
pattern: { {remarks: defaultData.Remarks},
value: /^\d\d?[/-]\d\d?[/-]\d\d\d?\d?$/, ])
message: "Invalid Date format (MM-DD-YYYY)" }
}, }, [defaultData, edit, setValue])
})}/>
return (
<Input name="precipitation" placeholder="Precipitation (Inches)" errors={errors} <form className={formClasses} onSubmit={handleSubmit(submitManual)}>
register={register({
required: {value: true, message: "You must provide the precipitation."}, <div className="text-lg font-medium mb-4">{title}</div>
pattern: { {manualError && <div
value: /^\d*\.?\d*$/, className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded">
message: "Invalid format. Precipitation must be a number. E.g 0.24" {manualError}
} </div>
})}/> }
<Input name="latitude" type="text" placeholder="Latitude" errors={errors} <Input name="date" type="text" placeholder="Date (MM-DD-YYYY)" errors={errors}
register={register({ register={register({
required: {value: true, message: "Latitude and Longitude are both required."} required: {value: true, message: "You must provide a date."},
})}/> pattern: {
value: /^\d\d?[/-]\d\d?[/-]\d\d\d?\d?$/,
<Input name="longitude" type="text" placeholder="Longitude" errors={errors} message: "Invalid Date format (MM-DD-YYYY)"
register={register({ },
required: {value: true, message: "Latitude and Longitude are both required."} })}/>
})}/>
<Input name="precipitation" placeholder="Precipitation (Inches)" errors={errors}
<Input name="remarks" type="text" placeholder="Remarks (Optional)" errors={errors} register={register({
register={register}/> required: {value: true, message: "You must provide the precipitation."},
pattern: {
<div className="flex flex-1 justify-center"> value: /^\d*\.?\d*$/,
<input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 text-center rounded" message: "Invalid format. Precipitation must be a number. E.g 0.24"
type="submit" value="Submit"/> }
</div> })}/>
</form> <Input name="latitude" type="text" placeholder="Latitude" errors={errors}
); register={register({
} required: {value: true, message: "Latitude and Longitude are both required."}
})}/>
<Input name="longitude" type="text" placeholder="Longitude" errors={errors}
register={register({
required: {value: true, message: "Latitude and Longitude are both required."}
})}/>
<Input name="remarks" type="text" placeholder="Remarks (Optional)" errors={errors}
register={register}/>
<div className="flex flex-1 justify-center">
<input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 text-center rounded"
type="submit" value="Submit"/>
</div>
</form>
);
}
export default FileManualForm; export default FileManualForm;

@ -1,129 +1,129 @@
import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react'; import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react';
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {AuthContext} from "../App"; import {AuthContext} from "../App";
import {SERVER_URL} from "../auth"; import {SERVER_URL} from "../auth";
import {Input} from "../custom"; import {Input} from "../custom";
const LIMIT = 2 * 1024 * 1024; // 2 mb const LIMIT = 2 * 1024 * 1024; // 2 mb
const dispatch = (state, action) => { const dispatch = (state, action) => {
switch (action.type) { switch (action.type) {
case "lat": case "lat":
return {...state, lat: action.value} return {...state, lat: action.value}
case "long": case "long":
return {...state, long: action.value} return {...state, long: action.value}
default: default:
return; return;
} }
} }
function FileUploadForm() { function FileUploadForm() {
const {authState} = useContext(AuthContext) const {authState} = useContext(AuthContext)
const [files, setFiles] = useState(null); const [files, setFiles] = useState(null);
const [load, setLoad] = useState(0) const [load, setLoad] = useState(0)
const [status, setStatus] = useState("Upload a file") const [status, setStatus] = useState("Upload a file")
const [latLong, setLatLong] = useReducer(dispatch, {lat: "", long: ""}, undefined) const [latLong, setLatLong] = useReducer(dispatch, {lat: "", long: ""}, undefined)
let form = useRef(null); let form = useRef(null);
async function query(url, data) { async function query(url, data) {
console.debug("FETCH: " + SERVER_URL + url + " - " + data) console.debug("FETCH: " + SERVER_URL + url + " - " + data)
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let req = new XMLHttpRequest() let req = new XMLHttpRequest()
req.open('post', SERVER_URL + url) req.open('post', SERVER_URL + url)
req.upload.addEventListener('progress', e => { req.upload.addEventListener('progress', e => {
if (e.loaded === e.total) setStatus("Processing...") if (e.loaded === e.total) setStatus("Processing...")
else { else {
let percent = (e.loaded / e.total * 100) let percent = (e.loaded / e.total * 100)
setLoad(percent) setLoad(percent)
setStatus("Uploading " + percent.toFixed(0) + "%") setStatus("Uploading " + percent.toFixed(0) + "%")
} }
}); });
req.addEventListener('load', () => { req.addEventListener('load', () => {
resolve({status: req.status, json: JSON.parse(req.response)}) resolve({status: req.status, json: JSON.parse(req.response)})
}); });
req.addEventListener('error', e => { req.addEventListener('error', e => {
resolve({status: req.status, json: JSON.parse(req.response)}) resolve({status: req.status, json: JSON.parse(req.response)})
}) })
req.send(data) req.send(data)
}) })
} }
const upload = useCallback(async () => { const upload = useCallback(async () => {
if (!files || files.length === 0) return if (!files || files.length === 0) return
const target = files[0] const target = files[0]
if (target.size > LIMIT) return toast.error("File size too large! Max 2 mb.") 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!") if (!target.name.includes(".xlsx")) return toast.error("Invalid file type!")
const {lat, long} = latLong; const {lat, long} = latLong;
setStatus("Uploading") setStatus("Uploading")
let file = new FormData() let file = new FormData()
file.append("file", target) file.append("file", target)
file.append("token", authState.token); file.append("token", authState.token);
file.append("latitude", lat); file.append("latitude", lat);
file.append("longitude", long); file.append("longitude", long);
let {status, json} = await query("/data/upload", file) let {status, json} = await query("/data/upload", file)
form.current.reset(); form.current.reset();
setStatus("Upload a file") setStatus("Upload a file")
setLoad(0) setLoad(0)
if (status !== 200) return toast.error(json.reason) if (status !== 200) return toast.error(json.reason)
return toast.success("File successfully uploaded.") return toast.success("File successfully uploaded.")
}, [files, authState.token, form, latLong]) }, [files, authState.token, form, latLong])
useEffect(() => { useEffect(() => {
(async () => upload())(); (async () => upload())();
}, [files, authState.token, upload, authState, setLoad, setStatus]) }, [files, authState.token, upload, authState, setLoad, setStatus])
return <div className="bg-white shadow p-4 overflow-hidden rounded-lg flex flex-col justify-center return <div className="bg-white shadow p-4 overflow-hidden rounded-lg flex flex-col justify-center
my-6 px-6 w-11/12 lg:mx-2 lg:my-5 lg:px-5 lg:w-5/12"> my-6 px-6 w-11/12 lg:mx-2 lg:my-5 lg:px-5 lg:w-5/12">
<div> <div>
<div className="text-lg font-medium text-center mb-2">Upload File Entries</div> <div className="text-lg font-medium text-center mb-2">Upload File Entries</div>
<div className="text-xs font-light text-center text-black mb-2"> <div className="text-xs font-light text-center text-black mb-2">
Click the button to upload an .xlsx file Click the button to upload an .xlsx file
</div> </div>
<hr className="mb-6 border-gray-300 mx-12"/> <hr className="mb-6 border-gray-300 mx-12"/>
</div> </div>
<form className="mt-2 items-center" ref={form}> <form className="mt-2 items-center" ref={form}>
<p className="text-gray-500 italic text-xs ">Latitude and longitude is required if your data doesn't have it</p> <p className="text-gray-500 italic text-xs ">Latitude and longitude is required if your data doesn't have it</p>
<Input name="lat" type="text" placeholder="Latitude" <Input name="lat" type="text" placeholder="Latitude"
setValue={e => setLatLong({type: "lat", value: e})}/> setValue={e => setLatLong({type: "lat", value: e})}/>
<Input name="long" type="text" placeholder="Longitude" <Input name="long" type="text" placeholder="Longitude"
setValue={e => setLatLong({type: "long", value: e})}/> setValue={e => setLatLong({type: "long", value: e})}/>
<div className="w-full flex justify-center"> <div className="w-full flex justify-center">
<label className="px-4 pb-2 bg-white text-blue rounded-lg shadow-lg loading inline-block <label className="px-4 pb-2 bg-white text-blue rounded-lg shadow-lg loading inline-block
relative border border-blue cursor-pointer bg-blue-500 hover:bg-blue-700 text-white"> relative border border-blue cursor-pointer bg-blue-500 hover:bg-blue-700 text-white">
<style dangerouslySetInnerHTML={{ <style dangerouslySetInnerHTML={{
__html: __html:
'.loading::after {' + '.loading::after {' +
'content: "";' + 'content: "";' +
`width: ${load}%;` + `width: ${load}%;` +
'background: rgb(255, 255, 255, 0.4);' + 'background: rgb(255, 255, 255, 0.4);' +
'position: absolute;' + 'position: absolute;' +
'top: 0;' + 'top: 0;' +
'bottom: 0;' + 'bottom: 0;' +
'left: 0;' + 'left: 0;' +
'transition: width 0.3s;' 'transition: width 0.3s;'
}}/> }}/>
<div className="flex items-center"> <div className="flex items-center">
<svg className="w-4 h-4 mt-2 mr-3" fill="currentColor" xmlns="http://www.w3.org/2000/svg" <svg className="w-4 h-4 mt-2 mr-3" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"> viewBox="0 0 20 20">
<path <path
d="M16.88 9.1A4 4 0 0 1 16 17H5a5 5 0 0 1-1-9.9V7a3 3 0 0 1 4.52-2.59A4.98 4.98 0 0 1 17 8c0 .38-.04.74-.12 1.1zM11 11h3l-4-4-4 4h3v3h2v-3z"/> d="M16.88 9.1A4 4 0 0 1 16 17H5a5 5 0 0 1-1-9.9V7a3 3 0 0 1 4.52-2.59A4.98 4.98 0 0 1 17 8c0 .38-.04.74-.12 1.1zM11 11h3l-4-4-4 4h3v3h2v-3z"/>
</svg> </svg>
<span className="mt-2 font-bold">{status}</span> <span className="mt-2 font-bold">{status}</span>
<input type='file' className="hidden" name="upload" accept=".xlsx" <input type='file' className="hidden" name="upload" accept=".xlsx"
onChange={e => setFiles(e.target.files)}/> onChange={e => setFiles(e.target.files)}/>
</div> </div>
</label> </label>
</div> </div>
</form> </form>
</div>; </div>;
} }
export default FileUploadForm; export default FileUploadForm;

@ -1,30 +1,30 @@
import React from 'react'; import React from 'react';
import Navbar from "./Navbar"; import Navbar from "./Navbar";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
const background = require('../assets/background.png') const background = require('../assets/background.png')
function Home(props) { function Home(props) {
return ( return (
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col" <div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
style={{backgroundImage: `url(${background})`}}> style={{backgroundImage: `url(${background})`}}>
<Navbar history={props.history}/> <Navbar history={props.history}/>
<div <div
className="flex flex-col lg:max-w-full sm:max-w-xl text-center justify-center className="flex flex-col lg:max-w-full sm:max-w-xl text-center justify-center
lg:px-0 md:mt-32 sm:mt-20 mt-16 px-4"> lg:px-0 md:mt-32 sm:mt-20 mt-16 px-4">
<h1 className="md:text-4xl text-2xl font-bold">Your Rainfall Data in One Place</h1> <h1 className="md:text-4xl text-2xl font-bold">Your Rainfall Data in One Place</h1>
<div className="mt-6 sm:text-xl text-sm font-light">Look through your rainfall data with ease.</div> <div className="mt-6 sm:text-xl text-sm font-light">Look through your rainfall data with ease.</div>
<Link to="/upload"> <Link to="/upload">
<div className="rounded-full bg-blue-600 inline-block mt-4 sm:py-3 py-3 sm:px-8 px-4"> <div className="rounded-full bg-blue-600 inline-block mt-4 sm:py-3 py-3 sm:px-8 px-4">
Start Uploading Data Start Uploading Data
</div> </div>
</Link> </Link>
</div> </div>
</div> </div>
); );
} }
export default Home; export default Home;

@ -1,83 +1,83 @@
import React, {useContext, useEffect, useState} from 'react'; import React, {useContext, useEffect, useState} from 'react';
import Navbar from "./Navbar"; import Navbar from "./Navbar";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {Input} from "../custom"; import {Input} from "../custom";
import {AuthContext} from "../App"; import {AuthContext} from "../App";
import {login} from "../auth"; import {login} from "../auth";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useForm} from "react-hook-form"; import {useForm} from "react-hook-form";
const background = require('../assets/background.png') const background = require('../assets/background.png')
function Login(props) { function Login(props) {
const {register, handleSubmit, errors} = useForm(); const {register, handleSubmit, errors} = useForm();
const [error, setError] = useState("") const [error, setError] = useState("")
const [status, setStatus] = useState("Sign In") const [status, setStatus] = useState("Sign In")
const {authState, authDispatch} = useContext(AuthContext) const {authState, authDispatch} = useContext(AuthContext)
const [canLogin, setLogin] = useState(true) const [canLogin, setLogin] = useState(true)
const [cooldown, setCooldown] = useState(0) const [cooldown, setCooldown] = useState(0)
function submit(data) { function submit(data) {
if (!canLogin) return; if (!canLogin) return;
setError(""); setError("");
setStatus("Loading.."); setStatus("Loading..");
setLogin(false); setLogin(false);
(async () => { (async () => {
let {status, json} = await login(data.username.trim(), data.password.trim()) let {status, json} = await login(data.username.trim(), data.password.trim())
setCooldown(setTimeout(() => setLogin(true), 1000)); setCooldown(setTimeout(() => setLogin(true), 1000));
if (status !== 200) { if (status !== 200) {
setStatus("Sign In") setStatus("Sign In")
return setError(json.reason) return setError(json.reason)
} }
toast.success("You have been logged in.") toast.success("You have been logged in.")
window.localStorage.setItem("authtoken", json.token) window.localStorage.setItem("authtoken", json.token)
authDispatch({response: {token: json.token}}) authDispatch({response: {token: json.token}})
})() })()
} }
useEffect(() => { useEffect(() => {
let loc = new URLSearchParams(props.location.search).get("return") let loc = new URLSearchParams(props.location.search).get("return")
if (authState.loggedIn) props.history.push("/" + (loc ? loc : "")) if (authState.loggedIn) props.history.push("/" + (loc ? loc : ""))
return () => clearTimeout(cooldown) return () => clearTimeout(cooldown)
}, [props, authState, cooldown]) }, [props, authState, cooldown])
return <div className="w-screen h-screen overflow-x-hidden bg-cover bg-center flex items-center flex-col" return <div className="w-screen h-screen overflow-x-hidden bg-cover bg-center flex items-center flex-col"
style={{backgroundImage: `url(${background})`}}> style={{backgroundImage: `url(${background})`}}>
<Navbar history={props.history}/> <Navbar history={props.history}/>
<form className="mt-32 sm:w-96 w-full" onSubmit={handleSubmit(submit)}> <form className="mt-32 sm:w-96 w-full" onSubmit={handleSubmit(submit)}>
<div className="text-2xl w-full font-semibold block "> <div className="text-2xl w-full font-semibold block ">
<div className="rounded-t-md inline bg-gray-100 text-gray-700 px-5 py-2">Log In</div> <div className="rounded-t-md inline bg-gray-100 text-gray-700 px-5 py-2">Log In</div>
</div> </div>
<div className="px-8 pt-6 pb-8 mb-4 px-12 rounded-md rounded-tl-none bg-gray-100"> <div className="px-8 pt-6 pb-8 mb-4 px-12 rounded-md rounded-tl-none bg-gray-100">
{error && {error &&
<div className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded"> <div className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded">
{error} {error}
</div> </div>
} }
<Input name="username" type="text" placeholder="Username or Email" errors={errors} <Input name="username" type="text" placeholder="Username or Email" errors={errors}
register={register({required: {value: true, message: "You must provide a username."}})}>Username</Input> register={register({required: {value: true, message: "You must provide a username."}})}>Username</Input>
<Input name="password" type="password" placeholder="********" errors={errors} <Input name="password" type="password" placeholder="********" errors={errors}
register={register({required: {value: true, message: "You must provide a password."}})}>Password</Input> register={register({required: {value: true, message: "You must provide a password."}})}>Password</Input>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 mr-2 rounded" type="submit" <input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 mr-2 rounded" type="submit"
value={status}/> value={status}/>
<Link to="/reset" className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800"> <Link to="/reset" className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800">
Reset Password Reset Password
</Link> </Link>
</div> </div>
</div> </div>
</form> </form>
</div>; </div>;
} }
export default Login; export default Login;

@ -1,20 +1,20 @@
import React, {useContext, useEffect} from 'react'; import React, {useContext, useEffect} from 'react';
import {AuthContext} from "../App"; import {AuthContext} from "../App";
import {logout} from "../auth"; import {logout} from "../auth";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
function Logout(props) { function Logout(props) {
const {authDispatch, authState} = useContext(AuthContext) const {authDispatch, authState} = useContext(AuthContext)
useEffect(() => { useEffect(() => {
if (authState.loggedIn) authDispatch({response: logout(authState.token)}) if (authState.loggedIn) authDispatch({response: logout(authState.token)})
else toast.warn("You are not logged in!") else toast.warn("You are not logged in!")
props.history.push("/") props.history.push("/")
}) })
return ( return (
<div>You will soon be redirected. Click <Link to="/">here</Link> if you are not.</div> <div>You will soon be redirected. Click <Link to="/">here</Link> if you are not.</div>
); );
} }
export default Logout; export default Logout;

@ -1,69 +1,78 @@
import React, {useContext, useEffect, useState} from 'react'; import React, {useContext, useEffect, useState} from 'react';
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
import {AuthContext} from "../App"; import {AuthContext} from "../App";
const pathsCenter = [ const pathsCenter = [
{name: "All Data", to: "/all", extra: ""}, {name: "All Data", to: "/all", extra: ""},
{name: "Upload Data", to: "/upload", extra: ""}, {name: "Upload Data", to: "/upload", extra: ""},
{name: "Your Data", to: "/", extra: ""}, {name: "My Data", to: "/mydata", extra: ""},
] ]
const pathsGuest = [ const pathsGuest = [
{name: "Log In", to: "/login", extra: ""}, {name: "Log In", to: "/login", extra: ""},
{name: "Sign Up", to: "/register", extra: "rounded-full border border-white px-4 pb-1 pt-1 inline-block"}, {name: "Sign Up", to: "/register", extra: "rounded-full border border-white px-4 pb-1 pt-1 inline-block"},
] ]
const pathsLoggedIn = [ const pathsLoggedIn = [
{name: "Account", to: "/account", extra: ""}, {name: "Account", to: "/account", extra: ""},
{name: "Sign Out", to: "/logout", extra: ""}, {name: "Logout", to: "/logout", extra: ""},
] ]
function Navbar({color = "text-white", hover="hover:border-white"}) { const pathsAdmin = [
const [showDropdown, setDropdown] = useState(false) {name: "Admin", to: "/admin", extra: ""},
const [userPaths, setUserPaths] = useState(pathsGuest) {name: "Account", to: "/account", extra: ""},
const {authState} = useContext(AuthContext) {name: "Logout", to: "/logout", extra: ""},
]
useEffect(() => {
if (authState.loggedIn) { function Navbar({color = "text-white", hover = "hover:border-white"}) {
setUserPaths(() => pathsLoggedIn) const [showDropdown, setDropdown] = useState(false)
} else setUserPaths(() => pathsGuest) const [userPaths, setUserPaths] = useState(pathsGuest)
}, [authState.loggedIn]) const {authState} = useContext(AuthContext)
return <header className={"lg:flex lg:justify-between lg:items-center px-4 py-3 w-full mt-2 " + color}> useEffect(() => {
<div className="flex flex-1 justify-between items-center mt-3 self-start"> console.log(authState)
<Link to={"/"}> if (authState.loggedIn) {
<h1 className="text-xl font-bold">RainTrack</h1> if (authState.admin) setUserPaths(() => pathsAdmin)
</Link> else setUserPaths(() => pathsLoggedIn)
<button onClick={() => setDropdown(!showDropdown)}
className="px-2 py-1 lg:hidden block border border-white rounded-md cursor-pointer"> } else setUserPaths(() => pathsGuest)
<svg className="fill-current" xmlns="http://www.w3.org/2000/svg" width="20" height="20" }, [authState])
viewBox="0 0 20 20"><title>Toggle Navigation</title>
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/> return <header className={"lg:flex lg:justify-between lg:items-center px-4 py-3 w-full mt-2 " + color}>
</svg> <div className="flex flex-1 justify-between items-center mt-3 self-start">
</button> <Link to={"/"}>
</div> <h1 className="text-xl font-bold">RainTrack</h1>
</Link>
<LinksContainer flex="lg:justify-center flex-2" <button onClick={() => setDropdown(!showDropdown)}
dropDown={showDropdown} paths={pathsCenter} color={color} hover={hover}/> className="px-2 py-1 lg:hidden block border border-white rounded-md cursor-pointer">
<svg className="fill-current" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
<LinksContainer flex="lg:justify-end flex-1" viewBox="0 0 20 20"><title>Toggle Navigation</title>
dropDown={showDropdown} paths={userPaths} color={color} hover={hover}/> <path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/>
</svg>
</header>; </button>
} </div>
function LinksContainer(props) { <LinksContainer flex="lg:justify-center flex-2"
return <div dropDown={showDropdown} paths={pathsCenter} color={color} hover={hover}/>
className={`lg:px-2 lg:pt-2 lg:pb-4 lg:flex lg:p-0 ${props.flex} ${props.dropDown ? "block" : "hidden"}`}>
{props.paths.map(path => <Link to={path.to} key={path.name}> <LinksContainer flex="lg:justify-end flex-1"
<div dropDown={showDropdown} paths={userPaths} color={color} hover={hover}/>
className={`lg:px-2 py-1 lg:mx-1 mt-1 block text-left font-semibold border-b-2 border-transparent
${props.hover} ${path.extra}`}> </header>;
{path.name} }
</div>
</Link>)} function LinksContainer(props) {
</div> return <div
} className={`lg:px-2 lg:pt-2 lg:pb-4 lg:flex lg:p-0 ${props.flex} ${props.dropDown ? "block" : "hidden"}`}>
{props.paths.map(path => <Link to={path.to} key={path.name}>
export default Navbar; <div
className={`lg:px-2 py-1 lg:mx-1 mt-1 block text-left font-semibold border-b-2 border-transparent
${props.hover} ${path.extra}`}>
{path.name}
</div>
</Link>)}
</div>
}
export default Navbar;

@ -1,28 +1,28 @@
import React from 'react'; import React from 'react';
import Navbar from "./Navbar"; import Navbar from "./Navbar";
import {Link} from "react-router-dom"; import {Link} from "react-router-dom";
const background = require('../assets/background.png') const background = require('../assets/background.png')
function NotFound(props) { function NotFound(props) {
return ( return (
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col" <div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
style={{backgroundImage: `url(${background})`}}> style={{backgroundImage: `url(${background})`}}>
<Navbar history={props.history}/> <Navbar history={props.history}/>
<div className="flex flex-col lg:max-w-full sm:max-w-xl text-center justify-center <div className="flex flex-col lg:max-w-full sm:max-w-xl text-center justify-center
lg:px-0 md:mt-32 sm:mt-20 mt-16 px-4"> lg:px-0 md:mt-32 sm:mt-20 mt-16 px-4">
<h1 className="md:text-3xl sm:text-2xl text-xl font-bold"> <h1 className="md:text-3xl sm:text-2xl text-xl font-bold">
The page you requested was not found. <br/> The page you requested was not found. <br/>
Please confirm your URL or go back to the home page. Please confirm your URL or go back to the home page.
</h1> </h1>
<Link to="/"> <Link to="/">
<div className="rounded-full bg-blue-600 inline-block mt-4 sm:py-3 py-3 sm:px-8 px-4">Go Home</div> <div className="rounded-full bg-blue-600 inline-block mt-4 sm:py-3 py-3 sm:px-8 px-4">Go Home</div>
</Link> </Link>
</div> </div>
</div> </div>
); );
} }
export default NotFound; export default NotFound;

@ -1,86 +1,86 @@
import React, {useContext, useEffect, useState} from 'react'; import React, {useContext, useEffect, useState} from 'react';
import Navbar from "./Navbar"; import Navbar from "./Navbar";
import {Input} from "../custom"; import {Input} from "../custom";
import {AuthContext} from "../App"; import {AuthContext} from "../App";
import {registration} from "../auth"; import {registration} from "../auth";
import {toast} from "react-toastify"; import {toast} from "react-toastify";
import {useForm} from "react-hook-form"; import {useForm} from "react-hook-form";
const background = require('../assets/background.png') const background = require('../assets/background.png')
function Register(props) { function Register(props) {
const {authState, authDispatch} = useContext(AuthContext) const {authState, authDispatch} = useContext(AuthContext)
const {register, handleSubmit, errors} = useForm(); const {register, handleSubmit, errors} = useForm();
const [error, setError] = useState("") const [error, setError] = useState("")
const [status, setStatus] = useState("Register") const [status, setStatus] = useState("Register")
const [canRegister, setRegister] = useState(true) const [canRegister, setRegister] = useState(true)
function submit(data) { function submit(data) {
if (!canRegister) return; if (!canRegister) return;
setError("") setError("")
setStatus("Loading.."); setStatus("Loading..");
setRegister(false); setRegister(false);
(async () => { (async () => {
let {json, status} = await registration(data.user.trim(), data.email.trim(), data.pass.trim()) let {json, status} = await registration(data.user.trim(), data.email.trim(), data.pass.trim())
setTimeout(() => setRegister(true), 1000); setTimeout(() => setRegister(true), 1000);
if (status !== 200) { if (status !== 200) {
setStatus("Register"); setStatus("Register");
return setError(json.reason) return setError(json.reason)
} }
toast.success("Your account has been registered.") toast.success("Your account has been registered.")
window.localStorage.setItem("authtoken", json.token) window.localStorage.setItem("authtoken", json.token)
authDispatch({response: {token: json.token, loggedIn: true}}) authDispatch({response: {token: json.token, loggedIn: true}})
})() })()
} }
useEffect(() => { useEffect(() => {
if (authState.loggedIn) props.history.push("/") if (authState.loggedIn) props.history.push("/")
}, [authState.loggedIn, props.history]) }, [authState.loggedIn, props.history])
return <div className="w-screen h-screen overflow-x-hidden bg-cover bg-center flex items-center flex-col" return <div className="w-screen h-screen overflow-x-hidden bg-cover bg-center flex items-center flex-col"
style={{backgroundImage: `url(${background})`}}> style={{backgroundImage: `url(${background})`}}>
<Navbar history={props.history}/> <Navbar history={props.history}/>
<form className="mt-24 sm:w-96 w-full" onSubmit={handleSubmit(submit)}> <form className="mt-24 sm:w-96 w-full" onSubmit={handleSubmit(submit)}>
<div className="text-2xl w-full font-semibold block "> <div className="text-2xl w-full font-semibold block ">
<div className="rounded-t-md inline bg-gray-100 text-gray-700 px-5 py-2">Create Account</div> <div className="rounded-t-md inline bg-gray-100 text-gray-700 px-5 py-2">Create Account</div>
</div> </div>
<div className="px-8 pt-6 pb-8 -mt-1 mb-4 px-12 rounded-md rounded-tl-none bg-gray-100"> <div className="px-8 pt-6 pb-8 -mt-1 mb-4 px-12 rounded-md rounded-tl-none bg-gray-100">
{error && {error &&
<div <div
className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded">{error}</div> className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded">{error}</div>
} }
<Input name="user" type="text" placeholder="Username" errors={errors} <Input name="user" type="text" placeholder="Username" errors={errors}
register={register({ register={register({
required: {value: true, message: "You must provide a username."}, required: {value: true, message: "You must provide a username."},
minLength: {value: 3, message: "Your username must be between 3 and 16 characters."}, minLength: {value: 3, message: "Your username must be between 3 and 16 characters."},
maxLength: {value: 16, message: "Your username must be between 3 and 16 characters."}, maxLength: {value: 16, message: "Your username must be between 3 and 16 characters."},
})}>Username</Input> })}>Username</Input>
<Input name="email" type="text" placeholder="Email" errors={errors} <Input name="email" type="text" placeholder="Email" errors={errors}
register={register({ register={register({
required: {value: true, message: "You must provide a password."}, required: {value: true, message: "You must provide a password."},
pattern: {value: /\S+@\S+\.\S+/, message: "Invalid Email"} pattern: {value: /\S+@\S+\.\S+/, message: "Invalid Email"}
})}>Email</Input> })}>Email</Input>
<Input name="pass" type="password" placeholder="********" errors={errors} <Input name="pass" type="password" placeholder="********" errors={errors}
register={register({ register={register({
required: {value: true, message: "You must provide a password."}, required: {value: true, message: "You must provide a password."},
minLength: {value: 8, message: "Your password must be at least 8 characters long."} minLength: {value: 8, message: "Your password must be at least 8 characters long."}
})}>Password</Input> })}>Password</Input>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 mr-2 rounded" type="submit" <input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 mr-2 rounded" type="submit"
value={status}/> value={status}/>
</div> </div>
</div> </div>
</form> </form>
</div>; </div>;
} }
export default Register; export default Register;

@ -1,32 +1,32 @@
import React, {useContext, useEffect} from 'react'; import React, {useContext, useEffect} from 'react';
import Navbar from "./Navbar"; import Navbar from "./Navbar";
import {AuthContext} from "../App"; import {AuthContext} from "../App";
import FileUploadForm from "./FileUploadForm"; import FileUploadForm from "./FileUploadForm";
import FileManualForm from "./FileManualForm"; import FileManualForm from "./FileManualForm";
const background = require('../assets/wave.png') const background = require('../assets/wave.png')
function Upload(props) { function Upload(props) {
const {authState} = useContext(AuthContext) const {authState} = useContext(AuthContext)
useEffect(() => { useEffect(() => {
if (!authState.loggedIn) props.history.push("/login?return=upload") if (!authState.loggedIn) props.history.push("/login?return=upload")
}, [authState.loggedIn, props.history]) }, [authState.loggedIn, props.history])
return ( return (
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col" <div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
style={{backgroundImage: `url(${background})`}}> style={{backgroundImage: `url(${background})`}}>
<Navbar history={props.history} color={"text-blue-600"} hover={"hover:border-blue-600"}/> <Navbar history={props.history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
<div className="container text-gray-700"> <div className="container text-gray-700">
<div className="flex flex-wrap justify-center -mx-6 md:mx-0 overflow-hidden lg:-mx-12 xl:-mx-5 mt-16"> <div className="flex flex-wrap justify-center -mx-6 md:mx-0 overflow-hidden lg:-mx-12 xl:-mx-5 mt-16">
<FileUploadForm/> <FileUploadForm/>
<FileManualForm/> <FileManualForm/>
</div> </div>
</div> </div>
</div> </div>
); );
} }
export default Upload; export default Upload;

@ -1,109 +1,207 @@
import React, {useCallback, useContext, useEffect, useState} from 'react'; import React, {useCallback, useContext, useEffect, useState} from 'react';
import Navbar from "./Navbar"; import Navbar from "./Navbar";
import {AuthContext} from "../App"; import {AuthContext} from "../App";
import Griddle, {ColumnDefinition, plugins, RowDefinition} from "griddle-react"; import Griddle, {ColumnDefinition, plugins, RowDefinition} from "griddle-react";
import {query} from "../auth"; import {query} from "../auth";
import "flatpickr/dist/themes/material_blue.css";
const background = require('../assets/wave.png') import "../styles/tables.css"
import Flatpickr from "react-flatpickr";
import {Input} from "../custom";
function ViewAll(props) { import {max, mean, median, min, mode, std} from "mathjs";
const {authState} = useContext(AuthContext) import {toast} from "react-toastify";
const [data, setData] = useState([{}])
const [items, setItems] = useState(10)
const background = require('../assets/wave.png')
useEffect(() => { const iqr = require('compute-iqr');
if (!authState.loggedIn) props.history.push("/login?return=all")
}, [authState.loggedIn, props.history])
const defaultData = () => [{
const initiateData = useCallback(async () => { ID: "No Data Found",
let {json, status} = await query("/data/query", {token: authState.token, validated: false}).catch(() => { Date: "",
return {status: 500, json: {reason: "The authentication server is offline!"}} Precipitation: "",
}) Latitude: "",
if (status !== 200) { Longitude: "",
return Remarks: "",
} }]
let data = [] function ViewAll({history, ...props}) {
json.entries.forEach((entry, index) => { const {authState} = useContext(AuthContext)
const date = new Date(entry.date); const [data, setData] = useState(defaultData())
const formattedDate = date.getFullYear() + "-" + (date.getMonth() + 1).toString().padStart(2, "0") + "-" + date.getDate().toString().padStart(2, "0") const [items, setItems] = useState(10)
const group = json.groups.find(gr => gr.id === entry.group_id) const [status, setStatus] = useState("")
data[index] = {
ID: entry.id, const initialDates = () => {
Date: formattedDate, const now = new Date()
Precipitation: entry.precipitation, const lastYear = new Date();
Latitude: group.latitude, lastYear.setFullYear(lastYear.getFullYear() - 1)
Longitude: group.longitude, return [lastYear, now]
Remarks: entry.remarks, }
} const [dateRange, setDateRange] = useState([]);
})
setData(data) useEffect(() => {
const params = new URLSearchParams(props.location.search)
}, [setData, authState.token]) let range = initialDates()
if (params.get("after")) range[0] = new Date(params.get("after"))
useEffect(() => { if (params.get("before")) range[1] = new Date(params.get("before"))
(async () => { setDateRange(range)
initiateData() }, [props.location])
})();
}, [setData, initiateData])
useEffect(() => {
const styleConfig = { if (!authState.loggedIn) history.push("/login?return=all")
classNames: { }, [authState, history])
Filter: "shadow appearance-none border rounded mb-1 py-2 px-3 mb-4 text-gray-700 leading-tight h-8",
Table: "table table-bordered table-hover table-striped", const initiateData = useCallback(async () => {
PreviousButton: "border border-gray-300 px-2 py-1 w-20 mr-2 rounded", if (dateRange.length !== 2) return
NextButton: "border border-gray-300 px-2 py-1 w-20 rounded ml-2" let after = dateRange[0]
} let before = dateRange[1]
} before.setDate(before.getDate() + 1)
after.setDate(after.getDate() - 1)
return (
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col" const now = new Date();
style={{backgroundImage: `url(${background})`}}> now.setDate(now.getDate() + 5)
<Navbar history={props.history} color={"text-blue-600"} hover={"hover:border-blue-600"}/> const hundred = new Date(1900, 0, 0)
<div className="container text-gray-700 bg-white p-6 mt-12 mb-32 rounded"> if (after > now || after < hundred ||
<Griddle data={data} plugins={[plugins.LocalPlugin]} styleConfig={styleConfig} before > now || before < hundred)
pageProperties={{ return toast.error("Invalid Date Range!")
currentPage: 1,
pageSize: items setStatus("Loading Data...")
}} let {json, status} = await query("/data/query", {
components={{ token: authState.token,
Layout: Layout(setItems) validated: true,
}}> after_date: after,
<RowDefinition> before_date: before,
<ColumnDefinition id="ID"/> }).catch(() => ({status: 500, json: {reason: "The authentication server is offline!"}}))
<ColumnDefinition id="Date"/> setStatus("")
<ColumnDefinition id="Precipitation"/>
<ColumnDefinition id="Latitude"/> if (status !== 200) return toast.error(json.reason)
<ColumnDefinition id="Longitude"/>
<ColumnDefinition id="Remarks"/> let newData = defaultData()
</RowDefinition> const formatDate = (date) => {
</Griddle> return date.getFullYear() + "-" + (date.getMonth() + 1).toString().padStart(2, "0") + "-" + date.getDate().toString().padStart(2, "0")
</div> }
<div className="container text-gray-700 bg-white p-6 mb-32 rounded"> json.entries.forEach((entry, index) => {
<div className="text-2xl font-semibold">Statistics</div> const date = new Date(entry.date);
</div> const group = json.groups.find(gr => gr.id === entry.group_id)
</div> newData[index] = {
); ID: entry.id,
} Date: formatDate(date),
Precipitation: entry.precipitation,
export default ViewAll; Latitude: group.latitude,
Longitude: group.longitude,
const Layout = (setItems) => (({Table, Filter, Pagination}) => ( Remarks: entry.remarks,
<div> }
<div className="flex w-96 pb-4 h-12 justify-between"> })
<Filter/> setData(newData)
<select onChange={e => setItems(Number(e.target.value))}> before.setDate(before.getDate() - 1)
<option value="10">Show 10 items</option> after.setDate(after.getDate() + 1)
<option value="20">Show 20 items</option> window.history.pushState("Search", "Rain Track", "/all?after=" + formatDate(after) + "&before=" + formatDate(before))
<option value="50">Show 50 items</option> },
<option value="100">Show 100 items</option> [setData, authState.token, dateRange])
</select>
</div> useEffect(() => {
<Table/> (async () => {
<Pagination/> initiateData()
</div> })();
}, [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"
}
}
return (
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
style={{backgroundImage: `url(${background})`}}>
<Navbar history={history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
<div className="container text-gray-700 bg-white p-6 mt-12 mb-32 rounded">
<div className="flex">
<Flatpickr
value={dateRange}
onChange={date => setDateRange(date)}
options={{
dateFormat: "m-d-Y",
mode: "range",
wrap: true,
position: "below"
}}>
<div className="w-72">
<Input name="date" type='text' others={{"data-input": true}} className="input h-8"
placeholder="Select Date Range">
Select Date Range
</Input>
</div>
</Flatpickr>
<div className="italic text-sm ml-4 w-full flex items-center">{status}</div>
</div>
<hr className="bg-gray-400 mb-4"/>
<Griddle data={data} plugins={[plugins.LocalPlugin]} styleConfig={styleConfig}
pageProperties={{
currentPage: 1,
pageSize: items
}}
components={{
Layout: Layout(setItems)
}}>
<RowDefinition>
<ColumnDefinition id="ID"/>
<ColumnDefinition id="Date"/>
<ColumnDefinition id="Precipitation"/>
<ColumnDefinition id="Latitude"/>
<ColumnDefinition id="Longitude"/>
<ColumnDefinition id="Remarks"/>
</RowDefinition>
</Griddle>
</div>
<div className="container text-gray-700 bg-white p-6 mb-32 rounded">
<div className="text-2xl font-semibold mb-4">Statistics</div>
<div className="grid lg:grid-cols-3 md:grid-cols-2 grid-cols-1 row-gap-4 col-gap-12">
<Stat data={data} calculate={(items) => iqr(items)}>Interquartile Range</Stat>
<Stat data={data} calculate={(items) => std(items)}>Standard Deviation</Stat>
<Stat data={data} calculate={(items) => mean(items)}>Mean (Average)</Stat>
<Stat data={data} calculate={(items) => median(items)}>Median</Stat>
<Stat data={data} calculate={(items) => mode(items)}>Mode</Stat>
<Stat data={data} calculate={(items) => max(items) - min(items)}>Range</Stat>
</div>
</div>
</div>
);
}
const Stat = ({children, data, calculate}) => (
<div className="rounded border-t-4 border-blue-400 shadow p-2 pt-1">
<div className="lg:text-xl font-semibold text-gray-700">{children}</div>
<div className="text-md">{data.length !== 0 && calculate(data.map(set => set.Precipitation)).toString()}</div>
</div>
)
export default ViewAll;
const Layout = (setItems) => (({Table, Filter, Pagination}) => (
<div>
<div className="flex w-96 pb-4 h-12 justify-between">
<Filter/>
<select onChange={e => setItems(Number(e.target.value))}>
<option value="10">Show 10 items</option>
<option value="20">Show 20 items</option>
<option value="50">Show 50 items</option>
<option value="100">Show 100 items</option>
</select>
</div>
<div className="lg:overflow-x-auto overflow-x-scroll">
<Table/>
</div>
<Pagination/>
</div>
)) ))

@ -0,0 +1,241 @@
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 MyData({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)
}, [props.location])
useEffect(() => {
if (!authState.loggedIn) history.push("/login?return=mydata")
}, [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 (
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
style={{backgroundImage: `url(${background})`}}>
<Navbar history={history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
<div className="container text-gray-700 bg-white p-6 mt-12 mb-32 rounded">
<div className="flex">
<Flatpickr
value={dateRange}
onChange={date => setDateRange(date)}
options={{
dateFormat: "m-d-Y",
mode: "range",
wrap: true,
position: "below"
}}>
<div className="w-72">
<Input name="date" type='text' others={{"data-input": true}} className="input h-8"
placeholder="Select Date Range">
Select Date Range
</Input>
</div>
</Flatpickr>
<div className="italic text-sm ml-4 w-full flex items-center">{status}</div>
</div>
<hr className="bg-gray-400 mb-4"/>
<Griddle data={data} plugins={[plugins.LocalPlugin]} styleConfig={styleConfig}
pageProperties={{
currentPage: 1,
pageSize: items
}}
components={{
Layout: Layout(setItems)
}}>
<RowDefinition>
<ColumnDefinition id="ID"/>
<ColumnDefinition id="Date" title="Date"/>
<ColumnDefinition id="Precipitation"/>
<ColumnDefinition id="Latitude"/>
<ColumnDefinition id="Longitude"/>
<ColumnDefinition id="Remarks"/>
<ColumnDefinition id="Verified"/>
<ColumnDefinition id=" "
customComponent={(items) => <EditComponent {...items} dispatch={dispatchEdit}/>}/>
</RowDefinition>
</Griddle>
</div>
<Modal
onRequestClose={() => setEditModal({open: false, data: {ID: 0}})}
shouldCloseOnOverlayClick
isOpen={editModal.open}>
<div className="flex justify-center">
<FileManualForm title={"Edit Data"} id={editModal.data.ID} editType defaultData={editModal.data}/>
</div>
</Modal>
</div>
);
}
export default MyData;
const Layout = (setItems) => (({Table, Filter, Pagination}) => (
<div>
<div className="flex w-96 pb-4 h-12 justify-between">
<Filter/>
<select onChange={e => setItems(Number(e.target.value))}>
<option value="10">Show 10 items</option>
<option value="20">Show 20 items</option>
<option value="50">Show 50 items</option>
<option value="100">Show 100 items</option>
</select>
</div>
<div className="lg:overflow-x-auto overflow-x-scroll">
<Table/>
</div>
<Pagination/>
</div>
))
const EditComponent = ({griddleKey, dispatch}) => {
return <div className="flex justify-around">
<span onClick={() => dispatch([0, griddleKey])}><AiFillEdit/></span>
<span onClick={() => dispatch([1, griddleKey])}><AiFillDelete/></span>
</div>
}

@ -1,14 +1,14 @@
import React from "react"; import React from "react";
export function Input({name, type, errors = {}, mainClasses = "mb-2", ...props}) { export function Input({name, type, errors = {}, mainClasses = "mb-2", ...props}) {
const classes = () => "input " + (errors[name] ? "error" : "") const classes = () => "input w-full h-10 " + (errors[name] ? "error" : "")
return <div className={mainClasses}> return <div className={mainClasses}>
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor={name}>{props.children}</label> <label className="block text-gray-700 text-sm font-bold mb-2" htmlFor={name}>{props.children}</label>
<input className={classes()} id={name} name={name} ref={props.register} <input className={classes()} id={name} name={name} ref={props.register}
type={type} autoComplete="off" placeholder={props.placeholder} type={type} autoComplete="off" placeholder={props.placeholder} {...props.others}
defaultValue={props.value && props.value} onChange={props.setValue && (e => props.setValue(e.target.value))} defaultValue={props.value && props.value} onChange={props.setValue && (e => props.setValue(e.target.value))}
/> />
<p className="input-error">&nbsp;{errors[name]?.message}</p> <p className="input-error">&nbsp;{errors[name]?.message}</p>
</div> </div>
} }

@ -2,7 +2,9 @@ import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import App from './App'; import App from './App';
import * as serviceWorker from './serviceWorker'; import * as serviceWorker from './serviceWorker';
import ReactModal from "react-modal";
ReactModal.setAppElement("#root")
ReactDOM.render( ReactDOM.render(
<React.StrictMode> <React.StrictMode>
<App/> <App/>

@ -608,289 +608,6 @@ video {
} }
} }
table {
border-collapse: collapse;
}
caption {
padding-top: .75rem;
padding-bottom: .75rem;
color: #6c757d;
text-align: left;
caption-side: bottom;
}
th {
text-align: inherit;
}
.table {
width: 100%;
max-width: 100%;
margin-bottom: 1rem;
background-color: transparent;
}
.table td,.table th {
padding: .75rem;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
.table td {
border-top: 1px solid #dee2e6;
}
.table thead th {
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
}
.table tbody+tbody {
border-top: 2px solid #dee2e6;
}
.table .table {
background-color: #fff;
}
.table-sm td,.table-sm th {
padding: .3rem;
}
.table-bordered {
border: 1px solid #dee2e6;
}
.table-bordered td,.table-bordered th {
border: 1px solid #dee2e6;
}
.table-bordered thead td,.table-bordered thead th {
border-bottom-width: 2px;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0,0,0,.05);
}
.table-hover tbody tr:hover {
background-color: rgba(0,0,0,.075);
}
.table-primary,.table-primary>td,.table-primary>th {
background-color: #b8daff;
}
.table-hover .table-primary:hover {
background-color: #9fcdff;
}
.table-hover .table-primary:hover>td,.table-hover .table-primary:hover>th {
background-color: #9fcdff;
}
.table-secondary,.table-secondary>td,.table-secondary>th {
background-color: #d6d8db;
}
.table-hover .table-secondary:hover {
background-color: #c8cbcf;
}
.table-hover .table-secondary:hover>td,.table-hover .table-secondary:hover>th {
background-color: #c8cbcf;
}
.table-success,.table-success>td,.table-success>th {
background-color: #c3e6cb;
}
.table-hover .table-success:hover {
background-color: #b1dfbb;
}
.table-hover .table-success:hover>td,.table-hover .table-success:hover>th {
background-color: #b1dfbb;
}
.table-info,.table-info>td,.table-info>th {
background-color: #bee5eb;
}
.table-hover .table-info:hover {
background-color: #abdde5;
}
.table-hover .table-info:hover>td,.table-hover .table-info:hover>th {
background-color: #abdde5;
}
.table-warning,.table-warning>td,.table-warning>th {
background-color: #ffeeba;
}
.table-hover .table-warning:hover {
background-color: #ffe8a1;
}
.table-hover .table-warning:hover>td,.table-hover .table-warning:hover>th {
background-color: #ffe8a1;
}
.table-danger,.table-danger>td,.table-danger>th {
background-color: #f5c6cb;
}
.table-hover .table-danger:hover {
background-color: #f1b0b7;
}
.table-hover .table-danger:hover>td,.table-hover .table-danger:hover>th {
background-color: #f1b0b7;
}
.table-light,.table-light>td,.table-light>th {
background-color: #fdfdfe;
}
.table-hover .table-light:hover {
background-color: #ececf6;
}
.table-hover .table-light:hover>td,.table-hover .table-light:hover>th {
background-color: #ececf6;
}
.table-dark,.table-dark>td,.table-dark>th {
background-color: #c6c8ca;
}
.table-hover .table-dark:hover {
background-color: #b9bbbe;
}
.table-hover .table-dark:hover>td,.table-hover .table-dark:hover>th {
background-color: #b9bbbe;
}
.table-active,.table-active>td,.table-active>th {
background-color: rgba(0,0,0,.075);
}
.table-hover .table-active:hover {
background-color: rgba(0,0,0,.075);
}
.table-hover .table-active:hover>td,.table-hover .table-active:hover>th {
background-color: rgba(0,0,0,.075);
}
.table .thead-dark th {
color: #fff;
background-color: #212529;
border-color: #32383e;
}
.table .thead-light th {
color: #495057;
background-color: #e9ecef;
border-color: #dee2e6;
}
.table-dark {
color: #fff;
background-color: #212529;
}
.table-dark td,.table-dark th,.table-dark thead th {
border-color: #32383e;
}
.table-dark.table-bordered {
border: 0;
}
.table-dark.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(255,255,255,.05);
}
.table-dark.table-hover tbody tr:hover {
background-color: rgba(255,255,255,.075);
}
.table-responsive {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
}
.table-responsive>.table-bordered {
border: 0;
}
.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th {
border: 0;
}
@media (max-width:575.98px) {
.table-responsive-sm {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
}
.table-responsive-sm>.table-bordered {
border: 0;
}
}
@media (max-width:767.98px) {
.table-responsive-md {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
}
.table-responsive-sm>.table-bordered {
border: 0;
}
}
@media (max-width:991.98px) {
.table-responsive-lg {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
}
.table-responsive-sm>.table-bordered {
border: 0;
}
}
@media (max-width:1199.98px) {
.table-responsive-xl {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
-ms-overflow-style: -ms-autohiding-scrollbar;
}
.table-responsive-sm>.table-bordered {
border: 0;
}
}
/* purgecss end ignore */ /* purgecss end ignore */
.space-y-0 > :not(template) ~ :not(template) { .space-y-0 > :not(template) ~ :not(template) {
@ -8211,6 +7928,10 @@ th {
max-width: 92rem; max-width: 92rem;
} }
.max-w-10xl {
max-width: 128rem;
}
.max-w-full { .max-w-full {
max-width: 100%; max-width: 100%;
} }
@ -15295,6 +15016,10 @@ th {
/*forms*/ /*forms*/
body {
overflow-x: hidden;
}
.input { .input {
box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06); box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06);
-webkit-appearance: none; -webkit-appearance: none;
@ -15302,7 +15027,6 @@ th {
appearance: none; appearance: none;
border-width: 1px; border-width: 1px;
border-radius: 0.25rem; border-radius: 0.25rem;
width: 100%;
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
padding-top: 0.5rem; padding-top: 0.5rem;
padding-bottom: 0.5rem; padding-bottom: 0.5rem;
@ -15312,7 +15036,6 @@ th {
color: #4a5568; color: #4a5568;
color: rgba(74, 85, 104, var(--text-opacity)); color: rgba(74, 85, 104, var(--text-opacity));
line-height: 1.25; line-height: 1.25;
height: 2.5rem;
vertical-align: text-top; vertical-align: text-top;
transition: all 0.3s; transition: all 0.3s;
} }
@ -15355,6 +15078,13 @@ th {
table-layout: auto; table-layout: auto;
} }
.flatpickr-weekday, .flatpickr-day {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */ /* Konqueror HTML */ /* Firefox */ /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
}
@media (min-width: 640px) { @media (min-width: 640px) {
.sm\:space-y-0 > :not(template) ~ :not(template) { .sm\:space-y-0 > :not(template) ~ :not(template) {
--space-y-reverse: 0; --space-y-reverse: 0;
@ -22674,6 +22404,10 @@ th {
max-width: 92rem; max-width: 92rem;
} }
.sm\:max-w-10xl {
max-width: 128rem;
}
.sm\:max-w-full { .sm\:max-w-full {
max-width: 100%; max-width: 100%;
} }
@ -37076,6 +36810,10 @@ th {
max-width: 92rem; max-width: 92rem;
} }
.md\:max-w-10xl {
max-width: 128rem;
}
.md\:max-w-full { .md\:max-w-full {
max-width: 100%; max-width: 100%;
} }
@ -51478,6 +51216,10 @@ th {
max-width: 92rem; max-width: 92rem;
} }
.lg\:max-w-10xl {
max-width: 128rem;
}
.lg\:max-w-full { .lg\:max-w-full {
max-width: 100%; max-width: 100%;
} }
@ -65880,6 +65622,10 @@ th {
max-width: 92rem; max-width: 92rem;
} }
.xl\:max-w-10xl {
max-width: 128rem;
}
.xl\:max-w-full { .xl\:max-w-full {
max-width: 100%; max-width: 100%;
} }

@ -0,0 +1 @@
table{border-collapse:collapse}caption{padding-top:0.75rem;padding-bottom:0.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}.table{width:100%;max-width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:0.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table td{border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:0.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary > td,.table-primary > th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover > td,.table-hover .table-primary:hover > th{background-color:#9fcdff}.table-secondary,.table-secondary > td,.table-secondary > th{background-color:#d6d8db}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover > td,.table-hover .table-secondary:hover > th{background-color:#c8cbcf}.table-success,.table-success > td,.table-success > th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover > td,.table-hover .table-success:hover > th{background-color:#b1dfbb}.table-info,.table-info > td,.table-info > th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover > td,.table-hover .table-info:hover > th{background-color:#abdde5}.table-warning,.table-warning > td,.table-warning > th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover > td,.table-hover .table-warning:hover > th{background-color:#ffe8a1}.table-danger,.table-danger > td,.table-danger > th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover > td,.table-hover .table-danger:hover > th{background-color:#f1b0b7}.table-light,.table-light > td,.table-light > th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover > td,.table-hover .table-light:hover > th{background-color:#ececf6}.table-dark,.table-dark > td,.table-dark > th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover > td,.table-hover .table-dark:hover > th{background-color:#b9bbbe}.table-active,.table-active > td,.table-active > th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover > td,.table-hover .table-active:hover > th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive > .table-bordered{border:0}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm > .table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm > .table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm > .table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm > .table-bordered{border:0}}

@ -1,47 +1,61 @@
/* purgecss start ignore */ /* purgecss start ignore */
@tailwind base; @tailwind base;
@tailwind components; @tailwind components;
/* purgecss end ignore */ /* purgecss end ignore */
@tailwind utilities; @tailwind utilities;
/*forms*/ /*forms*/
.input { body {
@apply shadow appearance-none border rounded w-full mb-1 py-2 px-3 text-gray-700 leading-tight h-10 align-text-top; overflow-x: hidden;
transition: all 0.3s; }
}
.input {
.input.error { @apply shadow appearance-none border rounded mb-1 py-2 px-3 text-gray-700 leading-tight align-text-top;
@apply border-red-500 transition: all 0.3s;
} }
.input:focus { .input.error {
@apply text-black outline-none; @apply border-red-500
-webkit-box-shadow: 0px 0px 4px 3px #4f89ed; }
-moz-box-shadow: 0px 0px 4px 3px #4f89ed;
box-shadow: 0px 0px 4px 3px #4f89ed; .input:focus {
} @apply text-black outline-none;
-webkit-box-shadow: 0px 0px 4px 3px #4f89ed;
.input.error:focus { -moz-box-shadow: 0px 0px 4px 3px #4f89ed;
-webkit-box-shadow: 0px 0px 4px 3px #f56565; box-shadow: 0px 0px 4px 3px #4f89ed;
-moz-box-shadow: 0px 0px 4px 3px #f56565; }
box-shadow: 0px 0px 4px 3px #f56565;
} .input.error:focus {
-webkit-box-shadow: 0px 0px 4px 3px #f56565;
.input-error { -moz-box-shadow: 0px 0px 4px 3px #f56565;
@apply text-red-500 text-xs italic h-5 box-shadow: 0px 0px 4px 3px #f56565;
}
}
.input-error {
.Toastify__toast { @apply text-red-500 text-xs italic h-5
padding-left: 24px !important;
@apply text-sm; }
}
.Toastify__toast {
padding-left: 24px !important;
/* table stuff */ @apply text-sm;
}
.griddle-table {
@apply table-auto;
/* table stuff */
.griddle-table {
@apply table-auto;
}
.flatpickr-weekday, .flatpickr-day {
-webkit-touch-callout: none; /* iOS Safari */
-webkit-user-select: none; /* Safari */
-khtml-user-select: none; /* Konqueror HTML */
-moz-user-select: none; /* Firefox */
-ms-user-select: none; /* Internet Explorer/Edge */
user-select: none; /* Non-prefixed version, currently
supported by Chrome and Opera */
} }

@ -345,6 +345,7 @@ module.exports = {
'5xl': '64rem', '5xl': '64rem',
'6xl': '72rem', '6xl': '72rem',
'8xl': '92rem', '8xl': '92rem',
'10xl': '128rem',
full: '100%', full: '100%',
...breakpoints(theme('screens')), ...breakpoints(theme('screens')),
}), }),
@ -737,6 +738,5 @@ module.exports = {
}, },
corePlugins: {}, corePlugins: {},
plugins: [ plugins: [
require('tailwindcss-tables')(),
], ],
} }