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",

@ -11,17 +11,16 @@ const purgecss = require('@fullhuman/postcss-purgecss')({
"./src/App.js",
],
defaultExtractor: content => {
const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []
const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || []
defaultExtractor: content => content.match(/[\w-:/]+(?<!:)/g) || []
return broadMatches.concat(innerMatches)
}
})
const tailwind = tailwindcss('./tailwind.js')
console.log(tailwind)
module.exports = {
plugins: [
tailwindcss('./tailwind.js'),
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,5 +1,6 @@
<?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$">

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

@ -69,6 +69,7 @@ export async function query(url, data) {
headers: {"Content-Type": "application/json"},
body: JSON.stringify(data)
})
return {status: response.status, json: await response.json()}
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,14 +1,14 @@
import React, {useContext, useState} from 'react';
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() {
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} = useForm();
const {register, handleSubmit, errors, setError, reset, setValue} = useForm();
const manualUpload = async (data) => {
const {status, json} = await query("/data/add", data).catch(() => {
@ -27,28 +27,46 @@ function FileManualForm() {
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 (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="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)}>
<form className={formClasses} onSubmit={handleSubmit(submitManual)}>
<div className="text-lg font-medium mb-4">Upload Individual Entry</div>
<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}

@ -5,7 +5,7 @@ import {AuthContext} from "../App";
const pathsCenter = [
{name: "All Data", to: "/all", extra: ""},
{name: "Upload Data", to: "/upload", extra: ""},
{name: "Your Data", to: "/", extra: ""},
{name: "My Data", to: "/mydata", extra: ""},
]
const pathsGuest = [
@ -15,19 +15,28 @@ const pathsGuest = [
const pathsLoggedIn = [
{name: "Account", to: "/account", extra: ""},
{name: "Sign Out", to: "/logout", extra: ""},
{name: "Logout", to: "/logout", extra: ""},
]
function Navbar({color = "text-white", hover="hover:border-white"}) {
const pathsAdmin = [
{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) {
setUserPaths(() => pathsLoggedIn)
if (authState.admin) setUserPaths(() => pathsAdmin)
else setUserPaths(() => pathsLoggedIn)
} else setUserPaths(() => pathsGuest)
}, [authState.loggedIn])
}, [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">

@ -4,55 +4,113 @@ 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(props) {
function ViewAll({history, ...props}) {
const {authState} = useContext(AuthContext)
const [data, setData] = useState([{}])
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) props.history.push("/login?return=all")
}, [authState.loggedIn, props.history])
if (!authState.loggedIn) history.push("/login?return=all")
}, [authState, 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
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")
}
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] = {
newData[index] = {
ID: entry.id,
Date: formattedDate,
Date: formatDate(date),
Precipitation: entry.precipitation,
Latitude: group.latitude,
Longitude: group.longitude,
Remarks: entry.remarks,
}
})
setData(data)
}, [setData, authState.token])
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])
}, [setData, initiateData, dateRange, authState.username])
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",
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"
}
@ -61,9 +119,31 @@ function ViewAll(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} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
<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,
@ -84,17 +164,33 @@ function ViewAll(props) {
</div>
<div className="container text-gray-700 bg-white p-6 mb-32 rounded">
<div className="text-2xl font-semibold">Statistics</div>
<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>
@ -103,7 +199,9 @@ const Layout = (setItems) => (({Table, Filter, Pagination}) => (
<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,12 +1,12 @@
import React from "react";
export function Input({name, type, errors = {}, mainClasses = "mb-2", ...props}) {
const classes = () => "input " + (errors[name] ? "error" : "")
const classes = () => "input w-full h-10 " + (errors[name] ? "error" : "")
return <div className={mainClasses}>
<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}
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>

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

@ -7,8 +7,12 @@
/*forms*/
body {
overflow-x: hidden;
}
.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;
@apply shadow appearance-none border rounded mb-1 py-2 px-3 text-gray-700 leading-tight align-text-top;
transition: all 0.3s;
}
@ -45,3 +49,13 @@
.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')(),
],
}