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/json-merge-patch": "0.0.4",
|
||||||
"@types/react-router-dom": "^5.1.5",
|
"@types/react-router-dom": "^5.1.5",
|
||||||
"bs58": "^4.0.1",
|
"bs58": "^4.0.1",
|
||||||
"bulma": "^0.8.2",
|
"bulma": "^0.9.4",
|
||||||
"bulma-switch": "^2.0.0",
|
"bulma-switch": "^2.0.0",
|
||||||
"json-merge-patch": "^0.2.3",
|
"json-merge-patch": "^0.2.3",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
|
@ -1232,9 +1232,9 @@
|
||||||
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
|
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
|
||||||
},
|
},
|
||||||
"node_modules/bulma": {
|
"node_modules/bulma": {
|
||||||
"version": "0.8.2",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz",
|
||||||
"integrity": "sha512-vMM/ijYSxX+Sm+nD7Lmc1UgWDy2JcL2nTKqwgEqXuOMU+IGALbXd5MLt/BcjBAPLIx36TtzhzBcSnOP974gcqA=="
|
"integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ=="
|
||||||
},
|
},
|
||||||
"node_modules/bulma-switch": {
|
"node_modules/bulma-switch": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
@ -9952,9 +9952,9 @@
|
||||||
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
|
"integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug="
|
||||||
},
|
},
|
||||||
"bulma": {
|
"bulma": {
|
||||||
"version": "0.8.2",
|
"version": "0.9.4",
|
||||||
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.8.2.tgz",
|
"resolved": "https://registry.npmjs.org/bulma/-/bulma-0.9.4.tgz",
|
||||||
"integrity": "sha512-vMM/ijYSxX+Sm+nD7Lmc1UgWDy2JcL2nTKqwgEqXuOMU+IGALbXd5MLt/BcjBAPLIx36TtzhzBcSnOP974gcqA=="
|
"integrity": "sha512-86FlT5+1GrsgKbPLRRY7cGDg8fsJiP/jzTqXXVqiUZZ2aZT8uemEOHlU1CDU+TxklPEZ11HZNNWclRBBecP4CQ=="
|
||||||
},
|
},
|
||||||
"bulma-switch": {
|
"bulma-switch": {
|
||||||
"version": "2.0.0",
|
"version": "2.0.0",
|
||||||
|
|
|
@ -8,7 +8,7 @@
|
||||||
"@types/json-merge-patch": "0.0.4",
|
"@types/json-merge-patch": "0.0.4",
|
||||||
"@types/react-router-dom": "^5.1.5",
|
"@types/react-router-dom": "^5.1.5",
|
||||||
"bs58": "^4.0.1",
|
"bs58": "^4.0.1",
|
||||||
"bulma": "^0.8.2",
|
"bulma": "^0.9.4",
|
||||||
"bulma-switch": "^2.0.0",
|
"bulma-switch": "^2.0.0",
|
||||||
"json-merge-patch": "^0.2.3",
|
"json-merge-patch": "^0.2.3",
|
||||||
"react": "^16.13.1",
|
"react": "^16.13.1",
|
||||||
|
|
|
@ -83,7 +83,8 @@ export function handleAddTask(project: Project, action: AddTask): Project {
|
||||||
tasks: {
|
tasks: {
|
||||||
...project.tasks,
|
...project.tasks,
|
||||||
[task.id]: task,
|
[task.id]: task,
|
||||||
}
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,7 +103,8 @@ export function handleRemoveTask(project: Project, action: RemoveTask): Project
|
||||||
delete tasks[action.id];
|
delete tasks[action.id];
|
||||||
return {
|
return {
|
||||||
...project,
|
...project,
|
||||||
tasks
|
tasks,
|
||||||
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -131,8 +133,9 @@ export function handleUpdateTaskEstimation(project: Project, action: UpdateTaskE
|
||||||
[action.id]: {
|
[action.id]: {
|
||||||
...project.tasks[action.id],
|
...project.tasks[action.id],
|
||||||
estimations: estimations,
|
estimations: estimations,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,7 +153,8 @@ export function updateProjectLabel(label: string): UpdateProjectLabel {
|
||||||
export function handleUpdateProjectLabel(project: Project, action: UpdateProjectLabel): Project {
|
export function handleUpdateProjectLabel(project: Project, action: UpdateProjectLabel): Project {
|
||||||
return {
|
return {
|
||||||
...project,
|
...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],
|
...project.tasks[action.id],
|
||||||
label: action.label,
|
label: action.label,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -195,7 +200,8 @@ export function handleUpdateParam(project: Project, action: UpdateParam): Projec
|
||||||
params: {
|
params: {
|
||||||
...project.params,
|
...project.params,
|
||||||
[action.name]: action.value,
|
[action.name]: action.value,
|
||||||
}
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,7 +228,8 @@ export function handleUpdateTaskCategoryLabel(project: Project, action: UpdateTa
|
||||||
label: action.label
|
label: action.label
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -249,7 +256,8 @@ export function handleUpdateTaskCategoryCost(project: Project, action: UpdateTas
|
||||||
costPerTimeUnit: action.costPerTimeUnit
|
costPerTimeUnit: action.costPerTimeUnit
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,7 +281,8 @@ export function handleAddTaskCategory(project: Project, action: AddTaskCategory)
|
||||||
...project.params.taskCategories,
|
...project.params.taskCategories,
|
||||||
[taskCategory.id]: taskCategory,
|
[taskCategory.id]: taskCategory,
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -295,7 +304,8 @@ export function handleRemoveTaskCategory(project: Project, action: RemoveTaskCat
|
||||||
params: {
|
params: {
|
||||||
...project.params,
|
...project.params,
|
||||||
taskCategories
|
taskCategories
|
||||||
}
|
},
|
||||||
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -313,5 +323,8 @@ export function handlePatchProject(project: Project, action: PatchProject): Proj
|
||||||
const p = diff(project, action.from);
|
const p = diff(project, action.from);
|
||||||
console.log('patch to apply', p);
|
console.log('patch to apply', p);
|
||||||
if (!p) return project;
|
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 { Params, defaults } from "./params";
|
||||||
import { uuidV4 } from "../util/uuid";
|
import { uuidV4 } from "../util/uuid";
|
||||||
|
|
||||||
|
@ -10,6 +10,8 @@ export interface Project {
|
||||||
description: string
|
description: string
|
||||||
tasks: Tasks
|
tasks: Tasks
|
||||||
params: Params
|
params: Params
|
||||||
|
createdAt: Date
|
||||||
|
updatedAt: Date
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Tasks {
|
export interface Tasks {
|
||||||
|
@ -25,5 +27,7 @@ export function newProject(id?: string): Project {
|
||||||
params: {
|
params: {
|
||||||
...defaults
|
...defaults
|
||||||
},
|
},
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
};
|
};
|
||||||
}
|
}
|
|
@ -1,53 +1,90 @@
|
||||||
import React, { FunctionComponent } from "react";
|
import React, { FunctionComponent, MouseEvent, useCallback, useState } from "react";
|
||||||
import style from "./style.module.css";
|
import style from "./style.module.css";
|
||||||
import { useHistory } from 'react-router';
|
import { useHistory } from 'react-router';
|
||||||
import { base58UUID } from '../../util/uuid';
|
import { base58UUID } from '../../util/uuid';
|
||||||
import { useStoredProjectList } from "../../hooks/use-stored-project-list";
|
import { useStoredProjectList } from "../../hooks/use-stored-project-list";
|
||||||
|
import { formatDate } from "../../util/date";
|
||||||
|
import { Direction, useSort } from "../../hooks/useSort";
|
||||||
|
|
||||||
const Home: FunctionComponent = () => {
|
const Home: FunctionComponent = () => {
|
||||||
const [ projects, refreshProjects ] = useStoredProjectList();
|
const [projects] = useStoredProjectList()
|
||||||
const history = useHistory();
|
const [sortingKey, setSortingKey] = useState('updatedAt')
|
||||||
|
const [sortingDirection, setSortingDirection] = useState<Direction>(Direction.DESC)
|
||||||
|
const sortedProjects = useSort(projects, sortingKey, sortingDirection)
|
||||||
|
const history = useHistory()
|
||||||
|
|
||||||
const openNewProject = () => {
|
const openNewProject = () => {
|
||||||
const uuid = base58UUID();
|
const uuid = base58UUID()
|
||||||
history.push(`/p/${uuid}`);
|
history.push(`/p/${uuid}`)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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 (
|
return (
|
||||||
<div className={`container ${style.home}`}>
|
<div className={`container ${style.home}`}>
|
||||||
<div className="columns">
|
<div className="columns">
|
||||||
<div className="column">
|
<div className="column">
|
||||||
<div className="buttons is-right">
|
<div className="buttons is-right">
|
||||||
<button className="button is-primary"
|
<button className="button is-primary is-medium"
|
||||||
onClick={openNewProject}>
|
onClick={openNewProject}>
|
||||||
<strong>+</strong> Nouveau projet
|
<strong>+</strong> Nouveau projet
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div className="panel">
|
<div className="box">
|
||||||
<p className="panel-heading">
|
<div className="table-container">
|
||||||
Mes projets
|
<table className="table is-fullwidth is-hoverable">
|
||||||
</p>
|
<thead>
|
||||||
{/* <div className="panel-block">
|
<tr className="is-size-5">
|
||||||
<p className="control has-icons-left">
|
<TableHeader onClick={sortBy} label="Projet" isCurrentSortingKey={sortingKey === 'label'} sortingDirection={sortingDirection} sortingKey="label" />
|
||||||
<input className="input" type="text" placeholder="Search" />
|
<TableHeader onClick={sortBy} label="Mis à jour le" isCurrentSortingKey={sortingKey === 'updatedAt'} sortingDirection={sortingDirection} sortingKey="updatedAt" />
|
||||||
<span className="icon is-left">🔍</span>
|
</tr>
|
||||||
</p>
|
</thead>
|
||||||
</div> */}
|
<tbody>
|
||||||
{
|
{
|
||||||
projects.map(p => (
|
sortedProjects.map(p => (
|
||||||
<a key={`project-${p.id}`} className="panel-block" href={`/p/${p.id}`}>
|
<tr
|
||||||
<span className="panel-icon">🗒️</span>
|
key={`project-${p.id}`}
|
||||||
{ p.label ? p.label : "Projet sans nom" }
|
data-project-id={p.id}
|
||||||
</a>
|
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 ?
|
projects.length === 0 ?
|
||||||
<p className="panel-block">
|
<tr className={style.noProjects}>
|
||||||
<div className={style.noProjects}>Aucun project pour l'instant.</div>
|
<td colSpan={2}>
|
||||||
</p> :
|
Aucun project pour l'instant.
|
||||||
|
</td>
|
||||||
|
</tr> :
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -55,4 +92,18 @@ const Home: FunctionComponent = () => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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;
|
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
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type ProjectID string
|
type ProjectID string
|
||||||
|
|
||||||
type ProjectEntry struct {
|
type ProjectEntry struct {
|
||||||
|
@ -14,6 +16,8 @@ type Project struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
Tasks map[TaskID]Task `json:"tasks"`
|
Tasks map[TaskID]Task `json:"tasks"`
|
||||||
Params Params `json:"params"`
|
Params Params `json:"params"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Params struct {
|
type Params struct {
|
||||||
|
|
Loading…
Reference in New Issue