diff --git a/covas_mobile/lib/pages/EditEvent.dart b/covas_mobile/lib/pages/EditEvent.dart index fedc2bb..3f95af3 100644 --- a/covas_mobile/lib/pages/EditEvent.dart +++ b/covas_mobile/lib/pages/EditEvent.dart @@ -180,24 +180,23 @@ class _EditEventState extends State if (accessToken.isNotEmpty) { try { await dotenv.load(); - final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? ''; - print("place non encoded : ${place}"); - final url = - 'https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${mapboxAccessToken}&types=poi,address,place'; - var encoded = Uri.encodeFull(url); - print("encoded : ${encoded}"); - final response = await http.get(Uri.parse(encoded)); + final ApiTokenGoogle = dotenv.env['PLACE_API_KEY'] ?? ''; + // Searchbox API for geocoding the place (No session token) + final searchboxUrl = Uri.parse( + 'https://maps.googleapis.com/maps/api/place/textsearch/json?query=${place}&key=${ApiTokenGoogle}'); - if (response.statusCode == 200) { - final data = json.decode(response.body); + // Perform the request + final searchboxResponse = await http.get(searchboxUrl); + + if (searchboxResponse.statusCode == 200) { + final data = json.decode(searchboxResponse.body); print("data : ${data}"); - if (data['features'].isNotEmpty) { - place = data['features'][0]['place_name']; - final coordinates = - data['features'][0]['geometry']['coordinates']; - final longitude = coordinates[0]; // Longitude - final latitude = coordinates[1]; // Latitude + if (data['results'].isNotEmpty) { + place = data['results'][0]['formatted_address']; + final coordinates = data['results'][0]['geometry']['location']; + final longitude = coordinates["lng"]; // Longitude + final latitude = coordinates["lat"]; // Latitude var urlGet = Uri.parse( "${globals.api}/events/search?item=${name}&date_event=${startDate}&min_lat=$latitude&max_lat=$latitude" "&min_lon=$longitude&max_lon=$longitude"); diff --git a/covas_mobile/lib/pages/ListItemMenu.dart b/covas_mobile/lib/pages/ListItemMenu.dart index 3614d9c..7f42243 100644 --- a/covas_mobile/lib/pages/ListItemMenu.dart +++ b/covas_mobile/lib/pages/ListItemMenu.dart @@ -14,6 +14,7 @@ import '../variable/globals.dart' as globals; import 'package:permission_handler/permission_handler.dart'; import "Camera.dart"; import 'package:camera/camera.dart'; +import 'package:textfield_tags/textfield_tags.dart'; void main() { initializeDateFormatting("fr_FR", null).then((_) => runApp(const MyApp())); @@ -43,17 +44,22 @@ class _MyHomePageState extends State { List filteredPosts = []; String geographicalZone = ''; String itemName = ''; + String itemTags = ''; String query = ''; List> suggestions = []; List> suggestionsItem = []; + List> suggestionsTags = []; TextEditingController inputGeo = TextEditingController(); TextEditingController startDatepicker = TextEditingController(); TextEditingController endDatepicker = TextEditingController(); TextEditingController inputItem = TextEditingController(); + TextEditingController inputTags = TextEditingController(); bool showDateFields = false; // State to toggle date fields bool showArrow = true; bool showInputSearch = true; + bool showInputGeo = true; + bool showInputTag = true; // Fetching events from API static Future> getPosts() async { PermissionStatus status = await Permission.location.status; @@ -264,6 +270,7 @@ class _MyHomePageState extends State { if (suggestions.isNotEmpty) { showArrow = false; showInputSearch = false; + showInputTag = false; } }); } else { @@ -318,6 +325,10 @@ class _MyHomePageState extends State { stringParameter = stringParameter + "&item=${inputItem.text}"; endpoint = "events/search"; } + if (inputTags.text.isNotEmpty) { + stringParameter = stringParameter + "&tags=${inputTags.text}"; + endpoint = "events/search"; + } if (stringParameter.isNotEmpty) { stringParameter = "$stringParameter&$dateParameter"; } else { @@ -353,6 +364,36 @@ class _MyHomePageState extends State { } } + Future searchSuggestionsByTag(String input) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + var accessToken = prefs.getString("access_token") ?? ""; + + if (accessToken.isNotEmpty) { + var url = Uri.parse("${globals.api}/tags?name=${input}"); + final response = await http.get(url, headers: { + "Content-Type": "application/json", + HttpHeaders.cookieHeader: "access_token=$accessToken" + }); + print("status code tags : ${response.statusCode}"); + + if (response.statusCode == 200) { + final data = json.decode(utf8.decode(response.bodyBytes)); + print("tags ${data}"); + setState(() { + suggestionsTags = (data as List) + .map((feature) => {'name': feature['name']}) + .toList(); + print("suggesttion tag : ${suggestionsTags}"); + if (suggestionsTags.isNotEmpty) { + showInputGeo = false; + showInputSearch = false; + showArrow = false; + } + }); + } + } + } + Future fetchPostsByLocation() async { SharedPreferences prefs = await SharedPreferences.getInstance(); var accessToken = prefs.getString("access_token") ?? ""; @@ -363,6 +404,7 @@ class _MyHomePageState extends State { "Content-Type": "application/json", HttpHeaders.cookieHeader: "access_token=$accessToken" }); + print("status code : ${response.statusCode}"); if (response.statusCode == 200) { final List body = json.decode(utf8.decode(response.bodyBytes)); @@ -464,6 +506,7 @@ class _MyHomePageState extends State { showInputSearch = true; // Optionally clear suggestions /// Clear the filtered posts + showInputTag = true; }); fetchPostsByLocation(); }, @@ -485,6 +528,7 @@ class _MyHomePageState extends State { suggestions.clear(); // Optionally clear suggestions showArrow = true; showInputSearch = true; + showInputTag = true; /// Clear the filted posts }); @@ -517,6 +561,7 @@ class _MyHomePageState extends State { suggestions.clear(); showArrow = true; showInputSearch = true; + showInputTag = true; }); SharedPreferences prefs = await SharedPreferences.getInstance(); @@ -533,6 +578,80 @@ class _MyHomePageState extends State { ); } + Padding _buildTagsField() { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Column( + children: [ + TextField( + controller: inputTags, + decoration: InputDecoration( + labelText: 'Search by tags', + border: OutlineInputBorder(), + suffixIcon: inputTags.text.isEmpty + ? null + : IconButton( + icon: const Icon(Icons.clear), + onPressed: () { + setState(() { + inputTags.clear(); + }); + fetchPostsByLocation(); + }, + ), + ), + onChanged: (value) { + if (value.isNotEmpty) { + setState(() { + itemTags = value; + searchSuggestionsByTag(value); + }); + } else { + setState(() { + inputTags.clear(); + showArrow = true; + showInputSearch = true; + showInputGeo = true; // Optionally clear suggestions + itemTags = ''; // Clear the text field + + /// Clear the filted posts + }); + fetchPostsByLocation(); + } + }), + if (suggestionsTags.isNotEmpty) + Container( + height: 200, + decoration: BoxDecoration( + border: Border.all(color: Colors.blue), + borderRadius: BorderRadius.circular(8), + ), + child: ListView.builder( + shrinkWrap: true, + itemCount: suggestionsTags.length, + itemBuilder: (context, index) { + return ListTile( + title: Text(suggestionsTags[index]['name']), + onTap: () async { + setState(() { + itemTags = suggestionsTags[index]['name']; + inputTags.text = itemTags; + suggestionsTags.clear(); + showArrow = true; + showInputSearch = true; + showInputGeo = true; + }); + await fetchPostsByLocation(); + }, + ); + }, + ), + ), + ], + ), + ); + } + Padding _buildItemZoneSearchField() { return Padding( padding: const EdgeInsets.all(8.0), @@ -567,11 +686,11 @@ class _MyHomePageState extends State { }); } else { setState(() { + showDateFields = true; + showArrow = true; // Optionally clear suggestions inputItem.clear(); // Clear the text field itemName = ''; // Reset the geographical zone state suggestionsItem.clear(); - showDateFields = true; - showArrow = true; // Optionally clear suggestions /// Clear the filted posts }); @@ -626,6 +745,7 @@ class _MyHomePageState extends State { body: Column( children: [ if (showInputSearch) _buildItemZoneSearchField(), + if ((showDateFields) && (showInputTag)) _buildTagsField(), if ((showDateFields) && (showArrow)) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -633,7 +753,8 @@ class _MyHomePageState extends State { Flexible(child: _buildDateField("start")), Flexible(child: _buildDateField("end")) ]), - if (showDateFields) _buildGeographicalZoneSearchField(), + if ((showDateFields) && (showInputGeo)) + _buildGeographicalZoneSearchField(), if (showArrow) IconButton( onPressed: () { diff --git a/covas_mobile/lib/pages/UpdateEventImage.dart b/covas_mobile/lib/pages/UpdateEventImage.dart index bfadf0d..83a1c85 100644 --- a/covas_mobile/lib/pages/UpdateEventImage.dart +++ b/covas_mobile/lib/pages/UpdateEventImage.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:http/http.dart' as http; +import 'package:uuid/uuid.dart'; import 'package:intl/intl.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:textfield_tags/textfield_tags.dart'; @@ -138,6 +139,7 @@ class _UpdateeventImageState extends State } Future _updateEvent(BuildContext context) async { + // Gather inputs var name = inputName.text; var place = inputGeo.text; var description = inputDesc.text; @@ -152,177 +154,132 @@ class _UpdateeventImageState extends State var startDate = "${startDateFormat}T${startTimepicker.text.replaceAll('-', ':')}"; var endDate = "${endDateFormat}T${endTimepicker.text.replaceAll('-', ':')}"; - if (startDateCompare.isAfter(dateNow)) { - SharedPreferences prefs = await SharedPreferences.getInstance(); - var accessToken = prefs.getString("access_token") ?? ""; - if (accessToken.isNotEmpty) { - try { - await dotenv.load(); - final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? ''; - print("place non encoded : ${place}"); - final url = - 'https://api.mapbox.com/geocoding/v5/mapbox.places/${place}.json?access_token=${mapboxAccessToken}&types=poi,address,place'; - var encoded = Uri.encodeFull(url); - print("encoded : ${encoded}"); - final response = await http.get(Uri.parse(encoded)); - - if (response.statusCode == 200) { - final data = json.decode(response.body); - print("data : ${data}"); - - if (data['features'].isNotEmpty) { - place = data['features'][0]['place_name']; - final coordinates = - data['features'][0]['geometry']['coordinates']; - final longitude = coordinates[0]; // Longitude - final latitude = coordinates[1]; // Latitude - var urlGet = Uri.parse( - "${globals.api}/events/search?item=${name}&date_event=${startDate}"); - - var responseGet = await http.get(urlGet, headers: { - HttpHeaders.cookieHeader: 'access_token=${accessToken}' - }); - if (responseGet.statusCode == 200) { - var events = jsonDecode(utf8.decode(responseGet.bodyBytes)); - print("reponse http : ${events.length}"); - if (events.length == 0) { - urlGet = Uri.parse( - "${globals.api}/events/search?min_lat=$latitude&max_lat=$latitude" - "&min_lon=$longitude&max_lon=$longitude&date_event=${startDate}"); - responseGet = await http.get(urlGet, headers: { - HttpHeaders.cookieHeader: 'access_token=${accessToken}' - }); - if (responseGet.statusCode == 200) { - events = jsonDecode(utf8.decode(responseGet.bodyBytes)); - print("reponse http : ${events.length}"); - if (events.length != 0) { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => - ItemMenu(title: events[0]["id"]))); - return; - } - } - } else { - Navigator.push( - context, - MaterialPageRoute( - builder: (_) => ItemMenu(title: events[0]["id"]))); - return; - } - } - - final params = { - 'expiration': '15552000', - 'key': dotenv.env["IMGBB_API_KEY"], - }; - print("Post Img"); - final urlPost = Uri.parse('https://api.imgbb.com/1/upload') - .replace(queryParameters: params); - File image = File(widget.imagePath); - Uint8List _bytes = await image.readAsBytes(); - String _base64String = base64.encode(_bytes); - - final req = http.MultipartRequest('POST', urlPost) - ..fields['image'] = _base64String; - - final stream = await req.send(); - final res = await http.Response.fromStream(stream); - - final status = res.statusCode; - print("code status imgbb ${status}"); - if (status == 200) { - var body = json.decode(utf8.decode(res.bodyBytes)); - String imgUrl = body["data"]["url"]; - - //String credentials = "${pseudo}:${password}"; - //Codec stringToBase64 = utf8.fuse(base64); - //String encoded = stringToBase64.encode(credentials); - var urlPut = Uri.parse("${globals.api}/events"); - var responsePut = await http.put(urlPut, - headers: { - HttpHeaders.cookieHeader: 'access_token=${accessToken}', - HttpHeaders.acceptHeader: - 'application/json, text/plain, */*', - HttpHeaders.contentTypeHeader: 'application/json' - }, - body: jsonEncode({ - 'name': name, - 'place': place, - 'start_date': startDate, - 'end_date': endDate, - 'organizers': organizers, - 'latitude': latitude, - 'longitude': longitude, - 'description': description, - "imgUrl": imgUrl, - "tags": tags - })); - print(responsePut.statusCode); - if ((responsePut.statusCode == 200) || - (responsePut.statusCode == 201)) { - showEventDialog(context, "Evenement ${name} ajoute"); - } else { - var text = ""; - switch (responsePut.statusCode) { - case 400: - { - text = "Requête mal construite"; - } - break; - case 406: - { - text = "Mot de passe incorrect"; - } - break; - case 404: - { - text = "Utilisateur inconnu"; - } - break; - case 403: - { - text = "Utilisateur desactive"; - } - break; - case 410: - { - text = "Token invalide"; - } - break; - case 500: - { - text = "Probleme interne du serveur"; - } - break; - default: - { - text = "Probleme d'authentification inconnu"; - } - break; - } - showErrorDialog(context, text); - } - } else { - print("imgbb error : ${status}"); - } - } else { - showErrorDialog(context, "Aucune donnée geographique"); - } - } else { - showErrorDialog(context, "Mapbox non accessible"); - } - } catch (e) { - showErrorDialog(context, "${e}"); - } - } else { - showErrorDialog(context, "Champ vide"); - } - } else { + if (!startDateCompare.isAfter(dateNow)) { showErrorDialog(context, "Evenement non futur"); + return; } + + SharedPreferences prefs = await SharedPreferences.getInstance(); + var accessToken = prefs.getString("access_token") ?? ""; + + if (accessToken.isEmpty) { + showErrorDialog(context, "Token d'accès manquant"); + return; + } + + try { + await dotenv.load(); + final ApiTokenGoogle = dotenv.env['PLACE_API_KEY'] ?? ''; + // Searchbox API for geocoding the place (No session token) + final searchboxUrl = Uri.parse( + 'https://maps.googleapis.com/maps/api/place/textsearch/json?query=${place}&key=${ApiTokenGoogle}'); + + // Perform the request + final searchboxResponse = await http.get(searchboxUrl); + + if (searchboxResponse.statusCode != 200) { + showErrorDialog(context, "Erreur lors de la géocodage avec Searchbox"); + return; + } + + final searchboxData = json.decode(searchboxResponse.body); + if (searchboxData['results'].isEmpty) { + showErrorDialog(context, "Lieu introuvable"); + return; + } + + // Extract place details from the searchbox response + final firstFeature = searchboxData['results'][0]; + place = firstFeature["formatted_address"]; + final coordinates = firstFeature['geometry']['location']; + final longitude = coordinates["lng"]; + final latitude = coordinates["lat"]; + + // Check if a similar event exists + final eventsUrl = Uri.parse( + "${globals.api}/events/search?item=$name&date_event=$startDate"); + final eventsResponse = await http.get(eventsUrl, headers: { + HttpHeaders.cookieHeader: 'access_token=$accessToken', + }); + + if (eventsResponse.statusCode == 200) { + final events = json.decode(utf8.decode(eventsResponse.bodyBytes)); + if (events.isNotEmpty) { + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => ItemMenu(title: events[0]["id"]), + ), + ); + return; + } + } + + // Upload image to imgbb + final imgbbUrl = Uri.parse( + 'https://api.imgbb.com/1/upload?expiration=15552000&key=${dotenv.env["IMGBB_API_KEY"]}'); + File image = File(widget.imagePath); + Uint8List imageBytes = await image.readAsBytes(); + String base64Image = base64.encode(imageBytes); + + final imgbbRequest = http.MultipartRequest('POST', imgbbUrl) + ..fields['image'] = base64Image; + final imgbbResponse = + await http.Response.fromStream(await imgbbRequest.send()); + + if (imgbbResponse.statusCode != 200) { + showErrorDialog(context, "Erreur lors de l'upload d'image"); + return; + } + + final imgbbData = json.decode(imgbbResponse.body); + final imgUrl = imgbbData['data']['url']; + + // Create or update the event + final eventUrl = Uri.parse("${globals.api}/events"); + final eventResponse = await http.put( + eventUrl, + headers: { + HttpHeaders.cookieHeader: 'access_token=$accessToken', + HttpHeaders.acceptHeader: 'application/json, text/plain, */*', + HttpHeaders.contentTypeHeader: 'application/json', + }, + body: jsonEncode({ + 'name': name, + 'place': place, + 'start_date': startDate, + 'end_date': endDate, + 'organizers': organizers, + 'latitude': latitude, + 'longitude': longitude, + 'description': description, + 'imgUrl': imgUrl, + 'tags': tags, + }), + ); + + if (eventResponse.statusCode == 200 || eventResponse.statusCode == 201) { + showEventDialog(context, "Événement $name ajouté"); + } else { + handleHttpError(eventResponse.statusCode, context); + } + } catch (e) { + showErrorDialog(context, "Erreur: ${e.toString()}"); + } + } + +// Utility function to handle HTTP errors + void handleHttpError(int statusCode, BuildContext context) { + final errorMessages = { + 400: "Requête mal construite", + 403: "Utilisateur désactivé", + 404: "Utilisateur inconnu", + 406: "Mot de passe incorrect", + 410: "Token invalide", + 500: "Problème interne du serveur", + }; + showErrorDialog(context, errorMessages[statusCode] ?? "Erreur inconnue"); } void start() async { @@ -360,23 +317,27 @@ class _UpdateeventImageState extends State Future searchSuggestions(String input) async { await dotenv.load(fileName: ".env"); // Load .env file - final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? ''; - final url = - 'https://api.mapbox.com/geocoding/v5/mapbox.places/${input}.json?access_token=${mapboxAccessToken}&types=poi,address,place'; - var encoded = Uri.encodeFull(url); - final response = await http.get(Uri.parse(encoded)); - print("response code suggesttion : ${response.statusCode}"); + final ApiTokenGoogle = dotenv.env['PLACE_API_KEY'] ?? ''; + + // Define the Searchbox API URL + final searchboxUrl = Uri.parse( + 'https://maps.googleapis.com/maps/api/place/textsearch/json?query=${input}&key=${ApiTokenGoogle}'); + + // Perform the request + final response = await http.get(searchboxUrl); + print("response code suggestion: ${response.statusCode}"); if (response.statusCode == 200) { final data = json.decode(response.body); - print("data suggestion : ${data}"); + print("data suggestion: ${data}"); + setState(() { - suggestions = (data['features'] as List) + // Map the results to extract name and full_address + suggestions = (data['results'] as List) .map((feature) => { - 'place_name': feature['place_name'], - 'text': feature['text'], - 'geometry': feature[ - 'geometry'], // Include geometry for latitude/longitude + 'name': feature['name'], + 'formatted_address': feature[ + 'formatted_address'] // Adjusted to match the data structure }) .toList(); }); @@ -426,17 +387,14 @@ class _UpdateeventImageState extends State itemCount: suggestions.length, itemBuilder: (context, index) { return ListTile( - title: Text(suggestions[index]['text']), - subtitle: Text(suggestions[index]['place_name']), + title: Text(suggestions[index]['name']), + subtitle: Text(suggestions[index]['formatted_address']), onTap: () async { print("suggestion tapped : ${suggestions[index]}"); - final latitude = - suggestions[index]['geometry']['coordinates'][1]; - final longitude = - suggestions[index]['geometry']['coordinates'][0]; setState(() { - geographicalZone = suggestions[index]['text']; + geographicalZone = + suggestions[index]['formatted_address']; inputGeo.text = geographicalZone; suggestions.clear(); });