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