feat: project updated/created date + sorting on homepage

This commit is contained in:
wpetit 2023-08-31 21:56:07 -06:00
parent 18f43482e0
commit 3fc3d813d3
8 changed files with 181 additions and 66 deletions

View File

@ -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",

View File

@ -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",

View File

@ -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;
}

View File

@ -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
}

View File

@ -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(),
};
}

View File

@ -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>&nbsp;&nbsp;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>&nbsp;&nbsp;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;

20
client/src/util/date.ts Normal file
View File

@ -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)
}

View File

@ -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 {