diff --git a/lib/main.dart b/lib/main.dart index 0739f58..cf18c98 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -29,6 +29,7 @@ class Academia extends StatelessWidget { Get.put(RewardController()); Get.put(CoursesController()); Get.put(EventsController()); + Get.put(OrganizationController()); LocalNotifierService().requestPermission(); return GetMaterialApp( @@ -36,11 +37,11 @@ class Academia extends StatelessWidget { useMaterial3: true, colorSchemeSeed: Colors.amber, ), - darkTheme: ThemeData( - brightness: Brightness.dark, - useMaterial3: true, - colorSchemeSeed: Colors.amber, - ), + // darkTheme: ThemeData( + // brightness: Brightness.dark, + // useMaterial3: true, + // colorSchemeSeed: Colors.amber, + // ), home: Obx( () => userController.isLoggedIn.value ? const LayoutPage() diff --git a/lib/models/core/settings/settings_model.freezed.dart b/lib/models/core/settings/settings_model.freezed.dart index 023dc36..7543853 100644 --- a/lib/models/core/settings/settings_model.freezed.dart +++ b/lib/models/core/settings/settings_model.freezed.dart @@ -205,7 +205,7 @@ class _$SettingsImpl implements _Settings { this.showAudit = false, this.showExamTimetable = false, this.passcode = "", - this.primaryColor = "#FFFFFF"}); + this.primaryColor = "##72D5E0"}); factory _$SettingsImpl.fromJson(Map json) => _$$SettingsImplFromJson(json); diff --git a/lib/models/core/settings/settings_model.g.dart b/lib/models/core/settings/settings_model.g.dart index ccfb9b4..6288ef5 100644 --- a/lib/models/core/settings/settings_model.g.dart +++ b/lib/models/core/settings/settings_model.g.dart @@ -15,7 +15,7 @@ _$SettingsImpl _$$SettingsImplFromJson(Map json) => showAudit: json['showAudit'] as bool? ?? false, showExamTimetable: json['showExamTimetable'] as bool? ?? false, passcode: json['passcode'] as String? ?? "", - primaryColor: json['primaryColor'] as String? ?? "#FFFFFF", + primaryColor: json['primaryColor'] as String? ?? "##72D5E0", ); Map _$$SettingsImplToJson(_$SettingsImpl instance) => diff --git a/lib/storage/schemas.dart b/lib/storage/schemas.dart index 33671bf..a56a12d 100644 --- a/lib/storage/schemas.dart +++ b/lib/storage/schemas.dart @@ -68,34 +68,6 @@ const schemas = { awarded_at TEXT NOT NULL ); """, - - // Organizations - "organizations": """ - CREATE TABLE IF NOT EXISTS organizations ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - phone TEXT, - email TEXT, - description TEXT, - profile TEXT, - date_added TEXT - ); - """, - - // Stories - "stories": """ - CREATE TABLE IF NOT EXISTS stories ( - id TEXT PRIMARY KEY, - organization TEXT NOT NULL, - hex_code TEXT, - text TEXT, - media TEXT, - viewed INTEGER, - date_added TEXT, - date_of_expiry TEXT - ); - """, - // todos "todos": """ CREATE TABLE IF NOT EXISTS todos ( diff --git a/lib/tools/chirp/pages/chirp_home_page.dart b/lib/tools/chirp/pages/chirp_home_page.dart index 4a9887b..0fb1eeb 100644 --- a/lib/tools/chirp/pages/chirp_home_page.dart +++ b/lib/tools/chirp/pages/chirp_home_page.dart @@ -44,40 +44,26 @@ class ChirpHomePage extends StatelessWidget { icon: const Icon(Ionicons.flame_outline), ), ], - expandedHeight: 300, - flexibleSpace: FlexibleSpaceBar( - background: Container( - decoration: const BoxDecoration( - image: DecorationImage( - image: AssetImage( - "assets/images/sketchbook-passersby-people-working-around-1.png", - ), - ), - ), - ), - ), + expandedHeight: 200, pinned: true, floating: true, snap: true, - bottom: const TabBar( - tabs: [ - Tab(text: 'Trending'), - Tab(text: "Your Posts"), - Tab(text: "Organizations"), - ], - ), - ), - SliverVisibility( - visible: false, - sliver: SliverPersistentHeader( - floating: true, - delegate: - PersistentStorySliverDelegate(child: const SizedBox()), - ), + bottom: PreferredSize( + preferredSize: Size(MediaQuery.of(context).size.width, 140), + child: const Column( + children: [ + StoryHeader(), + TabBar(tabs: [ + Tab(text: 'Trending'), + Tab(text: "Your Posts"), + Tab(text: "Organizations"), + ]), + ], + )), ), const SliverFillRemaining( hasScrollBody: true, - fillOverscroll: true, + fillOverscroll: false, child: TabBarView( children: [ FeedPage(), diff --git a/lib/tools/chirp/pages/story_view_page.dart b/lib/tools/chirp/pages/story_view_page.dart new file mode 100644 index 0000000..3fb405f --- /dev/null +++ b/lib/tools/chirp/pages/story_view_page.dart @@ -0,0 +1,108 @@ +import 'package:academia/exports/barrel.dart'; +import 'package:story_view/story_view.dart'; + +class StoryViewPage extends StatefulWidget { + const StoryViewPage({ + super.key, + required this.organization, + required this.stories, + }); + + final Organization organization; + final List stories; + + @override + State createState() => _StoryViewPageState(); +} + +class _StoryViewPageState extends State { + final StoryController _storyController = StoryController(); + + List buildStoryItems(List stories) { + List storyItems = []; + for (final story in stories) { + switch (story.fileType) { + case "image": + storyItems.add( + StoryItem.pageImage( + url: "${ChirpService.urlPrefix}${story.media}", + controller: _storyController, + caption: Text( + story.description, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), + ), + loadingWidget: const CircularProgressIndicator.adaptive(), + ), + ); + + break; + case "video": + storyItems.add( + StoryItem.pageVideo( + "${ChirpService.urlPrefix}${story.media}", + controller: _storyController, + caption: Text( + story.description, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), + ), + loadingWidget: const CircularProgressIndicator.adaptive(), + ), + ); + break; + + case "text": + storyItems.add( + StoryItem.text( + title: story.description, + backgroundColor: + Theme.of(context).colorScheme.tertiaryContainer), + ); + break; + + default: + } + } + + return storyItems; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + extendBodyBehindAppBar: true, + appBar: AppBar( + backgroundColor: Colors.transparent, + title: Row( + children: [ + CircleAvatar( + backgroundImage: CachedNetworkImageProvider( + widget.organization.logo!, + ), + ), + const SizedBox(width: 8), + Text( + widget.organization.name, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: Colors.white, + ), + ), + ], + ), + ), + body: StoryView( + repeat: false, + onComplete: () { + Navigator.pop(context); + }, + indicatorColor: Theme.of(context).colorScheme.secondaryContainer, + indicatorForegroundColor: Theme.of(context).primaryColor, + storyItems: buildStoryItems(widget.stories), + controller: _storyController, + ), + ); + } +} diff --git a/lib/tools/chirp/widgets/story_header.dart b/lib/tools/chirp/widgets/story_header.dart index 3df619f..2e44ac7 100644 --- a/lib/tools/chirp/widgets/story_header.dart +++ b/lib/tools/chirp/widgets/story_header.dart @@ -1,40 +1,87 @@ import 'package:academia/exports/barrel.dart'; -import 'package:flutter/material.dart'; +import 'package:academia/tools/chirp/pages/story_view_page.dart'; +import 'package:get/get.dart'; -class PersistentStorySliverDelegate extends SliverPersistentHeaderDelegate { - PersistentStorySliverDelegate({ - required this.child, - this.numberofStories = 5, - }); - final Widget child; - int numberofStories; - @override - Widget build( - BuildContext context, double shrinkOffset, bool overlapsContent) { - return Container( - width: MediaQuery.of(context).size.width, - padding: const EdgeInsets.symmetric(horizontal: 4), - child: ListView.separated( - itemBuilder: (context, index) { - return const CircleAvatar( - radius: 45, - ); - }, - itemCount: numberofStories, - separatorBuilder: (context, index) => const SizedBox(width: 4), - scrollDirection: Axis.horizontal, - ), - ); - } +class StoryHeader extends StatelessWidget { + const StoryHeader({super.key}); @override - double get maxExtent => 120.0; // Height of the header + Widget build(BuildContext context) { + final OrganizationController organizationController = + Get.find(); + final UserController userController = Get.find(); - @override - double get minExtent => 100.0; // Height of the header when scrolled + return SizedBox( + height: 100, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: FutureBuilder( + future: + organizationController.fetchStories(userController.authHeaders), + builder: (context, snapshot) { + if (snapshot.connectionState != ConnectionState.done) { + return ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.horizontal, + itemBuilder: (context, index) => const CircleAvatar(radius: 35), + ); + } - @override - bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) { - return oldDelegate != this; + return Obx( + () => ListView.separated( + scrollDirection: Axis.horizontal, + itemCount: organizationController.stories.keys.length, + separatorBuilder: (context, index) => const SizedBox(width: 4), + itemBuilder: (context, index) { + final data = + organizationController.stories.keys.elementAt(index); + return Visibility( + visible: organizationController.stories.values + .elementAt(index) + .isNotEmpty, + child: GestureDetector( + onTap: () { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StoryViewPage( + organization: data, + stories: organizationController.stories.values + .elementAt(index), + ), + ), + ); + }, + child: Column( + children: [ + Badge( + backgroundColor: Colors.blue, + label: Text( + organizationController.stories.values + .elementAt(index) + .length + .toString(), + ), + child: CircleAvatar( + radius: 35, + backgroundImage: + CachedNetworkImageProvider(data.logo!), + ), + ), + Text( + data.name, + style: Theme.of(context).textTheme.bodySmall, + ), + ], + ), + ), + ); + }, + ), + ); + }, + ), + ), + ); } } diff --git a/lib/tools/organizations/controllers/organization_controller.dart b/lib/tools/organizations/controllers/organization_controller.dart new file mode 100644 index 0000000..edcc8a4 --- /dev/null +++ b/lib/tools/organizations/controllers/organization_controller.dart @@ -0,0 +1,80 @@ +import 'package:academia/exports/barrel.dart'; +import 'package:dartz/dartz.dart'; +import 'package:get/get.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class OrganizationController extends GetxController { + // For sending requests + final OrganizationService _organizationService = OrganizationService(); + final StoryService _storyService = StoryService(); + final RxMap> stories = + RxMap>(); + RxList organizations = RxList(); + + late SharedPreferences prefs; + + @override + void onInit() async { + super.onInit(); + prefs = await SharedPreferences.getInstance(); + + final rawStoredOrganizations = prefs.getString("organizations"); + + if (rawStoredOrganizations != null) { + organizations.value = _decodeOrganizations(rawStoredOrganizations); + + for (final org in organizations) { + stories[org] = List.empty(growable: true); + } + return; + } + } + + String _encodeOrganizatons(List organizations) { + List> jsonList = + organizations.map((org) => org.toJson()).toList(); + return jsonEncode(jsonList); + } + + List _decodeOrganizations(String rawOrganizations) { + final List raw = jsonDecode(rawOrganizations); + + return raw.map((e) { + return Organization.fromJson(e as Map); + }).toList(); + } + + Future fetchStories(Map authCreds) async { + final result = await _storyService.fetchStories(authCreds); + + return result.fold((l) { + return false; + }, (r) { + for (final story in r) { + // print(story); + final orgwithStory = stories.entries + .firstWhere((elem) => elem.key.id == story.keys.first) + .key; + stories[orgwithStory] = story.values.first; + } + return true; + }); + } + + Future>> fetchOrganizations( + Map authCreds) async { + final result = await _organizationService.fetchOrganizations(authCreds); + return result.fold((l) { + return left(l); + }, (r) async { + prefs.setString( + "organizations", + _encodeOrganizatons(r), + ); + + organizations.value = r; + + return right(r); + }); + } +} diff --git a/lib/tools/organizations/models/core/organization.dart b/lib/tools/organizations/models/core/organization.dart index 300f132..3f933f7 100644 --- a/lib/tools/organizations/models/core/organization.dart +++ b/lib/tools/organizations/models/core/organization.dart @@ -1,73 +1,26 @@ -class Organization { - final String id; - final String name; - final String email; - final bool active; - final String? description; - final String? logo; - final String? banner; - final String? organizationPage; - final DateTime? createdAt; - final DateTime? updatedAt; - final String owner; - final String location; - final String phone; +import 'package:freezed_annotation/freezed_annotation.dart'; - Organization({ - required this.id, - required this.name, - required this.email, - required this.active, - this.description, - this.logo, - this.banner, - this.organizationPage, - this.createdAt, - this.updatedAt, - required this.owner, - required this.location, - required this.phone, - }); +part 'organization.freezed.dart'; +part 'organization.g.dart'; - // Factory method to create an instance from JSON - factory Organization.fromJson(Map json) { - return Organization( - id: json['id'], - name: json['name'], - email: json['email'], - active: json['active'], - description: json['description'], - logo: json['logo'], - banner: json['banner'], - organizationPage: json['organization_page'], - createdAt: json['created_at'] != null - ? DateTime.parse(json['created_at']) - : null, - updatedAt: json['updated_at'] != null - ? DateTime.parse(json['updated_at']) - : null, - owner: json['owner'], - location: json['location'], - phone: json['phone'], - ); - } +@freezed +class Organization with _$Organization { + const factory Organization({ + required String id, + required String name, + required String email, + required bool active, + String? description, + String? logo, + String? banner, + @JsonKey(name: 'organization_page') String? organizationPage, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, + required String owner, + required String location, + required String phone, + }) = _Organization; - // Method to convert an instance to JSON - Map toJson() { - return { - 'id': id, - 'name': name, - 'email': email, - 'active': active, - 'description': description, - 'logo': logo, - 'banner': banner, - 'organization_page': organizationPage, - 'created_at': createdAt?.toIso8601String(), - 'updated_at': updatedAt?.toIso8601String(), - 'owner': owner, - 'location': location, - 'phone': phone, - }; - } + factory Organization.fromJson(Map json) => + _$OrganizationFromJson(json); } diff --git a/lib/tools/organizations/models/core/organization.freezed.dart b/lib/tools/organizations/models/core/organization.freezed.dart new file mode 100644 index 0000000..0f8bf8e --- /dev/null +++ b/lib/tools/organizations/models/core/organization.freezed.dart @@ -0,0 +1,436 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'organization.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +Organization _$OrganizationFromJson(Map json) { + return _Organization.fromJson(json); +} + +/// @nodoc +mixin _$Organization { + String get id => throw _privateConstructorUsedError; + String get name => throw _privateConstructorUsedError; + String get email => throw _privateConstructorUsedError; + bool get active => throw _privateConstructorUsedError; + String? get description => throw _privateConstructorUsedError; + String? get logo => throw _privateConstructorUsedError; + String? get banner => throw _privateConstructorUsedError; + @JsonKey(name: 'organization_page') + String? get organizationPage => throw _privateConstructorUsedError; + @JsonKey(name: 'created_at') + DateTime? get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at') + DateTime? get updatedAt => throw _privateConstructorUsedError; + String get owner => throw _privateConstructorUsedError; + String get location => throw _privateConstructorUsedError; + String get phone => throw _privateConstructorUsedError; + + /// Serializes this Organization to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Organization + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $OrganizationCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $OrganizationCopyWith<$Res> { + factory $OrganizationCopyWith( + Organization value, $Res Function(Organization) then) = + _$OrganizationCopyWithImpl<$Res, Organization>; + @useResult + $Res call( + {String id, + String name, + String email, + bool active, + String? description, + String? logo, + String? banner, + @JsonKey(name: 'organization_page') String? organizationPage, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, + String owner, + String location, + String phone}); +} + +/// @nodoc +class _$OrganizationCopyWithImpl<$Res, $Val extends Organization> + implements $OrganizationCopyWith<$Res> { + _$OrganizationCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Organization + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? email = null, + Object? active = null, + Object? description = freezed, + Object? logo = freezed, + Object? banner = freezed, + Object? organizationPage = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + Object? owner = null, + Object? location = null, + Object? phone = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + active: null == active + ? _value.active + : active // ignore: cast_nullable_to_non_nullable + as bool, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + logo: freezed == logo + ? _value.logo + : logo // ignore: cast_nullable_to_non_nullable + as String?, + banner: freezed == banner + ? _value.banner + : banner // ignore: cast_nullable_to_non_nullable + as String?, + organizationPage: freezed == organizationPage + ? _value.organizationPage + : organizationPage // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + owner: null == owner + ? _value.owner + : owner // ignore: cast_nullable_to_non_nullable + as String, + location: null == location + ? _value.location + : location // ignore: cast_nullable_to_non_nullable + as String, + phone: null == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$OrganizationImplCopyWith<$Res> + implements $OrganizationCopyWith<$Res> { + factory _$$OrganizationImplCopyWith( + _$OrganizationImpl value, $Res Function(_$OrganizationImpl) then) = + __$$OrganizationImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String name, + String email, + bool active, + String? description, + String? logo, + String? banner, + @JsonKey(name: 'organization_page') String? organizationPage, + @JsonKey(name: 'created_at') DateTime? createdAt, + @JsonKey(name: 'updated_at') DateTime? updatedAt, + String owner, + String location, + String phone}); +} + +/// @nodoc +class __$$OrganizationImplCopyWithImpl<$Res> + extends _$OrganizationCopyWithImpl<$Res, _$OrganizationImpl> + implements _$$OrganizationImplCopyWith<$Res> { + __$$OrganizationImplCopyWithImpl( + _$OrganizationImpl _value, $Res Function(_$OrganizationImpl) _then) + : super(_value, _then); + + /// Create a copy of Organization + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? name = null, + Object? email = null, + Object? active = null, + Object? description = freezed, + Object? logo = freezed, + Object? banner = freezed, + Object? organizationPage = freezed, + Object? createdAt = freezed, + Object? updatedAt = freezed, + Object? owner = null, + Object? location = null, + Object? phone = null, + }) { + return _then(_$OrganizationImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + name: null == name + ? _value.name + : name // ignore: cast_nullable_to_non_nullable + as String, + email: null == email + ? _value.email + : email // ignore: cast_nullable_to_non_nullable + as String, + active: null == active + ? _value.active + : active // ignore: cast_nullable_to_non_nullable + as bool, + description: freezed == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String?, + logo: freezed == logo + ? _value.logo + : logo // ignore: cast_nullable_to_non_nullable + as String?, + banner: freezed == banner + ? _value.banner + : banner // ignore: cast_nullable_to_non_nullable + as String?, + organizationPage: freezed == organizationPage + ? _value.organizationPage + : organizationPage // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: freezed == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + updatedAt: freezed == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime?, + owner: null == owner + ? _value.owner + : owner // ignore: cast_nullable_to_non_nullable + as String, + location: null == location + ? _value.location + : location // ignore: cast_nullable_to_non_nullable + as String, + phone: null == phone + ? _value.phone + : phone // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$OrganizationImpl implements _Organization { + const _$OrganizationImpl( + {required this.id, + required this.name, + required this.email, + required this.active, + this.description, + this.logo, + this.banner, + @JsonKey(name: 'organization_page') this.organizationPage, + @JsonKey(name: 'created_at') this.createdAt, + @JsonKey(name: 'updated_at') this.updatedAt, + required this.owner, + required this.location, + required this.phone}); + + factory _$OrganizationImpl.fromJson(Map json) => + _$$OrganizationImplFromJson(json); + + @override + final String id; + @override + final String name; + @override + final String email; + @override + final bool active; + @override + final String? description; + @override + final String? logo; + @override + final String? banner; + @override + @JsonKey(name: 'organization_page') + final String? organizationPage; + @override + @JsonKey(name: 'created_at') + final DateTime? createdAt; + @override + @JsonKey(name: 'updated_at') + final DateTime? updatedAt; + @override + final String owner; + @override + final String location; + @override + final String phone; + + @override + String toString() { + return 'Organization(id: $id, name: $name, email: $email, active: $active, description: $description, logo: $logo, banner: $banner, organizationPage: $organizationPage, createdAt: $createdAt, updatedAt: $updatedAt, owner: $owner, location: $location, phone: $phone)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$OrganizationImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.name, name) || other.name == name) && + (identical(other.email, email) || other.email == email) && + (identical(other.active, active) || other.active == active) && + (identical(other.description, description) || + other.description == description) && + (identical(other.logo, logo) || other.logo == logo) && + (identical(other.banner, banner) || other.banner == banner) && + (identical(other.organizationPage, organizationPage) || + other.organizationPage == organizationPage) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (identical(other.owner, owner) || other.owner == owner) && + (identical(other.location, location) || + other.location == location) && + (identical(other.phone, phone) || other.phone == phone)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash( + runtimeType, + id, + name, + email, + active, + description, + logo, + banner, + organizationPage, + createdAt, + updatedAt, + owner, + location, + phone); + + /// Create a copy of Organization + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$OrganizationImplCopyWith<_$OrganizationImpl> get copyWith => + __$$OrganizationImplCopyWithImpl<_$OrganizationImpl>(this, _$identity); + + @override + Map toJson() { + return _$$OrganizationImplToJson( + this, + ); + } +} + +abstract class _Organization implements Organization { + const factory _Organization( + {required final String id, + required final String name, + required final String email, + required final bool active, + final String? description, + final String? logo, + final String? banner, + @JsonKey(name: 'organization_page') final String? organizationPage, + @JsonKey(name: 'created_at') final DateTime? createdAt, + @JsonKey(name: 'updated_at') final DateTime? updatedAt, + required final String owner, + required final String location, + required final String phone}) = _$OrganizationImpl; + + factory _Organization.fromJson(Map json) = + _$OrganizationImpl.fromJson; + + @override + String get id; + @override + String get name; + @override + String get email; + @override + bool get active; + @override + String? get description; + @override + String? get logo; + @override + String? get banner; + @override + @JsonKey(name: 'organization_page') + String? get organizationPage; + @override + @JsonKey(name: 'created_at') + DateTime? get createdAt; + @override + @JsonKey(name: 'updated_at') + DateTime? get updatedAt; + @override + String get owner; + @override + String get location; + @override + String get phone; + + /// Create a copy of Organization + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$OrganizationImplCopyWith<_$OrganizationImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/tools/organizations/models/core/organization.g.dart b/lib/tools/organizations/models/core/organization.g.dart new file mode 100644 index 0000000..479c377 --- /dev/null +++ b/lib/tools/organizations/models/core/organization.g.dart @@ -0,0 +1,45 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'organization.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$OrganizationImpl _$$OrganizationImplFromJson(Map json) => + _$OrganizationImpl( + id: json['id'] as String, + name: json['name'] as String, + email: json['email'] as String, + active: json['active'] as bool, + description: json['description'] as String?, + logo: json['logo'] as String?, + banner: json['banner'] as String?, + organizationPage: json['organization_page'] as String?, + createdAt: json['created_at'] == null + ? null + : DateTime.parse(json['created_at'] as String), + updatedAt: json['updated_at'] == null + ? null + : DateTime.parse(json['updated_at'] as String), + owner: json['owner'] as String, + location: json['location'] as String, + phone: json['phone'] as String, + ); + +Map _$$OrganizationImplToJson(_$OrganizationImpl instance) => + { + 'id': instance.id, + 'name': instance.name, + 'email': instance.email, + 'active': instance.active, + 'description': instance.description, + 'logo': instance.logo, + 'banner': instance.banner, + 'organization_page': instance.organizationPage, + 'created_at': instance.createdAt?.toIso8601String(), + 'updated_at': instance.updatedAt?.toIso8601String(), + 'owner': instance.owner, + 'location': instance.location, + 'phone': instance.phone, + }; diff --git a/lib/tools/organizations/models/core/story.dart b/lib/tools/organizations/models/core/story.dart new file mode 100644 index 0000000..6e2e2af --- /dev/null +++ b/lib/tools/organizations/models/core/story.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'story.freezed.dart'; +part 'story.g.dart'; + +@freezed +class Story with _$Story { + const factory Story({ + required String id, + required String description, + String? media, // Nullable media field + @JsonKey(name: 'file_type') required String fileType, + @JsonKey(name: 'number_of_views') required int numberOfViews, + String? url, // Nullable URL field + @JsonKey(name: 'created_at') required DateTime createdAt, + @JsonKey(name: 'updated_at') required DateTime updatedAt, + @JsonKey(name: 'viewed', defaultValue: false) required bool viewed, + required String organization, + }) = _Story; + + // Factory constructor for creating a new Story instance from a Map (JSON) + factory Story.fromJson(Map json) => _$StoryFromJson(json); +} diff --git a/lib/tools/organizations/models/core/story.freezed.dart b/lib/tools/organizations/models/core/story.freezed.dart new file mode 100644 index 0000000..31bc539 --- /dev/null +++ b/lib/tools/organizations/models/core/story.freezed.dart @@ -0,0 +1,369 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'story.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models'); + +Story _$StoryFromJson(Map json) { + return _Story.fromJson(json); +} + +/// @nodoc +mixin _$Story { + String get id => throw _privateConstructorUsedError; + String get description => throw _privateConstructorUsedError; + String? get media => + throw _privateConstructorUsedError; // Nullable media field + @JsonKey(name: 'file_type') + String get fileType => throw _privateConstructorUsedError; + @JsonKey(name: 'number_of_views') + int get numberOfViews => throw _privateConstructorUsedError; + String? get url => throw _privateConstructorUsedError; // Nullable URL field + @JsonKey(name: 'created_at') + DateTime get createdAt => throw _privateConstructorUsedError; + @JsonKey(name: 'updated_at') + DateTime get updatedAt => throw _privateConstructorUsedError; + @JsonKey(name: 'viewed', defaultValue: false) + bool get viewed => throw _privateConstructorUsedError; + String get organization => throw _privateConstructorUsedError; + + /// Serializes this Story to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of Story + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $StoryCopyWith get copyWith => throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $StoryCopyWith<$Res> { + factory $StoryCopyWith(Story value, $Res Function(Story) then) = + _$StoryCopyWithImpl<$Res, Story>; + @useResult + $Res call( + {String id, + String description, + String? media, + @JsonKey(name: 'file_type') String fileType, + @JsonKey(name: 'number_of_views') int numberOfViews, + String? url, + @JsonKey(name: 'created_at') DateTime createdAt, + @JsonKey(name: 'updated_at') DateTime updatedAt, + @JsonKey(name: 'viewed', defaultValue: false) bool viewed, + String organization}); +} + +/// @nodoc +class _$StoryCopyWithImpl<$Res, $Val extends Story> + implements $StoryCopyWith<$Res> { + _$StoryCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of Story + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? description = null, + Object? media = freezed, + Object? fileType = null, + Object? numberOfViews = null, + Object? url = freezed, + Object? createdAt = null, + Object? updatedAt = null, + Object? viewed = null, + Object? organization = null, + }) { + return _then(_value.copyWith( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + media: freezed == media + ? _value.media + : media // ignore: cast_nullable_to_non_nullable + as String?, + fileType: null == fileType + ? _value.fileType + : fileType // ignore: cast_nullable_to_non_nullable + as String, + numberOfViews: null == numberOfViews + ? _value.numberOfViews + : numberOfViews // ignore: cast_nullable_to_non_nullable + as int, + url: freezed == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + viewed: null == viewed + ? _value.viewed + : viewed // ignore: cast_nullable_to_non_nullable + as bool, + organization: null == organization + ? _value.organization + : organization // ignore: cast_nullable_to_non_nullable + as String, + ) as $Val); + } +} + +/// @nodoc +abstract class _$$StoryImplCopyWith<$Res> implements $StoryCopyWith<$Res> { + factory _$$StoryImplCopyWith( + _$StoryImpl value, $Res Function(_$StoryImpl) then) = + __$$StoryImplCopyWithImpl<$Res>; + @override + @useResult + $Res call( + {String id, + String description, + String? media, + @JsonKey(name: 'file_type') String fileType, + @JsonKey(name: 'number_of_views') int numberOfViews, + String? url, + @JsonKey(name: 'created_at') DateTime createdAt, + @JsonKey(name: 'updated_at') DateTime updatedAt, + @JsonKey(name: 'viewed', defaultValue: false) bool viewed, + String organization}); +} + +/// @nodoc +class __$$StoryImplCopyWithImpl<$Res> + extends _$StoryCopyWithImpl<$Res, _$StoryImpl> + implements _$$StoryImplCopyWith<$Res> { + __$$StoryImplCopyWithImpl( + _$StoryImpl _value, $Res Function(_$StoryImpl) _then) + : super(_value, _then); + + /// Create a copy of Story + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? description = null, + Object? media = freezed, + Object? fileType = null, + Object? numberOfViews = null, + Object? url = freezed, + Object? createdAt = null, + Object? updatedAt = null, + Object? viewed = null, + Object? organization = null, + }) { + return _then(_$StoryImpl( + id: null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as String, + description: null == description + ? _value.description + : description // ignore: cast_nullable_to_non_nullable + as String, + media: freezed == media + ? _value.media + : media // ignore: cast_nullable_to_non_nullable + as String?, + fileType: null == fileType + ? _value.fileType + : fileType // ignore: cast_nullable_to_non_nullable + as String, + numberOfViews: null == numberOfViews + ? _value.numberOfViews + : numberOfViews // ignore: cast_nullable_to_non_nullable + as int, + url: freezed == url + ? _value.url + : url // ignore: cast_nullable_to_non_nullable + as String?, + createdAt: null == createdAt + ? _value.createdAt + : createdAt // ignore: cast_nullable_to_non_nullable + as DateTime, + updatedAt: null == updatedAt + ? _value.updatedAt + : updatedAt // ignore: cast_nullable_to_non_nullable + as DateTime, + viewed: null == viewed + ? _value.viewed + : viewed // ignore: cast_nullable_to_non_nullable + as bool, + organization: null == organization + ? _value.organization + : organization // ignore: cast_nullable_to_non_nullable + as String, + )); + } +} + +/// @nodoc +@JsonSerializable() +class _$StoryImpl implements _Story { + const _$StoryImpl( + {required this.id, + required this.description, + this.media, + @JsonKey(name: 'file_type') required this.fileType, + @JsonKey(name: 'number_of_views') required this.numberOfViews, + this.url, + @JsonKey(name: 'created_at') required this.createdAt, + @JsonKey(name: 'updated_at') required this.updatedAt, + @JsonKey(name: 'viewed', defaultValue: false) required this.viewed, + required this.organization}); + + factory _$StoryImpl.fromJson(Map json) => + _$$StoryImplFromJson(json); + + @override + final String id; + @override + final String description; + @override + final String? media; +// Nullable media field + @override + @JsonKey(name: 'file_type') + final String fileType; + @override + @JsonKey(name: 'number_of_views') + final int numberOfViews; + @override + final String? url; +// Nullable URL field + @override + @JsonKey(name: 'created_at') + final DateTime createdAt; + @override + @JsonKey(name: 'updated_at') + final DateTime updatedAt; + @override + @JsonKey(name: 'viewed', defaultValue: false) + final bool viewed; + @override + final String organization; + + @override + String toString() { + return 'Story(id: $id, description: $description, media: $media, fileType: $fileType, numberOfViews: $numberOfViews, url: $url, createdAt: $createdAt, updatedAt: $updatedAt, viewed: $viewed, organization: $organization)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$StoryImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.description, description) || + other.description == description) && + (identical(other.media, media) || other.media == media) && + (identical(other.fileType, fileType) || + other.fileType == fileType) && + (identical(other.numberOfViews, numberOfViews) || + other.numberOfViews == numberOfViews) && + (identical(other.url, url) || other.url == url) && + (identical(other.createdAt, createdAt) || + other.createdAt == createdAt) && + (identical(other.updatedAt, updatedAt) || + other.updatedAt == updatedAt) && + (identical(other.viewed, viewed) || other.viewed == viewed) && + (identical(other.organization, organization) || + other.organization == organization)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, description, media, fileType, + numberOfViews, url, createdAt, updatedAt, viewed, organization); + + /// Create a copy of Story + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$StoryImplCopyWith<_$StoryImpl> get copyWith => + __$$StoryImplCopyWithImpl<_$StoryImpl>(this, _$identity); + + @override + Map toJson() { + return _$$StoryImplToJson( + this, + ); + } +} + +abstract class _Story implements Story { + const factory _Story( + {required final String id, + required final String description, + final String? media, + @JsonKey(name: 'file_type') required final String fileType, + @JsonKey(name: 'number_of_views') required final int numberOfViews, + final String? url, + @JsonKey(name: 'created_at') required final DateTime createdAt, + @JsonKey(name: 'updated_at') required final DateTime updatedAt, + @JsonKey(name: 'viewed', defaultValue: false) required final bool viewed, + required final String organization}) = _$StoryImpl; + + factory _Story.fromJson(Map json) = _$StoryImpl.fromJson; + + @override + String get id; + @override + String get description; + @override + String? get media; // Nullable media field + @override + @JsonKey(name: 'file_type') + String get fileType; + @override + @JsonKey(name: 'number_of_views') + int get numberOfViews; + @override + String? get url; // Nullable URL field + @override + @JsonKey(name: 'created_at') + DateTime get createdAt; + @override + @JsonKey(name: 'updated_at') + DateTime get updatedAt; + @override + @JsonKey(name: 'viewed', defaultValue: false) + bool get viewed; + @override + String get organization; + + /// Create a copy of Story + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$StoryImplCopyWith<_$StoryImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/tools/organizations/models/core/story.g.dart b/lib/tools/organizations/models/core/story.g.dart new file mode 100644 index 0000000..de86995 --- /dev/null +++ b/lib/tools/organizations/models/core/story.g.dart @@ -0,0 +1,34 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'story.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$StoryImpl _$$StoryImplFromJson(Map json) => _$StoryImpl( + id: json['id'] as String, + description: json['description'] as String, + media: json['media'] as String?, + fileType: json['file_type'] as String, + numberOfViews: (json['number_of_views'] as num).toInt(), + url: json['url'] as String?, + createdAt: DateTime.parse(json['created_at'] as String), + updatedAt: DateTime.parse(json['updated_at'] as String), + viewed: json['viewed'] as bool? ?? false, + organization: json['organization'] as String, + ); + +Map _$$StoryImplToJson(_$StoryImpl instance) => + { + 'id': instance.id, + 'description': instance.description, + 'media': instance.media, + 'file_type': instance.fileType, + 'number_of_views': instance.numberOfViews, + 'url': instance.url, + 'created_at': instance.createdAt.toIso8601String(), + 'updated_at': instance.updatedAt.toIso8601String(), + 'viewed': instance.viewed, + 'organization': instance.organization, + }; diff --git a/lib/tools/organizations/models/models.dart b/lib/tools/organizations/models/models.dart index bd475c3..af499ac 100644 --- a/lib/tools/organizations/models/models.dart +++ b/lib/tools/organizations/models/models.dart @@ -1,3 +1,5 @@ export '../models/core/organization.dart'; export '../models/core/membership.dart'; export '../models/services/organization_service.dart'; +export 'services/story_service.dart'; +export 'core/story.dart'; diff --git a/lib/tools/organizations/models/services/organization_service.dart b/lib/tools/organizations/models/services/organization_service.dart index ff31099..13568a6 100644 --- a/lib/tools/organizations/models/services/organization_service.dart +++ b/lib/tools/organizations/models/services/organization_service.dart @@ -31,6 +31,34 @@ class OrganizationService with ChirpService { } } + Future>> fetchOrganization( + Map authHeaders, String id) async { + try { + // fetch organizations + final response = await http.get( + Uri.parse("${ChirpService.urlPrefix}/organizations/find/$id"), + headers: authHeaders, + ); + + if (response.statusCode == 200) { + List> rawOrganizations = + json.decode(response.body).cast>(); + + return Right( + rawOrganizations.map((e) => Organization.fromJson(e)).toList(), + ); + } + + return const Left( + "Something went wrong white attempting to fetch organizations", + ); + } catch (e) { + return const Left( + "Please check your internet connection and try that again", + ); + } + } + Future>> fetchUserMemberships( String userID, Map authHeaders) async { try { diff --git a/lib/tools/organizations/models/services/story_service.dart b/lib/tools/organizations/models/services/story_service.dart new file mode 100644 index 0000000..cecf19a --- /dev/null +++ b/lib/tools/organizations/models/services/story_service.dart @@ -0,0 +1,49 @@ +import 'package:academia/exports/barrel.dart'; +import 'package:dartz/dartz.dart'; +import 'package:http/http.dart' as http; + +class StoryService with ChirpService { + Future>>>> fetchStories( + Map authHeaders) async { + try { + // fetch organizations + final response = await http.get( + Uri.parse("${ChirpService.urlPrefix}/stories/"), + headers: authHeaders, + ); + + if (response.statusCode == 200) { + Map rawData = json.decode(response.body); + + List>> parsedStories = + rawData.entries.map((entry) { + // Organization ID is the key + String organizationId = entry.key; + + // The value is a List of story JSON objects + List storiesJson = entry.value; + + // Parse each story JSON into a Story model + List stories = storiesJson + .map((storyJson) => + Story.fromJson(storyJson as Map)) + .toList(); + + // Return a map with organization ID and corresponding list of Story objects + return {organizationId: stories}; + }).toList(); + + // Return the parsed stories wrapped in a Right + return Right(parsedStories); + } + + return const Left( + "Something went wrong white attempting to fetch organizations", + ); + } catch (e) { + return const Left( + "Please check your internet connection and try that again", + ); + } + } +} diff --git a/lib/tools/organizations/organizations.dart b/lib/tools/organizations/organizations.dart index a6f8a1a..5f639e3 100644 --- a/lib/tools/organizations/organizations.dart +++ b/lib/tools/organizations/organizations.dart @@ -1,3 +1,4 @@ export 'pages/organizations_page.dart'; export 'models/models.dart'; export 'pages/membership_pages.dart'; +export 'controllers/organization_controller.dart'; diff --git a/lib/tools/organizations/pages/organizations_page.dart b/lib/tools/organizations/pages/organizations_page.dart index f519a7b..3cb2cea 100644 --- a/lib/tools/organizations/pages/organizations_page.dart +++ b/lib/tools/organizations/pages/organizations_page.dart @@ -13,18 +13,19 @@ class OrganizationsPage extends StatefulWidget { class _OrganizationsPageState extends State with AutomaticKeepAliveClientMixin { final UserController userController = Get.find(); + final OrganizationController organizationController = + Get.find(); @override bool get wantKeepAlive => true; - final OrganizationService _service = OrganizationService(); - // TH loading state late Future>> _loader; @override void initState() { super.initState(); - _loader = _service.fetchOrganizations(userController.authHeaders); + _loader = + organizationController.fetchOrganizations(userController.authHeaders); } @override @@ -32,7 +33,8 @@ class _OrganizationsPageState extends State super.build(context); return RefreshIndicator( onRefresh: () async { - _loader = _service.fetchOrganizations(userController.authHeaders); + _loader = organizationController + .fetchOrganizations(userController.authHeaders); setState(() {}); }, child: FutureBuilder( @@ -44,6 +46,12 @@ class _OrganizationsPageState extends State leading: CircleAvatar(), trailing: CircleAvatar(radius: 5)), ); } + // + // if (snapshot.hasError) { + // return const Center( + // child: Text("Error"), + // ); + // } return snapshot.data!.fold((l) { return Center(