feat: project updated/created date + sorting on homepage
This commit is contained in:
parent
18f43482e0
commit
3fc3d813d3
|
@ -13,7 +13,7 @@
|
|||
"@types/json-merge-patch": "0.0.4",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"bs58": "^4.0.1",
|
||||
"bulma": "^0.8.2",
|
||||
"bulma": "^0.9.4",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"json-merge-patch": "^0.2.3",
|
||||
"react": "^16.13.1",
|
||||
|
@ -1232,9 +1232,9 @@
|
|||
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
|
||||
},
|
||||
"node_modules/bulma": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.8.2.tgz",
|
||||
"integrity": "sha512-vMM/ijYSxX+Sm+nD7Lmc1UgWDy2JcL2nTKqwgEqXuOMU+IGALbXd5MLt/BcjBAPLIx36TtzhzBcSnOP974gcqA=="
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz",
|
||||
"integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ=="
|
||||
},
|
||||
"node_modules/bulma-switch": {
|
||||
"version": "2.0.0",
|
||||
|
@ -9952,9 +9952,9 @@
|
|||
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
|
||||
},
|
||||
"bulma": {
|
||||
"version": "0.8.2",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.8.2.tgz",
|
||||
"integrity": "sha512-vMM/ijYSxX+Sm+nD7Lmc1UgWDy2JcL2nTKqwgEqXuOMU+IGALbXd5MLt/BcjBAPLIx36TtzhzBcSnOP974gcqA=="
|
||||
"version": "0.9.4",
|
||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz",
|
||||
"integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ=="
|
||||
},
|
||||
"bulma-switch": {
|
||||
"version": "2.0.0",
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
"@types/json-merge-patch": "0.0.4",
|
||||
"@types/react-router-dom": "^5.1.5",
|
||||
"bs58": "^4.0.1",
|
||||
"bulma": "^0.8.2",
|
||||
"bulma": "^0.9.4",
|
||||
"bulma-switch": "^2.0.0",
|
||||
"json-merge-patch": "^0.2.3",
|
||||
"react": "^16.13.1",
|
||||
|
|
|
@ -83,7 +83,8 @@ export function handleAddTask(project: Project, action: AddTask): Project {
|
|||
tasks: {
|
||||
...project.tasks,
|
||||
[task.id]: task,
|
||||
}
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -102,7 +103,8 @@ export function handleRemoveTask(project: Project, action: RemoveTask): Project
|
|||
delete tasks[action.id];
|
||||
return {
|
||||
...project,
|
||||
tasks
|
||||
tasks,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -131,8 +133,9 @@ export function handleUpdateTaskEstimation(project: Project, action: UpdateTaskE
|
|||
[action.id]: {
|
||||
...project.tasks[action.id],
|
||||
estimations: estimations,
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -150,7 +153,8 @@ export function updateProjectLabel(label: string): UpdateProjectLabel {
|
|||
export function handleUpdateProjectLabel(project: Project, action: UpdateProjectLabel): Project {
|
||||
return {
|
||||
...project,
|
||||
label: action.label
|
||||
label: action.label,
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -174,7 +178,8 @@ export function handleUpdateTaskLabel(project: Project, action: UpdateTaskLabel)
|
|||
...project.tasks[action.id],
|
||||
label: action.label,
|
||||
}
|
||||
}
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -195,7 +200,8 @@ export function handleUpdateParam(project: Project, action: UpdateParam): Projec
|
|||
params: {
|
||||
...project.params,
|
||||
[action.name]: action.value,
|
||||
}
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -222,7 +228,8 @@ export function handleUpdateTaskCategoryLabel(project: Project, action: UpdateTa
|
|||
label: action.label
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -249,7 +256,8 @@ export function handleUpdateTaskCategoryCost(project: Project, action: UpdateTas
|
|||
costPerTimeUnit: action.costPerTimeUnit
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -273,7 +281,8 @@ export function handleAddTaskCategory(project: Project, action: AddTaskCategory)
|
|||
...project.params.taskCategories,
|
||||
[taskCategory.id]: taskCategory,
|
||||
}
|
||||
}
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -295,7 +304,8 @@ export function handleRemoveTaskCategory(project: Project, action: RemoveTaskCat
|
|||
params: {
|
||||
...project.params,
|
||||
taskCategories
|
||||
}
|
||||
},
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -313,5 +323,8 @@ export function handlePatchProject(project: Project, action: PatchProject): Proj
|
|||
const p = diff(project, action.from);
|
||||
console.log('patch to apply', p);
|
||||
if (!p) return project;
|
||||
return applyPatch(project, p) as Project;
|
||||
const patched: Project = applyPatch(project, p) as Project
|
||||
if (typeof patched.createdAt === 'string') patched.createdAt = new Date(patched.createdAt)
|
||||
if (typeof patched.updatedAt === 'string') patched.updatedAt = new Date(patched.updatedAt)
|
||||
return patched;
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
import { useCallback, useEffect, useState } from "react";
|
||||
|
||||
export enum Direction {
|
||||
ASC = 1,
|
||||
DESC = -1
|
||||
}
|
||||
|
||||
export function useSort<T>(items: T[], key: string, direction: Direction = Direction.ASC): T[] {
|
||||
const [ sorted, setSorted ] = useState(items);
|
||||
useEffect(() => {
|
||||
const sorted = [ ...items ]
|
||||
|
||||
sorted.sort((a: any, b: any) => {
|
||||
if (a[key] > b[key]) return direction;
|
||||
if (a[key] < b[key]) return -direction;
|
||||
return 0
|
||||
})
|
||||
|
||||
setSorted(sorted)
|
||||
}, [key, items, direction])
|
||||
|
||||
return sorted
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import { Task, TaskCategory, TaskID } from './task';
|
||||
import { Task } from './task';
|
||||
import { Params, defaults } from "./params";
|
||||
import { uuidV4 } from "../util/uuid";
|
||||
|
||||
|
@ -10,6 +10,8 @@ export interface Project {
|
|||
description: string
|
||||
tasks: Tasks
|
||||
params: Params
|
||||
createdAt: Date
|
||||
updatedAt: Date
|
||||
}
|
||||
|
||||
export interface Tasks {
|
||||
|
@ -25,5 +27,7 @@ export function newProject(id?: string): Project {
|
|||
params: {
|
||||
...defaults
|
||||
},
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
}
|
|
@ -1,58 +1,109 @@
|
|||
import React, { FunctionComponent } from "react";
|
||||
import React, { FunctionComponent, MouseEvent, useCallback, useState } from "react";
|
||||
import style from "./style.module.css";
|
||||
import { useHistory } from 'react-router';
|
||||
import { base58UUID } from '../../util/uuid';
|
||||
import { useStoredProjectList } from "../../hooks/use-stored-project-list";
|
||||
import { formatDate } from "../../util/date";
|
||||
import { Direction, useSort } from "../../hooks/useSort";
|
||||
|
||||
const Home: FunctionComponent = () => {
|
||||
const [ projects, refreshProjects ] = useStoredProjectList();
|
||||
const history = useHistory();
|
||||
const [projects] = useStoredProjectList()
|
||||
const [sortingKey, setSortingKey] = useState('updatedAt')
|
||||
const [sortingDirection, setSortingDirection] = useState<Direction>(Direction.DESC)
|
||||
const sortedProjects = useSort(projects, sortingKey, sortingDirection)
|
||||
const history = useHistory()
|
||||
|
||||
const openNewProject = () => {
|
||||
const uuid = base58UUID();
|
||||
history.push(`/p/${uuid}`);
|
||||
};
|
||||
const openNewProject = () => {
|
||||
const uuid = base58UUID()
|
||||
history.push(`/p/${uuid}`)
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`container ${style.home}`}>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<div className="buttons is-right">
|
||||
<button className="button is-primary"
|
||||
onClick={openNewProject}>
|
||||
<strong>+</strong> Nouveau projet
|
||||
</button>
|
||||
</div>
|
||||
<div className="panel">
|
||||
<p className="panel-heading">
|
||||
Mes projets
|
||||
</p>
|
||||
{/* <div className="panel-block">
|
||||
<p className="control has-icons-left">
|
||||
<input className="input" type="text" placeholder="Search" />
|
||||
<span className="icon is-left">🔍</span>
|
||||
</p>
|
||||
</div> */}
|
||||
{
|
||||
projects.map(p => (
|
||||
<a key={`project-${p.id}`} className="panel-block" href={`/p/${p.id}`}>
|
||||
<span className="panel-icon">🗒️</span>
|
||||
{ p.label ? p.label : "Projet sans nom" }
|
||||
</a>
|
||||
))
|
||||
}
|
||||
{
|
||||
projects.length === 0 ?
|
||||
<p className="panel-block">
|
||||
<div className={style.noProjects}>Aucun project pour l'instant.</div>
|
||||
</p> :
|
||||
null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
const openProject = useCallback((evt: MouseEvent<HTMLTableRowElement>) => {
|
||||
const projectId = evt.currentTarget.dataset.projectId;
|
||||
history.push(`/p/${projectId}`)
|
||||
}, [])
|
||||
|
||||
const sortBy = useCallback((evt: MouseEvent<HTMLTableCellElement>) => {
|
||||
const key = evt.currentTarget.dataset.sortKey
|
||||
if (!key) return
|
||||
|
||||
if (sortingKey !== key) {
|
||||
setSortingKey(key)
|
||||
return
|
||||
}
|
||||
|
||||
setSortingDirection(sortingDirection => -sortingDirection)
|
||||
|
||||
}, [sortingKey, sortingDirection])
|
||||
|
||||
return (
|
||||
<div className={`container ${style.home}`}>
|
||||
<div className="columns">
|
||||
<div className="column">
|
||||
<div className="buttons is-right">
|
||||
<button className="button is-primary is-medium"
|
||||
onClick={openNewProject}>
|
||||
<strong>+</strong> Nouveau projet
|
||||
</button>
|
||||
</div>
|
||||
<div className="box">
|
||||
<div className="table-container">
|
||||
<table className="table is-fullwidth is-hoverable">
|
||||
<thead>
|
||||
<tr className="is-size-5">
|
||||
<TableHeader onClick={sortBy} label="Projet" isCurrentSortingKey={sortingKey === 'label'} sortingDirection={sortingDirection} sortingKey="label" />
|
||||
<TableHeader onClick={sortBy} label="Mis à jour le" isCurrentSortingKey={sortingKey === 'updatedAt'} sortingDirection={sortingDirection} sortingKey="updatedAt" />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{
|
||||
sortedProjects.map(p => (
|
||||
<tr
|
||||
key={`project-${p.id}`}
|
||||
data-project-id={p.id}
|
||||
onClick={openProject}
|
||||
className="is-clickable">
|
||||
<td>
|
||||
<span className="mr-3">🗒️</span>
|
||||
{p.label ? p.label : "Projet sans nom"}
|
||||
</td>
|
||||
<td>
|
||||
{p.updatedAt ? formatDate(p.updatedAt) : "--"}
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
}
|
||||
{
|
||||
projects.length === 0 ?
|
||||
<tr className={style.noProjects}>
|
||||
<td colSpan={2}>
|
||||
Aucun project pour l'instant.
|
||||
</td>
|
||||
</tr> :
|
||||
null
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
interface TableHeaderProps {
|
||||
onClick?: React.MouseEventHandler<HTMLTableCellElement>
|
||||
sortingKey: string
|
||||
isCurrentSortingKey: boolean
|
||||
sortingDirection: Direction
|
||||
label: string
|
||||
}
|
||||
|
||||
const TableHeader: FunctionComponent<TableHeaderProps> = ({ onClick, isCurrentSortingKey, sortingKey, label, sortingDirection }) => {
|
||||
return (
|
||||
<th className="is-clickable" onClick={onClick} data-sort-key={sortingKey}>{label}<span className={`ml-1 ${!isCurrentSortingKey ? 'is-hidden' : ''}`}>{sortingDirection === Direction.ASC ? '↑' : '↓'}</span></th>
|
||||
)
|
||||
}
|
||||
|
||||
export default Home;
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
export function asDate (d: Date|string): Date {
|
||||
if (typeof d === 'string') return new Date(d)
|
||||
if (d instanceof Date) return d
|
||||
|
||||
throw new Error(`Unexpected date value '${JSON.stringify(d)}' !`)
|
||||
}
|
||||
|
||||
const intl = Intl.DateTimeFormat(navigator.language, {
|
||||
weekday: 'long',
|
||||
month: 'short',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
second: 'numeric'
|
||||
})
|
||||
|
||||
export function formatDate (d: Date|string): string {
|
||||
d = asDate(d)
|
||||
return intl.format(d)
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type ProjectID string
|
||||
|
||||
type ProjectEntry struct {
|
||||
|
@ -14,6 +16,8 @@ type Project struct {
|
|||
Description string `json:"description"`
|
||||
Tasks map[TaskID]Task `json:"tasks"`
|
||||
Params Params `json:"params"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type Params struct {
|
||||
|
|
Loading…
Reference in New Issue