style: redesign projects page
This change separates the project logo and details into separate pages and makes the list more visually striking.
This commit is contained in:
parent
fdce8c3cff
commit
cbfdefae71
@ -20,6 +20,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "scss", "scss", "{822F528E-3
|
||||
src\scss\app.scss = src\scss\app.scss
|
||||
src\scss\prism.vs.scss = src\scss\prism.vs.scss
|
||||
src\scss\prism.css = src\scss\prism.css
|
||||
src\scss\ribbon.scss = src\scss\ribbon.scss
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ts", "ts", "{BB9F76AC-292A-4F47-809D-8BBBA6E0A048}"
|
||||
|
12
OliverBooth/Data/StringListConverter.cs
Normal file
12
OliverBooth/Data/StringListConverter.cs
Normal file
@ -0,0 +1,12 @@
|
||||
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
|
||||
|
||||
namespace OliverBooth.Data;
|
||||
|
||||
internal sealed class StringListConverter : ValueConverter<IReadOnlyList<string>, string>
|
||||
{
|
||||
public StringListConverter(char separator = ' ') :
|
||||
base(v => string.Join(separator, v),
|
||||
s => s.Split(separator, StringSplitOptions.None))
|
||||
{
|
||||
}
|
||||
}
|
@ -0,0 +1,20 @@
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore.Metadata.Builders;
|
||||
|
||||
namespace OliverBooth.Data.Web.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Represents the configuration for the <see cref="ProgrammingLanguage" /> entity.
|
||||
/// </summary>
|
||||
internal sealed class ProgrammingLanguageConfiguration : IEntityTypeConfiguration<ProgrammingLanguage>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public void Configure(EntityTypeBuilder<ProgrammingLanguage> builder)
|
||||
{
|
||||
builder.ToTable("ProgrammingLanguage");
|
||||
builder.HasKey(e => e.Key);
|
||||
|
||||
builder.Property(e => e.Key).IsRequired();
|
||||
builder.Property(e => e.Name).IsRequired();
|
||||
}
|
||||
}
|
@ -21,8 +21,13 @@ internal sealed class ProjectConfiguration : IEntityTypeConfiguration<Project>
|
||||
builder.Property(e => e.Name).IsRequired();
|
||||
builder.Property(e => e.HeroUrl).IsRequired();
|
||||
builder.Property(e => e.Description).IsRequired();
|
||||
builder.Property(e => e.Details).IsRequired();
|
||||
builder.Property(e => e.Status).HasConversion<EnumToStringConverter<ProjectStatus>>().IsRequired();
|
||||
builder.Property(e => e.RemoteUrl);
|
||||
builder.Property(e => e.RemoteTarget);
|
||||
builder.Property(e => e.Tagline);
|
||||
builder.Property(e => e.Languages).HasConversion(
|
||||
v => string.Join(' ', v),
|
||||
s => s.Split(' ', StringSplitOptions.None));
|
||||
}
|
||||
}
|
||||
|
20
OliverBooth/Data/Web/IProgrammingLanguage.cs
Normal file
20
OliverBooth/Data/Web/IProgrammingLanguage.cs
Normal file
@ -0,0 +1,20 @@
|
||||
namespace OliverBooth.Data.Web;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a programming language.
|
||||
/// </summary>
|
||||
public interface IProgrammingLanguage
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the unique key for this programming language.
|
||||
/// </summary>
|
||||
/// <value>The unique key.</value>
|
||||
/// <remarks>This is generally the file extension of the language.</remarks>
|
||||
string Key { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of this programming language.
|
||||
/// </summary>
|
||||
/// <value>The name.</value>
|
||||
string Name { get; }
|
||||
}
|
@ -11,6 +11,12 @@ public interface IProject
|
||||
/// <value>The description of the project.</value>
|
||||
string Description { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the details of the project.
|
||||
/// </summary>
|
||||
/// <value>The details.</value>
|
||||
string Details { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the URL of the hero image.
|
||||
/// </summary>
|
||||
@ -23,6 +29,12 @@ public interface IProject
|
||||
/// <value>The ID of the project.</value>
|
||||
Guid Id { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the set of languages used for this project.
|
||||
/// </summary>
|
||||
/// <value>The languages.</value>
|
||||
IReadOnlyList<string> Languages { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the name of the project.
|
||||
/// </summary>
|
||||
@ -58,4 +70,10 @@ public interface IProject
|
||||
/// </summary>
|
||||
/// <value>The status of the project.</value>
|
||||
ProjectStatus Status { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the tagline of the project.
|
||||
/// </summary>
|
||||
/// <value>The tagline.</value>
|
||||
string? Tagline { get; }
|
||||
}
|
||||
|
72
OliverBooth/Data/Web/ProgrammingLanguage.cs
Normal file
72
OliverBooth/Data/Web/ProgrammingLanguage.cs
Normal file
@ -0,0 +1,72 @@
|
||||
namespace OliverBooth.Data.Web;
|
||||
|
||||
/// <inheritdoc cref="IProgrammingLanguage" />
|
||||
internal sealed class ProgrammingLanguage : IEquatable<ProgrammingLanguage>, IProgrammingLanguage
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public string Key { get; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; internal set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether two instances of <see cref="ProgrammingLanguage" /> are equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first instance of <see cref="ProgrammingLanguage" /> to compare.</param>
|
||||
/// <param name="right">The second instance of <see cref="ProgrammingLanguage" /> to compare.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="left" /> and <paramref name="right" /> are equal; otherwise,
|
||||
/// <see langword="false" />.
|
||||
/// </returns>
|
||||
public static bool operator ==(ProgrammingLanguage? left, ProgrammingLanguage? right) => Equals(left, right);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether two instances of <see cref="ProgrammingLanguage" /> are not equal.
|
||||
/// </summary>
|
||||
/// <param name="left">The first instance of <see cref="ProgrammingLanguage" /> to compare.</param>
|
||||
/// <param name="right">The second instance of <see cref="ProgrammingLanguage" /> to compare.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="left" /> and <paramref name="right" /> are not equal; otherwise,
|
||||
/// <see langword="false" />.
|
||||
/// </returns>
|
||||
public static bool operator !=(ProgrammingLanguage? left, ProgrammingLanguage? right) => !(left == right);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether this instance of <see cref="ProgrammingLanguage" /> is equal to another
|
||||
/// instance.
|
||||
/// </summary>
|
||||
/// <param name="other">An instance to compare with this instance.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="other" /> is equal to this instance; otherwise,
|
||||
/// <see langword="false" />.
|
||||
/// </returns>
|
||||
public bool Equals(ProgrammingLanguage? other)
|
||||
{
|
||||
if (ReferenceEquals(null, other)) return false;
|
||||
if (ReferenceEquals(this, other)) return true;
|
||||
return Key.Equals(other.Key);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether this instance is equal to a specified object.
|
||||
/// </summary>
|
||||
/// <param name="obj">An object to compare with this instance.</param>
|
||||
/// <returns>
|
||||
/// <see langword="true" /> if <paramref name="obj" /> is an instance of <see cref="ProgrammingLanguage" /> and
|
||||
/// equals the value of this instance; otherwise, <see langword="false" />.
|
||||
/// </returns>
|
||||
public override bool Equals(object? obj)
|
||||
{
|
||||
return ReferenceEquals(this, obj) || obj is ProgrammingLanguage other && Equals(other);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the hash code for this instance.
|
||||
/// </summary>
|
||||
/// <returns>The hash code.</returns>
|
||||
public override int GetHashCode()
|
||||
{
|
||||
// ReSharper disable once NonReadonlyMemberInGetHashCode
|
||||
return Key.GetHashCode();
|
||||
}
|
||||
}
|
@ -8,12 +8,18 @@ internal sealed class Project : IEquatable<Project>, IProject
|
||||
/// <inheritdoc />
|
||||
public string Description { get; private set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Details { get; private set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string HeroUrl { get; private set; } = string.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Guid Id { get; private set; } = Guid.NewGuid();
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<string> Languages { get; private set; } = ArraySegment<string>.Empty;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string Name { get; private set; } = string.Empty;
|
||||
|
||||
@ -32,6 +38,9 @@ internal sealed class Project : IEquatable<Project>, IProject
|
||||
/// <inheritdoc />
|
||||
public ProjectStatus Status { get; private set; } = ProjectStatus.Ongoing;
|
||||
|
||||
/// <inheritdoc />
|
||||
public string? Tagline { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns a value indicating whether two instances of <see cref="Project" /> are equal.
|
||||
/// </summary>
|
||||
|
@ -31,6 +31,12 @@ internal sealed class WebContext : DbContext
|
||||
/// <value>The collection of blacklist entries.</value>
|
||||
public DbSet<BlacklistEntry> ContactBlacklist { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of programming languages in the database.
|
||||
/// </summary>
|
||||
/// <value>The collection of programming languages.</value>
|
||||
public DbSet<ProgrammingLanguage> ProgrammingLanguages { get; private set; } = null!;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the collection of projects in the database.
|
||||
/// </summary>
|
||||
@ -57,10 +63,12 @@ internal sealed class WebContext : DbContext
|
||||
optionsBuilder.UseMySql(connectionString, serverVersion);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnModelCreating(ModelBuilder modelBuilder)
|
||||
{
|
||||
modelBuilder.ApplyConfiguration(new BlacklistEntryConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new BookConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new ProgrammingLanguageConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new ProjectConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new TemplateConfiguration());
|
||||
modelBuilder.ApplyConfiguration(new SiteConfigurationConfiguration());
|
||||
|
@ -4,102 +4,63 @@
|
||||
@inject IProjectService ProjectService
|
||||
@{
|
||||
ViewData["Title"] = "Projects";
|
||||
|
||||
IEnumerable<IProject> projects = ProjectService.GetProjects(ProjectStatus.Ongoing).OrderBy(p => p.Rank)
|
||||
.Concat(ProjectService.GetProjects(ProjectStatus.Past).OrderBy(p => p.Rank))
|
||||
.Concat(ProjectService.GetProjects(ProjectStatus.Hiatus).OrderBy(p => p.Rank));
|
||||
}
|
||||
|
||||
<main class="container">
|
||||
<h1 class="display-4">Projects</h1>
|
||||
|
||||
@foreach (IProject[] chunk in ProjectService.GetProjects(ProjectStatus.Ongoing).OrderBy(p => p.Rank).Chunk(2))
|
||||
@foreach (IProject[] chunk in projects.Chunk(2))
|
||||
{
|
||||
<div class="card-group row" style="margin-top: 20px;">
|
||||
@foreach (IProject project in chunk)
|
||||
{
|
||||
<div class="col-xs-1 col-md-6 col-lg-6 d-flex align-items-stretch">
|
||||
<div class="card border-success project-card">
|
||||
<div class="card-header text-bg-success">In Active Development</div>
|
||||
<img src="https://cdn.olivr.me/projects/hero/@project.HeroUrl" class="card-img-top" alt="@project.Name">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@project.Name</h5>
|
||||
<p class="card-text">@Html.Raw(ProjectService.GetDescription(project))</p>
|
||||
@if (!string.IsNullOrWhiteSpace(project.RemoteUrl))
|
||||
{
|
||||
<a href="@project.RemoteUrl" class="btn btn-primary">
|
||||
@if (string.IsNullOrWhiteSpace(project.RemoteTarget))
|
||||
{
|
||||
<span>View website</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>View on @project.RemoteTarget</span>
|
||||
}
|
||||
@switch (project.Status)
|
||||
{
|
||||
case ProjectStatus.Ongoing:
|
||||
<div class="project-card">
|
||||
<div class="ribbon ribbon-top-left green">
|
||||
<a asp-page="Project" asp-route-slug="@project.Slug">
|
||||
<span>Ongoing</span>
|
||||
</a>
|
||||
</div>
|
||||
<a asp-page="Project" asp-route-slug="@project.Slug">
|
||||
<img src="https://cdn.olivr.me/projects/hero/@project.HeroUrl" class="card-img-top" alt="@project.Name">
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
break;
|
||||
|
||||
@foreach (IProject[] chunk in ProjectService.GetProjects(ProjectStatus.Past).Chunk(2))
|
||||
{
|
||||
<div class="card-group row" style="margin-top: 20px;">
|
||||
@foreach (IProject project in chunk)
|
||||
{
|
||||
<div class="col-xs-1 col-md-6 col-lg-6 d-flex align-items-stretch">
|
||||
<div class="card border-info project-card">
|
||||
<div class="card-header text-bg-info">Past Work</div>
|
||||
<img src="https://cdn.olivr.me/projects/hero/@project.HeroUrl" class="card-img-top" alt="@project.Name">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@project.Name</h5>
|
||||
<p class="card-text">@Html.Raw(ProjectService.GetDescription(project))</p>
|
||||
@if (!string.IsNullOrWhiteSpace(project.RemoteUrl))
|
||||
{
|
||||
<a href="@project.RemoteUrl" class="btn btn-primary">
|
||||
@if (string.IsNullOrWhiteSpace(project.RemoteTarget))
|
||||
{
|
||||
<span>View website</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>View on @project.RemoteTarget</span>
|
||||
}
|
||||
case ProjectStatus.Past:
|
||||
<div class="border-info project-card">
|
||||
<div class="ribbon ribbon-top-left blue">
|
||||
<a asp-page="Project" asp-route-slug="@project.Slug">
|
||||
<span>Past Work</span>
|
||||
</a>
|
||||
</div>
|
||||
<a asp-page="Project" asp-route-slug="@project.Slug">
|
||||
<img src="https://cdn.olivr.me/projects/hero/@project.HeroUrl" class="card-img-top" alt="@project.Name">
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
break;
|
||||
|
||||
@foreach (IProject[] chunk in ProjectService.GetProjects(ProjectStatus.Hiatus).Chunk(2))
|
||||
{
|
||||
<div class="card-group row" style="margin-top: 20px;">
|
||||
@foreach (IProject project in chunk)
|
||||
{
|
||||
<div class="col-xs-1 col-md-6 col-lg-6 d-flex align-items-stretch">
|
||||
<div class="card border-dark project-card">
|
||||
<div class="card-header text-bg-dark">On Hiatus</div>
|
||||
<img src="https://cdn.olivr.me/projects/hero/@project.HeroUrl" class="card-img-top" alt="@project.Name">
|
||||
<div class="card-body">
|
||||
<h5 class="card-title">@project.Name</h5>
|
||||
<p class="card-text">@Html.Raw(ProjectService.GetDescription(project))</p>
|
||||
@if (!string.IsNullOrWhiteSpace(project.RemoteUrl))
|
||||
{
|
||||
<a href="@project.RemoteUrl" class="btn btn-primary">
|
||||
@if (string.IsNullOrWhiteSpace(project.RemoteTarget))
|
||||
{
|
||||
<span>View website</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>View on @project.RemoteTarget</span>
|
||||
}
|
||||
case ProjectStatus.Hiatus:
|
||||
<div class="border-danger project-card">
|
||||
<div class="ribbon ribbon-top-left red">
|
||||
|
||||
<a asp-page="Project" asp-route-slug="@project.Slug">
|
||||
<span>On Hiatus</span>
|
||||
</a>
|
||||
</div>
|
||||
<a asp-page="Project" asp-route-slug="@project.Slug">
|
||||
<img src="https://cdn.olivr.me/projects/hero/@project.HeroUrl" class="card-img-top" alt="@project.Name">
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
84
OliverBooth/Pages/Projects/Project.cshtml
Normal file
84
OliverBooth/Pages/Projects/Project.cshtml
Normal file
@ -0,0 +1,84 @@
|
||||
@page "/project/{slug}"
|
||||
@using Markdig
|
||||
@using OliverBooth.Data.Web
|
||||
@using OliverBooth.Services
|
||||
@model Project
|
||||
@inject IProjectService ProjectService
|
||||
@inject MarkdownPipeline MarkdownPipeline
|
||||
|
||||
@{
|
||||
ViewData["Title"] = "Projects";
|
||||
}
|
||||
|
||||
<main class="container">
|
||||
@if (Model.SelectedProject is not { } project)
|
||||
{
|
||||
<h1>Project Not Found</h1>
|
||||
return;
|
||||
}
|
||||
|
||||
<nav style="--bs-breadcrumb-divider: '>';" aria-label="breadcrumb">
|
||||
<ol class="breadcrumb">
|
||||
<li class="breadcrumb-item">
|
||||
<a asp-page="/Projects/Index">Projects</a>
|
||||
</li>
|
||||
<li class="breadcrumb-item active" aria-current="page">@project.Name</li>
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<h1>@project.Name</h1>
|
||||
@if (!string.IsNullOrWhiteSpace(project.Tagline))
|
||||
{
|
||||
<p class="lead">@project.Tagline</p>
|
||||
}
|
||||
|
||||
<p class="text-center">
|
||||
<img src="https://cdn.olivr.me/projects/hero/@project.HeroUrl" class="img-fluid" alt="@project.Name">
|
||||
</p>
|
||||
|
||||
<table class="table">
|
||||
<tr>
|
||||
<th style="width: 20%">Languages</th>
|
||||
<td>
|
||||
@foreach (IProgrammingLanguage language in ProjectService.GetProgrammingLanguages(project))
|
||||
{
|
||||
<img src="https://cdn.olivr.me/img/assets/p/@(language.Key).svg" alt="@language.Name" title="@language.Name" style="height: 2em">
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th>Status</th>
|
||||
<td>
|
||||
@switch (project.Status)
|
||||
{
|
||||
case ProjectStatus.Ongoing:
|
||||
<span class="badge rounded-pill text-bg-success">In Active Development</span>
|
||||
break;
|
||||
|
||||
case ProjectStatus.Past:
|
||||
<span class="badge rounded-pill text-bg-primary">Completed / Retired</span>
|
||||
break;
|
||||
|
||||
case ProjectStatus.Hiatus:
|
||||
<span class="badge rounded-pill text-bg-danger">On Hiatus</span>
|
||||
break;
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
@if (project.RemoteUrl != null)
|
||||
{
|
||||
<tr>
|
||||
<th>View</th>
|
||||
<td>
|
||||
<a href="@project.RemoteUrl">
|
||||
@(new Uri(project.RemoteUrl).Host) <i class="fa-solid fa-arrow-up-right-from-square"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
<tr>
|
||||
<th>Details</th>
|
||||
<td class="trim-p">@Html.Raw(Markdown.ToHtml(project.Details, MarkdownPipeline))</td>
|
||||
</tr>
|
||||
</table>
|
||||
</main>
|
25
OliverBooth/Pages/Projects/Project.cshtml.cs
Normal file
25
OliverBooth/Pages/Projects/Project.cshtml.cs
Normal file
@ -0,0 +1,25 @@
|
||||
using Microsoft.AspNetCore.Mvc.RazorPages;
|
||||
using OliverBooth.Data.Web;
|
||||
using OliverBooth.Services;
|
||||
|
||||
namespace OliverBooth.Pages.Projects;
|
||||
|
||||
public class Project : PageModel
|
||||
{
|
||||
private readonly IProjectService _projectService;
|
||||
|
||||
public Project(IProjectService projectService)
|
||||
{
|
||||
_projectService = projectService;
|
||||
}
|
||||
|
||||
public IProject? SelectedProject { get; private set; }
|
||||
|
||||
public void OnGet(string slug)
|
||||
{
|
||||
if (_projectService.TryGetProject(slug, out IProject? project))
|
||||
{
|
||||
SelectedProject = project;
|
||||
}
|
||||
}
|
||||
}
|
@ -59,6 +59,7 @@
|
||||
<link rel="stylesheet" href="~/css/prism.min.css" asp-append-version="true">
|
||||
<link rel="stylesheet" href="~/css/prism.vs.min.css" asp-append-version="true">
|
||||
<link rel="stylesheet" href="~/css/app.min.css" asp-append-version="true">
|
||||
<link rel="stylesheet" href="~/css/ribbon.min.css" asp-append-version="true">
|
||||
</head>
|
||||
<body>
|
||||
<header class="container" style="margin-top: 20px;">
|
||||
|
@ -22,6 +22,13 @@ public interface IProjectService
|
||||
/// <returns>A read-only list of projects.</returns>
|
||||
IReadOnlyList<IProject> GetAllProjects();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the programming languages used in the specified project.
|
||||
/// </summary>
|
||||
/// <param name="project">The project whose languages to return.</param>
|
||||
/// <returns>A read only view of the languages.</returns>
|
||||
IReadOnlyList<IProgrammingLanguage> GetProgrammingLanguages(IProject project);
|
||||
|
||||
/// <summary>
|
||||
/// Gets all projects with the specified status.
|
||||
/// </summary>
|
||||
|
@ -1,4 +1,5 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Humanizer;
|
||||
using Markdig;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using OliverBooth.Data.Web;
|
||||
@ -37,6 +38,15 @@ internal sealed class ProjectService : IProjectService
|
||||
return context.Projects.OrderBy(p => p.Rank).ThenBy(p => p.Name).ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IProgrammingLanguage> GetProgrammingLanguages(IProject project)
|
||||
{
|
||||
using WebContext context = _dbContextFactory.CreateDbContext();
|
||||
return project.Languages
|
||||
.Select(l => context.ProgrammingLanguages.Find(l) ?? new ProgrammingLanguage { Name = l.Titleize() })
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public IReadOnlyList<IProject> GetProjects(ProjectStatus status = ProjectStatus.Ongoing)
|
||||
{
|
||||
|
@ -179,11 +179,9 @@ article {
|
||||
}
|
||||
|
||||
.project-card {
|
||||
transition: all 0.2s ease-in-out;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.05);
|
||||
}
|
||||
position: relative;
|
||||
background: #000;
|
||||
box-shadow: 0 0 15px rgba(0, 0, 0, .1);
|
||||
}
|
||||
|
||||
.blog-card {
|
||||
@ -315,4 +313,8 @@ table.reading-list {
|
||||
.book-cover {
|
||||
width: 50px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
td.trim-p p:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
177
src/scss/ribbon.scss
Normal file
177
src/scss/ribbon.scss
Normal file
@ -0,0 +1,177 @@
|
||||
.ribbon {
|
||||
width: 150px;
|
||||
height: 150px;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
|
||||
&::before, &::after {
|
||||
position: absolute;
|
||||
z-index: -1;
|
||||
content: '';
|
||||
display: block;
|
||||
}
|
||||
|
||||
span {
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 225px;
|
||||
padding: 15px 0;
|
||||
box-shadow: 0 5px 10px rgba(0, 0, 0, .1);
|
||||
font: 700 18px/1 'Lato', sans-serif;
|
||||
text-shadow: 0 1px 1px rgba(0, 0, 0, .2);
|
||||
text-transform: uppercase;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
&.ribbon-top-left {
|
||||
top: -10px;
|
||||
left: -10px;
|
||||
|
||||
&::before, &::after {
|
||||
border-top-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
right: -25px;
|
||||
top: 30px;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.ribbon-top-right {
|
||||
top: -10px;
|
||||
right: -10px;
|
||||
|
||||
&::before, &::after {
|
||||
border-top-color: transparent;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
left: -25px;
|
||||
top: 30px;
|
||||
transform: rotate(45deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.ribbon-bottom-left {
|
||||
bottom: -10px;
|
||||
left: -10px;
|
||||
|
||||
&::before, &::after {
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
bottom: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
right: -25px;
|
||||
bottom: 30px;
|
||||
transform: rotate(225deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.ribbon-bottom-right {
|
||||
bottom: -10px;
|
||||
right: -10px;
|
||||
|
||||
&::before, &::after {
|
||||
border-bottom-color: transparent;
|
||||
border-right-color: transparent;
|
||||
}
|
||||
|
||||
&::before {
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&::after {
|
||||
top: 0;
|
||||
right: 0;
|
||||
}
|
||||
|
||||
span {
|
||||
left: -25px;
|
||||
bottom: 30px;
|
||||
transform: rotate(-225deg);
|
||||
}
|
||||
}
|
||||
|
||||
&.red {
|
||||
$bg: #db3434;
|
||||
$fg: #fff;
|
||||
|
||||
&::before, &::after {
|
||||
border: 5px solid darken($bg, 20%);
|
||||
}
|
||||
|
||||
span {
|
||||
background-color: $bg;
|
||||
color: $fg;
|
||||
}
|
||||
}
|
||||
|
||||
&.green {
|
||||
$bg: #34db34;
|
||||
$fg: #fff;
|
||||
|
||||
&::before, &::after {
|
||||
border: 5px solid darken($bg, 20%);
|
||||
}
|
||||
|
||||
span {
|
||||
background-color: $bg;
|
||||
color: $fg;
|
||||
}
|
||||
}
|
||||
|
||||
&.blue {
|
||||
$bg: #3498db;
|
||||
$fg: #fff;
|
||||
|
||||
&::before, &::after {
|
||||
border: 5px solid darken($bg, 20%);
|
||||
}
|
||||
|
||||
span {
|
||||
background-color: $bg;
|
||||
color: $fg;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.project-card .ribbon {
|
||||
transform: scale(75%);
|
||||
left: -26px;
|
||||
top: -26px;
|
||||
}
|
Loading…
Reference in New Issue
Block a user