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
watch:
npm run dev
build:
npm run build
dokku-build:
docker build \
-t guesstimate-dokku:latest \
@ -10,4 +16,4 @@ dokku-run:
dokku-deploy:
$(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 />
<Router onChange={handleRoute}>
<Route path="/" component={Home} />
<Route path="/p/:uuid" component={Project} />
<Route path="/p/:projectId" component={Project} />
<NotFoundPage default />
</Router>
</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 { Task, TaskID, EstimationConfidence } from "../models/task";
import { useReducer } from "preact/hooks";
export interface Action {
type: string
@ -10,6 +11,10 @@ export type ProjectReducerActions =
RemoveTaskAction |
UpdateTaskEstimation
export function useProjectReducer(project: Project) {
return useReducer(projectReducer, project);
}
export function projectReducer(project: Project, action: ProjectReducerActions): Project {
switch(action.type) {
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
}
export function newProject(): Project {
export function newProject(id?: string): Project {
return {
id: uuidV4(),
id: id ? id : uuidV4(),
label: "",
description: "",
tasks: {},

View File

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

View File

@ -1,3 +1,9 @@
.home {
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.
export const home: string;
export const noProjects: string;

View File

@ -1,15 +1,23 @@
import { FunctionalComponent, h } from "preact";
import { useReducer } from "preact/hooks";
import { useEffect } from "preact/hooks";
import * as style from "./style.css";
import { newProject } from "../../models/project";
import TaskTable from "./tasks-table";
import TimePreview from "./time-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 { getProjectStorageKey } from "../../util/storage";
import { useLocalStorage } from "../../hooks/use-local-storage";
const Project: FunctionalComponent = () => {
const [project, dispatch] = useReducer(projectReducer, newProject());
export interface ProjectProps {
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) => {
dispatch(addTask(task));
@ -23,8 +31,18 @@ const Project: FunctionalComponent = () => {
dispatch(updateTaskEstimation(taskId, confidence, value));
};
// Save project in local storage on change
useEffect(()=> {
storeProject(project);
}, [project]);
return (
<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">
<ul>
<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>
</tr>
</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>
</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}`
}