diff --git a/app/Http/Controllers/AuthController.php b/app/Http/Controllers/AuthController.php index 932d264..5ecc0c0 100644 --- a/app/Http/Controllers/AuthController.php +++ b/app/Http/Controllers/AuthController.php @@ -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)); } diff --git a/app/Http/Controllers/LocationController.php b/app/Http/Controllers/LocationController.php new file mode 100644 index 0000000..4cf5dfb --- /dev/null +++ b/app/Http/Controllers/LocationController.php @@ -0,0 +1,22 @@ +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'))); + } +} diff --git a/app/Http/Controllers/WeatherController.php b/app/Http/Controllers/WeatherController.php new file mode 100644 index 0000000..f4ef85c --- /dev/null +++ b/app/Http/Controllers/WeatherController.php @@ -0,0 +1,45 @@ +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); + } + } +} diff --git a/app/Http/Resources/AuthUserResource.php b/app/Http/Resources/AuthUserResource.php index a9f89ef..7c47b92 100644 --- a/app/Http/Resources/AuthUserResource.php +++ b/app/Http/Resources/AuthUserResource.php @@ -18,6 +18,7 @@ class AuthUserResource extends JsonResource 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, + 'locations' => LocationResource::collection($this->whenLoaded('locations')), ]; } } diff --git a/app/Http/Resources/LocationResource.php b/app/Http/Resources/LocationResource.php new file mode 100644 index 0000000..7b93433 --- /dev/null +++ b/app/Http/Resources/LocationResource.php @@ -0,0 +1,24 @@ + + */ + public function toArray(Request $request): array + { + return [ + 'id' => $this->id, + 'user_id' => $this->user_id, + 'latitude' => $this->latitude, + 'longitude' => $this->longitude, + ]; + } +} diff --git a/app/Models/Location.php b/app/Models/Location.php new file mode 100644 index 0000000..7f01802 --- /dev/null +++ b/app/Models/Location.php @@ -0,0 +1,16 @@ +belongsTo(User::class); + } +} diff --git a/app/Models/User.php b/app/Models/User.php index 8dcb2ea..f909022 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -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); diff --git a/app/Models/Weather.php b/app/Models/Weather.php new file mode 100644 index 0000000..8b77441 --- /dev/null +++ b/app/Models/Weather.php @@ -0,0 +1,11 @@ + 0, - 'supports_credentials' => false, + 'supports_credentials' => true, ]; diff --git a/config/weather.php b/config/weather.php new file mode 100644 index 0000000..36f1e66 --- /dev/null +++ b/config/weather.php @@ -0,0 +1,5 @@ + env('OPEN_WEATHER_MAP_API_KEY'), +]; diff --git a/database/migrations/2023_09_17_204012_create_locations_table.php b/database/migrations/2023_09_17_204012_create_locations_table.php new file mode 100644 index 0000000..056051b --- /dev/null +++ b/database/migrations/2023_09_17_204012_create_locations_table.php @@ -0,0 +1,30 @@ +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'); + } +}; diff --git a/resources/js/components/Field.tsx b/resources/js/components/Field.tsx index 8bdfc20..9270dbf 100644 --- a/resources/js/components/Field.tsx +++ b/resources/js/components/Field.tsx @@ -3,14 +3,14 @@ import React, { ReactElement } from "react" -const Field: FC = ({children, type = 'text', ...props}) => { +const Field: FC = ({children, type = 'text', className = '', ...props}) => { return
{children && } -
@@ -27,5 +27,7 @@ interface FieldProps { value: any, placeholder?: string, autoFocus?: boolean, + className?: string, + step?: string, onChange: (event: React.ChangeEvent) => void, } diff --git a/resources/js/components/Header.tsx b/resources/js/components/Header.tsx index 2c334d3..da13c4d 100644 --- a/resources/js/components/Header.tsx +++ b/resources/js/components/Header.tsx @@ -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
Bermite
- {/*{authUser && }*/} + {authUser?.locations && authUser.locations.length > 0 && } {authUser ? - {authUser.name} + {authUser.name} : Connexion diff --git a/resources/js/customPrototypes.ts b/resources/js/customPrototypes.ts index d8eec6f..907eb4b 100644 --- a/resources/js/customPrototypes.ts +++ b/resources/js/customPrototypes.ts @@ -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] +} diff --git a/resources/js/hooks/AuthUser.tsx b/resources/js/hooks/AuthUser.tsx index 69dc6cb..acb8513 100644 --- a/resources/js/hooks/AuthUser.tsx +++ b/resources/js/hooks/AuthUser.tsx @@ -70,4 +70,5 @@ interface User { id: number, name: string, email: string, + locations: {id: number, latitude: number, longitude: number}[], } diff --git a/resources/js/hooks/AxiosTools.tsx b/resources/js/hooks/AxiosTools.tsx index 5ce62da..fe7e929 100644 --- a/resources/js/hooks/AxiosTools.tsx +++ b/resources/js/hooks/AxiosTools.tsx @@ -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) } } diff --git a/resources/js/pages/Auth/Profile.tsx b/resources/js/pages/Auth/Profile.tsx index 64ba946..c0ac463 100644 --- a/resources/js/pages/Auth/Profile.tsx +++ b/resources/js/pages/Auth/Profile.tsx @@ -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 -
-

Profile de l'utilisateur

+
+

Profile de l'utilisateur

-
+
Nom: {authUser?.name}
Email: {authUser?.email}
-
+ {/*
Update name & email
*/} {/*
Change password
*/} {/*
Delete Account
*/} + +

Météo

+ + {authUser?.locations && authUser.locations.length > 0 ? <> +

Emplacements

+
    + {authUser?.locations.map(location =>
  • {location.latitude} - {location.longitude}
  • )} +
+ :
+

Ajouter un emplacement

+
+ setLatitude(Number(event.target.value))}> + Latitude + + setLongitude(Number(event.target.value))}> + Longitude + +
+ +
+
+
} + +
} diff --git a/resources/js/pages/Meteo.tsx b/resources/js/pages/Meteo.tsx deleted file mode 100644 index ae12144..0000000 --- a/resources/js/pages/Meteo.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from "react" -import PageLayout from "../components/PageLayout" - -const Meteo = () => { - - return - Météo - -} - -export default Meteo diff --git a/resources/js/pages/Router.tsx b/resources/js/pages/Router.tsx index b990ed0..1f30351 100644 --- a/resources/js/pages/Router.tsx +++ b/resources/js/pages/Router.tsx @@ -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 = () => { {/*} />*/} } /> } /> - } /> + } /> } /> } /> diff --git a/resources/js/pages/Weather.tsx b/resources/js/pages/Weather.tsx new file mode 100644 index 0000000..9736ac0 --- /dev/null +++ b/resources/js/pages/Weather.tsx @@ -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(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 + + {errorLabel()} + + +
+ {currentWeather?.main.temp.toFixed()} °C + {currentWeather?.weather[0].description} +
+
+
+ {currentWeather && {currentWeather?.weather[0].main}} +
+
+ {currentWeather?.main.temp_max.toFixed()} °C + {currentWeather?.main.temp_min.toFixed()} °C +
+
+
+
+ {weatherDays?.filter(([date]) => date !== (new Date).toSQLDate()) + .map(([date, values]) => )} +
+
+} + +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
+
+ {(new Date(date)).getWeekDay()} + {weatherState?.description} +
+
+ {weatherState?.main + +
+
+ {weatherState?.max.toFixed()} °C + {weatherState?.min.toFixed()} °C +
+
+} diff --git a/resources/js/types.ts b/resources/js/types.ts index b30a43e..ea1ba0c 100644 --- a/resources/js/types.ts +++ b/resources/js/types.ts @@ -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[] +} diff --git a/routes/api.php b/routes/api.php index 1280abe..73cbd3e 100644 --- a/routes/api.php +++ b/routes/api.php @@ -1,7 +1,9 @@ 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'); }); diff --git a/tailwind.config.js b/tailwind.config.js index c36a143..6b681bd 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -14,6 +14,12 @@ export default { fontFamily: { sans: ['Figtree', ...defaultTheme.fontFamily.sans], }, + colors: { + secondary: { + 'ligth':'#c6c5c5', + DEFAULT:'#666666', + } + }, }, },