Local storage basic persistence

This commit is contained in:
wpetit 2020-04-20 14:07:26 +02:00
parent 3ebb707fd3
commit 10983f6ac4
12 changed files with 150 additions and 14 deletions

View File

@ -1,5 +1,11 @@
DOKKU_URL := dokku@dev.lookingfora.name:guesstimate DOKKU_URL := dokku@dev.lookingfora.name:guesstimate
watch:
npm run dev
build:
npm run build
dokku-build: dokku-build:
docker build \ docker build \
-t guesstimate-dokku:latest \ -t guesstimate-dokku:latest \
@ -10,4 +16,4 @@ dokku-run:
dokku-deploy: dokku-deploy:
$(if $(shell git config remote.dokku.url),, git remote add dokku $(DOKKU_URL)) $(if $(shell git config remote.dokku.url),, git remote add dokku $(DOKKU_URL))
git push -f dokku $(shell git rev-parse HEAD):master git push -f dokku $(shell git rev-parse HEAD):refs/heads/master

View File

@ -23,7 +23,7 @@ const App: FunctionalComponent = () => {
<Header /> <Header />
<Router onChange={handleRoute}> <Router onChange={handleRoute}>
<Route path="/" component={Home} /> <Route path="/" component={Home} />
<Route path="/p/:uuid" component={Project} /> <Route path="/p/:projectId" component={Project} />
<NotFoundPage default /> <NotFoundPage default />
</Router> </Router>
</div> </div>

View File

@ -0,0 +1,37 @@
import { useState } from "preact/hooks";
export function useLocalStorage<T>(key: string, initialValue: T) {
// State to store our value
// Pass initial state function to useState so logic is only executed once
const [storedValue, setStoredValue] = useState(() => {
try {
// Get from local storage by key
const item = window.localStorage.getItem(key);
// Parse stored json or if none return initialValue
return item ? JSON.parse(item) : initialValue;
} catch (error) {
// If error also return initialValue
console.error(error);
return initialValue;
}
});
// Return a wrapped version of useState's setter function that ...
// ... persists the new value to localStorage.
const setValue = (value: T) => {
try {
// Allow value to be a function so we have same API as useState
const valueToStore =
value instanceof Function ? value(storedValue) : value;
// Save state
setStoredValue(valueToStore);
// Save to local storage
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
// A more advanced implementation would handle the error case
console.error(error);
}
};
return [storedValue, setValue];
}

View File

@ -1,5 +1,6 @@
import { Project } from "../models/project"; import { Project } from "../models/project";
import { Task, TaskID, EstimationConfidence } from "../models/task"; import { Task, TaskID, EstimationConfidence } from "../models/task";
import { useReducer } from "preact/hooks";
export interface Action { export interface Action {
type: string type: string
@ -10,6 +11,10 @@ export type ProjectReducerActions =
RemoveTaskAction | RemoveTaskAction |
UpdateTaskEstimation UpdateTaskEstimation
export function useProjectReducer(project: Project) {
return useReducer(projectReducer, project);
}
export function projectReducer(project: Project, action: ProjectReducerActions): Project { export function projectReducer(project: Project, action: ProjectReducerActions): Project {
switch(action.type) { switch(action.type) {
case ADD_TASK: case ADD_TASK:

View File

@ -0,0 +1,35 @@
import {Project} from "../models/project";
import { useState } from "preact/hooks";
import { ProjectStorageKeyPrefix } from "../util/storage";
export function loadStoredProjects(): Project[] {
const projects: Project[] = [];
Object.keys(window.localStorage).forEach(key => {
if (key.startsWith(ProjectStorageKeyPrefix)) {
try {
const data = window.localStorage.getItem(key);
if (data) {
const project = JSON.parse(data);
projects.push(project);
}
} catch(err) {
console.error(err);
}
}
});
return projects
}
export function useStoredProjectList(): [Project[], () => void] {
const [ projects, setProjects ] = useState(() => {
return loadStoredProjects();
});
const refresh = () => {
setProjects(loadStoredProjects());
};
return [ projects, refresh];
}

View File

@ -16,9 +16,9 @@ export interface Tasks {
[id: string]: Task [id: string]: Task
} }
export function newProject(): Project { export function newProject(id?: string): Project {
return { return {
id: uuidV4(), id: id ? id : uuidV4(),
label: "", label: "",
description: "", description: "",
tasks: {}, tasks: {},

View File

@ -2,8 +2,10 @@ import { FunctionalComponent, h } from "preact";
import * as style from "./style.css"; import * as style from "./style.css";
import { route } from 'preact-router'; import { route } from 'preact-router';
import { base58UUID } from '../../util/uuid'; import { base58UUID } from '../../util/uuid';
import { useStoredProjectList } from "../../hooks/use-stored-project-list";
const Home: FunctionalComponent = () => { const Home: FunctionalComponent = () => {
const [ projects, refreshProjects ] = useStoredProjectList();
const openNewProject = () => { const openNewProject = () => {
const uuid = base58UUID(); const uuid = base58UUID();
@ -24,16 +26,27 @@ const Home: FunctionalComponent = () => {
<p class="panel-heading"> <p class="panel-heading">
Mes projets Mes projets
</p> </p>
<div class="panel-block"> {/* <div class="panel-block">
<p class="control has-icons-left"> <p class="control has-icons-left">
<input class="input" type="text" placeholder="Search" /> <input class="input" type="text" placeholder="Search" />
<span class="icon is-left">🔍</span> <span class="icon is-left">🔍</span>
</p> </p>
</div> </div> */}
<a class="panel-block"> {
<span class="panel-icon">🗒</span> projects.map(p => (
Projet #1 <a class="panel-block" href={`/p/${p.id}`}>
</a> <span class="panel-icon">🗒</span>
{ p.label ? p.label : "Projet sans nom" }
</a>
))
}
{
projects.length === 0 ?
<p class="panel-block">
<div class={style.noProjects}>Aucun project pour l'instant.</div>
</p> :
null
}
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,3 +1,9 @@
.home { .home {
height: 100%; height: 100%;
} }
.noProjects {
width: 100%;
text-align: center;
font-style: italic;
}

View File

@ -1,2 +1,3 @@
// This file is automatically generated from your CSS. Any edits will be overwritten. // This file is automatically generated from your CSS. Any edits will be overwritten.
export const home: string; export const home: string;
export const noProjects: string;

View File

@ -1,15 +1,23 @@
import { FunctionalComponent, h } from "preact"; import { FunctionalComponent, h } from "preact";
import { useReducer } from "preact/hooks"; import { useEffect } from "preact/hooks";
import * as style from "./style.css"; import * as style from "./style.css";
import { newProject } from "../../models/project"; import { newProject } from "../../models/project";
import TaskTable from "./tasks-table"; import TaskTable from "./tasks-table";
import TimePreview from "./time-preview"; import TimePreview from "./time-preview";
import FinancialPreview from "./financial-preview"; import FinancialPreview from "./financial-preview";
import { projectReducer, addTask, updateTaskEstimation, removeTask } from "../../hooks/project-reducer"; import { useProjectReducer, addTask, updateTaskEstimation, removeTask } from "../../hooks/use-project-reducer";
import { Task, TaskID, EstimationConfidence } from "../../models/task"; import { Task, TaskID, EstimationConfidence } from "../../models/task";
import { getProjectStorageKey } from "../../util/storage";
import { useLocalStorage } from "../../hooks/use-local-storage";
const Project: FunctionalComponent = () => { export interface ProjectProps {
const [project, dispatch] = useReducer(projectReducer, newProject()); projectId: string
}
const Project: FunctionalComponent<ProjectProps> = ({ projectId }) => {
const projectStorageKey = getProjectStorageKey(projectId);
const [ storedProject, storeProject ] = useLocalStorage(projectStorageKey, newProject(projectId));
const [ project, dispatch ] = useProjectReducer(storedProject);
const onTaskAdd = (task: Task) => { const onTaskAdd = (task: Task) => {
dispatch(addTask(task)); dispatch(addTask(task));
@ -23,8 +31,18 @@ const Project: FunctionalComponent = () => {
dispatch(updateTaskEstimation(taskId, confidence, value)); dispatch(updateTaskEstimation(taskId, confidence, value));
}; };
// Save project in local storage on change
useEffect(()=> {
storeProject(project);
}, [project]);
return ( return (
<div class={`container ${style.estimation}`}> <div class={`container ${style.estimation}`}>
<h3 class="is-size-3">
{project.label ? project.label : "Projet sans nom"}
&nbsp;
<i class="icon is-size-4">🖋</i>
</h3>
<div class="tabs"> <div class="tabs">
<ul> <ul>
<li class="is-active"> <li class="is-active">

View File

@ -51,6 +51,13 @@ const TimePreview: FunctionalComponent<TimePreviewProps> = ({ project }) => {
<td>{`${estimations.p68.e} ± ${estimations.p68.sd} j/h`}</td> <td>{`${estimations.p68.e} ± ${estimations.p68.sd} j/h`}</td>
</tr> </tr>
</tbody> </tbody>
<tfoot>
<tr>
<td colSpan={2}>
<a class="is-small is-pulled-right" href="https://en.wikipedia.org/wiki/Three-point_estimation" target="_blank"> Estimation à 3 points</a>
</td>
</tr>
</tfoot>
</table> </table>
</div> </div>
); );

8
src/util/storage.ts Normal file
View File

@ -0,0 +1,8 @@
import { useState } from "preact/hooks/src";
import { ProjectID } from "../models/project";
export const ProjectStorageKeyPrefix = "project-";
export function getProjectStorageKey(id: ProjectID): string {
return `${ProjectStorageKeyPrefix}${id}`
}