Created
February 23, 2025 08:06
-
-
Save Muhammad-Haris-2/42c6d744164d22aade64b4201561b9c8 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import 'package:flutter/cupertino.dart'; | |
| import 'package:flutter/material.dart'; | |
| import 'package:flutter_rating_bar/flutter_rating_bar.dart'; | |
| import 'package:flutter_svg/flutter_svg.dart'; | |
| import 'package:google_maps_flutter/google_maps_flutter.dart'; | |
| import 'package:intl/intl.dart'; | |
| import 'package:shimmer/shimmer.dart'; | |
| import 'package:vertical_scrollable_tabview/vertical_scrollable_tabview.dart'; | |
| import 'package:scroll_to_index/scroll_to_index.dart'; | |
| import '../../constants/theme/app_colors.dart'; | |
| import '../../models/restaurant_model.dart'; | |
| import '../../services/review_service.dart'; | |
| import '../../widgets/menu_items.dart'; | |
| import 'add_update_restaurants_screen.dart'; | |
| class RestaurantDetailScreen extends StatefulWidget { | |
| final Restaurant restaurant; | |
| const RestaurantDetailScreen({super.key, required this.restaurant}); | |
| @override | |
| State<RestaurantDetailScreen> createState() => _RestaurantDetailScreenState(); | |
| } | |
| class _RestaurantDetailScreenState extends State<RestaurantDetailScreen> with TickerProviderStateMixin { | |
| int _currentPage = 0; | |
| late TabController tabController; | |
| late AutoScrollController autoScrollController; | |
| @override | |
| void initState() { | |
| tabController = TabController(length: 6, vsync: this); | |
| autoScrollController = AutoScrollController(); | |
| super.initState(); | |
| } | |
| @override | |
| void dispose() { | |
| tabController.dispose(); | |
| autoScrollController.dispose(); | |
| super.dispose(); | |
| } | |
| @override | |
| Widget build(BuildContext context) { | |
| return Scaffold( | |
| body: VerticalScrollableTabView( | |
| autoScrollController: autoScrollController, | |
| scrollbarThumbVisibility: false, | |
| tabController: tabController, | |
| slivers: [ | |
| SliverAppBar( | |
| expandedHeight: 350.0, | |
| toolbarHeight: kToolbarHeight, | |
| floating: false, | |
| pinned: true, | |
| backgroundColor: CustomColors.backgroundColor, | |
| surfaceTintColor: Colors.transparent, | |
| leading: Padding( | |
| padding: const EdgeInsets.all(8.0), | |
| child: Container( | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(12), | |
| color: Colors.white.withOpacity(0.5), | |
| ), | |
| child: IconButton( | |
| icon: const Icon(Icons.arrow_back), | |
| color: Colors.black, | |
| onPressed: () => Navigator.pop(context), | |
| ), | |
| ), | |
| ), | |
| actions: [ | |
| Padding( | |
| padding: const EdgeInsets.all(8.0), | |
| child: Container( | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(12), | |
| color: Colors.white.withOpacity(0.5), | |
| ), | |
| child: IconButton( | |
| icon: const Icon(Icons.edit_note), | |
| color: Colors.black, | |
| onPressed: () { | |
| Navigator.push( | |
| context, | |
| MaterialPageRoute(builder: (context) => RestaurantCreateUpdateScreen(restaurant: widget.restaurant)), | |
| ); | |
| }, | |
| ), | |
| ), | |
| ), | |
| ], | |
| flexibleSpace: FlexibleSpaceBar( | |
| background: Container( | |
| margin: const EdgeInsets.only(bottom: 50), | |
| color: CustomColors.greyColor.withOpacity(0.1), | |
| child: Stack( | |
| children: [ | |
| if (widget.restaurant.imageUrls.isEmpty) | |
| Center( | |
| child: Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| SvgPicture.asset( | |
| 'assets/icons/gallery.svg', | |
| height: 40, | |
| colorFilter: const ColorFilter.mode(CustomColors.greyColor, BlendMode.srcIn), | |
| ), | |
| const SizedBox(height: 8), | |
| const Text( | |
| 'No Image Available', | |
| style: TextStyle( | |
| color: CustomColors.greyColor, | |
| fontSize: 16, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ) | |
| else | |
| PageView.builder( | |
| itemCount: widget.restaurant.imageUrls.length, | |
| itemBuilder: (context, index) { | |
| return Image.network( | |
| widget.restaurant.imageUrls[index], | |
| fit: BoxFit.cover, | |
| width: double.infinity, | |
| errorBuilder: (context, _, __) { | |
| return Column( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: [ | |
| SvgPicture.asset( | |
| 'assets/icons/gallery.svg', | |
| height: 40, | |
| colorFilter: const ColorFilter.mode(CustomColors.greyColor, BlendMode.srcIn), | |
| ), | |
| const SizedBox(height: 8), | |
| const Text( | |
| 'No Image Available', | |
| style: TextStyle( | |
| color: CustomColors.greyColor, | |
| fontSize: 16, | |
| ), | |
| ), | |
| ], | |
| ); | |
| }, | |
| ); | |
| }, | |
| onPageChanged: (index) => setState(() => _currentPage = index), | |
| ), | |
| Positioned( | |
| bottom: 10.0, | |
| left: 0.0, | |
| right: 0.0, | |
| child: Row( | |
| mainAxisAlignment: MainAxisAlignment.center, | |
| children: List.generate( | |
| widget.restaurant.imageUrls.length, | |
| (index) { | |
| return Container( | |
| margin: const EdgeInsets.symmetric(horizontal: 3.0), | |
| width: _currentPage == index ? 12.0 : 8.0, | |
| height: _currentPage == index ? 12.0 : 8.0, | |
| decoration: BoxDecoration( | |
| shape: BoxShape.circle, | |
| color: _currentPage == index ? Colors.white : Colors.white.withOpacity(0.5), | |
| ), | |
| ); | |
| }, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ), | |
| bottom: PreferredSize( | |
| preferredSize: const Size.fromHeight(50), | |
| child: Container( | |
| color: CustomColors.backgroundColor, | |
| child: TabBar( | |
| isScrollable: true, | |
| controller: tabController, | |
| indicatorColor: CustomColors.primaryColor, | |
| labelColor: CustomColors.primaryColor, | |
| unselectedLabelColor: CustomColors.greyColor, | |
| indicatorWeight: 3.0, | |
| tabs: const [ | |
| Tab(text: "Details"), | |
| Tab(text: "About"), | |
| Tab(text: "Opening Hours"), | |
| Tab(text: "Menu Items"), | |
| Tab(text: "Customer Reviews"), | |
| Tab(text: "Location"), | |
| ], | |
| onTap: (index) => VerticalScrollableTabBarStatus.setIndex(index), | |
| ), | |
| ), | |
| ), | |
| ), | |
| ], | |
| listItemData: [ | |
| _buildDetails(), | |
| _buildAbout(), | |
| _buildOpeningHours(), | |
| _buildMenue(), | |
| _buildReviews(), | |
| _buildLocation(), | |
| ], | |
| verticalScrollPosition: VerticalScrollPosition.begin, | |
| eachItemChild: (object, index) => object, | |
| ), | |
| ); | |
| } | |
| Padding _buildLocation() { | |
| return Padding( | |
| padding: const EdgeInsets.all(16), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const Text( | |
| "Location", | |
| style: TextStyle( | |
| fontSize: 18, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| const SizedBox(height: 10), | |
| Container( | |
| height: 200, | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(16), | |
| border: Border.all(width: 1, color: Colors.grey.shade300), | |
| ), | |
| child: ClipRRect( | |
| borderRadius: BorderRadius.circular(16), | |
| child: GoogleMap( | |
| initialCameraPosition: CameraPosition( | |
| target: widget.restaurant.latLng, | |
| zoom: 14, | |
| ), | |
| markers: { | |
| const Marker( | |
| markerId: MarkerId('restaurantLocation'), | |
| position: LatLng(37.7749, -122.4194), | |
| ), | |
| }, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Padding _buildReviews() { | |
| return Padding( | |
| padding: const EdgeInsets.all(16), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| const Text( | |
| "Customer Reviews", | |
| style: TextStyle( | |
| fontSize: 18, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| const SizedBox(height: 10), | |
| StreamBuilder( | |
| stream: ReviewService().getReviews(widget.restaurant.id), | |
| builder: (context, snapshot) { | |
| if (snapshot.connectionState == ConnectionState.waiting) { | |
| return Shimmer.fromColors( | |
| baseColor: Colors.grey.shade300, | |
| highlightColor: Colors.grey.shade100, | |
| child: GridView.builder( | |
| shrinkWrap: true, | |
| physics: const NeverScrollableScrollPhysics(), | |
| padding: EdgeInsets.zero, | |
| itemCount: 4, | |
| gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( | |
| crossAxisCount: 2, | |
| crossAxisSpacing: 8, | |
| mainAxisSpacing: 8, | |
| mainAxisExtent: 180, | |
| ), | |
| itemBuilder: (context, index) { | |
| return Container( | |
| width: MediaQuery.of(context).size.width * 0.8, | |
| decoration: BoxDecoration( | |
| color: Colors.white, | |
| borderRadius: BorderRadius.circular(8), | |
| ), | |
| ); | |
| }, | |
| ), | |
| ); | |
| } else if (snapshot.hasError) { | |
| return Container( | |
| padding: const EdgeInsets.all(20), | |
| width: double.infinity, | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(16), | |
| color: Colors.red.withOpacity(0.05), | |
| border: Border.all(width: 1, color: Colors.red), | |
| ), | |
| alignment: Alignment.center, | |
| child: Row( | |
| children: [ | |
| const Icon( | |
| CupertinoIcons.exclamationmark_circle, | |
| color: Colors.red, | |
| ), | |
| const SizedBox(width: 10), | |
| Expanded( | |
| child: Text( | |
| snapshot.error.toString(), | |
| style: const TextStyle( | |
| fontSize: 16, | |
| color: Colors.red, | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } else if (!snapshot.hasData || (snapshot.data?.isEmpty ?? false)) { | |
| return Container( | |
| padding: const EdgeInsets.all(20), | |
| width: double.infinity, | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(16), | |
| border: Border.all(width: 1, color: Colors.grey.shade300), | |
| ), | |
| alignment: Alignment.center, | |
| child: const Text( | |
| "No reviews yet.", | |
| style: TextStyle( | |
| fontSize: 18, | |
| color: CustomColors.greyColor, | |
| ), | |
| ), | |
| ); | |
| } | |
| final reviews = snapshot.data!; | |
| double averageRating = reviews.map((e) => e.rating).reduce((a, b) => a + b) / reviews.length; | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Row( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Column( | |
| children: [ | |
| Text( | |
| averageRating.toStringAsFixed(1), | |
| style: const TextStyle(fontSize: 50), | |
| ), | |
| const SizedBox(height: 10), | |
| RatingBarIndicator( | |
| rating: averageRating, | |
| itemCount: 5, | |
| itemSize: 24, | |
| unratedColor: Colors.grey.shade300, | |
| itemBuilder: (context, index) => const Icon( | |
| Icons.star, | |
| color: Colors.amber, | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(width: 16), | |
| Expanded( | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| for (int star = 5; star >= 1; star--) ...[ | |
| Row( | |
| children: [ | |
| Text('$star', style: const TextStyle(fontSize: 16)), | |
| const SizedBox(width: 8), | |
| Expanded( | |
| child: Stack( | |
| children: [ | |
| Container( | |
| height: 8, | |
| decoration: BoxDecoration( | |
| color: Colors.grey.shade300, | |
| borderRadius: BorderRadius.circular(5), | |
| ), | |
| ), | |
| FractionallySizedBox( | |
| widthFactor: reviews.where((e) => e.rating.round() == star).length / reviews.length, | |
| child: Container( | |
| height: 8, | |
| decoration: BoxDecoration( | |
| color: CustomColors.primaryColor, | |
| borderRadius: BorderRadius.circular(5), | |
| ), | |
| ), | |
| ), | |
| ], | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 8), | |
| ], | |
| ], | |
| ), | |
| ), | |
| ], | |
| ), | |
| const Divider(height: 20), | |
| ListView.builder( | |
| padding: EdgeInsets.zero, | |
| shrinkWrap: true, | |
| physics: const NeverScrollableScrollPhysics(), | |
| itemCount: reviews.length, | |
| itemBuilder: (context, index) { | |
| final review = reviews[index]; | |
| return Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Row( | |
| children: [ | |
| const CircleAvatar( | |
| backgroundColor: CustomColors.primaryColor, | |
| foregroundColor: Colors.white, | |
| child: Text("M"), | |
| ), | |
| const SizedBox(width: 8), | |
| Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text( | |
| review.customerName, | |
| style: const TextStyle( | |
| fontSize: 16, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| Text( | |
| DateFormat('dd MMM yyyy').format(review.createdAt), | |
| style: const TextStyle( | |
| fontSize: 14, | |
| color: CustomColors.greyColor, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 8), | |
| RatingBarIndicator( | |
| rating: review.rating, | |
| itemCount: 5, | |
| itemSize: 24, | |
| unratedColor: Colors.grey.shade300, | |
| itemBuilder: (context, index) => const Icon( | |
| Icons.star, | |
| color: Colors.amber, | |
| ), | |
| ), | |
| const SizedBox(height: 8), | |
| Text( | |
| review.feedback, | |
| style: const TextStyle( | |
| fontSize: 14, | |
| color: CustomColors.greyColor, | |
| ), | |
| ), | |
| const Divider(height: 20), | |
| ], | |
| ); | |
| }, | |
| ), | |
| ], | |
| ); | |
| }, | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Padding _buildMenue() { | |
| return Padding( | |
| padding: const EdgeInsets.all(16), | |
| child: MenuItems(restaurantId: widget.restaurant.id), | |
| ); | |
| } | |
| Padding _buildOpeningHours() { | |
| final today = DateFormat('EEEE').format(DateTime.now()); | |
| final todayHours = widget.restaurant.operatingHours.schedule[today]; | |
| final startTime = _timeFromString(todayHours?.start ?? '00:00'); | |
| final endTime = _timeFromString(todayHours?.end ?? '00:00'); | |
| List<String> slots = []; | |
| DateTime currentTime = DateTime(2024, 1, 1, startTime.hour, startTime.minute); | |
| DateTime endTimeDate = DateTime(2024, 1, 1, endTime.hour, endTime.minute); | |
| while (currentTime.isBefore(endTimeDate)) { | |
| slots.add(DateFormat('HH:mm').format(currentTime)); | |
| currentTime = currentTime.add(const Duration(hours: 1)); | |
| } | |
| return Padding( | |
| padding: const EdgeInsets.all(16), | |
| child: Column( | |
| children: [ | |
| const Row( | |
| children: [ | |
| Text( | |
| 'Operating Hours', | |
| style: TextStyle( | |
| fontSize: 18, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| Spacer(), | |
| Icon( | |
| Icons.calendar_month, | |
| color: CustomColors.primaryColor, | |
| ), | |
| SizedBox(width: 8), | |
| Text( | |
| "Today", | |
| style: TextStyle( | |
| fontSize: 18, | |
| fontWeight: FontWeight.bold, | |
| color: CustomColors.primaryColor, | |
| ), | |
| ) | |
| ], | |
| ), | |
| const SizedBox(height: 8), | |
| if (slots.isEmpty) | |
| Container( | |
| padding: const EdgeInsets.all(20), | |
| width: double.infinity, | |
| decoration: BoxDecoration( | |
| borderRadius: BorderRadius.circular(16), | |
| border: Border.all(width: 1, color: Colors.grey.shade300), | |
| ), | |
| alignment: Alignment.center, | |
| child: const Text( | |
| "No Openings For Today", | |
| style: TextStyle( | |
| fontSize: 18, | |
| color: CustomColors.greyColor, | |
| ), | |
| ), | |
| ) | |
| else | |
| GridView.builder( | |
| shrinkWrap: true, | |
| physics: const NeverScrollableScrollPhysics(), | |
| padding: EdgeInsets.zero, | |
| itemCount: slots.length, | |
| gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( | |
| crossAxisCount: 3, | |
| crossAxisSpacing: 8, | |
| mainAxisSpacing: 8, | |
| mainAxisExtent: 46, | |
| ), | |
| itemBuilder: (BuildContext context, int index) { | |
| return Container( | |
| decoration: BoxDecoration( | |
| color: CustomColors.primaryColor, | |
| borderRadius: BorderRadius.circular(8), | |
| ), | |
| alignment: Alignment.center, | |
| child: Text( | |
| slots[index], | |
| style: const TextStyle( | |
| fontSize: 16, | |
| fontWeight: FontWeight.bold, | |
| color: Colors.white, | |
| ), | |
| ), | |
| ); | |
| }, | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Padding _buildAbout() { | |
| return const Padding( | |
| padding: EdgeInsets.all(16), | |
| child: Column( | |
| crossAxisAlignment: CrossAxisAlignment.start, | |
| children: [ | |
| Text( | |
| "About", | |
| style: TextStyle( | |
| fontSize: 18, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| Text( | |
| "This is a test description for the restaurant. This restaurant offers a variety of dishes and a cozy atmosphere. It is known for its excellent service and delicious food. Come and enjoy a great dining experience.", | |
| style: TextStyle( | |
| fontSize: 14, | |
| color: CustomColors.greyColor, | |
| ), | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| Padding _buildDetails() { | |
| return Padding( | |
| padding: const EdgeInsets.fromLTRB(16, 16, 16, 0), | |
| child: Column( | |
| children: [ | |
| Row( | |
| children: [ | |
| Expanded( | |
| child: Text( | |
| widget.restaurant.name, | |
| style: const TextStyle( | |
| fontSize: 18, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| ), | |
| const SizedBox(width: 8), | |
| const Text( | |
| "4.3", | |
| style: TextStyle( | |
| fontSize: 16, | |
| fontWeight: FontWeight.bold, | |
| ), | |
| ), | |
| const Icon( | |
| Icons.star_rounded, | |
| color: Colors.amber, | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 10), | |
| Row( | |
| children: [ | |
| const Icon( | |
| Icons.restaurant, | |
| color: CustomColors.greyColor, | |
| ), | |
| const SizedBox(width: 8), | |
| Text( | |
| widget.restaurant.type, | |
| style: const TextStyle(fontSize: 14), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 6), | |
| Row( | |
| children: [ | |
| const Icon( | |
| Icons.location_on_outlined, | |
| color: CustomColors.greyColor, | |
| ), | |
| const SizedBox(width: 8), | |
| Expanded( | |
| child: Text( | |
| widget.restaurant.location, | |
| style: const TextStyle(fontSize: 14), | |
| ), | |
| ), | |
| ], | |
| ), | |
| const SizedBox(height: 6), | |
| Row( | |
| children: [ | |
| const Icon( | |
| Icons.people, | |
| color: CustomColors.greyColor, | |
| ), | |
| const SizedBox(width: 8), | |
| Text( | |
| '${widget.restaurant.totalCapacity}', | |
| style: const TextStyle(fontSize: 14), | |
| ), | |
| ], | |
| ), | |
| ], | |
| ), | |
| ); | |
| } | |
| TimeOfDay _timeFromString(String timeStr) { | |
| final timeParts = timeStr.split(':'); | |
| return TimeOfDay(hour: int.parse(timeParts[0]), minute: int.parse(timeParts[1])); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment