Overcoming Development Challenges in a Restaurant Review Platform(Tzine) Using Flutter and Next.js

Developing a restaurant review platform that integrates both mobile and web applications presents a unique set of challenges. By leveraging technologies such as Flutter, MongoDB, Cloud Firestore, and Next.js, you can create a robust and efficient system. Here, we explore some challenges faced and their solutions.

Fetching Data from API Endpoints in Flutter

For developers new to Flutter, working with JSON data can be daunting. Flutter provides libraries like http and convert to facilitate HTTP requests and JSON parsing. Start by understanding the JSON structure from your API and use Flutter's jsonDecode to parse the data. This approach allows you to map JSON data to Dart objects, simplifying data manipulation within your app.

Section of the API Client

  Future<Map<String,dynamic>> fetchRestaurants() async {
    final response = await http.get(Uri.parse('$baseUrl/restaurants'));
    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Failed to load restaurants');
    }
  }

  Future<Map<String,dynamic>> fetchRestaurant(String restaurantId) async {
    final response = await http.get(
      Uri.parse('$baseUrl/restaurants/$restaurantId'),
    );
    if (response.statusCode == 200) {
      return jsonDecode(response.body);
    } else {
      throw Exception('Failed to load restaurant');
    }
  }

Responsible for initiating API calls

Future<Map<String, dynamic>> _fetchRestaurants() async {
    try {
      final data = await ApiClient().fetchRestaurants();
      return data['data'];
    } catch (e) {
      print("Error fetching restaurants: $e");

      rethrow;
    }
  }

Managing Contact Requests and Notifications

Handling contact requests from restaurants involves storing data and sending notifications. Used MongoDB to store these requests securely. Automate email notifications to both the requester and the admin using Resend . This ensures timely communication and efficient management of new requests.

# Contact Form System with Email Notifications

This system provides a complete contact form solution for your Tzine web app, including email notifications via Resend and database storage for admin management.

## Features

- ✅ Contact form submission with validation
- ✅ Automatic confirmation email to users
- ✅ Admin notification email for new submissions
- ✅ Database storage of all contact requests
- ✅ Admin dashboard to manage contact requests
- ✅ Status tracking (pending, contacted, completed)
- ✅ Beautiful HTML email templates

## API Endpoints

### Contact Form Submission
```
POST /api/contact
```
Handles form submissions, saves to database, and sends emails.

### Admin Endpoints
```
GET /api/admin/contacts    # Get all contact requests
PATCH /api/admin/contacts  # Update contact status
DELETE /api/admin/contacts # Delete contact request
```

## Contact Form Fields

The contact form includes:
- Name (required)
- Email (required)
- Phone (optional)
- Restaurant Name (required)
- Location (required)
- Message (optional)

## Email Templates

### User Confirmation Email
- Professional HTML template
- Includes submission details
- Clear next steps information
- Contact information for support

### Admin Notification Email
- Clean format for quick review
- All submission details
- Timestamp and action reminders

## Admin Dashboard

Access the admin dashboard at `/admin/contacts` to:
- View all contact requests
- Filter by status (pending, contacted, completed)
- Update request status
- Direct email/phone links
- View submission timestamps

## Database Schema

The `Contact` model includes:
- Basic contact information
- Restaurant details
- Status tracking
- Timestamps (created/updated)
- Indexes for performance

## Usage

1. Users fill out the contact form at `/contact`
2. Form submits to `/api/contact`
3. Data is saved to MongoDB
4. User receives confirmation email
5. Admin receives notification email
6. Admin manages requests via `/admin/contacts`

## Security Notes

- Admin endpoints require JWT authentication
- Input validation on all form fields
- Email addresses are sanitized
- Rate limiting recommended for production

Logs from Resend Service

Implementing an Efficient Search Feature

Initially, implementing a search feature directly on the mobile app can lead to performance issues. To address this, create a search API endpoint in your web app using Next.js. This offloads processing from the mobile app, enhancing performance. Consider using search technologies like Elasticsearch or Algolia to provide fast and accurate search results.

Section of Search API End Point

 export async function GET(request: NextRequest) {
  try {
    await connectDB();

    const { searchParams } = new URL(request.url);
    const query = searchParams.get('q')?.toLowerCase() || '';
    const type = searchParams.get('type') || 'all'; // 'all', 'restaurants', 'dishes', 'menus'
    const location = searchParams.get('location')?.toLowerCase() || '';
    const limit = parseInt(searchParams.get('limit') || '20');
    const page = parseInt(searchParams.get('page') || '1');

    if (!query.trim()) {
      return NextResponse.json({
        success: false,
        message: 'Search query is required'
      }, { status: 400 });
    }

    const skip = (page - 1) * limit;
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    const results: any = {
      restaurants: [],
      dishes: [],
      totalResults: 0,
      currentPage: page,
      totalPages: 0,
      hasNextPage: false,
      hasPrevPage: false
    };

    // Search Restaurants
    if (type === 'all' || type === 'restaurants') {
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      const restaurantQuery: any = {
        $or: [
          { name: { $regex: query, $options: 'i' } },
          { location: { $regex: query, $options: 'i' } },
          { owner: { $regex: query, $options: 'i' } },
          { instagramHandle: { $regex: query, $options: 'i' } }
        ]
      };

      // Add location filter if specified
      if (location) {
        restaurantQuery.location = { $regex: location, $options: 'i' };
      }

      const restaurants = await Restaurant.find(restaurantQuery)
        .select('name location owner email phoneNumber instagramHandle coverPhoto createdAt')
        .sort({ createdAt: -1 })
        .limit(type === 'restaurants' ? limit : Math.floor(limit / 2))
        .skip(type === 'restaurants' ? skip : 0);

      results.restaurants = restaurants;
    }
    return NextResponse.json({
      success: true,
      data: results,
      message: `Found ${results.totalResults} results for "${query}"`
    });

  } catch (error) {
    console.error('Search error:', error);
    return NextResponse.json({
      success: false,
      error: 'Internal server error',
      message: 'An error occurred while searching'
    }, { status: 500 });
  }

Search Function on The Flutter App

Future<List<dynamic>> _fetchSearchResults(String query) async {
  if (query.isEmpty) {
    return []; // No need to search for an empty query.
  }

  try {
    // Call the new server-side search method from your ApiClient.
    final searchResponse = await ApiClient().search(query);

    // The server-side endpoint returns a combined data structure,
    // so we can extract and combine the results here.
    final List<dynamic> restaurants = searchResponse['data']['restaurants'] ?? [];
    final List<dynamic> dishes = searchResponse['data']['dishes'] ?? [];


    // Map restaurants to have a 'type' field
    final List<dynamic> restaurantResults = restaurants.map((item) {
      return {'type': 'restaurant', ...item};
    }).toList();

    // Map dishes to have a 'type' field
    final List<dynamic> dishResults = dishes.map((item) {
      // The server response already includes restaurant info, so no need to add it here.
      return {'type': 'dish', ...item};
    }).toList();

    // Combine both lists into a single result list
    final List<dynamic> combinedResults = [...restaurantResults, ...dishResults];

    return combinedResults;
  } catch (e) {
    print('Error during search: $e');
    return [];
  }
}

Handling Authentication Tokens for Admin Access

Another issue was the admin's inability to view contact requests due to improper token management. Initially, accesing tokens in local storage led to empty strings errors since the token was stored in site cookies as Login API Endpoint /api/login/ and besides switching to cookies for storing authentication tokens is a more secure approach. Cookies can be accessed server-side, ensuring proper authentication and authorization for admin pages.

// Use credentials: 'include' to automatically send cookies
      const response = await fetch(`/api/admin/contacts?${queryParams}`, {
        credentials: 'include',
        headers: {
          'Content-Type': 'application/json',
        },
      });

Conclusion

Building a restaurant review platform with Flutter and Next.js involves navigating various technical challenges. By effectively using these technologies, you can create a seamless and efficient user experience. Whether it's data fetching, managing data , implementing search features, or handling authentication, the right tools and strategies can help you overcome these challenges and build a successful platform.

link to the live website tzine

0
Subscribe to my newsletter

Read articles from Augustine Leonard Musaroche directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Augustine Leonard Musaroche
Augustine Leonard Musaroche