first commt

This commit is contained in:
Romulus21
2023-09-10 08:49:10 +02:00
commit 2dab3b48e1
134 changed files with 16163 additions and 0 deletions

3
resources/css/app.css Normal file
View File

@@ -0,0 +1,3 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

17
resources/js/app.tsx Normal file
View File

@@ -0,0 +1,17 @@
//import './bootstrap';
import '../css/app.css';
import App from "./pages/App";
import { createRoot } from 'react-dom/client';
import React from 'react';
import './customPrototypes'
//const appName = import.meta.env.VITE_APP_NAME || 'Laravel';
const container = document.getElementById('app')
if (container) {
const root = createRoot(container)
root.render(<App />)
}

32
resources/js/bootstrap.js vendored Normal file
View File

@@ -0,0 +1,32 @@
/**
* We'll load the axios HTTP library which allows us to easily issue requests
* to our Laravel back-end. This library automatically handles sending the
* CSRF token as a header based on the value of the "XSRF" token cookie.
*/
import axios from 'axios';
window.axios = axios;
window.axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
/**
* Echo exposes an expressive API for subscribing to channels and listening
* for events that are broadcast by Laravel. Echo and event broadcasting
* allows your team to easily build robust real-time web applications.
*/
// import Echo from 'laravel-echo';
// import Pusher from 'pusher-js';
// window.Pusher = Pusher;
// window.Echo = new Echo({
// broadcaster: 'pusher',
// key: import.meta.env.VITE_PUSHER_APP_KEY,
// cluster: import.meta.env.VITE_PUSHER_APP_CLUSTER ?? 'mt1',
// wsHost: import.meta.env.VITE_PUSHER_HOST ? import.meta.env.VITE_PUSHER_HOST : `ws-${import.meta.env.VITE_PUSHER_APP_CLUSTER}.pusher.com`,
// wsPort: import.meta.env.VITE_PUSHER_PORT ?? 80,
// wssPort: import.meta.env.VITE_PUSHER_PORT ?? 443,
// forceTLS: (import.meta.env.VITE_PUSHER_SCHEME ?? 'https') === 'https',
// enabledTransports: ['ws', 'wss'],
// });

View File

@@ -0,0 +1,30 @@
import React, {
FC, HTMLInputTypeAttribute,
ReactElement
} from "react"
const Field: FC<FieldProps> = ({children, type = 'text', ...props}) => {
return <div className="form-group">
{children && <label className="block text-gray-900 dark:text-gray-200"
htmlFor={props.id ?? undefined}>
{children}
</label>}
<input className="w-full mt-2 rounded dark:bg-gray-700"
type={type}
{...props}/>
</div>
}
export default Field
interface FieldProps {
children?: ReactElement|string,
type?: HTMLInputTypeAttribute,
name: string,
id?: string,
value: any,
placeholder?: string,
autoFocus?: boolean,
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
}

View File

@@ -0,0 +1,27 @@
import React from "react";
import {Link} from "react-router-dom";
import useAuthUser from "../hooks/AuthUser";
const Header = () => {
const {authUser, logout} = useAuthUser()
return <header className="bg-blue-700 text-white py-3 px-5 text-xl flex justify-between">
<div>
<Link to="/">Bermite</Link>
</div>
{authUser && <nav className="flex gap-2">
<Link to="/pluviometrie">Pluviométrie</Link>
<Link to="/meteo">Météo</Link>
</nav>}
{authUser ? <span className="flex gap-2"><Link to="/profile">{authUser.name}</Link><button onClick={logout}>logout</button></span>
: <span className="flex gap-2">
<Link to="/connexion">Connexion</Link>
<Link to="/sinscrire">S'inscrire</Link>
</span>}
</header>
}
export default Header

View File

@@ -0,0 +1,10 @@
import React, {PropsWithChildren} from "react"
const PageLayout = ({children}: PropsWithChildren) => {
return <div className="m-2">
{children}
</div>
}
export default PageLayout

View File

@@ -0,0 +1,45 @@
import React, {Dispatch, FC, FormEvent, SetStateAction, useState} from "react"
import useAxiosTools from "../../hooks/AxiosTools";
import Field from "../Field";
const AddRainfall: FC<AddRainfallProps> = ({reload}) => {
const {axiosPost} = useAxiosTools()
const [date, setDate] = useState((new Date()).toSQLDate())
const [value, setValue] = useState(0)
const handleSubmit = async (event: FormEvent) => {
event.preventDefault()
try {
await axiosPost('/api/rainfalls', {date, value})
setDate((new Date()).toSQLDate())
setValue(0)
reload(new Date())
} catch (e) {
console.error(e)
}
}
return <form onSubmit={handleSubmit}>
<h2>Ajout d'une mesure</h2>
<Field type="date"
name="date"
placeholder="Email"
value={date}
onChange={event => setDate(event.target.value)}
autoFocus>Date</Field>
<Field type="number"
name="value"
placeholder="10"
value={value}
onChange={event => setValue(Number(event.target.value))}>Mesure</Field>
<button type="submit" className="mt-3 w-full bg-blue-700 rounded">Valider</button>
</form>
}
export default AddRainfall
interface AddRainfallProps {
reload: Dispatch<SetStateAction<Date>>
}

View File

@@ -0,0 +1,44 @@
import React, {FC, useEffect, useState} from "react"
import useAxiosTools from "../../hooks/AxiosTools";
import {rainfall} from "../../types";
import {AxiosError} from "axios";
const LastFiveMesure: FC<LastFiveMesureProps> = ({loadedAt}) => {
const {error, setError, axiosGet} = useAxiosTools()
const [data, setData] = useState<rainfall[]>([])
useEffect(() => {
fetchData()
}, [loadedAt])
const fetchData = async () => {
try {
const res = await axiosGet('/api/rainfalls/last')
setData(res.data)
} catch (e) {
if (e instanceof AxiosError) {
setError(e.message)
} else {
console.error(e)
}
}
}
return <div>
<h1>5 dernières mesures</h1>
{error && <div>{error}</div>}
<ul>
{data.map(line => <li key={line.id} className="w-36 flex justify-between">
<span>{(new Date(line.date)).toLocaleDateString()}</span>
<span>{line.value}</span>
</li>)}
</ul>
</div>
}
export default LastFiveMesure
interface LastFiveMesureProps {
loadedAt: Date,
}

View File

@@ -0,0 +1,100 @@
import * as d3 from "d3"
import React, {FC, LegacyRef, useEffect, useRef} from "react"
import {rainfall} from "../../types";
const RainfallGraph: FC<RainfallGraphProps> = ({width, height, data, start_date, end_date}) => {
const svgRef = useRef<SVGSVGElement|null>(null)
useEffect(() => {
renderSVG()
}, [width, height, data])
const renderSVG = () => {
const margin = {top: 10, right: 30, bottom: 30, left: 20}
const gWidth = width - margin.left - margin.right
const gHeight = height - margin.top - margin.bottom
console.log('data', data)
d3.select(svgRef.current).selectAll("*").remove()
const svg = d3.select(svgRef.current)
.attr('width', gWidth + margin.left + margin.right)
.attr('height', gHeight + margin.top + margin.bottom)
.attr('class', 'relative')
.append('g')
.attr('transform', "translate(" + margin.left + "," + margin.top + ")")
const yMax = data.reduce((result, item) => item.value > result ? item.value : result, 0)
const y = d3.scaleLinear()
.domain([0, yMax + 10])
.range([gHeight, 0])
const x = d3.scaleUtc()
.domain([(new Date(start_date)), (new Date(end_date))])
.range([margin.left, width - margin.right])
const yAxis = svg.append('g')
.attr('transform', `translate(${margin.left},0)`)
.call(d3.axisLeft(y).ticks(height / 80))
.call(g => g.select('.domain').remove())
.call(g => g.append('text')
.attr('x', -margin.left)
.attr('y', 10)
.attr('fill', 'currentColor')
.attr('text-anchor', 'start')
)
svg.append("g")
.attr("transform", "translate(0," + gHeight + ")")
.call(d3.axisBottom(x)
.tickFormat(
// @ts-ignore
d3.timeFormat("%d/%m/%Y")
)
, 0)
if (data.length === 0) {
// no values text
const titleBox = svg.append("g")
.attr("x", (width))
.attr("y", height)
titleBox.append("text")
.attr("x", (width / 2))
.attr("y", height / 2)
.attr("text-anchor", "middle")
.style("font-size", "20px")
.text('Aucune Donnée')
} else {
yAxis.call(g => g.selectAll(".tick line").clone()
.attr("x2", width - margin.left - margin.right)
.attr("stroke-opacity", 0.1))
}
svg.selectAll(".bar")
.data(data)
.enter()
.append("rect")
.attr("class", "bar")
.attr("fill", "steelblue")
.attr("x", d => x(new Date(d.date)) * (1 - (100 / data.length)/100))
.attr("y", d => y(d.value))
.attr("width", (width - 44 + ((width - 44) / (data.length + 1))) / (data.length + 1) - 2)
.attr("height", d => height - margin.bottom - 10 - y(d.value))
.append('title')
.text(d => `${(new Date(d.date)).toLocaleDateString()} : ${d.value}`)
}
return <svg ref={svgRef} />
}
export default RainfallGraph
interface RainfallGraphProps {
width: number,
height: number,
data: rainfall[],
start_date: string,
end_date: string,
}

View File

@@ -0,0 +1,7 @@
interface Date {
toSQLDate(): string,
}
Date.prototype.toSQLDate = function (): string {
return (new Date(this)).toISOString().split('T')[0]
}

View File

@@ -0,0 +1,73 @@
import React, {
createContext,
Dispatch,
PropsWithChildren,
SetStateAction,
useContext,
useEffect,
useState
} from "react";
import axios from "axios";
const AuthUserContext = createContext<AuthUserProps|undefined>(undefined)
interface AuthUserProps {
authUser?: User|null,
setAuthUser: Dispatch<SetStateAction<User | null>>,
loadingAuthUser: boolean,
logout: () => void,
}
export const AuthUserProvider = ({children}: PropsWithChildren) => {
const [authUser, setAuthUser] = useState<User|null>(null)
const [loadingAuthUser, setLoadingAuthUser] = useState(true)
useEffect(() => {
(async () => {
try {
const res = await axios.get('/api/user')
setAuthUser(res.data)
} catch (e) {
// @ts-ignore
if (e.response.status === 401) {
console.info('no user login')
}
} finally {
setLoadingAuthUser(false)
}
})()
}, [])
const logout = async () => {
try {
setLoadingAuthUser(false)
const res = await axios.delete('/api/logout')
setAuthUser(null)
window.location.replace('/')
} catch (e) {
console.error(e)
} finally {
setLoadingAuthUser(false)
}
}
return <AuthUserContext.Provider value={{authUser, setAuthUser, loadingAuthUser, logout}}>
{children}
</AuthUserContext.Provider>
}
const useAuthUser = () => {
const context = useContext(AuthUserContext)
if (context === undefined) {
throw new Error('Add AuthUserProvider to use AuthUserContext')
}
return context
}
export default useAuthUser
interface User {
id: number,
name: string,
email: string,
}

View File

@@ -0,0 +1,15 @@
import {useState} from "react";
import axios from "axios";
const useAxiosTools = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState<string|null>(null)
const axiosGet = axios.get
const axiosPost = axios.post
return {loading, setLoading, error, setError, axiosGet, axiosPost}
}
export default useAxiosTools

View File

@@ -0,0 +1,36 @@
import {useEffect, useLayoutEffect, useRef, useState} from "react"
const useDimension = () => {
const RESET_TIMEOUT = 300
let movement_timer: number|undefined = undefined
const targetRef = useRef<any>()
const [dimensions, setDimensions] = useState({ width:0, height: 0 })
useEffect(() => {
window.addEventListener('resize', ()=>{
clearInterval(movement_timer)
movement_timer = setTimeout(testDimensions, RESET_TIMEOUT)
})
})
useLayoutEffect(() => {
testDimensions()
}, [])
const testDimensions = () => {
if (targetRef.current) {
setDimensions({
width: targetRef.current.offsetWidth,
height: targetRef.current.offsetHeight
})
}
}
return {
targetRef,
dimensions,
}
}
export default useDimension

View File

@@ -0,0 +1,14 @@
import React from "react"
import {AuthUserProvider} from "../hooks/AuthUser";
import Router from "./Router";
const App = () => {
return <main className="dark:bg-gray-900 dark:text-white h-screen">
<AuthUserProvider>
<Router />
</AuthUserProvider>
</main>
}
export default App

View File

@@ -0,0 +1,46 @@
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";
const Login = () => {
const navigate = useNavigate()
const {setAuthUser} = useAuthUser()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (event: FormEvent) => {
event.preventDefault()
try {
await axios.get('/sanctum/csrf-cookie')
const res = await axios.post('/api/login', {email, password})
setAuthUser(res.data.user)
navigate('/')
} catch (e) {
console.error(e)
}
}
return <div>
<form onSubmit={handleSubmit} className="w-96 mx-auto mt-10 border shadow p-3">
<h1 className="text-center">Connexion</h1>
<Field type="email"
name="email"
placeholder="Email"
value={email}
onChange={event => setEmail(event.target.value)}
autoFocus>Email</Field>
<Field type="password"
name="password"
placeholder="******"
value={password}
onChange={event => setPassword(event.target.value)}>Mot de passe</Field>
<button type="submit" className="mt-5 bg-blue-700 w-full block text-white px-5 py-2 text-lg rounded">Valider</button>
</form>
</div>
}
export default Login

View File

@@ -0,0 +1,19 @@
import React from "react";
import useAuthUser from "../../hooks/AuthUser";
const Profile = () => {
const {authUser} = useAuthUser()
return <>
<h1>Profile</h1>
<div>
<span>Nom: <strong>{authUser?.name}</strong></span>
</div>
<div>Update name & email</div>
<div>Change password</div>
<div>Delete Account</div>
</>
}
export default Profile

View File

@@ -0,0 +1,51 @@
import React, {ChangeEvent, FormEvent, SyntheticEvent, useState} from "react"
import Field from "../../components/Field";
import axios from "axios";
import {useNavigate} from "react-router-dom";
const Register = () => {
const navigate = useNavigate()
const [name, setName] = useState('')
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
const handleSubmit = async (event: FormEvent) => {
event.preventDefault()
try {
await axios.get('/sanctum/csrf-cookie')
const res = await axios.post('/api/register', {name, email, password})
console.log(name, email, password)
navigate('/')
} catch (e) {
console.error(e)
}
}
return <div>
<form onSubmit={handleSubmit} className="w-96 mx-auto mt-10 border shadow p-3">
<h1 className="text-center">S'inscrire</h1>
<Field placeholder="Nom"
name="name"
value={name}
onChange={event => setName(event.target.value)}
autoFocus>Nom</Field>
<Field type="email"
name="email"
placeholder="Email"
value={email}
onChange={event => setEmail(event.target.value)}
autoFocus>Email</Field>
<Field type="password"
name="password"
placeholder="******"
value={password}
onChange={event => setPassword(event.target.value)}
autoFocus>Mot de passe</Field>
<button type="submit" className="mt-5 bg-blue-700 w-full block text-white px-5 py-2 text-lg rounded">Valider</button>
</form>
</div>
}
export default Register

View File

@@ -0,0 +1,11 @@
import React from "react"
import {Link} from "react-router-dom";
const Home = () => {
return <div>Home <Link to="/connexion">Connexion</Link>
<Link to="/sinscrire">S'inscrire</Link>
</div>
}
export default Home

View File

@@ -0,0 +1,11 @@
import React from "react"
import PageLayout from "../components/PageLayout";
const Meteo = () => {
return <PageLayout>
Météo
</PageLayout>
}
export default Meteo

View File

@@ -0,0 +1,65 @@
import React, {useEffect, useState} from "react"
import PageLayout from "../components/PageLayout";
import LastFiveMesure from "../components/rainfall/LastFiveMesure";
import AddRainfall from "../components/rainfall/AddRainfall";
import RainfallGraph from "../components/rainfall/RainfallGraph";
import useAxiosTools from "../hooks/AxiosTools";
import {rainfall} from "../types";
import Field from "../components/Field";
import useDimension from "../hooks/DimensionHook";
const Rainfall = () => {
const [loadedAt, reload] = useState(new Date)
const [graphData, setGraphData] = useState<rainfall[]>([])
const [graphDetails, setGraphDetails] = useState({
start_date: (new Date((new Date()).setMonth((new Date).getMonth() - 1))).toSQLDate(),
end_date: (new Date()).toSQLDate(),
})
const {axiosGet} = useAxiosTools()
const {targetRef, dimensions} = useDimension()
useEffect(() => {
fetchGraphData()
}, [loadedAt])
useEffect(() => {
fetchGraphData()
}, [graphDetails])
const fetchGraphData = async () => {
try {
const params = `start=${graphDetails.start_date}&end=${graphDetails.end_date}`
const res = await axiosGet(`/api/rainfalls/graph?${params}`)
setGraphData(res.data)
} catch (e) {
console.error(e)
}
}
return <PageLayout>
<div className="flex justify-between">
<LastFiveMesure loadedAt={loadedAt} />
<AddRainfall reload={reload} />
</div>
<form className="flex">
<Field name="start_date"
type="date"
value={graphDetails.start_date}
onChange={e => setGraphDetails({...graphDetails, start_date: (new Date(e.target.value)).toSQLDate()})} />
<Field name="start_date"
type="date"
value={graphDetails.end_date}
onChange={e => setGraphDetails({...graphDetails, end_date: (new Date(e.target.value)).toSQLDate()})} />
</form>
<div ref={targetRef}>
<RainfallGraph width={dimensions.width}
height={500}
data={graphData} start_date={graphDetails.start_date}
end_date={graphDetails.end_date} />
</div>
</PageLayout>
}
export default Rainfall

View File

@@ -0,0 +1,35 @@
import React, {Suspense} from "react";
import {BrowserRouter, Link, Route, Routes} from "react-router-dom";
import Home from "./Home";
import Login from "./Auth/Login";
import Register from "./Auth/Register";
import useAuthUser from "../hooks/AuthUser";
import Profile from "./Auth/Profile";
import Header from "../components/Header";
import Rainfall from "./Rainfall";
import Meteo from "./Meteo";
const Router = () => {
const {authUser, loadingAuthUser, logout} = useAuthUser()
return <>
{loadingAuthUser ? '...loading'
: <BrowserRouter>
<Suspense fallback={'... loading'}>
<Header />
<Routes>
<Route path="/" element={<Home />} />
<Route path="/profile" element={<Profile />} />
<Route path="/connexion" element={<Login />} />
<Route path="/sinscrire" element={<Register />} />
<Route path="/meteo" element={<Meteo />} />
<Route path="/pluviometrie" element={<Rainfall />} />
</Routes>
</Suspense>
</BrowserRouter>
}
</>
}
export default Router

5
resources/js/types.ts Normal file
View File

@@ -0,0 +1,5 @@
export interface rainfall {
id?: number,
date: string,
value: number,
}

View File

@@ -0,0 +1,20 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=figtree:400,500,600&display=swap" rel="stylesheet" />
<!-- Scripts -->
@viteReactRefresh
@vite(['resources/js/app.tsx'])
</head>
<body class="font-sans antialiased">
<div id="app" />
</body>
</html>

View File

@@ -0,0 +1,12 @@
<x-mail::message>
# Introduction
The body of your message.
<x-mail::button :url="''">
Button Text
</x-mail::button>
Thanks,<br>
{{ config('app.name') }}
</x-mail::message>

File diff suppressed because one or more lines are too long