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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -5477,6 +5494,11 @@
"strip-eof": "^1.0.0"
}
},
"exenv": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/exenv/-/exenv-1.2.2.tgz",
"integrity": "sha1-KueOhdmJQVhnCwPUe+wfA72Ru50="
},
"exit": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz",
@ -5936,6 +5958,11 @@
"write": "1.0.3"
}
},
"flatpickr": {
"version": "4.6.3",
"resolved": "https://registry.npmjs.org/flatpickr/-/flatpickr-4.6.3.tgz",
"integrity": "sha512-007VucCkqNOMMb9ggRLNuJowwaJcyOh4sKAFcdGfahfGc7JQbf94zSzjdBq/wVyHWUEs5o3+idhFZ0wbZMRmVQ=="
},
"flatted": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz",
@ -5980,22 +6007,9 @@
}
},
"follow-redirects": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.11.0.tgz",
"integrity": "sha512-KZm0V+ll8PfBrKwMzdo5D13b1bur9Iq9Zd/RMmAoQQcl2PxxFml8cxXPaaPYVbV0RjNjq1CU7zIzAOqtUPudmA==",
"requires": {
"debug": "^3.0.0"
},
"dependencies": {
"debug": {
"version": "3.2.6",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz",
"integrity": "sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ==",
"requires": {
"ms": "^2.1.1"
}
}
}
"version": "1.12.1",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.12.1.tgz",
"integrity": "sha512-tmRv0AVuR7ZyouUHLeNSiO6pqulF7dYa3s19c6t+wz9LD69/uSzdMxJ2S91nTI9U3rt/IldxpzMOFejp6f0hjg=="
},
"for-in": {
"version": "1.0.2",
@ -6663,9 +6677,9 @@
"integrity": "sha1-ksnBN0w1CF912zWexWzCV8u5P6Q="
},
"http-proxy": {
"version": "1.18.0",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.0.tgz",
"integrity": "sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ==",
"version": "1.18.1",
"resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz",
"integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==",
"requires": {
"eventemitter3": "^4.0.0",
"follow-redirects": "^1.0.0",
@ -8111,9 +8125,9 @@
}
},
"lodash": {
"version": "4.17.15",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz",
"integrity": "sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A=="
"version": "4.17.19",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.19.tgz",
"integrity": "sha512-JNvd8XER9GQX0v2qJgsaN/mzFCNA5BRe/j8JN9d+tWyGLSodKQHKFicdwNYzWwI3wjRnaKPsGj1XkBjx/F96DQ=="
},
"lodash-es": {
"version": "4.17.15",
@ -11402,11 +11416,28 @@
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz",
"integrity": "sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA=="
},
"react-flatpickr": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/react-flatpickr/-/react-flatpickr-3.10.0.tgz",
"integrity": "sha512-eeU5OlUZgWDyp2XvIeZ4+rI4T4VkqU1MdZUb+cXtTHfHYfLV+gpP0lAhRrij+QXlbPXTJFAbPerihohlOeTJ0A==",
"requires": {
"flatpickr": "^4.5.7",
"prop-types": "^15.5.10"
}
},
"react-hook-form": {
"version": "5.7.2",
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-5.7.2.tgz",
"integrity": "sha512-bJvY348vayIvEUmSK7Fvea/NgqbT2racA2IbnJz/aPlQ3GBtaTeDITH6rtCa6y++obZzG6E3Q8VuoXPir7QYUg=="
},
"react-icons": {
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/react-icons/-/react-icons-3.10.0.tgz",
"integrity": "sha512-WsQ5n1JToG9VixWilSo1bHv842Cj5aZqTGiS3Ud47myF6aK7S/IUY2+dHcBdmkQcCFRuHsJ9OMUI0kTDfjyZXQ==",
"requires": {
"camelcase": "^5.0.0"
}
},
"react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@ -11417,6 +11448,17 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
},
"react-modal": {
"version": "3.11.2",
"resolved": "https://registry.npmjs.org/react-modal/-/react-modal-3.11.2.tgz",
"integrity": "sha512-o8gvvCOFaG1T7W6JUvsYjRjMVToLZgLIsi5kdhFIQCtHxDkA47LznX62j+l6YQkpXDbvQegsDyxe/+JJsFQN7w==",
"requires": {
"exenv": "^1.2.0",
"prop-types": "^15.5.10",
"react-lifecycles-compat": "^3.0.0",
"warning": "^4.0.3"
}
},
"react-redux": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-5.1.2.tgz",
@ -13955,6 +13997,19 @@
"spdx-expression-parse": "^3.0.0"
}
},
"validate.io-array": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/validate.io-array/-/validate.io-array-1.0.6.tgz",
"integrity": "sha1-W1osr9j4uFq7L4hroVPy2Tond00="
},
"validate.io-object": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/validate.io-object/-/validate.io-object-1.0.4.tgz",
"integrity": "sha1-3KAezu45DhENvCr4Q8gfe/M6Qas=",
"requires": {
"validate.io-array": "^1.0.1"
}
},
"value-equal": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz",
@ -14016,6 +14071,14 @@
"makeerror": "1.0.x"
}
},
"warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"requires": {
"loose-envify": "^1.0.0"
}
},
"watchpack": {
"version": "1.6.1",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.6.1.tgz",

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

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

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

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

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

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

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

@ -1,129 +1,129 @@
import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react';
import {toast} from "react-toastify";
import {AuthContext} from "../App";
import {SERVER_URL} from "../auth";
import {Input} from "../custom";
const LIMIT = 2 * 1024 * 1024; // 2 mb
const dispatch = (state, action) => {
switch (action.type) {
case "lat":
return {...state, lat: action.value}
case "long":
return {...state, long: action.value}
default:
return;
}
}
function FileUploadForm() {
const {authState} = useContext(AuthContext)
const [files, setFiles] = useState(null);
const [load, setLoad] = useState(0)
const [status, setStatus] = useState("Upload a file")
const [latLong, setLatLong] = useReducer(dispatch, {lat: "", long: ""}, undefined)
let form = useRef(null);
async function query(url, data) {
console.debug("FETCH: " + SERVER_URL + url + " - " + data)
return new Promise((resolve, reject) => {
let req = new XMLHttpRequest()
req.open('post', SERVER_URL + url)
req.upload.addEventListener('progress', e => {
if (e.loaded === e.total) setStatus("Processing...")
else {
let percent = (e.loaded / e.total * 100)
setLoad(percent)
setStatus("Uploading " + percent.toFixed(0) + "%")
}
});
req.addEventListener('load', () => {
resolve({status: req.status, json: JSON.parse(req.response)})
});
req.addEventListener('error', e => {
resolve({status: req.status, json: JSON.parse(req.response)})
})
req.send(data)
})
}
const upload = useCallback(async () => {
if (!files || files.length === 0) return
const target = files[0]
if (target.size > LIMIT) return toast.error("File size too large! Max 2 mb.")
if (!target.name.includes(".xlsx")) return toast.error("Invalid file type!")
const {lat, long} = latLong;
setStatus("Uploading")
let file = new FormData()
file.append("file", target)
file.append("token", authState.token);
file.append("latitude", lat);
file.append("longitude", long);
let {status, json} = await query("/data/upload", file)
form.current.reset();
setStatus("Upload a file")
setLoad(0)
if (status !== 200) return toast.error(json.reason)
return toast.success("File successfully uploaded.")
}, [files, authState.token, form, latLong])
useEffect(() => {
(async () => upload())();
}, [files, authState.token, upload, authState, setLoad, setStatus])
return <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">
<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">
Click the button to upload an .xlsx file
</div>
<hr className="mb-6 border-gray-300 mx-12"/>
</div>
<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>
<Input name="lat" type="text" placeholder="Latitude"
setValue={e => setLatLong({type: "lat", value: e})}/>
<Input name="long" type="text" placeholder="Longitude"
setValue={e => setLatLong({type: "long", value: e})}/>
<div className="w-full flex justify-center">
<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">
<style dangerouslySetInnerHTML={{
__html:
'.loading::after {' +
'content: "";' +
`width: ${load}%;` +
'background: rgb(255, 255, 255, 0.4);' +
'position: absolute;' +
'top: 0;' +
'bottom: 0;' +
'left: 0;' +
'transition: width 0.3s;'
}}/>
<div className="flex items-center">
<svg className="w-4 h-4 mt-2 mr-3" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<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"/>
</svg>
<span className="mt-2 font-bold">{status}</span>
<input type='file' className="hidden" name="upload" accept=".xlsx"
onChange={e => setFiles(e.target.files)}/>
</div>
</label>
</div>
</form>
</div>;
}
import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react';
import {toast} from "react-toastify";
import {AuthContext} from "../App";
import {SERVER_URL} from "../auth";
import {Input} from "../custom";
const LIMIT = 2 * 1024 * 1024; // 2 mb
const dispatch = (state, action) => {
switch (action.type) {
case "lat":
return {...state, lat: action.value}
case "long":
return {...state, long: action.value}
default:
return;
}
}
function FileUploadForm() {
const {authState} = useContext(AuthContext)
const [files, setFiles] = useState(null);
const [load, setLoad] = useState(0)
const [status, setStatus] = useState("Upload a file")
const [latLong, setLatLong] = useReducer(dispatch, {lat: "", long: ""}, undefined)
let form = useRef(null);
async function query(url, data) {
console.debug("FETCH: " + SERVER_URL + url + " - " + data)
return new Promise((resolve, reject) => {
let req = new XMLHttpRequest()
req.open('post', SERVER_URL + url)
req.upload.addEventListener('progress', e => {
if (e.loaded === e.total) setStatus("Processing...")
else {
let percent = (e.loaded / e.total * 100)
setLoad(percent)
setStatus("Uploading " + percent.toFixed(0) + "%")
}
});
req.addEventListener('load', () => {
resolve({status: req.status, json: JSON.parse(req.response)})
});
req.addEventListener('error', e => {
resolve({status: req.status, json: JSON.parse(req.response)})
})
req.send(data)
})
}
const upload = useCallback(async () => {
if (!files || files.length === 0) return
const target = files[0]
if (target.size > LIMIT) return toast.error("File size too large! Max 2 mb.")
if (!target.name.includes(".xlsx")) return toast.error("Invalid file type!")
const {lat, long} = latLong;
setStatus("Uploading")
let file = new FormData()
file.append("file", target)
file.append("token", authState.token);
file.append("latitude", lat);
file.append("longitude", long);
let {status, json} = await query("/data/upload", file)
form.current.reset();
setStatus("Upload a file")
setLoad(0)
if (status !== 200) return toast.error(json.reason)
return toast.success("File successfully uploaded.")
}, [files, authState.token, form, latLong])
useEffect(() => {
(async () => upload())();
}, [files, authState.token, upload, authState, setLoad, setStatus])
return <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">
<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">
Click the button to upload an .xlsx file
</div>
<hr className="mb-6 border-gray-300 mx-12"/>
</div>
<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>
<Input name="lat" type="text" placeholder="Latitude"
setValue={e => setLatLong({type: "lat", value: e})}/>
<Input name="long" type="text" placeholder="Longitude"
setValue={e => setLatLong({type: "long", value: e})}/>
<div className="w-full flex justify-center">
<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">
<style dangerouslySetInnerHTML={{
__html:
'.loading::after {' +
'content: "";' +
`width: ${load}%;` +
'background: rgb(255, 255, 255, 0.4);' +
'position: absolute;' +
'top: 0;' +
'bottom: 0;' +
'left: 0;' +
'transition: width 0.3s;'
}}/>
<div className="flex items-center">
<svg className="w-4 h-4 mt-2 mr-3" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20">
<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"/>
</svg>
<span className="mt-2 font-bold">{status}</span>
<input type='file' className="hidden" name="upload" accept=".xlsx"
onChange={e => setFiles(e.target.files)}/>
</div>
</label>
</div>
</form>
</div>;
}
export default FileUploadForm;

@ -1,30 +1,30 @@
import React from 'react';
import Navbar from "./Navbar";
import {Link} from "react-router-dom";
const background = require('../assets/background.png')
function Home(props) {
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={props.history}/>
<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">
<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>
<Link to="/upload">
<div className="rounded-full bg-blue-600 inline-block mt-4 sm:py-3 py-3 sm:px-8 px-4">
Start Uploading Data
</div>
</Link>
</div>
</div>
);
}
import React from 'react';
import Navbar from "./Navbar";
import {Link} from "react-router-dom";
const background = require('../assets/background.png')
function Home(props) {
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={props.history}/>
<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">
<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>
<Link to="/upload">
<div className="rounded-full bg-blue-600 inline-block mt-4 sm:py-3 py-3 sm:px-8 px-4">
Start Uploading Data
</div>
</Link>
</div>
</div>
);
}
export default Home;

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

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

@ -1,69 +1,78 @@
import React, {useContext, useEffect, useState} from 'react';
import {Link} from "react-router-dom";
import {AuthContext} from "../App";
const pathsCenter = [
{name: "All Data", to: "/all", extra: ""},
{name: "Upload Data", to: "/upload", extra: ""},
{name: "Your Data", to: "/", extra: ""},
]
const pathsGuest = [
{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"},
]
const pathsLoggedIn = [
{name: "Account", to: "/account", extra: ""},
{name: "Sign Out", to: "/logout", extra: ""},
]
function Navbar({color = "text-white", hover="hover:border-white"}) {
const [showDropdown, setDropdown] = useState(false)
const [userPaths, setUserPaths] = useState(pathsGuest)
const {authState} = useContext(AuthContext)
useEffect(() => {
if (authState.loggedIn) {
setUserPaths(() => pathsLoggedIn)
} else setUserPaths(() => pathsGuest)
}, [authState.loggedIn])
return <header className={"lg:flex lg:justify-between lg:items-center px-4 py-3 w-full mt-2 " + color}>
<div className="flex flex-1 justify-between items-center mt-3 self-start">
<Link to={"/"}>
<h1 className="text-xl font-bold">RainTrack</h1>
</Link>
<button onClick={() => setDropdown(!showDropdown)}
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"
viewBox="0 0 20 20"><title>Toggle Navigation</title>
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/>
</svg>
</button>
</div>
<LinksContainer flex="lg:justify-center flex-2"
dropDown={showDropdown} paths={pathsCenter} color={color} hover={hover}/>
<LinksContainer flex="lg:justify-end flex-1"
dropDown={showDropdown} paths={userPaths} color={color} hover={hover}/>
</header>;
}
function LinksContainer(props) {
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}>
<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;
import React, {useContext, useEffect, useState} from 'react';
import {Link} from "react-router-dom";
import {AuthContext} from "../App";
const pathsCenter = [
{name: "All Data", to: "/all", extra: ""},
{name: "Upload Data", to: "/upload", extra: ""},
{name: "My Data", to: "/mydata", extra: ""},
]
const pathsGuest = [
{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"},
]
const pathsLoggedIn = [
{name: "Account", to: "/account", extra: ""},
{name: "Logout", to: "/logout", extra: ""},
]
const pathsAdmin = [
{name: "Admin", to: "/admin", extra: ""},
{name: "Account", to: "/account", extra: ""},
{name: "Logout", to: "/logout", extra: ""},
]
function Navbar({color = "text-white", hover = "hover:border-white"}) {
const [showDropdown, setDropdown] = useState(false)
const [userPaths, setUserPaths] = useState(pathsGuest)
const {authState} = useContext(AuthContext)
useEffect(() => {
console.log(authState)
if (authState.loggedIn) {
if (authState.admin) setUserPaths(() => pathsAdmin)
else setUserPaths(() => pathsLoggedIn)
} else setUserPaths(() => pathsGuest)
}, [authState])
return <header className={"lg:flex lg:justify-between lg:items-center px-4 py-3 w-full mt-2 " + color}>
<div className="flex flex-1 justify-between items-center mt-3 self-start">
<Link to={"/"}>
<h1 className="text-xl font-bold">RainTrack</h1>
</Link>
<button onClick={() => setDropdown(!showDropdown)}
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"
viewBox="0 0 20 20"><title>Toggle Navigation</title>
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/>
</svg>
</button>
</div>
<LinksContainer flex="lg:justify-center flex-2"
dropDown={showDropdown} paths={pathsCenter} color={color} hover={hover}/>
<LinksContainer flex="lg:justify-end flex-1"
dropDown={showDropdown} paths={userPaths} color={color} hover={hover}/>
</header>;
}
function LinksContainer(props) {
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}>
<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 Navbar from "./Navbar";
import {Link} from "react-router-dom";
const background = require('../assets/background.png')
function NotFound(props) {
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={props.history}/>
<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">
<h1 className="md:text-3xl sm:text-2xl text-xl font-bold">
The page you requested was not found. <br/>
Please confirm your URL or go back to the home page.
</h1>
<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>
</Link>
</div>
</div>
);
}
import React from 'react';
import Navbar from "./Navbar";
import {Link} from "react-router-dom";
const background = require('../assets/background.png')
function NotFound(props) {
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={props.history}/>
<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">
<h1 className="md:text-3xl sm:text-2xl text-xl font-bold">
The page you requested was not found. <br/>
Please confirm your URL or go back to the home page.
</h1>
<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>
</Link>
</div>
</div>
);
}
export default NotFound;

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

@ -1,32 +1,32 @@
import React, {useContext, useEffect} from 'react';
import Navbar from "./Navbar";
import {AuthContext} from "../App";
import FileUploadForm from "./FileUploadForm";
import FileManualForm from "./FileManualForm";
const background = require('../assets/wave.png')
function Upload(props) {
const {authState} = useContext(AuthContext)
useEffect(() => {
if (!authState.loggedIn) props.history.push("/login?return=upload")
}, [authState.loggedIn, props.history])
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={props.history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
<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">
<FileUploadForm/>
<FileManualForm/>
</div>
</div>
</div>
);
}
import React, {useContext, useEffect} from 'react';
import Navbar from "./Navbar";
import {AuthContext} from "../App";
import FileUploadForm from "./FileUploadForm";
import FileManualForm from "./FileManualForm";
const background = require('../assets/wave.png')
function Upload(props) {
const {authState} = useContext(AuthContext)
useEffect(() => {
if (!authState.loggedIn) props.history.push("/login?return=upload")
}, [authState.loggedIn, props.history])
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={props.history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
<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">
<FileUploadForm/>
<FileManualForm/>
</div>
</div>
</div>
);
}
export default Upload;

@ -1,109 +1,207 @@
import React, {useCallback, useContext, useEffect, useState} from 'react';
import Navbar from "./Navbar";
import {AuthContext} from "../App";
import Griddle, {ColumnDefinition, plugins, RowDefinition} from "griddle-react";
import {query} from "../auth";
const background = require('../assets/wave.png')
function ViewAll(props) {
const {authState} = useContext(AuthContext)
const [data, setData] = useState([{}])
const [items, setItems] = useState(10)
useEffect(() => {
if (!authState.loggedIn) props.history.push("/login?return=all")
}, [authState.loggedIn, props.history])
const initiateData = useCallback(async () => {
let {json, status} = await query("/data/query", {token: authState.token, validated: false}).catch(() => {
return {status: 500, json: {reason: "The authentication server is offline!"}}
})
if (status !== 200) {
return
}
let data = []
json.entries.forEach((entry, index) => {
const date = new Date(entry.date);
const formattedDate = date.getFullYear() + "-" + (date.getMonth() + 1).toString().padStart(2, "0") + "-" + date.getDate().toString().padStart(2, "0")
const group = json.groups.find(gr => gr.id === entry.group_id)
data[index] = {
ID: entry.id,
Date: formattedDate,
Precipitation: entry.precipitation,
Latitude: group.latitude,
Longitude: group.longitude,
Remarks: entry.remarks,
}
})
setData(data)
}, [setData, authState.token])
useEffect(() => {
(async () => {
initiateData()
})();
}, [setData, initiateData])
const styleConfig = {
classNames: {
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",
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={props.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">
<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">Statistics</div>
</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>
<Table/>
<Pagination/>
</div>
import React, {useCallback, useContext, useEffect, 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 {max, mean, median, min, mode, std} from "mathjs";
import {toast} from "react-toastify";
const background = require('../assets/wave.png')
const iqr = require('compute-iqr');
const defaultData = () => [{
ID: "No Data Found",
Date: "",
Precipitation: "",
Latitude: "",
Longitude: "",
Remarks: "",
}]
function ViewAll({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([]);
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=all")
}, [authState, history])
const initiateData = useCallback(async () => {
if (dateRange.length !== 2) 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,
validated: true,
after_date: after,
before_date: before,
}).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,
Remarks: entry.remarks,
}
})
setData(newData)
before.setDate(before.getDate() - 1)
after.setDate(after.getDate() + 1)
window.history.pushState("Search", "Rain Track", "/all?after=" + formatDate(after) + "&before=" + formatDate(before))
},
[setData, authState.token, dateRange])
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"
}
}
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";
export function Input({name, type, errors = {}, mainClasses = "mb-2", ...props}) {
const classes = () => "input " + (errors[name] ? "error" : "")
return <div className={mainClasses}>
<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}
type={type} autoComplete="off" placeholder={props.placeholder}
defaultValue={props.value && props.value} onChange={props.setValue && (e => props.setValue(e.target.value))}
/>
<p className="input-error">&nbsp;{errors[name]?.message}</p>
</div>
import React from "react";
export function Input({name, type, errors = {}, mainClasses = "mb-2", ...props}) {
const classes = () => "input w-full h-10 " + (errors[name] ? "error" : "")
return <div className={mainClasses}>
<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}
type={type} autoComplete="off" placeholder={props.placeholder} {...props.others}
defaultValue={props.value && props.value} onChange={props.setValue && (e => props.setValue(e.target.value))}
/>
<p className="input-error">&nbsp;{errors[name]?.message}</p>
</div>
}

@ -2,7 +2,9 @@ import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import * as serviceWorker from './serviceWorker';
import ReactModal from "react-modal";
ReactModal.setAppElement("#root")
ReactDOM.render(
<React.StrictMode>
<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 */
.space-y-0 > :not(template) ~ :not(template) {
@ -8211,6 +7928,10 @@ th {
max-width: 92rem;
}
.max-w-10xl {
max-width: 128rem;
}
.max-w-full {
max-width: 100%;
}
@ -15295,6 +15016,10 @@ th {
/*forms*/
body {
overflow-x: hidden;
}
.input {
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;
@ -15302,7 +15027,6 @@ th {
appearance: none;
border-width: 1px;
border-radius: 0.25rem;
width: 100%;
margin-bottom: 0.25rem;
padding-top: 0.5rem;
padding-bottom: 0.5rem;
@ -15312,7 +15036,6 @@ th {
color: #4a5568;
color: rgba(74, 85, 104, var(--text-opacity));
line-height: 1.25;
height: 2.5rem;
vertical-align: text-top;
transition: all 0.3s;
}
@ -15355,6 +15078,13 @@ th {
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) {
.sm\:space-y-0 > :not(template) ~ :not(template) {
--space-y-reverse: 0;
@ -22674,6 +22404,10 @@ th {
max-width: 92rem;
}
.sm\:max-w-10xl {
max-width: 128rem;
}
.sm\:max-w-full {
max-width: 100%;
}
@ -37076,6 +36810,10 @@ th {
max-width: 92rem;
}
.md\:max-w-10xl {
max-width: 128rem;
}
.md\:max-w-full {
max-width: 100%;
}
@ -51478,6 +51216,10 @@ th {
max-width: 92rem;
}
.lg\:max-w-10xl {
max-width: 128rem;
}
.lg\:max-w-full {
max-width: 100%;
}
@ -65880,6 +65622,10 @@ th {
max-width: 92rem;
}
.xl\:max-w-10xl {
max-width: 128rem;
}
.xl\:max-w-full {
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 */
@tailwind base;
@tailwind components;
/* purgecss end ignore */
@tailwind utilities;
/*forms*/
.input {
@apply shadow appearance-none border rounded w-full mb-1 py-2 px-3 text-gray-700 leading-tight h-10 align-text-top;
transition: all 0.3s;
}
.input.error {
@apply border-red-500
}
.input:focus {
@apply text-black outline-none;
-webkit-box-shadow: 0px 0px 4px 3px #4f89ed;
-moz-box-shadow: 0px 0px 4px 3px #4f89ed;
box-shadow: 0px 0px 4px 3px #4f89ed;
}
.input.error:focus {
-webkit-box-shadow: 0px 0px 4px 3px #f56565;
-moz-box-shadow: 0px 0px 4px 3px #f56565;
box-shadow: 0px 0px 4px 3px #f56565;
}
.input-error {
@apply text-red-500 text-xs italic h-5
}
.Toastify__toast {
padding-left: 24px !important;
@apply text-sm;
}
/* table stuff */
.griddle-table {
@apply table-auto;
/* purgecss start ignore */
@tailwind base;
@tailwind components;
/* purgecss end ignore */
@tailwind utilities;
/*forms*/
body {
overflow-x: hidden;
}
.input {
@apply shadow appearance-none border rounded mb-1 py-2 px-3 text-gray-700 leading-tight align-text-top;
transition: all 0.3s;
}
.input.error {
@apply border-red-500
}
.input:focus {
@apply text-black outline-none;
-webkit-box-shadow: 0px 0px 4px 3px #4f89ed;
-moz-box-shadow: 0px 0px 4px 3px #4f89ed;
box-shadow: 0px 0px 4px 3px #4f89ed;
}
.input.error:focus {
-webkit-box-shadow: 0px 0px 4px 3px #f56565;
-moz-box-shadow: 0px 0px 4px 3px #f56565;
box-shadow: 0px 0px 4px 3px #f56565;
}
.input-error {
@apply text-red-500 text-xs italic h-5
}
.Toastify__toast {
padding-left: 24px !important;
@apply text-sm;
}
/* 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',
'6xl': '72rem',
'8xl': '92rem',
'10xl': '128rem',
full: '100%',
...breakpoints(theme('screens')),
}),
@ -737,6 +738,5 @@ module.exports = {
},
corePlugins: {},
plugins: [
require('tailwindcss-tables')(),
],
}