use Prettier

This commit is contained in:
Romulus21
2022-02-05 01:25:59 +01:00
parent 0b95d65898
commit 8223211d6a
30 changed files with 1072 additions and 713 deletions

6
.prettierrc.json Normal file
View File

@@ -0,0 +1,6 @@
{
"bracketSameLine": true,
"tabWidth": 4,
"semi": false,
"singleQuote": true
}

37
package-lock.json generated
View File

@@ -12,7 +12,9 @@
"preact": "^10.3.2", "preact": "^10.3.2",
"preact-cli-tailwind": "^3.0.0", "preact-cli-tailwind": "^3.0.0",
"preact-render-to-string": "^5.1.4", "preact-render-to-string": "^5.1.4",
"preact-router": "^3.2.1" "preact-router": "^3.2.1",
"prettier": "^2.5.1",
"prettier-plugin-tailwindcss": "^0.1.5"
}, },
"devDependencies": { "devDependencies": {
"enzyme": "^3.10.0", "enzyme": "^3.10.0",
@@ -16065,6 +16067,28 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/prettier": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz",
"integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg==",
"bin": {
"prettier": "bin-prettier.js"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/prettier-plugin-tailwindcss": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.1.5.tgz",
"integrity": "sha512-e+jTxwiHL4I3Ot8OjV1LAiiaAx0Zgy71xTL7xNmJtNmhpja7GKzFSAoulqBDS1D57B7lbZDCvDEKIWIQSBUmBQ==",
"engines": {
"node": ">=12.17.0"
},
"peerDependencies": {
"prettier": ">=2.2.0"
}
},
"node_modules/pretty-bytes": { "node_modules/pretty-bytes": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",
@@ -34427,6 +34451,17 @@
"integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=",
"dev": true "dev": true
}, },
"prettier": {
"version": "2.5.1",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-2.5.1.tgz",
"integrity": "sha512-vBZcPRUR5MZJwoyi3ZoyQlc1rXeEck8KgeC9AwwOn+exuxLxq5toTRDTSaVrXHxelDMHy9zlicw8u66yxoSUFg=="
},
"prettier-plugin-tailwindcss": {
"version": "0.1.5",
"resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.1.5.tgz",
"integrity": "sha512-e+jTxwiHL4I3Ot8OjV1LAiiaAx0Zgy71xTL7xNmJtNmhpja7GKzFSAoulqBDS1D57B7lbZDCvDEKIWIQSBUmBQ==",
"requires": {}
},
"pretty-bytes": { "pretty-bytes": {
"version": "4.0.2", "version": "4.0.2",
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",

View File

@@ -31,7 +31,9 @@
"preact": "^10.3.2", "preact": "^10.3.2",
"preact-cli-tailwind": "^3.0.0", "preact-cli-tailwind": "^3.0.0",
"preact-render-to-string": "^5.1.4", "preact-render-to-string": "^5.1.4",
"preact-router": "^3.2.1" "preact-router": "^3.2.1",
"prettier": "^2.5.1",
"prettier-plugin-tailwindcss": "^0.1.5"
}, },
"jest": { "jest": {
"preset": "jest-preset-preact", "preset": "jest-preset-preact",

File diff suppressed because one or more lines are too long

View File

@@ -1,23 +1,43 @@
import { createContext } from "preact"; import { createContext } from 'preact'
import usePlants from "./hooks/PlantsHook"; import usePlants from './hooks/PlantsHook'
import { useLocalStorage } from "./hooks/LocalStorageHook" import { useLocalStorage } from './hooks/LocalStorageHook'
import useUser from "./hooks/UserHook"; import useUser from './hooks/UserHook'
import {TranslateProvider} from "./components/Translation"; import { TranslateProvider } from './components/Translation'
export const UserContext = createContext(null) export const UserContext = createContext(null)
export const PlantsContext = createContext(null) export const PlantsContext = createContext(null)
export default function ContextsProviders({children}) { export default function ContextsProviders({ children }) {
const [data, setData] = useLocalStorage('data', {}) const [data, setData] = useLocalStorage('data', {})
const [user, setUser] = useUser(data, setData) const [user, setUser] = useUser(data, setData)
const {plants, addPlant, editPlant, removePlant, addAction, doneTask, history, archivedEntries} = usePlants(data, setData) const {
plants,
addPlant,
editPlant,
removePlant,
addAction,
doneTask,
history,
archivedEntries,
} = usePlants(data, setData)
return <TranslateProvider> return (
<TranslateProvider>
<UserContext.Provider value={[user, setUser]}> <UserContext.Provider value={[user, setUser]}>
<PlantsContext.Provider value={{plants, addPlant, editPlant, removePlant, addAction, doneTask, history, archivedEntries}}> <PlantsContext.Provider
value={{
plants,
addPlant,
editPlant,
removePlant,
addAction,
doneTask,
history,
archivedEntries,
}}>
{children} {children}
</PlantsContext.Provider> </PlantsContext.Provider>
</UserContext.Provider> </UserContext.Provider>
</TranslateProvider> </TranslateProvider>
)
} }

View File

@@ -1,19 +1,17 @@
import { h } from 'preact'; import { h } from 'preact'
import { Router } from 'preact-router'; import { Router } from 'preact-router'
import ContextsProviders from '../Contexts'; import ContextsProviders from '../Contexts'
import Header from './Header'; import Header from './Header'
// Code-splitting is automated for `routes` directory // Code-splitting is automated for `routes` directory
import Home from '../routes/Home'; import Home from '../routes/Home'
import Plant from '../routes/Plant'; import Plant from '../routes/Plant'
import Profile from '../routes/Profile'; import Profile from '../routes/Profile'
import Style from "../routes/Style"; import Style from '../routes/Style'
const App = () => { const App = () => {
return ( return (
<div id="app" className="h-screen overflow-auto flex flex-col"> <div id="app" className="flex h-screen flex-col overflow-auto">
<ContextsProviders> <ContextsProviders>
<Header /> <Header />
<main className="flex-1 dark:bg-gray-800 dark:text-white"> <main className="flex-1 dark:bg-gray-800 dark:text-white">
@@ -26,6 +24,7 @@ const App = () => {
</main> </main>
</ContextsProviders> </ContextsProviders>
</div> </div>
)} )
}
export default App; export default App

View File

@@ -1,18 +1,30 @@
import { classNames } from "../utilities/classNames" export const Button = ({
children,
export const Button = ({ children, className = "", type = "button", ...props }) => { className = '',
type = 'button',
...props
}) => {
return ( return (
<button type={type} className={classNames("border px-2 py-1 shadow transition-all duration-300 rounded hover:ring-2 focus:ring-2 ring-offset disabled:opacity-50 disabled:cursor-not-allowed", 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} {children}
</button> </button>
) )
} }
export const SmallButton = ({ children, className = "", type = "button", ...props }) => { export const SmallButton = ({
children,
className = '',
type = 'button',
...props
}) => {
return ( 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} {children}
</button> </button>
) )

View File

@@ -1,8 +1,13 @@
import {classNames} from "../utilities/classNames"; import { Text } from './Translation'
import {Text} from "./Translation";
export const InputField = ({children, name, value = "", type = "text", textSupport = "", ...props}) => {
export const InputField = ({
children,
name,
value = '',
type = 'text',
textSupport = '',
...props
}) => {
const id = props.id ?? name const id = props.id ?? name
const classStyle = props.className ?? '' const classStyle = props.className ?? ''
@@ -10,22 +15,31 @@ export const InputField = ({children, name, value = "", type = "text", textSuppo
delete props.className delete props.className
} }
return <fieldset className={classNames(classStyle)}> return (
{children && <label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <fieldset className={classStyle}>
{ children } {children && (
</label>} <label
<input id={id} htmlFor={id}
className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{children}
</label>
)}
<input
id={id}
name={name} name={name}
type={type} type={type}
value={value} value={value}
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" 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}/> {...props}
<span className="text-gray-500 text-sm"><Text text={textSupport} /></span> />
<span className="text-sm text-gray-500">
<Text text={textSupport} />
</span>
</fieldset> </fieldset>
)
} }
export const TextAreaField = ({children, name, ...props}) => { export const TextAreaField = ({ children, name, ...props }) => {
const id = props.id ?? name const id = props.id ?? name
const classStyle = props.className ?? '' const classStyle = props.className ?? ''
@@ -33,29 +47,54 @@ export const TextAreaField = ({children, name, ...props}) => {
delete props.className delete props.className
} }
return <fieldset className={classNames(classStyle)}> return (
{children && <label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <fieldset className={classStyle}>
{ children } {children && (
</label>} <label
<textarea id={id} htmlFor={id}
className="block text-sm font-medium text-gray-700 dark:text-gray-300">
{children}
</label>
)}
<textarea
id={id}
name={name} 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} {...props}
/> />
</fieldset> </fieldset>
)
} }
export const SelectField = ({children, name, options, className = '', ...props}) => { export const SelectField = ({
children,
name,
options,
className = '',
...props
}) => {
const id = props.id ?? name const id = props.id ?? name
return <fieldset className={className}> return (
{children && <label htmlFor={id} className="block text-sm font-medium text-gray-700 dark:text-gray-300"> <fieldset className={className}>
{ children } {children && (
</label>} <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}> htmlFor={id}
{options.map((option, index) => <option key={index} value={option} className="block text-sm font-medium text-gray-700 dark:text-gray-300">
className="capitalize"><Text text={option} /></option>)} {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> </select>
</fieldset> </fieldset>
)
} }

View File

@@ -1,15 +1,19 @@
import { Link } from "preact-router/match" import { Link } from 'preact-router/match'
import {Text} from "./Translation" import { Text } from './Translation'
const Header = () => { const Header = () => {
return ( return (
<header className="bg-primary text-white flex justify-between items-center p-2 text-lg"> <header className="flex items-center justify-between bg-primary p-2 text-lg text-white">
<Link href="/" class="font-bold text-xl cursor-pointer"> <Link href="/" class="cursor-pointer text-xl font-bold">
<Text text="Plantes" /> <Text text="Plantes" />
</Link> </Link>
<nav className="flex gap-2"> <nav className="flex gap-2">
<NavLink path="/style"><Text text="Style" /></NavLink> <NavLink path="/style">
<NavLink path="/profile"><Text text="Me" /></NavLink> <Text text="Style" />
</NavLink>
<NavLink path="/profile">
<Text text="Me" />
</NavLink>
</nav> </nav>
</header> </header>
) )
@@ -22,8 +26,7 @@ const NavLink = ({ path, children }) => {
<Link <Link
href={path} href={path}
activeClassName="font-bold bg-primary-dark" activeClassName="font-bold bg-primary-dark"
class="py-1 px-2 rounded cursor-pointer" class="cursor-pointer rounded py-1 px-2">
>
{children} {children}
</Link> </Link>
) )

View File

@@ -1,33 +1,52 @@
import {Text} from "./Translation"; 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 (
<>
return <> {isOpen && (
{isOpen && <div
<div className="overlay fixed block top-0 bottom-0 left-0 right-0 z-10 bg-gray-800 bg-opacity-80" onClick={handleClose}> className="overlay fixed top-0 bottom-0 left-0 right-0 z-10 block bg-gray-800 bg-opacity-80"
<div className="h-full flex justify-center items-center"> onClick={handleClose}>
<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 h-full items-center justify-center">
<div className="flex-1"> <div
{children} 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>
{!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}><Text text="Close" /></button>
</div>}
</div> </div>
</div> </div>
</div> </div>
} )}
</> </>
)
} }
export const ModalTitle = ({children, ...props}) => { 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> 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>
)
} }
export const closeModal = (e, setter) => { export const closeModal = (e, setter) => {
if (e.target.classList.contains("overlay") || e.target.classList.contains("close-button")) { if (
e.target.classList.contains('overlay') ||
e.target.classList.contains('close-button')
) {
setter(false) setter(false)
} }
} }

View File

@@ -1,9 +1,3 @@
import { classNames } from "../utilities/classNames"; export const PageLayout = ({ children, ...props }) => {
return <div className={`p-2 ${props.class ?? ''}`}>{children}</div>
export const PageLayout = ({children, ...props}) => {
return <div class={classNames("p-2", props.class ?? "")}>
{children}
</div>
} }

View File

@@ -1,81 +1,108 @@
import { Link } from "preact-router/match" import { Link } from "preact-router/match";
import {getPicture} from "../utilities/pictures" import { getPicture } from "../utilities/pictures";
import {InputField, TextAreaField} from "./Form" import { InputField, TextAreaField } from "./Form";
import {Button} from "./Button" import { Button } from "./Button";
import {useState} from "preact/hooks" import { useState } from "preact/hooks";
import {Text} from "./Translation" import { Text } from "./Translation";
export const PlantThumb = ({ plant, children }) => { export const PlantThumb = ({ plant, children }) => {
return (
return <Link href={`/plant/${plant.id}`} class="block h-48 cursor-pointer"> <Link href={`/plant/${plant.id}`} class="block h-48 cursor-pointer">
<div className="group bg-primary-light relative rounded shadow-lg flex flex-col"> <div className="group relative flex flex-col rounded bg-primary-light shadow-lg">
<img src={getPicture(plant.id)} alt="" className="object-cover h-48 w-full min-h-48 min-w-48 rounded" /> <img
<div className="bg-primary group-hover:bg-primary-dark text-white p-2 text-center absolute bottom-0 w-full rounded-bl rounded-br"> 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} {children}
</div> </div>
<div title="Actions" className="absolute right-2 top-2 rounded-full flex justify-center items-center bg-primary group-hover:bg-primary-dark 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} {plant.actions.length}
</div> </div>
</div> </div>
</Link> </Link>
} );
};
export const PlantForm = ({children, plant, ...props}) => { export const PlantForm = ({ children, plant, ...props }) => {
const [plantForm, setPlantForm] = useState(plant);
const [plantForm, setPlantForm] = useState(plant) return (
<form onSubmit={(e) => props.onChange(e, plantForm)}>
return <form onSubmit={e => props.onChange(e, plantForm)}> <InputField
name="name"
<InputField name="name"
className="mb-2 mt-5" className="mb-2 mt-5"
value={plantForm.name} value={plantForm.name}
onChange={(e) => setPlantForm({ ...plantForm, name: e.target.value }) }> onChange={(e) => setPlantForm({ ...plantForm, name: e.target.value })}
>
<Text text="Name" /> <Text text="Name" />
</InputField> </InputField>
<TextAreaField name="description" <TextAreaField
name="description"
className="mb-5" className="mb-5"
value={plantForm.description} value={plantForm.description}
onChange={(e) => setPlantForm({ ...plantForm, description: e.target.value })}> onChange={(e) =>
setPlantForm({ ...plantForm, description: e.target.value })
}
>
<Text text="Description" /> <Text text="Description" />
</TextAreaField> </TextAreaField>
<fieldset className="mb-5 flex gap-2 max-w-[300px] mx-auto"> <fieldset className="mx-auto mb-5 flex max-w-[300px] gap-2">
<div className="flex-1"> <div className="flex-1">
<input id="indoor" <input
id="indoor"
name="indoor" name="indoor"
type="radio" type="radio"
checked={plantForm.indoor === true} checked={plantForm.indoor === true}
onChange={() => setPlantForm({ ...plantForm, indoor: true })} onChange={() => setPlantForm({ ...plantForm, indoor: true })}
className="sr-only peer" className="peer sr-only"
/> />
<label htmlFor="indoor" className="w-full block peer-checked:bg-blue-700 bg-blue-500 hover:bg-blue-700 px-2 py-1 text-center rounded border dark:border-white"> <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" /> <Text text="Indoor" />
</label> </label>
</div> </div>
<div className="flex-1"> <div className="flex-1">
<input id="outdoor" <input
id="outdoor"
name="outdoor" name="outdoor"
type="radio" type="radio"
checked={plantForm.indoor === false} checked={plantForm.indoor === false}
onChange={() => setPlantForm({ ...plantForm, indoor: false })} onChange={() => setPlantForm({ ...plantForm, indoor: false })}
className="sr-only peer" className="peer sr-only"
/> />
<label htmlFor="outdoor" className="w-full block peer-checked:bg-blue-700 bg-blue-500 hover:bg-blue-700 px-2 py-1 text-center rounded border dark:border-white"> <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" /> <Text text="Outdoor" />
</label> </label>
</div> </div>
</fieldset> </fieldset>
<InputField name="spot" <InputField
name="spot"
className="mt-5" className="mt-5"
value={plantForm.spot} value={plantForm.spot}
onChange={(e) => setPlantForm({ ...plantForm, spot: e.target.value }) }> onChange={(e) => setPlantForm({ ...plantForm, spot: e.target.value })}
>
<Text text="Spot" /> <Text text="Spot" />
</InputField> </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"> <Button
{ children } 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> </Button>
</form> </form>
} );
};

View File

@@ -1,29 +1,40 @@
const SVGSkeleton = ({ paths, viewBox = '0 0 512 512', ...props }) => {
const SVGSkeleton = ({paths, viewBox = "0 0 512 512", ...props}) => { return (
return <svg xmlns="http://www.w3.org/2000/svg" <svg
xmlns="http://www.w3.org/2000/svg"
viewBox={viewBox} viewBox={viewBox}
fill="currentColor" fill="currentColor"
className={props.className ?? ''} className={props.className ?? ''}
{...props}> {...props}>
{props.title && <title>{ props.title }</title>} {props.title && <title>{props.title}</title>}
{paths} {paths}
</svg> </svg>
)
} }
export const PlusSVG = (props) => SVGSkeleton({ export const PlusSVG = (props) =>
viewBox: "0 0 448 512", SVGSkeleton({
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" />, viewBox: '0 0 448 512',
...props 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({ export const EditSVG = (props) =>
viewBox: "0 0 576 512", SVGSkeleton({
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" />, viewBox: '0 0 576 512',
...props 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({ export const TriangleSVG = (props) =>
viewBox: "0 0 320 512", SVGSkeleton({
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" />, viewBox: '0 0 320 512',
...props 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,
})

View File

@@ -1,11 +1,10 @@
import {Button} from "./Button" import { Button } from './Button'
import { Link } from "preact-router/match" import { Link } from 'preact-router/match'
import {useContext} from "preact/hooks" import { useContext } from 'preact/hooks'
import {PlantsContext} from "../Contexts" import { PlantsContext } from '../Contexts'
import {Text} from "./Translation" import { Text } from './Translation'
export const Tasks = () => { export const Tasks = () => {
const { plants, doneTask, history } = useContext(PlantsContext) const { plants, doneTask, history } = useContext(PlantsContext)
const taskIsRequired = (action) => { const taskIsRequired = (action) => {
@@ -15,7 +14,10 @@ export const Tasks = () => {
if (history()[action.id]) { if (history()[action.id]) {
let lastTask = new Date(history()[action.id]) let lastTask = new Date(history()[action.id])
return lastTask.addDays(Number(action.frequency)).toSQLDate() < (new Date()).toSQLDate() return (
lastTask.addDays(Number(action.frequency)).toSQLDate() <
new Date().toSQLDate()
)
} }
return true return true
} }
@@ -24,31 +26,60 @@ export const Tasks = () => {
if (history()[action.id]) { if (history()[action.id]) {
let lastTask = new Date(history()[action.id]) let lastTask = new Date(history()[action.id])
const oneDay = 1000 * 60 * 60 * 24 const oneDay = 1000 * 60 * 60 * 24
return Math.round(((new Date()) - lastTask)/oneDay) return Math.round((new Date() - lastTask) / oneDay)
} }
return 0 return 0
} }
return <div className="mb-5"> return (
<div className="mb-5">
<h1> <h1>
<Text text="Tasks" /> <Text text="Tasks" />
</h1> </h1>
<div> <div>
{plants.map(plant => plant.actions.filter(action => taskIsRequired(action)).map(action => <div className="flex items-center gap-2 my-2"> {plants.map((plant) =>
plant.actions
.filter((action) => taskIsRequired(action))
.map((action) => (
<div className="my-2 flex items-center gap-2">
<span> <span>
<Button className="bg-blue-500 hover:bg-blue-700" onClick={() => doneTask(action.id)}> <Button
className="bg-blue-500 hover:bg-blue-700"
onClick={() => doneTask(action.id)}>
<Text text="Done" /> <Text text="Done" />
</Button> </Button>
</span> </span>
<span className="capitalize"> <span className="capitalize">
<Link href={`/plant/${plant.id}`} class="font-bold mr-2">{plant.name}</Link> <Link
href={`/plant/${plant.id}`}
class="mr-2 font-bold">
{plant.name}
</Link>
<Text text={action.action_type} /> <Text text={action.action_type} />
</span> </span>
{Number(action.frequency) === 0 {Number(action.frequency) === 0 ? (
? <span><Text text="when you want" /></span> <span>
: (taskDelay(action) > 1 ? <span><Text text="since {{count}} days" count={taskDelay(action)} /></span> <Text text="when you want" />
: <span><Text text="every {{count}} days" count={action.frequency} /></span>)} </span>
</div>))} ) : 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>
</div> </div>
)
} }

View File

@@ -1,12 +1,11 @@
import {en} from "../lang/en" import en from '../lang/en.json'
import {fr} from "../lang/fr" import fr from '../lang/fr.json'
import {useContext, useEffect, useState} from "preact/hooks" import { useContext, useEffect, useState } from 'preact/hooks'
import {createContext} from "preact" import { createContext } from 'preact'
export const TranslateContext = createContext(null) export const TranslateContext = createContext(null)
export const Text = ({text, count = null}) => { export const Text = ({ text, count = null }) => {
const lang = useContext(TranslateContext) const lang = useContext(TranslateContext)
const [translate, setTranslate] = useState() const [translate, setTranslate] = useState()
@@ -27,23 +26,23 @@ export const Text = ({text, count = null}) => {
setTranslate(text) setTranslate(text)
}, []) }, [])
return <>{ translate }</> return <>{translate}</>
} }
export const TranslateProvider = ({children}) => { export const TranslateProvider = ({ children }) => {
let translate = null let translate = null
if (typeof navigator !== "undefined") { if (typeof navigator !== 'undefined') {
const userLang = navigator.language || navigator.userLanguage const userLang = navigator.language || navigator.userLanguage
if (userLang === "en") { if (userLang === 'en') {
translate = en translate = en
} else if (userLang === "fr") { } else if (userLang === 'fr') {
translate = fr translate = fr
} }
} }
return <TranslateContext.Provider value={translate}> return (
{ children } <TranslateContext.Provider value={translate}>
{children}
</TranslateContext.Provider> </TranslateContext.Provider>
)
} }

View File

@@ -1,4 +1,4 @@
import { useState } from "preact/hooks" import { useState } from 'preact/hooks'
export const useLocalStorage = (key, initialValue) => { export const useLocalStorage = (key, initialValue) => {
const [storedValue, setStoredValue] = useState(() => { const [storedValue, setStoredValue] = useState(() => {

View File

@@ -1,46 +1,57 @@
import { useEffect, useState } from "preact/hooks" import { useEffect, useState } from 'preact/hooks'
import {route} from "preact-router"; import { route } from 'preact-router'
import {actionId} from "../utilities/actions"; import { actionId } from '../utilities/actions'
const usePlants = (data, setData) => { const usePlants = (data, setData) => {
const [plants, setPlants] = useState([]) const [plants, setPlants] = useState([])
useEffect(() =>{ useEffect(() => {
setPlants(data.plants ?? []) setPlants(data.plants ?? [])
}, []) }, [])
const addPlant = (plantForm) => { const addPlant = (plantForm) => {
let plant = 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.id = maxId === -Infinity ? 1 : maxId + 1
plant.actions = [] plant.actions = []
setPlants([...plants, plant]) setPlants([...plants, plant])
setData({...data, plants: [...plants, plant]}) setData({ ...data, plants: [...plants, plant] })
} }
const editPlant = (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 plants[plantIndex] = plant
savePlants(plants) savePlants(plants)
} }
const removePlant = (plant) => { 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) savePlants(plants)
route('/', true) route('/', true)
} }
const addAction = (plant, action) => { const addAction = (plant, action) => {
action.id = action.action_type + '-' + plant.id action.id = action.action_type + '-' + plant.id
let actionIndex = plant.actions.findIndex(item => item.action_type === action.action_type) let actionIndex = plant.actions.findIndex(
actionIndex >= 0 ? plant.actions[actionIndex] = action (item) => item.action_type === action.action_type
)
actionIndex >= 0
? (plant.actions[actionIndex] = action)
: plant.actions.push(action) : plant.actions.push(action)
editPlant(plant) editPlant(plant)
} }
const savePlants = (plants) => { const savePlants = (plants) => {
setPlants(plants) setPlants(plants)
setData({...data, plants: plants}) setData({ ...data, plants: plants })
} }
const doneTask = (action) => { const doneTask = (action) => {
@@ -51,10 +62,10 @@ const usePlants = (data, setData) => {
archived.push({ archived.push({
plantId: Number(action.split('-')[1]), plantId: Number(action.split('-')[1]),
action: actionId(action.split('-')[0]), action: actionId(action.split('-')[0]),
time: (new Date()).toSQLDate() time: new Date().toSQLDate(),
}) })
setData({...data, history: history, archived: archived}) setData({ ...data, history: history, archived: archived })
} }
const history = () => data.history ?? {} const history = () => data.history ?? {}
@@ -64,7 +75,16 @@ const usePlants = (data, setData) => {
return archived.where('plantId', plantId) return archived.where('plantId', plantId)
} }
return {plants, addPlant, editPlant, removePlant, addAction, doneTask, history, archivedEntries} return {
plants,
addPlant,
editPlant,
removePlant,
addAction,
doneTask,
history,
archivedEntries,
}
} }
export default usePlants export default usePlants

View File

@@ -1,15 +1,15 @@
import { useEffect, useState } from "preact/hooks" import { useEffect, useState } from 'preact/hooks'
const useUser = (data, setData) => { 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(() =>{ useEffect(() => {
setData({...data, user: user}) setData({ ...data, user: user })
document.querySelector('html').classList.toggle('dark', user.dark_mode) document.querySelector('html').classList.toggle('dark', user.dark_mode)
}, [user]) }, [user])
return [user, setUser] return [user, setUser]
} }

View File

@@ -1,23 +1,25 @@
import App from './components/App'; import App from './components/App'
import './style'; import './style'
Number.prototype.pad = function(n) { Number.prototype.pad = function (n) {
return (new Array(n).join('0') + this).slice(-n) return (new Array(n).join('0') + this).slice(-n)
} }
Date.prototype.addDays = function (days) { Date.prototype.addDays = function (days) {
let date = new Date(this.valueOf()); let date = new Date(this.valueOf())
date.setDate(date.getDate() + days); date.setDate(date.getDate() + days)
return date; return date
} }
Date.prototype.toFrDate = function() { Date.prototype.toFrDate = function () {
let month = ((this.getMonth() + 1 < 10) ? '0' : '') + (this.getMonth() + 1) let month = (this.getMonth() + 1 < 10 ? '0' : '') + (this.getMonth() + 1)
return `${this.getDate()}/${month}/${this.getFullYear()}` return `${this.getDate()}/${month}/${this.getFullYear()}`
} }
Date.prototype.toSQLDate = function() { Date.prototype.toSQLDate = function () {
return `${this.getFullYear()}-${Number(this.getMonth() + 1).pad(2)}-${Number(this.getDate()).pad(2)}` return `${this.getFullYear()}-${Number(this.getMonth() + 1).pad(
2
)}-${Number(this.getDate()).pad(2)}`
} }
/** /**
@@ -26,8 +28,8 @@ Date.prototype.toSQLDate = function() {
* @param {string} search to filter by * @param {string} search to filter by
* @returns {*[]} * @returns {*[]}
*/ */
Array.prototype.where = function(field, search) { Array.prototype.where = function (field, search) {
return this.filter(item => item[field] === search) return this.filter((item) => item[field] === search)
} }
/** /**
@@ -42,4 +44,4 @@ Array.prototype.sortObjectsBy = function (field, desc = false) {
: this.sort((a, b) => a[field] > b[field]) : this.sort((a, b) => a[field] > b[field])
} }
export default App; export default App

View File

@@ -1,10 +0,0 @@
export const en = {
"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"}
}

12
src/lang/en.json Normal file
View 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"
}
}

View File

@@ -1,39 +0,0 @@
export const fr = {
"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": "arrossage",
"when you want": "quand je veux",
"since {{count}} days": {one: "depuis {{count}} jour", many: "depuis {{count}} jours"}
}

45
src/lang/fr.json Normal file
View 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"
}
}

View File

@@ -1,11 +1,11 @@
import { createPortal } from "preact/compat" import { createPortal } from 'preact/compat'
import { useContext, useState } from "preact/hooks" import { useContext, useState } from 'preact/hooks'
import {closeModal, Modal, ModalTitle} from "../components/Modals" import { closeModal, Modal, ModalTitle } from '../components/Modals'
import { PageLayout } from "../components/PageLayout" import { PageLayout } from '../components/PageLayout'
import {PlantForm, PlantThumb} from "../components/Plants" import { PlantForm, PlantThumb } from '../components/Plants'
import { PlantsContext } from "../Contexts" import { PlantsContext } from '../Contexts'
import {Tasks} from "../components/Tasks" import { Tasks } from '../components/Tasks'
import {Text} from "../components/Translation"; import { Text } from '../components/Translation'
export const Home = () => { export const Home = () => {
const [addModal, setAddModal] = useState(false) const [addModal, setAddModal] = useState(false)
@@ -20,28 +20,35 @@ export const Home = () => {
return ( return (
<PageLayout class="relative"> <PageLayout class="relative">
<Tasks /> <Tasks />
<h1><Text text="Plants" /></h1> <h1>
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-2"> <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) => ( {plants.map((plant) => (
<PlantThumb plant={plant}>{plant.name}</PlantThumb> <PlantThumb plant={plant}>{plant.name}</PlantThumb>
))} ))}
</div> </div>
<div className="w-full sticky bottom-5 flex justify-end"> <div className="sticky bottom-5 flex w-full justify-end">
<div onClick={() => setAddModal(true)} <div
className="rounded-full shadow w-16 h-16 flex items-center justify-center cursor-pointer bg-primary hover:bg-primary-dark text-white"> 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="+" /> <Text text="+" />
</div> </div>
</div> </div>
{typeof window !== "undefined" && createPortal( {typeof window !== 'undefined' &&
<Modal isOpen={addModal} onChange={e => closeModal(e, setAddModal)}> createPortal(
<Modal
isOpen={addModal}
onChange={(e) => closeModal(e, setAddModal)}>
<ModalTitle> <ModalTitle>
<Text text="Add Plant" /> <Text text="Add Plant" />
</ModalTitle> </ModalTitle>
<PlantForm plant={{}} onChange={handleSubmit}><Text text="Add" /></PlantForm> <PlantForm plant={{}} onChange={handleSubmit}>
<Text text="Add" />
</PlantForm>
</Modal>, </Modal>,
document.getElementById('app') document.getElementById('app')
)} )}

View File

@@ -1,32 +1,38 @@
import {createPortal, useRef} from "preact/compat" import { createPortal, useRef } from 'preact/compat'
import { useContext, useEffect, useState } from "preact/hooks" import { useContext, useEffect, useState } from 'preact/hooks'
import {Button, SmallButton} from "../components/Button" import { Button, SmallButton } from '../components/Button'
import {closeModal, Modal, ModalTitle} from "../components/Modals" import { closeModal, Modal, ModalTitle } from '../components/Modals'
import { PageLayout } from "../components/PageLayout" import { PageLayout } from '../components/PageLayout'
import { PlantsContext } from "../Contexts" import { PlantsContext } from '../Contexts'
import {InputField, SelectField} from "../components/Form" import { InputField, SelectField } from '../components/Form'
import {getPicture, storePicture} from "../utilities/pictures" import { getPicture, storePicture } from '../utilities/pictures'
import {EditSVG, PlusSVG, TriangleSVG} from "../components/SVG" import { EditSVG, PlusSVG, TriangleSVG } from '../components/SVG'
import {classNames} from "../utilities/classNames" import { PlantForm } from '../components/Plants'
import {PlantForm} from "../components/Plants" import { ACTION_TYPES, actionId } from '../utilities/actions'
import {ACTION_TYPES, actionId} from "../utilities/actions" import { Text } from '../components/Translation'
import {Text} from "../components/Translation"
const Plant = ({id}) => {
const Plant = ({ id }) => {
const [addModal, setAddModal] = useState(false) const [addModal, setAddModal] = useState(false)
const [editModal, setEditModal] = useState(false) const [editModal, setEditModal] = useState(false)
const [deleteModal, setDeleteModal] = useState(false) const [deleteModal, setDeleteModal] = useState(false)
const {plants, editPlant, removePlant, addAction, doneTask, history, archivedEntries} = useContext(PlantsContext) const {
plants,
editPlant,
removePlant,
addAction,
doneTask,
history,
archivedEntries,
} = useContext(PlantsContext)
const [plant, setPlant] = useState({}) const [plant, setPlant] = useState({})
const archived = archivedEntries(Number(id)) const archived = archivedEntries(Number(id))
const [actionForm, setActionForm] = useState({}) 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 pictureName = 'picture-' + id
const picture = useRef(null) const picture = useRef(null)
useEffect(() => { useEffect(() => {
const plantFind = plants.find(plant => plant.id === Number(id)) const plantFind = plants.find((plant) => plant.id === Number(id))
setPlant(plantFind) setPlant(plantFind)
}, []) }, [])
@@ -56,89 +62,173 @@ const Plant = ({id}) => {
const handleOpenEditModal = () => setEditModal(true) const handleOpenEditModal = () => setEditModal(true)
const addPicture = e => storePicture(e, id) const addPicture = (e) => storePicture(e, id)
return <PageLayout> return (
<div className="flex justify-between items-center"> <PageLayout>
<h1 className="my-2 flex-1 text-center">{ plant.name }</h1> <div className="flex items-center justify-between">
<h1 className="my-2 flex-1 text-center">{plant.name}</h1>
<div> <div>
<Button className="bg-blue-500 hover:bg-blue-700 mr-2" onClick={() => handleOpenEditModal()}><Text text="Edit" /></Button> <Button
<Button className="bg-red-500 hover:bg-red-700" onClick={() => setDeleteModal(true)}><Text text="Delete" /></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>
</div> </div>
<div className="flex justify-around flex-wrap gap-5"> <div className="flex flex-wrap justify-around gap-5">
<div className="relative"> <div className="relative">
<img id="picture" ref={picture} src={getPicture(id)} alt="" className="inline-block min-w-[300px] min-h-[200px] bg-gray-400"/> <img
<SmallButton className="absolute top-2 left-2 cursor-pointer bg-blue-500 hover:bg-blue-700 max-w-[300px]"> 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"> <label className="relative">
<EditSVG className="w-4 h-4 block z-10 cursor-pointer" /> <EditSVG className="z-10 block h-4 w-4 cursor-pointer" />
<input id="input-file" type="file" name="picture" className="absolute opacity-0 max-w-[300px] w-0 h-0 inline" onChange={addPicture}/> <input
id="input-file"
type="file"
name="picture"
className="absolute inline h-0 w-0 max-w-[300px] opacity-0"
onChange={addPicture}
/>
</label> </label>
</SmallButton> </SmallButton>
</div> </div>
<div className="flex-1"> <div className="flex-1">
{plant.description && <p>{ plant.description }</p>} {plant.description && <p>{plant.description}</p>}
{plant.hasOwnProperty('indoor') && <span className="inline-block bg-green-500 text-white px-2 py-1 text-sm font-semibold rounded shadow my-2"><Text text={ plant.indoor ? 'Indoor' : 'Outdoor' } /></span>} {plant.hasOwnProperty('indoor') && (
{plant.spot && <p><Text text="Spot:" /> <span className="font-bold">{ plant.spot }</span></p>} <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>
<div> <div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h2><Text text="Actions" /></h2> <h2>
<SmallButton className="bg-blue-500 hover:bg-blue-700 mb-2 mt-5" <Text text="Actions" />
</h2>
<SmallButton
className="mb-2 mt-5 bg-blue-500 hover:bg-blue-700"
onClick={() => setAddModal(true)}> onClick={() => setAddModal(true)}>
<PlusSVG className="w-4 h-4" /> <PlusSVG className="h-4 w-4" />
</SmallButton> </SmallButton>
</div> </div>
{plant.actions && plant.actions.map(action => { {plant.actions &&
plant.actions.map((action) => {
let isDone = false let isDone = false
let lastTask = false let lastTask = false
if (history()[action.id]) { if (history()[action.id]) {
lastTask = new Date(history()[action.id]) lastTask = new Date(history()[action.id])
isDone = lastTask.addDays(Number(action.frequency)).toSQLDate() >= (new Date()).toSQLDate() isDone =
lastTask
.addDays(Number(action.frequency))
.toSQLDate() >= new Date().toSQLDate()
} }
return <div key={action.action_type} className="flex items-center gap-2 my-2"> return (
<div
key={action.action_type}
className="my-2 flex items-center gap-2">
<span> <span>
<Button className={classNames(isDone ? "bg-green-500 hover:bg-green-700" : "bg-blue-500 hover:bg-blue-700")} <Button
className={
isDone
? 'bg-green-500 hover:bg-green-700'
: 'bg-blue-500 hover:bg-blue-700'
}
onClick={() => doneTask(action.id)}> onClick={() => doneTask(action.id)}>
<Text text="Done" /> <Text text="Done" />
</Button> </Button>
</span> </span>
<span className="capitalize font-bold"><Text text={action.action_type} /></span> <span className="font-bold capitalize">
{Number(action.frequency) === 0 <Text text={action.action_type} />
? <span><Text text="when you want" /></span> </span>
: <span><Text text="every {{count}} days" count={action.frequency} /></span>} {Number(action.frequency) === 0 ? (
<span className="text-gray-500"><Text text="last task" /> {lastTask ? lastTask.toFrDate() : <Text text="never" />}</span> <span>
<ArchivedAction archived={archived.where('action', actionId(action.action_type))} /> <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>
)
})} })}
</div> </div>
{createPortal( {createPortal(
<Modal isOpen={addModal} onChange={e => closeModal(e, setAddModal)}> <Modal
<ModalTitle><Text text="Add Action" /></ModalTitle> isOpen={addModal}
onChange={(e) => closeModal(e, setAddModal)}>
<ModalTitle>
<Text text="Add Action" />
</ModalTitle>
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<SelectField
<SelectField name="action_type" name="action_type"
className="mb-2 mt-5" className="mb-2 mt-5"
value={actionForm.action_type} value={actionForm.action_type}
options={ACTION_TYPES} options={ACTION_TYPES}
onChange={(e) => setActionForm({ ...actionForm, action_type: e.target.value })}> onChange={(e) =>
setActionForm({
...actionForm,
action_type: e.target.value,
})
}>
<Text text="Name" /> <Text text="Name" />
</SelectField> </SelectField>
<InputField name="frequency" <InputField
name="frequency"
className="mb-5" className="mb-5"
type="number" type="number"
textSupport="A number of days between 2 actions. You can use 0 to set no notification." textSupport="A number of days between 2 actions. You can use 0 to set no notification."
value={actionForm.frequency} value={actionForm.frequency}
onChange={(e) => setActionForm({ ...actionForm, frequency: e.target.value })}> onChange={(e) =>
setActionForm({
...actionForm,
frequency: e.target.value,
})
}>
<Text text="Frequency" /> <Text text="Frequency" />
</InputField> </InputField>
<Button
<Button type="submit" 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"> 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" /> <Text text="Add" />
</Button> </Button>
</form> </form>
@@ -147,39 +237,65 @@ const Plant = ({id}) => {
)} )}
{createPortal( {createPortal(
<Modal isOpen={editModal} onChange={e => closeModal(e, setEditModal)}> <Modal
isOpen={editModal}
onChange={(e) => closeModal(e, setEditModal)}>
<ModalTitle> <ModalTitle>
<Text text="Edit Plant" /> <Text text="Edit Plant" />
</ModalTitle> </ModalTitle>
<PlantForm plant={plant} onChange={handleEditSubmit}><Text text="Edit" /></PlantForm> <PlantForm plant={plant} onChange={handleEditSubmit}>
<Text text="Edit" />
</PlantForm>
</Modal>, </Modal>,
app app
)} )}
{createPortal( {createPortal(
<Modal isOpen={deleteModal} onChange={e => closeModal(e, setDeleteModal)}> <Modal
isOpen={deleteModal}
onChange={(e) => closeModal(e, setDeleteModal)}>
<ModalTitle> <ModalTitle>
<Text text="Delete Plant ?" /> <Text text="Delete Plant ?" />
</ModalTitle> </ModalTitle>
<Button className="bg-red-500 hover:bg-red-700 mt-10" onClick={handleDeletePlant}><Text text="Confirm delete plant" /></Button> <Button
className="mt-10 bg-red-500 hover:bg-red-700"
onClick={handleDeletePlant}>
<Text text="Confirm delete plant" />
</Button>
</Modal>, </Modal>,
app app
)} )}
</PageLayout> </PageLayout>
)
} }
export default Plant export default Plant
const ArchivedAction = ({archived = []}) => { const ArchivedAction = ({ archived = [] }) => {
console.log(archived) console.log(archived)
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
return <> return (
{archived.length > 0 && <span className="relative" onClick={() => setIsOpen(!isOpen)}> <>
{ archived.length } <TriangleSVG className={classNames("w-6 inline transform transition-all duration-300", isOpen ? 'rotate-180' : '')} /> {archived.length > 0 && (
{archived.length > 0 && isOpen && <ul className="absolute z-10 bg-white dark:bg-gray-800 border border-primary p-2 rounded"> <span className="relative" onClick={() => setIsOpen(!isOpen)}>
{archived.sortObjectsBy('time', true).map(archive => <li>{(new Date(archive.time)).toFrDate()}</li>)} {archived.length}{' '}
</ul>} <TriangleSVG
</span>} 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>
)}
</> </>
)
} }

View File

@@ -1,13 +1,13 @@
import { useContext, useEffect, useState } from "preact/hooks"; import { useContext, useEffect, useState } from 'preact/hooks'
import { PageLayout } from "../components/PageLayout"; import { PageLayout } from '../components/PageLayout'
import {PlantsContext, UserContext} from "../Contexts"; import { PlantsContext, UserContext } from '../Contexts'
import {Button} from "../components/Button"; import { Button } from '../components/Button'
import {useLocalStorage} from "../hooks/LocalStorageHook"; import { useLocalStorage } from '../hooks/LocalStorageHook'
import {Text} from "../components/Translation"; import { Text } from '../components/Translation'
export default function Profile() { export default function Profile() {
const [user, setUser] = useContext(UserContext) const [user, setUser] = useContext(UserContext)
const {plants} = useContext(PlantsContext) const { plants } = useContext(PlantsContext)
const [darkMode, setDarkMode] = useState(false) const [darkMode, setDarkMode] = useState(false)
const [data, setData] = useLocalStorage('data', {}) const [data, setData] = useLocalStorage('data', {})
@@ -16,46 +16,55 @@ export default function Profile() {
}, []) }, [])
useEffect(() => { useEffect(() => {
setUser({...user, dark_mode: darkMode}) setUser({ ...user, dark_mode: darkMode })
}, [darkMode]) }, [darkMode])
const handleChangeDarkMode = () => { const handleChangeDarkMode = () => {
setUser({...user, dark_mode: !user.dark_mode}) setUser({ ...user, dark_mode: !user.dark_mode })
} }
const handleDownloadConfig = async () => { const handleDownloadConfig = async () => {
const fileName = "plants" const fileName = 'plants'
const json = JSON.stringify(data) const json = JSON.stringify(data)
const blob = new Blob([json], {type: 'application/json'}) const blob = new Blob([json], { type: 'application/json' })
const href = await URL.createObjectURL(blob) const href = await URL.createObjectURL(blob)
const link = document.createElement('a') const link = document.createElement('a')
link.href = href link.href = href
link.download = fileName + ".json" link.download = fileName + '.json'
document.body.appendChild(link) document.body.appendChild(link)
link.click() link.click()
document.body.removeChild(link) document.body.removeChild(link)
} }
return <PageLayout> return (
<h1 className=""><Text text="Profile" /></h1> <PageLayout>
<h1 className="">
<Text text="Profile" />
</h1>
<label htmlFor="dark_mode"> <label htmlFor="dark_mode">
<input type="checkbox" <input
type="checkbox"
id="dark_mode" id="dark_mode"
name="dark_mode" name="dark_mode"
className="mr-2" className="mr-2"
checked={user.dark_mode} checked={user.dark_mode}
onChange={() => handleChangeDarkMode()} /> onChange={() => handleChangeDarkMode()}
/>
<Text text="Dark Mode" /> <Text text="Dark Mode" />
</label> </label>
<div className="mt-5"> <div className="mt-5">
<Text text="{{count}} Plants" count={plants.length} /> <Text text="{{count}} Plants" count={plants.length} />
</div> </div>
<div className="mt-5"> <div className="mt-5">
<h2><Text text="Download config" /></h2> <h2>
<Button className="bg-primary hover:bg-primary-dark border-primary-dark ring-primary" <Text text="Download config" />
</h2>
<Button
className="border-primary-dark bg-primary ring-primary hover:bg-primary-dark"
onClick={handleDownloadConfig}> onClick={handleDownloadConfig}>
<Text text="Download" /> <Text text="Download" />
</Button> </Button>
</div> </div>
</PageLayout> </PageLayout>
)
} }

View File

@@ -1,9 +1,9 @@
import {PageLayout} from "../components/PageLayout"; import { PageLayout } from '../components/PageLayout'
import {Button} from "../components/Button"; import { Button } from '../components/Button'
export default function Style() { export default function Style() {
return (
return <PageLayout> <PageLayout>
<h1>Style</h1> <h1>Style</h1>
<div className="flex"> <div className="flex">
@@ -20,11 +20,21 @@ export default function Style() {
<div> <div>
<div className="my-2 flex gap-2"> <div className="my-2 flex gap-2">
<Button className="bg-primary hover:bg-primary-dark border-primary-dark ring-primary">Primary</Button> <Button className="border-primary-dark bg-primary ring-primary hover:bg-primary-dark">
<Button className="bg-primary-light hover:bg-primary text-primary hover:text-primary-darkest ring-primary">Smooth</Button> Primary
<Button className="border-primary text-primary ring-primary-dark">Ghost</Button> </Button>
<Button className="border-primary text-primary ring-primary-dark border-transparent">Raised</Button> <Button className="bg-primary-light text-primary ring-primary hover:bg-primary hover:text-primary-darkest">
<Button className="bg-primary hover:bg-primary-dark border-primary-dark ring-primary rounded-full">Previous</Button> 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>
<div className="my-2"> <div className="my-2">
<Button>Secondary</Button> <Button>Secondary</Button>
@@ -58,22 +68,19 @@ export default function Style() {
<div> <div>
<div className="flex"> <div className="flex">
<div className="bg-primary-light my-2 p-2 flex-1"> <div className="my-2 flex-1 bg-primary-light p-2">
Primary light Primary light
</div> </div>
<div className="bg-primary my-2 p-2 flex-1"> <div className="my-2 flex-1 bg-primary p-2">Primary</div>
Primary <div className="my-2 flex-1 bg-primary-dark p-2">
</div>
<div className="bg-primary-dark my-2 p-2 flex-1">
Primary dark Primary dark
</div> </div>
<div className="bg-primary-darkest my-2 p-2 flex-1"> <div className="my-2 flex-1 bg-primary-darkest p-2">
Primary darkest Primary darkest
</div> </div>
</div> </div>
<div className="bg-gray-500 my-2"> <div className="my-2 bg-gray-500">Background Primary</div>
Background Primary
</div>
</div> </div>
</PageLayout> </PageLayout>
)
} }

View File

@@ -1,6 +1,5 @@
export const ACTION_TYPES = ['watering', 'spraying', 'bathing'] export const ACTION_TYPES = ['watering', 'spraying', 'bathing']
export const actionId = (action) => { export const actionId = (action) => {
return ACTION_TYPES.findIndex(item => item === action) return ACTION_TYPES.findIndex((item) => item === action)
} }

View File

@@ -1,4 +0,0 @@
export function classNames() {
// console.log(Object.values(arguments), Object.values(arguments).join(' '));
return Object.values(arguments).join(' ')
}

View File

@@ -1,68 +1,66 @@
const pictureName = (id) => {
const pictureName = id => {
return 'picture-' + id return 'picture-' + id
} }
export const getPicture = id => { export const getPicture = (id) => {
return localStorage.getItem('picture-' + id ?? '') ?? "" return localStorage.getItem(pictureName(id) ?? '') ?? ''
} }
export const storePicture = (e, id) => { export const storePicture = (e, id) => {
const files = e.target.files; const files = e.target.files
let file = files[0]; let file = files[0]
if (file) { if (file) {
const reader = new FileReader(); const reader = new FileReader()
reader.onload = function(e) { reader.onload = function (e) {
document.getElementById("picture").src = e.target.result; document.getElementById('picture').src = e.target.result
}; }
reader.readAsDataURL(file); reader.readAsDataURL(file)
} }
const filesToUploads = document.getElementById("input-file").files; const filesToUploads = document.getElementById('input-file').files
file = filesToUploads[0]; file = filesToUploads[0]
if (file) { if (file) {
const reader = new FileReader() const reader = new FileReader()
let dataPicture let dataPicture
reader.onload = function (e) { reader.onload = function (e) {
let img = document.createElement("img"); let img = document.createElement('img')
img.src = e.target.result; img.src = e.target.result
let canvas = document.createElement("canvas"); let canvas = document.createElement('canvas')
var ctx = canvas.getContext("2d"); var ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0); ctx.drawImage(img, 0, 0)
const MAX_WIDTH = 300; const MAX_WIDTH = 300
const MAX_HEIGHT = 300; const MAX_HEIGHT = 300
let width = img.width; let width = img.width
let height = img.height; let height = img.height
if (width > height) { if (width > height) {
if (width > MAX_WIDTH) { if (width > MAX_WIDTH) {
height *= MAX_WIDTH / width; height *= MAX_WIDTH / width
width = MAX_WIDTH; width = MAX_WIDTH
} }
} else { } else {
if (height > MAX_HEIGHT) { if (height > MAX_HEIGHT) {
width *= MAX_HEIGHT / height; width *= MAX_HEIGHT / height
height = MAX_HEIGHT; height = MAX_HEIGHT
} }
} }
canvas.width = width; canvas.width = width
canvas.height = height; canvas.height = height
var ctx = canvas.getContext("2d"); var ctx = canvas.getContext('2d')
ctx.drawImage(img, 0, 0, width, height); ctx.drawImage(img, 0, 0, width, height)
dataPicture = canvas.toDataURL(file.type); dataPicture = canvas.toDataURL(file.type)
document.getElementById("picture").src = dataPicture; document.getElementById('picture').src = dataPicture
setTimeout(() => { setTimeout(() => {
localStorage.setItem(pictureName(id), dataPicture) localStorage.setItem(pictureName(id), dataPicture)
}, 500) }, 500)
}
}; reader.readAsDataURL(file)
reader.readAsDataURL(file);
} }
} }