diff --git a/src/Portfolio.Api/Program.cs b/src/Portfolio.Api/Program.cs index e093629..cdef3e7 100644 --- a/src/Portfolio.Api/Program.cs +++ b/src/Portfolio.Api/Program.cs @@ -75,7 +75,6 @@ await using (var scope = app.Services.CreateAsyncScope()) { await db.Database.EnsureCreatedAsync(); } -app.UseHttpsRedirection(); app.MapDefaultEndpoints(); app.MapControllers(); diff --git a/src/Portfolio.Web/Components/App.razor b/src/Portfolio.Web/Components/App.razor index 939e292..d0c615a 100644 --- a/src/Portfolio.Web/Components/App.razor +++ b/src/Portfolio.Web/Components/App.razor @@ -5,12 +5,15 @@ - - + + + + + diff --git a/src/Portfolio.Web/Components/CancellableComponent.cs b/src/Portfolio.Web/Components/CancellableComponent.cs new file mode 100644 index 0000000..a2aab92 --- /dev/null +++ b/src/Portfolio.Web/Components/CancellableComponent.cs @@ -0,0 +1,11 @@ +using Microsoft.AspNetCore.Components; + +namespace Portfolio.Web.Components; + +public class CancellableComponent : ComponentBase, IDisposable { + protected CancellationTokenSource TokenSource { get; } = new(); + + public void Dispose() { + TokenSource.Dispose(); + } +} \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Layout/FooterLinks.razor b/src/Portfolio.Web/Components/Layout/FooterLinks.razor new file mode 100644 index 0000000..03ffba1 --- /dev/null +++ b/src/Portfolio.Web/Components/Layout/FooterLinks.razor @@ -0,0 +1,19 @@ + diff --git a/src/Portfolio.Web/Components/Layout/FooterLinks.razor.css b/src/Portfolio.Web/Components/Layout/FooterLinks.razor.css new file mode 100644 index 0000000..dff7425 --- /dev/null +++ b/src/Portfolio.Web/Components/Layout/FooterLinks.razor.css @@ -0,0 +1,30 @@ +footer { + display: flex; + margin-top: auto; + margin-bottom: 2rem; + padding-top: 5rem; + justify-content: space-between; + margin-inline: 12.5vw; +} + +.title { + background: var(--gradient); + background-clip: text; + color: transparent; + user-select: none; + font-weight: 500; +} + +a { + text-decoration: none; +} + +.socials { + display: flex; + gap: 0.75rem; +} + +.socials a { + height: 25px; + display: flex; +} diff --git a/src/Portfolio.Web/Components/Layout/MainLayout.razor b/src/Portfolio.Web/Components/Layout/MainLayout.razor index e3b2918..1b11967 100644 --- a/src/Portfolio.Web/Components/Layout/MainLayout.razor +++ b/src/Portfolio.Web/Components/Layout/MainLayout.razor @@ -1,23 +1,13 @@ @inherits LayoutComponentBase -
- +
+ -
-
- About -
- -
- @Body -
-
-
- -
- An unhandled error has occurred. - Reload - 🗙 -
\ No newline at end of file +
+ @Body +
+ + + \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Layout/MainLayout.razor.css b/src/Portfolio.Web/Components/Layout/MainLayout.razor.css index 38d1f25..9e6e6ae 100644 --- a/src/Portfolio.Web/Components/Layout/MainLayout.razor.css +++ b/src/Portfolio.Web/Components/Layout/MainLayout.razor.css @@ -1,98 +1,17 @@ -.page { - position: relative; +main { + background-color: var(--background); + min-height: 100%; display: flex; flex-direction: column; } -main { - flex: 1; -} - -.sidebar { - background-image: linear-gradient(180deg, rgb(5, 39, 103) 0%, #3a0647 70%); -} - -.top-row { - background-color: #f7f7f7; - border-bottom: 1px solid #d6d5d5; - justify-content: flex-end; - height: 3.5rem; - display: flex; - align-items: center; -} - - .top-row ::deep a, .top-row ::deep .btn-link { - white-space: nowrap; - margin-left: 1.5rem; - text-decoration: none; - } - - .top-row ::deep a:hover, .top-row ::deep .btn-link:hover { - text-decoration: underline; - } - - .top-row ::deep a:first-child { - overflow: hidden; - text-overflow: ellipsis; - } - -@media (max-width: 640.98px) { - .top-row { - justify-content: space-between; - } - - .top-row ::deep a, .top-row ::deep .btn-link { - margin-left: 0; - } -} - -@media (min-width: 641px) { - .page { - flex-direction: row; - } - - .sidebar { - width: 250px; - height: 100vh; - position: sticky; - top: 0; - } - - .top-row { - position: sticky; - top: 0; - z-index: 1; - } - - .top-row.auth ::deep a:first-child { - flex: 1; - text-align: right; - width: 0; - } - - .top-row, article { - padding-left: 2rem !important; - padding-right: 1.5rem !important; - } -} - -#blazor-error-ui { - color-scheme: light only; - background: lightyellow; - bottom: 0; - box-shadow: 0 -1px 2px rgba(0, 0, 0, 0.2); - box-sizing: border-box; - display: none; - left: 0; - padding: 0.6rem 1.25rem 0.7rem 1.25rem; - position: fixed; +.nav-box { width: 100%; - z-index: 1000; + position: sticky; + top: 0; } - #blazor-error-ui .dismiss { - cursor: pointer; - position: absolute; - right: 0.75rem; - top: 0.5rem; - } +.content { + flex-grow: 1; + margin-inline: 12.5vw; +} diff --git a/src/Portfolio.Web/Components/Layout/NavMenu.razor b/src/Portfolio.Web/Components/Layout/NavMenu.razor deleted file mode 100644 index 0b37b9d..0000000 --- a/src/Portfolio.Web/Components/Layout/NavMenu.razor +++ /dev/null @@ -1,29 +0,0 @@ - - - - - \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Layout/NavMenu.razor.css b/src/Portfolio.Web/Components/Layout/NavMenu.razor.css deleted file mode 100644 index a2aeace..0000000 --- a/src/Portfolio.Web/Components/Layout/NavMenu.razor.css +++ /dev/null @@ -1,105 +0,0 @@ -.navbar-toggler { - appearance: none; - cursor: pointer; - width: 3.5rem; - height: 2.5rem; - color: white; - position: absolute; - top: 0.5rem; - right: 1rem; - border: 1px solid rgba(255, 255, 255, 0.1); - background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 30 30'%3e%3cpath stroke='rgba%28255, 255, 255, 0.55%29' stroke-linecap='round' stroke-miterlimit='10' stroke-width='2' d='M4 7h22M4 15h22M4 23h22'/%3e%3c/svg%3e") no-repeat center/1.75rem rgba(255, 255, 255, 0.1); -} - -.navbar-toggler:checked { - background-color: rgba(255, 255, 255, 0.5); -} - -.top-row { - min-height: 3.5rem; - background-color: rgba(0,0,0,0.4); -} - -.navbar-brand { - font-size: 1.1rem; -} - -.bi { - display: inline-block; - position: relative; - width: 1.25rem; - height: 1.25rem; - margin-right: 0.75rem; - top: -1px; - background-size: cover; -} - -.bi-house-door-fill-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-house-door-fill' viewBox='0 0 16 16'%3E%3Cpath d='M6.5 14.5v-3.505c0-.245.25-.495.5-.495h2c.25 0 .5.25.5.5v3.5a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5v-7a.5.5 0 0 0-.146-.354L13 5.793V2.5a.5.5 0 0 0-.5-.5h-1a.5.5 0 0 0-.5.5v1.293L8.354 1.146a.5.5 0 0 0-.708 0l-6 6A.5.5 0 0 0 1.5 7.5v7a.5.5 0 0 0 .5.5h4a.5.5 0 0 0 .5-.5Z'/%3E%3C/svg%3E"); -} - -.bi-plus-square-fill-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-plus-square-fill' viewBox='0 0 16 16'%3E%3Cpath d='M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2H2zm6.5 4.5v3h3a.5.5 0 0 1 0 1h-3v3a.5.5 0 0 1-1 0v-3h-3a.5.5 0 0 1 0-1h3v-3a.5.5 0 0 1 1 0z'/%3E%3C/svg%3E"); -} - -.bi-list-nested-nav-menu { - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='white' class='bi bi-list-nested' viewBox='0 0 16 16'%3E%3Cpath fill-rule='evenodd' d='M4.5 11.5A.5.5 0 0 1 5 11h10a.5.5 0 0 1 0 1H5a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 3 7h10a.5.5 0 0 1 0 1H3a.5.5 0 0 1-.5-.5zm-2-4A.5.5 0 0 1 1 3h10a.5.5 0 0 1 0 1H1a.5.5 0 0 1-.5-.5z'/%3E%3C/svg%3E"); -} - -.nav-item { - font-size: 0.9rem; - padding-bottom: 0.5rem; -} - - .nav-item:first-of-type { - padding-top: 1rem; - } - - .nav-item:last-of-type { - padding-bottom: 1rem; - } - - .nav-item ::deep .nav-link { - color: #d7d7d7; - background: none; - border: none; - border-radius: 4px; - height: 3rem; - display: flex; - align-items: center; - line-height: 3rem; - width: 100%; - } - -.nav-item ::deep a.active { - background-color: rgba(255,255,255,0.37); - color: white; -} - -.nav-item ::deep .nav-link:hover { - background-color: rgba(255,255,255,0.1); - color: white; -} - -.nav-scrollable { - display: none; -} - -.navbar-toggler:checked ~ .nav-scrollable { - display: block; -} - -@media (min-width: 641px) { - .navbar-toggler { - display: none; - } - - .nav-scrollable { - /* Never collapse the sidebar for wide screens */ - display: block; - - /* Allow sidebar to scroll for tall menus */ - height: calc(100vh - 3.5rem); - overflow-y: auto; - } -} diff --git a/src/Portfolio.Web/Components/Layout/Navigation.razor b/src/Portfolio.Web/Components/Layout/Navigation.razor new file mode 100644 index 0000000..f739cd2 --- /dev/null +++ b/src/Portfolio.Web/Components/Layout/Navigation.razor @@ -0,0 +1,25 @@ + diff --git a/src/Portfolio.Web/Components/Layout/Navigation.razor.css b/src/Portfolio.Web/Components/Layout/Navigation.razor.css new file mode 100644 index 0000000..407a864 --- /dev/null +++ b/src/Portfolio.Web/Components/Layout/Navigation.razor.css @@ -0,0 +1,58 @@ +.navigation { + display: flex; + height: 50px; + justify-content: space-between; + align-items: center; + background-color: var(--background); + border-bottom: 1px solid var(--border-color); + padding-inline: 0.5rem; +} + +.links { + display: flex; + gap: 1rem; +} + +.navigation > a { + text-decoration: none; + font-size: 1.25rem; +} + +.socials { + display: flex; + gap: 0.75rem; +} + +.socials a { + height: 25px; + display: flex; +} + +.links ::deep a { + text-decoration: none; + position: relative; + + &:before { + content: ""; + position: absolute; + width: 100%; + height: 2px; + bottom: 0; + left: 0; + background-color: var(--text); + visibility: hidden; + transform: scaleX(0); + transform-origin: left; + transition: all 0.3s ease-in-out; + } +} + +.links ::deep a.active::before { + visibility: visible; + transform: scaleX(1); +} + +.links ::deep a:hover::before { + visibility: visible; + transform: scaleX(1); +} diff --git a/src/Portfolio.Web/Components/Pages/Counter.razor b/src/Portfolio.Web/Components/Pages/Counter.razor deleted file mode 100644 index 15c11c3..0000000 --- a/src/Portfolio.Web/Components/Pages/Counter.razor +++ /dev/null @@ -1,19 +0,0 @@ -@page "/counter" -@rendermode InteractiveServer - -Counter - -

Counter

- -

Current count: @currentCount

- - - -@code { - private int currentCount = 0; - - private void IncrementCount() { - currentCount++; - } - -} \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Pages/Home.razor b/src/Portfolio.Web/Components/Pages/Home.razor index dfcdf75..8902f34 100644 --- a/src/Portfolio.Web/Components/Pages/Home.razor +++ b/src/Portfolio.Web/Components/Pages/Home.razor @@ -1,6 +1,6 @@ @page "/" -Home +Portfolio von Leon Hoppe

Hello, world!

diff --git a/src/Portfolio.Web/Components/Pages/Weather.razor b/src/Portfolio.Web/Components/Pages/Weather.razor deleted file mode 100644 index f8204c6..0000000 --- a/src/Portfolio.Web/Components/Pages/Weather.razor +++ /dev/null @@ -1,61 +0,0 @@ -@page "/weather" -@attribute [StreamRendering] - -Weather - -

Weather

- -

This component demonstrates showing data.

- -@if (forecasts == null) { -

- Loading... -

-} -else { - - - - - - - - - - - @foreach (var forecast in forecasts) { - - - - - - - } - -
DateTemp. (C)Temp. (F)Summary
@forecast.Date.ToShortDateString()@forecast.TemperatureC@forecast.TemperatureF@forecast.Summary
-} - -@code { - private WeatherForecast[]? forecasts; - - protected override async Task OnInitializedAsync() { - // Simulate asynchronous loading to demonstrate streaming rendering - await Task.Delay(500); - - var startDate = DateOnly.FromDateTime(DateTime.Now); - var summaries = new[] { "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" }; - forecasts = Enumerable.Range(1, 5).Select(index => new WeatherForecast { - Date = startDate.AddDays(index), - TemperatureC = Random.Shared.Next(-20, 55), - Summary = summaries[Random.Shared.Next(summaries.Length)] - }).ToArray(); - } - - private class WeatherForecast { - public DateOnly Date { get; set; } - public int TemperatureC { get; set; } - public string? Summary { get; set; } - public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); - } - -} \ No newline at end of file diff --git a/src/Portfolio.Web/Components/Routes.razor b/src/Portfolio.Web/Components/Routes.razor index ae94e9e..090a0f6 100644 --- a/src/Portfolio.Web/Components/Routes.razor +++ b/src/Portfolio.Web/Components/Routes.razor @@ -1,6 +1,5 @@  - \ No newline at end of file diff --git a/src/Portfolio.Web/Program.cs b/src/Portfolio.Web/Program.cs index d82cbbf..6ab644c 100644 --- a/src/Portfolio.Web/Program.cs +++ b/src/Portfolio.Web/Program.cs @@ -1,4 +1,6 @@ +using Portfolio.Shared.Services; using Portfolio.Web.Components; +using Portfolio.Web.Services; var builder = WebApplication.CreateBuilder(args); @@ -8,6 +10,15 @@ builder.Services.AddRazorComponents() builder.AddServiceDefaults(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); +builder.Services.AddScoped(); + +builder.Services.AddHttpClient("api", client => { + client.BaseAddress = new Uri("http://api"); +}); + var app = builder.Build(); // Configure the HTTP request pipeline. @@ -17,7 +28,6 @@ if (!app.Environment.IsDevelopment()) { app.UseHsts(); } -app.UseHttpsRedirection(); app.MapDefaultEndpoints(); app.UseAntiforgery(); diff --git a/src/Portfolio.Web/Services/AboutRepository.cs b/src/Portfolio.Web/Services/AboutRepository.cs new file mode 100644 index 0000000..e666782 --- /dev/null +++ b/src/Portfolio.Web/Services/AboutRepository.cs @@ -0,0 +1,20 @@ +using Portfolio.Shared.Models; +using Portfolio.Shared.Services; + +namespace Portfolio.Web.Services; + +internal sealed class AboutRepository(IHttpClientFactory factory) : IAboutRepository { + private About DefaultValue => new() { + AboutMe = string.Empty, + Future = string.Empty + }; + + public async Task GetAbout(CancellationToken ct) { + var client = factory.CreateClient("api"); + var response = await client.GetAsync("api/about", ct); + if (!response.IsSuccessStatusCode) return DefaultValue; + + var data = await response.Content.ReadFromJsonAsync(ct); + return data ?? DefaultValue; + } +} \ No newline at end of file diff --git a/src/Portfolio.Web/Services/ProjectRepository.cs b/src/Portfolio.Web/Services/ProjectRepository.cs new file mode 100644 index 0000000..eccefc4 --- /dev/null +++ b/src/Portfolio.Web/Services/ProjectRepository.cs @@ -0,0 +1,15 @@ +using Portfolio.Shared.Models; +using Portfolio.Shared.Services; + +namespace Portfolio.Web.Services; + +internal sealed class ProjectRepository(IHttpClientFactory factory) : IProjectRepository { + public async Task> GetProjects(CancellationToken ct) { + var client = factory.CreateClient("api"); + var response = await client.GetAsync("api/projects", ct); + if (!response.IsSuccessStatusCode) return []; + + var data = await response.Content.ReadFromJsonAsync>(ct); + return data ?? []; + } +} \ No newline at end of file diff --git a/src/Portfolio.Web/Services/TechnologyRepository.cs b/src/Portfolio.Web/Services/TechnologyRepository.cs new file mode 100644 index 0000000..d723a6d --- /dev/null +++ b/src/Portfolio.Web/Services/TechnologyRepository.cs @@ -0,0 +1,15 @@ +using Portfolio.Shared.Models; +using Portfolio.Shared.Services; + +namespace Portfolio.Web.Services; + +internal sealed class TechnologyRepository(IHttpClientFactory factory) : ITechnologyRepository { + public async Task> GetTechnologies(CancellationToken ct) { + var client = factory.CreateClient("api"); + var response = await client.GetAsync("api/technologies", ct); + if (!response.IsSuccessStatusCode) return []; + + var data = await response.Content.ReadFromJsonAsync>(ct); + return data ?? []; + } +} \ No newline at end of file diff --git a/src/Portfolio.Web/Services/TimelineRepository.cs b/src/Portfolio.Web/Services/TimelineRepository.cs new file mode 100644 index 0000000..0057e61 --- /dev/null +++ b/src/Portfolio.Web/Services/TimelineRepository.cs @@ -0,0 +1,15 @@ +using Portfolio.Shared.Models; +using Portfolio.Shared.Services; + +namespace Portfolio.Web.Services; + +internal sealed class TimelineRepository(IHttpClientFactory factory) : ITimelineRepository { + public async Task> GetTimeline(TimelineEntryType type, CancellationToken ct) { + var client = factory.CreateClient("api"); + var result = await client.GetAsync($"api/timeline/{type}", ct); + if (!result.IsSuccessStatusCode) return []; + + var data = await result.Content.ReadFromJsonAsync>(ct); + return data ?? []; + } +} \ No newline at end of file diff --git a/src/Portfolio.Web/wwwroot/app.css b/src/Portfolio.Web/wwwroot/app.css index e69de29..e9aa8a2 100644 --- a/src/Portfolio.Web/wwwroot/app.css +++ b/src/Portfolio.Web/wwwroot/app.css @@ -0,0 +1,32 @@ +:root { + --primary: #8e5bd2; + --secondary: #11a8bd; + + --gradient: linear-gradient(90deg, var(--primary), var(--secondary)); + --gradient-angled: linear-gradient(135deg, var(--primary), var(--secondary)); + --gradient-straight: linear-gradient(var(--primary), var(--secondary)); + + --padding: 12.5vw; + --padding-small: 5vw; + --mobile-width: 750px; + + --desc-color: #7c8393; + --border-color: #2d2d2d; + + --text: #ffffff; + --background: #0f1724; +} + +* { + box-sizing: border-box; + color: var(--text); + font-family: "Ubuntu", serif; + font-weight: 400; + font-style: normal; +} + +html, body { + margin: 0; + padding: 0; + height: 100%; +} diff --git a/src/Portfolio.Web/wwwroot/favicon.ico b/src/Portfolio.Web/wwwroot/favicon.ico new file mode 100644 index 0000000..0073cd5 Binary files /dev/null and b/src/Portfolio.Web/wwwroot/favicon.ico differ diff --git a/src/Portfolio.Web/wwwroot/favicon.png b/src/Portfolio.Web/wwwroot/favicon.png deleted file mode 100644 index a1dc44a..0000000 --- a/src/Portfolio.Web/wwwroot/favicon.png +++ /dev/null @@ -1,3 +0,0 @@ -version https://git-lfs.github.com/spec/v1 -oid sha256:e265ac0f2dda1e5dfa65b1adf330722bb3ef7789115283604d8cd19f098f1f08 -size 1148 diff --git a/src/Portfolio.Web/wwwroot/socials/gitlab.png b/src/Portfolio.Web/wwwroot/socials/gitlab.png new file mode 100644 index 0000000..a294eb8 --- /dev/null +++ b/src/Portfolio.Web/wwwroot/socials/gitlab.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:72a2cad5025aa931d6ea56c3201d1f18e68a8cd39788c7c80d5b2b82aa5143ef +size 591 diff --git a/src/Portfolio.Web/wwwroot/socials/instagram.png b/src/Portfolio.Web/wwwroot/socials/instagram.png new file mode 100644 index 0000000..473e499 --- /dev/null +++ b/src/Portfolio.Web/wwwroot/socials/instagram.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:fdc2ac0085453fedb24be138132b4858add40ec998259ae94fafb9decd459e69 +size 1772 diff --git a/src/Portfolio.Web/wwwroot/socials/mail.png b/src/Portfolio.Web/wwwroot/socials/mail.png new file mode 100644 index 0000000..f0cb6ba --- /dev/null +++ b/src/Portfolio.Web/wwwroot/socials/mail.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:407b558182f98f948d045cd6b4ea62bec3b8a8322cada63b2b07a7ea12393ffe +size 1459