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'; import '../classes/MyDrawer.dart'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; import 'ItemMenu.dart'; import '../classes/alert.dart'; import '../classes/eventAdded.dart'; import '../variable/globals.dart' as globals; void main() { runApp(MyApp()); } class MyApp extends StatelessWidget { Map events = {}; String imagePath = ""; @override Widget build(BuildContext context) { return MaterialApp( debugShowCheckedModeBanner: false, home: UpdateeventImage(events: events, imagePath: imagePath), ); } } class UpdateeventImage extends StatefulWidget { const UpdateeventImage( {Key? key, required this.events, required this.imagePath}) : super(key: key); final Map events; final String imagePath; @override _UpdateeventImageState createState() => _UpdateeventImageState(); } class _UpdateeventImageState extends State with ShowAlertDialog, ShowEventDialog { TextEditingController inputName = TextEditingController(); TextEditingController inputDate = TextEditingController(); TextEditingController inputDesc = TextEditingController(); TextEditingController inputGeo = TextEditingController(); TextEditingController startDatepicker = TextEditingController(); TextEditingController startTimepicker = TextEditingController(); TextEditingController endDatepicker = TextEditingController(); TextEditingController endTimepicker = TextEditingController(); final _stringTagController = StringTagController(); List> suggestions = []; String geographicalZone = ""; List initialTags = []; final _stringOrgaController = StringTagController(); List initialOrga = []; onTapFunctionDatePicker( {required BuildContext context, required String position}) async { String date = "start_date"; if (position == "end") { date = "end_date"; } DateTime dateEvent; if (widget.events[date].toString().isEmpty) { dateEvent = DateTime.now(); } else { dateEvent = DateTime.parse(widget.events[date]); } DateTime? pickedDate = await showDatePicker( context: context, firstDate: dateEvent, initialDate: dateEvent, lastDate: DateTime(2104)); if (pickedDate == null) return; if (position == "start") { startDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedDate); } if (position == "end") { endDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedDate); } } onTapFunctionTimePicker( {required BuildContext context, required String position}) async { String date = "start_date"; if (position == "end") { date = "end_date"; } TimeOfDay timeEvent; if (widget.events[date].toString().isEmpty) { timeEvent = TimeOfDay.now(); } else { timeEvent = TimeOfDay.fromDateTime(DateTime.parse(widget.events[date])); } TimeOfDay? pickedDate = await showTimePicker(context: context, initialTime: timeEvent); if (pickedDate == null) return; if (position == "start") { startTimepicker.text = pickedDate.format(context); } if (position == "end") { endTimepicker.text = pickedDate.format(context); } } convertNulltoEmptyString(var check) { if (check == null) { return ""; } return check; } convertNulltoArray(List check) { if (check == null) { return []; } return check; } String formatDate(String date) { var splitedDate = date.split("-"); var day = splitedDate[0]; var month = splitedDate[1]; var year = splitedDate[2]; return "${year}-${month}-${day}"; } Future _updateEvent(BuildContext context) async { // Gather inputs var name = inputName.text; var place = inputGeo.text; var description = inputDesc.text; List tags = List.from(_stringTagController.getTags as List); List organizers = List.from(_stringOrgaController.getTags as List); var startDateFormat = formatDate(startDatepicker.text); DateTime startDateCompare = DateTime.parse(startDateFormat); DateTime dateNow = DateTime.now(); var endDateFormat = formatDate(endDatepicker.text); var startDate = "${startDateFormat}T${startTimepicker.text.replaceAll('-', ':')}"; var endDate = "${endDateFormat}T${endTimepicker.text.replaceAll('-', ':')}"; if (!startDateCompare.isAfter(dateNow)) { showAlertDialog(context, "Erreur evenement", "Evenement non futur"); return; } SharedPreferences prefs = await SharedPreferences.getInstance(); var accessToken = prefs.getString("access_token") ?? ""; if (accessToken.isEmpty) { showAlertDialog(context, "Erreur token", "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) { showAlertDialog(context, "Erreur map", "Erreur lors de la géocodage avec Searchbox"); return; } final searchboxData = json.decode(searchboxResponse.body); if (searchboxData['results'].isEmpty) { showAlertDialog(context, "Erreur", "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) { showAlertDialog( context, "Erreur serveur", "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) { showAlertDialog(context, "Erreur", "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", }; showAlertDialog(context, "Erreur serveur", errorMessages[statusCode] ?? "Erreur inconnue"); } void start() async { print("events : ${widget.events}"); inputName.text = convertNulltoEmptyString(widget.events["name"]); inputGeo.text = convertNulltoEmptyString(widget.events["place"]); inputDesc.text = convertNulltoEmptyString(widget.events["description"]); if (widget.events["start_date"].toString().isNotEmpty) { DateTime pickedStartDate = DateTime.parse(convertNulltoEmptyString(widget.events["start_date"])); startDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedStartDate); startTimepicker.text = DateFormat("HH-mm").format(pickedStartDate); } if (widget.events["end_date"].toString().isNotEmpty) { DateTime pickedEndDate = DateTime.parse(convertNulltoEmptyString(widget.events["end_date"])); endDatepicker.text = DateFormat("dd-MM-yyyy").format(pickedEndDate); endTimepicker.text = DateFormat("HH-mm").format(pickedEndDate); } initialTags = List.from(widget.events['tags'] as List); initialOrga = List.from(widget.events['organizers'] as List); } @override void initState() { start(); super.initState(); } final _formKey = GlobalKey(); String? _validateField(String? value) { return value!.isEmpty ? 'Champ requis' : null; } Future searchSuggestions(String input) async { await dotenv.load(fileName: ".env"); // Load .env file 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}"); setState(() { // Map the results to extract name and full_address suggestions = (data['results'] as List) .map((feature) => { 'name': feature['name'], 'formatted_address': feature[ 'formatted_address'] // Adjusted to match the data structure }) .toList(); }); } else { throw Exception('Failed to load suggestions'); } } Padding _buildGeographicalZoneSearchField() { return Padding( padding: const EdgeInsets.only(left: 15.0, right: 15.0, top: 15, bottom: 0), child: Column( children: [ TextField( controller: inputGeo, decoration: InputDecoration( labelText: 'Lieu', border: OutlineInputBorder(), suffixIcon: IconButton( icon: const Icon(Icons.clear), onPressed: () { setState(() { inputGeo.clear(); // Clear the text field geographicalZone = ''; // Reset the geographical zone state suggestions.clear(); // Optionally clear suggestions }); }, ), ), onChanged: (value) { setState(() { geographicalZone = value; searchSuggestions(value); }); }, ), 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) { return ListTile( title: Text(suggestions[index]['name']), subtitle: Text(suggestions[index]['formatted_address']), onTap: () async { print("suggestion tapped : ${suggestions[index]}"); setState(() { geographicalZone = suggestions[index]['formatted_address']; inputGeo.text = geographicalZone; suggestions.clear(); }); }, ); }, ), ), ], ), ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( title: Text("Add or Update a event"), backgroundColor: Colors.blue, foregroundColor: Colors.white, ), drawer: MyDrawer(), body: Form( key: _formKey, child: SingleChildScrollView( child: Column( children: [ Padding( padding: const EdgeInsets.only(top: 60.0), child: Center( child: Container( width: 200, height: 150, decoration: BoxDecoration( borderRadius: BorderRadius.circular(100.0)), child: Image.file(File(widget.imagePath))), ), ), Padding( //padding: const EdgeInsets.only(left:15.0,right: 15.0,top:0,bottom: 0), padding: EdgeInsets.symmetric(horizontal: 15), child: TextFormField( controller: inputName, validator: (value) => _validateField(value), decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Nom', hintText: 'Modifier le nom de l\'évènement'), ), ), _buildGeographicalZoneSearchField(), Padding( padding: const EdgeInsets.only( left: 15.0, right: 15.0, top: 15, bottom: 0), //padding: EdgeInsets.symmetric(horizontal: 15), child: TextFormField( controller: startDatepicker, readOnly: true, validator: (value) => _validateField(value), decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Date de debut', hintText: 'Cliquez ici pour selectionner une date'), onTap: () => onTapFunctionDatePicker( context: context, position: "start")), ), Padding( padding: const EdgeInsets.only( left: 15.0, right: 15.0, top: 15, bottom: 0), //padding: EdgeInsets.symmetric(horizontal: 15), child: TextFormField( controller: startTimepicker, readOnly: true, validator: (value) => _validateField(value), decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Heure de debut', hintText: 'Cliquez ici pour selectionner une heure'), onTap: () => onTapFunctionTimePicker( context: context, position: "start")), ), Padding( padding: const EdgeInsets.only( left: 15.0, right: 15.0, top: 15, bottom: 0), //padding: EdgeInsets.symmetric(horizontal: 15), child: TextFormField( controller: endDatepicker, readOnly: true, validator: (value) => _validateField(value), decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Date de fin', hintText: 'Cliquez ici pour selectionner une date'), onTap: () => onTapFunctionDatePicker( context: context, position: "end")), ), Padding( padding: const EdgeInsets.only( left: 15.0, right: 15.0, top: 15, bottom: 0), //padding: EdgeInsets.symmetric(horizontal: 15), child: TextFormField( controller: endTimepicker, readOnly: true, validator: (value) => _validateField(value), decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Heure de fin', hintText: 'Cliquez ici pour selectionner une heure'), onTap: () => onTapFunctionTimePicker( context: context, position: "end")), ), TextFieldTags( textfieldTagsController: _stringTagController, initialTags: initialTags, textSeparators: const [' ', ','], validator: (String tag) { if (_stringTagController.getTags!.contains(tag)) { return 'Tu as deja rentre ce tag'; } return null; }, inputFieldBuilder: (context, inputFieldValues) { return Padding( padding: const EdgeInsets.only( left: 15.0, right: 15.0, top: 15, bottom: 0), child: TextField( controller: inputFieldValues.textEditingController, focusNode: inputFieldValues.focusNode, onChanged: inputFieldValues.onTagChanged, onSubmitted: inputFieldValues.onTagSubmitted, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Tags', hintText: inputFieldValues.tags.isNotEmpty ? '' : "Enter tag...", errorText: inputFieldValues.error, prefixIcon: inputFieldValues.tags.isNotEmpty ? SingleChildScrollView( controller: inputFieldValues.tagScrollController, scrollDirection: Axis.vertical, child: Padding( padding: const EdgeInsets.only( top: 8, bottom: 8, left: 8, ), child: Wrap( runSpacing: 4.0, spacing: 4.0, children: inputFieldValues.tags .map((String tag) { return Container( decoration: const BoxDecoration( borderRadius: BorderRadius.all( Radius.circular(20.0), ), color: Colors.blue, ), margin: const EdgeInsets.symmetric( horizontal: 5.0), padding: const EdgeInsets.symmetric( horizontal: 10.0, vertical: 5.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ InkWell( child: Text( '$tag', style: const TextStyle( color: Colors.white), ), onTap: () { //print("$tag selected"); }, ), const SizedBox(width: 4.0), InkWell( child: const Icon( Icons.cancel, size: 14.0, color: Color.fromARGB( 255, 233, 233, 233), ), onTap: () { inputFieldValues .onTagRemoved(tag); }, ) ], ), ); }).toList()), ), ) : null, ), ), ); }), TextFieldTags( textfieldTagsController: _stringOrgaController, initialTags: initialOrga, textSeparators: const [','], validator: (String tag) { if (_stringOrgaController.getTags!.contains(tag)) { return 'Cet organisateur est déjà rentré'; } return null; }, inputFieldBuilder: (context, inputFieldValues) { return Padding( padding: const EdgeInsets.only( left: 15.0, right: 15.0, top: 15, bottom: 0), child: TextField( controller: inputFieldValues.textEditingController, focusNode: inputFieldValues.focusNode, onChanged: inputFieldValues.onTagChanged, onSubmitted: inputFieldValues.onTagSubmitted, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Organisateurs', hintText: inputFieldValues.tags.isNotEmpty ? '' : "Enter un organisateur...", errorText: inputFieldValues.error, prefixIcon: inputFieldValues.tags.isNotEmpty ? SingleChildScrollView( controller: inputFieldValues.tagScrollController, scrollDirection: Axis.vertical, child: Padding( padding: const EdgeInsets.only( top: 8, bottom: 8, left: 8, ), child: Wrap( runSpacing: 4.0, spacing: 4.0, children: inputFieldValues.tags .map((String tag) { return Container( decoration: const BoxDecoration( borderRadius: BorderRadius.all( Radius.circular(20.0), ), color: Colors.blue, ), margin: const EdgeInsets.symmetric( horizontal: 5.0), padding: const EdgeInsets.symmetric( horizontal: 10.0, vertical: 5.0), child: Row( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ InkWell( child: Text( '$tag', style: const TextStyle( color: Colors.white), ), onTap: () { //print("$tag selected"); }, ), const SizedBox(width: 4.0), InkWell( child: const Icon( Icons.cancel, size: 14.0, color: Color.fromARGB( 255, 233, 233, 233), ), onTap: () { inputFieldValues .onTagRemoved(tag); }, ) ], ), ); }).toList()), ), ) : null, ), ), ); }), Padding( padding: const EdgeInsets.only( left: 15.0, right: 15.0, top: 15, bottom: 0), //padding: EdgeInsets.symmetric(horizontal: 15), child: TextField( controller: inputDesc, keyboardType: TextInputType.multiline, maxLines: 10, decoration: InputDecoration( border: OutlineInputBorder(), labelText: 'Description', hintText: 'Décrire l\'evènement'), ), ), SizedBox( height: 30, ), Container( height: 50, width: 250, decoration: BoxDecoration( color: Colors.blue, borderRadius: BorderRadius.circular(20)), child: TextButton( onPressed: () { if (_formKey.currentState!.validate()) { _updateEvent(context); } }, child: Text( 'Ajouter', style: TextStyle(color: Colors.white, fontSize: 25), ), ), ) ], ), ), )); } }