diff --git a/.gitignore b/.gitignore index add57be..3a748d5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ bin/ obj/ /packages/ riderModule.iml -/_ReSharper.Caches/ \ No newline at end of file +/_ReSharper.Caches/ +.DS_Store \ No newline at end of file diff --git a/NewsService.cs b/NewsService.cs index 2f7de47..c5b7bdb 100644 --- a/NewsService.cs +++ b/NewsService.cs @@ -1,26 +1,44 @@ using Firebase.Database; using Firebase.Database.Query; +using Microsoft.Extensions.Logging; namespace HackerNews; -public class NewsService +public class NewsService(ILogger logger) { private const string FirebaseDatabaseUrl = "https://hacker-news.firebaseio.com/v0/"; private readonly FirebaseClient _firebaseClient = new(FirebaseDatabaseUrl); + private readonly ILogger _logger = logger; public async Task GetTopStoryAsJson() { - return await _firebaseClient - .Child("topstories.json?print=pretty") - .OnceAsJsonAsync(); + try + { + return await _firebaseClient + .Child("topstories.json?print=pretty") + .OnceAsJsonAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching top stories from Firebase"); + throw; + } } public async Task GetTopStory(string topStoryId) { - return await _firebaseClient - .Child("item") - .Child(topStoryId) - .Child(".json?print=pretty") - .OnceSingleAsync(); + try + { + return await _firebaseClient + .Child("item") + .Child(topStoryId) + .Child(".json?print=pretty") + .OnceSingleAsync(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching top story with ID: {TopStoryId}", topStoryId); + throw; + } } -} \ No newline at end of file +} diff --git a/NewsViewModel.cs b/NewsViewModel.cs index 89f6ff4..a222807 100644 --- a/NewsViewModel.cs +++ b/NewsViewModel.cs @@ -1,26 +1,38 @@ using System.Collections.ObjectModel; using CommunityToolkit.Mvvm.ComponentModel; +using Microsoft.Extensions.Logging; using Newtonsoft.Json; namespace HackerNews; -public class NewsViewModel: ObservableObject +public class NewsViewModel(NewsService newsService, ILogger logger) : ObservableObject { - public ObservableCollection TopStoryCollection { get; } = []; - - private readonly NewsService _newsService = new(); + public ObservableCollection TopStoryCollection { get; } = new ObservableCollection(); + + private readonly NewsService _newsService = newsService; + private readonly ILogger _logger = logger; public async Task Refresh() { - TopStoryCollection.Clear(); - - await foreach (var story in GetTopStories().ConfigureAwait(false)) + // Ensure this method is called on the UI thread + await MainThread.InvokeOnMainThreadAsync(async () => { - InsertIntoSortedCollection(TopStoryCollection, (a, b) => b.Score.CompareTo(a.Score), story); - //Console.WriteLine(story.Title); - } + try + { + TopStoryCollection.Clear(); + + await foreach (var story in GetTopStories().ConfigureAwait(false)) + { + InsertIntoSortedCollection(TopStoryCollection, (a, b) => b.Score.CompareTo(a.Score), story); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while refreshing top stories."); + } + }); } - + static void InsertIntoSortedCollection(ObservableCollection collection, Comparison comparison, T modelToInsert) { if (collection.Count is 0) @@ -47,13 +59,29 @@ static void InsertIntoSortedCollection(ObservableCollection collection, Co async IAsyncEnumerable GetTopStories() { - var topStories = await _newsService.GetTopStoryAsJson(); - var topStoryJson = JsonConvert.DeserializeObject>(topStories); - var getTopStoryTaskList = topStoryJson.Select(x => x.ToString()).ToList(); - foreach (var topStoryId in getTopStoryTaskList) + var stories = new List(); + + try + { + var topStories = await _newsService.GetTopStoryAsJson(); + var topStoryJson = JsonConvert.DeserializeObject>(topStories); + + foreach (var topStoryId in topStoryJson) + { + var story = await _newsService.GetTopStory(topStoryId); + stories.Add(story); // Add to the temporary list + } + } + catch (Exception ex) + { + _logger.LogError(ex, "Error while fetching top stories."); + throw; + } + + // Yield the results after the try-catch block + foreach (var story in stories) { - var story = await _newsService.GetTopStory(topStoryId); yield return story; } } -} \ No newline at end of file +} diff --git a/README.md b/README.md index 4afbd84..f8b6a17 100644 --- a/README.md +++ b/README.md @@ -1,31 +1,60 @@ -# HackerNews - -A .NET MAUI app for displaying the top posts on Hacker News. - -This app demonstrates how to use IAsyncEnumerable + C# 8.0 to improve performance. Thanks to IAsyncEnumerable, the items are added to the list as soon as they're available making the app feel faster and more responsive. - -This app also uses the Firebase Realtime Database REST API. +# Hacker News App + +Welcome to the Hacker News App! This application fetches and displays the top stories from the Hacker News API, allowing users to stay updated with the latest tech news. + +## Features + +- Fetches the latest top stories from Hacker News (up to 500 stories). +- Displays story titles, authors, and scores. +- Sorts stories by score in descending order. +- Easy to use and visually appealing interface. + +## Technologies Used + +- [.NET MAUI](https://dotnet.microsoft.com/apps/maui) for cross-platform app development. +- [Firebase](https://firebase.google.com/) for real-time data storage and retrieval. +- [CommunityToolkit.Mvvm](https://learn.microsoft.com/en-us/dotnet/communitytoolkit/mvvm/overview) for MVVM architecture. +- [Newtonsoft.Json](https://www.newtonsoft.com/json) for JSON serialization and deserialization. +- [Microsoft.Extensions.Logging](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging) for logging errors and important information. +- **IAsyncEnumerable** for improved performance when fetching top stories from the Hacker News API. + +## Installation + +To get started with the Hacker News App, follow these steps: + +1. Clone this repository: + ```bash + git clone https://github.com/iNoles/HackerNews.git + cd HackerNews + ``` +2. Open the project in your preferred IDE (e.g., Visual Studio). +3. Restore the NuGet packages: + ```bash + dotnet restore + ``` +4. Build the project: + ```bash + dotnet build + ``` +5. Run the application: + ```bash + dotnet run + ``` + +## Usage + +1. Launch the application. +2. The app will automatically fetch the top stories from Hacker News. +3. View the stories in a sorted list, with the ability to refresh the list to get the latest stories. ## Screenshots +![Hacker News App Desktop](https://github.com/iNoles/HackerNews/assets/49764/9f4ebdcb-014b-4979-a244-f81c6903f89b) -desktop - -## Getting Started - -### Prerequisites -- Install the latest version for .NET 8 -- Install [.NET MAUI workload](https://docs.microsoft.com/en-us/dotnet/maui/get-started/installation) on your development machine. - -### Clone the Repository - -```bash -git clone https://github.com/iNoles/HackerNews.git -cd HackerNews -``` +## Contributing -### Run and Build +Contributions are what make the open-source community such an amazing place to be, learn, inspire, and create. Any contributions you make are greatly appreciated. -```bash -dotnet build -dotnet run -``` +- Fork the project. +- Create your feature branch (e.g., ``git checkout -b feature/AmazingFeature``). +- Commit your changes (e.g., ``git commit -m 'Add some AmazingFeature'``). +- Push to the branch (e.g., ``git push origin feature/AmazingFeature``). \ No newline at end of file