add linter

This commit is contained in:
Romulus21
2024-02-18 23:30:50 +01:00
parent b53d378ec1
commit ebfc56eba3
34 changed files with 568 additions and 262 deletions

48
.eslintrc.json Normal file
View File

@@ -0,0 +1,48 @@
{
"env": {
"browser": true,
"es2021": true
},
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:@typescript-eslint/stylistic",
"plugin:react/recommended",
"plugin:react-hooks/recommended",
"plugin:tailwindcss/recommended"
],
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 12,
"sourceType": "module"
},
"plugins": [
"react",
"react-hooks",
"tailwindcss"
],
"rules": {
"no-undef": ["error", {"typeof": true}],
"react-hooks/exhaustive-deps": "off",
"react/react-in-jsx-scope": "off",
"react/require-default-props": "off",
"semi": [2, "never"],
"tailwindcss/no-custom-classname": ["warn", {
"cssFiles": ["resources/css/app.css"],
"whitelist": []
}],
"tailwindcss/enforces-negative-arbitrary-values": "warn",
"tailwindcss/enforces-shorthand": "warn",
"tailwindcss/migration-from-tailwind-2": "warn",
// "tailwindcss/no-arbitrary-value": "off",
// "tailwindcss/no-arbitrary-value": "error",
"tailwindcss/no-contradicting-classname": "error"
},
"settings": {
"react": {
"version": "18.2"
}
}
}

View File

@@ -48,7 +48,7 @@ class ToDoController extends Controller
$data['checked'] = $request->input('checked') ? now() : null;
$todo->update($data);
if ($request->user()->currentTimeTracker->to_do_id === $todo->id) {
if ($request->user()->currentTimeTracker?->to_do_id === $todo->id) {
$request->user()->stopCurrentTimeTracker();
}

View File

@@ -23,7 +23,8 @@ class ToDoRequest extends FormRequest
{
return [
'name' => ['string', 'min:3', 'max:255'],
'checked' => ['boolean'],
'description' => ['string', 'max:2000'],
'checked' => ['boolean', 'nullable'],
];
}
}

View File

@@ -18,6 +18,7 @@ class ToDoResource extends JsonResource
'id' => $this->id,
'user_id' => $this->user_id,
'name' => $this->name,
'description' => $this->description,
'checked' => $this->checked,
'duration' => $this->duration,
];

View File

@@ -3,11 +3,15 @@
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build"
"build": "vite build",
"lint": "./node_modules/.bin/eslint resources/js/ --ext .js,.jsx,ts,tsx",
"lint-fix": "eslint ./ --fix"
},
"devDependencies": {
"@types/react": "^18.2.48",
"@types/react-dom": "^18.2.18",
"@typescript-eslint/eslint-plugin": "^7.0.1",
"@typescript-eslint/parser": "^7.0.1",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.17",
"axios": "^1.6.4",

208
pnpm-lock.yaml generated
View File

@@ -22,6 +22,12 @@ devDependencies:
'@types/react-dom':
specifier: ^18.2.18
version: 18.2.18
'@typescript-eslint/eslint-plugin':
specifier: ^7.0.1
version: 7.0.1(@typescript-eslint/parser@7.0.1)(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/parser':
specifier: ^7.0.1
version: 7.0.1(eslint@8.56.0)(typescript@5.3.3)
'@vitejs/plugin-react':
specifier: ^4.2.1
version: 4.2.1(vite@5.0.12)
@@ -771,6 +777,10 @@ packages:
resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==}
dev: true
/@types/json-schema@7.0.15:
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
dev: true
/@types/prop-types@15.7.11:
resolution: {integrity: sha512-ga8y9v9uyeiLdpKddhxYQkxNDrfvuPrlFb0N1qnZZByvcElJaXthF1UhvCh9TLWJBEHeNtdnbysW7Y6Uq8CVng==}
dev: true
@@ -793,6 +803,142 @@ packages:
resolution: {integrity: sha512-WZLiwShhwLRmeV6zH+GkbOFT6Z6VklCItrDioxUnv+u4Ll+8vKeFySoFyK/0ctcRpOmwAicELfmys1sDc/Rw+A==}
dev: true
/@types/semver@7.5.7:
resolution: {integrity: sha512-/wdoPq1QqkSj9/QOeKkFquEuPzQbHTWAMPH/PaUMB+JuR31lXhlWXRZ52IpfDYVlDOUBvX09uBrPwxGT1hjNBg==}
dev: true
/@typescript-eslint/eslint-plugin@7.0.1(@typescript-eslint/parser@7.0.1)(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-OLvgeBv3vXlnnJGIAgCLYKjgMEU+wBGj07MQ/nxAaON+3mLzX7mJbhRYrVGiVvFiXtwFlkcBa/TtmglHy0UbzQ==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
'@typescript-eslint/parser': ^7.0.0
eslint: ^8.56.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@eslint-community/regexpp': 4.10.0
'@typescript-eslint/parser': 7.0.1(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/scope-manager': 7.0.1
'@typescript-eslint/type-utils': 7.0.1(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/utils': 7.0.1(eslint@8.56.0)(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 7.0.1
debug: 4.3.4
eslint: 8.56.0
graphemer: 1.4.0
ignore: 5.3.0
natural-compare: 1.4.0
semver: 7.6.0
ts-api-utils: 1.2.1(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/parser@7.0.1(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-8GcRRZNzaHxKzBPU3tKtFNing571/GwPBeCvmAUw0yBtfE2XVd0zFKJIMSWkHJcPQi0ekxjIts6L/rrZq5cxGQ==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^8.56.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/scope-manager': 7.0.1
'@typescript-eslint/types': 7.0.1
'@typescript-eslint/typescript-estree': 7.0.1(typescript@5.3.3)
'@typescript-eslint/visitor-keys': 7.0.1
debug: 4.3.4
eslint: 8.56.0
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/scope-manager@7.0.1:
resolution: {integrity: sha512-v7/T7As10g3bcWOOPAcbnMDuvctHzCFYCG/8R4bK4iYzdFqsZTbXGln0cZNVcwQcwewsYU2BJLay8j0/4zOk4w==}
engines: {node: ^16.0.0 || >=18.0.0}
dependencies:
'@typescript-eslint/types': 7.0.1
'@typescript-eslint/visitor-keys': 7.0.1
dev: true
/@typescript-eslint/type-utils@7.0.1(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-YtT9UcstTG5Yqy4xtLiClm1ZpM/pWVGFnkAa90UfdkkZsR1eP2mR/1jbHeYp8Ay1l1JHPyGvoUYR6o3On5Nhmw==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^8.56.0
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/typescript-estree': 7.0.1(typescript@5.3.3)
'@typescript-eslint/utils': 7.0.1(eslint@8.56.0)(typescript@5.3.3)
debug: 4.3.4
eslint: 8.56.0
ts-api-utils: 1.2.1(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/types@7.0.1:
resolution: {integrity: sha512-uJDfmirz4FHib6ENju/7cz9SdMSkeVvJDK3VcMFvf/hAShg8C74FW+06MaQPODHfDJp/z/zHfgawIJRjlu0RLg==}
engines: {node: ^16.0.0 || >=18.0.0}
dev: true
/@typescript-eslint/typescript-estree@7.0.1(typescript@5.3.3):
resolution: {integrity: sha512-SO9wHb6ph0/FN5OJxH4MiPscGah5wjOd0RRpaLvuBv9g8565Fgu0uMySFEPqwPHiQU90yzJ2FjRYKGrAhS1xig==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
typescript:
optional: true
dependencies:
'@typescript-eslint/types': 7.0.1
'@typescript-eslint/visitor-keys': 7.0.1
debug: 4.3.4
globby: 11.1.0
is-glob: 4.0.3
minimatch: 9.0.3
semver: 7.6.0
ts-api-utils: 1.2.1(typescript@5.3.3)
typescript: 5.3.3
transitivePeerDependencies:
- supports-color
dev: true
/@typescript-eslint/utils@7.0.1(eslint@8.56.0)(typescript@5.3.3):
resolution: {integrity: sha512-oe4his30JgPbnv+9Vef1h48jm0S6ft4mNwi9wj7bX10joGn07QRfqIqFHoMiajrtoU88cIhXf8ahwgrcbNLgPA==}
engines: {node: ^16.0.0 || >=18.0.0}
peerDependencies:
eslint: ^8.56.0
dependencies:
'@eslint-community/eslint-utils': 4.4.0(eslint@8.56.0)
'@types/json-schema': 7.0.15
'@types/semver': 7.5.7
'@typescript-eslint/scope-manager': 7.0.1
'@typescript-eslint/types': 7.0.1
'@typescript-eslint/typescript-estree': 7.0.1(typescript@5.3.3)
eslint: 8.56.0
semver: 7.6.0
transitivePeerDependencies:
- supports-color
- typescript
dev: true
/@typescript-eslint/visitor-keys@7.0.1:
resolution: {integrity: sha512-hwAgrOyk++RTXrP4KzCg7zB2U0xt7RUU0ZdMSCsqF3eKUwkdXUMyTb0qdCuji7VIbcpG62kKTU9M1J1c9UpFBw==}
engines: {node: ^16.0.0 || >=18.0.0}
dependencies:
'@typescript-eslint/types': 7.0.1
eslint-visitor-keys: 3.4.3
dev: true
/@ungap/structured-clone@1.2.0:
resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==}
dev: true
@@ -903,6 +1049,11 @@ packages:
is-string: 1.0.7
dev: true
/array-union@2.1.0:
resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==}
engines: {node: '>=8'}
dev: true
/array.prototype.flat@1.3.2:
resolution: {integrity: sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==}
engines: {node: '>= 0.4'}
@@ -1184,6 +1335,13 @@ packages:
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
dev: true
/dir-glob@3.0.1:
resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==}
engines: {node: '>=8'}
dependencies:
path-type: 4.0.0
dev: true
/dlv@1.1.3:
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
dev: true
@@ -1699,6 +1857,18 @@ packages:
define-properties: 1.2.1
dev: true
/globby@11.1.0:
resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==}
engines: {node: '>=10'}
dependencies:
array-union: 2.1.0
dir-glob: 3.0.1
fast-glob: 3.3.2
ignore: 5.3.0
merge2: 1.4.1
slash: 3.0.0
dev: true
/gopd@1.0.1:
resolution: {integrity: sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==}
dependencies:
@@ -2101,6 +2271,13 @@ packages:
yallist: 3.1.1
dev: true
/lru-cache@6.0.0:
resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==}
engines: {node: '>=10'}
dependencies:
yallist: 4.0.0
dev: true
/merge2@1.4.1:
resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==}
engines: {node: '>= 8'}
@@ -2309,6 +2486,11 @@ packages:
minipass: 7.0.4
dev: true
/path-type@4.0.0:
resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==}
engines: {node: '>=8'}
dev: true
/picocolors@1.0.0:
resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==}
dev: true
@@ -2601,6 +2783,14 @@ packages:
hasBin: true
dev: true
/semver@7.6.0:
resolution: {integrity: sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==}
engines: {node: '>=10'}
hasBin: true
dependencies:
lru-cache: 6.0.0
dev: true
/set-function-length@1.2.0:
resolution: {integrity: sha512-4DBHDoyHlM1IRPGYcoxexgh67y4ueR53FKV1yyxwFMY7aCqcN/38M1+SwZ/qJQ8iLv7+ck385ot4CcisOAPT9w==}
engines: {node: '>= 0.4'}
@@ -2646,6 +2836,11 @@ packages:
engines: {node: '>=14'}
dev: true
/slash@3.0.0:
resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==}
engines: {node: '>=8'}
dev: true
/source-map-js@1.0.2:
resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==}
engines: {node: '>=0.10.0'}
@@ -2820,6 +3015,15 @@ packages:
is-number: 7.0.0
dev: true
/ts-api-utils@1.2.1(typescript@5.3.3):
resolution: {integrity: sha512-RIYA36cJn2WiH9Hy77hdF9r7oEwxAtB/TS9/S4Qd90Ap4z5FSiin5zEiTL44OII1Y3IIlEvxwxFUVgrHSZ/UpA==}
engines: {node: '>=16'}
peerDependencies:
typescript: '>=4.2.0'
dependencies:
typescript: 5.3.3
dev: true
/ts-interface-checker@0.1.13:
resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==}
dev: true
@@ -3034,6 +3238,10 @@ packages:
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
dev: true
/yallist@4.0.0:
resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==}
dev: true
/yaml@2.3.4:
resolution: {integrity: sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==}
engines: {node: '>= 14'}

View File

@@ -1,7 +1,7 @@
import React from 'react';
import './bootstrap';
import {createRoot} from "react-dom/client";
import App from "./pages/App";
import React from 'react'
import './bootstrap'
import {createRoot} from "react-dom/client"
import App from "./pages/App"
import '../css/app.css'
import './utilities/customProperties.ts'

View File

@@ -4,10 +4,10 @@
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
import axios from 'axios';
window.axios = axios;
import axios from 'axios'
window.axios = axios
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
/**
* Echo exposes an expressive API for subscribing to channels and listening

View File

@@ -1,9 +1,9 @@
import React, {FC} from "react";
import {PropsWithChildren} from "react";
import React, {FC} from "react"
import {PropsWithChildren} from "react"
const Card: FC<PropsWithChildren<CardProps>> = ({children, className = ''}) => {
return <div className={`${className} border m-1 rounded py-1 px-2`}>
return <div className={`${className} m-1 rounded border px-2 py-1`}>
{children}
</div>
}

View File

@@ -10,7 +10,7 @@ const Field: FC<FieldProps> = ({children, type = 'text', className = '', classNa
htmlFor={props.id ?? undefined}>
{children}
</label>}
<input className={`${className} w-full mt-2 rounded dark:bg-gray-700`}
<input className={`${className} mt-2 w-full rounded dark:bg-gray-700`}
type={type}
{...props}/>
<div className={`error-message`} />
@@ -24,7 +24,7 @@ interface FieldProps {
type?: HTMLInputTypeAttribute,
name: string,
id?: string,
value: any,
value: string|number|undefined,
placeholder?: string,
autoFocus?: boolean,
className?: string,
@@ -32,3 +32,27 @@ interface FieldProps {
step?: string,
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
}
export const TextArea: FC<TextAreaProps> = ({children, name, value, className = '', classNameForm = '', ...props}) => {
return <div className={`form-control ${classNameForm}`}>
{children && <label className="block text-gray-900 dark:text-gray-200"
htmlFor={props.id ?? undefined}>
{children}
</label>}
<textarea name={name} className={`${className} mt-2 w-full rounded dark:bg-gray-700`} {...props}>
{value}
</textarea>
<div className={`error-message`}/>
</div>
}
interface TextAreaProps {
children?: ReactElement|string,
id?: string,
name: string,
value: string|undefined,
className?: string,
classNameForm?: string,
onChange: (event: React.ChangeEvent<HTMLTextAreaElement>) => void,
}

View File

@@ -1,7 +1,7 @@
import React from "react"
import {Link, NavLink, useLocation} from "react-router-dom"
import useAuthUser from "../hooks/AuthUser";
import Tracker from "./TimeTrackers/Tracker";
import useAuthUser from "../hooks/AuthUser"
import Tracker from "./TimeTrackers/Tracker"
const Header = () => {
@@ -10,8 +10,8 @@ const Header = () => {
console.log(authUser)
return <header className="bg-blue-700 text-white text-xl">
<div className="flex py-3 px-5 justify-between">
return <header className="bg-blue-700 text-xl text-white">
<div className="flex justify-between px-5 py-3">
<div>
<Link to="/">Ticcat</Link>
</div>
@@ -26,7 +26,7 @@ const Header = () => {
{/*<Link to="/sinscrire">S'inscrire</Link>*/}
</span>}
</div>
<nav className="bg-gray-600 px-5 flex gap-5">
<nav className="flex gap-5 bg-gray-600 px-5">
<NavLink to="/">ToDos</NavLink>
<NavLink to="/times">Times</NavLink>
</nav>

View File

@@ -1,4 +1,4 @@
import React, {FC, FormEvent, PropsWithChildren, ReactNode} from "react"
import React, {FC, FormEvent, ReactNode} from "react"
import ReactDOM from "react-dom"
export const Modal: FC<ModalProps> = ({children, show, closeModal, ...props}) => {
@@ -9,7 +9,7 @@ export const Modal: FC<ModalProps> = ({children, show, closeModal, ...props}) =>
}
return show && <Overlay onClick={closeModal}>
<div className="dark:bg-gray-900 cursor-auto dark:text-white bg-white"
<div className="cursor-auto bg-white dark:bg-gray-900 dark:text-white"
onClick={stopEvent}
{...props}>
{children}
@@ -29,7 +29,7 @@ const Overlay: FC<OverlayProps> = ({children, ...props}) => {
if (app) {
return ReactDOM.createPortal(
<button className={"flex justify-center items-center w-full fixed inset-0 z-10 bg-gray-900/50"}
<button className={"fixed inset-0 z-10 flex w-full items-center justify-center bg-gray-900/50"}
{...props}>
{children}
</button>,

View File

@@ -23,6 +23,12 @@ export const DraggableSVG: FC<ComponentProps<any>> = (props) => SVGSkeleton({
...props
})
export const EditSVG: FC<ComponentProps<any>> = (props) => SVGSkeleton({
viewBox: "0 0 576 512",
paths: <path d="M402.3 344.9l32-32c5-5 13.7-1.5 13.7 5.7V464c0 26.5-21.5 48-48 48H48c-26.5 0-48-21.5-48-48V112c0-26.5 21.5-48 48-48h273.5c7.1 0 10.7 8.6 5.7 13.7l-32 32c-1.5 1.5-3.5 2.3-5.7 2.3H48v352h352V350.5c0-2.1.8-4.1 2.3-5.6zm156.6-201.8L296.3 405.7l-90.4 10c-26.2 2.9-48.5-19.2-45.6-45.6l10-90.4L432.9 17.1c22.9-22.9 59.9-22.9 82.7 0l43.2 43.2c22.9 22.9 22.9 60 .1 82.8zM460.1 174L402 115.9 216.2 301.8l-7.3 65.3 65.3-7.3L460.1 174zm64.8-79.7l-43.2-43.2c-4.1-4.1-10.8-4.1-14.8 0L436 82l58.1 58.1 30.9-30.9c4-4.2 4-10.8-.1-14.9z" />,
...props
})
export const PauseSVG: FC<ComponentProps<any>> = (props) => SVGSkeleton({
paths: <path d="M16 19q-.825 0-1.412-.587T14 17V7q0-.825.588-1.412T16 5q.825 0 1.413.588T18 7v10q0 .825-.587 1.413T16 19m-8 0q-.825 0-1.412-.587T6 17V7q0-.825.588-1.412T8 5q.825 0 1.413.588T10 7v10q0 .825-.587 1.413T8 19"/>,
...props

View File

@@ -1,20 +1,17 @@
import React, {FC, FormEvent, ReactEventHandler, useState} from "react"
import Field from "../Field";
import {timeTracker} from "../../utilities/types";
import React, {FC, FormEvent, useState} from "react"
import Field from "../Field"
import {timeTracker} from "../../utilities/types"
const TimeTrackerEdit: FC<TimeTrackerEditProps> = ({timeTracker}) => {
const [trackerForm, setTrackerForm] = useState<timeTracker>(timeTracker)
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log(trackerForm, event.target.value)
setTrackerForm({...trackerForm, [event.target.name]: event.target.value.replace('T', ' ')})
}
const onSubmit = (event: FormEvent) => {
event.preventDefault()
console.log(trackerForm)
console.log('submit', trackerForm, event)
}
return <form onSubmit={onSubmit}>
@@ -27,7 +24,7 @@ const TimeTrackerEdit: FC<TimeTrackerEditProps> = ({timeTracker}) => {
value={trackerForm.end_at}
onChange={handleChange}/>
<button type="submit">Valider</button>
<button type="submit" onClick={onSubmit}>Valider</button>
</form>
}

View File

@@ -1,7 +1,7 @@
import React, {useEffect, useState} from "react"
import useTracker from "../../hooks/TraskerHook"
import {Link} from "react-router-dom";
import {StopSVG} from "../SVG";
import {Link} from "react-router-dom"
import {StopSVG} from "../SVG"
const Tracker = () => {
@@ -14,14 +14,14 @@ const Tracker = () => {
useEffect(() => {
setTimeout(() => setTimer(formatTimer(currentTimeTracker?.start_at)), 1000)
}, [timer]);
}, [timer])
const formatTimer = (startAt: string|null|undefined) => {
if (!startAt) {
return '--:--'
}
let timer = Math.floor(((new Date()).getTime() - (new Date(startAt)).getTime()) / 1000)
return timer.durationify()
const timer = Math.floor(((new Date()).getTime() - (new Date(startAt)).getTime()) / 1000)
return Number(timer).durationify()
// let hours = Math.floor(timer / 3600)
// let minutes = Math.floor((timer - hours * 3600) / 60)
// let secondes = timer - hours * 3600 - minutes * 60

View File

@@ -1,7 +1,7 @@
import React, {FC, useEffect, useState} from "react"
import useAxiosTools from "../../hooks/AxiosTools";
import {toDo} from "../../utilities/types";
import {PlaySVG} from "../SVG";
import useAxiosTools from "../../hooks/AxiosTools"
import {toDo} from "../../utilities/types"
import {PlaySVG} from "../SVG"
const ToDoFinish: FC<ToDoFinishProps> = ({reload}) => {
@@ -36,14 +36,14 @@ const ToDoFinish: FC<ToDoFinishProps> = ({reload}) => {
}
return <div>
<button className="flex justify-between items-center w-full bg-blue-700 px-2 py-1 rounded cursor-pointer"
<button className="flex w-full cursor-pointer items-center justify-between rounded bg-blue-700 px-2 py-1"
onClick={handleShow}>
<h2 className="inline">Tâches terminées</h2>
<span><PlaySVG className={`w-4 transition ${showTodos ? 'rotate-90' : 'rotate-180'}`} /></span>
</button>
{errorLabel()}
{showTodos && <ul className="list-disc m-2">
{showTodos && <ul className="m-2 list-disc">
{toDos.map(toDo => <li key={toDo.id} className="flex gap-2">
<span>{toDo.checked ? (new Date(toDo.checked)).toSmallFrDate() : ''}</span>
<span className="flex-1">{toDo.name}</span>

View File

@@ -1,11 +1,9 @@
import React, {FC, useEffect, useState} from "react";
import useAxiosTools from "../../hooks/AxiosTools";
import {toDo} from "../../utilities/types";
import {Link} from "react-router-dom";
import useTracker from "../../hooks/TraskerHook";
import {Simulate} from "react-dom/test-utils";
import load = Simulate.load;
import {DraggableSVG, PauseSVG, PlaySVG} from "../SVG";
import React, {FC, useEffect, useState} from "react"
import useAxiosTools from "../../hooks/AxiosTools"
import {toDo} from "../../utilities/types"
import {Link} from "react-router-dom"
import useTracker from "../../hooks/TraskerHook"
import {DraggableSVG, PauseSVG, PlaySVG} from "../SVG"
const ToDoIndex: FC<ToDoIndexProps> = ({reload, setReload}) => {
@@ -21,7 +19,7 @@ const ToDoIndex: FC<ToDoIndexProps> = ({reload, setReload}) => {
if (reload && !loading) {
fetchToDos()
}
}, [reload]);
}, [reload])
const fetchToDos = async () => {
try {
@@ -57,18 +55,18 @@ const ToDoIndex: FC<ToDoIndexProps> = ({reload, setReload}) => {
onChange={() =>toggleCheck(toDo)}
className=""/>
<Link to={"/todos/" + toDo.id}
className={`${toDo.checked ? 'line-through' : ''} flex-1 flex justify-between`}>
className={`${toDo.checked ? 'line-through' : ''} flex flex-1 justify-between`}>
<span>{toDo.name}</span>
<span className="text-gray-400 text-md mr-2">{toDo.duration.durationify()}</span>
<span className="mr-2 text-sm text-gray-400">{toDo.duration.durationify()}</span>
</Link>
{toDo.id === currentTimeTracker?.to_do?.id
? <button className="cursor-pointer w-7 justify-center flex items-center"
? <button className="flex w-7 cursor-pointer items-center justify-center"
type="button"
title="Commencer"
onClick={stopCurrentTimeTrack}>
<PauseSVG className="w-7"/>
</button>
: <button className="cursor-pointer w-7 justify-center flex items-center"
: <button className="flex w-7 cursor-pointer items-center justify-center"
type="button"
title="Commencer"
onClick={() => startTrackToDo(toDo)}>

View File

@@ -1,101 +0,0 @@
import React, {FC, HTMLInputTypeAttribute, ReactElement, useEffect, useState} from "react"
import {useParams} from "react-router-dom";
import useAxiosTools from "../../hooks/AxiosTools";
import {timeTracker, toDo} from "../../utilities/types";
const ToDoShow = () => {
const {id} = useParams()
console.log(id)
const {setLoading, errorCatch, errorLabel, axiosGet, axiosPut} = useAxiosTools(true)
const [toDo, setToDo] = useState<toDo|null>(null)
useEffect(() => {
fetchToDo()
}, [])
const fetchToDo = async () => {
try {
const res = await axiosGet('/api/todos/' + id)
setToDo(res.data)
} catch (error) {
errorCatch(error)
} finally {
setLoading(false)
}
}
return <div>
<h1>{toDo?.name}</h1>
<h1>{toDo?.description}</h1>
{toDo && <ToDoTimeTrackers toDo={toDo} />}
</div>
}
export default ToDoShow
const ToDoTimeTrackers: FC<ToDoTimeTrackers> = ({toDo: toDo}) => {
const {setLoading, errorCatch, errorLabel, axiosGet, axiosPut} = useAxiosTools(true)
const [timeTrackers, setTimeTrackers] = useState<timeTracker[]>([])
useEffect(() => {
fetchTimeTrackers()
}, [])
const fetchTimeTrackers = async () => {
try {
const res = await axiosGet(`/api/todos/${toDo.id}/time-trackers`)
setTimeTrackers(res.data)
} catch (error) {
errorCatch(error)
} finally {
setLoading(false)
}
}
const timeSpend = () => {
let timer = 0
let more = false
timeTrackers.forEach(timeTracker => {
if (! timeTracker.end_at) {
more = true
} else {
timer += ((new Date(timeTracker.end_at)).getTime()) - (new Date(timeTracker.start_at)).getTime()
}
})
timer = Math.floor(timer / 1000)
return (more ? '+' : '') + timer.durationify()
// let hours = Math.floor(timer / 3600)
// let minutes = Math.floor((timer - hours * 3600) / 60)
// let secondes = timer - hours * 3600 - minutes * 60
// return `${more ? '+' : ''} ${hours}:${String(minutes).padStart(2, '0')}:${String(secondes).padStart(2, '0')}`
}
return <div>
<div>Temps passé : {timeSpend()}</div>
<table className="mx-auto">
<thead>
<tr>
<th>Début</th>
<th>Fin</th>
<th>Différence</th>
</tr>
</thead>
<tbody>
{timeTrackers.map(timeTracker => <tr key={timeTracker.id} className="text-center">
<td>{timeTracker.start_at ? (new Date(timeTracker.start_at)).toSmallFrDate() : ''}</td>
<td>{timeTracker.end_at ? (new Date(timeTracker.end_at)).toSmallFrDate() : ''}</td>
<td className="text-right">{timeTracker.start_at && timeTracker.end_at ? (new Date(timeTracker.end_at)).difference(new Date(timeTracker.start_at)) : ''}</td>
</tr>)}
</tbody>
</table>
</div>
}
interface ToDoTimeTrackers {
toDo: toDo,
}

View File

@@ -1,6 +1,6 @@
import React, {FC, FormEvent, useState} from "react"
import Field from "../Field";
import useAxiosTools from "../../hooks/AxiosTools";
import Field from "../Field"
import useAxiosTools from "../../hooks/AxiosTools"
const ToDoStore: FC<ToDoStoreProps> = ({setReload}) => {
@@ -24,10 +24,10 @@ const ToDoStore: FC<ToDoStoreProps> = ({setReload}) => {
<Field name="todo"
value={toDo}
classNameForm="flex-1"
className="h-10 !mt-0 rounded-r-none px-2"
className="!mt-0 h-10 rounded-r-none px-2"
onChange={event => setToDo(event.target.value)} />
<button type="submit"
className="bg-blue-900 h-10 rounded-r px-5">
className="h-10 rounded-r bg-blue-900 px-5">
Ajouter
</button>
</form>

View File

@@ -6,8 +6,8 @@ import React, {
useContext,
useEffect,
useState
} from "react";
import axios from "axios";
} from "react"
import axios from "axios"
const AuthUserContext = createContext<AuthUserProps|undefined>(undefined)
@@ -44,7 +44,7 @@ export const AuthUserProvider = ({children}: PropsWithChildren) => {
const logout = async () => {
try {
setLoadingAuthUser(false)
const res = await axios.delete('/api/logout')
await axios.delete('/api/logout')
setAuthUser(null)
window.location.replace('/')
} catch (e) {

View File

@@ -1,7 +1,6 @@
import {useState} from "react";
import axios from "axios";
import React from "react";
import {cleanErrorsForm, displayFormErrors} from "../utilities/form";
import React, {useState} from "react"
import axios from "axios"
import {cleanErrorsForm, displayFormErrors} from "../utilities/form"
const useAxiosTools = (isLoading = false) => {
@@ -23,7 +22,7 @@ const useAxiosTools = (isLoading = false) => {
const errorLabel = () => {
return error ? <div className="bg-red-600 rounded m-2 text-center text-white px-2 py-1 mx-auto">{error}</div>: null
return error ? <div className="m-2 mx-auto rounded bg-red-600 px-2 py-1 text-center text-white">{error}</div>: null
}
const cleanErrors = () => {

View File

@@ -1,6 +1,6 @@
import React, {createContext, PropsWithChildren, useContext, useEffect, useState} from "react";
import {timeTracker, toDo} from "../utilities/types";
import useAxiosTools from "./AxiosTools";
import React, {createContext, PropsWithChildren, useContext, useEffect, useState} from "react"
import {timeTracker, toDo} from "../utilities/types"
import useAxiosTools from "./AxiosTools"
const TrackerContext = createContext<TrackerProps|undefined>(undefined)
@@ -9,12 +9,10 @@ interface TrackerProps {
currentTimeTracker: timeTracker|null,
startTrackToDo: (toDo: toDo) => void,
stopCurrentTimeTrack: () => void,
isToDoTracked: (toDo: toDo) => boolean,
}
export const TrackerProvider = ({children}: PropsWithChildren) => {
const [currentTimeTracker, setCurrentTimeTracker] = useState<timeTracker|null>(null)
const [toDoTracked, setToDoTracked] = useState<toDo|null>(null)
const {axiosGet, axiosPost, axiosDelete} = useAxiosTools()
useEffect(() => {
@@ -41,16 +39,14 @@ export const TrackerProvider = ({children}: PropsWithChildren) => {
const stopCurrentTimeTrack = async () => {
try {
const res = await axiosDelete(`/api/time-trackers/user`)
await axiosDelete(`/api/time-trackers/user`)
setCurrentTimeTracker(null)
} catch (error) {
console.error(error)
}
}
const isToDoTracked = (toDo: toDo) => toDo.id === toDoTracked?.id
return <TrackerContext.Provider value={{currentTimeTracker, startTrackToDo, stopCurrentTimeTrack, isToDoTracked}}>
return <TrackerContext.Provider value={{currentTimeTracker, startTrackToDo, stopCurrentTimeTrack}}>
{children}
</TrackerContext.Provider>
}

View File

@@ -1,7 +1,7 @@
import React from "react"
import Router from "./Router"
import {AuthUserProvider} from "../hooks/AuthUser";
import {TrackerProvider} from "../hooks/TraskerHook";
import {AuthUserProvider} from "../hooks/AuthUser"
import {TrackerProvider} from "../hooks/TraskerHook"
const App = () => {

View File

@@ -1,9 +1,6 @@
import React, {FormEvent, SyntheticEvent, useState} from "react"
import Field from "../../components/Field";
import axios from "axios";
import {useNavigate} from "react-router-dom";
import useAuthUser from "../../hooks/AuthUser";
import useAxiosTools from "../../hooks/AxiosTools";
import React, {FormEvent, useState} from "react"
import Field from "../../components/Field"
import useAxiosTools from "../../hooks/AxiosTools"
const ForgotPassword = () => {
@@ -16,15 +13,15 @@ const ForgotPassword = () => {
try {
cleanErrors()
await axiosGet('/sanctum/csrf-cookie')
const res = await axiosPost('/api/forgot', {email})
await axiosPost('/api/forgot', {email})
setMessage(true)
} catch (e) {
errorCatch(e)
} catch (error) {
errorCatch(error)
}
}
return <div>
<form onSubmit={handleSubmit} className="w-96 mx-auto mt-10 border shadow p-3">
<form onSubmit={handleSubmit} className="mx-auto mt-10 w-96 border p-3 shadow">
<h1 className="text-center">Mot de passe oublié</h1>
{message && <p className="bg-green-600">Un email vous a é envoyé pour modifier le mot de passe.</p>}
@@ -37,7 +34,7 @@ const ForgotPassword = () => {
value={email}
onChange={event => setEmail(event.target.value)}
autoFocus>Email</Field>
<button type="submit" className="mt-5 btn-primary w-full block text-lg">Valider</button>
<button type="submit" className="btn-primary mt-5 block w-full text-lg">Valider</button>
</form>
</div>
}

View File

@@ -1,10 +1,9 @@
import React, {FormEvent, SyntheticEvent, useState} from "react"
import axios from "axios";
import {Link, useNavigate} from "react-router-dom";
import useAxiosTools from "../../hooks/AxiosTools";
import Field from "../../components/Field";
import useAuthUser from "../../hooks/AuthUser";
import Card from "../../components/Card";
import React, {FormEvent, useState} from "react"
import {Link, useNavigate} from "react-router-dom"
import useAxiosTools from "../../hooks/AxiosTools"
import Field from "../../components/Field"
import useAuthUser from "../../hooks/AuthUser"
import Card from "../../components/Card"
const Login = () => {
@@ -28,9 +27,9 @@ const Login = () => {
}
return <div>
<Card className="w-96 mx-auto mt-10 p-2 overflow-hidden">
<Card className="mx-auto mt-10 w-96 overflow-hidden p-2">
<form onSubmit={handleSubmit}>
<h1 className="text-center bg-blue-500 -mx-2 -mt-1 text-lg font-bold px-2 py-1 mb-2">
<h1 className="-mx-2 -mt-1 mb-2 bg-blue-500 px-2 py-1 text-center text-lg font-bold">
Connexion
</h1>
{errorLabel()}
@@ -46,7 +45,7 @@ const Login = () => {
placeholder="******"
value={password}
onChange={event => setPassword(event.target.value)}>Mot de passe</Field>
<button type="submit" className="mt-5 btn-primary w-full block text-lg">Valider</button>
<button type="submit" className="btn-primary mt-5 block w-full text-lg">Valider</button>
<Link to="/mot-de-passe-oubliee" className="mt-2 inline-block">Mot de passe oublié ?</Link>
</form>
</Card>

View File

@@ -1,9 +1,9 @@
import React, {FormEvent, useState} from "react"
import useAuthUser from "../../hooks/AuthUser"
import Field from "../../components/Field";
import useAxiosTools from "../../hooks/AxiosTools";
import PageLayout from "../../components/PageLayout";
import Card from "../../components/Card";
import Field from "../../components/Field"
import useAxiosTools from "../../hooks/AxiosTools"
import PageLayout from "../../components/PageLayout"
import Card from "../../components/Card"
const Profile = () => {
@@ -65,7 +65,7 @@ const Profile = () => {
Longitude
</Field>
<div className="self-end">
<button type="submit" className="btn-primary w-24 h-10">Valider</button>
<button type="submit" className="btn-primary h-10 w-24">Valider</button>
</div>
</div>
</form>}

View File

@@ -1,9 +1,9 @@
import React, {ChangeEvent, FormEvent, SyntheticEvent, useState} from "react"
import Field from "../../components/Field";
import axios from "axios";
import {useNavigate} from "react-router-dom";
import Card from "../../components/Card";
import useAuthUser from "../../hooks/AuthUser";
import React, {FormEvent, useState} from "react"
import Field from "../../components/Field"
import axios from "axios"
import {useNavigate} from "react-router-dom"
import Card from "../../components/Card"
import useAuthUser from "../../hooks/AuthUser"
const Register = () => {
@@ -26,10 +26,10 @@ const Register = () => {
}
return <div>
<Card className="w-96 mx-auto mt-10 p-2 overflow-hidden">
<Card className="mx-auto mt-10 w-96 overflow-hidden p-2">
<form onSubmit={handleSubmit}>
<h1 className="text-center bg-blue-500 -mx-2 -mt-1 text-lg font-bold px-2 py-1 mb-2">
S'inscrire
<h1 className="-mx-2 -mt-1 mb-2 bg-blue-500 px-2 py-1 text-center text-lg font-bold">
S&apos;inscrire
</h1>
<Field placeholder="Nom"
@@ -49,7 +49,7 @@ const Register = () => {
value={password}
onChange={event => setPassword(event.target.value)}
autoFocus>Mot de passe</Field>
<button type="submit" className="mt-5 btn-primary w-full block text-lg">Valider</button>
<button type="submit" className="btn-primary mt-5 block w-full text-lg">Valider</button>
</form>
</Card>
</div>

View File

@@ -1,13 +1,12 @@
import Field from "../../components/Field";
import React, {FormEvent, useState} from "react";
import {useNavigate, useParams} from "react-router-dom";
import useAuthUser from "../../hooks/AuthUser";
import axios from "axios";
import useAxiosTools from "../../hooks/AxiosTools";
import Field from "../../components/Field"
import React, {FormEvent, useState} from "react"
import {useNavigate, useParams} from "react-router-dom"
import useAuthUser from "../../hooks/AuthUser"
import useAxiosTools from "../../hooks/AxiosTools"
const Reset = () => {
let {token} = useParams()
const {token} = useParams()
const navigate = useNavigate()
const {setAuthUser} = useAuthUser()
const [email, setEmail] = useState('')
@@ -29,7 +28,7 @@ const Reset = () => {
}
return <div>
<form onSubmit={handleSubmit} className="w-96 mx-auto mt-10 border shadow p-3">
<form onSubmit={handleSubmit} className="mx-auto mt-10 w-96 border p-3 shadow">
<h1 className="text-center">Modifier voter mot de passe</h1>
{errorLabel()}
@@ -50,7 +49,7 @@ const Reset = () => {
placeholder="******"
value={samePassword}
onChange={event => setSamePassword(event.target.value)}>Confirmation du mot de passe</Field>
<button type="submit" className="mt-5 btn-primary w-full block text-lg">Valider</button>
<button type="submit" className="btn-primary mt-5 block w-full text-lg">Valider</button>
</form>
</div>
}

View File

@@ -1,8 +1,8 @@
import React, {useState} from "react"
import useAuthUser from "../hooks/AuthUser";
import ToDoStore from "../components/toDos/ToDoStore";
import ToDoIndex from "../components/toDos/ToDoIndex";
import ToDoFinish from "../components/toDos/ToDoFinish";
import useAuthUser from "../hooks/AuthUser"
import ToDoStore from "../components/toDos/ToDoStore"
import ToDoIndex from "../components/toDos/ToDoIndex"
import ToDoFinish from "../components/toDos/ToDoFinish"
const Home = () => {

View File

@@ -1,19 +1,19 @@
import React from "react";
import {BrowserRouter, Route, Routes} from "react-router-dom";
import {Suspense} from "react";
import Profile from "./Auth/Profile";
import Login from "./Auth/Login";
import Header from "../components/Header";
import Home from "./Home";
import useAuthUser from "../hooks/AuthUser";
import Register from "./Auth/Register";
import ToDoShow from "../components/toDos/ToDoShow";
import TimeTrackersIndex from "./TimeTrackersIndex";
import React from "react"
import {BrowserRouter, Route, Routes} from "react-router-dom"
import {Suspense} from "react"
import Profile from "./Auth/Profile"
import Login from "./Auth/Login"
import Header from "../components/Header"
import Home from "./Home"
import useAuthUser from "../hooks/AuthUser"
import Register from "./Auth/Register"
import ToDoShow from "./ToDos/ToDoShow"
import TimeTrackersIndex from "./TimeTrackersIndex"
const Router = () => {
console.log('router')
const {authUser, loadingAuthUser, logout} = useAuthUser()
const {loadingAuthUser} = useAuthUser()
return <>
{loadingAuthUser ? '...loading'

View File

@@ -1,14 +1,15 @@
import React, {useEffect, useState} from "react"
import useAxiosTools from "../hooks/AxiosTools";
import {timeTracker, toDo} from "../utilities/types";
import {PlaySVG} from "../components/SVG";
import useTracker from "../hooks/TraskerHook";
import {Modal} from "../components/Modals";
import TimeTrackerEdit from "../components/TimeTrackers/TimeTrackerEdit";
import useAxiosTools from "../hooks/AxiosTools"
import {timeTracker} from "../utilities/types"
import {EditSVG, PlaySVG} from "../components/SVG"
import useTracker from "../hooks/TraskerHook"
import {Modal} from "../components/Modals"
import TimeTrackerEdit from "../components/TimeTrackers/TimeTrackerEdit"
import {Link} from "react-router-dom"
const TimeTrackersIndex = () => {
const {loading, setLoading, errorCatch, errorLabel, axiosGet, axiosPut} = useAxiosTools(true)
const {loading, setLoading, errorCatch, errorLabel, axiosGet} = useAxiosTools(true)
const [timeTrackers, setTimeTrackers] = useState<timeTracker[]>([])
const [showTrackers, setShowTrackers] = useState<timeTracker|null>(null)
const {startTrackToDo} = useTracker()
@@ -31,19 +32,19 @@ const TimeTrackersIndex = () => {
return <div className="p-5">
{errorLabel()}
<ul>
{timeTrackers.map(tracker => <li key={tracker.id} className="flex justify-between gap-5">
<span className="text-center w-36">{tracker.start_at ? (new Date(tracker.start_at)).toSmallFrDate() : ''}</span>
<span className="text-center w-36">{(new Date(tracker.end_at)).toSmallFrDate()}</span>
<span className={`flex-1 ${tracker.to_do?.checked ? 'line-through' : ''}`}>{tracker.to_do.name}</span>
{timeTrackers.map(tracker => <li key={tracker.id} className="flex justify-between gap-5 odd:bg-blue-950">
<span className="w-24 text-center">{tracker.start_at ? (new Date(tracker.start_at)).toSmallFrDate() : ''}</span>
<span className="w-24 text-center">{(new Date(tracker.end_at)).toSmallFrDate()}</span>
<Link to={"/todos/" + tracker.to_do?.id} className={`flex-1 ${tracker.to_do?.checked ? 'line-through' : ''}`}>{tracker.to_do.name}</Link>
<span className="flex gap-2">
{!tracker?.to_do?.checked && <button className="cursor-pointer w-7 justify-center flex items-center"
{!tracker?.to_do?.checked && <button className="flex w-7 cursor-pointer items-center justify-center"
type="button"
title="Commencer"
onClick={() => startTrackToDo(tracker.to_do)}>
<PlaySVG className="w-4"/>
</button>}
<button onClick={() => setShowTrackers(tracker)}>
Edit
<EditSVG className="w-5"/>
</button>
</span>
</li>)}

View File

@@ -0,0 +1,131 @@
import React, {FC, useEffect, useState} from "react"
import {useParams} from "react-router-dom"
import useAxiosTools from "../../hooks/AxiosTools"
import {timeTracker, toDo} from "../../utilities/types"
import {EditSVG} from "../../components/SVG"
import Field, {TextArea} from "../../components/Field"
const ToDoShow = () => {
const {id} = useParams()
const {setLoading, errorCatch, errorLabel, axiosGet, axiosPut} = useAxiosTools(true)
const [toDo, setToDo] = useState<toDo|null>(null)
const [editMode, setEditMode] = useState(false)
const [editForm, setEditForm] = useState<toDo|null>(null)
useEffect(() => {
fetchToDo()
}, [])
const fetchToDo = async () => {
try {
const res = await axiosGet('/api/todos/' + id)
setToDo(res.data)
} catch (error) {
errorCatch(error)
} finally {
setLoading(false)
}
}
const handleEditTodoMode = async () => {
if (editMode) {
try {
const res = await axiosPut('/api/todos/' + id, editForm)
setToDo(res.data)
} catch (error) {
errorCatch(error)
}
} else {
setEditForm(toDo)
}
setEditMode(!editMode)
}
const handleEditTodo = (event: React.ChangeEvent<HTMLInputElement|HTMLTextAreaElement>) => {
setEditForm({...editForm, [event.target.name]: event.target.value})
}
return <div className="relative p-5">
{errorLabel()}
{editMode ? <>
<button className="absolute right-5 flex" onClick={handleEditTodoMode}>Valider <EditSVG className="w-5"/></button>
<Field name="name" value={editForm?.name} onChange={handleEditTodo} />
<TextArea name="description" value={editForm?.description} onChange={handleEditTodo} />
</> : <>
<button className="absolute right-5" onClick={handleEditTodoMode}><EditSVG className="w-5"/></button>
<h1 className="text-lg font-bold">{toDo?.name}</h1>
<p>{toDo?.description}</p>
</>}
{toDo && <ToDoTimeTrackers toDo={toDo}/>}
</div>
}
export default ToDoShow
const ToDoTimeTrackers: FC<ToDoTimeTrackers> = ({toDo: toDo}) => {
const {setLoading, errorCatch, errorLabel, axiosGet} = useAxiosTools(true)
const [timeTrackers, setTimeTrackers] = useState<timeTracker[]>([])
useEffect(() => {
fetchTimeTrackers()
}, [])
const fetchTimeTrackers = async () => {
try {
const res = await axiosGet(`/api/todos/${toDo.id}/time-trackers`)
setTimeTrackers(res.data)
} catch (error) {
errorCatch(error)
} finally {
setLoading(false)
}
}
const timeSpend = () => {
let timer = 0
let more = false
timeTrackers.forEach(timeTracker => {
if (! timeTracker.end_at) {
more = true
} else {
timer += ((new Date(timeTracker.end_at)).getTime()) - (new Date(timeTracker.start_at)).getTime()
}
})
timer = Math.floor(timer / 1000)
return (more ? '+' : '') + timer.durationify()
// let hours = Math.floor(timer / 3600)
// let minutes = Math.floor((timer - hours * 3600) / 60)
// let secondes = timer - hours * 3600 - minutes * 60
// return `${more ? '+' : ''} ${hours}:${String(minutes).padStart(2, '0')}:${String(secondes).padStart(2, '0')}`
}
return <div className="p-5">
<div className="text-center">Temps passé : {timeSpend()}</div>
{errorLabel()}
<table className="mx-auto">
<thead>
<tr>
<th>Début</th>
<th>Fin</th>
<th>Différence</th>
</tr>
</thead>
<tbody>
{timeTrackers.map(timeTracker => <tr key={timeTracker.id} className="text-center">
<td className="px-1">{timeTracker.start_at ? (new Date(timeTracker.start_at)).toSmallFrDate() : ''}</td>
<td className="px-1">{timeTracker.end_at ? (new Date(timeTracker.end_at)).toSmallFrDate() : ''}</td>
<td className="px-1 text-right">{timeTracker.start_at && timeTracker.end_at ? (new Date(timeTracker.end_at)).difference(new Date(timeTracker.start_at)) : ''}</td>
</tr>)}
</tbody>
</table>
</div>
}
interface ToDoTimeTrackers {
toDo: toDo,
}

View File

@@ -57,15 +57,9 @@ Date.prototype.difference = function (start_at) {
if (!this) {
return '--:--'
}
let end_at = this
if (!start_at) {
end_at = new Date()
start_at = this
}
let timer = Math.floor((end_at.getTime() - start_at.getTime()) / 1000)
let hours = Math.floor(timer / 3600)
let minutes = Math.floor((timer - hours * 3600) / 60)
let secondes = timer - hours * 3600 - minutes * 60
const timer = Math.floor(((!start_at ? (new Date()) : this).getTime() - (!start_at ? this : start_at).getTime()) / 1000)
const hours = Math.floor(timer / 3600)
const minutes = Math.floor((timer - hours * 3600) / 60)
const secondes = timer - hours * 3600 - minutes * 60
return `${hours}:${String(minutes).padStart(2, '0')}:${String(secondes).padStart(2, '0')}`
}

View File

@@ -10,4 +10,8 @@ export default defineConfig({
}),
react(),
],
esbuild: {
jsxFactory: 'h',
jsxFragment: 'Fragment',
}
});