WIP: Working on endpoint for /all in ViewAll.js

master
ALI Hamza 2020-05-28 17:15:25 +07:00
parent b7348afb49
commit 9d6f948c51
35 changed files with 76052 additions and 297 deletions

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

1239
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -6,15 +6,26 @@
"@testing-library/jest-dom": "^4.2.4",
"@testing-library/react": "^9.5.0",
"@testing-library/user-event": "^7.2.1",
"cross-env": "^7.0.2",
"cssnano": "^4.1.10",
"griddle-react": "^1.13.1",
"mathjs": "^7.0.0",
"react": "^16.13.1",
"react-dom": "^16.13.1",
"react-scripts": "3.4.1"
"react-hook-form": "^5.7.2",
"react-router-dom": "^5.2.0",
"react-scripts": "3.4.1",
"react-toastify": "^6.0.4",
"tailwindcss-tables": "^0.4.0"
},
"scripts": {
"dev": "concurrently --kill-others \"npm run watch:css\" \"npm run start\"",
"start": "react-scripts start",
"build": "react-scripts build",
"build": "npm run build:css && react-scripts build",
"test": "react-scripts test",
"eject": "react-scripts eject"
"eject": "react-scripts eject",
"build:css": "cross-env NODE_ENV=production postcss src/styles/tailwind.css -o src/styles/app.css",
"watch:css": "postcss src/styles/tailwind.css -o src/styles/app.css -w"
},
"eslintConfig": {
"extends": "react-app"
@ -30,5 +41,11 @@
"last 1 firefox version",
"last 1 safari version"
]
},
"devDependencies": {
"autoprefixer": "^9.7.6",
"concurrently": "^5.2.0",
"postcss-cli": "^7.1.1",
"tailwindcss": "^1.4.6"
}
}

@ -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,43 +1,20 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<head>
<meta charset="utf-8"/>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico"/>
<meta name="viewport" content="width=device-width, initial-scale=1"/>
<meta name="theme-color" content="#4670de"/>
<meta
name="description"
content="Web site created using create-react-app"
content="RainTrack manages your rainfall data. Start uploading now."
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!--
manifest.json provides metadata used when your web app is installed on a
user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png"/>
<link rel="manifest" href="%PUBLIC_URL%/manifest.json"/>
<title>Rain Track</title>
</head>
<body>
<noscript>Unfortunately, this application requires javascript.</noscript>
<div id="root"></div>
</body>
</html>

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

@ -1,25 +1,60 @@
import React from 'react';
import logo from './logo.svg';
import './App.css';
import React, {useEffect, useReducer} from 'react';
import {BrowserRouter, Route, Switch} from "react-router-dom";
import {Slide, ToastContainer} from "react-toastify";
import 'react-toastify/dist/ReactToastify.min.css'
import './styles/app.css'
import {authenticate, defaultAuth, reducer} from "./auth";
// pages
import Home from "./components/Home";
import Login from "./components/Login";
import Logout from "./components/Logout";
import Register from "./components/Register";
import Upload from "./components/Upload";
import NotFound from "./components/NotFound";
import ViewAll from "./components/ViewAll";
export const AuthContext = React.createContext(undefined)
function App() {
const [authState, authDispatch] = useReducer(reducer, defaultAuth(), () => defaultAuth())
useEffect(() => {
(async () => {
authDispatch({response: await authenticate(authState.token)})
})().then()
}, [authState.token])
return (
<BrowserRouter>
<div className="App">
<header className="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
<AuthContext.Provider value={{authState, authDispatch}}>
<Switch>
<Route path="/login" exact component={Login}/>
<Route path="/logout" exact component={Logout}/>
<Route path="/register" exact component={Register}/>
<Route path="/upload" exact component={Upload}/>
<Route path="/all" exact component={ViewAll}/>
<Route path="/" exact component={Home}/>
<Route path="/" component={NotFound}/>
</Switch>
</AuthContext.Provider>
</div>
<ToastContainer
position="bottom-right"
autoClose={3000}
newestOnTop
closeOnClick
rtl={false}
pauseOnFocusLoss
draggable
transition={Slide}
pauseOnHover={false}
/>
</BrowserRouter>
);
}

@ -1,9 +1,9 @@
import React from 'react';
import { render } from '@testing-library/react';
import {render} from '@testing-library/react';
import App from './App';
test('renders learn react link', () => {
const { getByText } = render(<App />);
const {getByText} = render(<App/>);
const linkElement = getByText(/learn react/i);
expect(linkElement).toBeInTheDocument();
});

@ -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">&nbsp;{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,12 +1,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import App from './App';
import * as serviceWorker from './serviceWorker';
ReactDOM.render(
<React.StrictMode>
<App />
<App/>
</React.StrictMode>,
document.getElementById('root')
);

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

@ -101,7 +101,7 @@ function registerValidSW(swUrl, config) {
function checkValidServiceWorker(swUrl, config) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl, {
headers: { 'Service-Worker': 'script' },
headers: {'Service-Worker': 'script'},
})
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.

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')(),
],
}