first commit

This commit is contained in:
Romulus21
2021-12-17 15:37:05 +01:00
commit 2d49672d20
43 changed files with 39806 additions and 0 deletions

26
src/Contexts.js Normal file
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 626 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

38
src/components/App.js Normal file
View 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
View 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
View 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
View 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
View 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>
}

View 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
View 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
View 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>
}

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

View 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
View 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)
}
*/
}