Unlock Your Learning Potential with Skillify: A Journey through the Ultimate Upskilling Platform

"Empower Your Learning Journey with skillify: Where Innovation Meets Education"

Β·

22 min read

Unlock Your Learning Potential with Skillify: A Journey through the Ultimate Upskilling Platform

Introduction

In an era driven by technology and innovation, continuous learning and upskilling have become essential for personal and professional growth. The need for easily accessible, engaging, and comprehensive learning resources has never been greater. That's where Skillify comes into play, a revolutionary Flutter-based application that's here to transform your learning experience.

In this blog, we'll take you on a journey through the exciting world of skillify, explaining how it leverages Flutter for a seamless user experience, Supabase for robust database management and authentication, and the power of Outerbase commands for efficient CRUD operations. Get ready to discover the app that will supercharge your development skills while providing an enriching community experience.

The Genesis of Skillify

Flutter: The Engine Behind the Elegance

Before we dive into the heart of skillify, let's talk about the technology that powers its seamless and elegant user interface: Flutter. Flutter, an open-source UI software development toolkit from Google, is renowned for creating natively compiled applications for mobile, web, and desktop from a single codebase.

With Flutter, skillify achieves a visually stunning and responsive design that caters to both Android and iOS users. It ensures that the user experience is consistent, intuitive, and engaging across different devices, making learning a joyous experience.

Supabase: A Database and Authentication Marvel

Supabase, the next-generation open-source Firebase alternative, serves as the backbone of skillify. It excels in managing database operations and user authentication, ensuring data security and reliability.

With Supabase, you can trust that your learning progress is stored safely, and your personal information is protected. The authentication system seamlessly integrates with the app, providing users with a hassle-free login experience.

The Heart of Skillify

What exactly is Outerbase?

Outerbase is the interface for your database. It modernizes the tooling around databases that allows you and your entire team to access their data at any time, in a secure, cloud environment.

Outerbase simplifies database work and boosts efficiency. You can collaborate with your team in real-time to speed up tasks. If you are also not familiar with SQL like me then no worries Outerbase has an AI tool called EZQL. With EZQL, our natural language to SQL agent, querying data is straightforward. You have the option to save these queries for later use. Creating easy-to-read charts is also a breeze. Plus, you can automate your workflows with our Commands.

Outerbase Commands: Your Bridge to Data Magic

Now, let's get into the nitty-gritty of how skillify efficiently manages and delivers course materials, exams, and more. Enter Outerbase commandsβ€”a smart and efficient way to perform CRUD (Create, Read, Update, Delete) operations.

Outerbase Commands introduce a versatile approach to streamlining various tasks within the Outerbase environment. These commands are built on the foundation of WebAssembly, which grants you the flexibility to write them in your preferred programming language. For skillify I've used SQL which is one of their offerings to execute database queries and manipulations.

Learning Redefined

Small note: In Skillfy, we have features such as authentication, courses, exams, chat, leaderboards, etc. For courses and exams, I had to create multiple nodes to handle their functionalities. During that time, I wasn't very proficient with Outerbase commands, so I used Supabase for those features and Outerbase commands for the others.

πŸ” Admin-Only Access

To maintain the quality of content and user experience, we've implemented an admin-only access system. Admins have the power to add new courses, exams, and materials, and keep the platform fresh and exciting.

Diverse Courses for Every Aspiring Learner πŸ“š

Skillify prides itself on offering a diverse range of courses, each meticulously curated to empower learners with the latest skills and knowledge. Whether you're a budding programmer, a seasoned developer, or someone entirely new to the field, there's something here for you.

Here's the code snippet for adding a course.

  Future<void> addCourse(Course course) async {
    try {
      final user = _client.auth.currentUser;
      if (user == null) {
        throw const ServerException(
          message: 'User is not authenticated',
          statusCode: '401',
        );
      }


      var courseModel = (course as CourseModel).copyWith(
        id: const Uuid().v1(),
        groupId: const Uuid().v1(),
      );
      if (courseModel.imageIsFile) {
        final imagePath = 'courses/${courseModel.id}/profile_image'
            '/${courseModel.title}-pfp';
        final imageRef = await _dbClient
            .from('courses')
            .upload(
              imagePath,
              File(courseModel.image!),
              fileOptions: const FileOptions(
                upsert: true,
              ),
            )
            .then((value) async {
          final url = _dbClient.from('courses').getPublicUrl(imagePath);
          courseModel = courseModel.copyWith(image: url);
        });
      }
      await _client.from('courses').upsert(courseModel.toMap());

      final group = GroupModel(
        id: courseModel.groupId,
        name: course.title,
        courseId: courseModel.id,
        members: const [],
        groupImageUrl: courseModel.image,
      );
      return await _client.from('groups').upsert(group.toMap());
    } on StorageException catch (e) {
      throw ServerException(
        message: e.message ?? 'Unknown error occured',
        statusCode: e.statusCode,
      );
    } on ServerException {
      rethrow;
    } catch (e) {
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

Summary of the code:

  1. The addCourse function takes a Course object as an argument and is marked as asynchronous (async).

  2. Inside the function, it first attempts to check if the user is authenticated. If the user is not authenticated, it throws a ServerException with a message indicating that the user is not authenticated.

  3. The code generates unique identifiers (id and groupId) for the course using the Uuid package. These identifiers are then assigned to the course.

  4. If the course has an image (courseModel.imageIsFile is true), it uploads the image file to the bucket. The uploaded image's URL is then retrieved and associated with the course.

  5. The course data (including the image URL if applicable) is then upserted (a database operation that inserts or updates data) into the database.

  6. A GroupModel object is created with information related to the course, such as its name and image URL. This GroupModel is also upserted into the database.

  7. Error handling is included: If there is a storage-related error, it catches a StorageException and translates it into a ServerException. If there is a ServerException, it rethrows it. Any other exceptions are caught and converted into a ServerException as well.

In summary, this code takes a course object, checks if the user is authenticated, uploads an image if available, and then stores the course and related group data in the Postgres database, handling potential errors along the way.

Snippet for getting all the courses:

  Future<List<CourseModel>> getCourses() async {
    try {
      await DataSourceUtils.authorizeUser(_client);

      final url = Uri.parse(
        'https://comparative-turquoise.cmd.outerbase.io/getcourses',
      );

      final res = await http.get(url);
      if (res.statusCode == 200) {
        final jsonResponse = json.decode(res.body) as DataMap;
        if (jsonResponse['success'] == true) {
          final items = jsonResponse['response']['items'] as List;
          final courses = items
              .map((data) => CourseModel.fromMap(data as DataMap))
              .toList();
          return courses;
        }
      }
    } catch (e, s) {
      print(e);
      debugPrintStack(stackTrace: s);
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

For getting all the courses I've used the powerful Outerbase commands. Here's the SQL query that I've used to get the courses.

SELECT * FROM public.courses;

Summary:

  1. The getCourses function is marked as asynchronous (async) and returns a Future that resolves to a list of CourseModel objects.

  2. Inside the function, it first attempts to authorize the user by calling the authorizeUser method from the DataSourceUtils class (not shown in the provided code). This step presumably ensures that the user has the necessary permissions to access the data source.

  3. It constructs a URL (url) pointing to a remote server endpoint where the course data is expected to be available.

  4. The code then sends an HTTP GET request to the specified URL using the http package. The response is stored in the res variable.

  5. It checks if the HTTP response status code is 200, which indicates a successful request (i.e., "OK").

  6. If the response status code is 200, it parses the JSON response body using the json.decode method and assumes it's a map (DataMap) containing data.

  7. It checks if the 'success' field in the JSON response is true. If it is, it extracts the 'items' field, which is expected to be a list of course data.

  8. It then maps the individual course data items to CourseModel objects using the map function and CourseModel.fromMap constructor, creating a list of courses.

  9. The function returns the list of courses as a result.

  10. Error handling is included: If any exceptions are caught during the execution of this function, they are logged using print and debugPrintStack, and then a ServerException is thrown with a message indicating the error, along with a status code '505' to represent a server error.

In summary, this code fetches a list of courses from a remote server, handles success and error cases, and returns the list of courses as a result. It also includes error logging and exception handling to gracefully handle unexpected issues during the data retrieval process.

While exploring the incredible features of skillify, let's delve deeper into the Courses section. Here, we provide you with not just courses but a treasure trove of learning materials and videos.

πŸ“½οΈ Add Videos

Videos are a powerful medium for learning. That's why we've made it incredibly easy for course creators, and admins to enrich your learning experience. They can effortlessly add videos to each course, transforming complex topics into engaging visual lessons.

Whether it's a step-by-step coding tutorial, a hands-on demonstration of a concept, or an inspiring talk from an industry expert, our video feature covers it all. Imagine having the world's best instructors at your fingertips, ready to guide you through your learning journey.

  Future<void> addVideo(Video video) async {

    try {
      await DataSourceUtils.authorizeUser(_client);

      var videoModel = (video as VideoModel).copyWith(
        id: const Uuid().v1(),
      );
      if (videoModel.thumbnailIsFile) {
        final imagePath =
            'courses/${video.courseId}/videos/${video.id}/thumbnail';
        final imageRef = await _dbClient
            .from('courses')
            .upload(
              imagePath,
              File(videoModel.thumbnail!),
              fileOptions: const FileOptions(
                upsert: true,
              ),
            )
            .then((value) async {
          final url = _dbClient.from('courses').getPublicUrl(imagePath);
          videoModel = videoModel.copyWith(thumbnail: url);
        });
      }
      await _client.from('videos').upsert(videoModel.toMap());

      final response = await _client
          .from('courses')
          .select('numberOfVideos')
          .eq('id', video.courseId)
          .single()
          .execute();
      final currentNumberOfVideos = response.data!['numberOfVideos'];

      final updateResponse = await _client.from('courses').update({
        'numberOfVideos': currentNumberOfVideos + 1,
      }).eq('id', video.courseId);
    } on StorageException catch (e) {
      throw ServerException(
        message: e.message ?? 'Unknown error occured',
        statusCode: e.statusCode,
      );
    } on ServerException {
      rethrow;
    } catch (e) {
      print(e);
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

Summary:

  1. The addVideo function is marked as asynchronous (async) and doesn't return a specific value, indicating that it performs some operations but doesn't return any result.

  2. It starts by trying to authorize the user using the authorizeUser method from the DataSourceUtils class (not shown in the provided code). This step is presumably to ensure that the user has the necessary permissions.

  3. The function takes a Video object as an argument, which is then cast to a VideoModel. A unique identifier (id) is generated for the video using the Uuid package and assigned to the video model.

  4. If the video has a thumbnail (videoModel.thumbnailIsFile is true), it uploads the thumbnail image to the stroage bucket. The uploaded image's URL is then retrieved and associated with the video.

  5. The video model, including the thumbnail URL if applicable, is upserted (inserted or updated) into database using _client.from('videos').upsert(videoModel.toMap()).

  6. The code fetches the current number of videos associated with the course that this video belongs to. It queries the database using _client.from('courses'), filters by the course's id, and retrieves the 'numberOfVideos' field.

  7. It then updates the course's 'numberOfVideos' by incrementing it by 1 and performs an update operation on the course data in the data store.

  8. Error handling is included: If there is a storage-related error, it catches a StorageException and translates it into a ServerException. If there is a ServerException, it rethrows it. Any other exceptions are logged using print, and a ServerException is thrown with a message indicating the error and a status code '505' to represent a server error.

In summary, this code adds a video to some data storage, updates the associated course's video count, and handles potential errors along the way. It also includes error logging and exception handling to gracefully handle unexpected issues during the video addition process.

  Future<List<VideoModel>> getVideos(String courseId) async {
    try {
      await DataSourceUtils.authorizeUser(_client);

      final url = Uri.parse(
        'https://comparative-turquoise.cmd.outerbase.io/getvidoes?courseId=$courseId',
      );

      final res = await http.get(url);
      if (res.statusCode == 200) {
        final jsonResponse = json.decode(res.body) as DataMap;
        if (jsonResponse['success'] == true) {
          final items = jsonResponse['response']['items'] as List;
          final videos = items
              .map((data) => VideoModel.fromMap(data as DataMap))
              .toList();
          return videos;
        }
      }
    } catch (e, s) {
      print(e);
      debugPrintStack(stackTrace: s);
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

Here again, we are using Outerbase commands

  1. It authorizes the user's access using DataSourceUtils.authorizeUser(_client).

  2. Constructs a URL to the remote server endpoint, including the courseId as a query parameter.

  3. Sends an HTTP GET request to the specified URL and checks if the response status code is 200 (OK).

  4. If the response is successful, it parses the JSON response, extracts the video data, converts it to VideoModel objects, and returns them as a list.

  5. It includes error handling: If there are exceptions during execution, it logs the error, prints the stack trace, and throws a ServerException with a status code '505' to indicate a server error.

πŸ“š Attach Learning Materials

In addition to videos, we understand the importance of having comprehensive learning materials. For every course on skillify, you'll find a dedicated section where you can access essential reading materials, e-books, PDFs, articles, and more.

These materials provide you with the background knowledge, references, and in-depth explanations you need to truly grasp the subject matter. Whether you prefer visual, auditory, or text-based learning, we've got you covered.

The code is quite similar, except that this time I’ve used Supabase to fetch all the materials because I was encountering some errors with Outerbase commands. Therefore, I won’t bore you by explaining it again. πŸ˜… If you have any doubts, please let me know in the comments. I’ll be happy to help you.

Future<void> addMaterial(Resource material) async {

    try {
      await DataSourceUtils.authorizeUser(_client);

      var materialModel =
          (material as ResourceModel).copyWith(id: const Uuid().v1());
      final id = materialModel.id;

      if (material.isFile) {
        final materialFilePath =
            'courses/${material.courseId}/materials/$id/material';
        final materialRef = await _dbClient
            .from('courses')
            .upload(
              materialFilePath,
              File(materialModel.fileURL),
              fileOptions: const FileOptions(
                upsert: true,
              ),
            )
            .then((value) async {
          final url = _dbClient.from('courses').getPublicUrl(materialFilePath);
          materialModel = materialModel.copyWith(fileURL: url);
        });
      }

      await _client.from('materials').upsert(materialModel.toMap());

      final response = await _client
          .from('courses')
          .select('numberOfMaterials')
          .eq('id', material.courseId)
          .single()
          .execute();
      final currentNumberOfMaterials = response.data!['numberOfMaterials'];

      final updateResponse = await _client.from('courses').update({
        'numberOfMaterials': currentNumberOfMaterials + 1,
      }).eq('id', material.courseId);
    } on StorageException catch (e) {
      throw ServerException(
        message: e.message ?? 'Unknown error occured',
        statusCode: e.statusCode,
      );
    } on ServerException {
      rethrow;
    } catch (e) {
      print(e);
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

  @override
  Future<List<ResourceModel>> getMaterials(String courseId) async {
    try {
      await DataSourceUtils.authorizeUser(_client);
      final response = await _client
          .from('materials')
          .select()
          .eq('courseId', courseId)
          .execute();
      return (response.data as List)
          .map((material) => ResourceModel.fromMap(material as DataMap))
          .toList();
    } on StorageException catch (e) {
      throw ServerException(
        message: e.message ?? 'Unknown error occured',
        statusCode: e.statusCode,
      );
    } on ServerException {
      rethrow;
    } catch (e) {
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

Interactive Exams: Test Your Knowledge πŸ“

Learning is not just about absorbing information; it's also about applying it. Skillify takes this to heart, offering interactive exams that challenge your understanding and help you grow.

The timer ticks down as you answer questions that gauge your comprehension of the material. Receive instant feedback and track your progress, all within the app. It's a fantastic way to ensure that your newly acquired skills are rock solid.

For this feature, I have not used the Outerbase commands as I've said previously. That's why I'm not going to paste 300 lines of code here πŸ˜… if you want you can check that from the GitHub repo link attached below. I'll try to write these again using commands as now I find this pretty easy. One thing to note currently exam files can be only uploaded in JSON format obviously by the admins.

Building a Thriving Learning Community

Engaging Chatrooms: Connect and Collaborate πŸ’¬

Skillify isn't just about solo learningβ€”it's about building connections with like-minded individuals from around the world. Our vibrant community chatrooms are the perfect place to engage in discussions, share insights, and collaborate on projects.

When a course is added a group(where folks can chat) is also created automatically.

Joining a group:

UPDATE public.groups SET members = array_cat(members, ARRAY['{{request.body.userId}}']) WHERE id = '{{request.body.id}}'

As I've said previously I'm not that familiar with SQL so I used the great EZQL to generate this SQL node for me. Sorry I can't share the prompt cause it got deleted it's fairly simple as chatGPT.

  Future<void> joinGroup({
    required String groupId,
    required String userId,
  }) async {
    try {
      await DataSourceUtils.authorizeUser(_client);
      final data = {
        'userId': userId,
        'id': groupId,
      };
      final res = await http.put(
        Uri.parse('https://comparative-turquoise.cmd.outerbase.io/joinGroup'),
        headers: {
          'content-type': 'application/json',
        },
        body: jsonEncode(data),
      );
      if (res.statusCode == 200) {
        print('joined group');
      }
      final response = await _client
          .from('users')
          .select('groupIds')
          .eq('id', userId)
          .execute();

      final userDocument = response.data as List<dynamic>;

      final existingGroups =
          userDocument.isNotEmpty ? userDocument[0]['groupIds'] : [];

      existingGroups.add(groupId);

      final updateResponse = await _client.from('users').upsert([
        {
          'id': userId,
          'groupIds': existingGroups,
        }
      ]).execute();
    } on PostgrestException catch (e) {
      print(e.message);
      throw ServerException(message: e.message, statusCode: e.code);
    } on ServerException {
      rethrow;
    } catch (e, s) {
      print(e);
      debugPrintStack(stackTrace: s);
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

Summary:

  1. It's an asynchronous function that takes two required parameters: groupId (the ID of the group to join) and userId (the ID of the user who wants to join).

  2. It first attempts to authorize the user's access using DataSourceUtils.authorizeUser(_client).

  3. It constructs a JSON payload data containing the userId and groupId.

  4. The code sends an HTTP PUT request to a remote endpoint ( for joining a group), including the JSON payload in the request body.

  5. If the HTTP response status code is 200 (OK), it prints "joined group" to the console, indicating a successful group join.

  6. It then queries the user's data using _client.from('users'), specifically selecting the 'groupIds' field for the specified userId.

  7. It retrieves the user's existing group IDs, adding the newly joined group's ID to the list.

  8. It updates the user's data in the data store with the modified 'groupIds' field.

  9. Error handling is included: If there is a PostgrestException (possibly related to the HTTP request), it prints the exception message and throws a ServerException with the exception's message and code. If there is a ServerException, it rethrows it. For all other exceptions, it logs the error, prints the stack trace, and throws a ServerException with a message indicating the error and a status code '505' to represent a server error.

In summary, this code allows a user to join a group by sending an HTTP PUT request to a server, updating the user's data with the newly joined group, and handling various error scenarios along the way.

Sending messages:

INSERT INTO public.messages (id, sender_id, group_id, timestamp, message) VALUES (uuid_generate_v4(), '{{request.body.sender_id}}', '{{request.body.group_id}}', current_timestamp, '{{request.body.message}}')

I've used this SQL node to create the command.

  Future<void> sendMessage(Message message) async {
    try {
      await DataSourceUtils.authorizeUser(_client);
      final messageData = {
        'group_id': message.groupId,
        'sender_id': _client.auth.currentUser!.id,
        'message': message.message,
      };
      final res = await http.post(
        Uri.parse('https://comparative-turquoise.cmd.outerbase.io/messages'),
        headers: {
          'content-type': 'application/json',
        },
        body: jsonEncode(messageData),
      );

      if (res.statusCode == 200) {
        print('message sent');
      } else {
        print(
          'Failed sending messages. Status code: ${res.statusCode}, '
          'Response body: ${res.body}',
        );
      }
      final userName = await _client
          .from('users')
          .select('name')
          .eq('id', _client.auth.currentUser!.id);

      await _client.from('groups').update({
        'lastMessage': message.message,
        'lastMessageSenderName': userName,
        'lastMessageTimeStamp': message.timestamp.toIso8601String(),
      }).eq('id', message.groupId);
    } catch (e, s) {
      print(e);
      debugPrintStack(stackTrace: s);
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

Summary:

This function works similarly to the previous function. The main difference is that it sends an HTTP post request to send messages. After the message is sent successfully it updates the group table's lastMessage, lastMessageSenderName, lastMessageTimeStamp column with the latest message, message sender name and last message timestamp value. If any error occurs it prints it in the console.

Get messages:

SELECT * FROM public.messages WHERE group_id = '{{request.query.group_id}}' ORDER BY timestamp DESC

I used this node to create the get messages command.

  Stream<List<MessageModel>> getMessages(String groupId) async* {
    try {
      await DataSourceUtils.authorizeUser(_client);

      final url = Uri.parse(
        'https://comparative-turquoise.cmd.outerbase.io/getmessages?groupId=$groupId',
      );

      final res = await http.get(url);
      if (res.statusCode == 200) {
        final jsonResponse = json.decode(res.body) as DataMap;
        if (jsonResponse['success'] == true) {
          final items = jsonResponse['response']['items'] as List;
          final messages = items
              .map((data) => MessageModel.fromMap(data as DataMap))
              .toList();
          yield messages;
        }
      }
    } catch (e, s) {
      print(e);
      debugPrintStack(stackTrace: s);
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

Summary:

  1. It's an asynchronous generator function that takes a single parameter groupId (the ID of the group for which messages are requested).

  2. It first attempts to authorize the user's access using DataSourceUtils.authorizeUser(_client).

  3. It constructs a URL (url) to a remote server endpoint, including the groupId as a query parameter, indicating the group whose messages are requested.

  4. The code sends an HTTP GET request to the specified URL using the http package.

  5. If the HTTP response status code is 200 (OK), it parses the JSON response, assuming it's a map (DataMap) containing data.

  6. It checks if the 'success' field in the JSON response is true. If it is, it extracts the 'items' field, which is expected to be a list of message data.

  7. It maps the individual message data items to MessageModel objects using the map function and MessageModel.fromMap constructor, creating a list of messages.

  8. It uses the yield keyword to emit the list of messages as a stream.

  9. Error handling is included: If there are exceptions during execution, it prints the error, prints the stack trace using debugPrintStack, and throws a ServerException with a message indicating the error and a status code '505' to represent a server error.

In summary, this code provides a stream of messages associated with a specific group ID from a remote server. It uses asynchronous generators to yield messages as they are retrieved and handles potential errors during the retrieval process.

Get all groups:

SELECT * FROM groups;
  Stream<List<GroupModel>> getGroups() async* {
    try {
      await DataSourceUtils.authorizeUser(_client);
      final response = await http.get(
        Uri.parse('https://comparative-turquoise.cmd.outerbase.io/groups'),
      );
      if (response.statusCode == 200) {
        final responseData = jsonDecode(response.body) as Map<String, dynamic>;

        if (responseData.containsKey('response') &&
            responseData['response'] is Map<String, dynamic> &&
            responseData['response']['items'] is List) {
          final data = responseData['response']['items'] as List<dynamic>;

          final groups = data.map((item) {
            return GroupModel.fromMap(item as DataMap);
          }).toList();

          yield groups;
        } else {
          throw Exception('Invalid response data format');
        }
      } else {
        throw Exception('Failed to fetch groups: ${response.body}');
      }
    } catch (e, s) {
      print(e);
      debugPrintStack(stackTrace: s);
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

I'm not explaining this as it's similar to the above one.

Leave a group:

UPDATE public.groups SET members = array_remove(members, '{{request.body.userId}}') WHERE id = '{{request.body.id}}'
Future<void> leaveGroup({
  required String groupId,
  required String userId,
}) async {
  try {
    await DataSourceUtils.authorizeUser(_client);
    final data = {
      'userId': userId,
      'id': groupId,
    };
    final res = await http.put(
      Uri.parse('https://comparative-turquoise.cmd.outerbase.io/leavegroups'),
      headers: {
        'content-type': 'application/json',
      },
      body: jsonEncode(data),
    );
    if (res.statusCode == 200) {
      print('left group');
    }
    final response = await _client
        .from('users')
        .select('groupIds')
        .eq('id', userId)
        .execute();

    final userDocument = response.data as List<dynamic>;

    final existingGroups =
        userDocument.isNotEmpty ? userDocument[0]['groupIds'] : [];

    existingGroups.remove(groupId);

    final updateResponse = await _client.from('users').upsert([
      {
        'id': userId,
        'groupIds': existingGroups,
      }
    ]).execute();
  } on PostgrestException catch (e) {
    print(e.message);
    throw ServerException(message: e.message, statusCode: e.code);
  } on ServerException {
    rethrow;
  } catch (e, s) {
    print(e);
    debugPrintStack(stackTrace: s);
    throw ServerException(message: e.toString(), statusCode: '505');
  }
}

Summary:

  1. It's an asynchronous function that takes two required parameters: groupId (the ID of the group to leave) and userId (the ID of the user who wants to leave).

  2. It first attempts to authorize the user's access using DataSourceUtils.authorizeUser(_client).

  3. It constructs a JSON payload data containing the userId and groupId.

  4. The code sends an HTTP PUT request to a remote endpoint (presumably for leaving a group), including the JSON payload in the request body.

  5. If the HTTP response status code is 200 (OK), it prints "left group" to the console, indicating a successful group leave.

  6. It then queries the user's data using _client.from('users'), specifically selecting the 'groupIds' field for the specified userId.

  7. It retrieves the user's existing group IDs and removes the ID of the group that the user is leaving from the list.

  8. It updates the user's data in the data store with the modified 'groupIds' field.

  9. Error handling is included: If there is a PostgrestException (possibly related to the HTTP request), prints the exception message and throws a ServerException with the exception's message and code. If there is a ServerException, it rethrows it. For all other exceptions, it logs the error, prints the stack trace, and throws a ServerException with a message indicating the error and a status code '505' to represent a server error.

In summary, this code allows a user to leave a group by sending an HTTP PUT request to a server, updating the user's data to reflect the group left, and handling various error scenarios along the way.

Leaderboard: Fueling Your Competitive Spirit πŸ†

For those who thrive on competition, skillify offers a dynamic leaderboard. See where you stand among your peers, and strive to climb to the top. It's not just about acquiring knowledge; it's about mastering it.

We are currently displaying the top 10 folks on the leaderboard according to their points which they will get after completing an exam.

SELECT * FROM public.users ORDER BY points DESC LIMIT 10
  Future<List<LocalUserModel>> getTopLearners() async {
    try {
      await DataSourceUtils.authorizeUser(_client);
      final res = await http.get(
        Uri.parse('https://comparative-turquoise.cmd.outerbase.io/toplearners'),
      );
      if (res.statusCode == 200) {
        final data = jsonDecode(res.body)['response']['items'] as List<dynamic>;
        return data.map((e) => LocalUserModel.fromMap(e as DataMap)).toList();
      } else {
        throw ServerException(
          message: 'Unexpected status code ${res.statusCode}',
          statusCode: res.statusCode.toString(),
        );
      }
    } catch (e, s) {
      print(e);
      debugPrintStack(stackTrace: s);
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

Real-Time Notifications

Stay in the Loop

In the fast-paced world of technology, staying up-to-date is crucial. Skillify ensures that you're never left behind with real-time notifications. Get instant alerts whenever new courses, exams, or videos are added. It's your ticket to being at the forefront of knowledge.

Get notifications:

  Stream<List<NotificationModel>> getNotifications() {
    try {
      DataSourceUtils.authorizeUser(_client);

         final res = _client
          .from('notifications')
          .stream(primaryKey: ['id'])

          .order('sentAt')
          .execute()
          .map(
            (dataList) => dataList
                .map(
                  NotificationModel.fromMap,
                )
                .toList(),
          );

      return res;
    } on PostgrestException catch (e) {
      return Stream.error(
        ServerException(
          message: e.message ?? 'Unknown error occurred',
          statusCode: e.code,
        ),
      );
    } on ServerException catch (e) {
      return Stream.error(e);
    } catch (e) {
      print(e);
      return Stream.error(
        ServerException(message: e.toString(), statusCode: '505'),
      );
    }
  }

Here I've used Supbase realtime API to get notifications in realtime. I was trying to use commands first but I had to restart the app to get new notifications so I switched to Supabase. I'm also leaving my command code below maybe you can find a solution and tell me or you can work with that solution.

  Stream<List<NotificationModel>> getNotifications() async* {
    try {
      await DataSourceUtils.authorizeUser(_client);

      final url = Uri.parse(
        'https://comparative-turquoise.cmd.outerbase.io/getnotifications',
      );

      final res = await http.get(url);
      if (res.statusCode == 200) {
        final jsonResponse = json.decode(res.body) as DataMap;
        if (jsonResponse['success'] == true) {
          final items = jsonResponse['response']['items'] as List;
          final notifications = items
              .map((data) => NotificationModel.fromMap(data as DataMap))
              .toList();
          yield notifications;
        }
      }
    } catch (e, s) {
      print(e);
      debugPrintStack(stackTrace: s);
      throw ServerException(message: e.toString(), statusCode: '505');
    }
  }

Mark as read:

UPDATE public.notifications SET seen = true WHERE id = '{{request.body.id}}'
  Future<void> markAsRead(String notificationId) async {
    try {
      await DataSourceUtils.authorizeUser(_client);
      final data = {
        'id': notificationId,
      };
      final res = await http.put(
        Uri.parse('https://comparative-turquoise.cmd.outerbase.io/markasread'),
        headers: {
          'content-type': 'application/json',
        },
        body: jsonEncode(data),
      );
      if (res.statusCode == 200) {
        print('marked as read');
      }
    } on PostgrestException catch (e) {
      throw ServerException(message: e.message, statusCode: e.code);
    } on ServerException {
      rethrow;
    } catch (e) {
      throw ServerException(message: e.toString(), statusCode: '500');
    }
  }

Clear:

DELETE FROM public.notifications WHERE id = '{{request.body.id}}'
Future<void> clear(String notificationId) async {
  try {
    await DataSourceUtils.authorizeUser(_client);

   final data = {
        'id': notificationId,
      };

    final Uri uri = Uri.parse('https://comparative-turquoise.cmd.outerbase.io/clearnotification');

    final res = await http.delete(
      uri,
      headers: {
        'content-type': 'application/json',
      },

       body: jsonEncode(data),
    );

    if (res.statusCode == 200) {
      print('Notification deleted successfully');
    } 
  } on PostgrestException catch (e) {
    throw ServerException(message: e.message, statusCode: e.code);
  } on ServerException {
    rethrow;
  } catch (e) {
    throw ServerException(message: e.toString(), statusCode: '500');
  }
}

Github repo link: here

Demo video: here

A Visual Treat with Rive and Lottie

Animations That Delight

The aesthetics of an app play a significant role in user engagement, and skillify doesn't disappoint. It leverages the power of Rive and Lottie animations to make your learning experience not just educational but visually delightful as well.

Conclusion

As we draw the curtain on this exploration of skillify, we want to express our profound gratitude to Hashnode and Outerbase for providing us with this incredible opportunity. Participating in this event throughout this entire September has been an enriching learning experience and a true privilege.

With skillify, we've aimed to redefine the way you learn, grow, and connect in the digital age. It's not just an app; it's a gateway to endless possibilities, a hub of knowledge, and a vibrant community that thrives on curiosity and collaboration.

If you have any questions, please let me know in the comments; I would love to help you. Share your valuable feedback in the comments section below.

Β