Compare commits
10 Commits
a0ed432d2b
...
f5bddca70c
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f5bddca70c | ||
|
|
ad65ea2e35 | ||
|
|
64e99676ab | ||
|
|
906db7a908 | ||
|
|
7ba13389d7 | ||
|
|
3bead64695 | ||
|
|
49f0abd08c | ||
|
|
2f2077497d | ||
|
|
2f260555cb | ||
|
|
dc9351dd9a |
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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
850
composer.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -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',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
|
||||
@@ -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
20
lang/en/auth.php
Normal 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
19
lang/en/pagination.php
Normal 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' => '« Previous',
|
||||
'next' => 'Next »',
|
||||
|
||||
];
|
||||
22
lang/en/passwords.php
Normal file
22
lang/en/passwords.php
Normal 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
191
lang/en/validation.php
Normal 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
7
lang/fr/validation.php
Normal 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.'
|
||||
];
|
||||
34
package.json
34
package.json
@@ -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
2865
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
|
||||
@@ -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'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]" />}
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,21 +38,23 @@ const RainfallGraph = () => {
|
||||
}
|
||||
|
||||
return <PageLayout>
|
||||
<div className="flex flex-wrap justify-between gap-2">
|
||||
<LastFiveMesure loadedAt={loadedAt} />
|
||||
<AddRainfall reload={reload} />
|
||||
</div>
|
||||
|
||||
{errorLabel()}
|
||||
<form className="mx-5 mb-2 flex flex-wrap gap-2">
|
||||
<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()})} />
|
||||
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()})} />
|
||||
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}
|
||||
@@ -64,15 +66,19 @@ const RainfallGraph = () => {
|
||||
</select>
|
||||
</div>
|
||||
</form>
|
||||
<div>Total : <strong>{graphData.reduce((result, item) => result += item.value, 0)}</strong> mm</div>
|
||||
</div>
|
||||
|
||||
<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} />*/}
|
||||
</div>
|
||||
|
||||
<div className="flex flex-wrap justify-between gap-2">
|
||||
<AddRainfall reload={reload}/>
|
||||
<LastFiveMesure loadedAt={loadedAt}/>
|
||||
</div>
|
||||
</PageLayout>
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
@@ -68,10 +68,11 @@ 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>
|
||||
@@ -81,8 +82,10 @@ const Weather = () => {
|
||||
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>
|
||||
@@ -97,9 +100,17 @@ export default Weather
|
||||
|
||||
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,
|
||||
@@ -142,9 +153,11 @@ const WeatherCard: FC<{date: string, values: WeatherValue[]}> = ({date, values=
|
||||
}
|
||||
}, [])
|
||||
|
||||
return <div className="flex gap-5">
|
||||
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-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">
|
||||
@@ -158,4 +171,33 @@ const WeatherCard: FC<{date: string, values: WeatherValue[]}> = ({date, values=
|
||||
<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[],
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ export interface WeatherRequest {
|
||||
}
|
||||
|
||||
export interface WeatherValue {
|
||||
dt: number,
|
||||
dt_txt: string,
|
||||
main: {
|
||||
temp: number,
|
||||
|
||||
19
resources/views/vendor/pulse/dashboard.blade.php
vendored
Normal file
19
resources/views/vendor/pulse/dashboard.blade.php
vendored
Normal 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>
|
||||
Reference in New Issue
Block a user