Local storage basic persistence
This commit is contained in:
parent
3ebb707fd3
commit
10983f6ac4
8
Makefile
8
Makefile
|
@ -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
|
|
@ -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>
|
||||||
|
|
|
@ -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];
|
||||||
|
}
|
|
@ -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:
|
|
@ -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];
|
||||||
|
}
|
|
@ -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: {},
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -1,3 +1,9 @@
|
||||||
.home {
|
.home {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.noProjects {
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
|
@ -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;
|
||||||
|
|
|
@ -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"}
|
||||||
|
|
||||||
|
<i class="icon is-size-4">🖋️</i>
|
||||||
|
</h3>
|
||||||
<div class="tabs">
|
<div class="tabs">
|
||||||
<ul>
|
<ul>
|
||||||
<li class="is-active">
|
<li class="is-active">
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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}`
|
||||||
|
}
|
Loading…
Reference in New Issue