Creating event-related features for chipIn - Appwrite & Flutter: Event app tutorial

Β·

22 min read

Introduction

Welcome back! It's great that you've made it this far in the series. We still have many exciting features to work on. Today, we're taking another exciting leap forward. We'll enrich our application by integrating event creation and management features, a core component of any event planning app. Here's where things start to really come together - our app transforms into a fully functional platform.

In this blog post, our main objective is to showcase how we can leverage Appwrite's database service to enable our users to create and manage events. We will guide you through setting up the necessary models and services in Flutter, creating user-friendly interfaces for creating and joining events, and personalizing user experience by providing an overview of the events they're involved in. Let's get right into it!

Under the hood - Appwrite's database service

As we get ready to include the ability to create and manage events in our app, it's important to acknowledge the crucial role played by Appwrite's Database Service. I utilized this service to store user data, such as the events they created and participated in. This allows me to effortlessly retrieve and manipulate (programmatically) this information so I can display the events the user created and joined. So join me in setting up the database service!

Setting up the database for the event feature

Creating a database

  1. In the Appwrite Console of your project, go to the Database section and click the Create Database button.

  2. Name your Database. In my case, I am going to name it "chipIn Blogpost Database."

    πŸ’‘
    Think of a database as a library and collections as bookshelves. Each bookshelf contains books on a specific topic, and each book is like a document in the collection. You can search for books by going to the appropriate bookshelf and looking through the books on that shelf. Database > Collection > Documents or Library > Bookshelves > Books

Creating collections

  1. Create two collections: Events Collection and Attendees Collection

    • Click the Create collection button

    • Name your first collection to "Events Collection"

    • Once created, go back to create another collection

    • Name your second collection as "Attendees Collection"

Output:

  1. Copy the Database ID and the Collection ID of the two collections you created.

  2. Go back to your Flutter project and update the appwrite_constants.dart

     class AppwriteConstants {
       static const apiEndpoint = 'https://cloud.appwrite.io/v1';
       static const projectId = '<your-project-id>';
    
       static const databaseId = '<your-database-id>';
       static const eventsCollectionId = 'your-eventsCollection-id';
       static const attendeesCollectionId = 'your-attendeesCollection-id';
     }
    

    This code defines constants that are used to configure an Appwrite project. The constants specify the endpoint URL for the Appwrite API, the ID of the Appwrite project, the ID of the Appwrite database, and the IDs of two collections that store event and attendee data.

Putting attributes for each collection

In Appwrite, attributes define how data is structured and stored in collections. They specify the type, length, and requirements of the data, ensuring organized and easier data manipulation in our applications. Here are the steps to add attributes for each collection you created:

  1. Go back to your appwrite database view

  2. Click the Events Collection

  3. Go to the Attributes tab and click the Create Attribute button

    Here's the table guide you can use to create each attribute:

    Attribute Key

    Attribute Type

    Size

    Required or Array

    eventId

    String

    256

    required

    title

    String

    256

    required

    date

    Datetime

    n/a

    required

    location

    String

    256

    required

    creatorId

    String

    36

    required

    description

    String

    1000

    required

  4. After the Events Collection, proceed to the Attendees Collection

    Attribute Key

    Attribute Type

    Size

    Required or Array

    userId

    String

    36

    required

    eventId

    String

    36

    required

Here's what your collections would look like afterward:

Setting up the permissions in each collection

Setting up permissions for each collection in the Appwrite console allows you to control who can access and perform actions on the data within that collection, ensuring data security and privacy. Let me provide you with the steps:

  1. In the database you've created, click the Events Collection

  2. Go to the Settings tab

  3. Navigate to the Update Permissions section

  4. Click Add role

  5. Select Users

  6. Tick the checkbox for Create, Read, Update, Delete

  7. Click the Update button

Creating events in Appwrite

Our new codebase structure

lib/
β”œβ”€β”€ appwrite
β”‚   β”œβ”€β”€ appwrite_constants.dart
β”‚   └── appwrite_service.dart
β”œβ”€β”€ features
β”‚   β”œβ”€β”€ authentication
β”‚   β”‚   β”œβ”€β”€ controller
β”‚   β”‚   β”‚   └── auth_controller.dart
β”‚   β”‚   β”œβ”€β”€ services
β”‚   β”‚   β”‚   β”œβ”€β”€ auth_service.dart
β”‚   β”‚   β”‚   └── oauth_service.dart
β”‚   β”‚   └── views
β”‚   β”‚       β”œβ”€β”€ auth_main_view.dart
β”‚   β”‚       β”œβ”€β”€ login_view.dart
β”‚   β”‚       └── signup_view.dart
β”‚   β”œβ”€β”€ events // <-- New folder and sub-folders added here!
β”‚   β”‚   β”œβ”€β”€ models
β”‚   β”‚   β”‚   β”œβ”€β”€ attendee_model.dart
β”‚   β”‚   β”‚   └── event_model.dart
β”‚   β”‚   β”œβ”€β”€ services
β”‚   β”‚   β”‚   └── event_service.dart
β”‚   β”‚   β”œβ”€β”€ views
β”‚   β”‚   β”‚   β”œβ”€β”€ create_events.dart
β”‚   β”‚   β”‚   β”œβ”€β”€ created_events.dart
β”‚   β”‚   β”‚   β”œβ”€β”€ event_details.dart
β”‚   β”‚   β”‚   β”œβ”€β”€ join_events.dart
β”‚   β”‚   β”‚   └── joined_events.dart
β”‚   β”‚   └── widgets
β”‚   β”‚       └── event_card.dart
β”‚   └── home_page.dart
└── main.dart
  • events folder: The β€œevents” folder contains the code for creating and managing events in the app.

  • It has subdirectories for models, services, views, and widgets.

    • models: The models directory contains the attendee and event models.

    • services: The services directory contains the event service.

    • views: The views directory contains the user interface for creating and joining events, viewing created and joined events, and viewing event details.

    • widgets: The widgets directory contains the event card widget.

Creating a structure for the models and services

event_model.dart

import 'package:intl/intl.dart';

class MyEventModel {
  // Properties
  String eventId;
  String title;
  DateTime dateTime;
  String location;
  String creatorId;
  String description;

  String get eventDate => DateFormat('yyyy-MM-dd').format(dateTime);
  String get eventTime => DateFormat('HH:mm:ss').format(dateTime);

  // Constructor
  MyEventModel({
    required this.eventId,
    required this.title,
    required this.dateTime,
    required this.location,
    required this.creatorId,
    required this.description,
  });

  // fromJson
  factory MyEventModel.fromJson(Map<String, dynamic> json) {
    return MyEventModel(
      eventId: json['eventId'],
      title: json['title'],
      dateTime: DateTime.parse(json['date']),
      location: json['location'],
      creatorId: json['creatorId'],
      description: json['description'],
    );
  }

  // toJson
  Map<String, dynamic> toJson() {
    return {
      'eventId': eventId,
      'title': title,
      'date': dateTime.toIso8601String(),
      'location': location,
      'creatorId': creatorId,
      'description': description,
    };
  }
}

I built the MyEventModel class to organize and manage event data within my application. This class allows me to store essential information about an event, such as its title, date and time, location, creator, and description. By using this class, I can easily create, access, and manipulate event data in a structured manner.

The included methods for converting to and from JSON format enable seamless storage and retrieval of event information. To understand this further, imagine you are baking cookies. Here's the breakdown:

  1. Properties: These are like the ingredients in the cookie. eventId is the unique identifier for the event (like a special type of chocolate chip that each cookie has), title is the name of the event, dateTime is when the event will happen, location is where the event will be held, creatorId tells us who created the event, and description gives additional information about the event.

  2. Constructor: This is like the baking process. Just like how you need all ingredients to bake a cookie, you need to provide all the details (properties) to make an event (create an object of MyEventModel).

  3. fromJson: Sometimes, you get a pre-made cookie dough (a JSON object) from a store (Appwrite database). This function is like the process of turning that cookie dough into a baked cookie (an instance of MyEventModel).

  4. toJson: Conversely, sometimes, you want to store your homemade cookie (an instance of MyEventModel) back into the cookie dough form (a JSON object) for storing it in the fridge (the Appwrite database). This function helps you do just that.

The MyEventModel class, therefore, is like a recipe for creating, storing, and recreating cookies (or, in our case, events). It provides me structured way of handling event data throughout our application.

atendees_model.dart

class AttendeesModel {
  // Properties
  String userId;
  String eventId;

  // Constructor
  AttendeesModel({
    required this.userId,
    required this.eventId,
  });

  // fromJson
  AttendeesModel.fromJson(Map<String, dynamic> json)
      : userId = json['userId'],
        eventId = json['eventId'];
  // toJson
  Map<String, dynamic> toJson() {
    return {
      'userId': userId,
      'eventId': eventId,
    };
  }
}

Consider the AttendeesModel class as an invitation card for an event. Each instance of this class symbolizes a single invitation that has been sent out. Here's the breakdown:

  1. Properties: This invitation card has only two pieces of information: the userId, which represents the person (or user) the invitation is for, and the eventId, which identifies the event they're being invited to. Both of these pieces of information are essential for keeping track of who's attending what event.

  2. Constructor: The constructor is like the process of writing out an invitation card. You need the userId (who you're inviting) and the eventId (what event you're inviting them to).

  3. fromJson: Sometimes, you receive the details of the invitation in digital forms, like an email (a JSON object). This method is like transcribing that digital information onto a physical invitation card (an AttendeesModel instance).

  4. toJson: Other times, you must send out a digital invitation (a JSON object) using the details from a physical card (an AttendeesModel instance). This method converts the details on the card into a format suitable for email.

In essence, the AttendeesModel class helps us manage the guest list for our events in a structured way. We can see at a glance who is attending which event and vice versa.

Creating a feature for event creation and joining events

In this section, we will be doing the bulk of the coding. It can be a bit daunting, but I completely understand. After each code snippet, I'll include a Notion link that provides a thorough explanation of the code.

event_service.dart

import 'package:appwrite/appwrite.dart';
import 'package:chipin_blogpost/appwrite/appwrite_constants.dart';
import 'package:chipin_blogpost/appwrite/appwrite_service.dart';
import 'package:chipin_blogpost/features/events/models/attendee_model.dart';
import 'package:chipin_blogpost/features/events/models/event_model.dart';
import 'package:intl/intl.dart';
import 'package:uuid/uuid.dart';

class EventService {
  static final Client client = AppwriteService.getClient();
  static final Account account = Account(client);
  static final Databases databases = Databases(client);

  Future<bool> createEvent(MyEventModel event) async {
    try {
      DateTime eventDateTime =
          DateTime.parse('${event.eventDate} ${event.eventTime}');
      String formattedDate =
          DateFormat('yyyy-MM-dd HH:mm:ss').format(eventDateTime);

      // Make the eventID exactly equal to the documentID
      String eventId = const Uuid().v4().replaceAll('-', '');

      await databases.createDocument(
        databaseId: AppwriteConstants.databaseId,
        collectionId: AppwriteConstants.eventsCollectionId,
        // Assign the eventId to the documentId to have the same value
        documentId: eventId,
        data: {
          'eventId': eventId,
          'title': event.title,
          'date': formattedDate,
          'location': event.location,
          'creatorId': event.creatorId,
          'description': event.description,
        },
      );
      return true;
    } catch (e) {
      print('Failed to create event: $e');
      return false;
    }
  }

  static Future<bool> joinEvent(AttendeesModel attendee) async {
    try {
      String documentId = const Uuid().v4().replaceAll('-', '');
      await databases.createDocument(
        databaseId: AppwriteConstants.databaseId,
        collectionId: AppwriteConstants.attendeesCollectionId,
        documentId: documentId,
        data: {
          'userId': attendee.userId,
          'eventId': attendee.eventId,
        },
      );
      return true;
    } catch (e) {
      print('Failed to join event: $e');
      return false;
    }
  }

  Future<List<MyEventModel>> getMyCreatedEvents(String userId) async {
    List<MyEventModel> eventList = [];

    try {
      var response = await databases.listDocuments(
        databaseId: AppwriteConstants.databaseId,
        collectionId: AppwriteConstants.eventsCollectionId,
      );

      if (response.documents.isNotEmpty) {
        for (var item in response.documents) {
          MyEventModel event = MyEventModel.fromJson(item.data);
          if (event.creatorId == userId) {
            eventList.add(event);
          }
        }
      }
    } catch (e) {
      print('Failed to get events: $e');
    }
    return eventList;
  }

  Future<List<MyEventModel>> getMyJoinedEvents(String userId) async {
    List<MyEventModel> eventList = [];

    try {
      var response = await databases.listDocuments(
        databaseId: AppwriteConstants.databaseId,
        collectionId: AppwriteConstants.attendeesCollectionId,
      );

      if (response.documents.isNotEmpty) {
        for (var item in response.documents) {
          if (item.data['userId'] == userId) {
            String eventId = item.data['eventId'];
            var eventResponse = await databases.getDocument(
              databaseId: AppwriteConstants.databaseId,
              collectionId: AppwriteConstants.eventsCollectionId,
              documentId: eventId,
            );
            eventList.add(MyEventModel.fromJson(eventResponse.data));
          }
        }
      }
    } catch (e) {
      print('Failed to get events: $e');
    }
    return eventList;
  }

  static Future<List<MyEventModel>> getAllEvents() async {
    List<MyEventModel> eventList = [];

    try {
      var response = await databases.listDocuments(
        databaseId: AppwriteConstants.databaseId,
        collectionId: AppwriteConstants.eventsCollectionId,
      );

      if (response.documents.isNotEmpty) {
        for (var item in response.documents) {
          eventList.add(
            MyEventModel.fromJson(item.data),
          );
        }
      }
    } catch (e) {
      print('Failed to get events: $e');
    }
    return eventList;
  }
}

The EventService class is quite overwhelming to look at right now, and I totally understand.

πŸ’‘
This EventService class is essentially a toolbox that you can imagine that is filled with specific tools (methods) that perform tasks related to event management in our app. This toolbox is provided by the Appwrite service, and we've customized these tools to cater to our app's requirements.

Throughout the class, you'll notice I interact a lot with the Databases class. This is because Appwrite's Databases service acts like a big storage unit where all our app's data (like events and attendees) are stored. We use methods like createDocument, listDocuments, and getDocument to create, fetch, and manage data in this storage unit. It's how our toolbox interacts with the storage to perform its tasks.

If you would like to see a detailed explanation of each method/function, check out this Notion page called EventService Class Explanation.

User interactions - Creating and joining events

Up to this point, our focus has been on backend tasks that may not be noticeable to users, such as Appwrite Collections, Event Service functions, and Models. However, we will now redirect our attention to UI-related tasks that have a direct impact on end-users.

create_events.dart

import 'package:chipin_blogpost/features/authentication/services/auth_service.dart';
import 'package:chipin_blogpost/features/events/models/event_model.dart';
import 'package:chipin_blogpost/features/events/services/event_service.dart';
import 'package:flutter/material.dart';

class EventCreationScreen extends StatefulWidget {
  final EventService eventService;

  const EventCreationScreen({required this.eventService});

  @override
  _EventCreationScreenState createState() => _EventCreationScreenState();
}

class _EventCreationScreenState extends State<EventCreationScreen> {
  final _formKey = GlobalKey<FormState>();
  late final String _id = '';
  late String _title;
  DateTime _date = DateTime.now();
  TimeOfDay _time = TimeOfDay.now();
  late String _location;
  late String _description;

  Future<void> _selectDate() async {
    final DateTime? picked = await showDatePicker(
      context: context,
      initialDate: _date,
      firstDate: DateTime.now(),
      lastDate: DateTime(2100),
    );
    if (picked != null && picked != _date) {
      setState(() {
        _date = picked;
      });
    }
  }

  Future<void> _selectTime() async {
    final TimeOfDay? newTime = await showTimePicker(
      context: context,
      initialTime: _time,
    );
    if (newTime != null) {
      setState(() {
        _time = newTime;
      });
    }
  }

  void _submit() async {
    if (_formKey.currentState!.validate()) {
      _formKey.currentState!.save();

      DateTime dateTime = DateTime(
        _date.year,
        _date.month,
        _date.day,
        _time.hour,
        _time.minute,
      );

      MyEventModel newEvent = MyEventModel(
        eventId: _id,
        title: _title,
        dateTime: dateTime,
        location: _location,
        description: _description,
        creatorId: await AuthService.getCreatorId(),
      );

      bool success = await widget.eventService.createEvent(newEvent);

      if (success) {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('Event created successfully!'),
          ),
        );

        setState(() {
          _title = '';
          _date = DateTime.now();
          _time = TimeOfDay.now();
          _location = '';
          _description = '';
        });

        Navigator.pop(context);
      } else {
        ScaffoldMessenger.of(context).showSnackBar(
          const SnackBar(
            content: Text('Failed to create event.'),
          ),
        );
      }
    }
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Create Event'),
      ),
      body: Form(
        key: _formKey,
        child: ListView(
          padding: const EdgeInsets.all(16.0),
          children: [
            TextFormField(
              decoration: const InputDecoration(
                labelText: 'Title',
              ),
              validator: (value) =>
                  value!.isEmpty ? 'Please enter a title' : null,
              onSaved: (value) => _title = value!,
            ),
            TextFormField(
              decoration: const InputDecoration(
                labelText: 'Location',
              ),
              validator: (value) =>
                  value!.isEmpty ? 'Please enter a location' : null,
              onSaved: (value) => _location = value!,
            ),
            TextFormField(
              decoration: const InputDecoration(
                labelText: 'Description',
              ),
              validator: (value) =>
                  value!.isEmpty ? 'Please enter a description' : null,
              onSaved: (value) => _description = value!,
            ),
            ListTile(
              leading: const Icon(Icons.calendar_today),
              title: const Text('Date'),
              subtitle: Text(
                '${_date.year}-${_date.month}-${_date.day}',
              ),
              onTap: _selectDate,
            ),
            ListTile(
              leading: const Icon(Icons.access_time),
              title: const Text('Time'),
              subtitle: Text(
                _time.format(context),
              ),
              onTap: _selectTime,
            ),
            const SizedBox(height: 16.0),
            ElevatedButton(
              onPressed: _submit,
              child: const Text('Create Event'),
            ),
          ],
        ),
      ),
    );
  }
}

The EventCreationScreen is a screen where users can create events. It includes a form with fields for title, location, description, date, and time. When the form is submitted, the entered values are validated and used to create a new event using the EventService. If the event creation is successful, a success message is shown, and the form is reset. If it fails, an error message is displayed.

For a detailed explanation of the code, check out this Notion page called EventCreationScreen Widget Explanation.

join_events.dart

import 'package:chipin_blogpost/features/authentication/services/auth_service.dart';
import 'package:chipin_blogpost/features/events/models/attendee_model.dart';
import 'package:chipin_blogpost/features/events/models/event_model.dart';
import 'package:chipin_blogpost/features/events/services/event_service.dart';
import 'package:chipin_blogpost/features/events/widgets/event_card.dart';
import 'package:flutter/material.dart';

class JoinEventsScreen extends StatefulWidget {
  final EventService eventService;

  const JoinEventsScreen({required this.eventService});

  @override
  _JoinEventsScreenState createState() => _JoinEventsScreenState();
}

class _JoinEventsScreenState extends State<JoinEventsScreen> {
  Future<void> joinEvent(MyEventModel event) async {
    AttendeesModel myAttendeeModel = AttendeesModel(
      eventId: event.eventId,
      userId: currentUserId,
    );
    bool result = await EventService.joinEvent(myAttendeeModel);
    if (result) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Successfully joined event!'),
          duration: Duration(seconds: 2),
        ),
      );
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Failed to join event. Please try again.'),
          duration: Duration(seconds: 2),
        ),
      );
    }
  }

  List<MyEventModel> allEvents = [];
  String currentUserId = '';

  @override
  void initState() {
    super.initState();
    fetchAllEvents();
    getCurrentUserId();
  }

  Future<void> getCurrentUserId() async {
    currentUserId = await AuthService.getCreatorId();
  }

  Future<void> fetchAllEvents() async {
    try {
      List<MyEventModel> events = await EventService.getAllEvents();
      setState(() {
        allEvents = events;
      });
    } catch (error) {
      // Handle error
    }
  }

  @override
  Widget build(BuildContext context) {
    if (allEvents.isEmpty) {
      return const Center(
        child: Text('No events to join'),
      );
    } else {
      return Scaffold(
        appBar: AppBar(
          title: const Text('Join Events'),
        ),
        body: ListView.builder(
          itemCount: allEvents.length,
          itemBuilder: (context, index) {
            MyEventModel event = allEvents[index];
            bool showJoinButton = event.creatorId != currentUserId;
            return EventCard(
              event: event,
              showJoinButton: showJoinButton,
              onJoinPressed: () => joinEvent(event),
            );
          },
        ),
      );
    }
  }
}

This code represents a screen where users can join events. It fetches a list of events, displays them in a scrollable list, and allows users to join events by clicking a "Join" button. The code handles event joining logic and displays success or failure messages accordingly.

If you would like to see a detailed explanation of the code, visit this Notion page named JoinEventsScreen Widget Explanation.

Personalized user experience - My created events, joined Events, and event details

Our next feature will allow users to easily view the events they have created and those they have joined.

created_events.dart

import 'package:chipin_blogpost/features/authentication/services/auth_service.dart';
import 'package:chipin_blogpost/features/events/models/event_model.dart';
import 'package:chipin_blogpost/features/events/services/event_service.dart';
import 'package:chipin_blogpost/features/events/widgets/event_card.dart';
import 'package:flutter/material.dart';

class MyCreatedEventsScreen extends StatefulWidget {
  final EventService eventService;

  const MyCreatedEventsScreen({required this.eventService});

  @override
  _MyCreatedEventsScreenState createState() => _MyCreatedEventsScreenState();
}

class _MyCreatedEventsScreenState extends State<MyCreatedEventsScreen> {
  List<MyEventModel> myCreatedEvents = [];
  bool loading = true;

  @override
  void initState() {
    super.initState();
    fetchMyCreatedEvents();
  }

  Future<void> fetchMyCreatedEvents() async {
    try {
      String userId = await AuthService.getCreatorId();
      List<MyEventModel> events =
          await widget.eventService.getMyCreatedEvents(userId);
      setState(() {
        myCreatedEvents = events;
        loading = false;
      });
    } catch (error) {
      setState(() {
        loading = false;
      });
    }
  }

  @override
  Widget build(BuildContext context) {
    if (loading) {
      return const Center(
        child: CircularProgressIndicator(),
      );
    } else if (myCreatedEvents.isEmpty) {
      return const Center(
        child: Text('You do not have events created'),
      );
    } else {
      return ListView.builder(
        itemCount: myCreatedEvents.length,
        itemBuilder: (context, index) {
          MyEventModel event = myCreatedEvents[index];
          return EventCard(event: event, showJoinButton: false);
        },
      );
    }
  }
}

This code represents a screen where users can view their created events. It fetches the events using an event service, displays a loading indicator while fetching, shows a message if no events are found, and displays the events using a list view. Each event is represented by an event card widget.

If you want to see a detailed explanation of the code, visit this Notion page named MyCreatedEventsScreen Widget Explanation.

event_details.dart

import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:chipin_blogpost/features/authentication/services/auth_service.dart';
import 'package:chipin_blogpost/features/events/models/attendee_model.dart';
import 'package:chipin_blogpost/features/events/models/event_model.dart';
import 'package:chipin_blogpost/features/events/services/event_service.dart';

class EventDetailsScreen extends StatefulWidget {
  final MyEventModel event;

  EventDetailsScreen({required this.event});

  @override
  State<EventDetailsScreen> createState() => _EventDetailsScreenState();
}

class _EventDetailsScreenState extends State<EventDetailsScreen> {
  Future<void> joinEvent(MyEventModel event) async {
    AttendeesModel myAttendeeModel = AttendeesModel(
      eventId: event.eventId,
      userId: currentUserId,
    );
    bool result = await EventService.joinEvent(myAttendeeModel);
    if (result) {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Successfully joined event!'),
          duration: Duration(seconds: 2),
        ),
      );
    } else {
      ScaffoldMessenger.of(context).showSnackBar(
        const SnackBar(
          content: Text('Failed to join event. Please try again.'),
          duration: Duration(seconds: 2),
        ),
      );
    }
  }

  String currentUserId = '';

  @override
  void initState() {
    super.initState();
    getCurrentUserId();
  }

  Future<void> getCurrentUserId() async {
    currentUserId = await AuthService.getCreatorId();
  }

  @override
  Widget build(BuildContext context) {
    final dateFormat = DateFormat('EEEE, MMMM d, y');
    final timeFormat = DateFormat('h:mm a');

    return Scaffold(
      appBar: AppBar(
        title: const Text('Event Details'),
      ),
      body: SingleChildScrollView(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              widget.event.title,
              style: const TextStyle(
                fontSize: 24.0,
                fontWeight: FontWeight.bold,
                fontStyle: FontStyle.italic,
              ),
            ),
            const SizedBox(height: 16.0),
            Table(
              columnWidths: const {
                0: FlexColumnWidth(2),
                1: FlexColumnWidth(3),
              },
              border: TableBorder.all(),
              children: [
                _buildTableRow(
                  'Date',
                  dateFormat.format(widget.event.dateTime),
                  Icons.calendar_today,
                ),
                _buildTableRow(
                  'Time',
                  timeFormat.format(widget.event.dateTime),
                  Icons.access_time,
                ),
                _buildTableRow(
                  'Location',
                  widget.event.location,
                  Icons.location_on,
                ),
                _buildTableRow(
                  'Description',
                  widget.event.description,
                  Icons.description,
                ),
              ],
            ),
            const SizedBox(height: 16.0),
            Image.network(
              'https://images.unsplash.com/photo-1501281668745-f7f57925c3b4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1770&q=80',
            ),
            const SizedBox(height: 16.0),
            ElevatedButton(
              onPressed: () => joinEvent(widget.event),
              child: const Text('Join Event'),
            ),
          ],
        ),
      ),
    );
  }

  TableRow _buildTableRow(String label, String value, IconData icon) {
    return TableRow(
      children: [
        TableCell(
          child: Row(
            children: [
              Icon(icon),
              const SizedBox(width: 8.0),
              Text(label),
            ],
          ),
        ),
        TableCell(
          child: Text(value),
        ),
      ],
    );
  }
}

This code represents a screen that displays the details of a specific event. It includes the event's title, date, time, location, description, and image. Users have the option to join the event by clicking on the "Join Event" button. The event details are fetched from the provided MyEventModel. The joinEvent function is responsible for handling the join event functionality, where an attendee model is created and passed EventService to join the event. If the join is successful, a success message is shown, and if it fails, an error message is displayed. The user's ID is obtained using AuthService to determine if they are the creator of the event. The event details are styled and organized using various widgets and layouts from the Flutter framework.

Do you want a detailed explanation of this code? Check out this Notion page named EventDetailsScreen Widget Explanation.

Reusable components through an event card widget

import 'package:chipin_blogpost/features/events/models/event_model.dart';
import 'package:chipin_blogpost/features/events/views/event_details.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';

class EventCard extends StatelessWidget {
  final MyEventModel event;
  final bool showJoinButton;
  final VoidCallback? onJoinPressed;

  const EventCard({
    required this.event,
    this.showJoinButton = false,
    this.onJoinPressed,
  });

  @override
  Widget build(BuildContext context) {
    // Format the date and time to display in a readable format
    final formattedDate = DateFormat('EEE, MMM d, y').format(event.dateTime);
    final formattedTime = DateFormat('h:mm a').format(event.dateTime);

    return GestureDetector(
      onTap: () {
        // Navigate to the event details screen when the card is tapped
        Navigator.push(
          context,
          MaterialPageRoute(
            builder: (context) => EventDetailsScreen(event: event),
          ),
        );
      },
      child: Container(
        margin: const EdgeInsets.symmetric(vertical: 8.0),
        decoration: BoxDecoration(
          color: Colors.transparent, // Updated to a transparent color
          borderRadius: BorderRadius.circular(8.0),
        ),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            // Display the event image
            Image.network(
              'https://images.unsplash.com/photo-1501281668745-f7f57925c3b4?ixlib=rb-4.0.3&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D&auto=format&fit=crop&w=1770&q=80',
              fit: BoxFit.cover,
            ),
            Padding(
              padding: const EdgeInsets.all(16.0),
              child: Column(
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  // Display the event title
                  Text(
                    event.title,
                    style: const TextStyle(
                      fontWeight: FontWeight.bold,
                      fontSize: 18.0,
                      color: Colors.black,
                    ),
                    overflow: TextOverflow.ellipsis,
                    maxLines: 2,
                  ),
                  const SizedBox(height: 23.0),
                  Row(
                    children: [
                      const Icon(
                        Icons.access_time,
                        size: 16.0,
                      ),
                      const SizedBox(width: 4.0),
                      Text(
                        '$formattedDate at $formattedTime',
                        style: const TextStyle(
                          fontSize: 15.0,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 16.0),
                  Row(
                    children: [
                      const Icon(
                        Icons.location_on,
                        size: 16.0,
                      ),
                      const SizedBox(width: 4.0),
                      Text(
                        event.location,
                        style: const TextStyle(
                          fontSize: 15.0,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 16.0),
                  Row(
                    children: [
                      const Icon(
                        Icons.account_circle,
                        size: 16.0,
                      ),
                      const SizedBox(width: 4.0),
                      Text(
                        'Created by ${event.creatorId}',
                        style: const TextStyle(
                          fontSize: 15.0,
                        ),
                      ),
                    ],
                  ),
                  const SizedBox(height: 16.0),
                  Row(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      const Icon(
                        Icons.description,
                        size: 16.0,
                      ),
                      const SizedBox(width: 4.0),
                      Expanded(
                        child: Text(
                          event.description,
                          style: const TextStyle(
                            fontSize: 15.0,
                          ),
                        ),
                      ),
                    ],
                  ),
                ],
              ),
            ),
            if (showJoinButton)
              ElevatedButton(
                onPressed: onJoinPressed,
                child: const Text('Join'),
              ),
          ],
        ),
      ),
    );
  }
}

The code represents an event card widget that displays event information, such as the event title, date, time, location, creator, and description. It also provides an optional "Join" button. The card is interactive, and tapping on it navigates to a screen that shows detailed information about the event. The code uses various Flutter widgets to create the card layout, format date and time, and handle user interactions.

For a detailed explanation, check out EventCard Widget Explanation.

Once an event card has been created, we can utilize it to generate a carousel animation on the homepage of our application.

home_page.dart

import 'package:carousel_slider/carousel_slider.dart';
import 'package:chipin_blogpost/features/authentication/services/auth_service.dart';
import 'package:chipin_blogpost/features/authentication/views/auth_main_view.dart';
import 'package:chipin_blogpost/features/events/models/event_model.dart';
import 'package:chipin_blogpost/features/events/services/event_service.dart';
import 'package:chipin_blogpost/features/events/views/create_events.dart';
import 'package:chipin_blogpost/features/events/views/created_events.dart';
import 'package:chipin_blogpost/features/events/views/join_events.dart';
import 'package:chipin_blogpost/features/events/views/joined_events.dart';
import 'package:chipin_blogpost/features/events/widgets/event_card.dart';
import 'package:flutter/material.dart';
import 'package:flutter_speed_dial/flutter_speed_dial.dart';

class HomePage extends StatefulWidget {
  const HomePage({Key? key}) : super(key: key);

  @override
  _HomePageState createState() => _HomePageState();
}

class HomePageLayout extends StatelessWidget {
  const HomePageLayout({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Column(
      crossAxisAlignment: CrossAxisAlignment.center,
      children: [
        const SizedBox(height: 15.0),
        Expanded(
          child: FutureBuilder<List<MyEventModel>>(
            future: EventService.getAllEvents(),
            builder: (context, snapshot) {
              if (snapshot.hasData) {
                final events = snapshot.data!;
                if (events.isEmpty) {
                  return const Center(
                    child: Text('No events to display'),
                  );
                }
                return CarouselSlider.builder(
                  itemCount: events.length,
                  itemBuilder: (context, index, _) {
                    return EventCard(event: events[index]);
                  },
                  options: CarouselOptions(
                    height: 685,
                    viewportFraction: 0.8,
                    enlargeCenterPage: true,
                    enableInfiniteScroll: true,
                    autoPlay: true,
                    autoPlayInterval: const Duration(seconds: 2),
                    autoPlayCurve: Curves.fastOutSlowIn,
                  ),
                );
              } else if (snapshot.hasError) {
                return Text('Error: ${snapshot.error}');
              } else {
                return const CircularProgressIndicator();
              }
            },
          ),
        ),
      ],
    );
  }
}

class _HomePageState extends State<HomePage> {
  int _selectedIndex = 0;

  final List<Widget> _widgetOptions = <Widget>[
    const HomePageLayout(),
    MyJoinedEventsScreen(eventService: EventService()),
    MyCreatedEventsScreen(eventService: EventService()),
  ];

  void _onItemTapped(int index) {
    setState(() {
      _selectedIndex = index;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        automaticallyImplyLeading: false,
        title: FutureBuilder<String>(
          future: AuthService.getUserName(),
          builder: (context, snapshot) {
            if (snapshot.hasData) {
              final userName = snapshot.data!;
              return Text('Welcome, $userName!');
            } else {
              return const Text('Home Page');
            }
          },
        ),
        actions: [
          PopupMenuButton(
            itemBuilder: (BuildContext context) {
              return [
                const PopupMenuItem(
                  value: 'logout',
                  child: Text('Logout'),
                ),
              ];
            },
            onSelected: (value) async {
              if (value == 'logout') {
                await AuthService.logout();
                Navigator.of(context).pushAndRemoveUntil(
                  MaterialPageRoute(
                    builder: (context) => const AuthMainView(),
                  ),
                  (route) => false,
                );
              }
            },
          ),
        ],
      ),
      body: _widgetOptions.elementAt(_selectedIndex),
      bottomNavigationBar: BottomNavigationBar(
        items: const <BottomNavigationBarItem>[
          BottomNavigationBarItem(
            icon: Icon(Icons.home),
            label: 'Home',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.people),
            label: 'Joined Events',
          ),
          BottomNavigationBarItem(
            icon: Icon(Icons.event),
            label: 'My Events',
          ),
        ],
        currentIndex: _selectedIndex,
        onTap: _onItemTapped,
      ),
      floatingActionButton: SpeedDial(
        animatedIcon: AnimatedIcons.menu_close,
        visible: true,
        curve: Curves.bounceIn,
        children: [
          SpeedDialChild(
            child: const Icon(Icons.add),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => EventCreationScreen(
                    eventService: EventService(),
                  ),
                ),
              );
            },
            label: 'Create Event',
          ),
          SpeedDialChild(
            child: const Icon(Icons.arrow_upward),
            onTap: () {
              Navigator.push(
                context,
                MaterialPageRoute(
                  builder: (context) => JoinEventsScreen(
                    eventService: EventService(),
                  ),
                ),
              );
            },
            label: 'Join Events',
          ),
        ],
      ),
    );
  }
}

The code imports necessary packages and defines a EventCard widget that represents a card displaying event information. The build() method constructs the card's UI, including formatting date and time. The card supports gesture detection for navigation to event details and includes an optional "Join" button. Overall, the code creates a compact and reusable widget for displaying event details in a visually appealing card format.

For a detailed explanation, check out HomePage Widget Explanation.

What does it look like?

Check out the screenshots on the Event Feature Notion Page.

Conclusion

Have you made it this far? You are amazing! We've done the authentication feature and then the event feature. Great job! The event creation and management feature seamlessly integrates with the existing authentication functionality, enhancing the user experience. Authenticated users can create and manage their own events, ensuring exclusivity and trust within the community.

Authenticated users have the privilege to create and manage events while maintaining a sense of security. Overall, the integration of these features strengthens the app's functionality. Everything currently appears in the default color and design. In our upcoming blog post, we will be showcasing the Appwrite Pink Design and demonstrating how to integrate the colors by creating a new class called Palette. This series is nearing its end, so stay tuned for our next blog!

Additional Resources and Support

  1. Appwrite's Official Documentation: To understand more about Appwrite's various features, you can visit Appwrite's Official Documentation. Here, you'll find detailed explanations about every feature and how to use them effectively.

  2. Flutter's Official Documentation: Visit Flutter's Official Documentation for more details about Flutter and its features, which can help you while creating the Flutter project.

  3. Appwrite's Discord Channel: If you face any issues or have any queries regarding Appwrite, Appwrite's Discord Channel is the place to go. You'll find a healthy community ready to assist you.

  4. Flutter's Community: If you have questions or face difficulties related to Flutter, you can reach out to Flutter's community. Various channels are available, including a Google group, a subreddit, and a Discord channel.

Remember, there's no need to feel stuck. Appwrite's and Flutter's communities are friendly, informative, and ready to help newcomers to the technology. Happy coding!

Β