diff --git a/README.md b/README.md index 7109fcc..90b50fb 100644 --- a/README.md +++ b/README.md @@ -1,207 +1,19 @@ -# Task 2: Introduction to Navigation +# Fake Social Media App +My implementation of the second task of the GDSC and Programming Club Flutter tasks. +As requested, the app has 4 main pages, as well as inner pages to inspect posts and profiles. -## Table of Contents +### Home Page +![The Home Screen](assets/readme/home_feed.jpg) -- [Overview](#overview) -- [Learning Objectives](#learning-objectives) -- [Setup and Tutorial](#setup-and-tutorial) -- [Project Overview](#project-overview) -- [Submission Guidelines](#submission-guidelines) +### Profile Page +![My Profile Page](assets/readme/fahad_profile.jpg) -## Overview +### Search Page and Functionality +![The Initial Search Page](assets/readme/search_page.jpg) +![The Search Results](assets/readme/search_results.jpg) -In this project, you will continue on your previous profile page and create 4 main pages for your new social media app. The pages you select are up to you, but they should be related to a social media app. You can use the navigation method of your choice (Material page route or Named routes). +### The Notifications Page +![The Notifications Page](assets/readme/notifications_page.jpg) -## Learning Objectives - -- Learn how to navigate between pages in Flutter. -- Learn how to create a bottom navigation bar. -- Learn how to push and pop pages in Flutter. - -## Resources - -- [Bottom Navigation Bar](https://www.youtube.com/watch?v=xoKqQjSDZ60) -- [Modern Navigation Bar](https://www.youtube.com/watch?v=FEvYl8Mzsxw) -- [Flutter Basic Navigation](https://www.youtube.com/watch?v=C6nTXjQFVKI) - -## Custom Resources - -[Navigation Tutorial (English)](https://www.youtube.com/watch?v=iWwSdygvrsA&list=PL1LV47jH4m0cGRTJFqfN39YpNbLDY9_NE&pp=iAQB) - -## Setup and Tutorial - -### 1. Setup - -#### 1.1. Git and Github - -To setup this project, please follow this simple git and github tutorial provided [here](https://github.com/GDSC-IAU/git-and-github) - -### Tutorial - -#### 2.1. Setting up a Bottom Navigation Bar - -```dart -import 'package:flutter/material.dart'; -// Import all of the pages that we want to navigate to -// ............... - -// Create a stateful widget -class MainScreen extends StatefulWidget { - const MainScreen({super.key}); - @override - _MainScreenState createState() => _MainScreenState(); -} - -// Create a state -class _MainScreenState extends State { - // Create a list of pages that we want to navigate to - final List _pages = [ - // Add all of the pages that we want to navigate to - // ............... - ]; - - // Create a variable to keep track of the current page - int _selectedPageIndex = 0; - - // Create a method to change the current page - void _selectPage(int index) { - setState(() { - _selectedPageIndex = index; - }); - } - - @override - Widget build(BuildContext context) { - // Create a scaffold - return Scaffold( - // Create a bottom navigation bar - bottomNavigationBar: BottomNavigationBar( - // Set the current page - currentIndex: _selectedPageIndex, - // Set the items in the bottom navigation bar - items: [ - // Create a bottom navigation bar item - BottomNavigationBarItem( - // Set the icon of the item - icon: Icon(Icons.home), - // Set the label of the item - label: 'Home', - ), - // Create a bottom navigation bar item - BottomNavigationBarItem( - // Set the icon of the item - icon: Icon(Icons.search), - // Set the label of the item - label: 'Search', - ), - // Create a bottom navigation bar item - BottomNavigationBarItem( - // Set the icon of the item - icon: Icon(Icons.shopping_cart), - // Set the label of the item - label: 'Cart', - ), - // Create a bottom navigation bar item - BottomNavigationBarItem( - // Set the icon of the item - icon: Icon(Icons.person), - // Set the label of the item - label: 'Profile', - ), - ], - // Set the action that happens when an item is pressed - onTap: _selectPage, - ), - // Set the body of the scaffold - body: _pages[_selectedPageIndex], - ); - } -} -``` - -#### 2.2. Navigating to a New Page - -##### 2.2.1 Using Material page Route - -```dart -import 'package:flutter/material.dart'; -// Import the page that we want to navigate to -// ............... - -// Navigate Function using Push -void _navigate(BuildContext context) { - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SomePage(args: args,)// Add the page that we want to navigate to - ), - ); -} -``` - -##### 2.2.2 Using Named Routes - -```dart -import 'package:flutter/material.dart'; -// Import all of the pages that we want to navigate to -// ............... - -// In your MaterialApp widget, add the routes parameter -MaterialApp( - // Add the routes parameter - routes: { - // Add all of the pages that we want to navigate to - // ............... - "/somePage": (context) => SomePage(args: args),// Add the page that we want to navigate to - "/someOtherPage": (context) => SomeOtherPage(args: args),// Add the page that we want to navigate to - }, -); -``` - -Now we can use these named routes to navigate to a new page. - -```dart -// Navigate Function using Named Routes -void _navigate(BuildContext context) { - Navigator.pushNamed( - context, - '/somePage',// Add the name of the page that we want to navigate to - ); -} -``` - -## Project Overview - -In this project, you will continue on your previous profile page and create 4 main pages for your new social media app. The pages you select are up to you, but they should be related to a social media app. You can use the navigation method of your choice (Material page route or Named routes). - -### Requirements - -- The app must have a bottom navigation bar. -- The app must have a home page, search page, notifications page, and profile page. -- The app must have some inner page that uses the navigation method of your choice. (Material page route or Named routes) -- The app structure should be clean and easy to understand. -- The app should be well documented. -- The readme file should contain a brief description of the project and a screenshot of the app. - -### Bonus - -- Use a package from pub.dev to create a stylized bottom navigation bar -- Use a Hero widget to create a transition between pages - -## Submission Guidelines - -- The app should be pushed to Github and a pull request should be created. You can check how to push your code to Github in section [2.1.2 Add Changes](https://github.com/Programming-Club-IAU/git-and-github#212-add-changes). -- The pull request title should be in the following format: ` - `. You can check how to make a pull request in section [2.1.5. Create a pull request](ttps://github.com/Programming-Club-IAU/git-and-github#215-create-a-pull-request). -- The pull request description should contain the following: - - A brief description of the project. - - A screenshot of the app. - -## Design Inspiration - -### [Design 1](https://dribbble.com/shots/23642089-AI-Driven-Platform-for-Accelerating-Civic-Solutions) - -![Design](https://cdn.dribbble.com/userupload/13050999/file/original-3b016b478672ffd2bae1f937fe232754.jpg?resize=2048x1536) - -### [Design 2](https://dribbble.com/shots/17948799-Social-Media-App) - -![design](https://cdn.dribbble.com/users/7825060/screenshots/17948799/media/c4408cd5915d44239f47235c019693a5.png?resize=1600x1200&vertical=center) +### Models +The app uses 2 main models, the user model, and the post model. The "users.dart" and "posts.dart" files are essentially like big storage files to declare models so that it can make the process of adjusting the app much easier. It contains frequently used user models and post models respectively. diff --git a/android/app/src/main/kotlin/com/example/profile_page/MainActivity.kt b/android/app/src/main/kotlin/com/example/profile_page/MainActivity.kt new file mode 100644 index 0000000..41d88aa --- /dev/null +++ b/android/app/src/main/kotlin/com/example/profile_page/MainActivity.kt @@ -0,0 +1,6 @@ +package com.example.profile_page + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity: FlutterActivity() { +} diff --git a/assets/logos/github_logo.png b/assets/logos/github_logo.png new file mode 100644 index 0000000..47379eb Binary files /dev/null and b/assets/logos/github_logo.png differ diff --git a/assets/logos/x_logo.png b/assets/logos/x_logo.png new file mode 100644 index 0000000..dae2b91 Binary files /dev/null and b/assets/logos/x_logo.png differ diff --git a/assets/posts/persona.jpg b/assets/posts/persona.jpg new file mode 100644 index 0000000..7c25828 Binary files /dev/null and b/assets/posts/persona.jpg differ diff --git a/assets/profile_pics/default.jpg b/assets/profile_pics/default.jpg new file mode 100644 index 0000000..1b53062 Binary files /dev/null and b/assets/profile_pics/default.jpg differ diff --git a/assets/profile_pics/fahad.jpg b/assets/profile_pics/fahad.jpg new file mode 100644 index 0000000..2ba6ce0 Binary files /dev/null and b/assets/profile_pics/fahad.jpg differ diff --git a/assets/profile_pics/hassan.jpg b/assets/profile_pics/hassan.jpg new file mode 100644 index 0000000..b75c49a Binary files /dev/null and b/assets/profile_pics/hassan.jpg differ diff --git a/assets/profile_pics/radwan.jpeg b/assets/profile_pics/radwan.jpeg new file mode 100644 index 0000000..ef44171 Binary files /dev/null and b/assets/profile_pics/radwan.jpeg differ diff --git a/assets/readme/fahad_profile.jpg b/assets/readme/fahad_profile.jpg new file mode 100644 index 0000000..47bd4c0 Binary files /dev/null and b/assets/readme/fahad_profile.jpg differ diff --git a/assets/readme/home_feed.jpg b/assets/readme/home_feed.jpg new file mode 100644 index 0000000..eece184 Binary files /dev/null and b/assets/readme/home_feed.jpg differ diff --git a/assets/readme/notifications_page.jpg b/assets/readme/notifications_page.jpg new file mode 100644 index 0000000..63abe85 Binary files /dev/null and b/assets/readme/notifications_page.jpg differ diff --git a/assets/readme/search_page.jpg b/assets/readme/search_page.jpg new file mode 100644 index 0000000..64ae8ac Binary files /dev/null and b/assets/readme/search_page.jpg differ diff --git a/assets/readme/search_results.jpg b/assets/readme/search_results.jpg new file mode 100644 index 0000000..19e102b Binary files /dev/null and b/assets/readme/search_results.jpg differ diff --git a/lib/default_colors.dart b/lib/default_colors.dart new file mode 100644 index 0000000..3b5f1b3 --- /dev/null +++ b/lib/default_colors.dart @@ -0,0 +1,16 @@ +import 'package:flutter/material.dart'; + +class DefaultColors { + static const Color profileCardColor = Color.fromARGB(255, 76, 73, 86); + + static const Color fieldColor = Color.fromARGB(255, 47, 45, 56); + static const Color fieldOutlineColor = Color.fromARGB(255, 75, 74, 83); + + static const Color blockColor = Color.fromARGB(255, 39, 35, 43); + + // This is done for convenience's sake, I find that matching the blocks outline with the field color gives a nice look + // so this is just a way for me to avoid having to reset both the block outline and the field color each time I change it + static const Color blockOutlineColor = fieldColor; + + static const Color userHandleColor = Color.fromARGB(255, 156, 156, 156); +} diff --git a/lib/main.dart b/lib/main.dart index a725658..a266385 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'screens/main_screen.dart'; void main() { runApp(const MainApp()); @@ -9,11 +10,11 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - return const MaterialApp( - home: Scaffold( - body: Center( - child: Text('Hello World!'), - ), + return MaterialApp( + debugShowCheckedModeBanner: false, + theme: ThemeData.dark(), + home: const Scaffold( + body: MainScreen(), ), ); } diff --git a/lib/models/post_model.dart b/lib/models/post_model.dart new file mode 100644 index 0000000..b1d2430 --- /dev/null +++ b/lib/models/post_model.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import 'user_model.dart'; + +class PostModel { + const PostModel( + this.text, { + required this.user, + this.imageAttachment, + this.parent, + this.replies, + this.likes = 0, + this.dislikes = 0, + this.favorites = 0, + }); + + final UserModel user; + final String text; + final ImageProvider? imageAttachment; + final PostModel? parent; + final List? replies; + final int likes; + final int dislikes; + final int favorites; +} diff --git a/lib/models/user_model.dart b/lib/models/user_model.dart new file mode 100644 index 0000000..81e7f05 --- /dev/null +++ b/lib/models/user_model.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; + +class UserModel { + const UserModel({ + required this.handle, + required this.name, + this.profilePic = const AssetImage("assets/profile_pics/default.jpg"), + this.profileBio, + this.profileBanner, + this.following = 0, + this.followers = 0, + }); + + final String handle; + final String name; + final ImageProvider profilePic; + final String? profileBio; + final ImageProvider? profileBanner; + final int following; + final int followers; +} diff --git a/lib/posts.dart b/lib/posts.dart new file mode 100644 index 0000000..db7d515 --- /dev/null +++ b/lib/posts.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; + +import 'users.dart'; +import 'models/post_model.dart'; + +class FahadPosts { + static const PostModel persona = PostModel( + "The new Persona 3 remake is AWESOME", + user: Users.fahad, + likes: 6, + favorites: 2, + dislikes: 1, + imageAttachment: AssetImage("assets/posts/persona.jpg"), + ); + static const PostModel flutter = PostModel( + "Learning flutter's been pretty cool", + user: Users.fahad, + likes: 2, + ); +} + +class RadwanPosts { + static const PostModel event = PostModel( + "Yesterday's event was probably the best CCSIT event of all time ngl", + user: Users.radwan, + likes: 8, + favorites: 3, + ); + static const PostModel workshop = PostModel( + "Everybody don't forget to register for tomorrow's workshop!", + user: Users.radwan, + likes: 3, + ); +} + +class HassanPosts { + static const PostModel github = PostModel( + "Thank you everyone who attended our github workshop!", + user: Users.hassan, + likes: 7, + ); + static const PostModel math = PostModel( + "Mathematics is the art of explanation", + user: Users.hassan, + likes: 3, + ); +} + +class Posts { + static const List allPosts = [ + FahadPosts.flutter, + FahadPosts.persona, + RadwanPosts.event, + RadwanPosts.workshop, + HassanPosts.github, + HassanPosts.math, + ]; + + static const List homeFeed = [ + FahadPosts.persona, + RadwanPosts.event, + HassanPosts.github, + HassanPosts.math, + FahadPosts.flutter, + RadwanPosts.workshop, + ]; + + static List searchPosts(String searchCriteria) { + List posts = []; + + for (int i = 0; i < allPosts.length; i++) { + if (allPosts[i] + .text + .toLowerCase() + .contains(searchCriteria.toLowerCase())) { + posts.add(allPosts[i]); + } + } + + return posts; + } +} diff --git a/lib/screens/home_page.dart b/lib/screens/home_page.dart new file mode 100644 index 0000000..716c59b --- /dev/null +++ b/lib/screens/home_page.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:profile_page/models/post_model.dart'; +import '../widgets/post.dart'; + +class HomePage extends StatelessWidget { + const HomePage({super.key, required this.posts}); + + final List posts; + + @override + Widget build(BuildContext context) { + return ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: posts.length, + itemBuilder: (context, index) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: Post(post: posts[index]), + ); + }, + ); + } +} diff --git a/lib/screens/main_screen.dart b/lib/screens/main_screen.dart new file mode 100644 index 0000000..a084dc5 --- /dev/null +++ b/lib/screens/main_screen.dart @@ -0,0 +1,64 @@ +import 'package:flutter/material.dart'; +import 'package:profile_page/users.dart'; +import '../screens/search_page.dart'; +import 'home_page.dart'; +import 'profile_page.dart'; +import 'notifications_page.dart'; +import '../posts.dart'; + +class MainScreen extends StatefulWidget { + const MainScreen({super.key}); + + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + final List _pages = [ + const HomePage(posts: Posts.homeFeed), + const ProfilePage(user: Users.fahad), + const SearchPage(), + const NotificationsPage(), + ]; + + int _selectedPageIndex = 0; + void _selectPage(int index) { + setState(() { + _selectedPageIndex = index; + }); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + bottomNavigationBar: BottomNavigationBar( + type: BottomNavigationBarType.fixed, + items: const [ + BottomNavigationBarItem( + label: "Home", + icon: Icon(Icons.home_outlined), + activeIcon: Icon(Icons.home), + ), + BottomNavigationBarItem( + label: "My Profile", + icon: Icon(Icons.person_outline), + activeIcon: Icon(Icons.person), + ), + BottomNavigationBarItem( + label: "Search", + icon: Icon(Icons.search_outlined), + activeIcon: Icon(Icons.search), + ), + BottomNavigationBarItem( + label: "Notifications", + icon: Icon(Icons.notifications_outlined), + activeIcon: Icon(Icons.notifications), + ), + ], + currentIndex: _selectedPageIndex, + onTap: _selectPage, + ), + body: _pages[_selectedPageIndex], + ); + } +} diff --git a/lib/screens/notifications_page.dart b/lib/screens/notifications_page.dart new file mode 100644 index 0000000..d9de5cc --- /dev/null +++ b/lib/screens/notifications_page.dart @@ -0,0 +1,49 @@ +import 'package:flutter/material.dart'; +import 'package:profile_page/posts.dart'; +import 'package:profile_page/users.dart'; +import '../widgets/notification_item.dart'; + +class NotificationsPage extends StatelessWidget { + const NotificationsPage({super.key}); + + @override + Widget build(BuildContext context) { + return ListView( + physics: const BouncingScrollPhysics(), + children: const [ + Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: NotificationItem( + user: Users.radwan, + post: FahadPosts.flutter, + action: NotificationType.favorite, + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: NotificationItem( + user: Users.radwan, + post: FahadPosts.flutter, + action: NotificationType.like, + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: NotificationItem( + user: Users.hassan, + post: FahadPosts.persona, + action: NotificationType.like, + ), + ), + Padding( + padding: EdgeInsets.symmetric(vertical: 8.0), + child: NotificationItem( + user: Users.hassan, + post: FahadPosts.flutter, + action: NotificationType.like, + ), + ), + ], + ); + } +} diff --git a/lib/screens/post_inspect_screen.dart b/lib/screens/post_inspect_screen.dart new file mode 100644 index 0000000..9b6e8a5 --- /dev/null +++ b/lib/screens/post_inspect_screen.dart @@ -0,0 +1,24 @@ +import 'package:flutter/material.dart'; +import '../models/post_model.dart'; +import '../widgets/post.dart'; + +class PostInspectScreen extends StatelessWidget { + const PostInspectScreen({ + super.key, + required this.post, + }); + + final PostModel post; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: Center( + child: Post( + post: post, + inspectable: false, + ), + ), + ); + } +} diff --git a/lib/screens/profile_page.dart b/lib/screens/profile_page.dart new file mode 100644 index 0000000..3f9fcbe --- /dev/null +++ b/lib/screens/profile_page.dart @@ -0,0 +1,31 @@ +import 'package:flutter/material.dart'; +import '../widgets/profile_banner.dart'; +import '../models/user_model.dart'; + +class ProfilePage extends StatelessWidget { + const ProfilePage({ + super.key, + this.blockPadding = 20, + required this.user, + }); + + final double blockPadding; + final UserModel user; + + @override + Widget build(BuildContext context) { + return Center( + child: ListView( + physics: const BouncingScrollPhysics(), + addAutomaticKeepAlives: false, + // All the items in the list are wrapped with a Padding widget to create space between the items, that is the "blockPadding" + children: [ + Padding( + padding: const EdgeInsets.only(top: 20, bottom: 40), + child: ProfileBanner(user: user), + ), + ], + ), + ); + } +} diff --git a/lib/screens/search_page.dart b/lib/screens/search_page.dart new file mode 100644 index 0000000..3814feb --- /dev/null +++ b/lib/screens/search_page.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import 'package:profile_page/default_colors.dart'; +import 'package:profile_page/models/post_model.dart'; +import 'package:profile_page/utils.dart'; +import 'package:profile_page/widgets/post.dart'; +import '../widgets/search_field.dart'; + +class SearchPage extends StatelessWidget { + const SearchPage({super.key}); + + @override + Widget build(BuildContext context) { + return const Center( + child: SearchField(), + ); + } +} + +class SearchResultsPage extends StatelessWidget { + const SearchResultsPage({ + super.key, + required this.searchCriteria, + required this.searchResults, + }); + + final String searchCriteria; + final List searchResults; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: ListView.builder( + physics: const BouncingScrollPhysics(), + itemCount: searchResults.length + 1, + itemBuilder: (context, index) => (index == 0) + ? UnconstrainedBox( + child: Container( + margin: const EdgeInsetsDirectional.symmetric(vertical: 10), + padding: const EdgeInsets.all(10), + alignment: Alignment.center, + decoration: roundedRectangle( + DefaultColors.fieldColor, + outlineColor: DefaultColors.fieldOutlineColor, + roundness: 10, + ), + width: 200, + child: Text((searchResults.isEmpty) + ? "No results found" + : "Results for '$searchCriteria'"), + ), + ) + : Padding( + padding: const EdgeInsetsDirectional.symmetric(vertical: 10), + child: Post(post: searchResults[index - 1]), + ), + )); + } +} diff --git a/lib/users.dart b/lib/users.dart new file mode 100644 index 0000000..279844e --- /dev/null +++ b/lib/users.dart @@ -0,0 +1,32 @@ +import 'package:flutter/material.dart'; +import 'models/user_model.dart'; + +class Users { + static const UserModel fahad = UserModel( + handle: "AlqarniDev", + name: "Fahad Alqarni", + profilePic: AssetImage("assets/profile_pics/fahad.jpg"), + profileBio: + "Hi there! I'm a self-taught developer who's passionate about game development.", + followers: 17, + following: 95, + ); + + static const UserModel radwan = UserModel( + handle: "RadwanAlbahrani", + name: "Radwan Albahrani", + profilePic: AssetImage("assets/profile_pics/radwan.jpeg"), + profileBio: "Self Taught Programmer with proficiency in flutter.", + followers: 38, + following: 126, + ); + + static const UserModel hassan = UserModel( + handle: "k4h23u", + name: "Hassan", + profilePic: AssetImage("assets/profile_pics/hassan.jpg"), + profileBio: "Sophomore CCSIT student at IAU, Flutter & Java developer.", + followers: 26, + following: 74, + ); +} diff --git a/lib/utils.dart b/lib/utils.dart new file mode 100644 index 0000000..082dd25 --- /dev/null +++ b/lib/utils.dart @@ -0,0 +1,43 @@ +import 'package:flutter/material.dart'; + +BoxDecoration roundedRectangle(Color color, + {Color outlineColor = Colors.black, + double roundness = 25, + double outlineThickness = 0.5, + bool shadow = false, + Color shadowColor = const Color.fromARGB(60, 0, 0, 0), + double shadowIntensity = 1, + bool bloom = false, + double bloomIntensity = 10}) { + // Initialize BoxShadows list with empty array + // Later add bloom and shadow BoxShadows to the list, if they're enabled + final List shadows = []; + + // Essentially, bloom is just a BoxShadow with the same color of the rectangle and a high blur radius + if (bloom) { + BoxShadow bloom = BoxShadow( + color: color, + blurRadius: bloomIntensity, + offset: const Offset(0, 2), + ); + + shadows.add(bloom); + } + + if (shadow) { + BoxShadow shadow = BoxShadow( + color: shadowColor, + blurRadius: shadowIntensity, + offset: const Offset(2, 2), + ); + + shadows.add(shadow); + } + + return BoxDecoration( + color: color, + border: Border.all(width: outlineThickness, color: outlineColor), + borderRadius: BorderRadius.all(Radius.circular(roundness)), + boxShadow: shadows, + ); +} diff --git a/lib/widgets/notification_item.dart b/lib/widgets/notification_item.dart new file mode 100644 index 0000000..055b2c7 --- /dev/null +++ b/lib/widgets/notification_item.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:profile_page/default_colors.dart'; +import 'package:profile_page/screens/post_inspect_screen.dart'; +import 'package:profile_page/screens/profile_page.dart'; +import 'package:profile_page/utils.dart'; +import '../models/user_model.dart'; +import '../models/post_model.dart'; + +enum NotificationType { + like, + dislike, + favorite, +} + +String notificationTypeToString(NotificationType type) { + switch (type) { + case NotificationType.like: + return "liked"; + case NotificationType.dislike: + return "disliked"; + case NotificationType.favorite: + return "favorited"; + } +} + +class NotificationItem extends StatelessWidget { + const NotificationItem({ + super.key, + required this.user, + required this.post, + required this.action, + }); + + final UserModel user; + final PostModel post; + final NotificationType action; + + @override + Widget build(BuildContext context) { + return Stack( + alignment: Alignment.center, + children: [ + // Background Container + Container( + width: 375, + height: 200, + decoration: roundedRectangle( + DefaultColors.blockColor, + outlineColor: DefaultColors.blockOutlineColor, + roundness: 5, + ), + ), + + // Profile Picture + Positioned( + left: 28, + child: GestureDetector( + onTap: () => { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Scaffold( + body: ProfilePage( + user: user, + ), + ), + ), + ) + }, + child: CircleAvatar( + foregroundImage: user.profilePic, + radius: 50, + ), + ), + ), + + // Notification Message + Positioned( + top: 10, + bottom: 130, + left: 150, + right: 30, + child: Container( + alignment: Alignment.center, + child: Text( + "${user.name} ${notificationTypeToString(action)} your post!", + textAlign: TextAlign.center, + ), + ), + ), + + // Post Display + Positioned( + top: 80, + bottom: 20, + right: 30, + left: 150, + child: GestureDetector( + onTap: () => { + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PostInspectScreen(post: post), + )) + }, + child: Container( + alignment: Alignment.center, + decoration: roundedRectangle( + DefaultColors.fieldColor, + outlineColor: DefaultColors.fieldOutlineColor, + roundness: 5, + ), + child: Text( + post.text, + style: const TextStyle(fontSize: 14), + textAlign: TextAlign.center, + ), + ), + ), + ) + ], + ); + } +} diff --git a/lib/widgets/post.dart b/lib/widgets/post.dart new file mode 100644 index 0000000..be1e187 --- /dev/null +++ b/lib/widgets/post.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:profile_page/default_colors.dart'; +import '../models/post_model.dart'; +import '../utils.dart'; +import 'profile_display.dart'; +import '../screens/post_inspect_screen.dart'; + +class Post extends StatelessWidget { + const Post({ + super.key, + required this.post, + this.inspectable = true, + }); + + final PostModel post; + final bool inspectable; + + @override + Widget build(BuildContext context) { + // The widgets to be displayed in the column + + return Container( + margin: const EdgeInsets.only(left: 24.0, right: 24.0), + decoration: roundedRectangle( + DefaultColors.blockColor, + outlineColor: DefaultColors.blockOutlineColor, + roundness: 10, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // Profile Display (top-left) + Container( + alignment: Alignment.centerLeft, + padding: const EdgeInsets.symmetric( + horizontal: 16.0, + vertical: 12.0, + ), + child: ProfileDisplay(user: post.user), + ), + + // Post Text + Attachment + GestureDetector( + onTap: () => { + if (inspectable) + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PostInspectScreen(post: post), + ), + ), + }, + child: Container( + decoration: roundedRectangle( + DefaultColors.fieldColor, + outlineColor: DefaultColors.fieldOutlineColor, + roundness: 5, + ), + alignment: (post.imageAttachment != null) + ? Alignment.center + : Alignment.topLeft, + margin: const EdgeInsets.all(8.0), + padding: const EdgeInsets.symmetric( + horizontal: 8.0, + vertical: 4.0, + ), + child: Column( + children: [ + Text( + post.text, + style: const TextStyle(fontSize: 16), + ), + // Image attachment, if it exists + if (post.imageAttachment != null) + Container( + margin: const EdgeInsets.symmetric(vertical: 6), + height: 250, + width: 250, + decoration: BoxDecoration( + image: DecorationImage(image: post.imageAttachment!)), + ), + ], + ), + ), + ), + + // Post Engagements Row + Padding( + padding: const EdgeInsets.symmetric(vertical: 4.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + mainAxisSize: MainAxisSize.max, + children: [ + // Replies + Container( + height: 60, + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + const Icon( + Icons.comment_outlined, + size: 30, + ), + Text( + (post.replies == null) ? "0" : "${post.replies}", + style: const TextStyle(fontSize: 13), + ), + ], + ), + ), + + // Likes + Container( + height: 60, + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + const Icon( + Icons.thumb_up_outlined, + size: 30, + ), + Text( + "${post.likes}", + style: const TextStyle(fontSize: 13), + ), + ], + ), + ), + + // Dislikes + Container( + height: 60, + alignment: Alignment.topCenter, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + const Icon( + Icons.thumb_down_outlined, + size: 30, + ), + Text( + "${post.dislikes}", + style: const TextStyle(fontSize: 13), + ), + ], + ), + ), + + // Favorites + Container( + height: 60, + alignment: Alignment.topCenter, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + const Icon( + Icons.favorite_outline, + size: 30, + ), + Text( + "${post.favorites}", + style: const TextStyle(fontSize: 13), + ), + ], + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/widgets/profile_banner.dart b/lib/widgets/profile_banner.dart new file mode 100644 index 0000000..cbf15f7 --- /dev/null +++ b/lib/widgets/profile_banner.dart @@ -0,0 +1,131 @@ +import 'package:flutter/material.dart'; +import '../default_colors.dart'; +import '../utils.dart'; +import '../models/user_model.dart'; + +class ProfileBanner extends StatelessWidget { + const ProfileBanner({ + super.key, + required this.user, + this.cardColor = DefaultColors.profileCardColor, + }); + + final UserModel user; + final Color cardColor; + + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, + alignment: Alignment.center, + children: [ + // The big rectangle + Container( + width: 350, + height: 400, + padding: const EdgeInsets.all(25), + decoration: roundedRectangle( + cardColor, + bloomIntensity: 40, + shadow: false, + bloom: false, + ), + ), + + // ======= Profile Picture ======= + Positioned( + // positioned at top + top: -15, + + // Avatar wrapped in PhysicalModel to cast shadow + child: PhysicalModel( + color: Colors.black, + shape: BoxShape.circle, + elevation: 16, + child: CircleAvatar( + radius: 65, + foregroundImage: user.profilePic, + ), + ), + ), + + // The name field + Positioned( + top: 110, + child: Container( + alignment: Alignment.center, + padding: const EdgeInsets.all(6), + decoration: roundedRectangle( + Colors.white, + roundness: 5, + bloomIntensity: 16, + bloom: false, + shadow: false, + ), + child: Column( + children: [ + Text( + user.name, + style: const TextStyle( + color: Color.fromARGB(255, 6, 6, 6), + fontSize: 14, + ), + textAlign: TextAlign.center, + ), + Text( + "@${user.handle}", + style: const TextStyle( + color: Color.fromARGB(255, 92, 92, 92), + fontSize: 10, + ), + ) + ], + ), + ), + ), + + // Following / Followers + Positioned( + top: 190, + bottom: 170, + child: Container( + width: 300, + decoration: roundedRectangle( + DefaultColors.fieldColor, + outlineColor: DefaultColors.fieldOutlineColor, + roundness: 8, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Text("Followers ${user.followers}"), + const SizedBox(width: 4), + Text("Following ${user.following}"), + ], + ), + ), + ), + + // Profile Bio + Positioned( + top: 250, + bottom: 20, + child: Container( + padding: const EdgeInsets.all(5), + alignment: Alignment.center, + width: 325, + decoration: roundedRectangle( + DefaultColors.fieldColor, + outlineColor: DefaultColors.fieldOutlineColor, + ), + child: Text( + (user.profileBio != null) + ? user.profileBio! + : "No Bio Provided", + textAlign: TextAlign.center), + ), + ), + ], + ); + } +} diff --git a/lib/widgets/profile_display.dart b/lib/widgets/profile_display.dart new file mode 100644 index 0000000..448e6e0 --- /dev/null +++ b/lib/widgets/profile_display.dart @@ -0,0 +1,58 @@ +import 'package:flutter/material.dart'; +import '../screens/profile_page.dart'; +import '../models/user_model.dart'; +import '../default_colors.dart'; + +class ProfileDisplay extends StatelessWidget { + const ProfileDisplay({ + super.key, + required this.user, + }); + + final UserModel user; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Scaffold( + body: ProfilePage( + user: user, + )), + )), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + CircleAvatar( + foregroundImage: user.profilePic, + radius: 30, + ), + const SizedBox(width: 10), + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + textDirection: TextDirection.ltr, + children: [ + Text( + user.name, + style: const TextStyle(fontSize: 14), + ), + Text( + "@${user.handle}", + style: const TextStyle( + color: DefaultColors.userHandleColor, + fontSize: 10, + ), + ), + ], + ) + ], + ), + ); + } +} diff --git a/lib/widgets/search_field.dart b/lib/widgets/search_field.dart new file mode 100644 index 0000000..33b5135 --- /dev/null +++ b/lib/widgets/search_field.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'package:profile_page/default_colors.dart'; +import 'package:profile_page/posts.dart'; +import 'package:profile_page/screens/search_page.dart'; +import 'package:profile_page/utils.dart'; +import '../models/post_model.dart'; + +class SearchField extends StatefulWidget { + const SearchField({super.key}); + + @override + State createState() => _SearchFieldState(); +} + +class _SearchFieldState extends State { + late TextEditingController controller; + String searchCriterion = ''; + + @override + void initState() { + super.initState(); + controller = TextEditingController(); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + const Padding( + padding: EdgeInsets.only(bottom: 8), + child: Text( + "Search Posts", + style: TextStyle( + fontSize: 16, + ), + ), + ), + + // The Text Field + Container( + width: 350, + padding: const EdgeInsets.all(20), + decoration: roundedRectangle( + DefaultColors.fieldColor, + outlineColor: DefaultColors.fieldOutlineColor, + roundness: 25, + ), + child: TextField( + controller: controller, + onSubmitted: (String value) { + setState( + () { + searchCriterion = value; + }, + ); + List searchResults = Posts.searchPosts(value); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SearchResultsPage( + searchResults: searchResults, + searchCriteria: value, + ), + ), + ); + }, + ), + ), + ], + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 89fbba6..c3a8d04 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -41,6 +41,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.18.0" + crypto: + dependency: transitive + description: + name: crypto + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab + url: "https://pub.dev" + source: hosted + version: "3.0.3" fake_async: dependency: transitive description: @@ -49,6 +57,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -120,6 +136,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -160,6 +184,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.6.1" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c + url: "https://pub.dev" + source: hosted + version: "1.3.2" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: cd210a09f7c18cbe5a02511718e0334de6559871052c90a90c0cca46a4aa81c8 + url: "https://pub.dev" + source: hosted + version: "4.3.3" vector_math: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 73078d1..1e7f808 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,4 +1,4 @@ -name: social_media +name: profile_page description: "A new Flutter project." publish_to: 'none' version: 0.1.0 @@ -7,6 +7,7 @@ environment: sdk: '>=3.2.6 <4.0.0' dependencies: + uuid: ^4.3.2 flutter: sdk: flutter @@ -17,3 +18,8 @@ dev_dependencies: flutter: uses-material-design: true + assets: + - assets/ + - assets/logos/ + - assets/profile_pics/ + - assets/posts/ \ No newline at end of file