2024-10-28 23:50:20 +01:00
|
|
|
import 'package:flutter/material.dart';
|
|
|
|
import 'package:http/http.dart' as http;
|
|
|
|
import 'package:flutter_dotenv/flutter_dotenv.dart'; // Import dotenv
|
2024-06-23 17:23:26 +02:00
|
|
|
import 'dart:convert';
|
2024-06-24 23:57:43 +02:00
|
|
|
import 'dart:io';
|
2024-10-28 22:52:00 +01:00
|
|
|
import 'ItemMenu.dart';
|
|
|
|
import 'Camera.dart';
|
2024-06-30 15:17:10 +02:00
|
|
|
import '../classes/events.dart';
|
2024-06-23 20:57:54 +02:00
|
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
2024-06-27 00:01:07 +02:00
|
|
|
import 'package:intl/intl.dart';
|
|
|
|
import 'package:intl/date_symbol_data_local.dart';
|
2024-07-06 17:29:41 +02:00
|
|
|
import 'package:camera/camera.dart';
|
2024-10-28 23:50:20 +01:00
|
|
|
import 'package:mapbox_gl/mapbox_gl.dart'; // Add this import
|
2024-11-04 23:53:24 +01:00
|
|
|
import 'dart:math';
|
2024-06-30 15:17:10 +02:00
|
|
|
import '../variable/globals.dart' as globals;
|
2024-06-24 23:57:43 +02:00
|
|
|
|
2024-06-23 17:23:26 +02:00
|
|
|
void main() {
|
2024-10-28 22:52:00 +01:00
|
|
|
initializeDateFormatting("fr_FR", null).then((_) => runApp(const MyApp()));
|
2024-06-23 17:23:26 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
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<ListItemMenu> createState() => _MyHomePageState();
|
|
|
|
}
|
|
|
|
|
|
|
|
class _MyHomePageState extends State<ListItemMenu> {
|
2024-06-24 23:57:43 +02:00
|
|
|
Future<List<Events>> postsFuture = getPosts();
|
2024-10-23 22:43:03 +02:00
|
|
|
List<Events> filteredPosts = [];
|
2024-10-26 17:51:31 +02:00
|
|
|
String geographicalZone = '';
|
|
|
|
String query = '';
|
2024-11-04 23:53:24 +01:00
|
|
|
List<Map<String, dynamic>> suggestions = [];
|
2024-10-28 23:50:20 +01:00
|
|
|
TextEditingController inputGeo = TextEditingController();
|
2024-10-26 17:51:31 +02:00
|
|
|
|
2024-10-28 23:50:20 +01:00
|
|
|
// Fetching events from API
|
2024-06-24 23:57:43 +02:00
|
|
|
static Future<List<Events>> getPosts() async {
|
2024-06-23 20:57:54 +02:00
|
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
|
|
var accessToken = prefs.getString("access_token") ?? "";
|
2024-06-24 23:57:43 +02:00
|
|
|
final List<Events> body = [];
|
2024-06-23 20:57:54 +02:00
|
|
|
if (accessToken.isNotEmpty) {
|
2024-06-30 14:48:48 +02:00
|
|
|
var url = Uri.parse("${globals.api}/events");
|
2024-06-24 23:57:43 +02:00
|
|
|
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();
|
2024-06-23 20:57:54 +02:00
|
|
|
}
|
|
|
|
return body;
|
2024-06-23 17:23:26 +02:00
|
|
|
}
|
|
|
|
|
2024-10-28 23:50:20 +01:00
|
|
|
Future<void> searchSuggestions(String input) async {
|
|
|
|
await dotenv.load(fileName: ".env"); // Load .env file
|
|
|
|
|
|
|
|
final mapboxAccessToken = dotenv.env['MAPBOX_ACCESS_TOKEN'] ?? '';
|
|
|
|
final url =
|
2024-11-04 23:53:24 +01:00
|
|
|
'https://api.mapbox.com/geocoding/v5/mapbox.places/${input}.json?access_token=${mapboxAccessToken}&proximity=ip';
|
2024-10-28 23:50:20 +01:00
|
|
|
final response = await http.get(Uri.parse(url));
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
final data = json.decode(response.body);
|
|
|
|
setState(() {
|
|
|
|
suggestions = (data['features'] as List)
|
2024-11-04 23:53:24 +01:00
|
|
|
.map((feature) => {
|
|
|
|
'place_name': feature['place_name'],
|
|
|
|
'geometry': feature[
|
|
|
|
'geometry'], // Include geometry for latitude/longitude
|
|
|
|
})
|
2024-10-28 23:50:20 +01:00
|
|
|
.toList();
|
2024-10-23 22:43:03 +02:00
|
|
|
});
|
2024-10-28 23:50:20 +01:00
|
|
|
} else {
|
|
|
|
throw Exception('Failed to load suggestions');
|
2024-10-23 22:43:03 +02:00
|
|
|
}
|
2024-07-06 17:29:41 +02:00
|
|
|
}
|
|
|
|
|
2024-11-04 23:53:24 +01:00
|
|
|
Future<void> fetchPostsByLocation(double latitude, double longitude) async {
|
|
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
|
|
var accessToken = prefs.getString("access_token") ?? "";
|
|
|
|
|
|
|
|
if (accessToken.isNotEmpty) {
|
|
|
|
// Calculate the boundaries
|
|
|
|
double radiusInKm = 50;
|
|
|
|
double latDistance = radiusInKm / 111.0;
|
|
|
|
double lonDistance = radiusInKm / (111.0 * cos(latitude * pi / 180));
|
|
|
|
|
|
|
|
double minLat = latitude - latDistance;
|
|
|
|
double maxLat = latitude + latDistance;
|
|
|
|
double minLon = longitude - lonDistance;
|
|
|
|
double maxLon = longitude + lonDistance;
|
|
|
|
|
|
|
|
var url = Uri.parse("${globals.api}/events/search"
|
|
|
|
"?min_lat=$minLat&max_lat=$maxLat"
|
|
|
|
"&min_lon=$minLon&max_lon=$maxLon");
|
|
|
|
|
|
|
|
final response = await http.get(url, headers: {
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
HttpHeaders.cookieHeader: "access_token=$accessToken"
|
|
|
|
});
|
|
|
|
|
|
|
|
if (response.statusCode == 200) {
|
|
|
|
final List<dynamic> body = json.decode(utf8.decode(response.bodyBytes));
|
|
|
|
setState(() {
|
2024-11-05 15:11:53 +01:00
|
|
|
filteredPosts = body.isNotEmpty
|
|
|
|
? body
|
|
|
|
.map((e) => Events.fromJson(e as Map<String, dynamic>))
|
|
|
|
.toList()
|
|
|
|
: [];
|
2024-11-04 23:53:24 +01:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
throw Exception('Failed to load posts');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-10-28 22:52:00 +01:00
|
|
|
Padding _buildGeographicalZoneSearchField() {
|
|
|
|
return Padding(
|
|
|
|
padding: const EdgeInsets.all(8.0),
|
2024-10-28 23:50:20 +01:00
|
|
|
child: Column(
|
|
|
|
children: [
|
|
|
|
TextField(
|
|
|
|
controller: inputGeo,
|
|
|
|
decoration: InputDecoration(
|
|
|
|
labelText: 'Search by geographical zone',
|
|
|
|
border: OutlineInputBorder(),
|
2024-11-04 23:53:24 +01:00
|
|
|
suffixIcon: IconButton(
|
|
|
|
icon: Icon(Icons.clear),
|
|
|
|
onPressed: () {
|
|
|
|
setState(() {
|
|
|
|
inputGeo.clear(); // Clear the text field
|
|
|
|
geographicalZone = ''; // Reset the geographical zone state
|
|
|
|
suggestions.clear(); // Optionally clear suggestions
|
2024-11-05 15:11:53 +01:00
|
|
|
filteredPosts.clear();
|
2024-11-04 23:53:24 +01:00
|
|
|
});
|
|
|
|
},
|
|
|
|
),
|
2024-10-28 23:50:20 +01:00
|
|
|
),
|
|
|
|
onChanged: (value) {
|
|
|
|
setState(() {
|
|
|
|
geographicalZone = value;
|
|
|
|
searchSuggestions(value);
|
|
|
|
});
|
|
|
|
},
|
|
|
|
),
|
|
|
|
if (suggestions.isNotEmpty)
|
|
|
|
Container(
|
|
|
|
height: 200,
|
|
|
|
decoration: BoxDecoration(
|
2024-11-05 15:11:53 +01:00
|
|
|
border: Border.all(color: Colors.blue),
|
|
|
|
borderRadius: BorderRadius.circular(8),
|
2024-10-28 23:50:20 +01:00
|
|
|
),
|
|
|
|
child: ListView.builder(
|
2024-11-04 23:53:24 +01:00
|
|
|
shrinkWrap: true,
|
2024-10-28 23:50:20 +01:00
|
|
|
itemCount: suggestions.length,
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
return ListTile(
|
2024-11-04 23:53:24 +01:00
|
|
|
title: Text(suggestions[index]['place_name']),
|
|
|
|
onTap: () async {
|
|
|
|
final latitude =
|
|
|
|
suggestions[index]['geometry']['coordinates'][1];
|
|
|
|
final longitude =
|
|
|
|
suggestions[index]['geometry']['coordinates'][0];
|
|
|
|
|
2024-10-28 23:50:20 +01:00
|
|
|
setState(() {
|
2024-11-04 23:53:24 +01:00
|
|
|
geographicalZone = suggestions[index]['place_name'];
|
|
|
|
inputGeo.text = geographicalZone;
|
2024-10-28 23:50:20 +01:00
|
|
|
suggestions.clear();
|
|
|
|
});
|
2024-11-04 23:53:24 +01:00
|
|
|
|
|
|
|
await fetchPostsByLocation(latitude, longitude);
|
2024-10-28 23:50:20 +01:00
|
|
|
},
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2024-10-28 22:52:00 +01:00
|
|
|
),
|
|
|
|
);
|
2024-10-26 17:51:31 +02:00
|
|
|
}
|
|
|
|
|
2024-06-23 17:23:26 +02:00
|
|
|
@override
|
|
|
|
Widget build(BuildContext context) {
|
|
|
|
return Scaffold(
|
2024-10-23 22:43:03 +02:00
|
|
|
appBar: AppBar(
|
|
|
|
title: const Text("Item list menu"),
|
|
|
|
backgroundColor: Colors.blue,
|
|
|
|
foregroundColor: Colors.white,
|
|
|
|
actions: [
|
|
|
|
IconButton(
|
|
|
|
icon: const Icon(Icons.search),
|
|
|
|
onPressed: () {
|
|
|
|
showSearch(
|
|
|
|
context: context,
|
|
|
|
delegate: SearchDelegateExample(),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
),
|
|
|
|
],
|
|
|
|
),
|
2024-10-26 17:51:31 +02:00
|
|
|
body: Column(
|
|
|
|
children: [
|
2024-10-28 22:52:00 +01:00
|
|
|
_buildGeographicalZoneSearchField(),
|
2024-10-26 17:51:31 +02:00
|
|
|
Expanded(
|
|
|
|
child: FutureBuilder<List<Events>>(
|
|
|
|
future: postsFuture,
|
|
|
|
builder: (context, snapshot) {
|
|
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
2024-10-28 22:52:00 +01:00
|
|
|
return const Center(child: CircularProgressIndicator());
|
2024-10-26 17:51:31 +02:00
|
|
|
} else if (snapshot.hasData) {
|
|
|
|
final posts = snapshot.data!;
|
|
|
|
final displayedPosts =
|
|
|
|
filteredPosts.isEmpty ? posts : filteredPosts;
|
|
|
|
return buildPosts(displayedPosts);
|
|
|
|
} else {
|
|
|
|
return const Text("No data available");
|
|
|
|
}
|
|
|
|
},
|
|
|
|
),
|
|
|
|
),
|
|
|
|
],
|
2024-06-23 17:23:26 +02:00
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2024-10-28 23:50:20 +01:00
|
|
|
// Function to display fetched data on screen
|
2024-06-24 23:57:43 +02:00
|
|
|
Widget buildPosts(List<Events> posts) {
|
2024-10-28 22:52:00 +01:00
|
|
|
return ListView.separated(
|
|
|
|
itemCount: posts.length,
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
final post = posts[index];
|
|
|
|
final startDate = DateTime.parse(post.startDate!);
|
|
|
|
final date = DateFormat.yMd().format(startDate);
|
|
|
|
final time = DateFormat.Hm().format(startDate);
|
|
|
|
|
|
|
|
return ListTile(
|
2024-11-05 15:11:53 +01:00
|
|
|
title: Text('${post.name!}'),
|
|
|
|
subtitle: Text('${post.place!}\n${date} ${time}'),
|
|
|
|
onTap: () {
|
|
|
|
Navigator.push(
|
|
|
|
context,
|
|
|
|
MaterialPageRoute(builder: (_) => ItemMenu(title: post.id!)),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
2024-10-28 22:52:00 +01:00
|
|
|
},
|
2024-11-05 15:11:53 +01:00
|
|
|
separatorBuilder: (context, index) => Divider(),
|
2024-06-26 23:10:16 +02:00
|
|
|
);
|
2024-06-23 17:23:26 +02:00
|
|
|
}
|
|
|
|
}
|
2024-10-23 22:43:03 +02:00
|
|
|
|
|
|
|
class SearchDelegateExample extends SearchDelegate {
|
|
|
|
@override
|
|
|
|
List<Widget> buildActions(BuildContext context) {
|
|
|
|
return [
|
|
|
|
IconButton(
|
|
|
|
icon: const Icon(Icons.clear),
|
|
|
|
onPressed: () {
|
2024-11-05 15:11:53 +01:00
|
|
|
query = ''; // Clear the query text
|
|
|
|
showSuggestions(context); // Show suggestions
|
2024-10-23 22:43:03 +02:00
|
|
|
},
|
|
|
|
),
|
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget buildLeading(BuildContext context) {
|
|
|
|
return IconButton(
|
2024-11-05 15:11:53 +01:00
|
|
|
icon: const Icon(Icons.arrow_back), // Default back icon
|
2024-10-23 22:43:03 +02:00
|
|
|
onPressed: () {
|
2024-11-05 15:11:53 +01:00
|
|
|
close(context, null); // Close the search delegate
|
2024-10-23 22:43:03 +02:00
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget buildResults(BuildContext context) {
|
2024-11-05 15:11:53 +01:00
|
|
|
// Here, you can return the actual results based on the query
|
2024-10-23 22:43:03 +02:00
|
|
|
return FutureBuilder<List<Events>>(
|
2024-11-05 15:11:53 +01:00
|
|
|
future: searchPosts(query), // Implement your own search logic
|
2024-10-23 22:43:03 +02:00
|
|
|
builder: (context, snapshot) {
|
|
|
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
|
|
|
return const Center(child: CircularProgressIndicator());
|
|
|
|
} else if (snapshot.hasData) {
|
|
|
|
final posts = snapshot.data!;
|
|
|
|
return ListView.builder(
|
|
|
|
itemCount: posts.length,
|
|
|
|
itemBuilder: (context, index) {
|
|
|
|
final post = posts[index];
|
|
|
|
return ListTile(
|
|
|
|
title: Text(post.name!),
|
|
|
|
subtitle: Text(post.place!),
|
|
|
|
onTap: () {
|
|
|
|
Navigator.push(
|
|
|
|
context,
|
|
|
|
MaterialPageRoute(
|
|
|
|
builder: (_) => ItemMenu(title: post.id!),
|
|
|
|
),
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
},
|
|
|
|
);
|
|
|
|
} else {
|
|
|
|
return const Center(child: Text("No results found"));
|
|
|
|
}
|
|
|
|
},
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
@override
|
|
|
|
Widget buildSuggestions(BuildContext context) {
|
2024-11-05 15:11:53 +01:00
|
|
|
return Container(); // This is for showing search suggestions if needed
|
2024-10-23 22:43:03 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<Events>> searchPosts(String query) async {
|
|
|
|
SharedPreferences prefs = await SharedPreferences.getInstance();
|
|
|
|
var accessToken = prefs.getString("access_token") ?? "";
|
|
|
|
final List<Events> body = [];
|
|
|
|
if (accessToken.isNotEmpty) {
|
|
|
|
var url = Uri.parse("${globals.api}/events/search?item=$query");
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
}
|