Skip to content

Commit 42c9c70

Browse files
committed
Implemented a "Create new portfolio" page that displays a list of existing portfolios and allows the user to create new ones.
1 parent bfbc994 commit 42c9c70

File tree

7 files changed

+208
-11
lines changed

7 files changed

+208
-11
lines changed

Repository/SqlKataRepository.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,8 @@ public bool Update(T entry)
3838

3939
public bool Delete(T entry)
4040
{
41-
Db.Get().Query(tableName).Where("id", _getEntryId(entry)).AsDelete();
42-
// TODO
41+
Db.Get().Query(tableName).Where("id", _getEntryId(entry)).Delete();
42+
// TODO add tests
4343
return true;
4444
}
4545

Utils/EnumUtils.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
5+
namespace Utils
6+
{
7+
public class EnumUtils
8+
{
9+
public static List<TEnum> GetEnumList<TEnum>() where TEnum : Enum
10+
=> ((TEnum[])Enum.GetValues(typeof(TEnum))).ToList();
11+
}
12+
}

WebFrontend/App.razor

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
<Router AppAssembly="@typeof(Program).Assembly" PreferExactMatches="@true">
22
<Found Context="routeData">
3-
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
3+
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)"/>
4+
<MatPortalHost/>
5+
<MatToastContainer />
46
</Found>
57
<NotFound>
68
<LayoutView Layout="@typeof(MainLayout)">

WebFrontend/Pages/NewPortfolio.razor

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
@page "/newportfolio"
2+
@using Model
3+
@using Services
4+
@using Utils
5+
@using System.ComponentModel.DataAnnotations
6+
@inject IPortfolioService PortfolioService
7+
@inject IMatDialogService MatDialogService
8+
@inject IMatToaster Toaster
9+
10+
11+
<style>
12+
.demo-mat-card {
13+
max-width: 400px;
14+
margin-bottom: 2em;
15+
}
16+
17+
.demo-mat-card-content {
18+
padding: 1rem;
19+
}
20+
21+
.demo-mat-card-clean-margin {
22+
margin: 0px;
23+
}
24+
</style>
25+
26+
<div class="mat-layout-grid">
27+
<div class="mat-layout-grid-inner">
28+
<div class="mat-layout-grid-cell">
29+
<h2>Create a new portfolio</h2>
30+
<EditForm Model="FormModel" OnValidSubmit="SaveButtonClicked">
31+
<DataAnnotationsValidator/>
32+
<p>
33+
<MatTextField @bind-Value="@FormModel.Name" Label="Portfolio name"></MatTextField>
34+
<ValidationMessage For="@(() => FormModel.Name)"/>
35+
</p>
36+
37+
<p>
38+
<MatTextField @bind-Value="@FormModel.Description" Label="Description"></MatTextField>
39+
<ValidationMessage For="@(() => FormModel.Description)"/>
40+
</p>
41+
<MatRadioGroup @bind-Value="@FormModel.SelectedCurrency" Items="@AvailableCurrencies">
42+
<ItemTemplate Context="currencyContext">
43+
<div>
44+
<MatRadioButton Value="@currencyContext">@GetCurrencyLabel(@currencyContext)</MatRadioButton>
45+
</div>
46+
</ItemTemplate>
47+
</MatRadioGroup>
48+
<ValidationMessage For="@(() => FormModel.SelectedCurrency)"/>
49+
<p>
50+
<MatButton Raised="true" Type="submit">Create</MatButton>
51+
</p>
52+
</EditForm>
53+
</div>
54+
<div class="mat-layout-grid-cell">
55+
<h2>Existing portfolios</h2>
56+
@if (_existingPortfolios == null)
57+
{
58+
<MatProgressBar Indeterminate="true"></MatProgressBar>
59+
}
60+
else if (_existingPortfolios.Count > 0)
61+
{
62+
@foreach (var portfolio in _existingPortfolios)
63+
{
64+
<MatCard class="demo-mat-card">
65+
<MatCardContent>
66+
<div class="demo-mat-card-content">
67+
<MatHeadline6 class="demo-mat-card-clean-margin">
68+
<MatChipSet>
69+
@portfolio.Name
70+
<MatChip Label="@GetCurrencyLabel(portfolio.Currency)"/>
71+
</MatChipSet>
72+
</MatHeadline6>
73+
</div>
74+
75+
<MatBody2 class="demo-mat-card-content demo-mat-card-clean-margin">
76+
@portfolio.Description
77+
</MatBody2>
78+
</MatCardContent>
79+
<MatCardActions>
80+
<MatCardActionButtons>
81+
<MatButton>View</MatButton>
82+
</MatCardActionButtons>
83+
84+
<MatCardActionIcons>
85+
<MatIconButton Icon="@MatIconNames.Delete" OnClick="(_) => DeletePortfolio(portfolio)"></MatIconButton>
86+
</MatCardActionIcons>
87+
</MatCardActions>
88+
</MatCard>
89+
}
90+
}
91+
else
92+
{
93+
<MatSubtitle2>No portfolios found</MatSubtitle2>
94+
}
95+
96+
</div>
97+
</div>
98+
</div>
99+
100+
101+
@code
102+
{
103+
protected Currency[] AvailableCurrencies = EnumUtils.GetEnumList<Currency>().ToArray();
104+
protected const Currency DefaultCurerncy = Currency.Usd;
105+
106+
protected CreateFormModel FormModel = new();
107+
List<Portfolio> _existingPortfolios;
108+
109+
public class CreateFormModel
110+
{
111+
[Required]
112+
[MinLength(1)]
113+
public string Name { get; set; }
114+
115+
[Required]
116+
[MinLength(1)]
117+
public string Description { get; set; }
118+
119+
[Required] public Currency SelectedCurrency = DefaultCurerncy;
120+
121+
public void Reset()
122+
{
123+
Name = "";
124+
Description = "";
125+
SelectedCurrency = DefaultCurerncy;
126+
}
127+
}
128+
129+
protected override async Task OnInitializedAsync()
130+
{
131+
_existingPortfolios = PortfolioService.GetPortfolios();
132+
}
133+
134+
protected string GetCurrencyLabel(Currency currency)
135+
{
136+
switch (currency)
137+
{
138+
case Currency.Czk:
139+
return "CZK";
140+
case Currency.Eur:
141+
return "EUR";
142+
case Currency.Usd:
143+
return "USD";
144+
}
145+
return "UNDEFINED";
146+
}
147+
148+
void SaveButtonClicked()
149+
{
150+
PortfolioService.CreatePortfolio(FormModel.Name, FormModel.Description, FormModel.SelectedCurrency);
151+
FormModel.Reset();
152+
_existingPortfolios = PortfolioService.GetPortfolios();
153+
Toaster.Add("New portfolio successfully added", MatToastType.Success, "", "");
154+
}
155+
156+
async void DeletePortfolio(Portfolio portfolio)
157+
{
158+
var result = await MatDialogService.ConfirmAsync("Do you really wish to delete this portfolio including all of it's portfolio entries and market orders?");
159+
if (result)
160+
{
161+
PortfolioService.DeletePortfolio(portfolio);
162+
_existingPortfolios = PortfolioService.GetPortfolios();
163+
StateHasChanged();
164+
Toaster.Add($"Portfolio \"{portfolio.Name}\" sucessfully deleted", MatToastType.Info, "", "");
165+
}
166+
}
167+
}

WebFrontend/Shared/NavMenu.razor

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<MatNavMenu>
22
<MatNavItem Href=""><MatIcon Icon="home"></MatIcon>&nbsp; Home </MatNavItem>
3+
<MatNavItem Href="newportfolio"><MatIcon Icon="add"></MatIcon>&nbsp; New portfolio </MatNavItem>
34
<MatNavItem Href="counter"><MatIcon Icon="add"></MatIcon>&nbsp; Counter</MatNavItem>
45
<MatNavItem Href="fetchdata"><MatIcon Icon="list"></MatIcon>&nbsp; Fetch Data</MatNavItem>
56
</MatNavMenu>

WebFrontend/Startup.cs

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
using System.Net.Http;
33
using CryptoStatsSource;
44
using Database;
5+
using MatBlazor;
56
using Microsoft.AspNetCore.Builder;
67
using Microsoft.AspNetCore.Hosting;
78
using Microsoft.Data.Sqlite;
@@ -11,6 +12,7 @@
1112
using Model;
1213
using Repository;
1314
using ServerSideBlazor.Data;
15+
using Services;
1416
using SqlKata.Compilers;
1517

1618
namespace WebFrontend
@@ -32,21 +34,33 @@ public void ConfigureServices(IServiceCollection services)
3234
services.AddRazorPages();
3335
services.AddServerSideBlazor();
3436
services.AddSingleton<WeatherForecastService>();
35-
37+
services.AddMatBlazor();
38+
39+
services.AddMatToaster(config =>
40+
{
41+
config.Position = MatToastPosition.BottomCenter;
42+
config.PreventDuplicates = true;
43+
config.NewestOnTop = true;
44+
config.ShowCloseButton = true;
45+
config.MaximumOpacity = 95;
46+
config.VisibleStateDuration = 3000;
47+
});
3648

3749
services.AddScoped<ICryptoStatsSource, CoingeckoSource>();
3850

3951
// TODO ensure that SqlKataDatabase gets disposed
4052
var dbConnection = new SqliteConnection("Data Source=data.db");
4153
var db = new SqlKataDatabase(dbConnection, new SqliteCompiler());
42-
var portfolioRepository = new SqlKataPortfolioRepository(db);
43-
portfolioRepository.Add(new Portfolio("My portfolio", "ADA holdings"));
44-
foreach (var portfolio in portfolioRepository.All())
45-
{
46-
Console.WriteLine($"{portfolio.Name} - {portfolio.Description}");
47-
}
48-
dbConnection.Close();
4954
services.AddSingleton(ctx => db);
55+
56+
services.AddSingleton<IPortfolioRepository, SqlKataPortfolioRepository>();
57+
services.AddSingleton<IMarketOrderRepository, SqlKataMarketOrderRepository>();
58+
services.AddSingleton<IPortfolioEntryRepository, SqlKataPortfolioEntryRepository>();
59+
60+
services.AddSingleton<IPortfolioService, PortfolioServiceImpl>();
61+
services.AddSingleton<IMarketOrderService, MarketOrderServiceImpl>();
62+
services.AddSingleton<IPortfolioEntryService, PortfolioEntryServiceImpl>();
63+
5064
}
5165

5266
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.

WebFrontend/WebFrontend.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
<ItemGroup>
1717
<ProjectReference Include="..\CryptoStatsSource\CryptoStatsSource.csproj" />
1818
<ProjectReference Include="..\Repository\Repository.csproj" />
19+
<ProjectReference Include="..\Services\Services\Services.csproj" />
1920
</ItemGroup>
2021

2122
</Project>

0 commit comments

Comments
 (0)