Merge branch 'feature/technologyPage' into 'dev'
Resolve "Technology Page" Closes #5 See merge request leon.hoppe/Portfolio!5
This commit was merged in pull request #21.
This commit is contained in:
@@ -7,8 +7,6 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbCont
|
|||||||
|
|
||||||
public DbSet<Project> Projects { get; set; }
|
public DbSet<Project> Projects { get; set; }
|
||||||
|
|
||||||
public DbSet<Language> Languages { get; set; }
|
|
||||||
|
|
||||||
public DbSet<Technology> Technologies { get; set; }
|
public DbSet<Technology> Technologies { get; set; }
|
||||||
|
|
||||||
public DbSet<TimelineEntry> Timeline { get; set; }
|
public DbSet<TimelineEntry> Timeline { get; set; }
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ builder.Services.AddHopFrame(options => {
|
|||||||
.SetValue(langConfig, true);
|
.SetValue(langConfig, true);
|
||||||
|
|
||||||
table.Property(p => p.Languages)
|
table.Property(p => p.Languages)
|
||||||
.FormatEach<Language>((l, _) => l.Label)
|
.FormatEach<Technology>((l, _) => l.Name)
|
||||||
.List(false);
|
.List(false);
|
||||||
|
|
||||||
table.Property(p => p.Cover)
|
table.Property(p => p.Cover)
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
using System.ComponentModel.DataAnnotations;
|
|
||||||
using System.ComponentModel.DataAnnotations.Schema;
|
|
||||||
|
|
||||||
namespace Portfolio.Shared.Models;
|
|
||||||
|
|
||||||
public sealed class Language {
|
|
||||||
|
|
||||||
[Key, DatabaseGenerated(DatabaseGeneratedOption.Identity)]
|
|
||||||
public int Id { get; init; }
|
|
||||||
|
|
||||||
[MaxLength(255)]
|
|
||||||
public required string Label { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(255)]
|
|
||||||
public required string Identifier { get; set; }
|
|
||||||
|
|
||||||
[MaxLength(255)]
|
|
||||||
public string? Suffix { get; set; }
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -26,8 +26,8 @@ public sealed class Project {
|
|||||||
|
|
||||||
public ProjectStatus Status { get; set; } = ProjectStatus.Finished;
|
public ProjectStatus Status { get; set; } = ProjectStatus.Finished;
|
||||||
|
|
||||||
[ForeignKey("languages")]
|
[ForeignKey("technologies")]
|
||||||
public List<Language> Languages { get; init; } = new();
|
public List<Technology> Languages { get; init; } = new();
|
||||||
|
|
||||||
public DateTime Created { get; init; } = DateTime.Now;
|
public DateTime Created { get; init; } = DateTime.Now;
|
||||||
|
|
||||||
|
|||||||
@@ -10,13 +10,27 @@ public sealed class Technology {
|
|||||||
|
|
||||||
[MaxLength(255)]
|
[MaxLength(255)]
|
||||||
public required string Name { get; set; }
|
public required string Name { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(255)]
|
||||||
|
public required string Identifier { get; set; }
|
||||||
|
|
||||||
|
[MaxLength(255)]
|
||||||
|
public string? Suffix { get; set; }
|
||||||
|
|
||||||
public TechnologyLevel Level { get; set; } = TechnologyLevel.Beginner;
|
public TechnologyLevel Level { get; set; } = TechnologyLevel.Beginner;
|
||||||
|
|
||||||
|
public TechnologyType Type { get; set; } = TechnologyType.Language;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum TechnologyLevel : byte {
|
public enum TechnologyLevel {
|
||||||
Beginner = 0,
|
Beginner = 0,
|
||||||
Intermediate = 1,
|
Intermediate = 1,
|
||||||
Professional = 2
|
Professional = 2
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public enum TechnologyType {
|
||||||
|
Language = 0,
|
||||||
|
Framework = 1,
|
||||||
|
Additional = 2
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
<base href="/"/>
|
<base href="/"/>
|
||||||
<link rel="stylesheet" href="@Assets["app.css"]"/>
|
<link rel="stylesheet" href="@Assets["app.css"]"/>
|
||||||
<link rel="stylesheet" href="@Assets["Portfolio.Web.styles.css"]"/>
|
<link rel="stylesheet" href="@Assets["Portfolio.Web.styles.css"]"/>
|
||||||
|
<script src="@Assets["scroll-handler.js"]"></script>
|
||||||
<ImportMap/>
|
<ImportMap/>
|
||||||
<link rel="icon" href="favicon.ico"/>
|
<link rel="icon" href="favicon.ico"/>
|
||||||
<HeadOutlet/>
|
<HeadOutlet/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
@using Portfolio.Shared.Models
|
@using Portfolio.Shared.Models
|
||||||
|
|
||||||
<div class="border">
|
<div class="border" style="@("--index: " + Index)">
|
||||||
<div class="shell">
|
<div class="shell">
|
||||||
<img src="@Project.Cover" alt="project-cover">
|
<img src="@Project.Cover" alt="project-cover">
|
||||||
<h3>@Project.Name</h3>
|
<h3>@Project.Name</h3>
|
||||||
@@ -32,6 +32,9 @@
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public required Project Project { get; set; }
|
public required Project Project { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public required int Index { get; set; }
|
||||||
|
|
||||||
private string GetStatusName() {
|
private string GetStatusName() {
|
||||||
return Project.Status switch {
|
return Project.Status switch {
|
||||||
ProjectStatus.Finished => "Fertig",
|
ProjectStatus.Finished => "Fertig",
|
||||||
|
|||||||
@@ -6,6 +6,19 @@
|
|||||||
border-radius: 30px;
|
border-radius: 30px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 0 40px -10px var(--primary);
|
box-shadow: 0 0 40px -10px var(--primary);
|
||||||
|
opacity: 0;
|
||||||
|
animation: animate-in 200ms forwards;
|
||||||
|
animation-delay: calc(var(--index) * 200ms);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes animate-in {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.shell {
|
.shell {
|
||||||
|
|||||||
40
src/Portfolio.Web/Components/Components/TechnologyView.razor
Normal file
40
src/Portfolio.Web/Components/Components/TechnologyView.razor
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
@rendermode InteractiveServer
|
||||||
|
@using Portfolio.Shared.Models
|
||||||
|
|
||||||
|
<div class="technology">
|
||||||
|
<div class="tech-header">
|
||||||
|
<h2 class="tech-name">@Technology.Name</h2>
|
||||||
|
<span class="tech-level">@GetTechnologyLevelName()</span>
|
||||||
|
</div>
|
||||||
|
<div class="@("tech-progress level-" + (int)Technology.Level)" @ref="_element"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@inject IJSRuntime Runtime
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public required Technology Technology { get; set; }
|
||||||
|
|
||||||
|
private ElementReference _element;
|
||||||
|
|
||||||
|
public string GetTechnologyLevelName() {
|
||||||
|
switch (Technology.Level) {
|
||||||
|
case TechnologyLevel.Beginner:
|
||||||
|
return "Anfänger";
|
||||||
|
case TechnologyLevel.Intermediate:
|
||||||
|
return "Erweitert";
|
||||||
|
case TechnologyLevel.Professional:
|
||||||
|
return "Fortgeschritten";
|
||||||
|
|
||||||
|
default:
|
||||||
|
return "Normal";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||||
|
if (!firstRender) return;
|
||||||
|
await Runtime.InvokeVoidAsync("observeElement", _element);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
.technology {
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
|
||||||
|
.tech-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
|
||||||
|
.tech-name {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-level {
|
||||||
|
align-self: flex-end;
|
||||||
|
color: var(--desc-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tech-progress {
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--gradient);
|
||||||
|
box-shadow: 0 3px 10px 1px var(--primary-muted);
|
||||||
|
transition: width 200ms ease-out;
|
||||||
|
width: 0;
|
||||||
|
|
||||||
|
&.level-1 {
|
||||||
|
--width: 33%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-2 {
|
||||||
|
--width: 66%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.level-3 {
|
||||||
|
--width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.in-view {
|
||||||
|
animation: slider-in 500ms forwards ease-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slider-in {
|
||||||
|
from {
|
||||||
|
width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
width: var(--width);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ main {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
.content {
|
.content {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@
|
|||||||
background-color: var(--background);
|
background-color: var(--background);
|
||||||
border-bottom: 1px solid var(--border-color);
|
border-bottom: 1px solid var(--border-color);
|
||||||
padding-inline: 0.5rem;
|
padding-inline: 0.5rem;
|
||||||
z-index: 100;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.links {
|
.links {
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
@using Portfolio.Shared.Services
|
@using Portfolio.Shared.Services
|
||||||
@using Portfolio.Web.Components.Components
|
@using Portfolio.Web.Components.Components
|
||||||
|
|
||||||
|
<PageTitle>Projekte</PageTitle>
|
||||||
|
|
||||||
<h2>Alle Projekte</h2>
|
<h2>Alle Projekte</h2>
|
||||||
|
|
||||||
<div class="project-container">
|
<div class="project-container">
|
||||||
@foreach (var project in _projects) {
|
@foreach (var (index, project) in _projects.Index()) {
|
||||||
<ProjectView Project="project" />
|
<ProjectView Project="project" Index="index" />
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -4,10 +4,3 @@
|
|||||||
gap: 100px;
|
gap: 100px;
|
||||||
justify-content: space-evenly;
|
justify-content: space-evenly;
|
||||||
}
|
}
|
||||||
|
|
||||||
h2 {
|
|
||||||
margin-top: 3rem;
|
|
||||||
margin-bottom: 5rem;
|
|
||||||
font-size: 2rem;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|||||||
100
src/Portfolio.Web/Components/Pages/TechnologiesPage.razor
Normal file
100
src/Portfolio.Web/Components/Pages/TechnologiesPage.razor
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
@page "/technologies"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
@using System.Collections.Immutable
|
||||||
|
@using Portfolio.Shared.Models
|
||||||
|
@using Portfolio.Shared.Services
|
||||||
|
@using Portfolio.Web.Components.Components
|
||||||
|
|
||||||
|
<PageTitle>Technologien</PageTitle>
|
||||||
|
|
||||||
|
<section class="home-section" id="tech-projects">
|
||||||
|
<div class="artwork">
|
||||||
|
<div class="circle big-circle"></div>
|
||||||
|
<div class="circle small-circle"></div>
|
||||||
|
<div class="circle image"></div>
|
||||||
|
</div>
|
||||||
|
<h2>Technologien in Projekten</h2>
|
||||||
|
<div class="chart">
|
||||||
|
<div class="chart-container"><canvas id="chart"></canvas></div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="home-section" id="languages">
|
||||||
|
<h2>Programmiersprachen</h2>
|
||||||
|
<div class="technologies-wrapper">
|
||||||
|
@foreach (var tech in _technologies.Where(t => t.Type == TechnologyType.Language)) {
|
||||||
|
<TechnologyView Technology="tech" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="home-section" id="frameworks">
|
||||||
|
<h2>Frameworks</h2>
|
||||||
|
<div class="technologies-wrapper">
|
||||||
|
@foreach (var tech in _technologies.Where(t => t.Type == TechnologyType.Framework)) {
|
||||||
|
<TechnologyView Technology="tech" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="home-section" id="additional">
|
||||||
|
<h2>Zusätzliche Fähigkeiten</h2>
|
||||||
|
<div id="skills-wrapper">
|
||||||
|
@foreach (var skill in _technologies.Where(t => t.Type == TechnologyType.Additional)) {
|
||||||
|
<div class="skill">
|
||||||
|
<div class="dot"></div>
|
||||||
|
<h3>@skill.Name</h3>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
||||||
|
<script>
|
||||||
|
function displayChart(labels, data) {
|
||||||
|
if (Chart === undefined) return;
|
||||||
|
|
||||||
|
const element = document.querySelector('#chart');
|
||||||
|
Chart.defaults.color = "#FFFFFF";
|
||||||
|
new Chart(element, {
|
||||||
|
type: "doughnut",
|
||||||
|
data: {
|
||||||
|
labels: labels,
|
||||||
|
datasets: [{
|
||||||
|
label: "Projekte",
|
||||||
|
data: data
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@inherits CancellableComponent
|
||||||
|
@inject ITechnologyRepository TechnologyRepository
|
||||||
|
@inject IProjectRepository ProjectRepository
|
||||||
|
@inject IJSRuntime Runtime
|
||||||
|
|
||||||
|
@code {
|
||||||
|
|
||||||
|
private IEnumerable<Technology> _technologies = [];
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync() {
|
||||||
|
_technologies = await TechnologyRepository.GetTechnologies(TokenSource.Token);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender) {
|
||||||
|
if (firstRender) {
|
||||||
|
var projects = await ProjectRepository.GetProjects(TokenSource.Token);
|
||||||
|
|
||||||
|
var data = projects
|
||||||
|
.SelectMany(p => p.Languages)
|
||||||
|
.CountBy(l => l.Name)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
await Runtime.InvokeVoidAsync("displayChart",
|
||||||
|
data.Select(c => c.Key),
|
||||||
|
data.Select(c => c.Value));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
#tech-projects {
|
||||||
|
margin-top: 50px;
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.chart {
|
||||||
|
display: flex;
|
||||||
|
margin-top: 30px;
|
||||||
|
|
||||||
|
.chart-container {
|
||||||
|
width: 500px;
|
||||||
|
height: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork {
|
||||||
|
top: 40px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.home-section h2 {
|
||||||
|
margin-block: 5rem 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
#additional {
|
||||||
|
#skills-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
gap: 20px;
|
||||||
|
margin-top: 30px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.skill {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
background: var(--gradient);
|
||||||
|
box-shadow: 0 3px 10px 1px var(--primary-muted);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-weight: normal;
|
||||||
|
margin: 0;
|
||||||
|
font-size: 20px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
:root {
|
:root {
|
||||||
--primary: #8e5bd2;
|
--primary: rgb(142, 91, 210);
|
||||||
--secondary: #11a8bd;
|
--secondary: #11a8bd;
|
||||||
|
--primary-muted: rgba(142, 91, 210, 0.4);
|
||||||
|
|
||||||
--gradient: linear-gradient(90deg, var(--primary), var(--secondary));
|
--gradient: linear-gradient(90deg, var(--primary), var(--secondary));
|
||||||
--gradient-angled: linear-gradient(135deg, var(--primary), var(--secondary));
|
--gradient-angled: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||||
@@ -30,3 +31,61 @@ html, body {
|
|||||||
padding: 0;
|
padding: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
margin-top: 3rem;
|
||||||
|
margin-bottom: 5rem;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.artwork {
|
||||||
|
position: absolute;
|
||||||
|
left: 55%;
|
||||||
|
top: 19vh;
|
||||||
|
z-index: 0;
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
position: absolute;
|
||||||
|
aspect-ratio: 1 / 1;
|
||||||
|
z-index: -1;
|
||||||
|
background: var(--gradient-angled);
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
inset: 1px;
|
||||||
|
background-color: var(--background);
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.big-circle {
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
width: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.small-circle {
|
||||||
|
top: 100px;
|
||||||
|
left: 350px;
|
||||||
|
width: 150px;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
content: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.image {
|
||||||
|
top: -50px;
|
||||||
|
left: 170px;
|
||||||
|
width: 250px;
|
||||||
|
|
||||||
|
&:after {
|
||||||
|
background-image: url("/favicon.ico");
|
||||||
|
background-size: 112%;
|
||||||
|
background-position: -15px -15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
10
src/Portfolio.Web/wwwroot/scroll-handler.js
Normal file
10
src/Portfolio.Web/wwwroot/scroll-handler.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const observer = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (!entry.isIntersecting) return;
|
||||||
|
entry.target.classList.add("in-view");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function observeElement(element) {
|
||||||
|
observer?.observe(element);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user