add Meteo part

This commit is contained in:
Romulus21
2023-09-24 11:19:58 +02:00
parent bfaf82f264
commit 33b2044859
23 changed files with 429 additions and 31 deletions

View File

@@ -128,7 +128,7 @@ class AuthController extends Controller
public function user(Request $request): JsonResponse
{
$user = $request->user();
$user = $request->user()->load('locations');
return response()->json(new AuthUserResource($user));
}

View File

@@ -0,0 +1,22 @@
<?php
namespace App\Http\Controllers;
use App\Http\Resources\AuthUserResource;
use Illuminate\Http\Request;
class LocationController extends Controller
{
public function store(Request $request)
{
$data = $request->validate([
'latitude' => ['required', 'decimal:3,5'],
'longitude' => ['required', 'decimal:3,5'],
]);
$user = $request->user();
$user->locations()->create($data);
return response()->json(new AuthUserResource($user->load('locations')));
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Http\Controllers;
use Exception;
use GuzzleHttp\Client;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class WeatherController extends Controller
{
public function index(Request $request): JsonResponse
{
abort_if($request->user()->locations->count() === 0, 404, 'Coordonnées non renseignées dans le profile.');
$location = $request->user()->locations->first();
$idCity = 6427013;
$apiKey = config('weather.open_weather_map_api_key');
try {
$response = Cache::remember('weather-'.$location->id, 5 * 60, function () use ($location, $apiKey) {
// $url = 'https://api.openweathermap.org/data/2.5/forecast?id='.$idCity.'&appid='.$apiKey.$params;
$params = '&units=metric&lang=fr';
$url = 'https://api.openweathermap.org/data/2.5/forecast?lat='.$location->latitude.'&lon='.$location->longitude.'&appid='.$apiKey.$params;
$client = new Client();
$promise = $client->requestAsync('GET', $url);
$response = $promise->wait();
return json_decode($response->getBody()->getContents());
});
return response()->json($response);
} catch (Exception $e) {
Log::alert('unable to fetch data', [
'ip' => $request->ip(),
'code' => $e->getCode(),
'message' => $e->getMessage(),
]);
return response()->json('unable to fetch data', 500);
}
}
}

View File

@@ -18,6 +18,7 @@ class AuthUserResource extends JsonResource
'id' => $this->id,
'name' => $this->name,
'email' => $this->email,
'locations' => LocationResource::collection($this->whenLoaded('locations')),
];
}
}

View File

@@ -0,0 +1,24 @@
<?php
namespace App\Http\Resources;
use Illuminate\Http\Request;
use Illuminate\Http\Resources\Json\JsonResource;
class LocationResource extends JsonResource
{
/**
* Transform the resource into an array.
*
* @return array<string, mixed>
*/
public function toArray(Request $request): array
{
return [
'id' => $this->id,
'user_id' => $this->user_id,
'latitude' => $this->latitude,
'longitude' => $this->longitude,
];
}
}

16
app/Models/Location.php Normal file
View File

@@ -0,0 +1,16 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Location extends Model
{
protected $guarded = [];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}

View File

@@ -44,6 +44,11 @@ class User extends Authenticatable
'password' => 'hashed',
];
public function locations(): HasMany
{
return $this->hasMany(Location::class);
}
public function rainfalls(): HasMany
{
return $this->hasMany(Rainfall::class);

11
app/Models/Weather.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Weather extends Model
{
use HasFactory;
}

View File

@@ -29,6 +29,6 @@ return [
'max_age' => 0,
'supports_credentials' => false,
'supports_credentials' => true,
];

5
config/weather.php Normal file
View File

@@ -0,0 +1,5 @@
<?php
return [
'open_weather_map_api_key' => env('OPEN_WEATHER_MAP_API_KEY'),
];

View File

@@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('locations', function (Blueprint $table) {
$table->id();
$table->foreignIdFor(\App\Models\User::class)->constrained();
$table->double('longitude');
$table->double('latitude');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('locations');
}
};

View File

@@ -3,14 +3,14 @@ import React, {
ReactElement
} from "react"
const Field: FC<FieldProps> = ({children, type = 'text', ...props}) => {
const Field: FC<FieldProps> = ({children, type = 'text', className = '', ...props}) => {
return <div className="form-control">
{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"
<input className={`${className} w-full mt-2 rounded dark:bg-gray-700`}
type={type}
{...props}/>
<div className={`error-message`} />
@@ -27,5 +27,7 @@ interface FieldProps {
value: any,
placeholder?: string,
autoFocus?: boolean,
className?: string,
step?: string,
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void,
}

View File

@@ -1,24 +1,25 @@
import React from "react"
import {Link} from "react-router-dom"
import {Link, useLocation} from "react-router-dom"
import useAuthUser from "../hooks/AuthUser"
const Header = () => {
const {authUser, logout} = useAuthUser()
const {authUser} = useAuthUser()
const location = useLocation()
return <header className="flex justify-between py-3 px-5 bg-blue-700 text-white text-xl">
<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?.locations && authUser.locations.length > 0 && <nav className="flex gap-2">
<Link to="/pluviometrie" className={location.pathname === '/pluviometrie' ? 'font-bold' : ''}>Pluviométrie</Link>
<Link to="/meteo" className={location.pathname === '/meteo' ? 'font-bold' : ''}>Météo</Link>
</nav>}
{authUser
? <span className="flex gap-2">
<Link to="/profile">{authUser.name}</Link>
<Link to="/profile" className={location.pathname === '/profile' ? 'font-bold' : ''}>{authUser.name}</Link>
</span>
: <span className="flex gap-2">
<Link to="/connexion">Connexion</Link>

View File

@@ -1,7 +1,15 @@
const weekDays = ['lundi', 'mardi', 'mercredi', 'jeudi', 'vendredi', 'samedi', 'dimanche']
interface Date {
getWeekDay(): string,
toSQLDate(): string,
}
Date.prototype.toSQLDate = function (): string {
return (new Date(this)).toISOString().split('T')[0]
}
Date.prototype.getWeekDay = function () {
const dayIndex = this.getDay() === 0 ? 6 : this.getDay() - 1
return weekDays[dayIndex]
}

View File

@@ -70,4 +70,5 @@ interface User {
id: number,
name: string,
email: string,
locations: {id: number, latitude: number, longitude: number}[],
}

View File

@@ -17,7 +17,7 @@ const useAxiosTools = () => {
if (error.response && error.response.status === 422) {
displayFormErrors(error)
} else {
setError(error.response.data.message || error.message)
setError(error.response?.data.message || error.message)
}
}

View File

@@ -1,25 +1,76 @@
import React from "react"
import React, {FormEvent, useState} from "react"
import useAuthUser from "../../hooks/AuthUser"
import PageLayout from "../../components/PageLayout"
import Card from "../../components/Card";
import Field from "../../components/Field";
import useAxiosTools from "../../hooks/AxiosTools";
const Profile = () => {
const {authUser, logout} = useAuthUser()
const {authUser, setAuthUser, logout} = useAuthUser()
const [latitude, setLatitude] = useState(0)
const [longitude, setLongitude] = useState(0)
const {errorCatch, axiosPost} = useAxiosTools()
const submitLocation = async (event: FormEvent) => {
event.preventDefault()
try {
const res = await axiosPost(`/api/locations`, {latitude, longitude})
setAuthUser(res.data)
} catch (e) {
errorCatch(e)
}
}
return <PageLayout>
<div className="flex justify-between">
<h1 className="mb-5 text-lg font-bold">Profile de l&apos;utilisateur</h1>
<div className="m-1 my-5 flex justify-between">
<h1 className="text-lg font-bold">Profile de l&apos;utilisateur</h1>
<div>
<button type="button" onClick={logout} className="btn-primary text-lg font-bold">Se déconnecter</button>
</div>
</div>
<div>
<Card className="mb-5">
<div>Nom: <strong>{authUser?.name}</strong></div>
<div>Email: <strong>{authUser?.email}</strong></div>
</div>
</Card>
{/*<div>Update name & email</div>*/}
{/*<div>Change password</div>*/}
{/*<div>Delete Account</div>*/}
<Card>
<h2>Météo</h2>
{authUser?.locations && authUser.locations.length > 0 ? <>
<h3>Emplacements</h3>
<ul>
{authUser?.locations.map(location => <li key={location.id}>{location.latitude} - {location.longitude}</li>)}
</ul>
</> : <form onSubmit={submitLocation}>
<h3>Ajouter un emplacement</h3>
<div className="flex gap-2">
<Field name="latitude"
type="number"
step="0.0001"
value={latitude}
className="h-10"
onChange={event => setLatitude(Number(event.target.value))}>
Latitude
</Field>
<Field name="longitude"
type="number"
step="0.0001"
value={longitude}
className="h-10"
onChange={event => setLongitude(Number(event.target.value))}>
Longitude
</Field>
<div className="self-end">
<button type="submit" className="btn-primary w-24 h-10">Valider</button>
</div>
</div>
</form>}
</Card>
</PageLayout>
}

View File

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

View File

@@ -2,7 +2,7 @@ import React, {lazy, Suspense} from "react"
import {BrowserRouter, Route, Routes} from "react-router-dom"
import useAuthUser from "../hooks/AuthUser"
import Header from "../components/Header"
import Meteo from "./Meteo"
import Weather from "./Weather"
const ForgotPassword = lazy(() => import('./Auth/ForgotPassword'))
const Home = lazy(() => import('./Home'))
@@ -29,7 +29,7 @@ const Router = () => {
{/*<Route path="/sinscrire" element={<Register />} />*/}
<Route path="/mot-de-passe-oubliee" element={<ForgotPassword />} />
<Route path="/changer-le-mot-de-passe/:token" element={<Reset />} />
<Route path="/meteo" element={<Meteo />} />
<Route path="/meteo" element={<Weather />} />
<Route path="/pluviometrie" element={<Rainfall />} />
<Route path="/pluviometrie/mesures" element={<RainfallIndex />} />
</Routes>

View File

@@ -0,0 +1,146 @@
import React, {FC, useEffect, useState} from "react"
import PageLayout from "../components/PageLayout"
import useAxiosTools from "../hooks/AxiosTools";
import {WeatherValue} from "../types";
import Card from "../components/Card"
const Weather = () => {
const [currentWeather, setCurrentWeather] = useState<WeatherValue|null>(null)
const [weatherDays, setWeatherDays] = useState<[string, WeatherValue[]][]|null>(null)
const {errorLabel, errorCatch, cleanErrors, axiosGet} = useAxiosTools()
useEffect(() => {
(async () => fetchWeather())()
}, [])
const fetchWeather = async () => {
try {
cleanErrors()
const res = await axiosGet(`/api/weather`)
const currentWeather = res.data.list[0]
let weatherDays: [string, WeatherValue[]][] = []
let objectEntries = {index: -1, date: ''}
res.data.list.forEach((item: WeatherValue, index: number) => {
const date = item.dt_txt.split(' ')[0]
if (date === (new Date).toSQLDate()) {
if (currentWeather.main.temp_min > item.main.temp_min) {
currentWeather.main.temp_min = item.main.temp_min
}
if (currentWeather.main.temp_max < item.main.temp_max) {
currentWeather.main.temp_max = item.main.temp_max
}
}
if (date !== objectEntries.date) {
objectEntries = {index: objectEntries.index + 1, date}
}
if (!weatherDays[objectEntries.index]) {
weatherDays[objectEntries.index] = [date, []]
}
weatherDays[objectEntries.index][1] = [...weatherDays[objectEntries.index][1], item]
})
setWeatherDays(weatherDays)
setCurrentWeather(currentWeather)
} catch (e) {
errorCatch(e)
}
}
return <PageLayout>
{errorLabel()}
<Card className="flex justify-between">
<div className="flex flex-col m-2 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>
</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" />}
</div>
<div className="flex gap-1 flex-col">
<span className="text-4xl pt-5">{currentWeather?.main.temp_max.toFixed()} <span className="text-2xl">°C</span></span>
<span className="text-secondary text-2xl mt-2 dark:text-secondary-ligth">{currentWeather?.main.temp_min.toFixed()} °C</span>
</div>
</div>
</Card>
<div className="m-3">
{weatherDays?.filter(([date]) => date !== (new Date).toSQLDate())
.map(([date, values]) => <WeatherCard key={date} date={date} values={values}/>)}
</div>
</PageLayout>
}
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)
useEffect(() => {
const weatherState = {
min: 100,
max: -100,
main: '',
icon: '',
description: '',
}
const result: {[k: string]: number} = {}
values.forEach(value => {
if (value.main.temp_min < weatherState.min) {
weatherState.min = value.main.temp_min
}
if (value.main.temp_max > weatherState.max) {
weatherState.max = value.main.temp_max
}
const tag = value.weather[0].main
if (! result[tag]) {
result[tag] = 0
}
result[tag]++
})
let itemToReturn: {name:string, value: number}|null = null
for (const item in result) {
if (! itemToReturn || itemToReturn.value < result[item]) {
itemToReturn = {name: item, value: result[item]}
}
}
if (itemToReturn && itemToReturn.name) {
const nameToSearch = itemToReturn.name
const value = values.find(item => item.weather[0].main === nameToSearch)
if (value) {
weatherState.main = itemToReturn.name
weatherState.description = value.weather[0].description
weatherState.icon = value.weather[0].icon.replace('n', 'd')
setWeatherState(weatherState)
}
}
}, [])
return <div className="flex gap-5">
<div className="flex flex-col gap-2 flex-1 h-full">
<span className="font-bold text-lg" title={(new Date(date)).toLocaleDateString()}>{(new Date(date)).getWeekDay()}</span>
<span className="text-secondary dark:text-secondary-ligth">{weatherState?.description}</span>
</div>
<div className="flex items-center -mt-1.5">
<img src={`http://openweathermap.org/img/wn/${weatherState?.icon}@2x.png`}
className=""
alt={weatherState?.main + ' ' + weatherState?.icon} width="80px" />
</div>
<div className="flex gap-1 flex-col">
<span className="text-lg">{weatherState?.max.toFixed()} °C</span>
<span className="text-secondary dark:text-secondary-ligth">{weatherState?.min.toFixed()} °C</span>
</div>
</div>
}

View File

@@ -3,3 +3,32 @@ export interface rainfall {
date: string,
value: number,
}
export interface WeatherRequest {
list: WeatherValue[],
}
export interface WeatherValue {
dt_txt: string,
main: {
temp: number,
humidity: number,
pressure: number,
temp_max: number,
temp_min: number,
},
rain?: {
'3h': number,
},
weather: WeatherTime[],
}
export interface WeatherTime {
description: string,
icon: string,
main: 'Rain',
}
export interface WeatherCompilation {
[k: string]: WeatherValue[]
}

View File

@@ -1,7 +1,9 @@
<?php
use App\Http\Controllers\AuthController;
use App\Http\Controllers\LocationController;
use App\Http\Controllers\RainfallController;
use App\Http\Controllers\WeatherController;
use Illuminate\Support\Facades\Route;
/*
@@ -24,8 +26,12 @@ Route::middleware('auth:sanctum')->group(function () {
Route::get('/user', [AuthController::class, 'user'])->name('user');
Route::delete('/logout', [AuthController::class, 'logout'])->name('logout');
Route::post('/locations', [LocationController::class, 'store'])->name('location.store');
Route::get('/rainfalls/last', [RainfallController::class, 'lastRainfalls'])->name('rainfalls.last');
Route::get('/rainfalls/graph', [RainfallController::class, 'graphValue'])->name('rainfalls.graph');
Route::apiResource('rainfalls', RainfallController::class);
Route::get('weather', [WeatherController::class, 'index'])->name('weather.index');
});

View File

@@ -14,6 +14,12 @@ export default {
fontFamily: {
sans: ['Figtree', ...defaultTheme.fontFamily.sans],
},
colors: {
secondary: {
'ligth':'#c6c5c5',
DEFAULT:'#666666',
}
},
},
},