last changed
parent
9d6f948c51
commit
71b88f91d8
@ -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>
|
|
@ -1,30 +1,29 @@
|
|||||||
const tailwindcss = require('tailwindcss');
|
const tailwindcss = require('tailwindcss');
|
||||||
const nano = require('cssnano')
|
const nano = require('cssnano')
|
||||||
|
|
||||||
const purgecss = require('@fullhuman/postcss-purgecss')({
|
const purgecss = require('@fullhuman/postcss-purgecss')({
|
||||||
//
|
//
|
||||||
content: [
|
content: [
|
||||||
"./src/components/*.js",
|
"./src/components/*.js",
|
||||||
"./node_modules/react-toastify/dist/ReactToastify.min.css",
|
"./node_modules/react-toastify/dist/ReactToastify.min.css",
|
||||||
"./public/index.html",
|
"./public/index.html",
|
||||||
"./src/custom.js",
|
"./src/custom.js",
|
||||||
"./src/App.js",
|
"./src/App.js",
|
||||||
],
|
],
|
||||||
|
|
||||||
defaultExtractor: content => {
|
defaultExtractor: content => content.match(/[\w-:/]+(?<!:)/g) || []
|
||||||
const broadMatches = content.match(/[^<>"'`\s]*[^<>"'`\s:]/g) || []
|
|
||||||
const innerMatches = content.match(/[^<>"'`\s.()]*[^<>"'`\s.():]/g) || []
|
})
|
||||||
|
|
||||||
return broadMatches.concat(innerMatches)
|
const tailwind = tailwindcss('./tailwind.js')
|
||||||
}
|
console.log(tailwind)
|
||||||
})
|
|
||||||
|
module.exports = {
|
||||||
module.exports = {
|
plugins: [
|
||||||
plugins: [
|
tailwind,
|
||||||
tailwindcss('./tailwind.js'),
|
require('autoprefixer'),
|
||||||
require('autoprefixer'),
|
...process.env.NODE_ENV === 'production'
|
||||||
...process.env.NODE_ENV === 'production'
|
? [purgecss, nano]
|
||||||
? [purgecss, nano]
|
: []
|
||||||
: []
|
]
|
||||||
]
|
|
||||||
}
|
}
|
@ -1,10 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="WEB_MODULE" version="4">
|
<module type="WEB_MODULE" version="4">
|
||||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
<component name="Go" enabled="true" />
|
||||||
<exclude-output />
|
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||||
<content url="file://$MODULE_DIR$">
|
<exclude-output />
|
||||||
<excludeFolder url="file://$MODULE_DIR$/build" />
|
<content url="file://$MODULE_DIR$">
|
||||||
</content>
|
<excludeFolder url="file://$MODULE_DIR$/build" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
</content>
|
||||||
</component>
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
</module>
|
</module>
|
@ -1,74 +1,75 @@
|
|||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
|
|
||||||
// export const SERVER_URL = "http://192.168.1.8:8080/api"
|
// export const SERVER_URL = "http://192.168.1.8:8080/api"
|
||||||
export const SERVER_URL = "http://dev.teamortix.com:4290/api"
|
export const SERVER_URL = "http://dev.teamortix.com:4290/api"
|
||||||
|
|
||||||
export const defaultAuth = () => ({
|
export const defaultAuth = () => ({
|
||||||
loggedIn: window.localStorage.getItem("authtoken") !== null,
|
loggedIn: window.localStorage.getItem("authtoken") !== null,
|
||||||
token: window.localStorage.getItem("authtoken"),
|
token: window.localStorage.getItem("authtoken"),
|
||||||
username: '',
|
username: '',
|
||||||
email: '',
|
email: '',
|
||||||
admin: false,
|
admin: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
export const reducer = (state, action) => {
|
export const reducer = (state, action) => {
|
||||||
return {...state, ...action.response}
|
return {...state, ...action.response}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function authenticate(token) {
|
export async function authenticate(token) {
|
||||||
if (token == null)
|
if (token == null)
|
||||||
return defaultAuth()
|
return defaultAuth()
|
||||||
|
|
||||||
const {status, json} = await query("/account/info", {token}).catch(() => {
|
const {status, json} = await query("/account/info", {token}).catch(() => {
|
||||||
toast.error("The authentication server is offline!")
|
toast.error("The authentication server is offline!")
|
||||||
return defaultAuth()
|
return defaultAuth()
|
||||||
})
|
})
|
||||||
|
|
||||||
if (status !== 200) {
|
if (status !== 200) {
|
||||||
window.localStorage.removeItem("authtoken")
|
window.localStorage.removeItem("authtoken")
|
||||||
toast.warn("You have been logged out.")
|
toast.warn("You have been logged out.")
|
||||||
return defaultAuth()
|
return defaultAuth()
|
||||||
}
|
}
|
||||||
console.debug("Authentication Response:", json)
|
console.debug("Authentication Response:", json)
|
||||||
|
|
||||||
const {email, username, admin} = json;
|
const {email, username, admin} = json;
|
||||||
return {username, email, admin, loggedIn: true}
|
return {username, email, admin, loggedIn: true}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function registration(username, email, password) {
|
export async function registration(username, email, password) {
|
||||||
const {status, json} = await query("/register", {username, email, password}).catch(() => {
|
const {status, json} = await query("/register", {username, email, password}).catch(() => {
|
||||||
return {status: 500, json: {reason: "The authentication server is offline!"}}
|
return {status: 500, json: {reason: "The authentication server is offline!"}}
|
||||||
})
|
})
|
||||||
|
|
||||||
console.debug("Registration Response:", json)
|
console.debug("Registration Response:", json)
|
||||||
return {status, json}
|
return {status, json}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(usernameOrEmail, password) {
|
export async function login(usernameOrEmail, password) {
|
||||||
const {status, json} = await query("/login", {usernameOrEmail, password}).catch(() => {
|
const {status, json} = await query("/login", {usernameOrEmail, password}).catch(() => {
|
||||||
return {status: 500, json: {reason: "The authentication server is offline!"}}
|
return {status: 500, json: {reason: "The authentication server is offline!"}}
|
||||||
})
|
})
|
||||||
console.debug("Login Response:", json)
|
console.debug("Login Response:", json)
|
||||||
|
|
||||||
return {status, json}
|
return {status, json}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function logout(token) {
|
export function logout(token) {
|
||||||
window.localStorage.removeItem("authtoken")
|
window.localStorage.removeItem("authtoken")
|
||||||
query("/logout", {token}).then(() => {
|
query("/logout", {token}).then(() => {
|
||||||
toast.warn("You have been logged out.")
|
toast.warn("You have been logged out.")
|
||||||
})
|
})
|
||||||
return defaultAuth()
|
return defaultAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function query(url, data) {
|
export async function query(url, data) {
|
||||||
console.debug("FETCH: " + SERVER_URL + url + " - " + JSON.stringify(data))
|
console.debug("FETCH: " + SERVER_URL + url + " - " + JSON.stringify(data))
|
||||||
let response = await fetch(SERVER_URL + url, {
|
let response = await fetch(SERVER_URL + url, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {"Content-Type": "application/json"},
|
headers: {"Content-Type": "application/json"},
|
||||||
body: JSON.stringify(data)
|
body: JSON.stringify(data)
|
||||||
})
|
})
|
||||||
|
const json = await response.json()
|
||||||
return {status: response.status, json: await response.json()}
|
console.debug(json)
|
||||||
|
return {status: response.status, json}
|
||||||
}
|
}
|
@ -0,0 +1,243 @@
|
|||||||
|
import React, {useCallback, useContext, useEffect, useReducer, useState} from 'react';
|
||||||
|
import Navbar from "./Navbar";
|
||||||
|
|
||||||
|
import {AuthContext} from "../App";
|
||||||
|
import Griddle, {ColumnDefinition, plugins, RowDefinition} from "griddle-react";
|
||||||
|
import {query} from "../auth";
|
||||||
|
import "flatpickr/dist/themes/material_blue.css";
|
||||||
|
import "../styles/tables.css"
|
||||||
|
import Flatpickr from "react-flatpickr";
|
||||||
|
import {Input} from "../custom";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
import {AiFillDelete, AiFillEdit} from "react-icons/ai";
|
||||||
|
import Modal from "react-modal"
|
||||||
|
import FileManualForm from "./FileManualForm";
|
||||||
|
|
||||||
|
|
||||||
|
const background = require('../assets/wave.png')
|
||||||
|
|
||||||
|
const defaultData = () => [{
|
||||||
|
ID: "No Data Found",
|
||||||
|
Date: "",
|
||||||
|
Precipitation: "",
|
||||||
|
Latitude: "",
|
||||||
|
Longitude: "",
|
||||||
|
Remarks: "",
|
||||||
|
Edit: "",
|
||||||
|
}]
|
||||||
|
|
||||||
|
function Admin({history, ...props}) {
|
||||||
|
const {authState} = useContext(AuthContext)
|
||||||
|
const [data, setData] = useState(defaultData())
|
||||||
|
const [items, setItems] = useState(10)
|
||||||
|
const [status, setStatus] = useState("")
|
||||||
|
const initialDates = () => {
|
||||||
|
const now = new Date()
|
||||||
|
const lastYear = new Date();
|
||||||
|
lastYear.setFullYear(lastYear.getFullYear() - 1)
|
||||||
|
return [lastYear, now]
|
||||||
|
}
|
||||||
|
const [dateRange, setDateRange] = useState([]);
|
||||||
|
const [editModal, setEditModal] = useState({open: false, data: {}})
|
||||||
|
const [updateData, dispatchEdit] = useReducer((state, action) => action, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(props.location.search)
|
||||||
|
let range = initialDates()
|
||||||
|
if (params.get("after")) range[0] = new Date(params.get("after"))
|
||||||
|
if (params.get("before")) range[1] = new Date(params.get("before"))
|
||||||
|
setDateRange(range)
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(authState)
|
||||||
|
if (!authState.loggedIn) history.push("/login?return=admin")
|
||||||
|
if (!authState.admin) history.push("/")
|
||||||
|
}, [authState, history])
|
||||||
|
|
||||||
|
const deleteRow = async (data) => {
|
||||||
|
const {status, json} = await query("/data/delete", data).catch(e => {
|
||||||
|
console.debug(e)
|
||||||
|
return {status: 500, json: {reason: "The upload server is offline!"}}
|
||||||
|
})
|
||||||
|
console.debug("Delete Row Response", json)
|
||||||
|
return {status, json}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initiateData = useCallback(async () => {
|
||||||
|
if (dateRange.length !== 2 || !authState.username) return
|
||||||
|
let after = dateRange[0]
|
||||||
|
let before = dateRange[1]
|
||||||
|
before.setDate(before.getDate() + 1)
|
||||||
|
after.setDate(after.getDate() - 1)
|
||||||
|
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
now.setDate(now.getDate() + 5)
|
||||||
|
const hundred = new Date(1900, 0, 0)
|
||||||
|
|
||||||
|
if (after > now || after < hundred ||
|
||||||
|
before > now || before < hundred)
|
||||||
|
return toast.error("Invalid Date Range!")
|
||||||
|
|
||||||
|
setStatus("Loading Data...")
|
||||||
|
let {json, status} = await query("/data/query", {
|
||||||
|
token: authState.token,
|
||||||
|
after_date: after,
|
||||||
|
before_date: before,
|
||||||
|
username: authState.username,
|
||||||
|
}).catch(() => ({status: 500, json: {reason: "The authentication server is offline!"}}))
|
||||||
|
setStatus("")
|
||||||
|
if (status !== 200) return toast.error(json.reason)
|
||||||
|
|
||||||
|
let newData = defaultData()
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
return date.getFullYear() + "-" + (date.getMonth() + 1).toString().padStart(2, "0") + "-" + date.getDate().toString().padStart(2, "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
json.entries.forEach((entry, index) => {
|
||||||
|
const date = new Date(entry.date);
|
||||||
|
|
||||||
|
const group = json.groups.find(gr => gr.id === entry.group_id)
|
||||||
|
newData[index] = {
|
||||||
|
ID: entry.id,
|
||||||
|
Date: formatDate(date),
|
||||||
|
Precipitation: entry.precipitation,
|
||||||
|
Latitude: group.latitude,
|
||||||
|
Longitude: group.longitude,
|
||||||
|
Verified: group.validated ? "Yes" : "No",
|
||||||
|
Remarks: entry.remarks,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setData(newData)
|
||||||
|
|
||||||
|
before.setDate(before.getDate() - 1)
|
||||||
|
after.setDate(after.getDate() + 1)
|
||||||
|
window.history.pushState("Search", "Rain Track", "/mydata?after=" + formatDate(after) + "&before=" + formatDate(before))
|
||||||
|
}, [setData, authState.token, dateRange, authState.username])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
initiateData()
|
||||||
|
})();
|
||||||
|
}, [setData, initiateData, dateRange, authState.username])
|
||||||
|
const styleConfig = {
|
||||||
|
classNames: {
|
||||||
|
Filter: "input h-8",
|
||||||
|
Table: "table table-bordered table-hover table-striped lg:overflow-x-auto overflow-y-scroll",
|
||||||
|
PreviousButton: "border border-gray-300 px-2 py-1 w-20 mr-2 rounded",
|
||||||
|
NextButton: "border border-gray-300 px-2 py-1 w-20 rounded ml-2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
if (updateData.length !== 0) {
|
||||||
|
const row = data[updateData[1]]
|
||||||
|
if (!row) return
|
||||||
|
switch (updateData[0]) {
|
||||||
|
case 0:
|
||||||
|
setEditModal({open: true, data: row})
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
window.confirm("Are you sure you would like to delete this row?")
|
||||||
|
&& (async () => {
|
||||||
|
let {json, status} = await deleteRow({token: authState.token, id: row.ID})
|
||||||
|
if (status !== 200) return toast.error(json.reason)
|
||||||
|
toast.success("Entry successfully deleted")
|
||||||
|
dispatchEdit([])
|
||||||
|
setData(data.filter(r => r !== row))
|
||||||
|
})()
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [updateData, data, authState])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
|
||||||
|
style={{backgroundImage: `url(${background})`}}>
|
||||||
|
<Navbar history={history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
|
||||||
|
<div className="container text-gray-700 bg-white p-6 mt-12 mb-32 rounded">
|
||||||
|
<div className="flex">
|
||||||
|
<Flatpickr
|
||||||
|
value={dateRange}
|
||||||
|
onChange={date => setDateRange(date)}
|
||||||
|
options={{
|
||||||
|
dateFormat: "m-d-Y",
|
||||||
|
mode: "range",
|
||||||
|
wrap: true,
|
||||||
|
position: "below"
|
||||||
|
}}>
|
||||||
|
<div className="w-72">
|
||||||
|
<Input name="date" type='text' others={{"data-input": true}} className="input h-8"
|
||||||
|
placeholder="Select Date Range">
|
||||||
|
Select Date Range
|
||||||
|
</Input>
|
||||||
|
</div>
|
||||||
|
</Flatpickr>
|
||||||
|
<div className="italic text-sm ml-4 w-full flex items-center">{status}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="bg-gray-400 mb-4"/>
|
||||||
|
<Griddle data={data} plugins={[plugins.LocalPlugin]} styleConfig={styleConfig}
|
||||||
|
pageProperties={{
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: items
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Layout: Layout(setItems)
|
||||||
|
}}>
|
||||||
|
<RowDefinition>
|
||||||
|
<ColumnDefinition id="ID"/>
|
||||||
|
<ColumnDefinition id="Date" title="Date"/>
|
||||||
|
<ColumnDefinition id="Precipitation"/>
|
||||||
|
<ColumnDefinition id="Latitude"/>
|
||||||
|
<ColumnDefinition id="Longitude"/>
|
||||||
|
<ColumnDefinition id="Remarks"/>
|
||||||
|
<ColumnDefinition id="Verified"/>
|
||||||
|
<ColumnDefinition id=" "
|
||||||
|
customComponent={(items) => <EditComponent {...items} dispatch={dispatchEdit}/>}/>
|
||||||
|
</RowDefinition>
|
||||||
|
</Griddle>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
onRequestClose={() => setEditModal({open: false, data: {ID: 0}})}
|
||||||
|
shouldCloseOnOverlayClick
|
||||||
|
isOpen={editModal.open}>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<FileManualForm title={"Edit Data"} id={editModal.data.ID} editType defaultData={editModal.data}/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Admin;
|
||||||
|
|
||||||
|
const Layout = (setItems) => (({Table, Filter, Pagination}) => (
|
||||||
|
<div>
|
||||||
|
<div className="flex w-96 pb-4 h-12 justify-between">
|
||||||
|
<Filter/>
|
||||||
|
<select onChange={e => setItems(Number(e.target.value))}>
|
||||||
|
<option value="10">Show 10 items</option>
|
||||||
|
<option value="20">Show 20 items</option>
|
||||||
|
<option value="50">Show 50 items</option>
|
||||||
|
<option value="100">Show 100 items</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="lg:overflow-x-auto overflow-x-scroll">
|
||||||
|
<Table/>
|
||||||
|
</div>
|
||||||
|
<Pagination/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
const EditComponent = ({griddleKey, dispatch}) => {
|
||||||
|
|
||||||
|
return <div className="flex justify-around">
|
||||||
|
<span onClick={() => dispatch([0, griddleKey])}><AiFillEdit/></span>
|
||||||
|
<span onClick={() => dispatch([1, griddleKey])}><AiFillDelete/></span>
|
||||||
|
</div>
|
||||||
|
}
|
@ -1,98 +1,116 @@
|
|||||||
import React, {useContext, useState} from 'react';
|
import React, {useContext, useEffect, useState} from 'react';
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {AuthContext} from "../App";
|
import {AuthContext} from "../App";
|
||||||
import {useForm} from "react-hook-form";
|
import {useForm} from "react-hook-form";
|
||||||
import {query} from "../auth";
|
import {query} from "../auth";
|
||||||
import {Input} from "../custom";
|
import {Input} from "../custom";
|
||||||
|
|
||||||
function FileManualForm() {
|
function FileManualForm({id, title = "Upload Individual Entry", defaultData = {}, editType: edit = false}) {
|
||||||
const {authState} = useContext(AuthContext)
|
const {authState} = useContext(AuthContext)
|
||||||
const [manualError, setManualError] = useState("")
|
const [manualError, setManualError] = useState("")
|
||||||
const {register, handleSubmit, errors, setError, reset} = useForm();
|
const {register, handleSubmit, errors, setError, reset, setValue} = useForm();
|
||||||
|
|
||||||
const manualUpload = async (data) => {
|
const manualUpload = async (data) => {
|
||||||
const {status, json} = await query("/data/add", data).catch(() => {
|
const {status, json} = await query("/data/add", data).catch(() => {
|
||||||
return {status: 500, json: {reason: "The upload server is offline!"}}
|
return {status: 500, json: {reason: "The upload server is offline!"}}
|
||||||
})
|
})
|
||||||
console.debug("Manual Upload Response:", json)
|
console.debug("Manual Upload Response:", json)
|
||||||
return {status, json}
|
return {status, json}
|
||||||
}
|
}
|
||||||
|
|
||||||
function submitManual(data) {
|
function submitManual(data) {
|
||||||
let req = data
|
let req = data
|
||||||
|
|
||||||
let split = data.date.split(/[/-]/);
|
let split = data.date.split(/[/-]/);
|
||||||
// noinspection JSCheckFunctionSignatures
|
// noinspection JSCheckFunctionSignatures
|
||||||
let date = new Date(split[2], Number(split[0]) - 1, split[1])
|
let date = new Date(split[2], Number(split[0]) - 1, split[1])
|
||||||
req.date = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate()
|
req.date = date.getFullYear() + "-" + (date.getMonth() + 1) + "-" + date.getDate()
|
||||||
if (date > new Date()) return setError("date", "time", "Your date must be in the past.")
|
if (date > new Date()) return setError("date", "time", "Your date must be in the past.")
|
||||||
if (date < new Date(1900, 0, 0)) return setError("date", "time", "Your date must be after 1900.")
|
if (date < new Date(1900, 0, 0)) return setError("date", "time", "Your date must be after 1900.")
|
||||||
if (date.getDay() === 0) return setError("date", "format", "Invalid Date format (MM-DD-YYYY)")
|
if (date.getDate() === 0) return setError("date", "format", "Invalid Date format (MM-DD-YYYY)")
|
||||||
|
|
||||||
if (isNaN(parseFloat(data["precipitation"]))) {
|
if (isNaN(parseFloat(data["precipitation"]))) {
|
||||||
setError("precipitation", "format", "Invalid format. Precipitation must be a number. E.g 0.24")
|
setError("precipitation", "format", "Invalid format. Precipitation must be a number. E.g 0.24")
|
||||||
}
|
}
|
||||||
req.precipitation = parseFloat(data["precipitation"])
|
req.precipitation = parseFloat(data["precipitation"])
|
||||||
req.token = authState.token;
|
req.id = id
|
||||||
|
req.token = authState.token;
|
||||||
(async () => {
|
|
||||||
let {json, status} = await manualUpload(data)
|
(async () => {
|
||||||
if (status !== 200) return setManualError(json.reason)
|
let {json, status} = await manualUpload(data)
|
||||||
reset()
|
if (status !== 200) return setManualError(json.reason)
|
||||||
toast.success("Entry successfully uploaded")
|
reset()
|
||||||
})();
|
if (edit) window.location.reload()
|
||||||
}
|
toast.success("Entry successfully uploaded")
|
||||||
|
})();
|
||||||
|
}
|
||||||
return (
|
|
||||||
<form className="bg-white shadow p-4 overflow-hidden rounded-lg
|
const formClasses = edit
|
||||||
my-6 px-6 w-11/12 lg:mx-2 lg:my-5 lg:px-5 lg:w-5/12" onSubmit={handleSubmit(submitManual)}>
|
? "bg-white shadow-lg p-4 overflow-hidden rounded-lg my-6 px-6 max-w-lg w-128"
|
||||||
|
: "bg-white shadow-lg p-4 overflow-hidden rounded-lg my-6 px-6 w-11/12 lg:mx-2 lg:my-5 lg:px-5 lg:w-5/12"
|
||||||
<div className="text-lg font-medium mb-4">Upload Individual Entry</div>
|
|
||||||
{manualError && <div
|
useEffect(() => {
|
||||||
className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded">
|
if (edit) {
|
||||||
{manualError}
|
const date = new Date(defaultData.Date)
|
||||||
</div>
|
const formattedDate = (date.getMonth() + 1).toString().padStart(2, "0") + "-" + date.getDate().toString().padStart(2, "0") + "-" + date.getFullYear()
|
||||||
}
|
setValue([
|
||||||
|
{date: formattedDate},
|
||||||
<Input name="date" type="text" placeholder="Date (MM-DD-YYYY)" errors={errors}
|
{precipitation: defaultData.Precipitation},
|
||||||
register={register({
|
{latitude: defaultData.Latitude},
|
||||||
required: {value: true, message: "You must provide a date."},
|
{longitude: defaultData.Longitude},
|
||||||
pattern: {
|
{remarks: defaultData.Remarks},
|
||||||
value: /^\d\d?[/-]\d\d?[/-]\d\d\d?\d?$/,
|
])
|
||||||
message: "Invalid Date format (MM-DD-YYYY)"
|
}
|
||||||
},
|
}, [defaultData, edit, setValue])
|
||||||
})}/>
|
|
||||||
|
return (
|
||||||
<Input name="precipitation" placeholder="Precipitation (Inches)" errors={errors}
|
<form className={formClasses} onSubmit={handleSubmit(submitManual)}>
|
||||||
register={register({
|
|
||||||
required: {value: true, message: "You must provide the precipitation."},
|
<div className="text-lg font-medium mb-4">{title}</div>
|
||||||
pattern: {
|
{manualError && <div
|
||||||
value: /^\d*\.?\d*$/,
|
className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded">
|
||||||
message: "Invalid format. Precipitation must be a number. E.g 0.24"
|
{manualError}
|
||||||
}
|
</div>
|
||||||
})}/>
|
}
|
||||||
|
|
||||||
<Input name="latitude" type="text" placeholder="Latitude" errors={errors}
|
<Input name="date" type="text" placeholder="Date (MM-DD-YYYY)" errors={errors}
|
||||||
register={register({
|
register={register({
|
||||||
required: {value: true, message: "Latitude and Longitude are both required."}
|
required: {value: true, message: "You must provide a date."},
|
||||||
})}/>
|
pattern: {
|
||||||
|
value: /^\d\d?[/-]\d\d?[/-]\d\d\d?\d?$/,
|
||||||
<Input name="longitude" type="text" placeholder="Longitude" errors={errors}
|
message: "Invalid Date format (MM-DD-YYYY)"
|
||||||
register={register({
|
},
|
||||||
required: {value: true, message: "Latitude and Longitude are both required."}
|
})}/>
|
||||||
})}/>
|
|
||||||
|
<Input name="precipitation" placeholder="Precipitation (Inches)" errors={errors}
|
||||||
<Input name="remarks" type="text" placeholder="Remarks (Optional)" errors={errors}
|
register={register({
|
||||||
register={register}/>
|
required: {value: true, message: "You must provide the precipitation."},
|
||||||
|
pattern: {
|
||||||
<div className="flex flex-1 justify-center">
|
value: /^\d*\.?\d*$/,
|
||||||
<input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 text-center rounded"
|
message: "Invalid format. Precipitation must be a number. E.g 0.24"
|
||||||
type="submit" value="Submit"/>
|
}
|
||||||
</div>
|
})}/>
|
||||||
|
|
||||||
</form>
|
<Input name="latitude" type="text" placeholder="Latitude" errors={errors}
|
||||||
);
|
register={register({
|
||||||
}
|
required: {value: true, message: "Latitude and Longitude are both required."}
|
||||||
|
})}/>
|
||||||
|
|
||||||
|
<Input name="longitude" type="text" placeholder="Longitude" errors={errors}
|
||||||
|
register={register({
|
||||||
|
required: {value: true, message: "Latitude and Longitude are both required."}
|
||||||
|
})}/>
|
||||||
|
|
||||||
|
<Input name="remarks" type="text" placeholder="Remarks (Optional)" errors={errors}
|
||||||
|
register={register}/>
|
||||||
|
|
||||||
|
<div className="flex flex-1 justify-center">
|
||||||
|
<input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 text-center rounded"
|
||||||
|
type="submit" value="Submit"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default FileManualForm;
|
export default FileManualForm;
|
@ -1,129 +1,129 @@
|
|||||||
import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react';
|
import React, {useCallback, useContext, useEffect, useReducer, useRef, useState} from 'react';
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {AuthContext} from "../App";
|
import {AuthContext} from "../App";
|
||||||
import {SERVER_URL} from "../auth";
|
import {SERVER_URL} from "../auth";
|
||||||
import {Input} from "../custom";
|
import {Input} from "../custom";
|
||||||
|
|
||||||
const LIMIT = 2 * 1024 * 1024; // 2 mb
|
const LIMIT = 2 * 1024 * 1024; // 2 mb
|
||||||
|
|
||||||
const dispatch = (state, action) => {
|
const dispatch = (state, action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case "lat":
|
case "lat":
|
||||||
return {...state, lat: action.value}
|
return {...state, lat: action.value}
|
||||||
case "long":
|
case "long":
|
||||||
return {...state, long: action.value}
|
return {...state, long: action.value}
|
||||||
default:
|
default:
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function FileUploadForm() {
|
function FileUploadForm() {
|
||||||
const {authState} = useContext(AuthContext)
|
const {authState} = useContext(AuthContext)
|
||||||
const [files, setFiles] = useState(null);
|
const [files, setFiles] = useState(null);
|
||||||
const [load, setLoad] = useState(0)
|
const [load, setLoad] = useState(0)
|
||||||
const [status, setStatus] = useState("Upload a file")
|
const [status, setStatus] = useState("Upload a file")
|
||||||
const [latLong, setLatLong] = useReducer(dispatch, {lat: "", long: ""}, undefined)
|
const [latLong, setLatLong] = useReducer(dispatch, {lat: "", long: ""}, undefined)
|
||||||
let form = useRef(null);
|
let form = useRef(null);
|
||||||
|
|
||||||
async function query(url, data) {
|
async function query(url, data) {
|
||||||
console.debug("FETCH: " + SERVER_URL + url + " - " + data)
|
console.debug("FETCH: " + SERVER_URL + url + " - " + data)
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let req = new XMLHttpRequest()
|
let req = new XMLHttpRequest()
|
||||||
req.open('post', SERVER_URL + url)
|
req.open('post', SERVER_URL + url)
|
||||||
req.upload.addEventListener('progress', e => {
|
req.upload.addEventListener('progress', e => {
|
||||||
if (e.loaded === e.total) setStatus("Processing...")
|
if (e.loaded === e.total) setStatus("Processing...")
|
||||||
else {
|
else {
|
||||||
let percent = (e.loaded / e.total * 100)
|
let percent = (e.loaded / e.total * 100)
|
||||||
setLoad(percent)
|
setLoad(percent)
|
||||||
setStatus("Uploading " + percent.toFixed(0) + "%")
|
setStatus("Uploading " + percent.toFixed(0) + "%")
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
req.addEventListener('load', () => {
|
req.addEventListener('load', () => {
|
||||||
resolve({status: req.status, json: JSON.parse(req.response)})
|
resolve({status: req.status, json: JSON.parse(req.response)})
|
||||||
});
|
});
|
||||||
req.addEventListener('error', e => {
|
req.addEventListener('error', e => {
|
||||||
resolve({status: req.status, json: JSON.parse(req.response)})
|
resolve({status: req.status, json: JSON.parse(req.response)})
|
||||||
})
|
})
|
||||||
req.send(data)
|
req.send(data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const upload = useCallback(async () => {
|
const upload = useCallback(async () => {
|
||||||
if (!files || files.length === 0) return
|
if (!files || files.length === 0) return
|
||||||
const target = files[0]
|
const target = files[0]
|
||||||
if (target.size > LIMIT) return toast.error("File size too large! Max 2 mb.")
|
if (target.size > LIMIT) return toast.error("File size too large! Max 2 mb.")
|
||||||
if (!target.name.includes(".xlsx")) return toast.error("Invalid file type!")
|
if (!target.name.includes(".xlsx")) return toast.error("Invalid file type!")
|
||||||
const {lat, long} = latLong;
|
const {lat, long} = latLong;
|
||||||
|
|
||||||
setStatus("Uploading")
|
setStatus("Uploading")
|
||||||
|
|
||||||
let file = new FormData()
|
let file = new FormData()
|
||||||
file.append("file", target)
|
file.append("file", target)
|
||||||
file.append("token", authState.token);
|
file.append("token", authState.token);
|
||||||
file.append("latitude", lat);
|
file.append("latitude", lat);
|
||||||
file.append("longitude", long);
|
file.append("longitude", long);
|
||||||
|
|
||||||
let {status, json} = await query("/data/upload", file)
|
let {status, json} = await query("/data/upload", file)
|
||||||
form.current.reset();
|
form.current.reset();
|
||||||
|
|
||||||
setStatus("Upload a file")
|
setStatus("Upload a file")
|
||||||
setLoad(0)
|
setLoad(0)
|
||||||
|
|
||||||
if (status !== 200) return toast.error(json.reason)
|
if (status !== 200) return toast.error(json.reason)
|
||||||
return toast.success("File successfully uploaded.")
|
return toast.success("File successfully uploaded.")
|
||||||
}, [files, authState.token, form, latLong])
|
}, [files, authState.token, form, latLong])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => upload())();
|
(async () => upload())();
|
||||||
}, [files, authState.token, upload, authState, setLoad, setStatus])
|
}, [files, authState.token, upload, authState, setLoad, setStatus])
|
||||||
|
|
||||||
return <div className="bg-white shadow p-4 overflow-hidden rounded-lg flex flex-col justify-center
|
return <div className="bg-white shadow p-4 overflow-hidden rounded-lg flex flex-col justify-center
|
||||||
my-6 px-6 w-11/12 lg:mx-2 lg:my-5 lg:px-5 lg:w-5/12">
|
my-6 px-6 w-11/12 lg:mx-2 lg:my-5 lg:px-5 lg:w-5/12">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-lg font-medium text-center mb-2">Upload File Entries</div>
|
<div className="text-lg font-medium text-center mb-2">Upload File Entries</div>
|
||||||
<div className="text-xs font-light text-center text-black mb-2">
|
<div className="text-xs font-light text-center text-black mb-2">
|
||||||
Click the button to upload an .xlsx file
|
Click the button to upload an .xlsx file
|
||||||
</div>
|
</div>
|
||||||
<hr className="mb-6 border-gray-300 mx-12"/>
|
<hr className="mb-6 border-gray-300 mx-12"/>
|
||||||
</div>
|
</div>
|
||||||
<form className="mt-2 items-center" ref={form}>
|
<form className="mt-2 items-center" ref={form}>
|
||||||
<p className="text-gray-500 italic text-xs ">Latitude and longitude is required if your data doesn't have it</p>
|
<p className="text-gray-500 italic text-xs ">Latitude and longitude is required if your data doesn't have it</p>
|
||||||
|
|
||||||
<Input name="lat" type="text" placeholder="Latitude"
|
<Input name="lat" type="text" placeholder="Latitude"
|
||||||
setValue={e => setLatLong({type: "lat", value: e})}/>
|
setValue={e => setLatLong({type: "lat", value: e})}/>
|
||||||
<Input name="long" type="text" placeholder="Longitude"
|
<Input name="long" type="text" placeholder="Longitude"
|
||||||
setValue={e => setLatLong({type: "long", value: e})}/>
|
setValue={e => setLatLong({type: "long", value: e})}/>
|
||||||
|
|
||||||
<div className="w-full flex justify-center">
|
<div className="w-full flex justify-center">
|
||||||
|
|
||||||
<label className="px-4 pb-2 bg-white text-blue rounded-lg shadow-lg loading inline-block
|
<label className="px-4 pb-2 bg-white text-blue rounded-lg shadow-lg loading inline-block
|
||||||
relative border border-blue cursor-pointer bg-blue-500 hover:bg-blue-700 text-white">
|
relative border border-blue cursor-pointer bg-blue-500 hover:bg-blue-700 text-white">
|
||||||
<style dangerouslySetInnerHTML={{
|
<style dangerouslySetInnerHTML={{
|
||||||
__html:
|
__html:
|
||||||
'.loading::after {' +
|
'.loading::after {' +
|
||||||
'content: "";' +
|
'content: "";' +
|
||||||
`width: ${load}%;` +
|
`width: ${load}%;` +
|
||||||
'background: rgb(255, 255, 255, 0.4);' +
|
'background: rgb(255, 255, 255, 0.4);' +
|
||||||
'position: absolute;' +
|
'position: absolute;' +
|
||||||
'top: 0;' +
|
'top: 0;' +
|
||||||
'bottom: 0;' +
|
'bottom: 0;' +
|
||||||
'left: 0;' +
|
'left: 0;' +
|
||||||
'transition: width 0.3s;'
|
'transition: width 0.3s;'
|
||||||
}}/>
|
}}/>
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<svg className="w-4 h-4 mt-2 mr-3" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
|
<svg className="w-4 h-4 mt-2 mr-3" fill="currentColor" xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 20 20">
|
viewBox="0 0 20 20">
|
||||||
<path
|
<path
|
||||||
d="M16.88 9.1A4 4 0 0 1 16 17H5a5 5 0 0 1-1-9.9V7a3 3 0 0 1 4.52-2.59A4.98 4.98 0 0 1 17 8c0 .38-.04.74-.12 1.1zM11 11h3l-4-4-4 4h3v3h2v-3z"/>
|
d="M16.88 9.1A4 4 0 0 1 16 17H5a5 5 0 0 1-1-9.9V7a3 3 0 0 1 4.52-2.59A4.98 4.98 0 0 1 17 8c0 .38-.04.74-.12 1.1zM11 11h3l-4-4-4 4h3v3h2v-3z"/>
|
||||||
</svg>
|
</svg>
|
||||||
<span className="mt-2 font-bold">{status}</span>
|
<span className="mt-2 font-bold">{status}</span>
|
||||||
<input type='file' className="hidden" name="upload" accept=".xlsx"
|
<input type='file' className="hidden" name="upload" accept=".xlsx"
|
||||||
onChange={e => setFiles(e.target.files)}/>
|
onChange={e => setFiles(e.target.files)}/>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default FileUploadForm;
|
export default FileUploadForm;
|
@ -1,30 +1,30 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Navbar from "./Navbar";
|
import Navbar from "./Navbar";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
const background = require('../assets/background.png')
|
const background = require('../assets/background.png')
|
||||||
|
|
||||||
function Home(props) {
|
function Home(props) {
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
|
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
|
||||||
style={{backgroundImage: `url(${background})`}}>
|
style={{backgroundImage: `url(${background})`}}>
|
||||||
<Navbar history={props.history}/>
|
<Navbar history={props.history}/>
|
||||||
<div
|
<div
|
||||||
className="flex flex-col lg:max-w-full sm:max-w-xl text-center justify-center
|
className="flex flex-col lg:max-w-full sm:max-w-xl text-center justify-center
|
||||||
lg:px-0 md:mt-32 sm:mt-20 mt-16 px-4">
|
lg:px-0 md:mt-32 sm:mt-20 mt-16 px-4">
|
||||||
|
|
||||||
<h1 className="md:text-4xl text-2xl font-bold">Your Rainfall Data in One Place</h1>
|
<h1 className="md:text-4xl text-2xl font-bold">Your Rainfall Data in One Place</h1>
|
||||||
|
|
||||||
<div className="mt-6 sm:text-xl text-sm font-light">Look through your rainfall data with ease.</div>
|
<div className="mt-6 sm:text-xl text-sm font-light">Look through your rainfall data with ease.</div>
|
||||||
|
|
||||||
<Link to="/upload">
|
<Link to="/upload">
|
||||||
<div className="rounded-full bg-blue-600 inline-block mt-4 sm:py-3 py-3 sm:px-8 px-4">
|
<div className="rounded-full bg-blue-600 inline-block mt-4 sm:py-3 py-3 sm:px-8 px-4">
|
||||||
Start Uploading Data
|
Start Uploading Data
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Home;
|
export default Home;
|
@ -1,83 +1,83 @@
|
|||||||
import React, {useContext, useEffect, useState} from 'react';
|
import React, {useContext, useEffect, useState} from 'react';
|
||||||
import Navbar from "./Navbar";
|
import Navbar from "./Navbar";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {Input} from "../custom";
|
import {Input} from "../custom";
|
||||||
import {AuthContext} from "../App";
|
import {AuthContext} from "../App";
|
||||||
import {login} from "../auth";
|
import {login} from "../auth";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {useForm} from "react-hook-form";
|
import {useForm} from "react-hook-form";
|
||||||
|
|
||||||
const background = require('../assets/background.png')
|
const background = require('../assets/background.png')
|
||||||
|
|
||||||
function Login(props) {
|
function Login(props) {
|
||||||
const {register, handleSubmit, errors} = useForm();
|
const {register, handleSubmit, errors} = useForm();
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
const [status, setStatus] = useState("Sign In")
|
const [status, setStatus] = useState("Sign In")
|
||||||
const {authState, authDispatch} = useContext(AuthContext)
|
const {authState, authDispatch} = useContext(AuthContext)
|
||||||
const [canLogin, setLogin] = useState(true)
|
const [canLogin, setLogin] = useState(true)
|
||||||
const [cooldown, setCooldown] = useState(0)
|
const [cooldown, setCooldown] = useState(0)
|
||||||
|
|
||||||
function submit(data) {
|
function submit(data) {
|
||||||
if (!canLogin) return;
|
if (!canLogin) return;
|
||||||
setError("");
|
setError("");
|
||||||
setStatus("Loading..");
|
setStatus("Loading..");
|
||||||
setLogin(false);
|
setLogin(false);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
let {status, json} = await login(data.username.trim(), data.password.trim())
|
let {status, json} = await login(data.username.trim(), data.password.trim())
|
||||||
setCooldown(setTimeout(() => setLogin(true), 1000));
|
setCooldown(setTimeout(() => setLogin(true), 1000));
|
||||||
|
|
||||||
if (status !== 200) {
|
if (status !== 200) {
|
||||||
setStatus("Sign In")
|
setStatus("Sign In")
|
||||||
return setError(json.reason)
|
return setError(json.reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success("You have been logged in.")
|
toast.success("You have been logged in.")
|
||||||
window.localStorage.setItem("authtoken", json.token)
|
window.localStorage.setItem("authtoken", json.token)
|
||||||
authDispatch({response: {token: json.token}})
|
authDispatch({response: {token: json.token}})
|
||||||
})()
|
})()
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let loc = new URLSearchParams(props.location.search).get("return")
|
let loc = new URLSearchParams(props.location.search).get("return")
|
||||||
if (authState.loggedIn) props.history.push("/" + (loc ? loc : ""))
|
if (authState.loggedIn) props.history.push("/" + (loc ? loc : ""))
|
||||||
return () => clearTimeout(cooldown)
|
return () => clearTimeout(cooldown)
|
||||||
}, [props, authState, cooldown])
|
}, [props, authState, cooldown])
|
||||||
|
|
||||||
|
|
||||||
return <div className="w-screen h-screen overflow-x-hidden bg-cover bg-center flex items-center flex-col"
|
return <div className="w-screen h-screen overflow-x-hidden bg-cover bg-center flex items-center flex-col"
|
||||||
style={{backgroundImage: `url(${background})`}}>
|
style={{backgroundImage: `url(${background})`}}>
|
||||||
<Navbar history={props.history}/>
|
<Navbar history={props.history}/>
|
||||||
|
|
||||||
<form className="mt-32 sm:w-96 w-full" onSubmit={handleSubmit(submit)}>
|
<form className="mt-32 sm:w-96 w-full" onSubmit={handleSubmit(submit)}>
|
||||||
<div className="text-2xl w-full font-semibold block ">
|
<div className="text-2xl w-full font-semibold block ">
|
||||||
<div className="rounded-t-md inline bg-gray-100 text-gray-700 px-5 py-2">Log In</div>
|
<div className="rounded-t-md inline bg-gray-100 text-gray-700 px-5 py-2">Log In</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-8 pt-6 pb-8 mb-4 px-12 rounded-md rounded-tl-none bg-gray-100">
|
<div className="px-8 pt-6 pb-8 mb-4 px-12 rounded-md rounded-tl-none bg-gray-100">
|
||||||
{error &&
|
{error &&
|
||||||
<div className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded">
|
<div className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded">
|
||||||
{error}
|
{error}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Input name="username" type="text" placeholder="Username or Email" errors={errors}
|
<Input name="username" type="text" placeholder="Username or Email" errors={errors}
|
||||||
register={register({required: {value: true, message: "You must provide a username."}})}>Username</Input>
|
register={register({required: {value: true, message: "You must provide a username."}})}>Username</Input>
|
||||||
|
|
||||||
<Input name="password" type="password" placeholder="********" errors={errors}
|
<Input name="password" type="password" placeholder="********" errors={errors}
|
||||||
register={register({required: {value: true, message: "You must provide a password."}})}>Password</Input>
|
register={register({required: {value: true, message: "You must provide a password."}})}>Password</Input>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 mr-2 rounded" type="submit"
|
<input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 mr-2 rounded" type="submit"
|
||||||
value={status}/>
|
value={status}/>
|
||||||
<Link to="/reset" className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800">
|
<Link to="/reset" className="inline-block align-baseline font-bold text-sm text-blue-500 hover:text-blue-800">
|
||||||
Reset Password
|
Reset Password
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Login;
|
export default Login;
|
||||||
|
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import React, {useContext, useEffect} from 'react';
|
import React, {useContext, useEffect} from 'react';
|
||||||
import {AuthContext} from "../App";
|
import {AuthContext} from "../App";
|
||||||
import {logout} from "../auth";
|
import {logout} from "../auth";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
|
|
||||||
function Logout(props) {
|
function Logout(props) {
|
||||||
const {authDispatch, authState} = useContext(AuthContext)
|
const {authDispatch, authState} = useContext(AuthContext)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authState.loggedIn) authDispatch({response: logout(authState.token)})
|
if (authState.loggedIn) authDispatch({response: logout(authState.token)})
|
||||||
else toast.warn("You are not logged in!")
|
else toast.warn("You are not logged in!")
|
||||||
props.history.push("/")
|
props.history.push("/")
|
||||||
})
|
})
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>You will soon be redirected. Click <Link to="/">here</Link> if you are not.</div>
|
<div>You will soon be redirected. Click <Link to="/">here</Link> if you are not.</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Logout;
|
export default Logout;
|
||||||
|
@ -1,69 +1,78 @@
|
|||||||
import React, {useContext, useEffect, useState} from 'react';
|
import React, {useContext, useEffect, useState} from 'react';
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
import {AuthContext} from "../App";
|
import {AuthContext} from "../App";
|
||||||
|
|
||||||
const pathsCenter = [
|
const pathsCenter = [
|
||||||
{name: "All Data", to: "/all", extra: ""},
|
{name: "All Data", to: "/all", extra: ""},
|
||||||
{name: "Upload Data", to: "/upload", extra: ""},
|
{name: "Upload Data", to: "/upload", extra: ""},
|
||||||
{name: "Your Data", to: "/", extra: ""},
|
{name: "My Data", to: "/mydata", extra: ""},
|
||||||
]
|
]
|
||||||
|
|
||||||
const pathsGuest = [
|
const pathsGuest = [
|
||||||
{name: "Log In", to: "/login", extra: ""},
|
{name: "Log In", to: "/login", extra: ""},
|
||||||
{name: "Sign Up", to: "/register", extra: "rounded-full border border-white px-4 pb-1 pt-1 inline-block"},
|
{name: "Sign Up", to: "/register", extra: "rounded-full border border-white px-4 pb-1 pt-1 inline-block"},
|
||||||
]
|
]
|
||||||
|
|
||||||
const pathsLoggedIn = [
|
const pathsLoggedIn = [
|
||||||
{name: "Account", to: "/account", extra: ""},
|
{name: "Account", to: "/account", extra: ""},
|
||||||
{name: "Sign Out", to: "/logout", extra: ""},
|
{name: "Logout", to: "/logout", extra: ""},
|
||||||
]
|
]
|
||||||
|
|
||||||
function Navbar({color = "text-white", hover="hover:border-white"}) {
|
const pathsAdmin = [
|
||||||
const [showDropdown, setDropdown] = useState(false)
|
{name: "Admin", to: "/admin", extra: ""},
|
||||||
const [userPaths, setUserPaths] = useState(pathsGuest)
|
{name: "Account", to: "/account", extra: ""},
|
||||||
const {authState} = useContext(AuthContext)
|
{name: "Logout", to: "/logout", extra: ""},
|
||||||
|
]
|
||||||
useEffect(() => {
|
|
||||||
if (authState.loggedIn) {
|
function Navbar({color = "text-white", hover = "hover:border-white"}) {
|
||||||
setUserPaths(() => pathsLoggedIn)
|
const [showDropdown, setDropdown] = useState(false)
|
||||||
} else setUserPaths(() => pathsGuest)
|
const [userPaths, setUserPaths] = useState(pathsGuest)
|
||||||
}, [authState.loggedIn])
|
const {authState} = useContext(AuthContext)
|
||||||
|
|
||||||
return <header className={"lg:flex lg:justify-between lg:items-center px-4 py-3 w-full mt-2 " + color}>
|
useEffect(() => {
|
||||||
<div className="flex flex-1 justify-between items-center mt-3 self-start">
|
console.log(authState)
|
||||||
<Link to={"/"}>
|
if (authState.loggedIn) {
|
||||||
<h1 className="text-xl font-bold">RainTrack</h1>
|
if (authState.admin) setUserPaths(() => pathsAdmin)
|
||||||
</Link>
|
else setUserPaths(() => pathsLoggedIn)
|
||||||
<button onClick={() => setDropdown(!showDropdown)}
|
|
||||||
className="px-2 py-1 lg:hidden block border border-white rounded-md cursor-pointer">
|
} else setUserPaths(() => pathsGuest)
|
||||||
<svg className="fill-current" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
}, [authState])
|
||||||
viewBox="0 0 20 20"><title>Toggle Navigation</title>
|
|
||||||
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/>
|
return <header className={"lg:flex lg:justify-between lg:items-center px-4 py-3 w-full mt-2 " + color}>
|
||||||
</svg>
|
<div className="flex flex-1 justify-between items-center mt-3 self-start">
|
||||||
</button>
|
<Link to={"/"}>
|
||||||
</div>
|
<h1 className="text-xl font-bold">RainTrack</h1>
|
||||||
|
</Link>
|
||||||
<LinksContainer flex="lg:justify-center flex-2"
|
<button onClick={() => setDropdown(!showDropdown)}
|
||||||
dropDown={showDropdown} paths={pathsCenter} color={color} hover={hover}/>
|
className="px-2 py-1 lg:hidden block border border-white rounded-md cursor-pointer">
|
||||||
|
<svg className="fill-current" xmlns="http://www.w3.org/2000/svg" width="20" height="20"
|
||||||
<LinksContainer flex="lg:justify-end flex-1"
|
viewBox="0 0 20 20"><title>Toggle Navigation</title>
|
||||||
dropDown={showDropdown} paths={userPaths} color={color} hover={hover}/>
|
<path d="M0 3h20v2H0V3zm0 6h20v2H0V9zm0 6h20v2H0v-2z"/>
|
||||||
|
</svg>
|
||||||
</header>;
|
</button>
|
||||||
}
|
</div>
|
||||||
|
|
||||||
function LinksContainer(props) {
|
<LinksContainer flex="lg:justify-center flex-2"
|
||||||
return <div
|
dropDown={showDropdown} paths={pathsCenter} color={color} hover={hover}/>
|
||||||
className={`lg:px-2 lg:pt-2 lg:pb-4 lg:flex lg:p-0 ${props.flex} ${props.dropDown ? "block" : "hidden"}`}>
|
|
||||||
{props.paths.map(path => <Link to={path.to} key={path.name}>
|
<LinksContainer flex="lg:justify-end flex-1"
|
||||||
<div
|
dropDown={showDropdown} paths={userPaths} color={color} hover={hover}/>
|
||||||
className={`lg:px-2 py-1 lg:mx-1 mt-1 block text-left font-semibold border-b-2 border-transparent
|
|
||||||
${props.hover} ${path.extra}`}>
|
</header>;
|
||||||
{path.name}
|
}
|
||||||
</div>
|
|
||||||
</Link>)}
|
function LinksContainer(props) {
|
||||||
</div>
|
return <div
|
||||||
}
|
className={`lg:px-2 lg:pt-2 lg:pb-4 lg:flex lg:p-0 ${props.flex} ${props.dropDown ? "block" : "hidden"}`}>
|
||||||
|
{props.paths.map(path => <Link to={path.to} key={path.name}>
|
||||||
export default Navbar;
|
<div
|
||||||
|
className={`lg:px-2 py-1 lg:mx-1 mt-1 block text-left font-semibold border-b-2 border-transparent
|
||||||
|
${props.hover} ${path.extra}`}>
|
||||||
|
{path.name}
|
||||||
|
</div>
|
||||||
|
</Link>)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Navbar;
|
||||||
|
|
||||||
|
@ -1,28 +1,28 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import Navbar from "./Navbar";
|
import Navbar from "./Navbar";
|
||||||
import {Link} from "react-router-dom";
|
import {Link} from "react-router-dom";
|
||||||
|
|
||||||
const background = require('../assets/background.png')
|
const background = require('../assets/background.png')
|
||||||
|
|
||||||
function NotFound(props) {
|
function NotFound(props) {
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
|
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
|
||||||
style={{backgroundImage: `url(${background})`}}>
|
style={{backgroundImage: `url(${background})`}}>
|
||||||
<Navbar history={props.history}/>
|
<Navbar history={props.history}/>
|
||||||
<div className="flex flex-col lg:max-w-full sm:max-w-xl text-center justify-center
|
<div className="flex flex-col lg:max-w-full sm:max-w-xl text-center justify-center
|
||||||
lg:px-0 md:mt-32 sm:mt-20 mt-16 px-4">
|
lg:px-0 md:mt-32 sm:mt-20 mt-16 px-4">
|
||||||
|
|
||||||
<h1 className="md:text-3xl sm:text-2xl text-xl font-bold">
|
<h1 className="md:text-3xl sm:text-2xl text-xl font-bold">
|
||||||
The page you requested was not found. <br/>
|
The page you requested was not found. <br/>
|
||||||
Please confirm your URL or go back to the home page.
|
Please confirm your URL or go back to the home page.
|
||||||
</h1>
|
</h1>
|
||||||
|
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<div className="rounded-full bg-blue-600 inline-block mt-4 sm:py-3 py-3 sm:px-8 px-4">Go Home</div>
|
<div className="rounded-full bg-blue-600 inline-block mt-4 sm:py-3 py-3 sm:px-8 px-4">Go Home</div>
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default NotFound;
|
export default NotFound;
|
@ -1,86 +1,86 @@
|
|||||||
import React, {useContext, useEffect, useState} from 'react';
|
import React, {useContext, useEffect, useState} from 'react';
|
||||||
import Navbar from "./Navbar";
|
import Navbar from "./Navbar";
|
||||||
import {Input} from "../custom";
|
import {Input} from "../custom";
|
||||||
import {AuthContext} from "../App";
|
import {AuthContext} from "../App";
|
||||||
import {registration} from "../auth";
|
import {registration} from "../auth";
|
||||||
import {toast} from "react-toastify";
|
import {toast} from "react-toastify";
|
||||||
import {useForm} from "react-hook-form";
|
import {useForm} from "react-hook-form";
|
||||||
|
|
||||||
const background = require('../assets/background.png')
|
const background = require('../assets/background.png')
|
||||||
|
|
||||||
function Register(props) {
|
function Register(props) {
|
||||||
const {authState, authDispatch} = useContext(AuthContext)
|
const {authState, authDispatch} = useContext(AuthContext)
|
||||||
const {register, handleSubmit, errors} = useForm();
|
const {register, handleSubmit, errors} = useForm();
|
||||||
const [error, setError] = useState("")
|
const [error, setError] = useState("")
|
||||||
const [status, setStatus] = useState("Register")
|
const [status, setStatus] = useState("Register")
|
||||||
const [canRegister, setRegister] = useState(true)
|
const [canRegister, setRegister] = useState(true)
|
||||||
|
|
||||||
function submit(data) {
|
function submit(data) {
|
||||||
if (!canRegister) return;
|
if (!canRegister) return;
|
||||||
setError("")
|
setError("")
|
||||||
setStatus("Loading..");
|
setStatus("Loading..");
|
||||||
setRegister(false);
|
setRegister(false);
|
||||||
|
|
||||||
(async () => {
|
(async () => {
|
||||||
let {json, status} = await registration(data.user.trim(), data.email.trim(), data.pass.trim())
|
let {json, status} = await registration(data.user.trim(), data.email.trim(), data.pass.trim())
|
||||||
setTimeout(() => setRegister(true), 1000);
|
setTimeout(() => setRegister(true), 1000);
|
||||||
if (status !== 200) {
|
if (status !== 200) {
|
||||||
setStatus("Register");
|
setStatus("Register");
|
||||||
return setError(json.reason)
|
return setError(json.reason)
|
||||||
}
|
}
|
||||||
|
|
||||||
toast.success("Your account has been registered.")
|
toast.success("Your account has been registered.")
|
||||||
window.localStorage.setItem("authtoken", json.token)
|
window.localStorage.setItem("authtoken", json.token)
|
||||||
authDispatch({response: {token: json.token, loggedIn: true}})
|
authDispatch({response: {token: json.token, loggedIn: true}})
|
||||||
})()
|
})()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (authState.loggedIn) props.history.push("/")
|
if (authState.loggedIn) props.history.push("/")
|
||||||
}, [authState.loggedIn, props.history])
|
}, [authState.loggedIn, props.history])
|
||||||
|
|
||||||
return <div className="w-screen h-screen overflow-x-hidden bg-cover bg-center flex items-center flex-col"
|
return <div className="w-screen h-screen overflow-x-hidden bg-cover bg-center flex items-center flex-col"
|
||||||
style={{backgroundImage: `url(${background})`}}>
|
style={{backgroundImage: `url(${background})`}}>
|
||||||
<Navbar history={props.history}/>
|
<Navbar history={props.history}/>
|
||||||
|
|
||||||
<form className="mt-24 sm:w-96 w-full" onSubmit={handleSubmit(submit)}>
|
<form className="mt-24 sm:w-96 w-full" onSubmit={handleSubmit(submit)}>
|
||||||
<div className="text-2xl w-full font-semibold block ">
|
<div className="text-2xl w-full font-semibold block ">
|
||||||
<div className="rounded-t-md inline bg-gray-100 text-gray-700 px-5 py-2">Create Account</div>
|
<div className="rounded-t-md inline bg-gray-100 text-gray-700 px-5 py-2">Create Account</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="px-8 pt-6 pb-8 -mt-1 mb-4 px-12 rounded-md rounded-tl-none bg-gray-100">
|
<div className="px-8 pt-6 pb-8 -mt-1 mb-4 px-12 rounded-md rounded-tl-none bg-gray-100">
|
||||||
{error &&
|
{error &&
|
||||||
<div
|
<div
|
||||||
className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded">{error}</div>
|
className="px-2 py-2 mb-8 bg-red-300 text-red-900 text-xs font-semibold border border-red-600 rounded">{error}</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
<Input name="user" type="text" placeholder="Username" errors={errors}
|
<Input name="user" type="text" placeholder="Username" errors={errors}
|
||||||
register={register({
|
register={register({
|
||||||
required: {value: true, message: "You must provide a username."},
|
required: {value: true, message: "You must provide a username."},
|
||||||
minLength: {value: 3, message: "Your username must be between 3 and 16 characters."},
|
minLength: {value: 3, message: "Your username must be between 3 and 16 characters."},
|
||||||
maxLength: {value: 16, message: "Your username must be between 3 and 16 characters."},
|
maxLength: {value: 16, message: "Your username must be between 3 and 16 characters."},
|
||||||
})}>Username</Input>
|
})}>Username</Input>
|
||||||
|
|
||||||
<Input name="email" type="text" placeholder="Email" errors={errors}
|
<Input name="email" type="text" placeholder="Email" errors={errors}
|
||||||
register={register({
|
register={register({
|
||||||
required: {value: true, message: "You must provide a password."},
|
required: {value: true, message: "You must provide a password."},
|
||||||
pattern: {value: /\S+@\S+\.\S+/, message: "Invalid Email"}
|
pattern: {value: /\S+@\S+\.\S+/, message: "Invalid Email"}
|
||||||
})}>Email</Input>
|
})}>Email</Input>
|
||||||
|
|
||||||
<Input name="pass" type="password" placeholder="********" errors={errors}
|
<Input name="pass" type="password" placeholder="********" errors={errors}
|
||||||
register={register({
|
register={register({
|
||||||
required: {value: true, message: "You must provide a password."},
|
required: {value: true, message: "You must provide a password."},
|
||||||
minLength: {value: 8, message: "Your password must be at least 8 characters long."}
|
minLength: {value: 8, message: "Your password must be at least 8 characters long."}
|
||||||
})}>Password</Input>
|
})}>Password</Input>
|
||||||
|
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 mr-2 rounded" type="submit"
|
<input className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 mr-2 rounded" type="submit"
|
||||||
value={status}/>
|
value={status}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Register;
|
export default Register;
|
@ -1,32 +1,32 @@
|
|||||||
import React, {useContext, useEffect} from 'react';
|
import React, {useContext, useEffect} from 'react';
|
||||||
import Navbar from "./Navbar";
|
import Navbar from "./Navbar";
|
||||||
import {AuthContext} from "../App";
|
import {AuthContext} from "../App";
|
||||||
import FileUploadForm from "./FileUploadForm";
|
import FileUploadForm from "./FileUploadForm";
|
||||||
import FileManualForm from "./FileManualForm";
|
import FileManualForm from "./FileManualForm";
|
||||||
|
|
||||||
const background = require('../assets/wave.png')
|
const background = require('../assets/wave.png')
|
||||||
|
|
||||||
|
|
||||||
function Upload(props) {
|
function Upload(props) {
|
||||||
const {authState} = useContext(AuthContext)
|
const {authState} = useContext(AuthContext)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authState.loggedIn) props.history.push("/login?return=upload")
|
if (!authState.loggedIn) props.history.push("/login?return=upload")
|
||||||
}, [authState.loggedIn, props.history])
|
}, [authState.loggedIn, props.history])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
|
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
|
||||||
style={{backgroundImage: `url(${background})`}}>
|
style={{backgroundImage: `url(${background})`}}>
|
||||||
<Navbar history={props.history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
|
<Navbar history={props.history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
|
||||||
|
|
||||||
<div className="container text-gray-700">
|
<div className="container text-gray-700">
|
||||||
<div className="flex flex-wrap justify-center -mx-6 md:mx-0 overflow-hidden lg:-mx-12 xl:-mx-5 mt-16">
|
<div className="flex flex-wrap justify-center -mx-6 md:mx-0 overflow-hidden lg:-mx-12 xl:-mx-5 mt-16">
|
||||||
<FileUploadForm/>
|
<FileUploadForm/>
|
||||||
<FileManualForm/>
|
<FileManualForm/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Upload;
|
export default Upload;
|
@ -1,109 +1,207 @@
|
|||||||
import React, {useCallback, useContext, useEffect, useState} from 'react';
|
import React, {useCallback, useContext, useEffect, useState} from 'react';
|
||||||
import Navbar from "./Navbar";
|
import Navbar from "./Navbar";
|
||||||
|
|
||||||
import {AuthContext} from "../App";
|
import {AuthContext} from "../App";
|
||||||
import Griddle, {ColumnDefinition, plugins, RowDefinition} from "griddle-react";
|
import Griddle, {ColumnDefinition, plugins, RowDefinition} from "griddle-react";
|
||||||
import {query} from "../auth";
|
import {query} from "../auth";
|
||||||
|
import "flatpickr/dist/themes/material_blue.css";
|
||||||
const background = require('../assets/wave.png')
|
import "../styles/tables.css"
|
||||||
|
import Flatpickr from "react-flatpickr";
|
||||||
|
import {Input} from "../custom";
|
||||||
function ViewAll(props) {
|
import {max, mean, median, min, mode, std} from "mathjs";
|
||||||
const {authState} = useContext(AuthContext)
|
import {toast} from "react-toastify";
|
||||||
const [data, setData] = useState([{}])
|
|
||||||
const [items, setItems] = useState(10)
|
|
||||||
|
const background = require('../assets/wave.png')
|
||||||
useEffect(() => {
|
const iqr = require('compute-iqr');
|
||||||
if (!authState.loggedIn) props.history.push("/login?return=all")
|
|
||||||
}, [authState.loggedIn, props.history])
|
|
||||||
|
const defaultData = () => [{
|
||||||
const initiateData = useCallback(async () => {
|
ID: "No Data Found",
|
||||||
let {json, status} = await query("/data/query", {token: authState.token, validated: false}).catch(() => {
|
Date: "",
|
||||||
return {status: 500, json: {reason: "The authentication server is offline!"}}
|
Precipitation: "",
|
||||||
})
|
Latitude: "",
|
||||||
if (status !== 200) {
|
Longitude: "",
|
||||||
return
|
Remarks: "",
|
||||||
}
|
}]
|
||||||
|
|
||||||
let data = []
|
function ViewAll({history, ...props}) {
|
||||||
json.entries.forEach((entry, index) => {
|
const {authState} = useContext(AuthContext)
|
||||||
const date = new Date(entry.date);
|
const [data, setData] = useState(defaultData())
|
||||||
const formattedDate = date.getFullYear() + "-" + (date.getMonth() + 1).toString().padStart(2, "0") + "-" + date.getDate().toString().padStart(2, "0")
|
const [items, setItems] = useState(10)
|
||||||
const group = json.groups.find(gr => gr.id === entry.group_id)
|
const [status, setStatus] = useState("")
|
||||||
data[index] = {
|
|
||||||
ID: entry.id,
|
const initialDates = () => {
|
||||||
Date: formattedDate,
|
const now = new Date()
|
||||||
Precipitation: entry.precipitation,
|
const lastYear = new Date();
|
||||||
Latitude: group.latitude,
|
lastYear.setFullYear(lastYear.getFullYear() - 1)
|
||||||
Longitude: group.longitude,
|
return [lastYear, now]
|
||||||
Remarks: entry.remarks,
|
}
|
||||||
}
|
const [dateRange, setDateRange] = useState([]);
|
||||||
})
|
|
||||||
setData(data)
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(props.location.search)
|
||||||
}, [setData, authState.token])
|
let range = initialDates()
|
||||||
|
if (params.get("after")) range[0] = new Date(params.get("after"))
|
||||||
useEffect(() => {
|
if (params.get("before")) range[1] = new Date(params.get("before"))
|
||||||
(async () => {
|
setDateRange(range)
|
||||||
initiateData()
|
}, [props.location])
|
||||||
})();
|
|
||||||
}, [setData, initiateData])
|
|
||||||
|
useEffect(() => {
|
||||||
const styleConfig = {
|
if (!authState.loggedIn) history.push("/login?return=all")
|
||||||
classNames: {
|
}, [authState, history])
|
||||||
Filter: "shadow appearance-none border rounded mb-1 py-2 px-3 mb-4 text-gray-700 leading-tight h-8",
|
|
||||||
Table: "table table-bordered table-hover table-striped",
|
const initiateData = useCallback(async () => {
|
||||||
PreviousButton: "border border-gray-300 px-2 py-1 w-20 mr-2 rounded",
|
if (dateRange.length !== 2) return
|
||||||
NextButton: "border border-gray-300 px-2 py-1 w-20 rounded ml-2"
|
let after = dateRange[0]
|
||||||
}
|
let before = dateRange[1]
|
||||||
}
|
before.setDate(before.getDate() + 1)
|
||||||
|
after.setDate(after.getDate() - 1)
|
||||||
return (
|
|
||||||
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
|
const now = new Date();
|
||||||
style={{backgroundImage: `url(${background})`}}>
|
now.setDate(now.getDate() + 5)
|
||||||
<Navbar history={props.history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
|
const hundred = new Date(1900, 0, 0)
|
||||||
|
|
||||||
<div className="container text-gray-700 bg-white p-6 mt-12 mb-32 rounded">
|
if (after > now || after < hundred ||
|
||||||
<Griddle data={data} plugins={[plugins.LocalPlugin]} styleConfig={styleConfig}
|
before > now || before < hundred)
|
||||||
pageProperties={{
|
return toast.error("Invalid Date Range!")
|
||||||
currentPage: 1,
|
|
||||||
pageSize: items
|
setStatus("Loading Data...")
|
||||||
}}
|
let {json, status} = await query("/data/query", {
|
||||||
components={{
|
token: authState.token,
|
||||||
Layout: Layout(setItems)
|
validated: true,
|
||||||
}}>
|
after_date: after,
|
||||||
<RowDefinition>
|
before_date: before,
|
||||||
<ColumnDefinition id="ID"/>
|
}).catch(() => ({status: 500, json: {reason: "The authentication server is offline!"}}))
|
||||||
<ColumnDefinition id="Date"/>
|
setStatus("")
|
||||||
<ColumnDefinition id="Precipitation"/>
|
|
||||||
<ColumnDefinition id="Latitude"/>
|
if (status !== 200) return toast.error(json.reason)
|
||||||
<ColumnDefinition id="Longitude"/>
|
|
||||||
<ColumnDefinition id="Remarks"/>
|
let newData = defaultData()
|
||||||
</RowDefinition>
|
const formatDate = (date) => {
|
||||||
</Griddle>
|
return date.getFullYear() + "-" + (date.getMonth() + 1).toString().padStart(2, "0") + "-" + date.getDate().toString().padStart(2, "0")
|
||||||
</div>
|
}
|
||||||
|
|
||||||
<div className="container text-gray-700 bg-white p-6 mb-32 rounded">
|
json.entries.forEach((entry, index) => {
|
||||||
<div className="text-2xl font-semibold">Statistics</div>
|
const date = new Date(entry.date);
|
||||||
</div>
|
const group = json.groups.find(gr => gr.id === entry.group_id)
|
||||||
</div>
|
newData[index] = {
|
||||||
);
|
ID: entry.id,
|
||||||
}
|
Date: formatDate(date),
|
||||||
|
Precipitation: entry.precipitation,
|
||||||
export default ViewAll;
|
Latitude: group.latitude,
|
||||||
|
Longitude: group.longitude,
|
||||||
const Layout = (setItems) => (({Table, Filter, Pagination}) => (
|
Remarks: entry.remarks,
|
||||||
<div>
|
}
|
||||||
<div className="flex w-96 pb-4 h-12 justify-between">
|
})
|
||||||
<Filter/>
|
setData(newData)
|
||||||
<select onChange={e => setItems(Number(e.target.value))}>
|
before.setDate(before.getDate() - 1)
|
||||||
<option value="10">Show 10 items</option>
|
after.setDate(after.getDate() + 1)
|
||||||
<option value="20">Show 20 items</option>
|
window.history.pushState("Search", "Rain Track", "/all?after=" + formatDate(after) + "&before=" + formatDate(before))
|
||||||
<option value="50">Show 50 items</option>
|
},
|
||||||
<option value="100">Show 100 items</option>
|
[setData, authState.token, dateRange])
|
||||||
</select>
|
|
||||||
</div>
|
useEffect(() => {
|
||||||
<Table/>
|
(async () => {
|
||||||
<Pagination/>
|
initiateData()
|
||||||
</div>
|
})();
|
||||||
|
}, [setData, initiateData, dateRange, authState.username])
|
||||||
|
const styleConfig = {
|
||||||
|
classNames: {
|
||||||
|
Filter: "input h-8",
|
||||||
|
Table: "table table-bordered table-hover table-striped lg:overflow-x-auto overflow-y-scroll",
|
||||||
|
PreviousButton: "border border-gray-300 px-2 py-1 w-20 mr-2 rounded",
|
||||||
|
NextButton: "border border-gray-300 px-2 py-1 w-20 rounded ml-2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
|
||||||
|
style={{backgroundImage: `url(${background})`}}>
|
||||||
|
<Navbar history={history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
|
||||||
|
|
||||||
|
<div className="container text-gray-700 bg-white p-6 mt-12 mb-32 rounded">
|
||||||
|
<div className="flex">
|
||||||
|
|
||||||
|
<Flatpickr
|
||||||
|
value={dateRange}
|
||||||
|
onChange={date => setDateRange(date)}
|
||||||
|
options={{
|
||||||
|
dateFormat: "m-d-Y",
|
||||||
|
mode: "range",
|
||||||
|
wrap: true,
|
||||||
|
position: "below"
|
||||||
|
}}>
|
||||||
|
<div className="w-72">
|
||||||
|
<Input name="date" type='text' others={{"data-input": true}} className="input h-8"
|
||||||
|
placeholder="Select Date Range">
|
||||||
|
Select Date Range
|
||||||
|
</Input>
|
||||||
|
</div>
|
||||||
|
</Flatpickr>
|
||||||
|
<div className="italic text-sm ml-4 w-full flex items-center">{status}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="bg-gray-400 mb-4"/>
|
||||||
|
<Griddle data={data} plugins={[plugins.LocalPlugin]} styleConfig={styleConfig}
|
||||||
|
pageProperties={{
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: items
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Layout: Layout(setItems)
|
||||||
|
}}>
|
||||||
|
<RowDefinition>
|
||||||
|
<ColumnDefinition id="ID"/>
|
||||||
|
<ColumnDefinition id="Date"/>
|
||||||
|
<ColumnDefinition id="Precipitation"/>
|
||||||
|
<ColumnDefinition id="Latitude"/>
|
||||||
|
<ColumnDefinition id="Longitude"/>
|
||||||
|
<ColumnDefinition id="Remarks"/>
|
||||||
|
</RowDefinition>
|
||||||
|
</Griddle>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="container text-gray-700 bg-white p-6 mb-32 rounded">
|
||||||
|
<div className="text-2xl font-semibold mb-4">Statistics</div>
|
||||||
|
<div className="grid lg:grid-cols-3 md:grid-cols-2 grid-cols-1 row-gap-4 col-gap-12">
|
||||||
|
<Stat data={data} calculate={(items) => iqr(items)}>Interquartile Range</Stat>
|
||||||
|
<Stat data={data} calculate={(items) => std(items)}>Standard Deviation</Stat>
|
||||||
|
<Stat data={data} calculate={(items) => mean(items)}>Mean (Average)</Stat>
|
||||||
|
<Stat data={data} calculate={(items) => median(items)}>Median</Stat>
|
||||||
|
<Stat data={data} calculate={(items) => mode(items)}>Mode</Stat>
|
||||||
|
<Stat data={data} calculate={(items) => max(items) - min(items)}>Range</Stat>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const Stat = ({children, data, calculate}) => (
|
||||||
|
<div className="rounded border-t-4 border-blue-400 shadow p-2 pt-1">
|
||||||
|
<div className="lg:text-xl font-semibold text-gray-700">{children}</div>
|
||||||
|
<div className="text-md">{data.length !== 0 && calculate(data.map(set => set.Precipitation)).toString()}</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
export default ViewAll;
|
||||||
|
|
||||||
|
const Layout = (setItems) => (({Table, Filter, Pagination}) => (
|
||||||
|
<div>
|
||||||
|
<div className="flex w-96 pb-4 h-12 justify-between">
|
||||||
|
|
||||||
|
<Filter/>
|
||||||
|
<select onChange={e => setItems(Number(e.target.value))}>
|
||||||
|
<option value="10">Show 10 items</option>
|
||||||
|
<option value="20">Show 20 items</option>
|
||||||
|
<option value="50">Show 50 items</option>
|
||||||
|
<option value="100">Show 100 items</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="lg:overflow-x-auto overflow-x-scroll">
|
||||||
|
<Table/>
|
||||||
|
</div>
|
||||||
|
<Pagination/>
|
||||||
|
</div>
|
||||||
))
|
))
|
@ -0,0 +1,241 @@
|
|||||||
|
import React, {useCallback, useContext, useEffect, useReducer, useState} from 'react';
|
||||||
|
import Navbar from "./Navbar";
|
||||||
|
|
||||||
|
import {AuthContext} from "../App";
|
||||||
|
import Griddle, {ColumnDefinition, plugins, RowDefinition} from "griddle-react";
|
||||||
|
import {query} from "../auth";
|
||||||
|
import "flatpickr/dist/themes/material_blue.css";
|
||||||
|
import "../styles/tables.css"
|
||||||
|
import Flatpickr from "react-flatpickr";
|
||||||
|
import {Input} from "../custom";
|
||||||
|
import {toast} from "react-toastify";
|
||||||
|
import {AiFillDelete, AiFillEdit} from "react-icons/ai";
|
||||||
|
import Modal from "react-modal"
|
||||||
|
import FileManualForm from "./FileManualForm";
|
||||||
|
|
||||||
|
|
||||||
|
const background = require('../assets/wave.png')
|
||||||
|
|
||||||
|
const defaultData = () => [{
|
||||||
|
ID: "No Data Found",
|
||||||
|
Date: "",
|
||||||
|
Precipitation: "",
|
||||||
|
Latitude: "",
|
||||||
|
Longitude: "",
|
||||||
|
Remarks: "",
|
||||||
|
Edit: "",
|
||||||
|
}]
|
||||||
|
|
||||||
|
function MyData({history, ...props}) {
|
||||||
|
const {authState} = useContext(AuthContext)
|
||||||
|
const [data, setData] = useState(defaultData())
|
||||||
|
const [items, setItems] = useState(10)
|
||||||
|
const [status, setStatus] = useState("")
|
||||||
|
const initialDates = () => {
|
||||||
|
const now = new Date()
|
||||||
|
const lastYear = new Date();
|
||||||
|
lastYear.setFullYear(lastYear.getFullYear() - 1)
|
||||||
|
return [lastYear, now]
|
||||||
|
}
|
||||||
|
const [dateRange, setDateRange] = useState([]);
|
||||||
|
const [editModal, setEditModal] = useState({open: false, data: {}})
|
||||||
|
const [updateData, dispatchEdit] = useReducer((state, action) => action, [])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const params = new URLSearchParams(props.location.search)
|
||||||
|
let range = initialDates()
|
||||||
|
if (params.get("after")) range[0] = new Date(params.get("after"))
|
||||||
|
if (params.get("before")) range[1] = new Date(params.get("before"))
|
||||||
|
setDateRange(range)
|
||||||
|
}, [props.location])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!authState.loggedIn) history.push("/login?return=mydata")
|
||||||
|
}, [authState, history])
|
||||||
|
|
||||||
|
const deleteRow = async (data) => {
|
||||||
|
const {status, json} = await query("/data/delete", data).catch(e => {
|
||||||
|
console.debug(e)
|
||||||
|
return {status: 500, json: {reason: "The upload server is offline!"}}
|
||||||
|
})
|
||||||
|
console.debug("Delete Row Response", json)
|
||||||
|
return {status, json}
|
||||||
|
}
|
||||||
|
|
||||||
|
const initiateData = useCallback(async () => {
|
||||||
|
if (dateRange.length !== 2 || !authState.username) return
|
||||||
|
let after = dateRange[0]
|
||||||
|
let before = dateRange[1]
|
||||||
|
before.setDate(before.getDate() + 1)
|
||||||
|
after.setDate(after.getDate() - 1)
|
||||||
|
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
now.setDate(now.getDate() + 5)
|
||||||
|
const hundred = new Date(1900, 0, 0)
|
||||||
|
|
||||||
|
if (after > now || after < hundred ||
|
||||||
|
before > now || before < hundred)
|
||||||
|
return toast.error("Invalid Date Range!")
|
||||||
|
|
||||||
|
setStatus("Loading Data...")
|
||||||
|
let {json, status} = await query("/data/query", {
|
||||||
|
token: authState.token,
|
||||||
|
after_date: after,
|
||||||
|
before_date: before,
|
||||||
|
username: authState.username,
|
||||||
|
}).catch(() => ({status: 500, json: {reason: "The authentication server is offline!"}}))
|
||||||
|
setStatus("")
|
||||||
|
if (status !== 200) return toast.error(json.reason)
|
||||||
|
|
||||||
|
let newData = defaultData()
|
||||||
|
|
||||||
|
const formatDate = (date) => {
|
||||||
|
return date.getFullYear() + "-" + (date.getMonth() + 1).toString().padStart(2, "0") + "-" + date.getDate().toString().padStart(2, "0")
|
||||||
|
}
|
||||||
|
|
||||||
|
json.entries.forEach((entry, index) => {
|
||||||
|
const date = new Date(entry.date);
|
||||||
|
|
||||||
|
const group = json.groups.find(gr => gr.id === entry.group_id)
|
||||||
|
newData[index] = {
|
||||||
|
ID: entry.id,
|
||||||
|
Date: formatDate(date),
|
||||||
|
Precipitation: entry.precipitation,
|
||||||
|
Latitude: group.latitude,
|
||||||
|
Longitude: group.longitude,
|
||||||
|
Verified: group.validated ? "Yes" : "No",
|
||||||
|
Remarks: entry.remarks,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
setData(newData)
|
||||||
|
|
||||||
|
before.setDate(before.getDate() - 1)
|
||||||
|
after.setDate(after.getDate() + 1)
|
||||||
|
window.history.pushState("Search", "Rain Track", "/mydata?after=" + formatDate(after) + "&before=" + formatDate(before))
|
||||||
|
}, [setData, authState.token, dateRange, authState.username])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
(async () => {
|
||||||
|
initiateData()
|
||||||
|
})();
|
||||||
|
}, [setData, initiateData, dateRange, authState.username])
|
||||||
|
const styleConfig = {
|
||||||
|
classNames: {
|
||||||
|
Filter: "input h-8",
|
||||||
|
Table: "table table-bordered table-hover table-striped lg:overflow-x-auto overflow-y-scroll",
|
||||||
|
PreviousButton: "border border-gray-300 px-2 py-1 w-20 mr-2 rounded",
|
||||||
|
NextButton: "border border-gray-300 px-2 py-1 w-20 rounded ml-2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
useEffect(() => {
|
||||||
|
if (updateData.length !== 0) {
|
||||||
|
const row = data[updateData[1]]
|
||||||
|
if (!row) return
|
||||||
|
switch (updateData[0]) {
|
||||||
|
case 0:
|
||||||
|
setEditModal({open: true, data: row})
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
window.confirm("Are you sure you would like to delete this row?")
|
||||||
|
&& (async () => {
|
||||||
|
let {json, status} = await deleteRow({token: authState.token, id: row.ID})
|
||||||
|
if (status !== 200) return toast.error(json.reason)
|
||||||
|
toast.success("Entry successfully deleted")
|
||||||
|
dispatchEdit([])
|
||||||
|
setData(data.filter(r => r !== row))
|
||||||
|
})()
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [updateData, data, authState])
|
||||||
|
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen overflow-x-hidden bg-cover bg-center text-white flex items-center flex-col"
|
||||||
|
style={{backgroundImage: `url(${background})`}}>
|
||||||
|
<Navbar history={history} color={"text-blue-600"} hover={"hover:border-blue-600"}/>
|
||||||
|
<div className="container text-gray-700 bg-white p-6 mt-12 mb-32 rounded">
|
||||||
|
<div className="flex">
|
||||||
|
<Flatpickr
|
||||||
|
value={dateRange}
|
||||||
|
onChange={date => setDateRange(date)}
|
||||||
|
options={{
|
||||||
|
dateFormat: "m-d-Y",
|
||||||
|
mode: "range",
|
||||||
|
wrap: true,
|
||||||
|
position: "below"
|
||||||
|
}}>
|
||||||
|
<div className="w-72">
|
||||||
|
<Input name="date" type='text' others={{"data-input": true}} className="input h-8"
|
||||||
|
placeholder="Select Date Range">
|
||||||
|
Select Date Range
|
||||||
|
</Input>
|
||||||
|
</div>
|
||||||
|
</Flatpickr>
|
||||||
|
<div className="italic text-sm ml-4 w-full flex items-center">{status}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr className="bg-gray-400 mb-4"/>
|
||||||
|
<Griddle data={data} plugins={[plugins.LocalPlugin]} styleConfig={styleConfig}
|
||||||
|
pageProperties={{
|
||||||
|
currentPage: 1,
|
||||||
|
pageSize: items
|
||||||
|
}}
|
||||||
|
components={{
|
||||||
|
Layout: Layout(setItems)
|
||||||
|
}}>
|
||||||
|
<RowDefinition>
|
||||||
|
<ColumnDefinition id="ID"/>
|
||||||
|
<ColumnDefinition id="Date" title="Date"/>
|
||||||
|
<ColumnDefinition id="Precipitation"/>
|
||||||
|
<ColumnDefinition id="Latitude"/>
|
||||||
|
<ColumnDefinition id="Longitude"/>
|
||||||
|
<ColumnDefinition id="Remarks"/>
|
||||||
|
<ColumnDefinition id="Verified"/>
|
||||||
|
<ColumnDefinition id=" "
|
||||||
|
customComponent={(items) => <EditComponent {...items} dispatch={dispatchEdit}/>}/>
|
||||||
|
</RowDefinition>
|
||||||
|
</Griddle>
|
||||||
|
</div>
|
||||||
|
<Modal
|
||||||
|
onRequestClose={() => setEditModal({open: false, data: {ID: 0}})}
|
||||||
|
shouldCloseOnOverlayClick
|
||||||
|
isOpen={editModal.open}>
|
||||||
|
<div className="flex justify-center">
|
||||||
|
<FileManualForm title={"Edit Data"} id={editModal.data.ID} editType defaultData={editModal.data}/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MyData;
|
||||||
|
|
||||||
|
const Layout = (setItems) => (({Table, Filter, Pagination}) => (
|
||||||
|
<div>
|
||||||
|
<div className="flex w-96 pb-4 h-12 justify-between">
|
||||||
|
<Filter/>
|
||||||
|
<select onChange={e => setItems(Number(e.target.value))}>
|
||||||
|
<option value="10">Show 10 items</option>
|
||||||
|
<option value="20">Show 20 items</option>
|
||||||
|
<option value="50">Show 50 items</option>
|
||||||
|
<option value="100">Show 100 items</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div className="lg:overflow-x-auto overflow-x-scroll">
|
||||||
|
<Table/>
|
||||||
|
</div>
|
||||||
|
<Pagination/>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
|
||||||
|
const EditComponent = ({griddleKey, dispatch}) => {
|
||||||
|
|
||||||
|
return <div className="flex justify-around">
|
||||||
|
<span onClick={() => dispatch([0, griddleKey])}><AiFillEdit/></span>
|
||||||
|
<span onClick={() => dispatch([1, griddleKey])}><AiFillDelete/></span>
|
||||||
|
</div>
|
||||||
|
}
|
@ -1,14 +1,14 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
export function Input({name, type, errors = {}, mainClasses = "mb-2", ...props}) {
|
export function Input({name, type, errors = {}, mainClasses = "mb-2", ...props}) {
|
||||||
const classes = () => "input " + (errors[name] ? "error" : "")
|
const classes = () => "input w-full h-10 " + (errors[name] ? "error" : "")
|
||||||
|
|
||||||
return <div className={mainClasses}>
|
return <div className={mainClasses}>
|
||||||
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor={name}>{props.children}</label>
|
<label className="block text-gray-700 text-sm font-bold mb-2" htmlFor={name}>{props.children}</label>
|
||||||
<input className={classes()} id={name} name={name} ref={props.register}
|
<input className={classes()} id={name} name={name} ref={props.register}
|
||||||
type={type} autoComplete="off" placeholder={props.placeholder}
|
type={type} autoComplete="off" placeholder={props.placeholder} {...props.others}
|
||||||
defaultValue={props.value && props.value} onChange={props.setValue && (e => props.setValue(e.target.value))}
|
defaultValue={props.value && props.value} onChange={props.setValue && (e => props.setValue(e.target.value))}
|
||||||
/>
|
/>
|
||||||
<p className="input-error"> {errors[name]?.message}</p>
|
<p className="input-error"> {errors[name]?.message}</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
@ -0,0 +1 @@
|
|||||||
|
table{border-collapse:collapse}caption{padding-top:0.75rem;padding-bottom:0.75rem;color:#6c757d;text-align:left;caption-side:bottom}th{text-align:inherit}.table{width:100%;max-width:100%;margin-bottom:1rem;background-color:transparent}.table td,.table th{padding:0.75rem;vertical-align:top;border-top:1px solid #dee2e6}.table td{border-top:1px solid #dee2e6}.table thead th{vertical-align:bottom;border-bottom:2px solid #dee2e6}.table tbody+tbody{border-top:2px solid #dee2e6}.table .table{background-color:#fff}.table-sm td,.table-sm th{padding:0.3rem}.table-bordered{border:1px solid #dee2e6}.table-bordered td,.table-bordered th{border:1px solid #dee2e6}.table-bordered thead td,.table-bordered thead th{border-bottom-width:2px}.table-striped tbody tr:nth-of-type(odd){background-color:rgba(0,0,0,.05)}.table-hover tbody tr:hover{background-color:rgba(0,0,0,.075)}.table-primary,.table-primary > td,.table-primary > th{background-color:#b8daff}.table-hover .table-primary:hover{background-color:#9fcdff}.table-hover .table-primary:hover > td,.table-hover .table-primary:hover > th{background-color:#9fcdff}.table-secondary,.table-secondary > td,.table-secondary > th{background-color:#d6d8db}.table-hover .table-secondary:hover{background-color:#c8cbcf}.table-hover .table-secondary:hover > td,.table-hover .table-secondary:hover > th{background-color:#c8cbcf}.table-success,.table-success > td,.table-success > th{background-color:#c3e6cb}.table-hover .table-success:hover{background-color:#b1dfbb}.table-hover .table-success:hover > td,.table-hover .table-success:hover > th{background-color:#b1dfbb}.table-info,.table-info > td,.table-info > th{background-color:#bee5eb}.table-hover .table-info:hover{background-color:#abdde5}.table-hover .table-info:hover > td,.table-hover .table-info:hover > th{background-color:#abdde5}.table-warning,.table-warning > td,.table-warning > th{background-color:#ffeeba}.table-hover .table-warning:hover{background-color:#ffe8a1}.table-hover .table-warning:hover > td,.table-hover .table-warning:hover > th{background-color:#ffe8a1}.table-danger,.table-danger > td,.table-danger > th{background-color:#f5c6cb}.table-hover .table-danger:hover{background-color:#f1b0b7}.table-hover .table-danger:hover > td,.table-hover .table-danger:hover > th{background-color:#f1b0b7}.table-light,.table-light > td,.table-light > th{background-color:#fdfdfe}.table-hover .table-light:hover{background-color:#ececf6}.table-hover .table-light:hover > td,.table-hover .table-light:hover > th{background-color:#ececf6}.table-dark,.table-dark > td,.table-dark > th{background-color:#c6c8ca}.table-hover .table-dark:hover{background-color:#b9bbbe}.table-hover .table-dark:hover > td,.table-hover .table-dark:hover > th{background-color:#b9bbbe}.table-active,.table-active > td,.table-active > th{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover{background-color:rgba(0,0,0,.075)}.table-hover .table-active:hover > td,.table-hover .table-active:hover > th{background-color:rgba(0,0,0,.075)}.table .thead-dark th{color:#fff;background-color:#212529;border-color:#32383e}.table .thead-light th{color:#495057;background-color:#e9ecef;border-color:#dee2e6}.table-dark{color:#fff;background-color:#212529}.table-dark td,.table-dark th,.table-dark thead th{border-color:#32383e}.table-dark.table-bordered{border:0}.table-dark.table-striped tbody tr:nth-of-type(odd){background-color:rgba(255,255,255,.05)}.table-dark.table-hover tbody tr:hover{background-color:rgba(255,255,255,.075)}.table-responsive{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive > .table-bordered{border:0}.table-borderless tbody+tbody,.table-borderless td,.table-borderless th,.table-borderless thead th{border:0}@media (max-width:575.98px){.table-responsive-sm{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm > .table-bordered{border:0}}@media (max-width:767.98px){.table-responsive-md{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm > .table-bordered{border:0}}@media (max-width:991.98px){.table-responsive-lg{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm > .table-bordered{border:0}}@media (max-width:1199.98px){.table-responsive-xl{display:block;width:100%;overflow-x:auto;-webkit-overflow-scrolling:touch;-ms-overflow-style:-ms-autohiding-scrollbar}.table-responsive-sm > .table-bordered{border:0}}
|
@ -1,47 +1,61 @@
|
|||||||
/* purgecss start ignore */
|
/* purgecss start ignore */
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
/* purgecss end ignore */
|
/* purgecss end ignore */
|
||||||
|
|
||||||
@tailwind utilities;
|
@tailwind utilities;
|
||||||
|
|
||||||
/*forms*/
|
/*forms*/
|
||||||
|
|
||||||
.input {
|
body {
|
||||||
@apply shadow appearance-none border rounded w-full mb-1 py-2 px-3 text-gray-700 leading-tight h-10 align-text-top;
|
overflow-x: hidden;
|
||||||
transition: all 0.3s;
|
}
|
||||||
}
|
|
||||||
|
.input {
|
||||||
.input.error {
|
@apply shadow appearance-none border rounded mb-1 py-2 px-3 text-gray-700 leading-tight align-text-top;
|
||||||
@apply border-red-500
|
transition: all 0.3s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.input:focus {
|
.input.error {
|
||||||
@apply text-black outline-none;
|
@apply border-red-500
|
||||||
-webkit-box-shadow: 0px 0px 4px 3px #4f89ed;
|
}
|
||||||
-moz-box-shadow: 0px 0px 4px 3px #4f89ed;
|
|
||||||
box-shadow: 0px 0px 4px 3px #4f89ed;
|
.input:focus {
|
||||||
}
|
@apply text-black outline-none;
|
||||||
|
-webkit-box-shadow: 0px 0px 4px 3px #4f89ed;
|
||||||
.input.error:focus {
|
-moz-box-shadow: 0px 0px 4px 3px #4f89ed;
|
||||||
-webkit-box-shadow: 0px 0px 4px 3px #f56565;
|
box-shadow: 0px 0px 4px 3px #4f89ed;
|
||||||
-moz-box-shadow: 0px 0px 4px 3px #f56565;
|
}
|
||||||
box-shadow: 0px 0px 4px 3px #f56565;
|
|
||||||
}
|
.input.error:focus {
|
||||||
|
-webkit-box-shadow: 0px 0px 4px 3px #f56565;
|
||||||
.input-error {
|
-moz-box-shadow: 0px 0px 4px 3px #f56565;
|
||||||
@apply text-red-500 text-xs italic h-5
|
box-shadow: 0px 0px 4px 3px #f56565;
|
||||||
|
}
|
||||||
}
|
|
||||||
|
.input-error {
|
||||||
.Toastify__toast {
|
@apply text-red-500 text-xs italic h-5
|
||||||
padding-left: 24px !important;
|
|
||||||
@apply text-sm;
|
}
|
||||||
}
|
|
||||||
|
.Toastify__toast {
|
||||||
|
padding-left: 24px !important;
|
||||||
/* table stuff */
|
@apply text-sm;
|
||||||
|
}
|
||||||
.griddle-table {
|
|
||||||
@apply table-auto;
|
|
||||||
|
/* table stuff */
|
||||||
|
|
||||||
|
.griddle-table {
|
||||||
|
@apply table-auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flatpickr-weekday, .flatpickr-day {
|
||||||
|
-webkit-touch-callout: none; /* iOS Safari */
|
||||||
|
-webkit-user-select: none; /* Safari */
|
||||||
|
-khtml-user-select: none; /* Konqueror HTML */
|
||||||
|
-moz-user-select: none; /* Firefox */
|
||||||
|
-ms-user-select: none; /* Internet Explorer/Edge */
|
||||||
|
user-select: none; /* Non-prefixed version, currently
|
||||||
|
supported by Chrome and Opera */
|
||||||
}
|
}
|
Loading…
Reference in New Issue