Compare commits
10 Commits
7abf0bafa5
...
8223211d6a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8223211d6a | ||
|
|
0b95d65898 | ||
|
|
62cb27c85e | ||
|
|
021848b543 | ||
|
|
4e0fa8d0e5 | ||
|
|
413f47cb28 | ||
|
|
f35944752e | ||
|
|
dedda0157d | ||
|
|
05f1d7cbca | ||
|
|
e9a4550070 |
28
.eslintrc.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"env": {
|
||||
"browser": true,
|
||||
"jest/globals": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
},
|
||||
"ecmaVersion": 12,
|
||||
"sourceType": "module"
|
||||
},
|
||||
"plugins": [
|
||||
"jest"
|
||||
],
|
||||
"rules": {
|
||||
"jest/no-disabled-tests": "warn",
|
||||
"jest/no-focused-tests": "error",
|
||||
"jest/no-identical-title": "error",
|
||||
"jest/prefer-to-have-length": "warn",
|
||||
"jest/valid-expect": "error",
|
||||
"no-restricted-syntax": ["warn", {
|
||||
"selector": "CallExpression[callee.object.name='console'][callee.property.name!=/^(warn|info)$/]",
|
||||
"message": "Unexpected property on console object was called"
|
||||
}],
|
||||
"semi": [2, "never"]
|
||||
}
|
||||
}
|
||||
2
.gitignore
vendored
@@ -2,3 +2,5 @@ node_modules
|
||||
/build
|
||||
/*.log
|
||||
/.idea
|
||||
|
||||
deploy.sh
|
||||
6
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -1,6 +0,0 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<profile version="1.0">
|
||||
<option name="myName" value="Project Default" />
|
||||
<inspection_tool class="Eslint" enabled="true" level="WARNING" enabled_by_default="true" />
|
||||
</profile>
|
||||
</component>
|
||||
6
.prettierrc.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"bracketSameLine": true,
|
||||
"tabWidth": 4,
|
||||
"semi": false,
|
||||
"singleQuote": true
|
||||
}
|
||||
10408
package-lock.json
generated
27
package.json
@@ -6,19 +6,40 @@
|
||||
"scripts": {
|
||||
"build": "preact build",
|
||||
"serve": "sirv build --port 8080 --cors --single",
|
||||
"dev": "preact watch"
|
||||
"dev": "preact watch",
|
||||
"lint": "eslint src",
|
||||
"test": "jest"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": "preact",
|
||||
"ignorePatterns": [
|
||||
"build/"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"enzyme": "^3.10.0",
|
||||
"enzyme-adapter-preact-pure": "^2.0.0",
|
||||
"eslint": "^6.0.1",
|
||||
"eslint-config-preact": "^1.1.0",
|
||||
"eslint-plugin-jest": "^25.7.0",
|
||||
"jest": "^24.9.0",
|
||||
"jest-preset-preact": "^1.0.0",
|
||||
"preact-cli": "^3.0.0",
|
||||
"preact-cli-tailwind": "^3.0.0",
|
||||
"sirv-cli": "1.0.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"preact": "^10.3.2",
|
||||
"preact-cli-tailwind": "^3.0.0",
|
||||
"preact-render-to-string": "^5.1.4",
|
||||
"preact-router": "^3.2.1",
|
||||
"tailwindcss": "^2.2.19"
|
||||
"prettier": "^2.5.1",
|
||||
"prettier-plugin-tailwindcss": "^0.1.5"
|
||||
},
|
||||
"jest": {
|
||||
"preset": "jest-preset-preact",
|
||||
"setupFiles": [
|
||||
"<rootDir>/tests/__mocks__/browserMocks.js",
|
||||
"<rootDir>/tests/__mocks__/setupTests.js"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
1
size-plugin.json
Normal file
@@ -1,26 +1,43 @@
|
||||
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";
|
||||
import { createContext } from 'preact'
|
||||
import usePlants from './hooks/PlantsHook'
|
||||
import { useLocalStorage } from './hooks/LocalStorageHook'
|
||||
import useUser from './hooks/UserHook'
|
||||
import { TranslateProvider } from './components/Translation'
|
||||
|
||||
export const UserContext = createContext()
|
||||
export const PlantsContext = createContext()
|
||||
|
||||
export default function ContextsProviders({children}) {
|
||||
export const UserContext = createContext(null)
|
||||
export const PlantsContext = createContext(null)
|
||||
|
||||
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)
|
||||
const {
|
||||
plants,
|
||||
addPlant,
|
||||
editPlant,
|
||||
removePlant,
|
||||
addAction,
|
||||
doneTask,
|
||||
history,
|
||||
archivedEntries,
|
||||
} = 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}}>
|
||||
return (
|
||||
<TranslateProvider>
|
||||
<UserContext.Provider value={[user, setUser]}>
|
||||
<PlantsContext.Provider
|
||||
value={{
|
||||
plants,
|
||||
addPlant,
|
||||
editPlant,
|
||||
removePlant,
|
||||
addAction,
|
||||
doneTask,
|
||||
history,
|
||||
archivedEntries,
|
||||
}}>
|
||||
{children}
|
||||
</PlantsContext.Provider>
|
||||
</UserContext.Provider>
|
||||
</TranslateProvider>
|
||||
)
|
||||
}
|
||||
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 9.4 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 9.9 KiB |
|
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 9.2 KiB |
|
Before Width: | Height: | Size: 626 B After Width: | Height: | Size: 620 B |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 8.8 KiB After Width: | Height: | Size: 7.6 KiB |
@@ -1,27 +1,17 @@
|
||||
import { h } from 'preact';
|
||||
import { Router } from 'preact-router';
|
||||
import ContextsProviders from '../Contexts';
|
||||
import Header from './Header';
|
||||
|
||||
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";
|
||||
import Home from '../routes/Home'
|
||||
import Plant from '../routes/Plant'
|
||||
import Profile from '../routes/Profile'
|
||||
import Style from '../routes/Style'
|
||||
|
||||
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">
|
||||
<div id="app" className="flex h-screen flex-col overflow-auto">
|
||||
<ContextsProviders>
|
||||
<Header />
|
||||
<main className="flex-1 dark:bg-gray-800 dark:text-white">
|
||||
@@ -29,10 +19,12 @@ const App = () => {
|
||||
<Home path="/" />
|
||||
<Plant path="plant/:id" />
|
||||
<Profile path="/profile" />
|
||||
<Style path="/style" />
|
||||
</Router>
|
||||
</main>
|
||||
</ContextsProviders>
|
||||
</div>
|
||||
)}
|
||||
)
|
||||
}
|
||||
|
||||
export default App;
|
||||
export default App
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import { classNames } from "../utilities/classNames"
|
||||
|
||||
export const Button = ({ children, className = "", type = "text", ...props }) => {
|
||||
|
||||
export const Button = ({
|
||||
children,
|
||||
className = '',
|
||||
type = 'button',
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<button type={type} className={classNames("border px-2 py-1 shadow transition-all duration-300 rounded", className)} {...props}>
|
||||
<button
|
||||
type={type}
|
||||
className={`ring-offset rounded border px-2 py-1 shadow transition-all duration-300 hover:ring-2 focus:ring-2 disabled:cursor-not-allowed disabled:opacity-50 ${className}`}
|
||||
{...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
}
|
||||
|
||||
export const SmallButton = ({ children, className = "", type = "button", ...props }) => {
|
||||
|
||||
export const SmallButton = ({
|
||||
children,
|
||||
className = '',
|
||||
type = 'button',
|
||||
...props
|
||||
}) => {
|
||||
return (
|
||||
<button type={type} className={classNames("border border-white flex items-center justify-center w-6 h-6 shadow transition-all duration-300 rounded-full cursor-pointer", className)} {...props}>
|
||||
<button
|
||||
type={type}
|
||||
className={`flex h-6 w-6 cursor-pointer items-center justify-center rounded-full border border-white shadow transition-all duration-300 ${className}`}
|
||||
{...props}>
|
||||
{children}
|
||||
</button>
|
||||
)
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import {classNames} from "../utilities/classNames";
|
||||
|
||||
|
||||
export const InputField = ({children, name, type = "text", ...props}) => {
|
||||
import { Text } from './Translation'
|
||||
|
||||
export const InputField = ({
|
||||
children,
|
||||
name,
|
||||
value = '',
|
||||
type = 'text',
|
||||
textSupport = '',
|
||||
...props
|
||||
}) => {
|
||||
const id = props.id ?? name
|
||||
const classStyle = props.className ?? ''
|
||||
|
||||
@@ -10,20 +15,31 @@ export const InputField = ({children, name, type = "text", ...props}) => {
|
||||
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}
|
||||
return (
|
||||
<fieldset className={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}/>
|
||||
value={value}
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-2 py-1 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-500 sm:text-sm"
|
||||
{...props}
|
||||
/>
|
||||
<span className="text-sm text-gray-500">
|
||||
<Text text={textSupport} />
|
||||
</span>
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
|
||||
export const TextAreaField = ({children, name, ...props}) => {
|
||||
|
||||
export const TextAreaField = ({ children, name, ...props }) => {
|
||||
const id = props.id ?? name
|
||||
const classStyle = props.className ?? ''
|
||||
|
||||
@@ -31,29 +47,54 @@ export const TextAreaField = ({children, name, ...props}) => {
|
||||
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}
|
||||
return (
|
||||
<fieldset className={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"
|
||||
className="mt-1 block w-full rounded-md border border-gray-300 px-2 py-1 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-500 sm:text-sm"
|
||||
{...props}
|
||||
/>
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
|
||||
export const SelectField = ({children, name, options, className = '', ...props}) => {
|
||||
|
||||
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>)}
|
||||
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="mt-1 block w-full rounded-md border border-gray-300 px-2 py-1 focus:border-indigo-500 focus:ring-indigo-500 dark:bg-gray-500 sm:text-sm"
|
||||
{...props}>
|
||||
{options.map((option, index) => (
|
||||
<option key={index} value={option} className="capitalize">
|
||||
<Text text={option} />
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</fieldset>
|
||||
)
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
import { h } from "preact"
|
||||
import { Link } from "preact-router/match"
|
||||
import { Link } from 'preact-router/match'
|
||||
import { Text } from './Translation'
|
||||
|
||||
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
|
||||
<header className="flex items-center justify-between bg-primary p-2 text-lg text-white">
|
||||
<Link href="/" class="cursor-pointer text-xl font-bold">
|
||||
<Text text="Plantes" />
|
||||
</Link>
|
||||
<nav class="flex">
|
||||
<NavLink path="/profile">Me</NavLink>
|
||||
<nav className="flex gap-2">
|
||||
<NavLink path="/style">
|
||||
<Text text="Style" />
|
||||
</NavLink>
|
||||
<NavLink path="/profile">
|
||||
<Text text="Me" />
|
||||
</NavLink>
|
||||
</nav>
|
||||
</header>
|
||||
)
|
||||
@@ -20,9 +25,8 @@ const NavLink = ({ path, children }) => {
|
||||
return (
|
||||
<Link
|
||||
href={path}
|
||||
activeClassName="font-bold bg-green-800"
|
||||
class="py-1 px-2 rounded"
|
||||
>
|
||||
activeClassName="font-bold bg-primary-dark"
|
||||
class="cursor-pointer rounded py-1 px-2">
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
|
||||
@@ -1,28 +1,52 @@
|
||||
import { classNames } from "../utilities/classNames"
|
||||
import { Text } from './Translation'
|
||||
|
||||
export const Modal = ({children, isOpen, customClose = false, ...props}) => {
|
||||
export const Modal = ({ children, isOpen, customClose = false, ...props }) => {
|
||||
const handleClose = (e) => props.onChange(e)
|
||||
|
||||
const handleClose = e => props.onChange(e)
|
||||
return (
|
||||
<>
|
||||
{isOpen && (
|
||||
<div
|
||||
className="overlay fixed top-0 bottom-0 left-0 right-0 z-10 block bg-gray-800 bg-opacity-80"
|
||||
onClick={handleClose}>
|
||||
<div className="flex h-full items-center justify-center">
|
||||
<div
|
||||
className="flex min-h-48 min-w-48 flex-col rounded bg-white p-2 dark:bg-gray-700 dark:text-white"
|
||||
{...props}>
|
||||
<div className="flex-1">{children}</div>
|
||||
{!customClose && (
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
className="close-button rounded bg-gray-300 px-2 py-1 shadow hover:bg-gray-400 dark:bg-gray-600 hover:dark:bg-gray-800"
|
||||
onClick={handleClose}>
|
||||
<Text text="Close" />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
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">
|
||||
export const ModalTitle = ({ children, ...props }) => {
|
||||
return (
|
||||
<div
|
||||
className="-mx-2 -mt-2 rounded-tl rounded-tr bg-green-700 p-2 text-center text-2xl font-bold text-white"
|
||||
{...props}>
|
||||
{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 closeModal = (e, setter) => {
|
||||
if (
|
||||
e.target.classList.contains('overlay') ||
|
||||
e.target.classList.contains('close-button')
|
||||
) {
|
||||
setter(false)
|
||||
}
|
||||
</>
|
||||
}
|
||||
|
||||
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>
|
||||
}
|
||||
|
||||
@@ -1,9 +1,3 @@
|
||||
import { classNames } from "../utilities/classNames";
|
||||
|
||||
|
||||
export const PageLayout = ({children, ...props}) => {
|
||||
|
||||
return <div class={classNames("p-2", props.class ?? "")}>
|
||||
{children}
|
||||
</div>
|
||||
export const PageLayout = ({ children, ...props }) => {
|
||||
return <div className={`p-2 ${props.class ?? ''}`}>{children}</div>
|
||||
}
|
||||
@@ -1,17 +1,108 @@
|
||||
import { Link } from "preact-router/match"
|
||||
import {getPicture} from "../utilities/pictures";
|
||||
import { Link } from "preact-router/match";
|
||||
import { getPicture } from "../utilities/pictures";
|
||||
import { InputField, TextAreaField } from "./Form";
|
||||
import { Button } from "./Button";
|
||||
import { useState } from "preact/hooks";
|
||||
import { Text } from "./Translation";
|
||||
|
||||
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">
|
||||
return (
|
||||
<Link href={`/plant/${plant.id}`} class="block h-48 cursor-pointer">
|
||||
<div className="group relative flex flex-col rounded bg-primary-light shadow-lg">
|
||||
<img
|
||||
src={getPicture(plant.id)}
|
||||
alt=""
|
||||
className="h-48 min-h-48 w-full min-w-48 rounded object-cover"
|
||||
/>
|
||||
<div className="absolute bottom-0 w-full rounded-bl rounded-br bg-primary p-2 text-center text-white group-hover:bg-primary-dark">
|
||||
{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">
|
||||
<div
|
||||
title="Actions"
|
||||
className="absolute right-2 top-2 flex h-6 w-6 items-center justify-center rounded-full bg-primary group-hover:bg-primary-dark"
|
||||
>
|
||||
{plant.actions.length}
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
export const PlantForm = ({ children, plant, ...props }) => {
|
||||
const [plantForm, setPlantForm] = useState(plant);
|
||||
|
||||
return (
|
||||
<form onSubmit={(e) => props.onChange(e, plantForm)}>
|
||||
<InputField
|
||||
name="name"
|
||||
className="mb-2 mt-5"
|
||||
value={plantForm.name}
|
||||
onChange={(e) => setPlantForm({ ...plantForm, name: e.target.value })}
|
||||
>
|
||||
<Text text="Name" />
|
||||
</InputField>
|
||||
<TextAreaField
|
||||
name="description"
|
||||
className="mb-5"
|
||||
value={plantForm.description}
|
||||
onChange={(e) =>
|
||||
setPlantForm({ ...plantForm, description: e.target.value })
|
||||
}
|
||||
>
|
||||
<Text text="Description" />
|
||||
</TextAreaField>
|
||||
|
||||
<fieldset className="mx-auto mb-5 flex max-w-[300px] gap-2">
|
||||
<div className="flex-1">
|
||||
<input
|
||||
id="indoor"
|
||||
name="indoor"
|
||||
type="radio"
|
||||
checked={plantForm.indoor === true}
|
||||
onChange={() => setPlantForm({ ...plantForm, indoor: true })}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<label
|
||||
htmlFor="indoor"
|
||||
className="block w-full rounded border bg-blue-500 px-2 py-1 text-center hover:bg-blue-700 peer-checked:bg-blue-700 dark:border-white"
|
||||
>
|
||||
<Text text="Indoor" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div className="flex-1">
|
||||
<input
|
||||
id="outdoor"
|
||||
name="outdoor"
|
||||
type="radio"
|
||||
checked={plantForm.indoor === false}
|
||||
onChange={() => setPlantForm({ ...plantForm, indoor: false })}
|
||||
className="peer sr-only"
|
||||
/>
|
||||
<label
|
||||
htmlFor="outdoor"
|
||||
className="block w-full rounded border bg-blue-500 px-2 py-1 text-center hover:bg-blue-700 peer-checked:bg-blue-700 dark:border-white"
|
||||
>
|
||||
<Text text="Outdoor" />
|
||||
</label>
|
||||
</div>
|
||||
</fieldset>
|
||||
|
||||
<InputField
|
||||
name="spot"
|
||||
className="mt-5"
|
||||
value={plantForm.spot}
|
||||
onChange={(e) => setPlantForm({ ...plantForm, spot: e.target.value })}
|
||||
>
|
||||
<Text text="Spot" />
|
||||
</InputField>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="mx-auto mt-5 mb-2 block w-full bg-green-800 px-2 py-1 text-white shadow hover:bg-green-900"
|
||||
>
|
||||
{children}
|
||||
</Button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,23 +1,40 @@
|
||||
|
||||
const SVGSkeleton = ({paths, viewBox = "0 0 512 512", ...props}) => {
|
||||
return <svg xmlns="http://www.w3.org/2000/svg"
|
||||
const SVGSkeleton = ({ paths, viewBox = '0 0 512 512', ...props }) => {
|
||||
return (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox={viewBox}
|
||||
fill="currentColor"
|
||||
className={props.className ?? ''}
|
||||
{...props}>
|
||||
{props.title && <title>{ props.title }</title>}
|
||||
{props.title && <title>{props.title}</title>}
|
||||
{paths}
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export const PlusSVG = (props) => SVGSkeleton({
|
||||
viewBox: "0 0 448 512",
|
||||
paths: <path d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z" />,
|
||||
...props
|
||||
})
|
||||
export const PlusSVG = (props) =>
|
||||
SVGSkeleton({
|
||||
viewBox: '0 0 448 512',
|
||||
paths: (
|
||||
<path d="M416 208H272V64c0-17.67-14.33-32-32-32h-32c-17.67 0-32 14.33-32 32v144H32c-17.67 0-32 14.33-32 32v32c0 17.67 14.33 32 32 32h144v144c0 17.67 14.33 32 32 32h32c17.67 0 32-14.33 32-32V304h144c17.67 0 32-14.33 32-32v-32c0-17.67-14.33-32-32-32z" />
|
||||
),
|
||||
...props,
|
||||
})
|
||||
|
||||
export const EditSVG = (props) => SVGSkeleton({
|
||||
viewBox: "0 0 576 512",
|
||||
paths: <path d="M402.3 344.9l32-32c5-5 13.7-1.5 13.7 5.7V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h273.5c7.1 0 10.7 8.6 5.7 13.7l-32 32c-1.5 1.5-3.5 2.3-5.7 2.3H48v352h352V350.5c0-2.1.8-4.1 2.3-5.6zm156.6-201.8L296.3 405.7l-90.4 10c-26.2 2.9-48.5-19.2-45.6-45.6l10-90.4L432.9 17.1c22.9-22.9 59.9-22.9 82.7 0l43.2 43.2c22.9 22.9 22.9 60 .1 82.8zM460.1 174L402 115.9 216.2 301.8l-7.3 65.3 65.3-7.3L460.1 174zm64.8-79.7l-43.2-43.2c-4.1-4.1-10.8-4.1-14.8 0L436 82l58.1 58.1 30.9-30.9c4-4.2 4-10.8-.1-14.9z" />,
|
||||
...props
|
||||
})
|
||||
export const EditSVG = (props) =>
|
||||
SVGSkeleton({
|
||||
viewBox: '0 0 576 512',
|
||||
paths: (
|
||||
<path d="M402.3 344.9l32-32c5-5 13.7-1.5 13.7 5.7V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h273.5c7.1 0 10.7 8.6 5.7 13.7l-32 32c-1.5 1.5-3.5 2.3-5.7 2.3H48v352h352V350.5c0-2.1.8-4.1 2.3-5.6zm156.6-201.8L296.3 405.7l-90.4 10c-26.2 2.9-48.5-19.2-45.6-45.6l10-90.4L432.9 17.1c22.9-22.9 59.9-22.9 82.7 0l43.2 43.2c22.9 22.9 22.9 60 .1 82.8zM460.1 174L402 115.9 216.2 301.8l-7.3 65.3 65.3-7.3L460.1 174zm64.8-79.7l-43.2-43.2c-4.1-4.1-10.8-4.1-14.8 0L436 82l58.1 58.1 30.9-30.9c4-4.2 4-10.8-.1-14.9z" />
|
||||
),
|
||||
...props,
|
||||
})
|
||||
|
||||
export const TriangleSVG = (props) =>
|
||||
SVGSkeleton({
|
||||
viewBox: '0 0 320 512',
|
||||
paths: (
|
||||
<path d="M288.662 352H31.338c-17.818 0-26.741-21.543-14.142-34.142l128.662-128.662c7.81-7.81 20.474-7.81 28.284 0l128.662 128.662c12.6 12.599 3.676 34.142-14.142 34.142z" />
|
||||
),
|
||||
...props,
|
||||
})
|
||||
|
||||
@@ -1,27 +1,85 @@
|
||||
import {Button} from "./Button"
|
||||
import {useContext} from "preact/hooks"
|
||||
import {PlantsContext} from "../Contexts"
|
||||
import { Button } from './Button'
|
||||
import { Link } from 'preact-router/match'
|
||||
import { useContext } from 'preact/hooks'
|
||||
import { PlantsContext } from '../Contexts'
|
||||
import { Text } from './Translation'
|
||||
|
||||
export const Tasks = () => {
|
||||
|
||||
const { plants, doneTask, history } = useContext(PlantsContext)
|
||||
|
||||
const taskIsRequired = (action) => {
|
||||
if (Number(action.frequency) === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
if (history()[action.id]) {
|
||||
let lastTask = new Date(history()[action.id])
|
||||
return lastTask.addDays(Number(action.frequency)) < (new Date())
|
||||
return (
|
||||
lastTask.addDays(Number(action.frequency)).toSQLDate() <
|
||||
new Date().toSQLDate()
|
||||
)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
return <div className="mb-5">
|
||||
<h1>Tasks</h1>
|
||||
const taskDelay = (action) => {
|
||||
if (history()[action.id]) {
|
||||
let lastTask = new Date(history()[action.id])
|
||||
const oneDay = 1000 * 60 * 60 * 24
|
||||
return Math.round((new Date() - lastTask) / oneDay)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-5">
|
||||
<h1>
|
||||
<Text text="Tasks" />
|
||||
</h1>
|
||||
<div>
|
||||
{plants.map(plant => plant.actions.filter(action => taskIsRequired(action)).map(action => <div className="flex items-center gap-2 my-2">
|
||||
<span><Button className="bg-blue-500 hover:bg-blue-700" onClick={() => doneTask(action.id)}>Done</Button></span>
|
||||
<span className="capitalize"><b>{plant.name}</b> {action.action_type}</span>
|
||||
<span> every {action.frequency} days</span>
|
||||
</div>))}
|
||||
{plants.map((plant) =>
|
||||
plant.actions
|
||||
.filter((action) => taskIsRequired(action))
|
||||
.map((action) => (
|
||||
<div className="my-2 flex items-center gap-2">
|
||||
<span>
|
||||
<Button
|
||||
className="bg-blue-500 hover:bg-blue-700"
|
||||
onClick={() => doneTask(action.id)}>
|
||||
<Text text="Done" />
|
||||
</Button>
|
||||
</span>
|
||||
<span className="capitalize">
|
||||
<Link
|
||||
href={`/plant/${plant.id}`}
|
||||
class="mr-2 font-bold">
|
||||
{plant.name}
|
||||
</Link>
|
||||
<Text text={action.action_type} />
|
||||
</span>
|
||||
{Number(action.frequency) === 0 ? (
|
||||
<span>
|
||||
<Text text="when you want" />
|
||||
</span>
|
||||
) : taskDelay(action) > 1 ? (
|
||||
<span>
|
||||
<Text
|
||||
text="since {{count}} days"
|
||||
count={taskDelay(action)}
|
||||
/>
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Text
|
||||
text="every {{count}} days"
|
||||
count={action.frequency}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
48
src/components/Translation.js
Normal file
@@ -0,0 +1,48 @@
|
||||
import en from '../lang/en.json'
|
||||
import fr from '../lang/fr.json'
|
||||
import { useContext, useEffect, useState } from 'preact/hooks'
|
||||
import { createContext } from 'preact'
|
||||
|
||||
export const TranslateContext = createContext(null)
|
||||
|
||||
export const Text = ({ text, count = null }) => {
|
||||
const lang = useContext(TranslateContext)
|
||||
const [translate, setTranslate] = useState()
|
||||
|
||||
useEffect(() => {
|
||||
if (lang && lang[text]) {
|
||||
if (count > 1 && lang[text].many) {
|
||||
text = lang[text].many
|
||||
} else if (count <= 1 && lang[text].one) {
|
||||
text = lang[text].one
|
||||
} else {
|
||||
text = lang[text]
|
||||
}
|
||||
}
|
||||
|
||||
if (count && text.includes('{{count}}')) {
|
||||
text = text.replace('{{count}}', count)
|
||||
}
|
||||
setTranslate(text)
|
||||
}, [])
|
||||
|
||||
return <>{translate}</>
|
||||
}
|
||||
|
||||
export const TranslateProvider = ({ children }) => {
|
||||
let translate = null
|
||||
if (typeof navigator !== 'undefined') {
|
||||
const userLang = navigator.language || navigator.userLanguage
|
||||
if (userLang === 'en') {
|
||||
translate = en
|
||||
} else if (userLang === 'fr') {
|
||||
translate = fr
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TranslateContext.Provider value={translate}>
|
||||
{children}
|
||||
</TranslateContext.Provider>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState } from "preact/hooks"
|
||||
import { useState } from 'preact/hooks'
|
||||
|
||||
export const useLocalStorage = (key, initialValue) => {
|
||||
const [storedValue, setStoredValue] = useState(() => {
|
||||
|
||||
@@ -1,76 +1,90 @@
|
||||
import { useEffect, useState } from "preact/hooks"
|
||||
import {route} from "preact-router";
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
import { route } from 'preact-router'
|
||||
import { actionId } from '../utilities/actions'
|
||||
|
||||
const usePlants = (data, setData) => {
|
||||
const [plants, setPlants] = useState([])
|
||||
const [tasks, setTasks] = useState([])
|
||||
|
||||
useEffect(() =>{
|
||||
useEffect(() => {
|
||||
setPlants(data.plants ?? [])
|
||||
}, [])
|
||||
|
||||
const addPlant = (plantForm) => {
|
||||
let plant = plantForm
|
||||
let maxId = Math.max.apply(Math, plants.map(function(elem) { return elem.id; }))
|
||||
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]})
|
||||
setData({ ...data, plants: [...plants, plant] })
|
||||
}
|
||||
|
||||
const editPlant = (plant) => {
|
||||
let plantIndex = plants.findIndex(item => item.id === plant.id)
|
||||
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)
|
||||
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
|
||||
let actionIndex = plant.actions.findIndex(
|
||||
(item) => item.action_type === action.action_type
|
||||
)
|
||||
actionIndex >= 0
|
||||
? (plant.actions[actionIndex] = action)
|
||||
: plant.actions.push(action)
|
||||
editPlant(plant)
|
||||
}
|
||||
|
||||
const savePlants = (plants) => {
|
||||
setPlants(plants)
|
||||
setData({...data, plants: plants})
|
||||
setData({ ...data, plants: plants })
|
||||
}
|
||||
|
||||
const doneTask = (actionId) => {
|
||||
const doneTask = (action) => {
|
||||
let history = data.history ?? {}
|
||||
history[actionId] = new Date()
|
||||
setData({...data, history: history})
|
||||
notifyMe()
|
||||
history[action] = new Date()
|
||||
|
||||
let archived = data.archived ?? []
|
||||
archived.push({
|
||||
plantId: Number(action.split('-')[1]),
|
||||
action: actionId(action.split('-')[0]),
|
||||
time: new Date().toSQLDate(),
|
||||
})
|
||||
|
||||
setData({ ...data, history: history, archived: archived })
|
||||
}
|
||||
|
||||
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!')
|
||||
}
|
||||
})
|
||||
}
|
||||
const archivedEntries = (plantId) => {
|
||||
let archived = data.archived ?? []
|
||||
return archived.where('plantId', plantId)
|
||||
}
|
||||
|
||||
return {plants, addPlant, editPlant, removePlant, addAction, doneTask, history}
|
||||
return {
|
||||
plants,
|
||||
addPlant,
|
||||
editPlant,
|
||||
removePlant,
|
||||
addAction,
|
||||
doneTask,
|
||||
history,
|
||||
archivedEntries,
|
||||
}
|
||||
}
|
||||
|
||||
export default usePlants
|
||||
@@ -1,16 +1,15 @@
|
||||
import { useEffect, useState } from "preact/hooks"
|
||||
|
||||
import { useEffect, useState } from 'preact/hooks'
|
||||
|
||||
const useUser = (data, setData) => {
|
||||
const [user, setUser] = useState(data.user ?? {name: "me", dark_mode: false})
|
||||
const [user, setUser] = useState(
|
||||
data.user ?? { name: 'me', dark_mode: false }
|
||||
)
|
||||
|
||||
useEffect(() =>{
|
||||
setData({...data, user: user})
|
||||
console.log('in', user);
|
||||
useEffect(() => {
|
||||
setData({ ...data, user: user })
|
||||
document.querySelector('html').classList.toggle('dark', user.dark_mode)
|
||||
}, [user])
|
||||
|
||||
|
||||
return [user, setUser]
|
||||
}
|
||||
|
||||
|
||||
51
src/icon.svg
Normal file
|
After Width: | Height: | Size: 9.7 KiB |
50
src/index.js
@@ -1,15 +1,47 @@
|
||||
import App from './components/App';
|
||||
import './style';
|
||||
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;
|
||||
Number.prototype.pad = function (n) {
|
||||
return (new Array(n).join('0') + this).slice(-n)
|
||||
}
|
||||
|
||||
Date.prototype.toFrDate = function() {
|
||||
let month = ((this.getMonth() + 1 < 10) ? '0' : '') + (this.getMonth() + 1)
|
||||
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;
|
||||
Date.prototype.toSQLDate = function () {
|
||||
return `${this.getFullYear()}-${Number(this.getMonth() + 1).pad(
|
||||
2
|
||||
)}-${Number(this.getDate()).pad(2)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter array on a field common of the objects
|
||||
* @param {string} field to filter
|
||||
* @param {string} search to filter by
|
||||
* @returns {*[]}
|
||||
*/
|
||||
Array.prototype.where = function (field, search) {
|
||||
return this.filter((item) => item[field] === search)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort Array of Objects by a specific field
|
||||
* @param {string} field to sort
|
||||
* @param {boolean} desc way to sort
|
||||
* @returns {Array}
|
||||
*/
|
||||
Array.prototype.sortObjectsBy = function (field, desc = false) {
|
||||
return desc
|
||||
? this.sort((a, b) => a[field] < b[field])
|
||||
: this.sort((a, b) => a[field] > b[field])
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
12
src/lang/en.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"Add": "+",
|
||||
"Add Plant": "Add Plant",
|
||||
"Done": "Done",
|
||||
"Tasks": "Tasks",
|
||||
"watering": "watering",
|
||||
"when you want": "when you want",
|
||||
"since {{count}} day": {
|
||||
"one": "since {{count}} day",
|
||||
"many": "since {{count}} days"
|
||||
}
|
||||
}
|
||||
45
src/lang/fr.json
Normal file
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"Add": "Ajouter",
|
||||
"Add Action": "Ajouter une action",
|
||||
"Add Plant": "Ajouter une plante",
|
||||
"A number of days between 2 actions. You can use 0 to set no notification.":
|
||||
"Un nombre de jours entre deux actions. Vous pouvez utilisé le 0 pour ne pas avoir de notification.",
|
||||
"bathing": "baigner",
|
||||
"Close": "Fermer",
|
||||
"Confirm delete plant": "Confirmer la suppression de la plante",
|
||||
"Description": "Description",
|
||||
"Dark Mode": "Mode sombre",
|
||||
"Delete": "Supprimer",
|
||||
"Delete Plant ?": "Supprimer la plante ?",
|
||||
"Done": "Fait",
|
||||
"Download": "Télécharger",
|
||||
"Download config": "Télécharger votre vos données",
|
||||
"Edit": "Editer",
|
||||
"Edit Plant": "Editer la plante",
|
||||
"every {{count}} days": {
|
||||
"one": "tous les jours",
|
||||
"many": "tous les {{count}} jours"
|
||||
},
|
||||
"Frequency": "Fréquence",
|
||||
"Indoor": "Intérieur",
|
||||
"last task": "dernière fois",
|
||||
"Me": "Moi",
|
||||
"Name": "Nom",
|
||||
"never": "jamais",
|
||||
"Outdoor": "Extérieur",
|
||||
"Plantes": "Plantes",
|
||||
"Plants": "Plantes",
|
||||
"{{count}} Plants": "{{count}} Plantes",
|
||||
"Profile": "Profile",
|
||||
"Spot": "Emplacement",
|
||||
"Spot:": "Emplacement : ",
|
||||
"spraying": "asperger",
|
||||
"Style": "Style",
|
||||
"Tasks": "Tâches",
|
||||
"watering": "arrosage",
|
||||
"when you want": "quand je veux",
|
||||
"since {{count}} days": {
|
||||
"one": "depuis {{count}} jour",
|
||||
"many": "depuis {{count}} jours"
|
||||
}
|
||||
}
|
||||
@@ -5,7 +5,7 @@
|
||||
"display": "standalone",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#fff",
|
||||
"theme_color": "#673ab8",
|
||||
"theme_color": "#12a307",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/assets/icons/android-chrome-192x192.png",
|
||||
|
||||
@@ -1,68 +1,56 @@
|
||||
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"
|
||||
import { createPortal } from 'preact/compat'
|
||||
import { useContext, useState } from 'preact/hooks'
|
||||
import { closeModal, Modal, ModalTitle } from '../components/Modals'
|
||||
import { PageLayout } from '../components/PageLayout'
|
||||
import { PlantForm, PlantThumb } from '../components/Plants'
|
||||
import { PlantsContext } from '../Contexts'
|
||||
import { Tasks } from '../components/Tasks'
|
||||
import { Text } from '../components/Translation'
|
||||
|
||||
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) => {
|
||||
const handleSubmit = (e, plantForm) => {
|
||||
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">
|
||||
<h1>
|
||||
<Text text="Plants" />
|
||||
</h1>
|
||||
<div className="grid grid-cols-1 gap-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4">
|
||||
{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 className="sticky bottom-5 flex w-full justify-end">
|
||||
<div
|
||||
onClick={() => setAddModal(true)}
|
||||
className="flex h-16 w-16 cursor-pointer items-center justify-center rounded-full bg-primary text-white shadow hover:bg-primary-dark">
|
||||
<Text text="+" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{createPortal(
|
||||
<Modal isOpen={addModal} onChange={handleCloseAddModal}>
|
||||
{typeof window !== 'undefined' &&
|
||||
createPortal(
|
||||
<Modal
|
||||
isOpen={addModal}
|
||||
onChange={(e) => closeModal(e, setAddModal)}>
|
||||
<ModalTitle>
|
||||
Add Plant
|
||||
<Text text="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>
|
||||
<PlantForm plant={{}} onChange={handleSubmit}>
|
||||
<Text text="Add" />
|
||||
</PlantForm>
|
||||
</Modal>,
|
||||
app
|
||||
document.getElementById('app')
|
||||
)}
|
||||
</PageLayout>
|
||||
)
|
||||
|
||||
@@ -1,29 +1,38 @@
|
||||
import {createPortal, useRef} from "preact/compat"
|
||||
import { useContext, useEffect, useState } from "preact/hooks"
|
||||
import {Button, SmallButton} 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"
|
||||
import {EditSVG, PlusSVG} from "../components/SVG";
|
||||
import {classNames} from "../utilities/classNames";
|
||||
|
||||
const Plant = ({id}) => {
|
||||
import { createPortal, useRef } from 'preact/compat'
|
||||
import { useContext, useEffect, useState } from 'preact/hooks'
|
||||
import { Button, SmallButton } from '../components/Button'
|
||||
import { closeModal, 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'
|
||||
import { EditSVG, PlusSVG, TriangleSVG } from '../components/SVG'
|
||||
import { PlantForm } from '../components/Plants'
|
||||
import { ACTION_TYPES, actionId } from '../utilities/actions'
|
||||
import { Text } from '../components/Translation'
|
||||
|
||||
const Plant = ({ id }) => {
|
||||
const [addModal, setAddModal] = useState(false)
|
||||
const {plants, removePlant, addAction, doneTask, history} = useContext(PlantsContext)
|
||||
const [editModal, setEditModal] = useState(false)
|
||||
const [deleteModal, setDeleteModal] = useState(false)
|
||||
const {
|
||||
plants,
|
||||
editPlant,
|
||||
removePlant,
|
||||
addAction,
|
||||
doneTask,
|
||||
history,
|
||||
archivedEntries,
|
||||
} = useContext(PlantsContext)
|
||||
const [plant, setPlant] = useState({})
|
||||
const archived = archivedEntries(Number(id))
|
||||
const [actionForm, setActionForm] = useState({})
|
||||
const [image, setImage] = useState(localStorage.getItem("image" + id) ?? '')
|
||||
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))
|
||||
const plantFind = plants.find((plant) => plant.id === Number(id))
|
||||
setPlant(plantFind)
|
||||
}, [])
|
||||
|
||||
@@ -35,77 +44,258 @@ const Plant = ({id}) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
if (!actionForm.action_type) {
|
||||
actionForm.action_type = action_types[0]
|
||||
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 handleEditSubmit = (e, plantForm) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
editPlant(plantForm)
|
||||
setEditModal(false)
|
||||
setPlant(plantForm)
|
||||
}
|
||||
|
||||
const addPicture = e => storePicture(e, id)
|
||||
const handleDeletePlant = () => removePlant(plant)
|
||||
|
||||
return <PageLayout>
|
||||
<div className="flex justify-between items-center">
|
||||
<h1>{ plant.name }</h1>
|
||||
const handleOpenEditModal = () => setEditModal(true)
|
||||
|
||||
const addPicture = (e) => storePicture(e, id)
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<div className="flex items-center justify-between">
|
||||
<h1 className="my-2 flex-1 text-center">{plant.name}</h1>
|
||||
<div>
|
||||
<Button className="bg-red-500" onClick={() => removePlant(plant)}>Delete</Button>
|
||||
<Button
|
||||
className="mr-2 bg-blue-500 hover:bg-blue-700"
|
||||
onClick={() => handleOpenEditModal()}>
|
||||
<Text text="Edit" />
|
||||
</Button>
|
||||
<Button
|
||||
className="bg-red-500 hover:bg-red-700"
|
||||
onClick={() => setDeleteModal(true)}>
|
||||
<Text text="Delete" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<p>{ plant.description }</p>
|
||||
<div className="flex flex-wrap justify-around gap-5">
|
||||
<div className="relative">
|
||||
<img id="picture" ref={picture} src={getPicture(id)} alt=""/>
|
||||
<SmallButton className="absolute top-2 left-2 cursor-pointer bg-blue-500 hover:bg-blue-700 max-w-[300px]">
|
||||
<img
|
||||
id="picture"
|
||||
ref={picture}
|
||||
src={getPicture(id)}
|
||||
alt=""
|
||||
className="inline-block min-h-[200px] min-w-[300px] bg-gray-400"
|
||||
/>
|
||||
<SmallButton className="absolute top-2 left-2 max-w-[300px] cursor-pointer bg-blue-500 hover:bg-blue-700">
|
||||
<label className="relative">
|
||||
<EditSVG className="w-4 h-4 block z-10 cursor-pointer" />
|
||||
<input id="input-file" type="file" name="picture" className="absolute opacity-0 max-w-[300px] w-0 h-0 inline" onChange={addPicture}/>
|
||||
<EditSVG className="z-10 block h-4 w-4 cursor-pointer" />
|
||||
<input
|
||||
id="input-file"
|
||||
type="file"
|
||||
name="picture"
|
||||
className="absolute inline h-0 w-0 max-w-[300px] opacity-0"
|
||||
onChange={addPicture}
|
||||
/>
|
||||
</label>
|
||||
</SmallButton>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
{plant.description && <p>{plant.description}</p>}
|
||||
{plant.hasOwnProperty('indoor') && (
|
||||
<span className="my-2 inline-block rounded bg-green-500 px-2 py-1 text-sm font-semibold text-white shadow">
|
||||
<Text text={plant.indoor ? 'Indoor' : 'Outdoor'} />
|
||||
</span>
|
||||
)}
|
||||
{plant.spot && (
|
||||
<p>
|
||||
<Text text="Spot:" />{' '}
|
||||
<span className="font-bold">{plant.spot}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<h2>Actions</h2>
|
||||
<SmallButton className="bg-blue-500 hover:bg-blue-700 mb-2 mt-5" onClick={() => setAddModal(true)}>
|
||||
<PlusSVG className="w-4 h-4" />
|
||||
<h2>
|
||||
<Text text="Actions" />
|
||||
</h2>
|
||||
<SmallButton
|
||||
className="mb-2 mt-5 bg-blue-500 hover:bg-blue-700"
|
||||
onClick={() => setAddModal(true)}>
|
||||
<PlusSVG className="h-4 w-4" />
|
||||
</SmallButton>
|
||||
</div>
|
||||
{plant.actions && plant.actions.map(action => {
|
||||
{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())
|
||||
isDone =
|
||||
lastTask
|
||||
.addDays(Number(action.frequency))
|
||||
.toSQLDate() >= new Date().toSQLDate()
|
||||
}
|
||||
return <div key={action.action_type} className="flex items-center gap-2 my-2">
|
||||
<span><Button className={classNames(isDone ? "bg-green-500 hover:bg-green-700" : "bg-blue-500 hover:bg-blue-700")} onClick={() => doneTask(action.id)}>Done</Button></span>
|
||||
<span className="capitalize">{action.action_type}</span>
|
||||
<span>every {action.frequency} days</span>
|
||||
<span className="text-gray-500">last task {lastTask ? lastTask.toFrDate() : 'never'}</span>
|
||||
return (
|
||||
<div
|
||||
key={action.action_type}
|
||||
className="my-2 flex items-center gap-2">
|
||||
<span>
|
||||
<Button
|
||||
className={
|
||||
isDone
|
||||
? 'bg-green-500 hover:bg-green-700'
|
||||
: 'bg-blue-500 hover:bg-blue-700'
|
||||
}
|
||||
onClick={() => doneTask(action.id)}>
|
||||
<Text text="Done" />
|
||||
</Button>
|
||||
</span>
|
||||
<span className="font-bold capitalize">
|
||||
<Text text={action.action_type} />
|
||||
</span>
|
||||
{Number(action.frequency) === 0 ? (
|
||||
<span>
|
||||
<Text text="when you want" />
|
||||
</span>
|
||||
) : (
|
||||
<span>
|
||||
<Text
|
||||
text="every {{count}} days"
|
||||
count={action.frequency}
|
||||
/>
|
||||
</span>
|
||||
)}
|
||||
<span className="text-gray-500">
|
||||
<Text text="last task" />{' '}
|
||||
{lastTask ? (
|
||||
lastTask.toFrDate()
|
||||
) : (
|
||||
<Text text="never" />
|
||||
)}
|
||||
</span>
|
||||
<ArchivedAction
|
||||
archived={archived.where(
|
||||
'action',
|
||||
actionId(action.action_type)
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{createPortal(
|
||||
<Modal isOpen={addModal} onChange={handleCloseAddModal}>
|
||||
<ModalTitle>Add Action</ModalTitle>
|
||||
<Modal
|
||||
isOpen={addModal}
|
||||
onChange={(e) => closeModal(e, setAddModal)}>
|
||||
<ModalTitle>
|
||||
<Text text="Add Action" />
|
||||
</ModalTitle>
|
||||
<form onSubmit={handleSubmit}>
|
||||
<SelectField
|
||||
name="action_type"
|
||||
className="mb-2 mt-5"
|
||||
value={actionForm.action_type}
|
||||
options={ACTION_TYPES}
|
||||
onChange={(e) =>
|
||||
setActionForm({
|
||||
...actionForm,
|
||||
action_type: e.target.value,
|
||||
})
|
||||
}>
|
||||
<Text text="Name" />
|
||||
</SelectField>
|
||||
|
||||
<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>
|
||||
<InputField
|
||||
name="frequency"
|
||||
className="mb-5"
|
||||
type="number"
|
||||
textSupport="A number of days between 2 actions. You can use 0 to set no notification."
|
||||
value={actionForm.frequency}
|
||||
onChange={(e) =>
|
||||
setActionForm({
|
||||
...actionForm,
|
||||
frequency: e.target.value,
|
||||
})
|
||||
}>
|
||||
<Text text="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
|
||||
type="submit"
|
||||
className="mx-auto mt-5 mb-2 block w-full bg-green-800 px-2 py-1 text-white shadow hover:bg-green-900">
|
||||
<Text text="Add" />
|
||||
</Button>
|
||||
</form>
|
||||
</Modal>,
|
||||
app
|
||||
)}
|
||||
|
||||
{createPortal(
|
||||
<Modal
|
||||
isOpen={editModal}
|
||||
onChange={(e) => closeModal(e, setEditModal)}>
|
||||
<ModalTitle>
|
||||
<Text text="Edit Plant" />
|
||||
</ModalTitle>
|
||||
<PlantForm plant={plant} onChange={handleEditSubmit}>
|
||||
<Text text="Edit" />
|
||||
</PlantForm>
|
||||
</Modal>,
|
||||
app
|
||||
)}
|
||||
|
||||
{createPortal(
|
||||
<Modal
|
||||
isOpen={deleteModal}
|
||||
onChange={(e) => closeModal(e, setDeleteModal)}>
|
||||
<ModalTitle>
|
||||
<Text text="Delete Plant ?" />
|
||||
</ModalTitle>
|
||||
<Button
|
||||
className="mt-10 bg-red-500 hover:bg-red-700"
|
||||
onClick={handleDeletePlant}>
|
||||
<Text text="Confirm delete plant" />
|
||||
</Button>
|
||||
</Modal>,
|
||||
app
|
||||
)}
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
|
||||
export default Plant
|
||||
|
||||
const ArchivedAction = ({ archived = [] }) => {
|
||||
console.log(archived)
|
||||
const [isOpen, setIsOpen] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
{archived.length > 0 && (
|
||||
<span className="relative" onClick={() => setIsOpen(!isOpen)}>
|
||||
{archived.length}{' '}
|
||||
<TriangleSVG
|
||||
className={`inline w-6 transform transition-all duration-300 ${
|
||||
isOpen ? 'rotate-180' : ''
|
||||
}`}
|
||||
/>
|
||||
{archived.length > 0 && isOpen && (
|
||||
<ul className="absolute z-10 rounded border border-primary bg-white p-2 dark:bg-gray-800">
|
||||
{archived
|
||||
.sortObjectsBy('time', true)
|
||||
.map((archive) => (
|
||||
<li>{new Date(archive.time).toFrDate()}</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,33 +1,70 @@
|
||||
import { useContext, useEffect, useState } from "preact/hooks";
|
||||
import { PageLayout } from "../components/PageLayout";
|
||||
import { UserContext } from "../Contexts";
|
||||
import { useContext, useEffect, useState } from 'preact/hooks'
|
||||
import { PageLayout } from '../components/PageLayout'
|
||||
import { PlantsContext, UserContext } from '../Contexts'
|
||||
import { Button } from '../components/Button'
|
||||
import { useLocalStorage } from '../hooks/LocalStorageHook'
|
||||
import { Text } from '../components/Translation'
|
||||
|
||||
export default function Profile() {
|
||||
const [user, setUser] = useContext(UserContext)
|
||||
const { plants } = useContext(PlantsContext)
|
||||
const [darkMode, setDarkMode] = useState(false)
|
||||
const [data, setData] = useLocalStorage('data', {})
|
||||
|
||||
useEffect(() => {
|
||||
setDarkMode(user.dark_mode)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setUser({...user, dark_mode: darkMode})
|
||||
setUser({ ...user, dark_mode: darkMode })
|
||||
}, [darkMode])
|
||||
|
||||
const handleChangeDarkMode = () => {
|
||||
setUser({...user, dark_mode: !user.dark_mode})
|
||||
setUser({ ...user, dark_mode: !user.dark_mode })
|
||||
}
|
||||
|
||||
return <PageLayout>
|
||||
<h1 className="">Profile</h1>
|
||||
const handleDownloadConfig = async () => {
|
||||
const fileName = 'plants'
|
||||
const json = JSON.stringify(data)
|
||||
const blob = new Blob([json], { type: 'application/json' })
|
||||
const href = await URL.createObjectURL(blob)
|
||||
const link = document.createElement('a')
|
||||
link.href = href
|
||||
link.download = fileName + '.json'
|
||||
document.body.appendChild(link)
|
||||
link.click()
|
||||
document.body.removeChild(link)
|
||||
}
|
||||
|
||||
return (
|
||||
<PageLayout>
|
||||
<h1 className="">
|
||||
<Text text="Profile" />
|
||||
</h1>
|
||||
<label htmlFor="dark_mode">
|
||||
<input type="checkbox"
|
||||
<input
|
||||
type="checkbox"
|
||||
id="dark_mode"
|
||||
name="dark_mode"
|
||||
className="mr-2"
|
||||
checked={user.dark_mode}
|
||||
onChange={() => handleChangeDarkMode()} />
|
||||
Dark Mode
|
||||
onChange={() => handleChangeDarkMode()}
|
||||
/>
|
||||
<Text text="Dark Mode" />
|
||||
</label>
|
||||
<div className="mt-5">
|
||||
<Text text="{{count}} Plants" count={plants.length} />
|
||||
</div>
|
||||
<div className="mt-5">
|
||||
<h2>
|
||||
<Text text="Download config" />
|
||||
</h2>
|
||||
<Button
|
||||
className="border-primary-dark bg-primary ring-primary hover:bg-primary-dark"
|
||||
onClick={handleDownloadConfig}>
|
||||
<Text text="Download" />
|
||||
</Button>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
86
src/routes/Style.js
Normal file
@@ -0,0 +1,86 @@
|
||||
import { PageLayout } from '../components/PageLayout'
|
||||
import { Button } from '../components/Button'
|
||||
|
||||
export default function Style() {
|
||||
return (
|
||||
<PageLayout>
|
||||
<h1>Style</h1>
|
||||
|
||||
<div className="flex">
|
||||
<div>
|
||||
<h1>H1 Head</h1>
|
||||
<h2>H2 Headline</h2>
|
||||
<h3>H3 Headline</h3>
|
||||
<h4>H4 Headline</h4>
|
||||
<h5>H5 Headline</h5>
|
||||
<h6>H6 Headline</h6>
|
||||
<p>Text paragraph</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="my-2 flex gap-2">
|
||||
<Button className="border-primary-dark bg-primary ring-primary hover:bg-primary-dark">
|
||||
Primary
|
||||
</Button>
|
||||
<Button className="bg-primary-light text-primary ring-primary hover:bg-primary hover:text-primary-darkest">
|
||||
Smooth
|
||||
</Button>
|
||||
<Button className="border-primary text-primary ring-primary-dark">
|
||||
Ghost
|
||||
</Button>
|
||||
<Button className="border-primary border-transparent text-primary ring-primary-dark">
|
||||
Raised
|
||||
</Button>
|
||||
<Button className="rounded-full border-primary-dark bg-primary ring-primary hover:bg-primary-dark">
|
||||
Previous
|
||||
</Button>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<Button>Secondary</Button>
|
||||
<Button>Smooth</Button>
|
||||
<Button>Ghost</Button>
|
||||
<Button>Raised</Button>
|
||||
<Button>Previous</Button>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<Button>Warning</Button>
|
||||
<Button>Smooth</Button>
|
||||
<Button>Ghost</Button>
|
||||
<Button>Raised</Button>
|
||||
<Button>Previous</Button>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<Button>Success</Button>
|
||||
<Button>Smooth</Button>
|
||||
<Button>Ghost</Button>
|
||||
<Button>Raised</Button>
|
||||
<Button>Previous</Button>
|
||||
</div>
|
||||
<div className="my-2">
|
||||
<Button>Danger</Button>
|
||||
<Button>Smooth</Button>
|
||||
<Button>Ghost</Button>
|
||||
<Button>Raised</Button>
|
||||
<Button>Previous</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="flex">
|
||||
<div className="my-2 flex-1 bg-primary-light p-2">
|
||||
Primary light
|
||||
</div>
|
||||
<div className="my-2 flex-1 bg-primary p-2">Primary</div>
|
||||
<div className="my-2 flex-1 bg-primary-dark p-2">
|
||||
Primary dark
|
||||
</div>
|
||||
<div className="my-2 flex-1 bg-primary-darkest p-2">
|
||||
Primary darkest
|
||||
</div>
|
||||
</div>
|
||||
<div className="my-2 bg-gray-500">Background Primary</div>
|
||||
</div>
|
||||
</PageLayout>
|
||||
)
|
||||
}
|
||||
57
src/sw.js
@@ -1,55 +1,4 @@
|
||||
import { getFiles, setupPrecaching, setupRouting } from 'preact-cli/sw/';
|
||||
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))
|
||||
setupRouting()
|
||||
setupPrecaching(getFiles())
|
||||
|
||||
@@ -11,5 +11,6 @@
|
||||
</head>
|
||||
<body>
|
||||
<% preact.bodyEnd %>
|
||||
<script src="sw.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
5
src/utilities/actions.js
Normal file
@@ -0,0 +1,5 @@
|
||||
export const ACTION_TYPES = ['watering', 'spraying', 'bathing']
|
||||
|
||||
export const actionId = (action) => {
|
||||
return ACTION_TYPES.findIndex((item) => item === action)
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
export function classNames() {
|
||||
// console.log(Object.values(arguments), Object.values(arguments).join(' '));
|
||||
return Object.values(arguments).join(' ')
|
||||
}
|
||||
@@ -1,68 +1,66 @@
|
||||
|
||||
const pictureName = id => {
|
||||
const pictureName = (id) => {
|
||||
return 'picture-' + id
|
||||
}
|
||||
|
||||
export const getPicture = id => {
|
||||
return localStorage.getItem('picture-' + id ?? '')
|
||||
export const getPicture = (id) => {
|
||||
return localStorage.getItem(pictureName(id) ?? '') ?? ''
|
||||
}
|
||||
|
||||
export const storePicture = (e, id) => {
|
||||
const files = e.target.files;
|
||||
let file = files[0];
|
||||
const files = e.target.files
|
||||
let file = files[0]
|
||||
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
document.getElementById("picture").src = e.target.result;
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
const reader = new FileReader()
|
||||
reader.onload = function (e) {
|
||||
document.getElementById('picture').src = e.target.result
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
|
||||
const filesToUploads = document.getElementById("input-file").files;
|
||||
file = filesToUploads[0];
|
||||
const filesToUploads = document.getElementById('input-file').files
|
||||
file = filesToUploads[0]
|
||||
if (file) {
|
||||
const reader = new FileReader()
|
||||
let dataPicture
|
||||
|
||||
reader.onload = function (e) {
|
||||
let img = document.createElement("img");
|
||||
img.src = e.target.result;
|
||||
let img = document.createElement('img')
|
||||
img.src = e.target.result
|
||||
|
||||
let canvas = document.createElement("canvas");
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0);
|
||||
let canvas = document.createElement('canvas')
|
||||
var ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(img, 0, 0)
|
||||
|
||||
const MAX_WIDTH = 300;
|
||||
const MAX_HEIGHT = 300;
|
||||
let width = img.width;
|
||||
let height = img.height;
|
||||
const MAX_WIDTH = 300
|
||||
const MAX_HEIGHT = 300
|
||||
let width = img.width
|
||||
let height = img.height
|
||||
|
||||
if (width > height) {
|
||||
if (width > MAX_WIDTH) {
|
||||
height *= MAX_WIDTH / width;
|
||||
width = MAX_WIDTH;
|
||||
height *= MAX_WIDTH / width
|
||||
width = MAX_WIDTH
|
||||
}
|
||||
} else {
|
||||
if (height > MAX_HEIGHT) {
|
||||
width *= MAX_HEIGHT / height;
|
||||
height = MAX_HEIGHT;
|
||||
width *= MAX_HEIGHT / height
|
||||
height = MAX_HEIGHT
|
||||
}
|
||||
}
|
||||
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
var ctx = canvas.getContext("2d");
|
||||
ctx.drawImage(img, 0, 0, width, height);
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
var ctx = canvas.getContext('2d')
|
||||
ctx.drawImage(img, 0, 0, width, height)
|
||||
|
||||
dataPicture = canvas.toDataURL(file.type);
|
||||
document.getElementById("picture").src = dataPicture;
|
||||
dataPicture = canvas.toDataURL(file.type)
|
||||
document.getElementById('picture').src = dataPicture
|
||||
|
||||
setTimeout(() => {
|
||||
localStorage.setItem(pictureName(id), dataPicture)
|
||||
}, 500)
|
||||
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
reader.readAsDataURL(file)
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,38 @@ module.exports = {
|
||||
darkMode: 'class', // or 'media' or 'class'
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
primary: {
|
||||
light: "#70E000",
|
||||
DEFAULT: "#008000",
|
||||
dark: "#006400",
|
||||
darkest: '#004B23',
|
||||
},
|
||||
secondary: {
|
||||
light: "#1E89ED",
|
||||
DEFAULT: "#007ED9",
|
||||
dark: "#00528C",
|
||||
darkest: '#003b64',
|
||||
},
|
||||
warning: {
|
||||
light: "#1E89ED",
|
||||
DEFAULT: "#007ED9",
|
||||
dark: "#00528C",
|
||||
darkest: '#003b64',
|
||||
},
|
||||
success: {
|
||||
light: "#1E89ED",
|
||||
DEFAULT: "#007ED9",
|
||||
dark: "#00528C",
|
||||
darkest: '#003b64',
|
||||
},
|
||||
danger: {
|
||||
light: "#1E89ED",
|
||||
DEFAULT: "#007ED9",
|
||||
dark: "#00528C",
|
||||
darkest: '#003b64',
|
||||
},
|
||||
},
|
||||
minHeight: {
|
||||
'10': '2.5rem',
|
||||
'12': '3rem',
|
||||
@@ -22,8 +54,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
},
|
||||
variants: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
}
|
||||