Compare commits

...

10 Commits

Author SHA1 Message Date
Romulus21
f5bddca70c update pulse 2024-05-25 16:03:48 +02:00
Romulus21
ad65ea2e35 format code 2024-05-25 15:54:46 +02:00
Romulus21
64e99676ab fix auth redirect 2024-05-25 15:53:47 +02:00
Romulus21
906db7a908 update paackages 2024-05-25 15:51:38 +02:00
Romulus21
7ba13389d7 add fr lang error message 2024-05-25 15:51:13 +02:00
Romulus21
3bead64695 add rainfall sum 2024-05-03 10:17:05 +02:00
Romulus21
49f0abd08c add update package & weather details 2024-04-22 23:50:59 +02:00
Romulus21
2f2077497d add addrainfall on home 2024-04-22 22:23:59 +02:00
Romulus21
2f260555cb add month name to monthlytable 2024-03-11 09:10:02 +01:00
Romulus21
dc9351dd9a improve months values 2024-03-09 16:19:10 +01:00
24 changed files with 2205 additions and 2184 deletions

View File

@@ -80,7 +80,9 @@ class AuthController extends Controller
$data = $request->validate([
'name' => ['required', 'string', 'min:3'],
'email' => ['required', 'email', 'unique:users,email'],
'password' => ['required', 'min:8'],
'password' => ['required', 'min:8', 'regex:/^(?=.*?[A-Z])(?=.*[a-z])(?=.*[0-9]).{8,}$/'],
], [
'password.regex' => __('validation.password_rules'),
]);
$user = User::create($data);

View File

@@ -113,9 +113,17 @@ class RainfallController extends Controller
public function lastMonths(Request $request)
{
$lang = array_values(array_filter($request->getLanguages(), fn ($v) => str_contains($v, '_')))[0] ?? 'en_US';
setlocale(LC_TIME, $lang);
Carbon::setLocale(explode('_', $lang)[0]);
$firstOfLastYear = now()->subYear()->firstOfYear();
$diff = now()->diffInMonths($firstOfLastYear);
$result = [];
for ($i = 12; $i >= 0; $i--) {
for ($i = $diff; $i >= 0; $i--) {
$date = now()->subMonths($i);
$month = $date->month;
$firstOfMonth = now()->subMonths($i)->firstOfMonth();
$lastOfMonth = now()->subMonths($i)->lastOfMonth();
$rainfalls = $request->user()
@@ -123,10 +131,14 @@ class RainfallController extends Controller
->whereBetween('date', [$firstOfMonth, $lastOfMonth])
->sum('value');
$result[] = [
if (! isset($result[$month])) {
$result[$month] = [];
}
$result[$month][] = [
'year' => $date->year,
'month' => $date->month,
'label' => $date->monthName.' '.$date->year,
'label' => $date->monthName,
'values' => (int) $rainfalls,
];
}

View File

@@ -8,7 +8,7 @@
"php": "^8.3",
"guzzlehttp/guzzle": "^7.2",
"laravel/framework": "^10.10",
"laravel/pulse": "^1.0@beta",
"laravel/pulse": "^1.2",
"laravel/sanctum": "^3.2",
"laravel/tinker": "^2.8"
},

850
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -83,7 +83,7 @@ return [
|
*/
'locale' => 'en',
'locale' => 'fr',
/*
|--------------------------------------------------------------------------
@@ -96,7 +96,7 @@ return [
|
*/
'fallback_locale' => 'en',
'fallback_locale' => 'fr',
/*
|--------------------------------------------------------------------------
@@ -109,7 +109,7 @@ return [
|
*/
'faker_locale' => 'en_US',
'faker_locale' => 'fr_FR',
/*
|--------------------------------------------------------------------------

View File

@@ -0,0 +1,84 @@
<?php
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
use Laravel\Pulse\Support\PulseMigration;
return new class extends PulseMigration
{
/**
* Run the migrations.
*/
public function up(): void
{
if (! $this->shouldRun()) {
return;
}
Schema::create('pulse_values', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('timestamp');
$table->string('type');
$table->mediumText('key');
match ($this->driver()) {
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
'sqlite' => $table->string('key_hash'),
};
$table->mediumText('value');
$table->index('timestamp'); // For trimming...
$table->index('type'); // For fast lookups and purging...
$table->unique(['type', 'key_hash']); // For data integrity and upserts...
});
Schema::create('pulse_entries', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('timestamp');
$table->string('type');
$table->mediumText('key');
match ($this->driver()) {
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
'sqlite' => $table->string('key_hash'),
};
$table->bigInteger('value')->nullable();
$table->index('timestamp'); // For trimming...
$table->index('type'); // For purging...
$table->index('key_hash'); // For mapping...
$table->index(['timestamp', 'type', 'key_hash', 'value']); // For aggregate queries...
});
Schema::create('pulse_aggregates', function (Blueprint $table) {
$table->id();
$table->unsignedInteger('bucket');
$table->unsignedMediumInteger('period');
$table->string('type');
$table->mediumText('key');
match ($this->driver()) {
'mariadb', 'mysql' => $table->char('key_hash', 16)->charset('binary')->virtualAs('unhex(md5(`key`))'),
'pgsql' => $table->uuid('key_hash')->storedAs('md5("key")::uuid'),
'sqlite' => $table->string('key_hash'),
};
$table->string('aggregate');
$table->decimal('value', 20, 2);
$table->unsignedInteger('count')->nullable();
$table->unique(['bucket', 'period', 'type', 'aggregate', 'key_hash']); // Force "on duplicate update"...
$table->index(['period', 'bucket']); // For trimming...
$table->index('type'); // For purging...
$table->index(['period', 'type', 'aggregate', 'bucket']); // For aggregate queries...
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('pulse_values');
Schema::dropIfExists('pulse_entries');
Schema::dropIfExists('pulse_aggregates');
}
};

20
lang/en/auth.php Normal file
View File

@@ -0,0 +1,20 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Authentication Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used during authentication for various
| messages that we need to display to the user. You are free to modify
| these language lines according to your application's requirements.
|
*/
'failed' => 'These credentials do not match our records.',
'password' => 'The provided password is incorrect.',
'throttle' => 'Too many login attempts. Please try again in :seconds seconds.',
];

19
lang/en/pagination.php Normal file
View File

@@ -0,0 +1,19 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Pagination Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are used by the paginator library to build
| the simple pagination links. You are free to change them to anything
| you want to customize your views to better match your application.
|
*/
'previous' => '&laquo; Previous',
'next' => 'Next &raquo;',
];

22
lang/en/passwords.php Normal file
View File

@@ -0,0 +1,22 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Password Reset Language Lines
|--------------------------------------------------------------------------
|
| The following language lines are the default lines which match reasons
| that are given by the password broker for a password update attempt
| has failed, such as for an invalid token or invalid new password.
|
*/
'reset' => 'Your password has been reset.',
'sent' => 'We have emailed your password reset link.',
'throttled' => 'Please wait before retrying.',
'token' => 'This password reset token is invalid.',
'user' => "We can't find a user with that email address.",
];

191
lang/en/validation.php Normal file
View File

@@ -0,0 +1,191 @@
<?php
return [
/*
|--------------------------------------------------------------------------
| Validation Language Lines
|--------------------------------------------------------------------------
|
| The following language lines contain the default error messages used by
| the validator class. Some of these rules have multiple versions such
| as the size rules. Feel free to tweak each of these messages here.
|
*/
'accepted' => 'The :attribute field must be accepted.',
'accepted_if' => 'The :attribute field must be accepted when :other is :value.',
'active_url' => 'The :attribute field must be a valid URL.',
'after' => 'The :attribute field must be a date after :date.',
'after_or_equal' => 'The :attribute field must be a date after or equal to :date.',
'alpha' => 'The :attribute field must only contain letters.',
'alpha_dash' => 'The :attribute field must only contain letters, numbers, dashes, and underscores.',
'alpha_num' => 'The :attribute field must only contain letters and numbers.',
'array' => 'The :attribute field must be an array.',
'ascii' => 'The :attribute field must only contain single-byte alphanumeric characters and symbols.',
'before' => 'The :attribute field must be a date before :date.',
'before_or_equal' => 'The :attribute field must be a date before or equal to :date.',
'between' => [
'array' => 'The :attribute field must have between :min and :max items.',
'file' => 'The :attribute field must be between :min and :max kilobytes.',
'numeric' => 'The :attribute field must be between :min and :max.',
'string' => 'The :attribute field must be between :min and :max characters.',
],
'boolean' => 'The :attribute field must be true or false.',
'can' => 'The :attribute field contains an unauthorized value.',
'confirmed' => 'The :attribute field confirmation does not match.',
'current_password' => 'The password is incorrect.',
'date' => 'The :attribute field must be a valid date.',
'date_equals' => 'The :attribute field must be a date equal to :date.',
'date_format' => 'The :attribute field must match the format :format.',
'decimal' => 'The :attribute field must have :decimal decimal places.',
'declined' => 'The :attribute field must be declined.',
'declined_if' => 'The :attribute field must be declined when :other is :value.',
'different' => 'The :attribute field and :other must be different.',
'digits' => 'The :attribute field must be :digits digits.',
'digits_between' => 'The :attribute field must be between :min and :max digits.',
'dimensions' => 'The :attribute field has invalid image dimensions.',
'distinct' => 'The :attribute field has a duplicate value.',
'doesnt_end_with' => 'The :attribute field must not end with one of the following: :values.',
'doesnt_start_with' => 'The :attribute field must not start with one of the following: :values.',
'email' => 'The :attribute field must be a valid email address.',
'ends_with' => 'The :attribute field must end with one of the following: :values.',
'enum' => 'The selected :attribute is invalid.',
'exists' => 'The selected :attribute is invalid.',
'extensions' => 'The :attribute field must have one of the following extensions: :values.',
'file' => 'The :attribute field must be a file.',
'filled' => 'The :attribute field must have a value.',
'gt' => [
'array' => 'The :attribute field must have more than :value items.',
'file' => 'The :attribute field must be greater than :value kilobytes.',
'numeric' => 'The :attribute field must be greater than :value.',
'string' => 'The :attribute field must be greater than :value characters.',
],
'gte' => [
'array' => 'The :attribute field must have :value items or more.',
'file' => 'The :attribute field must be greater than or equal to :value kilobytes.',
'numeric' => 'The :attribute field must be greater than or equal to :value.',
'string' => 'The :attribute field must be greater than or equal to :value characters.',
],
'hex_color' => 'The :attribute field must be a valid hexadecimal color.',
'image' => 'The :attribute field must be an image.',
'in' => 'The selected :attribute is invalid.',
'in_array' => 'The :attribute field must exist in :other.',
'integer' => 'The :attribute field must be an integer.',
'ip' => 'The :attribute field must be a valid IP address.',
'ipv4' => 'The :attribute field must be a valid IPv4 address.',
'ipv6' => 'The :attribute field must be a valid IPv6 address.',
'json' => 'The :attribute field must be a valid JSON string.',
'lowercase' => 'The :attribute field must be lowercase.',
'lt' => [
'array' => 'The :attribute field must have less than :value items.',
'file' => 'The :attribute field must be less than :value kilobytes.',
'numeric' => 'The :attribute field must be less than :value.',
'string' => 'The :attribute field must be less than :value characters.',
],
'lte' => [
'array' => 'The :attribute field must not have more than :value items.',
'file' => 'The :attribute field must be less than or equal to :value kilobytes.',
'numeric' => 'The :attribute field must be less than or equal to :value.',
'string' => 'The :attribute field must be less than or equal to :value characters.',
],
'mac_address' => 'The :attribute field must be a valid MAC address.',
'max' => [
'array' => 'The :attribute field must not have more than :max items.',
'file' => 'The :attribute field must not be greater than :max kilobytes.',
'numeric' => 'The :attribute field must not be greater than :max.',
'string' => 'The :attribute field must not be greater than :max characters.',
],
'max_digits' => 'The :attribute field must not have more than :max digits.',
'mimes' => 'The :attribute field must be a file of type: :values.',
'mimetypes' => 'The :attribute field must be a file of type: :values.',
'min' => [
'array' => 'The :attribute field must have at least :min items.',
'file' => 'The :attribute field must be at least :min kilobytes.',
'numeric' => 'The :attribute field must be at least :min.',
'string' => 'The :attribute field must be at least :min characters.',
],
'min_digits' => 'The :attribute field must have at least :min digits.',
'missing' => 'The :attribute field must be missing.',
'missing_if' => 'The :attribute field must be missing when :other is :value.',
'missing_unless' => 'The :attribute field must be missing unless :other is :value.',
'missing_with' => 'The :attribute field must be missing when :values is present.',
'missing_with_all' => 'The :attribute field must be missing when :values are present.',
'multiple_of' => 'The :attribute field must be a multiple of :value.',
'not_in' => 'The selected :attribute is invalid.',
'not_regex' => 'The :attribute field format is invalid.',
'numeric' => 'The :attribute field must be a number.',
'password' => [
'letters' => 'The :attribute field must contain at least one letter.',
'mixed' => 'The :attribute field must contain at least one uppercase and one lowercase letter.',
'numbers' => 'The :attribute field must contain at least one number.',
'symbols' => 'The :attribute field must contain at least one symbol.',
'uncompromised' => 'The given :attribute has appeared in a data leak. Please choose a different :attribute.',
],
'present' => 'The :attribute field must be present.',
'present_if' => 'The :attribute field must be present when :other is :value.',
'present_unless' => 'The :attribute field must be present unless :other is :value.',
'present_with' => 'The :attribute field must be present when :values is present.',
'present_with_all' => 'The :attribute field must be present when :values are present.',
'prohibited' => 'The :attribute field is prohibited.',
'prohibited_if' => 'The :attribute field is prohibited when :other is :value.',
'prohibited_unless' => 'The :attribute field is prohibited unless :other is in :values.',
'prohibits' => 'The :attribute field prohibits :other from being present.',
'regex' => 'The :attribute field format is invalid.',
'required' => 'The :attribute field is required.',
'required_array_keys' => 'The :attribute field must contain entries for: :values.',
'required_if' => 'The :attribute field is required when :other is :value.',
'required_if_accepted' => 'The :attribute field is required when :other is accepted.',
'required_unless' => 'The :attribute field is required unless :other is in :values.',
'required_with' => 'The :attribute field is required when :values is present.',
'required_with_all' => 'The :attribute field is required when :values are present.',
'required_without' => 'The :attribute field is required when :values is not present.',
'required_without_all' => 'The :attribute field is required when none of :values are present.',
'same' => 'The :attribute field must match :other.',
'size' => [
'array' => 'The :attribute field must contain :size items.',
'file' => 'The :attribute field must be :size kilobytes.',
'numeric' => 'The :attribute field must be :size.',
'string' => 'The :attribute field must be :size characters.',
],
'starts_with' => 'The :attribute field must start with one of the following: :values.',
'string' => 'The :attribute field must be a string.',
'timezone' => 'The :attribute field must be a valid timezone.',
'unique' => 'The :attribute has already been taken.',
'uploaded' => 'The :attribute failed to upload.',
'uppercase' => 'The :attribute field must be uppercase.',
'url' => 'The :attribute field must be a valid URL.',
'ulid' => 'The :attribute field must be a valid ULID.',
'uuid' => 'The :attribute field must be a valid UUID.',
/*
|--------------------------------------------------------------------------
| Custom Validation Language Lines
|--------------------------------------------------------------------------
|
| Here you may specify custom validation messages for attributes using the
| convention "attribute.rule" to name the lines. This makes it quick to
| specify a specific custom language line for a given attribute rule.
|
*/
'custom' => [
'attribute-name' => [
'rule-name' => 'custom-message',
],
],
/*
|--------------------------------------------------------------------------
| Custom Validation Attributes
|--------------------------------------------------------------------------
|
| The following language lines are used to swap our attribute placeholder
| with something more reader friendly such as "E-Mail Address" instead
| of "email". This simply helps us make our message more expressive.
|
*/
'attributes' => [],
];

7
lang/fr/validation.php Normal file
View File

@@ -0,0 +1,7 @@
<?php
return [
'exists' => 'Le champ :attribute est invalide.',
'invalid_credentials' => 'Identifiant ou mot de passe incorrect.',
'password_rules' => 'Le mot de passe doit contenir des lettres majuscules et minuscules, des chiffres et au moins 8 caractères.'
];

View File

@@ -12,31 +12,29 @@
},
"devDependencies": {
"@tailwindcss/forms": "^0.5.7",
"@types/d3": "^7.4.3",
"@types/react": "^18.2.63",
"@types/react-dom": "^18.2.20",
"@typescript-eslint/eslint-plugin": "^7.1.1",
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@typescript-eslint/eslint-plugin": "^7.10.0",
"@vite-pwa/assets-generator": "^0.0.11",
"@vitejs/plugin-react": "^4.2.1",
"autoprefixer": "^10.4.18",
"axios": "^1.6.7",
"@vitejs/plugin-react": "^4.3.0",
"autoprefixer": "^10.4.19",
"axios": "^1.7.2",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.34.0",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-tailwindcss": "^3.14.3",
"eslint-plugin-react": "^7.34.1",
"eslint-plugin-react-hooks": "^4.6.2",
"eslint-plugin-tailwindcss": "^3.17.0",
"laravel-vite-plugin": "^0.8.1",
"postcss": "^8.4.35",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"postcss": "^8.4.38",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"rollup-plugin-copy": "^3.5.0",
"tailwindcss": "^3.4.1",
"typescript": "^5.3.3",
"vite": "^4.5.2"
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5",
"vite": "^4.5.3"
},
"dependencies": {
"d3": "^7.8.5",
"echarts": "^5.5.0",
"react-router-dom": "^6.22.2",
"react-router-dom": "^6.23.1",
"vite-plugin-pwa": "^0.17.5"
}
}

2865
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,15 +1,20 @@
import React from "react"
import {Link, useLocation} from "react-router-dom"
import useAuthUser from "../hooks/AuthUser"
import useDimension from "../hooks/DimensionHook"
const Header = () => {
const {authUser} = useAuthUser()
const location = useLocation()
const {targetRef, dimensions} = useDimension()
return <header className="flex justify-between bg-blue-700 px-5 py-3 text-xl text-white">
return <header ref={targetRef}
className="flex justify-between bg-blue-700 px-5 py-3 text-xl text-white">
<div>
<Link to="/">Bermite</Link>
<Link to="/" className={`font-bold`}>
{dimensions.width < 400 ? 'B' : 'Bermite'}
</Link>
</div>
{authUser?.locations && authUser.locations.length > 0 && <nav className="flex gap-2">
@@ -19,7 +24,9 @@ const Header = () => {
{authUser
? <span className="flex gap-2">
<Link to="/profile" className={location.pathname === '/profile' ? 'font-bold' : ''}>{authUser.name}</Link>
<Link to="/profile" className={`${location.pathname === '/profile' ? 'font-bold' : ''} font-bold`}>
{dimensions.width < 400 ? authUser.name[0] : authUser.name}
</Link>
</span>
: <span className="flex gap-2">
<Link to="/connexion">Connexion</Link>

View File

@@ -23,7 +23,7 @@ const AddRainfall: FC<AddRainfallProps> = ({reload}) => {
}
}
return <Card className="w-full min-w-[300px] self-start overflow-hidden md:w-auto">
return <Card className="w-full self-start overflow-hidden md:w-auto">
<h2 className="-mx-2 -mt-1 bg-blue-500 px-2 py-1 text-center text-lg font-bold text-white">
Ajout d&apos;une mesure
</h2>
@@ -38,7 +38,7 @@ const AddRainfall: FC<AddRainfallProps> = ({reload}) => {
</Field>
{!loading ? <Field type="number"
name="value"
value={data.value}
value={data.value ?? ''}
onChange={event => setData({...data, value: Number(event.target.value)})}>
Mesure
</Field> : <div className="h-[74px]" />}

View File

@@ -1,24 +1,17 @@
import React, {useState, useEffect} from "react"
import React, {FC, useState, useEffect} from "react"
import Card from "../Card"
import useAxiosTools from "../../hooks/AxiosTools"
import {AxiosError} from "axios"
import {monthlyRainfall} from "../../types"
const YearRainfall = () => {
const YearRainfall: FC<YearRainfallProps> = ({loadedAt}) => {
const {errorCatch, errorLabel, setError, axiosGet} = useAxiosTools()
const [data, setData] = useState<monthlyRainfall[]>([])
const months = Array(13)
.reduce((result, item, index) => {
const date = new Date()
console.log(item, index, date)
return item
}, [])
console.log(months)
const [data, setData] = useState<monthlyRainfall[][]>([])
useEffect(() => {
fetchData()
}, [])
}, [loadedAt])
const fetchData = async () => {
try {
@@ -34,15 +27,26 @@ const YearRainfall = () => {
}
return <div>
<Card className="w-full min-w-[300px] self-start overflow-hidden md:w-auto">
<Card className="w-full self-start overflow-hidden md:w-auto">
<h1 className="-mx-2 -mt-1 bg-blue-500 px-2 py-1 text-center text-lg font-bold text-white">Précipitations des derniers mois</h1>
{errorLabel()}
<table className="w-full text-center">
<table className="w-full overflow-y-scroll text-center">
<thead>
<tr>
<th>Mois</th>
<th>{(new Date).getFullYear()}</th>
<th>{(new Date).getFullYear() - 1}</th>
</tr>
</thead>
<tbody>
{data.map(line => <tr key={line.year + '-' + line.month} className="">
<td>{line.label}</td>
<td className="px-2 text-right">{line.values}</td>
</tr>)}
{Object.entries(data)
.map(([month, months]) => {
return <tr key={month}>
<td>{months[0].label}</td>
<td>{months.find(m => m.year === (new Date).getFullYear() && m.month === Number(month))?.values}</td>
<td>{months.find(m => m.year === ((new Date).getFullYear() - 1) && m.month === Number(month))?.values}</td>
</tr>
})}
</tbody>
</table>
</Card>
@@ -50,3 +54,7 @@ const YearRainfall = () => {
}
export default YearRainfall
interface YearRainfallProps {
loadedAt: Date,
}

View File

@@ -27,11 +27,12 @@ export const AuthUserProvider = ({children}: PropsWithChildren) => {
try {
const res = await axios.get('/api/user')
setAuthUser(res.data)
} catch (e) {
} catch (error) {
// @ts-expect-error check axios response status
if (e.response.status === 401) {
if (error.response.status === 401) {
console.info('no user login')
if (window.location.pathname !== '/connexion') {
let url = window.location.pathname.split('/')[1]
if (!['connexion', 'changer-le-mot-de-passe'].includes(url)) {
window.location.href = '/connexion'
}
}

View File

@@ -4,7 +4,7 @@ const useDimension = () => {
const RESET_TIMEOUT = 300
let movement_timer: number|undefined = undefined
const targetRef = useRef<HTMLDivElement|undefined>()
const targetRef = useRef<HTMLDivElement>(null)
const [dimensions, setDimensions] = useState({ width:0, height: 0 })
useEffect(() => {

View File

@@ -45,6 +45,9 @@ const Login = () => {
placeholder="******"
value={password}
onChange={event => setPassword(event.target.value)}>Mot de passe</Field>
<Field type="hidden"
name="form_info" onChange={() => setPassword('')} />
<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>

View File

@@ -1,16 +1,22 @@
import React from "react"
import React, {useState} from "react"
import useAuthUser from "../hooks/AuthUser"
import YearRainfall from "../components/rainfall/YearRainfaill"
import PageLayout from "../components/PageLayout"
import AddRainfall from "../components/rainfall/AddRainfall"
const Home = () => {
const {authUser} = useAuthUser()
const [loadedAt, reload] = useState(new Date)
return <div>
{authUser
? <PageLayout>
<YearRainfall />
<div className="flex flex-col gap-2">
<YearRainfall loadedAt={loadedAt} />
<AddRainfall reload={reload}/>
</div>
</PageLayout>
: <div className="px-5 pt-10">
<h1 className="text-lg font-bold">Application pour enregistrer sa pluviométrie</h1>

View File

@@ -21,12 +21,12 @@ const RainfallGraph = () => {
const {targetRef, dimensions} = useDimension()
useEffect(() => {
setLoading(true)
fetchGraphData()
}, [loadedAt, graphDetails])
const fetchGraphData = async () => {
try {
setLoading(true)
const params = `start=${graphDetails.start_date}&end=${graphDetails.end_date}&period=${graphDetails.period}`
const res = await axiosGet(`/api/rainfalls/graph?${params}`)
setGraphData(res.data)
@@ -38,41 +38,47 @@ const RainfallGraph = () => {
}
return <PageLayout>
<div className="flex flex-wrap justify-between gap-2">
<LastFiveMesure loadedAt={loadedAt} />
<AddRainfall reload={reload} />
{errorLabel()}
<div className="mx-5 mb-2 flex items-center justify-between flex-col md:flex-row gap-5">
<form className="flex flex-wrap gap-2">
<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()
})}/>
<div className="form-control">
<select className={` mt-2 w-full rounded dark:bg-gray-700`}
value={graphDetails.period}
onChange={e => setGraphDetails({...graphDetails, period: e.target.value})}>
<option value="day">Jour</option>
{/* <option value="week">Semaine</option>*/}
<option value="month">Mois</option>
<option value="year">Année</option>
</select>
</div>
</form>
<div>Total : <strong>{graphData.reduce((result, item) => result += item.value, 0)}</strong> mm</div>
</div>
{errorLabel()}
<form className="mx-5 mb-2 flex flex-wrap gap-2">
<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()})} />
<div className="form-control">
<select className={` mt-2 w-full rounded dark:bg-gray-700`}
value={graphDetails.period}
onChange={e => setGraphDetails({...graphDetails, period: e.target.value})}>
<option value="day">Jour</option>
{/* <option value="week">Semaine</option>*/}
<option value="month">Mois</option>
<option value="year">Année</option>
</select>
</div>
</form>
<div ref={targetRef} className="mb-20 min-h-96">
<RainFallEcharts width={dimensions.width}
height={500}
data={graphData}
loading={loading} />
{/*<RainfallGraph width={dimensions.width}*/}
{/* height={500}*/}
{/* data={graphData} start_date={graphDetails.start_date}*/}
{/* end_date={graphDetails.end_date} />*/}
loading={loading}/>
</div>
<div className="flex flex-wrap justify-between gap-2">
<AddRainfall reload={reload}/>
<LastFiveMesure loadedAt={loadedAt}/>
</div>
</PageLayout>
}

View File

@@ -1,4 +1,4 @@
import React, {FC, useEffect, useState} from "react"
import React, {FC, Dispatch, SetStateAction, useEffect, useState} from "react"
import PageLayout from "../components/PageLayout"
import useAxiosTools from "../hooks/AxiosTools"
import {WeatherValue} from "../types"
@@ -7,9 +7,9 @@ import Img from "../components/Img"
const Weather = () => {
const [currentWeather, setCurrentWeather] = useState<WeatherValue|null>(null)
const [currentWeather, setCurrentWeather] = useState<WeatherValue | null>(null)
const [fetchTime, setFetchTime] = useState(0)
const [weatherDays, setWeatherDays] = useState<[string, WeatherValue[]][]|null>(null)
const [weatherDays, setWeatherDays] = useState<[string, WeatherValue[]][] | null>(null)
const {loading, setLoading, errorLabel, errorCatch, cleanErrors, axiosGet} = useAxiosTools()
useEffect(() => {
@@ -21,7 +21,7 @@ const Weather = () => {
return () => clearInterval(timer)
}, [fetchTime])
const getCurrentTime = () => Number(((new Date).getTime() /1000).toFixed())
const getCurrentTime = () => Number(((new Date).getTime() / 1000).toFixed())
const fetchWeather = async () => {
try {
@@ -68,21 +68,24 @@ const Weather = () => {
{errorLabel()}
<Card className="flex justify-between">
<Card className={`flex justify-between ${loading ? 'animate-pulse' : ''}`}>
<div className="m-2 flex flex-col justify-between">
<span className="text-6xl">{currentWeather?.main.temp.toFixed()} °C</span>
<span className="text-secondary dark:text-secondary-ligth">{currentWeather?.weather[0].description}</span>
<span className="text-6xl">{currentWeather?.main.temp.toFixed() ?? '--'} °C</span>
<span
className="text-secondary dark:text-secondary-ligth">{currentWeather?.weather[0].description}</span>
</div>
<div className="flex items-stretch">
<div>
{/*{currentWeather && <img src={`http://openweathermap.org/img/wn/${currentWeather?.weather[0].icon}@2x.png`}*/}
{/* alt={currentWeather?.weather[0].main} width="120px" />}*/}
{currentWeather && <Img src={`images/icons/${currentWeather?.weather[0].icon}.svg`}
alt={currentWeather?.weather[0].main} width="120px" />}
alt={currentWeather?.weather[0].main} width="120px"/>}
</div>
<div className="flex flex-col gap-1">
<span className="pt-5 text-4xl">{currentWeather?.main.temp_max.toFixed()} <span className="text-2xl">°C</span></span>
<span className="mt-2 text-2xl text-secondary dark:text-secondary-ligth">{currentWeather?.main.temp_min.toFixed()} °C</span>
<span className="pt-5 text-4xl">{currentWeather?.main.temp_max.toFixed() ?? '--'} <span
className="text-2xl">°C</span></span>
<span
className="mt-2 text-2xl text-secondary dark:text-secondary-ligth">{currentWeather?.main.temp_min.toFixed() ?? '--'} °C</span>
</div>
</div>
</Card>
@@ -95,11 +98,19 @@ const Weather = () => {
export default Weather
const WeatherCard: FC<{date: string, values: WeatherValue[]}> = ({date, values= []}) => {
const WeatherCard: FC<{ date: string, values: WeatherValue[] }> = ({date, values = []}) => {
const [weatherState, setWeatherState] = useState<{main: string, description: string, icon: string, min: number, max: number}|null>(null)
const [weatherState, setWeatherState] = useState<{
main: string,
description: string,
icon: string,
min: number,
max: number
} | null>(null)
const [showDetails, setShowDetails] = useState(false)
useEffect(() => {
console.log(values)
const weatherState = {
min: 100,
max: -100,
@@ -117,15 +128,15 @@ const WeatherCard: FC<{date: string, values: WeatherValue[]}> = ({date, values=
}
const tag = value.weather[0].main
if (! result[tag]) {
if (!result[tag]) {
result[tag] = 0
}
result[tag]++
})
let itemToReturn: {name:string, value: number}|null = null
let itemToReturn: { name: string, value: number } | null = null
for (const item in result) {
if (! itemToReturn || itemToReturn.value < result[item]) {
if (!itemToReturn || itemToReturn.value < result[item]) {
itemToReturn = {name: item, value: result[item]}
}
}
@@ -142,20 +153,51 @@ const WeatherCard: FC<{date: string, values: WeatherValue[]}> = ({date, values=
}
}, [])
return <div className="flex gap-5">
<div className="flex h-full flex-1 flex-col gap-2">
<span className="text-lg font-bold" title={(new Date(date)).toLocaleDateString()}>{(new Date(date)).getWeekDay()}</span>
<span className="text-secondary dark:text-secondary-ligth">{weatherState?.description}</span>
</div>
<div className="-mt-1.5 flex items-center">
<Img src={`images/icons/${weatherState?.icon}.svg`}
alt={weatherState?.main + ' ' + weatherState?.icon}
width="80px" />
return <>
<div className="flex gap-5" onClick={(() => setShowDetails(!showDetails))}>
<div className="flex h-full flex-1 flex-col gap-2">
<span className="text-lg font-bold"
title={(new Date(date)).toLocaleDateString()}>{(new Date(date)).getWeekDay()}</span>
<span className="text-secondary dark:text-secondary-ligth">{weatherState?.description}</span>
</div>
<div className="-mt-1.5 flex items-center">
<Img src={`images/icons/${weatherState?.icon}.svg`}
alt={weatherState?.main + ' ' + weatherState?.icon}
width="80px"/>
</div>
<div className="flex flex-col gap-1">
<span className="text-lg">{weatherState?.max.toFixed()} °C</span>
<span className="text-secondary dark:text-secondary-ligth">{weatherState?.min.toFixed()} °C</span>
</div>
</div>
<div className="flex flex-col gap-1">
<span className="text-lg">{weatherState?.max.toFixed()} °C</span>
<span className="text-secondary dark:text-secondary-ligth">{weatherState?.min.toFixed()} °C</span>
</div>
</div>
<WeatherDetails showDetails={showDetails} closeDetails={() => showDetails(false)} values={values}/>
</>
}
const WeatherDetails: FC<WeatherDetailsProps> = ({showDetails, closeDetails, values}) => {
return <ul onClick={closeDetails}
className={`${showDetails ? 'h-44 opacity-100' : 'h-0 opacity-0'} flex gap-2 overflow-hidden overflow-x-auto transition-all`}>
{values.map(value => <li key={value.dt} className="w-40">
<div className="text-center">{Number(value.dt_txt.split(' ')[1].split(':')[0])} h</div>
<div>
<Img src={`images/icons/${value.weather[0].icon.replace('n', 'd')}.svg`}
alt={value.weather[0].description}
width="80px"/>
</div>
<div className="text-center">
<span className="font-bold">
{value.main.temp}
</span> °C
</div>
{value.weather[0].description}
</li>)}
</ul>
}
interface WeatherDetailsProps {
showDetails: boolean,
closeDetails: Dispatch<SetStateAction<boolean>>,
values: WeatherValue[],
}

View File

@@ -26,6 +26,7 @@ export interface WeatherRequest {
}
export interface WeatherValue {
dt: number,
dt_txt: string,
main: {
temp: number,

View File

@@ -0,0 +1,19 @@
<x-pulse>
<livewire:pulse.servers cols="full" />
<livewire:pulse.usage cols="4" rows="2" />
<livewire:pulse.queues cols="4" />
<livewire:pulse.cache cols="4" />
<livewire:pulse.slow-queries cols="8" />
<livewire:pulse.exceptions cols="6" />
<livewire:pulse.slow-requests cols="6" />
<livewire:pulse.slow-jobs cols="6" />
<livewire:pulse.slow-outgoing-requests cols="6" />
</x-pulse>