WIP: Working on endpoint for /all in ViewAll.js
							parent
							
								
									b7348afb49
								
							
						
					
					
						commit
						9d6f948c51
					
				@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					<component name="ProjectCodeStyleConfiguration">
 | 
				
			||||||
 | 
					  <state>
 | 
				
			||||||
 | 
					    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
 | 
				
			||||||
 | 
					  </state>
 | 
				
			||||||
 | 
					</component>
 | 
				
			||||||
@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					<?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>
 | 
				
			||||||
@ -0,0 +1,7 @@
 | 
				
			|||||||
 | 
					<component name="ProjectDictionaryState">
 | 
				
			||||||
 | 
					  <dictionary name="dev">
 | 
				
			||||||
 | 
					    <words>
 | 
				
			||||||
 | 
					      <w>navbar</w>
 | 
				
			||||||
 | 
					    </words>
 | 
				
			||||||
 | 
					  </dictionary>
 | 
				
			||||||
 | 
					</component>
 | 
				
			||||||
@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					<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>
 | 
				
			||||||
@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					<?xml version="1.0" encoding="UTF-8"?>
 | 
				
			||||||
 | 
					<project version="4">
 | 
				
			||||||
 | 
					  <component name="VcsDirectoryMappings">
 | 
				
			||||||
 | 
					    <mapping directory="" vcs="Git" />
 | 
				
			||||||
 | 
					  </component>
 | 
				
			||||||
 | 
					</project>
 | 
				
			||||||
											
												
													File diff suppressed because it is too large
													Load Diff
												
											
										
									
								@ -0,0 +1,30 @@
 | 
				
			|||||||
 | 
					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]
 | 
				
			||||||
 | 
									: []
 | 
				
			||||||
 | 
						]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,38 +0,0 @@
 | 
				
			|||||||
.App {
 | 
					 | 
				
			||||||
  text-align: center;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.App-logo {
 | 
					 | 
				
			||||||
  height: 40vmin;
 | 
					 | 
				
			||||||
  pointer-events: none;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@media (prefers-reduced-motion: no-preference) {
 | 
					 | 
				
			||||||
  .App-logo {
 | 
					 | 
				
			||||||
    animation: App-logo-spin infinite 20s linear;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.App-header {
 | 
					 | 
				
			||||||
  background-color: #282c34;
 | 
					 | 
				
			||||||
  min-height: 100vh;
 | 
					 | 
				
			||||||
  display: flex;
 | 
					 | 
				
			||||||
  flex-direction: column;
 | 
					 | 
				
			||||||
  align-items: center;
 | 
					 | 
				
			||||||
  justify-content: center;
 | 
					 | 
				
			||||||
  font-size: calc(10px + 2vmin);
 | 
					 | 
				
			||||||
  color: white;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.App-link {
 | 
					 | 
				
			||||||
  color: #61dafb;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@keyframes App-logo-spin {
 | 
					 | 
				
			||||||
  from {
 | 
					 | 
				
			||||||
    transform: rotate(0deg);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  to {
 | 
					 | 
				
			||||||
    transform: rotate(360deg);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -0,0 +1 @@
 | 
				
			|||||||
 | 
					<svg height="368pt" viewBox="0 0 368 368" width="368pt" xmlns="http://www.w3.org/2000/svg"><path d="m328 8h-288c-17.679688 0-32 14.320312-32 32v288c0 17.679688 14.320312 32 32 32h288c17.679688 0 32-14.320312 32-32v-288c0-17.679688-14.320312-32-32-32zm-96 176v112h-96v-112h-64l112-112 112 112zm0 0" fill="#cce4ff"/><g fill="#007aff"><path d="m189.65625 66.34375c-3.128906-3.128906-8.183594-3.128906-11.3125 0l-112 112c-2.289062 2.289062-2.976562 5.726562-1.734375 8.71875 1.230469 2.992188 4.160156 4.9375 7.390625 4.9375h56v104c0 4.425781 3.574219 8 8 8h96c4.425781 0 8-3.574219 8-8v-104h56c3.230469 0 6.160156-1.945312 7.390625-4.9375 1.242187-2.992188.554687-6.429688-1.734375-8.71875zm42.34375 109.65625c-4.425781 0-8 3.574219-8 8v104h-80v-104c0-4.425781-3.574219-8-8-8h-44.6875l92.6875-92.6875 92.6875 92.6875zm0 0"/><path d="m328 0h-288c-22.054688 0-40 17.945312-40 40v288c0 22.054688 17.945312 40 40 40h288c22.054688 0 40-17.945312 40-40v-288c0-22.054688-17.945312-40-40-40zm24 328c0 13.230469-10.769531 24-24 24h-288c-13.230469 0-24-10.769531-24-24v-288c0-13.230469 10.769531-24 24-24h288c13.230469 0 24 10.769531 24 24zm0 0"/></g></svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 1.1 KiB  | 
											
												Binary file not shown.
											
										
									
								| 
		 After Width: | Height: | Size: 94 KiB  | 
											
												Binary file not shown.
											
										
									
								| 
		 After Width: | Height: | Size: 22 KiB  | 
@ -0,0 +1,74 @@
 | 
				
			|||||||
 | 
					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()}
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,98 @@
 | 
				
			|||||||
 | 
					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>
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default FileManualForm;
 | 
				
			||||||
@ -0,0 +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>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default FileUploadForm;
 | 
				
			||||||
@ -0,0 +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>
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Home;
 | 
				
			||||||
@ -0,0 +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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +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;
 | 
				
			||||||
@ -0,0 +1,69 @@
 | 
				
			|||||||
 | 
					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;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@ -0,0 +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>
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default NotFound;
 | 
				
			||||||
@ -0,0 +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>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Register;
 | 
				
			||||||
@ -0,0 +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>
 | 
				
			||||||
 | 
						);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export default Upload;
 | 
				
			||||||
@ -0,0 +1,109 @@
 | 
				
			|||||||
 | 
					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>
 | 
				
			||||||
 | 
					))
 | 
				
			||||||
@ -0,0 +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>
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -1,13 +0,0 @@
 | 
				
			|||||||
body {
 | 
					 | 
				
			||||||
  margin: 0;
 | 
					 | 
				
			||||||
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
 | 
					 | 
				
			||||||
    'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
 | 
					 | 
				
			||||||
    sans-serif;
 | 
					 | 
				
			||||||
  -webkit-font-smoothing: antialiased;
 | 
					 | 
				
			||||||
  -moz-osx-font-smoothing: grayscale;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
code {
 | 
					 | 
				
			||||||
  font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
 | 
					 | 
				
			||||||
    monospace;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@ -1,7 +0,0 @@
 | 
				
			|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 841.9 595.3">
 | 
					 | 
				
			||||||
    <g fill="#61DAFB">
 | 
					 | 
				
			||||||
        <path d="M666.3 296.5c0-32.5-40.7-63.3-103.1-82.4 14.4-63.6 8-114.2-20.2-130.4-6.5-3.8-14.1-5.6-22.4-5.6v22.3c4.6 0 8.3.9 11.4 2.6 13.6 7.8 19.5 37.5 14.9 75.7-1.1 9.4-2.9 19.3-5.1 29.4-19.6-4.8-41-8.5-63.5-10.9-13.5-18.5-27.5-35.3-41.6-50 32.6-30.3 63.2-46.9 84-46.9V78c-27.5 0-63.5 19.6-99.9 53.6-36.4-33.8-72.4-53.2-99.9-53.2v22.3c20.7 0 51.4 16.5 84 46.6-14 14.7-28 31.4-41.3 49.9-22.6 2.4-44 6.1-63.6 11-2.3-10-4-19.7-5.2-29-4.7-38.2 1.1-67.9 14.6-75.8 3-1.8 6.9-2.6 11.5-2.6V78.5c-8.4 0-16 1.8-22.6 5.6-28.1 16.2-34.4 66.7-19.9 130.1-62.2 19.2-102.7 49.9-102.7 82.3 0 32.5 40.7 63.3 103.1 82.4-14.4 63.6-8 114.2 20.2 130.4 6.5 3.8 14.1 5.6 22.5 5.6 27.5 0 63.5-19.6 99.9-53.6 36.4 33.8 72.4 53.2 99.9 53.2 8.4 0 16-1.8 22.6-5.6 28.1-16.2 34.4-66.7 19.9-130.1 62-19.1 102.5-49.9 102.5-82.3zm-130.2-66.7c-3.7 12.9-8.3 26.2-13.5 39.5-4.1-8-8.4-16-13.1-24-4.6-8-9.5-15.8-14.4-23.4 14.2 2.1 27.9 4.7 41 7.9zm-45.8 106.5c-7.8 13.5-15.8 26.3-24.1 38.2-14.9 1.3-30 2-45.2 2-15.1 0-30.2-.7-45-1.9-8.3-11.9-16.4-24.6-24.2-38-7.6-13.1-14.5-26.4-20.8-39.8 6.2-13.4 13.2-26.8 20.7-39.9 7.8-13.5 15.8-26.3 24.1-38.2 14.9-1.3 30-2 45.2-2 15.1 0 30.2.7 45 1.9 8.3 11.9 16.4 24.6 24.2 38 7.6 13.1 14.5 26.4 20.8 39.8-6.3 13.4-13.2 26.8-20.7 39.9zm32.3-13c5.4 13.4 10 26.8 13.8 39.8-13.1 3.2-26.9 5.9-41.2 8 4.9-7.7 9.8-15.6 14.4-23.7 4.6-8 8.9-16.1 13-24.1zM421.2 430c-9.3-9.6-18.6-20.3-27.8-32 9 .4 18.2.7 27.5.7 9.4 0 18.7-.2 27.8-.7-9 11.7-18.3 22.4-27.5 32zm-74.4-58.9c-14.2-2.1-27.9-4.7-41-7.9 3.7-12.9 8.3-26.2 13.5-39.5 4.1 8 8.4 16 13.1 24 4.7 8 9.5 15.8 14.4 23.4zM420.7 163c9.3 9.6 18.6 20.3 27.8 32-9-.4-18.2-.7-27.5-.7-9.4 0-18.7.2-27.8.7 9-11.7 18.3-22.4 27.5-32zm-74 58.9c-4.9 7.7-9.8 15.6-14.4 23.7-4.6 8-8.9 16-13 24-5.4-13.4-10-26.8-13.8-39.8 13.1-3.1 26.9-5.8 41.2-7.9zm-90.5 125.2c-35.4-15.1-58.3-34.9-58.3-50.6 0-15.7 22.9-35.6 58.3-50.6 8.6-3.7 18-7 27.7-10.1 5.7 19.6 13.2 40 22.5 60.9-9.2 20.8-16.6 41.1-22.2 60.6-9.9-3.1-19.3-6.5-28-10.2zM310 490c-13.6-7.8-19.5-37.5-14.9-75.7 1.1-9.4 2.9-19.3 5.1-29.4 19.6 4.8 41 8.5 63.5 10.9 13.5 18.5 27.5 35.3 41.6 50-32.6 30.3-63.2 46.9-84 46.9-4.5-.1-8.3-1-11.3-2.7zm237.2-76.2c4.7 38.2-1.1 67.9-14.6 75.8-3 1.8-6.9 2.6-11.5 2.6-20.7 0-51.4-16.5-84-46.6 14-14.7 28-31.4 41.3-49.9 22.6-2.4 44-6.1 63.6-11 2.3 10.1 4.1 19.8 5.2 29.1zm38.5-66.7c-8.6 3.7-18 7-27.7 10.1-5.7-19.6-13.2-40-22.5-60.9 9.2-20.8 16.6-41.1 22.2-60.6 9.9 3.1 19.3 6.5 28.1 10.2 35.4 15.1 58.3 34.9 58.3 50.6-.1 15.7-23 35.6-58.4 50.6zM320.8 78.4z"/>
 | 
					 | 
				
			||||||
        <circle cx="420.9" cy="296.5" r="45.7"/>
 | 
					 | 
				
			||||||
        <path d="M520.5 78.1z"/>
 | 
					 | 
				
			||||||
    </g>
 | 
					 | 
				
			||||||
</svg>
 | 
					 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 2.6 KiB  | 
											
												
													File diff suppressed because it is too large
													Load Diff
												
											
										
									
								@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					/* 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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@ -0,0 +1,742 @@
 | 
				
			|||||||
 | 
					module.exports = {
 | 
				
			||||||
 | 
						purge: {
 | 
				
			||||||
 | 
							content: [
 | 
				
			||||||
 | 
								"./src/components/*.js",
 | 
				
			||||||
 | 
								"./node_modules/react-toastify/dist/ReactToastify.min.css",
 | 
				
			||||||
 | 
								"./public/index.html",
 | 
				
			||||||
 | 
								"./src/custom.js",
 | 
				
			||||||
 | 
								"./src/App.js",
 | 
				
			||||||
 | 
							]
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						target: 'relaxed',
 | 
				
			||||||
 | 
						prefix: '',
 | 
				
			||||||
 | 
						important: false,
 | 
				
			||||||
 | 
						separator: ':',
 | 
				
			||||||
 | 
						theme: {
 | 
				
			||||||
 | 
							screens: {
 | 
				
			||||||
 | 
								sm: '640px',
 | 
				
			||||||
 | 
								md: '768px',
 | 
				
			||||||
 | 
								lg: '1024px',
 | 
				
			||||||
 | 
								xl: '1280px',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							colors: {
 | 
				
			||||||
 | 
								transparent: 'transparent',
 | 
				
			||||||
 | 
								current: 'currentColor',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								black: '#000',
 | 
				
			||||||
 | 
								white: '#fff',
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
								gray: {
 | 
				
			||||||
 | 
									100: '#f7fafc',
 | 
				
			||||||
 | 
									200: '#edf2f7',
 | 
				
			||||||
 | 
									300: '#e2e8f0',
 | 
				
			||||||
 | 
									400: '#cbd5e0',
 | 
				
			||||||
 | 
									500: '#a0aec0',
 | 
				
			||||||
 | 
									600: '#718096',
 | 
				
			||||||
 | 
									700: '#4a5568',
 | 
				
			||||||
 | 
									800: '#2d3748',
 | 
				
			||||||
 | 
									900: '#1a202c',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								red: {
 | 
				
			||||||
 | 
									100: '#fff5f5',
 | 
				
			||||||
 | 
									200: '#fed7d7',
 | 
				
			||||||
 | 
									300: '#feb2b2',
 | 
				
			||||||
 | 
									400: '#fc8181',
 | 
				
			||||||
 | 
									500: '#f56565',
 | 
				
			||||||
 | 
									600: '#e53e3e',
 | 
				
			||||||
 | 
									700: '#c53030',
 | 
				
			||||||
 | 
									800: '#9b2c2c',
 | 
				
			||||||
 | 
									900: '#742a2a',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								orange: {
 | 
				
			||||||
 | 
									100: '#fffaf0',
 | 
				
			||||||
 | 
									200: '#feebc8',
 | 
				
			||||||
 | 
									300: '#fbd38d',
 | 
				
			||||||
 | 
									400: '#f6ad55',
 | 
				
			||||||
 | 
									500: '#ed8936',
 | 
				
			||||||
 | 
									600: '#dd6b20',
 | 
				
			||||||
 | 
									700: '#c05621',
 | 
				
			||||||
 | 
									800: '#9c4221',
 | 
				
			||||||
 | 
									900: '#7b341e',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								yellow: {
 | 
				
			||||||
 | 
									100: '#fffff0',
 | 
				
			||||||
 | 
									200: '#fefcbf',
 | 
				
			||||||
 | 
									300: '#faf089',
 | 
				
			||||||
 | 
									400: '#f6e05e',
 | 
				
			||||||
 | 
									500: '#ecc94b',
 | 
				
			||||||
 | 
									600: '#d69e2e',
 | 
				
			||||||
 | 
									700: '#b7791f',
 | 
				
			||||||
 | 
									800: '#975a16',
 | 
				
			||||||
 | 
									900: '#744210',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								green: {
 | 
				
			||||||
 | 
									100: '#f0fff4',
 | 
				
			||||||
 | 
									200: '#c6f6d5',
 | 
				
			||||||
 | 
									300: '#9ae6b4',
 | 
				
			||||||
 | 
									400: '#68d391',
 | 
				
			||||||
 | 
									500: '#48bb78',
 | 
				
			||||||
 | 
									600: '#38a169',
 | 
				
			||||||
 | 
									700: '#2f855a',
 | 
				
			||||||
 | 
									800: '#276749',
 | 
				
			||||||
 | 
									900: '#22543d',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								teal: {
 | 
				
			||||||
 | 
									100: '#e6fffa',
 | 
				
			||||||
 | 
									200: '#b2f5ea',
 | 
				
			||||||
 | 
									300: '#81e6d9',
 | 
				
			||||||
 | 
									400: '#4fd1c5',
 | 
				
			||||||
 | 
									500: '#38b2ac',
 | 
				
			||||||
 | 
									600: '#319795',
 | 
				
			||||||
 | 
									700: '#2c7a7b',
 | 
				
			||||||
 | 
									800: '#285e61',
 | 
				
			||||||
 | 
									900: '#234e52',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								blue: {
 | 
				
			||||||
 | 
									100: '#ebf8ff',
 | 
				
			||||||
 | 
									200: '#bee3f8',
 | 
				
			||||||
 | 
									300: '#90cdf4',
 | 
				
			||||||
 | 
									400: '#63b3ed',
 | 
				
			||||||
 | 
									500: '#4299e1',
 | 
				
			||||||
 | 
									600: '#487AE4',
 | 
				
			||||||
 | 
									700: '#2b6cb0',
 | 
				
			||||||
 | 
									800: '#2c5282',
 | 
				
			||||||
 | 
									900: '#2a4365',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								indigo: {
 | 
				
			||||||
 | 
									100: '#ebf4ff',
 | 
				
			||||||
 | 
									200: '#c3dafe',
 | 
				
			||||||
 | 
									300: '#a3bffa',
 | 
				
			||||||
 | 
									400: '#7f9cf5',
 | 
				
			||||||
 | 
									500: '#667eea',
 | 
				
			||||||
 | 
									600: '#5a67d8',
 | 
				
			||||||
 | 
									700: '#4c51bf',
 | 
				
			||||||
 | 
									800: '#434190',
 | 
				
			||||||
 | 
									900: '#3c366b',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								purple: {
 | 
				
			||||||
 | 
									100: '#faf5ff',
 | 
				
			||||||
 | 
									200: '#e9d8fd',
 | 
				
			||||||
 | 
									300: '#d6bcfa',
 | 
				
			||||||
 | 
									400: '#b794f4',
 | 
				
			||||||
 | 
									500: '#9f7aea',
 | 
				
			||||||
 | 
									600: '#805ad5',
 | 
				
			||||||
 | 
									700: '#6b46c1',
 | 
				
			||||||
 | 
									800: '#553c9a',
 | 
				
			||||||
 | 
									900: '#44337a',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
								pink: {
 | 
				
			||||||
 | 
									100: '#fff5f7',
 | 
				
			||||||
 | 
									200: '#fed7e2',
 | 
				
			||||||
 | 
									300: '#fbb6ce',
 | 
				
			||||||
 | 
									400: '#f687b3',
 | 
				
			||||||
 | 
									500: '#ed64a6',
 | 
				
			||||||
 | 
									600: '#d53f8c',
 | 
				
			||||||
 | 
									700: '#b83280',
 | 
				
			||||||
 | 
									800: '#97266d',
 | 
				
			||||||
 | 
									900: '#702459',
 | 
				
			||||||
 | 
								},
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							spacing: {
 | 
				
			||||||
 | 
								px: '1px',
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								'1': '0.25rem',
 | 
				
			||||||
 | 
								'2': '0.5rem',
 | 
				
			||||||
 | 
								'3': '0.75rem',
 | 
				
			||||||
 | 
								'4': '1rem',
 | 
				
			||||||
 | 
								'5': '1.25rem',
 | 
				
			||||||
 | 
								'6': '1.5rem',
 | 
				
			||||||
 | 
								'8': '2rem',
 | 
				
			||||||
 | 
								'10': '2.5rem',
 | 
				
			||||||
 | 
								'12': '3rem',
 | 
				
			||||||
 | 
								'16': '4rem',
 | 
				
			||||||
 | 
								'20': '5rem',
 | 
				
			||||||
 | 
								'24': '6rem',
 | 
				
			||||||
 | 
								'32': '8rem',
 | 
				
			||||||
 | 
								'40': '10rem',
 | 
				
			||||||
 | 
								'48': '12rem',
 | 
				
			||||||
 | 
								'56': '14rem',
 | 
				
			||||||
 | 
								'64': '16rem',
 | 
				
			||||||
 | 
								'72': '18rem',
 | 
				
			||||||
 | 
								'96': '24rem',
 | 
				
			||||||
 | 
								'128': '32rem',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							backgroundColor: theme => theme('colors'),
 | 
				
			||||||
 | 
							backgroundOpacity: theme => theme('opacity'),
 | 
				
			||||||
 | 
							backgroundPosition: {
 | 
				
			||||||
 | 
								bottom: 'bottom',
 | 
				
			||||||
 | 
								center: 'center',
 | 
				
			||||||
 | 
								left: 'left',
 | 
				
			||||||
 | 
								'left-bottom': 'left bottom',
 | 
				
			||||||
 | 
								'left-top': 'left top',
 | 
				
			||||||
 | 
								right: 'right',
 | 
				
			||||||
 | 
								'right-bottom': 'right bottom',
 | 
				
			||||||
 | 
								'right-top': 'right top',
 | 
				
			||||||
 | 
								top: 'top',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							backgroundSize: {
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								cover: 'cover',
 | 
				
			||||||
 | 
								contain: 'contain',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							borderColor: theme => ({
 | 
				
			||||||
 | 
								...theme('colors'),
 | 
				
			||||||
 | 
								default: theme('colors.gray.300', 'currentColor'),
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							borderOpacity: theme => theme('opacity'),
 | 
				
			||||||
 | 
							borderRadius: {
 | 
				
			||||||
 | 
								none: '0',
 | 
				
			||||||
 | 
								sm: '0.125rem',
 | 
				
			||||||
 | 
								default: '0.25rem',
 | 
				
			||||||
 | 
								md: '0.375rem',
 | 
				
			||||||
 | 
								lg: '0.5rem',
 | 
				
			||||||
 | 
								full: '9999px',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							borderWidth: {
 | 
				
			||||||
 | 
								default: '1px',
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								'2': '2px',
 | 
				
			||||||
 | 
								'4': '4px',
 | 
				
			||||||
 | 
								'8': '8px',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							boxShadow: {
 | 
				
			||||||
 | 
								xs: '0 0 0 1px rgba(0, 0, 0, 0.05)',
 | 
				
			||||||
 | 
								sm: '0 1px 2px 0 rgba(0, 0, 0, 0.05)',
 | 
				
			||||||
 | 
								default: '0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)',
 | 
				
			||||||
 | 
								md: '0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)',
 | 
				
			||||||
 | 
								lg: '0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05)',
 | 
				
			||||||
 | 
								xl: '0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04)',
 | 
				
			||||||
 | 
								'2xl': '0 25px 50px -12px rgba(0, 0, 0, 0.25)',
 | 
				
			||||||
 | 
								inner: 'inset 0 2px 4px 0 rgba(0, 0, 0, 0.06)',
 | 
				
			||||||
 | 
								outline: '0 0 0 3px rgba(66, 153, 225, 0.5)',
 | 
				
			||||||
 | 
								none: 'none',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							container: {},
 | 
				
			||||||
 | 
							cursor: {
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								default: 'default',
 | 
				
			||||||
 | 
								pointer: 'pointer',
 | 
				
			||||||
 | 
								wait: 'wait',
 | 
				
			||||||
 | 
								text: 'text',
 | 
				
			||||||
 | 
								move: 'move',
 | 
				
			||||||
 | 
								'not-allowed': 'not-allowed',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							divideColor: theme => theme('borderColor'),
 | 
				
			||||||
 | 
							divideOpacity: theme => theme('borderOpacity'),
 | 
				
			||||||
 | 
							divideWidth: theme => theme('borderWidth'),
 | 
				
			||||||
 | 
							fill: {
 | 
				
			||||||
 | 
								current: 'currentColor',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							flex: {
 | 
				
			||||||
 | 
								'1': '1 1 0%',
 | 
				
			||||||
 | 
								'2': '2 2 0%',
 | 
				
			||||||
 | 
								auto: '1 1 auto',
 | 
				
			||||||
 | 
								initial: '0 1 auto',
 | 
				
			||||||
 | 
								none: 'none',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							flexGrow: {
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								default: '1',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							flexShrink: {
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								default: '1',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							fontFamily: {
 | 
				
			||||||
 | 
								sans: [
 | 
				
			||||||
 | 
									// -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji"
 | 
				
			||||||
 | 
									'-apple-system',
 | 
				
			||||||
 | 
									'BlinkMacSystemFont',
 | 
				
			||||||
 | 
									'"Segoe UI"',
 | 
				
			||||||
 | 
									'Roboto',
 | 
				
			||||||
 | 
									'"Helvetica Neue"',
 | 
				
			||||||
 | 
									'Arial',
 | 
				
			||||||
 | 
									'"Noto Sans"',
 | 
				
			||||||
 | 
									'sans-serif',
 | 
				
			||||||
 | 
									'"Apple Color Emoji"',
 | 
				
			||||||
 | 
									'"Segoe UI Emoji"',
 | 
				
			||||||
 | 
									'"Segoe UI Symbol"',
 | 
				
			||||||
 | 
									'"Noto Color Emoji"',
 | 
				
			||||||
 | 
								],
 | 
				
			||||||
 | 
								serif: ['Georgia', 'Cambria', '"Times New Roman"', 'Times', 'serif'],
 | 
				
			||||||
 | 
								mono: ['Menlo', 'Monaco', 'Consolas', '"Liberation Mono"', '"Courier New"', 'monospace'],
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							fontSize: {
 | 
				
			||||||
 | 
								xs: '0.75rem',
 | 
				
			||||||
 | 
								sm: '0.875rem',
 | 
				
			||||||
 | 
								base: '1rem',
 | 
				
			||||||
 | 
								lg: '1.125rem',
 | 
				
			||||||
 | 
								xl: '1.25rem',
 | 
				
			||||||
 | 
								'2xl': '1.5rem',
 | 
				
			||||||
 | 
								'3xl': '1.875rem',
 | 
				
			||||||
 | 
								'4xl': '2.25rem',
 | 
				
			||||||
 | 
								'5xl': '3rem',
 | 
				
			||||||
 | 
								'6xl': '4rem',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							fontWeight: {
 | 
				
			||||||
 | 
								hairline: '100',
 | 
				
			||||||
 | 
								thin: '200',
 | 
				
			||||||
 | 
								light: '300',
 | 
				
			||||||
 | 
								normal: '400',
 | 
				
			||||||
 | 
								medium: '500',
 | 
				
			||||||
 | 
								semibold: '600',
 | 
				
			||||||
 | 
								bold: '700',
 | 
				
			||||||
 | 
								extrabold: '800',
 | 
				
			||||||
 | 
								black: '900',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							height: theme => ({
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								...theme('spacing'),
 | 
				
			||||||
 | 
								full: '100%',
 | 
				
			||||||
 | 
								screen: '100vh',
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							inset: {
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							letterSpacing: {
 | 
				
			||||||
 | 
								tighter: '-0.05em',
 | 
				
			||||||
 | 
								tight: '-0.025em',
 | 
				
			||||||
 | 
								normal: '0',
 | 
				
			||||||
 | 
								wide: '0.025em',
 | 
				
			||||||
 | 
								wider: '0.05em',
 | 
				
			||||||
 | 
								widest: '0.1em',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							lineHeight: {
 | 
				
			||||||
 | 
								none: '1',
 | 
				
			||||||
 | 
								tight: '1.25',
 | 
				
			||||||
 | 
								snug: '1.375',
 | 
				
			||||||
 | 
								normal: '1.5',
 | 
				
			||||||
 | 
								relaxed: '1.625',
 | 
				
			||||||
 | 
								loose: '2',
 | 
				
			||||||
 | 
								'3': '.75rem',
 | 
				
			||||||
 | 
								'4': '1rem',
 | 
				
			||||||
 | 
								'5': '1.25rem',
 | 
				
			||||||
 | 
								'6': '1.5rem',
 | 
				
			||||||
 | 
								'7': '1.75rem',
 | 
				
			||||||
 | 
								'8': '2rem',
 | 
				
			||||||
 | 
								'9': '2.25rem',
 | 
				
			||||||
 | 
								'10': '2.5rem',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							listStyleType: {
 | 
				
			||||||
 | 
								none: 'none',
 | 
				
			||||||
 | 
								disc: 'disc',
 | 
				
			||||||
 | 
								decimal: 'decimal',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							margin: (theme, {negative}) => ({
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								...theme('spacing'),
 | 
				
			||||||
 | 
								...negative(theme('spacing')),
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							maxHeight: {
 | 
				
			||||||
 | 
								full: '100%',
 | 
				
			||||||
 | 
								screen: '100vh',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							maxWidth: (theme, {breakpoints}) => ({
 | 
				
			||||||
 | 
								none: 'none',
 | 
				
			||||||
 | 
								xs: '20rem',
 | 
				
			||||||
 | 
								sm: '24rem',
 | 
				
			||||||
 | 
								md: '28rem',
 | 
				
			||||||
 | 
								lg: '32rem',
 | 
				
			||||||
 | 
								xl: '36rem',
 | 
				
			||||||
 | 
								'2xl': '42rem',
 | 
				
			||||||
 | 
								'3xl': '48rem',
 | 
				
			||||||
 | 
								'4xl': '56rem',
 | 
				
			||||||
 | 
								'5xl': '64rem',
 | 
				
			||||||
 | 
								'6xl': '72rem',
 | 
				
			||||||
 | 
								'8xl': '92rem',
 | 
				
			||||||
 | 
								full: '100%',
 | 
				
			||||||
 | 
								...breakpoints(theme('screens')),
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							minHeight: {
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								full: '100%',
 | 
				
			||||||
 | 
								screen: '100vh',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							minWidth: {
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								full: '100%',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							objectPosition: {
 | 
				
			||||||
 | 
								bottom: 'bottom',
 | 
				
			||||||
 | 
								center: 'center',
 | 
				
			||||||
 | 
								left: 'left',
 | 
				
			||||||
 | 
								'left-bottom': 'left bottom',
 | 
				
			||||||
 | 
								'left-top': 'left top',
 | 
				
			||||||
 | 
								right: 'right',
 | 
				
			||||||
 | 
								'right-bottom': 'right bottom',
 | 
				
			||||||
 | 
								'right-top': 'right top',
 | 
				
			||||||
 | 
								top: 'top',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							opacity: {
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								'25': '0.25',
 | 
				
			||||||
 | 
								'50': '0.5',
 | 
				
			||||||
 | 
								'75': '0.75',
 | 
				
			||||||
 | 
								'100': '1',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							order: {
 | 
				
			||||||
 | 
								first: '-9999',
 | 
				
			||||||
 | 
								last: '9999',
 | 
				
			||||||
 | 
								none: '0',
 | 
				
			||||||
 | 
								'1': '1',
 | 
				
			||||||
 | 
								'2': '2',
 | 
				
			||||||
 | 
								'3': '3',
 | 
				
			||||||
 | 
								'4': '4',
 | 
				
			||||||
 | 
								'5': '5',
 | 
				
			||||||
 | 
								'6': '6',
 | 
				
			||||||
 | 
								'7': '7',
 | 
				
			||||||
 | 
								'8': '8',
 | 
				
			||||||
 | 
								'9': '9',
 | 
				
			||||||
 | 
								'10': '10',
 | 
				
			||||||
 | 
								'11': '11',
 | 
				
			||||||
 | 
								'12': '12',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							padding: theme => theme('spacing'),
 | 
				
			||||||
 | 
							placeholderColor: theme => theme('colors'),
 | 
				
			||||||
 | 
							placeholderOpacity: theme => theme('opacity'),
 | 
				
			||||||
 | 
							space: (theme, {negative}) => ({
 | 
				
			||||||
 | 
								...theme('spacing'),
 | 
				
			||||||
 | 
								...negative(theme('spacing')),
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							stroke: {
 | 
				
			||||||
 | 
								current: 'currentColor',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							strokeWidth: {
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								'1': '1',
 | 
				
			||||||
 | 
								'2': '2',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							textColor: theme => theme('colors'),
 | 
				
			||||||
 | 
							textOpacity: theme => theme('opacity'),
 | 
				
			||||||
 | 
							width: theme => ({
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								...theme('spacing'),
 | 
				
			||||||
 | 
								'1/2': '50%',
 | 
				
			||||||
 | 
								'1/3': '33.333333%',
 | 
				
			||||||
 | 
								'2/3': '66.666667%',
 | 
				
			||||||
 | 
								'1/4': '25%',
 | 
				
			||||||
 | 
								'2/4': '50%',
 | 
				
			||||||
 | 
								'3/4': '75%',
 | 
				
			||||||
 | 
								'1/5': '20%',
 | 
				
			||||||
 | 
								'2/5': '40%',
 | 
				
			||||||
 | 
								'3/5': '60%',
 | 
				
			||||||
 | 
								'4/5': '80%',
 | 
				
			||||||
 | 
								'1/6': '16.666667%',
 | 
				
			||||||
 | 
								'2/6': '33.333333%',
 | 
				
			||||||
 | 
								'3/6': '50%',
 | 
				
			||||||
 | 
								'4/6': '66.666667%',
 | 
				
			||||||
 | 
								'5/6': '83.333333%',
 | 
				
			||||||
 | 
								'1/12': '8.333333%',
 | 
				
			||||||
 | 
								'2/12': '16.666667%',
 | 
				
			||||||
 | 
								'3/12': '25%',
 | 
				
			||||||
 | 
								'4/12': '33.333333%',
 | 
				
			||||||
 | 
								'5/12': '41.666667%',
 | 
				
			||||||
 | 
								'6/12': '50%',
 | 
				
			||||||
 | 
								'7/12': '58.333333%',
 | 
				
			||||||
 | 
								'8/12': '66.666667%',
 | 
				
			||||||
 | 
								'9/12': '75%',
 | 
				
			||||||
 | 
								'10/12': '83.333333%',
 | 
				
			||||||
 | 
								'11/12': '91.666667%',
 | 
				
			||||||
 | 
								full: '100%',
 | 
				
			||||||
 | 
								screen: '100vw',
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							zIndex: {
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								'10': '10',
 | 
				
			||||||
 | 
								'20': '20',
 | 
				
			||||||
 | 
								'30': '30',
 | 
				
			||||||
 | 
								'40': '40',
 | 
				
			||||||
 | 
								'50': '50',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							gap: theme => theme('spacing'),
 | 
				
			||||||
 | 
							gridTemplateColumns: {
 | 
				
			||||||
 | 
								none: 'none',
 | 
				
			||||||
 | 
								'1': 'repeat(1, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'2': 'repeat(2, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'3': 'repeat(3, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'4': 'repeat(4, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'5': 'repeat(5, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'6': 'repeat(6, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'7': 'repeat(7, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'8': 'repeat(8, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'9': 'repeat(9, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'10': 'repeat(10, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'11': 'repeat(11, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'12': 'repeat(12, minmax(0, 1fr))',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							gridColumn: {
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								'span-1': 'span 1 / span 1',
 | 
				
			||||||
 | 
								'span-2': 'span 2 / span 2',
 | 
				
			||||||
 | 
								'span-3': 'span 3 / span 3',
 | 
				
			||||||
 | 
								'span-4': 'span 4 / span 4',
 | 
				
			||||||
 | 
								'span-5': 'span 5 / span 5',
 | 
				
			||||||
 | 
								'span-6': 'span 6 / span 6',
 | 
				
			||||||
 | 
								'span-7': 'span 7 / span 7',
 | 
				
			||||||
 | 
								'span-8': 'span 8 / span 8',
 | 
				
			||||||
 | 
								'span-9': 'span 9 / span 9',
 | 
				
			||||||
 | 
								'span-10': 'span 10 / span 10',
 | 
				
			||||||
 | 
								'span-11': 'span 11 / span 11',
 | 
				
			||||||
 | 
								'span-12': 'span 12 / span 12',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							gridColumnStart: {
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								'1': '1',
 | 
				
			||||||
 | 
								'2': '2',
 | 
				
			||||||
 | 
								'3': '3',
 | 
				
			||||||
 | 
								'4': '4',
 | 
				
			||||||
 | 
								'5': '5',
 | 
				
			||||||
 | 
								'6': '6',
 | 
				
			||||||
 | 
								'7': '7',
 | 
				
			||||||
 | 
								'8': '8',
 | 
				
			||||||
 | 
								'9': '9',
 | 
				
			||||||
 | 
								'10': '10',
 | 
				
			||||||
 | 
								'11': '11',
 | 
				
			||||||
 | 
								'12': '12',
 | 
				
			||||||
 | 
								'13': '13',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							gridColumnEnd: {
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								'1': '1',
 | 
				
			||||||
 | 
								'2': '2',
 | 
				
			||||||
 | 
								'3': '3',
 | 
				
			||||||
 | 
								'4': '4',
 | 
				
			||||||
 | 
								'5': '5',
 | 
				
			||||||
 | 
								'6': '6',
 | 
				
			||||||
 | 
								'7': '7',
 | 
				
			||||||
 | 
								'8': '8',
 | 
				
			||||||
 | 
								'9': '9',
 | 
				
			||||||
 | 
								'10': '10',
 | 
				
			||||||
 | 
								'11': '11',
 | 
				
			||||||
 | 
								'12': '12',
 | 
				
			||||||
 | 
								'13': '13',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							gridTemplateRows: {
 | 
				
			||||||
 | 
								none: 'none',
 | 
				
			||||||
 | 
								'1': 'repeat(1, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'2': 'repeat(2, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'3': 'repeat(3, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'4': 'repeat(4, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'5': 'repeat(5, minmax(0, 1fr))',
 | 
				
			||||||
 | 
								'6': 'repeat(6, minmax(0, 1fr))',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							gridRow: {
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								'span-1': 'span 1 / span 1',
 | 
				
			||||||
 | 
								'span-2': 'span 2 / span 2',
 | 
				
			||||||
 | 
								'span-3': 'span 3 / span 3',
 | 
				
			||||||
 | 
								'span-4': 'span 4 / span 4',
 | 
				
			||||||
 | 
								'span-5': 'span 5 / span 5',
 | 
				
			||||||
 | 
								'span-6': 'span 6 / span 6',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							gridRowStart: {
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								'1': '1',
 | 
				
			||||||
 | 
								'2': '2',
 | 
				
			||||||
 | 
								'3': '3',
 | 
				
			||||||
 | 
								'4': '4',
 | 
				
			||||||
 | 
								'5': '5',
 | 
				
			||||||
 | 
								'6': '6',
 | 
				
			||||||
 | 
								'7': '7',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							gridRowEnd: {
 | 
				
			||||||
 | 
								auto: 'auto',
 | 
				
			||||||
 | 
								'1': '1',
 | 
				
			||||||
 | 
								'2': '2',
 | 
				
			||||||
 | 
								'3': '3',
 | 
				
			||||||
 | 
								'4': '4',
 | 
				
			||||||
 | 
								'5': '5',
 | 
				
			||||||
 | 
								'6': '6',
 | 
				
			||||||
 | 
								'7': '7',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							transformOrigin: {
 | 
				
			||||||
 | 
								center: 'center',
 | 
				
			||||||
 | 
								top: 'top',
 | 
				
			||||||
 | 
								'top-right': 'top right',
 | 
				
			||||||
 | 
								right: 'right',
 | 
				
			||||||
 | 
								'bottom-right': 'bottom right',
 | 
				
			||||||
 | 
								bottom: 'bottom',
 | 
				
			||||||
 | 
								'bottom-left': 'bottom left',
 | 
				
			||||||
 | 
								left: 'left',
 | 
				
			||||||
 | 
								'top-left': 'top left',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							scale: {
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								'50': '.5',
 | 
				
			||||||
 | 
								'75': '.75',
 | 
				
			||||||
 | 
								'90': '.9',
 | 
				
			||||||
 | 
								'95': '.95',
 | 
				
			||||||
 | 
								'100': '1',
 | 
				
			||||||
 | 
								'105': '1.05',
 | 
				
			||||||
 | 
								'110': '1.1',
 | 
				
			||||||
 | 
								'125': '1.25',
 | 
				
			||||||
 | 
								'150': '1.5',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							rotate: {
 | 
				
			||||||
 | 
								'-180': '-180deg',
 | 
				
			||||||
 | 
								'-90': '-90deg',
 | 
				
			||||||
 | 
								'-45': '-45deg',
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								'45': '45deg',
 | 
				
			||||||
 | 
								'90': '90deg',
 | 
				
			||||||
 | 
								'180': '180deg',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							translate: (theme, {negative}) => ({
 | 
				
			||||||
 | 
								...theme('spacing'),
 | 
				
			||||||
 | 
								...negative(theme('spacing')),
 | 
				
			||||||
 | 
								'-full': '-100%',
 | 
				
			||||||
 | 
								'-1/2': '-50%',
 | 
				
			||||||
 | 
								'1/2': '50%',
 | 
				
			||||||
 | 
								full: '100%',
 | 
				
			||||||
 | 
							}),
 | 
				
			||||||
 | 
							skew: {
 | 
				
			||||||
 | 
								'-12': '-12deg',
 | 
				
			||||||
 | 
								'-6': '-6deg',
 | 
				
			||||||
 | 
								'-3': '-3deg',
 | 
				
			||||||
 | 
								'0': '0',
 | 
				
			||||||
 | 
								'3': '3deg',
 | 
				
			||||||
 | 
								'6': '6deg',
 | 
				
			||||||
 | 
								'12': '12deg',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							transitionProperty: {
 | 
				
			||||||
 | 
								none: 'none',
 | 
				
			||||||
 | 
								all: 'all',
 | 
				
			||||||
 | 
								default: 'background-color, border-color, color, fill, stroke, opacity, box-shadow, transform',
 | 
				
			||||||
 | 
								colors: 'background-color, border-color, color, fill, stroke',
 | 
				
			||||||
 | 
								opacity: 'opacity',
 | 
				
			||||||
 | 
								shadow: 'box-shadow',
 | 
				
			||||||
 | 
								transform: 'transform',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							transitionTimingFunction: {
 | 
				
			||||||
 | 
								linear: 'linear',
 | 
				
			||||||
 | 
								in: 'cubic-bezier(0.4, 0, 1, 1)',
 | 
				
			||||||
 | 
								out: 'cubic-bezier(0, 0, 0.2, 1)',
 | 
				
			||||||
 | 
								'in-out': 'cubic-bezier(0.4, 0, 0.2, 1)',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							transitionDuration: {
 | 
				
			||||||
 | 
								'75': '75ms',
 | 
				
			||||||
 | 
								'100': '100ms',
 | 
				
			||||||
 | 
								'150': '150ms',
 | 
				
			||||||
 | 
								'200': '200ms',
 | 
				
			||||||
 | 
								'300': '300ms',
 | 
				
			||||||
 | 
								'500': '500ms',
 | 
				
			||||||
 | 
								'700': '700ms',
 | 
				
			||||||
 | 
								'1000': '1000ms',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
							transitionDelay: {
 | 
				
			||||||
 | 
								'75': '75ms',
 | 
				
			||||||
 | 
								'100': '100ms',
 | 
				
			||||||
 | 
								'150': '150ms',
 | 
				
			||||||
 | 
								'200': '200ms',
 | 
				
			||||||
 | 
								'300': '300ms',
 | 
				
			||||||
 | 
								'500': '500ms',
 | 
				
			||||||
 | 
								'700': '700ms',
 | 
				
			||||||
 | 
								'1000': '1000ms',
 | 
				
			||||||
 | 
							},
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						variants: {
 | 
				
			||||||
 | 
							accessibility: ['responsive', 'focus'],
 | 
				
			||||||
 | 
							alignContent: ['responsive'],
 | 
				
			||||||
 | 
							alignItems: ['responsive'],
 | 
				
			||||||
 | 
							alignSelf: ['responsive'],
 | 
				
			||||||
 | 
							appearance: ['responsive'],
 | 
				
			||||||
 | 
							backgroundAttachment: ['responsive'],
 | 
				
			||||||
 | 
							backgroundColor: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							backgroundOpacity: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							backgroundPosition: ['responsive'],
 | 
				
			||||||
 | 
							backgroundRepeat: ['responsive'],
 | 
				
			||||||
 | 
							backgroundSize: ['responsive'],
 | 
				
			||||||
 | 
							borderCollapse: ['responsive'],
 | 
				
			||||||
 | 
							borderColor: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							borderOpacity: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							borderRadius: ['responsive'],
 | 
				
			||||||
 | 
							borderStyle: ['responsive'],
 | 
				
			||||||
 | 
							borderWidth: ['responsive', 'hover'],
 | 
				
			||||||
 | 
							boxShadow: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							boxSizing: ['responsive'],
 | 
				
			||||||
 | 
							cursor: ['responsive'],
 | 
				
			||||||
 | 
							display: ['responsive'],
 | 
				
			||||||
 | 
							divideColor: ['responsive'],
 | 
				
			||||||
 | 
							divideOpacity: ['responsive'],
 | 
				
			||||||
 | 
							divideWidth: ['responsive'],
 | 
				
			||||||
 | 
							fill: ['responsive'],
 | 
				
			||||||
 | 
							flex: ['responsive'],
 | 
				
			||||||
 | 
							flexDirection: ['responsive'],
 | 
				
			||||||
 | 
							flexGrow: ['responsive'],
 | 
				
			||||||
 | 
							flexShrink: ['responsive'],
 | 
				
			||||||
 | 
							flexWrap: ['responsive'],
 | 
				
			||||||
 | 
							float: ['responsive'],
 | 
				
			||||||
 | 
							clear: ['responsive'],
 | 
				
			||||||
 | 
							fontFamily: ['responsive'],
 | 
				
			||||||
 | 
							fontSize: ['responsive'],
 | 
				
			||||||
 | 
							fontSmoothing: ['responsive'],
 | 
				
			||||||
 | 
							fontStyle: ['responsive'],
 | 
				
			||||||
 | 
							fontWeight: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							height: ['responsive'],
 | 
				
			||||||
 | 
							inset: ['responsive'],
 | 
				
			||||||
 | 
							justifyContent: ['responsive'],
 | 
				
			||||||
 | 
							letterSpacing: ['responsive'],
 | 
				
			||||||
 | 
							lineHeight: ['responsive'],
 | 
				
			||||||
 | 
							listStylePosition: ['responsive'],
 | 
				
			||||||
 | 
							listStyleType: ['responsive'],
 | 
				
			||||||
 | 
							margin: ['responsive'],
 | 
				
			||||||
 | 
							maxHeight: ['responsive'],
 | 
				
			||||||
 | 
							maxWidth: ['responsive'],
 | 
				
			||||||
 | 
							minHeight: ['responsive'],
 | 
				
			||||||
 | 
							minWidth: ['responsive'],
 | 
				
			||||||
 | 
							objectFit: ['responsive'],
 | 
				
			||||||
 | 
							objectPosition: ['responsive'],
 | 
				
			||||||
 | 
							opacity: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							order: ['responsive'],
 | 
				
			||||||
 | 
							outline: ['responsive', 'focus'],
 | 
				
			||||||
 | 
							overflow: ['responsive'],
 | 
				
			||||||
 | 
							padding: ['responsive'],
 | 
				
			||||||
 | 
							placeholderColor: ['responsive', 'focus'],
 | 
				
			||||||
 | 
							placeholderOpacity: ['responsive', 'focus'],
 | 
				
			||||||
 | 
							pointerEvents: ['responsive'],
 | 
				
			||||||
 | 
							position: ['responsive'],
 | 
				
			||||||
 | 
							resize: ['responsive'],
 | 
				
			||||||
 | 
							space: ['responsive'],
 | 
				
			||||||
 | 
							stroke: ['responsive'],
 | 
				
			||||||
 | 
							strokeWidth: ['responsive'],
 | 
				
			||||||
 | 
							tableLayout: ['responsive'],
 | 
				
			||||||
 | 
							textAlign: ['responsive'],
 | 
				
			||||||
 | 
							textColor: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							textOpacity: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							textDecoration: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							textTransform: ['responsive'],
 | 
				
			||||||
 | 
							userSelect: ['responsive'],
 | 
				
			||||||
 | 
							verticalAlign: ['responsive'],
 | 
				
			||||||
 | 
							visibility: ['responsive'],
 | 
				
			||||||
 | 
							whitespace: ['responsive'],
 | 
				
			||||||
 | 
							width: ['responsive'],
 | 
				
			||||||
 | 
							wordBreak: ['responsive'],
 | 
				
			||||||
 | 
							zIndex: ['responsive'],
 | 
				
			||||||
 | 
							gap: ['responsive'],
 | 
				
			||||||
 | 
							gridAutoFlow: ['responsive'],
 | 
				
			||||||
 | 
							gridTemplateColumns: ['responsive'],
 | 
				
			||||||
 | 
							gridColumn: ['responsive'],
 | 
				
			||||||
 | 
							gridColumnStart: ['responsive'],
 | 
				
			||||||
 | 
							gridColumnEnd: ['responsive'],
 | 
				
			||||||
 | 
							gridTemplateRows: ['responsive'],
 | 
				
			||||||
 | 
							gridRow: ['responsive'],
 | 
				
			||||||
 | 
							gridRowStart: ['responsive'],
 | 
				
			||||||
 | 
							gridRowEnd: ['responsive'],
 | 
				
			||||||
 | 
							transform: ['responsive'],
 | 
				
			||||||
 | 
							transformOrigin: ['responsive'],
 | 
				
			||||||
 | 
							scale: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							rotate: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							translate: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							skew: ['responsive', 'hover', 'focus'],
 | 
				
			||||||
 | 
							transitionProperty: ['responsive'],
 | 
				
			||||||
 | 
							transitionTimingFunction: ['responsive'],
 | 
				
			||||||
 | 
							transitionDuration: ['responsive'],
 | 
				
			||||||
 | 
							transitionDelay: ['responsive'],
 | 
				
			||||||
 | 
						},
 | 
				
			||||||
 | 
						corePlugins: {},
 | 
				
			||||||
 | 
						plugins: [
 | 
				
			||||||
 | 
							require('tailwindcss-tables')(),
 | 
				
			||||||
 | 
						],
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue