import 'package:flutter/material.dart'; import 'package:http/http.dart' as http; import 'package:flutter_dotenv/flutter_dotenv.dart'; // Import dotenv import 'dart:convert'; import 'dart:io'; import 'ItemMenu.dart'; import '../classes/events.dart'; import '../classes/MyDrawer.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:intl/intl.dart'; import 'package:intl/date_symbol_data_local.dart'; import 'dart:math'; import 'package:geolocator/geolocator.dart'; import '../variable/globals.dart' as globals; import 'package:permission_handler/permission_handler.dart'; import "Camera.dart"; import 'package:camera/camera.dart'; void main() { initializeDateFormatting("fr_FR", null).then((_) => runApp(const MyApp())); } class MyApp extends StatelessWidget { const MyApp({super.key}); @override Widget build(BuildContext context) { return MaterialApp( home: const ListItemMenu(), debugShowCheckedModeBanner: false, ); } } class ListItemMenu extends StatefulWidget { const ListItemMenu({super.key}); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State { Future> postsFuture = getPosts(); List filteredPosts = []; String geographicalZone = ''; String itemName = ''; String itemTags = ''; String query = ''; List> suggestionsGeo = []; 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; final List body = []; var url = Uri.parse("${globals.api}/events"); if (status.isGranted) { print("Location permission granted"); // Get the current position with high accuracy const LocationSettings locationSettings = LocationSettings( accuracy: LocationAccuracy.medium, timeLimit: Duration(seconds: 5), ); Position? position; try { position = await Geolocator.getCurrentPosition( locationSettings: locationSettings); } on LocationServiceDisabledException { // Handle location services disabled print('Location services are disabled.'); position = await Geolocator.getLastKnownPosition(); if (position == null) { print('No last known position available.'); } } catch (e) { // Handle other errors print('Failed to get location: $e'); position = await Geolocator.getLastKnownPosition(); if (position == null) { print('No last known position available.'); } } SharedPreferences prefs = await SharedPreferences.getInstance(); if (position != null) { // Calculate the boundaries double radiusInKm = prefs.getDouble("kilometer") ?? 50.0; double latDistance = radiusInKm / 111.0; double lonDistance = radiusInKm / (111.0 * cos(position.latitude * pi / 180)); double minLat = position.latitude - latDistance; double maxLat = position.latitude + latDistance; double minLon = position.longitude - lonDistance; double maxLon = position.longitude + lonDistance; DateTime currentDatetime = DateTime.now(); url = Uri.parse("${globals.api}/events/search" "?min_lat=$minLat&max_lat=$maxLat" "&min_lon=$minLon&max_lon=$maxLon¤t_datetime=${currentDatetime.toString()}"); } var accessToken = prefs.getString("access_token") ?? ""; if (accessToken.isNotEmpty) { final response = await http.get(url, headers: { "Content-Type": "application/json", HttpHeaders.cookieHeader: "access_token=${accessToken}" }); final List body = json.decode(utf8.decode(response.bodyBytes)); return body.map((e) => Events.fromJson(e)).toList(); } } return body; } String formatDate(String date) { var splitedDate = date.split("-"); var day = splitedDate[0]; var month = splitedDate[1]; var year = splitedDate[2]; return "${year}-${month}-${day}"; } @override void initState() { super.initState(); // Initialize data fetch when the page loads _getCurrentLocation(); } // Get the device's current location Future _getCurrentLocation() async { PermissionStatus status = await Permission.location.status; if (status.isGranted) { print("Location permission granted"); // Get the current position with high accuracy const LocationSettings locationSettings = LocationSettings( accuracy: LocationAccuracy.medium, timeLimit: Duration(seconds: 5)); Position? position; try { position = await Geolocator.getCurrentPosition( locationSettings: locationSettings); } on LocationServiceDisabledException { // Handle location services disabled print('Location services are disabled.'); position = await Geolocator.getLastKnownPosition(); if (position == null) { print('No last known position available.'); } } catch (e) { // Handle other errors print('Failed to get location: $e'); position = await Geolocator.getLastKnownPosition(); if (position == null) { print('No last known position available.'); } } // Reverse geocode: Get city and country from latitude and longitude using Mapbox Search API if (position != null) { _getCityAndCountry(position!.latitude, position!.longitude); } } } // Method to get city and country from latitude and longitude using Mapbox API Future _getCityAndCountry(double latitude, double longitude) async { await dotenv.load(fileName: ".env"); // Load .env file final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? ''; final url = Uri.parse( 'https://api.mapbox.com/geocoding/v5/mapbox.places/$longitude,$latitude.json?access_token=$mapboxAccessToken', ); try { // Send GET request to Mapbox API final response = await http.get(url); // If the request is successful (HTTP status 200) print("status mapbox : ${response.statusCode}"); if (response.statusCode == 200) { // Parse the response body final data = json.decode(response.body); // Extract the city and country from the response final features = data['features']; if (features.isNotEmpty) { String city = _getCityFromFeatures(features); String country = _getCountryFromFeatures(features); print("city : ${city} ${country}"); if (city.isNotEmpty && country.isNotEmpty) { SharedPreferences prefs = await SharedPreferences.getInstance(); prefs.setDouble("city_lat", latitude); prefs.setDouble("city_long", longitude); fetchPostsByLocation(); setState(() { inputGeo.text = "${city}, ${country}"; }); } else { fetchPostsByLocation(); } } else { fetchPostsByLocation(); } } else { fetchPostsByLocation(); throw Exception('Failed to load location data'); } } catch (e) { fetchPostsByLocation(); print("Error getting city and country: $e"); } } // Helper function to extract the city from the Mapbox features array String _getCityFromFeatures(List features) { for (var feature in features) { if (feature['place_type'] != null && feature['place_type'].contains('place')) { return feature['text'] ?? ''; } } return ''; } // Helper function to extract the country from the Mapbox features array String _getCountryFromFeatures(List features) { for (var feature in features) { if (feature['place_type'] != null && feature['place_type'].contains('country')) { return feature['text'] ?? ''; } } return ''; } Future searchSuggestionsGeo(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}&proximity=ip'; final response = await http.get(Uri.parse(url)); if (response.statusCode == 200) { final data = json.decode(response.body); setState(() { suggestionsGeo = (data['features'] as List) .map((feature) => { 'place_name': feature['place_name'], 'geometry': feature[ 'geometry'], // Include geometry for latitude/longitude }) .toList(); if (suggestionsGeo.isNotEmpty) { showArrow = false; showInputSearch = false; showInputTag = false; } }); } else { throw Exception('Failed to load suggestions'); } } Future getUrlForEvents() async { final prefs = await SharedPreferences.getInstance(); final latitude = prefs.getDouble("city_lat") ?? 0.0; final longitude = prefs.getDouble("city_long") ?? 0.0; final radiusInKm = prefs.getDouble("kilometer") ?? 50.0; String endpoint = "events"; String queryParameters = ""; if (latitude != 0.0 && longitude != 0.0) { final latDistance = radiusInKm / 111.0; final lonDistance = radiusInKm / (111.0 * cos(latitude * pi / 180)); final minLat = latitude - latDistance; final maxLat = latitude + latDistance; final minLon = longitude - lonDistance; final maxLon = longitude + lonDistance; endpoint = "events/search"; queryParameters = "min_lat=$minLat&max_lat=$maxLat&min_lon=$minLon&max_lon=$maxLon"; } final currentDate = DateTime.now(); String dateParameter = "current_datetime=${currentDate.toIso8601String()}"; if (startDatepicker.text.isNotEmpty || endDatepicker.text.isNotEmpty) { endpoint = "events/search"; if (startDatepicker.text.isNotEmpty) { final startDate = DateTime.parse(formatDate(startDatepicker.text)); dateParameter = "start_date=${startDate.toIso8601String()}"; } if (endDatepicker.text.isNotEmpty) { final endDate = DateTime.parse(formatDate(endDatepicker.text)); dateParameter += "&end_date=${endDate.toIso8601String()}"; } } if (inputItem.text.isNotEmpty) { queryParameters += "&item=${inputItem.text}"; } if (inputTags.text.isNotEmpty) { queryParameters += "&tags=${inputTags.text}"; } if (queryParameters.isNotEmpty) { queryParameters = "$queryParameters&$dateParameter"; } else { queryParameters = dateParameter; } return Uri.parse("${globals.api}/$endpoint?$queryParameters"); } Future searchSuggestionsByItem(String input) async { SharedPreferences prefs = await SharedPreferences.getInstance(); var accessToken = prefs.getString("access_token") ?? ""; if (accessToken.isNotEmpty) { var url = await getUrlForEvents(); final response = await http.get(url, headers: { "Content-Type": "application/json", HttpHeaders.cookieHeader: "access_token=$accessToken" }); if (response.statusCode == 200) { final data = json.decode(utf8.decode(response.bodyBytes)); setState(() { suggestionsItem = (data as List) .map((feature) => {'name': feature['name']}) .toList(); if (suggestionsItem.isNotEmpty) { showDateFields = false; showArrow = false; } }); print("status code : ${response.statusCode}"); } } } 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") ?? ""; if (accessToken.isNotEmpty) { var url = await getUrlForEvents(); final response = await http.get(url, headers: { "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)); print("results fetch : ${body}"); // Update state after getting the response setState(() { if (body.isNotEmpty) { // If we have results, map them to Events filteredPosts = body .map((e) => Events.fromJson(e as Map)) .toList(); } else { // If no results, clear filteredPosts filteredPosts.clear(); } }); } else { throw Exception('Failed to load posts'); } } } onTapFunctionDatePicker( {required BuildContext context, String position = ""}) async { DateTime dateEvent = DateTime.now(); if (startDatepicker.text.isNotEmpty) { dateEvent = DateTime.parse(formatDate(startDatepicker.text)); } DateTime? pickedDate = await showDatePicker( context: context, firstDate: dateEvent, lastDate: DateTime(2104)); if (pickedDate == null) return; if (position == "start") { startDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedDate); } else if (position == "end") { endDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedDate); } fetchPostsByLocation(); } Padding _buildDateField(String position) { TextEditingController datePicker = startDatepicker; String hintText = "Date de début"; if (position == "end") { datePicker = endDatepicker; hintText = "Date de fin"; } return Padding( padding: const EdgeInsets.all(8.0), //padding: EdgeInsets.symmetric(horizontal: 15), child: TextFormField( controller: datePicker, readOnly: true, decoration: InputDecoration( border: OutlineInputBorder(), suffixIcon: datePicker.text.isEmpty ? null : IconButton( icon: const Icon(Icons.clear), onPressed: () async { setState(() { datePicker.text = ''; }); fetchPostsByLocation(); }, ), hintText: hintText), onTap: () => onTapFunctionDatePicker(context: context, position: position)), ); } Widget _buildSearchField({ required TextEditingController controller, required String labelText, required Function(String) onChanged, required Function() onClear, required List> suggestions, required Function(Map) onSuggestionTap, }) { return Padding( padding: const EdgeInsets.all(8.0), child: Column( children: [ TextField( controller: controller, decoration: InputDecoration( labelText: labelText, border: OutlineInputBorder(), suffixIcon: controller.text.isEmpty ? null : IconButton( icon: const Icon(Icons.clear), onPressed: () => onClear(), ), ), onChanged: onChanged, ), if (suggestions.isNotEmpty) Container( height: 200, decoration: BoxDecoration( border: Border.all(color: Colors.blue), borderRadius: BorderRadius.circular(8), ), child: ListView.builder( shrinkWrap: true, itemCount: suggestions.length, itemBuilder: (context, index) { final suggestion = suggestions[index]; return ListTile( title: Text( suggestion['name'] ?? suggestion['place_name'] ?? ''), onTap: () => onSuggestionTap(suggestion), ); }, ), ), ], ), ); } Future popCamera() async { await availableCameras().then((value) => Navigator.push(context, MaterialPageRoute(builder: (_) => Camera(camera: value.first)))); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: const Text("Item list menu"), backgroundColor: Colors.blue, foregroundColor: Colors.white, ), drawer: MyDrawer(), body: Column( children: [ if (showInputSearch) _buildSearchField( controller: inputItem, labelText: 'Search by item', onChanged: (value) { if (value.isNotEmpty) { setState(() { itemName = value; searchSuggestionsByItem(value); }); } else { setState(() { inputItem.clear(); itemName = ''; suggestionsItem.clear(); showDateFields = true; showArrow = true; }); fetchPostsByLocation(); } }, onClear: () { setState(() { inputItem.clear(); itemName = ''; suggestionsItem.clear(); showDateFields = true; showArrow = true; }); fetchPostsByLocation(); }, suggestions: suggestionsItem, onSuggestionTap: (suggestion) async { setState(() { itemName = suggestion['name']; inputItem.text = itemName; suggestionsItem.clear(); showDateFields = true; showArrow = true; }); await fetchPostsByLocation(); }, ), if ((showDateFields) && (showInputTag)) _buildSearchField( controller: inputTags, labelText: 'Search by tags', onChanged: (value) { if (value.isNotEmpty) { setState(() { itemTags = value; searchSuggestionsByTag(value); }); } else { setState(() { inputTags.clear(); showArrow = true; showInputSearch = true; showInputGeo = true; itemTags = ''; }); fetchPostsByLocation(); } }, onClear: () { setState(() { inputTags.clear(); }); fetchPostsByLocation(); }, suggestions: suggestionsTags, onSuggestionTap: (suggestion) async { setState(() { itemTags = suggestion['name']; inputTags.text = itemTags; suggestionsTags.clear(); showArrow = true; showInputSearch = true; showInputGeo = true; }); await fetchPostsByLocation(); }, ), if ((showDateFields) && (showArrow)) Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Flexible(child: _buildDateField("start")), Flexible(child: _buildDateField("end")) ]), if ((showDateFields) && (showInputGeo)) _buildSearchField( controller: inputGeo, labelText: 'Search by geographical zone', onChanged: (value) async { if (value.isNotEmpty) { setState(() { geographicalZone = value; searchSuggestionsGeo(value); }); } else { final prefs = await SharedPreferences.getInstance(); prefs.remove("city_lat"); prefs.remove("city_long"); setState(() { inputGeo.clear(); geographicalZone = ''; suggestionsGeo.clear(); showArrow = true; showInputSearch = true; showInputTag = true; }); fetchPostsByLocation(); } }, onClear: () async { final prefs = await SharedPreferences.getInstance(); prefs.remove("city_lat"); prefs.remove("city_long"); setState(() { inputGeo.clear(); geographicalZone = ''; suggestionsGeo.clear(); showArrow = true; showInputSearch = true; showInputTag = true; }); fetchPostsByLocation(); }, suggestions: suggestionsGeo, onSuggestionTap: (suggestion) async { final latitude = suggestion['geometry']['coordinates'][1]; final longitude = suggestion['geometry']['coordinates'][0]; setState(() { geographicalZone = suggestion['place_name']; inputGeo.text = geographicalZone; suggestionsGeo.clear(); showArrow = true; showInputSearch = true; showInputTag = true; }); final prefs = await SharedPreferences.getInstance(); prefs.setDouble("city_lat", latitude); prefs.setDouble("city_long", longitude); await fetchPostsByLocation(); }, ), if (showArrow) IconButton( onPressed: () { setState(() { showDateFields = !showDateFields; // Toggle visibility }); }, icon: Icon( showDateFields ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down, color: Colors.blue, ), tooltip: showDateFields ? 'Show Date Fields' : 'Hide Date Fields', ), Expanded( child: FutureBuilder>( future: postsFuture, builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); } else if (snapshot.hasData) { final posts = snapshot.data!; final displayedPosts = filteredPosts.isEmpty ? posts : filteredPosts; return buildPosts(displayedPosts); } else { return const Text("No data available"); } }, ), ), ], ), floatingActionButton: FloatingActionButton( onPressed: popCamera, backgroundColor: Colors.blue, tooltip: 'Recherche', child: const Icon(Icons.photo_camera, color: Colors.white), ), ); } // Function to display fetched data on screen Widget buildPosts(List posts) { print("posts : ${posts}"); print("filteredposts : ${filteredPosts}"); final displayedPosts = filteredPosts; print("results ${displayedPosts}"); // If filteredPosts is empty, show a message saying no data is available if (displayedPosts.isEmpty) { return const Center( child: Text('No events available for this location.', style: TextStyle(fontSize: 18, color: Colors.grey)), ); } return ListView.separated( itemCount: displayedPosts.length, itemBuilder: (context, index) { final post = displayedPosts[index]; final startDate = DateTime.parse(post.startDate!); final date = DateFormat.yMd().format(startDate); final time = DateFormat.Hm().format(startDate); return ListTile( title: Text('${post.name!}'), subtitle: Text('${post.place!}\n${date} ${time}'), onTap: () { Navigator.push( context, MaterialPageRoute(builder: (_) => ItemMenu(title: post.id!)), ); }, ); }, separatorBuilder: (context, index) { return Divider(); }); } }