Resolve "Technology Page" #21
@@ -7,8 +7,6 @@ public class DatabaseContext(DbContextOptions<DatabaseContext> options) : DbCont
|
||||
|
||||
public DbSet<Project> Projects { get; set; }
|
||||
|
||||
public DbSet<Language> Languages { get; set; }
|
||||
|
||||
public DbSet<Technology> Technologies { get; set; }
|
||||
|
||||
public DbSet<TimelineEntry> Timeline { get; set; }
|
||||
|
||||
@@ -45,7 +45,7 @@ builder.Services.AddHopFrame(options => {
|
||||
.SetValue(langConfig, true);
|
||||
|
||||
table.Property(p => p.Languages)
|
||||
.FormatEach<Language>((l, _) => l.Label)
|
||||
.FormatEach<Technology>((l, _) => l.Name)
|
||||
.List(false);
|
||||
|
||||
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;
|
||||
|
||||
[ForeignKey("languages")]
|
||||
public List<Language> Languages { get; init; } = new();
|
||||
[ForeignKey("technologies")]
|
||||
public List<Technology> Languages { get; init; } = new();
|
||||
|
||||
public DateTime Created { get; init; } = DateTime.Now;
|
||||
|
||||
|
||||
@@ -10,13 +10,27 @@ public sealed class Technology {
|
||||
|
||||
[MaxLength(255)]
|
||||
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 TechnologyType Type { get; set; } = TechnologyType.Language;
|
||||
|
||||
}
|
||||
|
||||
public enum TechnologyLevel : byte {
|
||||
public enum TechnologyLevel {
|
||||
Beginner = 0,
|
||||
Intermediate = 1,
|
||||
Professional = 2
|
||||
}
|
||||
}
|
||||
|
||||
public enum TechnologyType {
|
||||
Language = 0,
|
||||
Framework = 1,
|
||||
Additional = 2
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
<base href="/"/>
|
||||
<link rel="stylesheet" href="@Assets["app.css"]"/>
|
||||
<link rel="stylesheet" href="@Assets["Portfolio.Web.styles.css"]"/>
|
||||
<script src="@Assets["scroll-handler.js"]"></script>
|
||||
<ImportMap/>
|
||||
<link rel="icon" href="favicon.ico"/>
|
||||
<HeadOutlet/>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
@using Portfolio.Shared.Models
|
||||
|
||||
<div class="border">
|
||||
<div class="border" style="@("--index: " + Index)">
|
||||
<div class="shell">
|
||||
<img src="@Project.Cover" alt="project-cover">
|
||||
<h3>@Project.Name</h3>
|
||||
@@ -32,6 +32,9 @@
|
||||
[Parameter]
|
||||
public required Project Project { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public required int Index { get; set; }
|
||||
|
||||
private string GetStatusName() {
|
||||
return Project.Status switch {
|
||||
ProjectStatus.Finished => "Fertig",
|
||||
|
||||
@@ -6,6 +6,19 @@
|
||||
border-radius: 30px;
|
||||
overflow: hidden;
|
||||
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 {
|
||||
|
||||
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%;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.content {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
background-color: var(--background);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
padding-inline: 0.5rem;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.links {
|
||||
|
||||
@@ -3,11 +3,13 @@
|
||||
@using Portfolio.Shared.Services
|
||||
@using Portfolio.Web.Components.Components
|
||||
|
||||
<PageTitle>Projekte</PageTitle>
|
||||
|
||||
<h2>Alle Projekte</h2>
|
||||
|
||||
<div class="project-container">
|
||||
@foreach (var project in _projects) {
|
||||
<ProjectView Project="project" />
|
||||
@foreach (var (index, project) in _projects.Index()) {
|
||||
<ProjectView Project="project" Index="index" />
|
||||
}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -4,10 +4,3 @@
|
||||
gap: 100px;
|
||||
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 {
|
||||
--primary: #8e5bd2;
|
||||
--primary: rgb(142, 91, 210);
|
||||
--secondary: #11a8bd;
|
||||
--primary-muted: rgba(142, 91, 210, 0.4);
|
||||
|
||||
--gradient: linear-gradient(90deg, var(--primary), var(--secondary));
|
||||
--gradient-angled: linear-gradient(135deg, var(--primary), var(--secondary));
|
||||
@@ -30,3 +31,61 @@ html, body {
|
||||
padding: 0;
|
||||
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