first commit
26
src/Contexts.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { createContext } from "preact";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import usePlants from "./hooks/PlantsHook";
|
||||
import { useLocalStorage } from "./hooks/LocalStorageHook"
|
||||
import useUser from "./hooks/UserHook";
|
||||
|
||||
export const UserContext = createContext()
|
||||
export const PlantsContext = createContext()
|
||||
|
||||
export default function ContextsProviders({children}) {
|
||||
|
||||
const [data, setData] = useLocalStorage('data', {})
|
||||
const [user, setUser] = useUser(data, setData)
|
||||
const {plants, addPlant, editPlant, removePlant, addAction, doneTask, history} = usePlants(data, setData)
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
console.log('first', user);
|
||||
}, [user])
|
||||
|
||||
return <UserContext.Provider value={[user, setUser]}>
|
||||
<PlantsContext.Provider value={{plants, addPlant, editPlant, removePlant, addAction, doneTask, history}}>
|
||||
{children}
|
||||
</PlantsContext.Provider>
|
||||
</UserContext.Provider>
|
||||
}
|
||||
BIN
src/assets/favicon.ico
Normal file
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/icons/android-chrome-192x192.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
src/assets/icons/android-chrome-512x512.png
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
src/assets/icons/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
src/assets/icons/favicon-16x16.png
Normal file
|
After Width: | Height: | Size: 626 B |
BIN
src/assets/icons/favicon-32x32.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
src/assets/icons/mstile-150x150.png
Normal file
|
After Width: | Height: | Size: 8.8 KiB |
38
src/components/App.js
Normal file
@@ -0,0 +1,38 @@
|
||||
import { h } from 'preact';
|
||||
import { Router } from 'preact-router';
|
||||
import ContextsProviders from '../Contexts';
|
||||
import Header from './Header';
|
||||
|
||||
|
||||
// Code-splitting is automated for `routes` directory
|
||||
import Home from '../routes/Home';
|
||||
import Plant from '../routes/Plant';
|
||||
import Profile from '../routes/Profile';
|
||||
import {useEffect} from "preact/hooks";
|
||||
|
||||
const App = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (!Notification) {
|
||||
alert('Le navigateur ne supporte pas les notifications.');
|
||||
} else if (Notification.permission !== 'granted') {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div id="app" class="h-screen overflow-auto flex flex-col">
|
||||
<ContextsProviders>
|
||||
<Header />
|
||||
<main className="flex-1 dark:bg-gray-800 dark:text-white">
|
||||
<Router>
|
||||
<Home path="/" />
|
||||
<Plant path="plant/:id" />
|
||||
<Profile path="/profile" />
|
||||
</Router>
|
||||
</main>
|
||||
</ContextsProviders>
|
||||
</div>
|
||||
)}
|
||||
|
||||
export default App;
|
||||
10
src/components/Button.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { classNames } from "../utilities/classNames"
|
||||
|
||||
export const Button = ({ children, className = "", type = "text", ...props }) => {
|
||||
|
||||
return (
|
||||
<button type={type} className={classNames("border px-2 py-1 shadow rounded", className)} {...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
59
src/components/Form.js
Normal file
@@ -0,0 +1,59 @@
|
||||
import {classNames} from "../utilities/classNames";
|
||||
|
||||
|
||||
export const InputField = ({children, name, type = "text", ...props}) => {
|
||||
|
||||
const id = props.id ?? name
|
||||
const classStyle = props.className ?? ''
|
||||
|
||||
if (props.className) {
|
||||
delete props.className
|
||||
}
|
||||
|
||||
return <fieldset className={classNames(classStyle)}>
|
||||
{children && <label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{ children }
|
||||
</label>}
|
||||
<input id={id}
|
||||
name={name}
|
||||
type={type}
|
||||
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full px-2 py-1 mt-1 sm:text-sm border border-gray-300 rounded-md dark:bg-gray-500"
|
||||
{...props}/>
|
||||
</fieldset>
|
||||
}
|
||||
|
||||
export const TextAreaField = ({children, name, ...props}) => {
|
||||
|
||||
const id = props.id ?? name
|
||||
const classStyle = props.className ?? ''
|
||||
|
||||
if (props.className) {
|
||||
delete props.className
|
||||
}
|
||||
|
||||
return <fieldset className={classNames(classStyle)}>
|
||||
{children && <label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{ children }
|
||||
</label>}
|
||||
<textarea id={id}
|
||||
name={name}
|
||||
className="focus:ring-indigo-500 focus:border-indigo-500 block w-full px-2 py-1 mt-1 sm:text-sm border border-gray-300 dark:bg-gray-500 rounded-md"
|
||||
{...props}
|
||||
/>
|
||||
</fieldset>
|
||||
}
|
||||
|
||||
export const SelectField = ({children, name, options, className = '', ...props}) => {
|
||||
|
||||
const id = props.id ?? name
|
||||
|
||||
return <fieldset className={className}>
|
||||
{children && <label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
|
||||
{ children }
|
||||
</label>}
|
||||
<select id={id} name={name} className="focus:ring-indigo-500 focus:border-indigo-500 block w-full px-2 py-1 mt-1 sm:text-sm border border-gray-300 dark:bg-gray-500 rounded-md" {...props}>
|
||||
{options.map((option, index) => <option key={index} value={option}
|
||||
className="capitalize">{option}</option>)}
|
||||
</select>
|
||||
</fieldset>
|
||||
}
|
||||
29
src/components/Header.js
Normal file
@@ -0,0 +1,29 @@
|
||||
import { h } from "preact"
|
||||
import { Link } from "preact-router/match"
|
||||
|
||||
const Header = () => {
|
||||
return (
|
||||
<header class="bg-green-700 text-white flex justify-between items-center p-2 text-lg">
|
||||
<Link href="/" class="font-bold text-xl">
|
||||
Plantes
|
||||
</Link>
|
||||
<nav class="flex">
|
||||
<NavLink path="/profile">Me</NavLink>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
}
|
||||
|
||||
export default Header
|
||||
|
||||
const NavLink = ({ path, children }) => {
|
||||
return (
|
||||
<Link
|
||||
href={path}
|
||||
activeClassName="font-bold bg-green-800"
|
||||
class="py-1 px-2 rounded"
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
28
src/components/Modals.js
Normal file
@@ -0,0 +1,28 @@
|
||||
import { classNames } from "../utilities/classNames"
|
||||
|
||||
export const Modal = ({children, isOpen, customClose = false, ...props}) => {
|
||||
|
||||
const handleClose = e => props.onChange(e)
|
||||
|
||||
return <>
|
||||
{isOpen &&
|
||||
<div className="overlay fixed block top-0 bottom-0 left-0 right-0 z-10 bg-gray-800 bg-opacity-80" onClick={handleClose}>
|
||||
<div className="h-full flex justify-center items-center">
|
||||
<div className="flex flex-col bg-white dark:bg-gray-700 dark:text-white rounded p-2 min-h-48 min-w-48" {...props}>
|
||||
<div className="flex-1">
|
||||
{children}
|
||||
</div>
|
||||
{!customClose &&<div className="flex justify-end">
|
||||
<button type="button" className="bg-gray-300 dark:bg-gray-600 hover:bg-gray-400 hover:dark:bg-gray-800 px-2 py-1 rounded shadow close-button" onClick={handleClose}>Close</button>
|
||||
</div>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
export const ModalTitle = ({children, ...props}) => {
|
||||
return <div className="bg-green-700 text-white text-2xl font-bold text-center -mx-2 -mt-2 p-2 rounded-tl rounded-tr" {...props}>{children}</div>
|
||||
}
|
||||
9
src/components/PageLayout.js
Normal file
@@ -0,0 +1,9 @@
|
||||
import { classNames } from "../utilities/classNames";
|
||||
|
||||
|
||||
export const PageLayout = ({children, ...props}) => {
|
||||
|
||||
return <div class={classNames("p-2", props.class ?? "")}>
|
||||
{children}
|
||||
</div>
|
||||
}
|
||||
17
src/components/Plants.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { Link } from "preact-router/match"
|
||||
import {getPicture} from "../utilities/pictures";
|
||||
|
||||
export const PlantThumb = ({ plant, children }) => {
|
||||
|
||||
return <Link href={`/plant/${plant.id}`} class="block h-48">
|
||||
<div className="bg-green-400 relative rounded shadow-lg flex flex-col">
|
||||
<img src={getPicture(plant.id)} alt="" className="object-cover h-48 w-full rounded" />
|
||||
<div className="bg-green-700 text-white p-2 text-center absolute bottom-0 w-full rounded-bl rounded-br">
|
||||
{children}
|
||||
</div>
|
||||
<div title="Actions" className="absolute right-2 top-2 rounded-full flex justify-center items-center bg-green-700 w-6 h-6">
|
||||
{plant.actions.length}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
}
|
||||
27
src/components/Tasks.js
Normal file
@@ -0,0 +1,27 @@
|
||||
import {Button} from "./Button"
|
||||
import {useContext} from "preact/hooks"
|
||||
import {PlantsContext} from "../Contexts"
|
||||
|
||||
export const Tasks = () => {
|
||||
|
||||
const { plants, doneTask, history } = useContext(PlantsContext)
|
||||
|
||||
const taskIsRequired = (action) => {
|
||||
if (history()[action.id]) {
|
||||
let lastTask = new Date(history()[action.id])
|
||||
return lastTask.addDays(Number(action.frequency)) < (new Date())
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return <div className="mb-5">
|
||||
<h1>Tasks</h1>
|
||||
<div>
|
||||
{plants.map(plant => plant.actions.filter(action => taskIsRequired(action)).map(action => <div className="flex items-center gap-2">
|
||||
<span className="capitalize"><b>{plant.name}</b> {action.action_type}</span>
|
||||
<span> every {action.frequency} days {action.id}</span>
|
||||
<span><Button onClick={() => doneTask(action.id)}>Done</Button></span>
|
||||
</div>))}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
26
src/hooks/LocalStorageHook.js
Normal file
@@ -0,0 +1,26 @@
|
||||
import { useState } from "preact/hooks"
|
||||
|
||||
export const useLocalStorage = (key, initialValue) => {
|
||||
const [storedValue, setStoredValue] = useState(() => {
|
||||
try {
|
||||
const item = window.localStorage.getItem(key)
|
||||
return item ? JSON.parse(item) : initialValue
|
||||
} catch (error) {
|
||||
console.warn('localstorageHook', error)
|
||||
return initialValue
|
||||
}
|
||||
})
|
||||
|
||||
const setValue = (value) => {
|
||||
try {
|
||||
const valueToStore =
|
||||
value instanceof Function ? value(storedValue) : value
|
||||
setStoredValue(valueToStore)
|
||||
window.localStorage.setItem(key, JSON.stringify(valueToStore))
|
||||
} catch (error) {
|
||||
console.warn('localstorageHook', error)
|
||||
}
|
||||
}
|
||||
|
||||
return [storedValue, setValue]
|
||||
}
|
||||
76
src/hooks/PlantsHook.js
Normal file
@@ -0,0 +1,76 @@
|
||||
import { useEffect, useState } from "preact/hooks"
|
||||
import {route} from "preact-router";
|
||||
|
||||
const usePlants = (data, setData) => {
|
||||
const [plants, setPlants] = useState([])
|
||||
const [tasks, setTasks] = useState([])
|
||||
|
||||
useEffect(() =>{
|
||||
setPlants(data.plants ?? [])
|
||||
}, [])
|
||||
|
||||
const addPlant = (plantForm) => {
|
||||
let plant = plantForm
|
||||
let maxId = Math.max.apply(Math, plants.map(function(elem) { return elem.id; }))
|
||||
plant.id = maxId === -Infinity ? 1 : maxId + 1
|
||||
plant.actions = []
|
||||
setPlants([...plants, plant])
|
||||
setData({...data, plants: [...plants, plant]})
|
||||
}
|
||||
|
||||
const editPlant = (plant) => {
|
||||
let plantIndex = plants.findIndex(item => item.id === plant.id)
|
||||
plants[plantIndex] = plant
|
||||
savePlants(plants)
|
||||
}
|
||||
|
||||
const removePlant = (plant) => {
|
||||
plants.splice(plants.findIndex(item => item.id === plant.id), 1)
|
||||
savePlants(plants)
|
||||
route('/', true)
|
||||
}
|
||||
|
||||
const addAction = (plant, action) => {
|
||||
action.id = action.action_type + '-' + plant.id
|
||||
let plantIndex = plants.findIndex(item => item.id === plant.id)
|
||||
let actionIndex = plant.actions.findIndex(item => item.action_type === action.action_type)
|
||||
console.log(actionIndex, plantIndex, plant, plants, action)
|
||||
actionIndex >= 0 ? plant.actions[actionIndex] = action
|
||||
: plant.actions.push(action)
|
||||
editPlant(plant)
|
||||
}
|
||||
|
||||
const savePlants = (plants) => {
|
||||
setPlants(plants)
|
||||
setData({...data, plants: plants})
|
||||
}
|
||||
|
||||
const doneTask = (actionId) => {
|
||||
let history = data.history ?? {}
|
||||
history[actionId] = new Date()
|
||||
setData({...data, history: history})
|
||||
notifyMe()
|
||||
}
|
||||
|
||||
const history = () => data.history ?? {}
|
||||
|
||||
const notifyMe = () => {
|
||||
if (!('Notification' in window)) {
|
||||
alert('Ce navigateur ne prend pas en charge la notification de bureau')
|
||||
} else if (Notification.permission === 'granted') {
|
||||
// Si tout va bien, créons une notification
|
||||
const notification = new Notification('Salut toi!')
|
||||
} else if (Notification.permission !== 'denied') {
|
||||
Notification.requestPermission().then((permission) => {
|
||||
// Si l'utilisateur accepte, créons une notification
|
||||
if (permission === 'granted') {
|
||||
const notification = new Notification('Salut toi!')
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return {plants, addPlant, editPlant, removePlant, addAction, doneTask, history}
|
||||
}
|
||||
|
||||
export default usePlants
|
||||
17
src/hooks/UserHook.js
Normal file
@@ -0,0 +1,17 @@
|
||||
import { useEffect, useState } from "preact/hooks"
|
||||
|
||||
|
||||
const useUser = (data, setData) => {
|
||||
const [user, setUser] = useState(data.user ?? {name: "me", dark_mode: false})
|
||||
|
||||
useEffect(() =>{
|
||||
setData({...data, user: user})
|
||||
console.log('in', user);
|
||||
document.querySelector('html').classList.toggle('dark', user.dark_mode)
|
||||
}, [user])
|
||||
|
||||
|
||||
return [user, setUser]
|
||||
}
|
||||
|
||||
export default useUser
|
||||
15
src/index.js
Normal file
@@ -0,0 +1,15 @@
|
||||
import App from './components/App';
|
||||
import './style';
|
||||
|
||||
Date.prototype.addDays = function (days) {
|
||||
let date = new Date(this.valueOf());
|
||||
date.setDate(date.getDate() + days);
|
||||
return date;
|
||||
}
|
||||
|
||||
Date.prototype.toFrDate = function() {
|
||||
let month = ((this.getMonth() + 1 < 10) ? '0' : '') + (this.getMonth() + 1)
|
||||
return `${this.getDate()}/${month}/${this.getFullYear()}`
|
||||
}
|
||||
|
||||
export default App;
|
||||
24
src/manifest.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "plantes",
|
||||
"short_name": "plantes",
|
||||
"start_url": "/",
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#fff",
|
||||
"theme_color": "#673ab8",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/icons/android-chrome-192x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192"
|
||||
},
|
||||
{
|
||||
"src": "/assets/icons/android-chrome-512x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512"
|
||||
}
|
||||
],
|
||||
"_version": "1.1.0",
|
||||
"sap.app": {},
|
||||
"sap.ui": {}
|
||||
}
|
||||
71
src/routes/Home.js
Normal file
@@ -0,0 +1,71 @@
|
||||
import { createPortal } from "preact/compat"
|
||||
import { useContext, useState } from "preact/hooks"
|
||||
import { Button } from "../components/Button"
|
||||
import {Modal, ModalTitle} from "../components/Modals"
|
||||
import { PageLayout } from "../components/PageLayout"
|
||||
import { PlantThumb } from "../components/Plants"
|
||||
import { PlantsContext } from "../Contexts"
|
||||
import {InputField, TextAreaField} from "../components/Form"
|
||||
import {Tasks} from "../components/Tasks"
|
||||
|
||||
export const Home = () => {
|
||||
const [addModal, setAddModal] = useState(false)
|
||||
const [plantForm, setPlantForm] = useState({})
|
||||
const app = document.getElementById("app")
|
||||
const { plants, addPlant } = useContext(PlantsContext)
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
console.log(plantForm)
|
||||
addPlant(plantForm)
|
||||
setAddModal(false)
|
||||
}
|
||||
|
||||
const handleCloseAddModal = (e) => {
|
||||
if (e.target.classList.contains("overlay") ||
|
||||
e.target.classList.contains("close-button")) {
|
||||
setAddModal(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout class="relative">
|
||||
|
||||
<Tasks />
|
||||
|
||||
<h1>Plants</h1>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2">
|
||||
{plants.map((plant) => (
|
||||
<PlantThumb plant={plant}>{plant.name}</PlantThumb>
|
||||
))}
|
||||
</div>
|
||||
<div className="w-full sticky bottom-5 flex justify-end">
|
||||
<div onClick={() => setAddModal(true)}
|
||||
className="rounded-full w-16 h-16 flex items-center justify-center cursor-pointer bg-green-800 hover-bg-green-900 text-white">
|
||||
Add +
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{createPortal(
|
||||
<Modal isOpen={addModal} onChange={handleCloseAddModal}>
|
||||
<ModalTitle>
|
||||
Add Plant
|
||||
</ModalTitle>
|
||||
<form onSubmit={handleSubmit}>
|
||||
|
||||
<InputField name="name" className="mb-2 mt-5" onChange={(e) => setPlantForm({ ...plantForm, name: e.target.value }) }>Name</InputField>
|
||||
<TextAreaField name="description" className="mb-5" onChange={(e) => setPlantForm({ ...plantForm, description: e.target.value })}>Description</TextAreaField>
|
||||
|
||||
<Button type="submit" className="block w-full mt-5 mb-2 bg-green-800 hover:bg-green-900 text-white mx-auto px-2 py-1 shadow">
|
||||
Add
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>,
|
||||
app
|
||||
)}
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home
|
||||
100
src/routes/Plant.js
Normal file
@@ -0,0 +1,100 @@
|
||||
import {createPortal, useRef} from "preact/compat";
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { Button } from "../components/Button";
|
||||
import {Modal, ModalTitle} from "../components/Modals";
|
||||
import { PageLayout } from "../components/PageLayout"
|
||||
import { PlantsContext } from "../Contexts";
|
||||
import {InputField, SelectField} from "../components/Form";
|
||||
import {getPicture, storePicture} from "../utilities/pictures";
|
||||
|
||||
const Plant = ({id}) => {
|
||||
|
||||
const [addModal, setAddModal] = useState(false)
|
||||
const {plants, removePlant, addAction, doneTask, history} = useContext(PlantsContext)
|
||||
const [plant, setPlant] = useState({})
|
||||
const [actionForm, setActionForm] = useState({})
|
||||
const [image, setImage] = useState(localStorage.getItem("image" + id) ?? '')
|
||||
const pictureName = 'picture-' + id
|
||||
const picture = useRef(null)
|
||||
|
||||
const action_types = ['watering', 'spraying', 'bathing']
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
const plantFind = plants.find(plant => plant.id === Number(id))
|
||||
setPlant(plantFind)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setImage(localStorage.getItem(pictureName))
|
||||
}, [picture])
|
||||
|
||||
const handleSubmit = (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!actionForm.action_type) {
|
||||
actionForm.action_type = action_types[0]
|
||||
}
|
||||
console.log("my event", e, actionForm)
|
||||
addAction(plant, actionForm)
|
||||
setAddModal(false)
|
||||
}
|
||||
|
||||
const handleCloseAddModal = (e) => {
|
||||
if (e.target.classList.contains("overlay") || e.target.classList.contains("close-button")) {
|
||||
setAddModal(false)
|
||||
}
|
||||
}
|
||||
|
||||
const addPicture = e => storePicture(document.getElementById("input-file"), id, setImage)
|
||||
|
||||
return <PageLayout>
|
||||
<div className="flex justify-between">
|
||||
<h1>{ plant.name }</h1>
|
||||
<Button onClick={() => removePlant(plant)}>Delete</Button>
|
||||
</div>
|
||||
<p>{ plant.description }</p>
|
||||
<div>
|
||||
<img id="picture" ref={picture} src={getPicture(id)} alt=""/>
|
||||
<img id="output" src="" alt=""/>
|
||||
<input id="input-file" type="file" name="picture" onChange={addPicture}/>
|
||||
</div>
|
||||
<div>
|
||||
<h2>Actions</h2>
|
||||
{plant.actions && plant.actions.map(action => {
|
||||
let isDone = false
|
||||
let lastTask = false
|
||||
if (history()[action.id]) {
|
||||
lastTask = new Date(history()[action.id])
|
||||
isDone = lastTask.addDays(Number(action.frequency)) < (new Date())
|
||||
}
|
||||
console.log(lastTask, isDone)
|
||||
return <div key={action.action_type} className="flex items-center gap-2">
|
||||
<span className="capitalize">{action.action_type}</span>
|
||||
<span>evey {action.frequency} days</span>
|
||||
<span><Button onClick={() => doneTask(action.id)}>Done</Button></span>
|
||||
<span>last task {lastTask ? lastTask.toFrDate() : 'never'}</span>
|
||||
</div>
|
||||
})}
|
||||
<Button className="bg-red-500" onClick={() => setAddModal(true)}>Add/Edit</Button>
|
||||
</div>
|
||||
|
||||
{createPortal(
|
||||
<Modal isOpen={addModal} onChange={handleCloseAddModal}>
|
||||
<ModalTitle>Add Action</ModalTitle>
|
||||
<form onSubmit={handleSubmit}>
|
||||
|
||||
<SelectField name="action_type" className="mb-2 mt-5" options={action_types} onChange={(e) => setActionForm({ ...actionForm, action_type: e.target.value })}>Name</SelectField>
|
||||
<InputField name="frequency" className="mb-5" onChange={(e) => setActionForm({ ...actionForm, frequency: e.target.value })}>Frequency</InputField>
|
||||
|
||||
<Button type="submit" className="block w-full mt-5 mb-2 bg-green-800 hover:bg-green-900 text-white mx-auto px-2 py-1 shadow">
|
||||
Add
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>,
|
||||
app
|
||||
)}
|
||||
</PageLayout>
|
||||
}
|
||||
|
||||
export default Plant
|
||||
24
src/routes/Profile.js
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { PageLayout } from "../components/PageLayout";
|
||||
import { UserContext } from "../Contexts";
|
||||
|
||||
export default function Profile() {
|
||||
const [user, setUser] = useContext(UserContext)
|
||||
const [darkMode, setDarkMode] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
setDarkMode(user.dark_mode)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setUser({...user, dark_mode: darkMode})
|
||||
}, [darkMode])
|
||||
|
||||
return <PageLayout>
|
||||
<h1 className="dark:text-red-600">Profile</h1>
|
||||
<label htmlFor="dark_mode">
|
||||
Dark Mode
|
||||
<input type="checkbox" id="dark_mode" name="dark_mode" value={darkMode} onChange={() => setDarkMode(!darkMode)} />
|
||||
</label>
|
||||
</PageLayout>
|
||||
}
|
||||
13
src/style/index.css
Normal file
@@ -0,0 +1,13 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
@layer base {
|
||||
h1 {
|
||||
@apply text-2xl font-bold mt-5 mb-2;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-xl font-bold mt-5 mb-2;
|
||||
}
|
||||
}
|
||||
55
src/sw.js
Normal file
@@ -0,0 +1,55 @@
|
||||
import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/';
|
||||
|
||||
setupRouting();
|
||||
setupPrecaching(getFiles());
|
||||
|
||||
function wait(ms) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
||||
const BASE = location.protocol + '//' + location.host
|
||||
const PREFIX = "V1"
|
||||
const CACHED_FILES = [
|
||||
`${BASE}/sw.js`,
|
||||
`${BASE}/js/app.js`,
|
||||
`${BASE}/css/app.css`,
|
||||
`${BASE}/offline.html`,
|
||||
]
|
||||
|
||||
self.addEventListener('install', (event) => {
|
||||
self.skipWaiting()
|
||||
event.waitUntil(
|
||||
(async () => {
|
||||
const cache = await caches.open(PREFIX)
|
||||
await cache.addAll(CACHED_FILES)
|
||||
})()
|
||||
)
|
||||
console.log(`${PREFIX} Install`)
|
||||
})
|
||||
|
||||
self.addEventListener('activate', (event) => {
|
||||
clients.claim()
|
||||
event.waitUntil((async() => {
|
||||
const keys = await caches.keys()
|
||||
await Promise.all(
|
||||
keys.map(key => {
|
||||
if (!key.includes(PREFIX)) {
|
||||
return caches.delete(key)
|
||||
}
|
||||
})
|
||||
)
|
||||
})())
|
||||
console.log(`${PREFIX} Activate`)
|
||||
})
|
||||
|
||||
const delay = 1000 * 60 * 60 * 24
|
||||
|
||||
console.log(localStorage.getItem('data'))
|
||||
|
||||
wait(delay)
|
||||
.then(() => {
|
||||
// do thing
|
||||
|
||||
}).catch(err => console.log(err))
|
||||
15
src/template.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><% preact.title %></title>
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<link rel="apple-touch-icon" href="/assets/icons/apple-touch-icon.png">
|
||||
<% preact.headEnd %>
|
||||
</head>
|
||||
<body>
|
||||
<% preact.bodyEnd %>
|
||||
</body>
|
||||
</html>
|
||||
4
src/utilities/classNames.js
Normal file
@@ -0,0 +1,4 @@
|
||||
export function classNames() {
|
||||
// console.log(Object.values(arguments), Object.values(arguments).join(' '));
|
||||
return Object.values(arguments).join(' ')
|
||||
}
|
||||
108
src/utilities/pictures.js
Normal file
@@ -0,0 +1,108 @@
|
||||
|
||||
const pictureName = id => {
|
||||
return 'picture-' + id
|
||||
}
|
||||
|
||||
export const getPicture = id => {
|
||||
return localStorage.getItem('picture-' + id ?? '')
|
||||
}
|
||||
|
||||
export const storePicture = (file, id, setter) => {
|
||||
var filesToUploads = document.getElementById("input-file").files;
|
||||
file = filesToUploads[0];
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
|
||||
reader.onload = function (e) {
|
||||
console.log(e)
|
||||
var img = document.createElement("img");
|
||||
img.src = e.target.result;
|
||||
|
||||
// Create your canvas
|
||||
var canvas = document.createElement("canvas");
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
var MAX_WIDTH = 500;
|
||||
var MAX_HEIGHT = 500;
|
||||
var width = img.width;
|
||||
var height = img.height;
|
||||
|
||||
// Add the resizing logic
|
||||
if (width > height) {
|
||||
if (width > MAX_WIDTH) {
|
||||
height *= MAX_WIDTH / width;
|
||||
width = MAX_WIDTH;
|
||||
}
|
||||
} else {
|
||||
if (height > MAX_HEIGHT) {
|
||||
width *= MAX_HEIGHT / height;
|
||||
height = MAX_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
//Specify the resizing result
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
let ctx2 = canvas.getContext("2d");
|
||||
ctx2.drawImage(img, 0, 0, width, height);
|
||||
console.log(img, ctx2)
|
||||
let dataurl = canvas.toDataURL(file.type);
|
||||
|
||||
console.log(dataurl)
|
||||
localStorage.setItem(pictureName(id), dataurl)
|
||||
setter(localStorage.getItem(pictureName(id)))
|
||||
document.getElementById("output").src = dataurl;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
/*
|
||||
reader.addEventListener("load", function () {
|
||||
|
||||
let img = document.createElement("img");
|
||||
img.src = reader.result
|
||||
|
||||
var canvas = document.createElement("canvas");
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
|
||||
var MAX_WIDTH = 500;
|
||||
var MAX_HEIGHT = 500;
|
||||
var width = img.width;
|
||||
var height = img.height;
|
||||
|
||||
// Add the resizing logic
|
||||
if (width > height) {
|
||||
if (width > MAX_WIDTH) {
|
||||
height *= MAX_WIDTH / width;
|
||||
width = MAX_WIDTH;
|
||||
}
|
||||
} else {
|
||||
if (height > MAX_HEIGHT) {
|
||||
width *= MAX_HEIGHT / height;
|
||||
height = MAX_HEIGHT;
|
||||
}
|
||||
}
|
||||
|
||||
//Specify the resizing result
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
|
||||
let dataurl = canvas.toDataURL(file.type);
|
||||
console.log(dataurl)
|
||||
// document.getElementById('picture').src = dataurl;
|
||||
|
||||
|
||||
localStorage.setItem(pictureName(id), dataurl)
|
||||
// localStorage.setItem(pictureName(id), reader.result)
|
||||
setter(localStorage.getItem(pictureName(id)))
|
||||
}, false);
|
||||
|
||||
if (imgPath) {
|
||||
reader.readAsDataURL(imgPath)
|
||||
}
|
||||
|
||||
*/
|
||||
}
|
||||