diff --git a/OliverBooth/Data/Web/Configuration/ProjectConfiguration.cs b/OliverBooth/Data/Web/Configuration/ProjectConfiguration.cs new file mode 100644 index 0000000..349b7fb --- /dev/null +++ b/OliverBooth/Data/Web/Configuration/ProjectConfiguration.cs @@ -0,0 +1,28 @@ +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace OliverBooth.Data.Web.Configuration; + +/// +/// Represents the configuration for the entity. +/// +internal sealed class ProjectConfiguration : IEntityTypeConfiguration +{ + /// + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Project"); + builder.HasKey(e => e.Id); + + builder.Property(e => e.Id).IsRequired(); + builder.Property(e => e.Rank).IsRequired(); + builder.Property(e => e.Slug).IsRequired(); + builder.Property(e => e.Name).IsRequired(); + builder.Property(e => e.HeroUrl).IsRequired(); + builder.Property(e => e.Description).IsRequired(); + builder.Property(e => e.Status).HasConversion>().IsRequired(); + builder.Property(e => e.RemoteUrl); + builder.Property(e => e.RemoteTarget); + } +} diff --git a/OliverBooth/Data/Web/IProject.cs b/OliverBooth/Data/Web/IProject.cs new file mode 100644 index 0000000..290e14b --- /dev/null +++ b/OliverBooth/Data/Web/IProject.cs @@ -0,0 +1,61 @@ +namespace OliverBooth.Data.Web; + +/// +/// Represents a project. +/// +public interface IProject +{ + /// + /// Gets the description of the project. + /// + /// The description of the project. + string Description { get; } + + /// + /// Gets the URL of the hero image. + /// + /// The URL of the hero image. + string HeroUrl { get; } + + /// + /// Gets the ID of the project. + /// + /// The ID of the project. + Guid Id { get; } + + /// + /// Gets the name of the project. + /// + /// The name of the project. + string Name { get; } + + /// + /// Gets the rank of the project. + /// + /// The rank of the project. + int Rank { get; } + + /// + /// Gets the host of the project. + /// + /// The host of the project. + string? RemoteTarget { get; } + + /// + /// Gets the URL of the project. + /// + /// The URL of the project. + string? RemoteUrl { get; } + + /// + /// Gets the slug of the project. + /// + /// The slug of the project. + string Slug { get; } + + /// + /// Gets the status of the project. + /// + /// The status of the project. + ProjectStatus Status { get; } +} diff --git a/OliverBooth/Data/Web/Project.cs b/OliverBooth/Data/Web/Project.cs new file mode 100644 index 0000000..b58f06d --- /dev/null +++ b/OliverBooth/Data/Web/Project.cs @@ -0,0 +1,95 @@ +namespace OliverBooth.Data.Web; + +/// +/// Represents a project. +/// +internal sealed class Project : IEquatable, IProject +{ + /// + public string Description { get; private set; } = string.Empty; + + /// + public string HeroUrl { get; private set; } = string.Empty; + + /// + public Guid Id { get; private set; } = Guid.NewGuid(); + + /// + public string Name { get; private set; } = string.Empty; + + /// + public int Rank { get; private set; } + + /// + public string? RemoteTarget { get; private set; } + + /// + public string? RemoteUrl { get; private set; } + + /// + public string Slug { get; private set; } = string.Empty; + + /// + public ProjectStatus Status { get; private set; } = ProjectStatus.Ongoing; + + /// + /// Returns a value indicating whether two instances of are equal. + /// + /// The first instance of to compare. + /// The second instance of to compare. + /// + /// if and are equal; otherwise, + /// . + /// + public static bool operator ==(Project? left, Project? right) => Equals(left, right); + + /// + /// Returns a value indicating whether two instances of are not equal. + /// + /// The first instance of to compare. + /// The second instance of to compare. + /// + /// if and are not equal; otherwise, + /// . + /// + public static bool operator !=(Project? left, Project? right) => !(left == right); + + /// + /// Returns a value indicating whether this instance of is equal to another + /// instance. + /// + /// An instance to compare with this instance. + /// + /// if is equal to this instance; otherwise, + /// . + /// + public bool Equals(Project? other) + { + if (ReferenceEquals(null, other)) return false; + if (ReferenceEquals(this, other)) return true; + return Id.Equals(other.Id); + } + + /// + /// Returns a value indicating whether this instance is equal to a specified object. + /// + /// An object to compare with this instance. + /// + /// if is an instance of and equals the + /// value of this instance; otherwise, . + /// + public override bool Equals(object? obj) + { + return ReferenceEquals(this, obj) || obj is Project other && Equals(other); + } + + /// + /// Gets the hash code for this instance. + /// + /// The hash code. + public override int GetHashCode() + { + // ReSharper disable once NonReadonlyMemberInGetHashCode + return Id.GetHashCode(); + } +} diff --git a/OliverBooth/Data/Web/ProjectStatus.cs b/OliverBooth/Data/Web/ProjectStatus.cs new file mode 100644 index 0000000..9e52969 --- /dev/null +++ b/OliverBooth/Data/Web/ProjectStatus.cs @@ -0,0 +1,18 @@ +using System.ComponentModel; + +namespace OliverBooth.Data.Web; + +/// +/// Represents the status of a project. +/// +public enum ProjectStatus +{ + [Description("The project is currently being worked on.")] + Ongoing, + + [Description("The project is on an indefinite hiatus.")] + Hiatus, + + [Description("The project is no longer being worked on.")] + Past +} diff --git a/OliverBooth/Data/Web/WebContext.cs b/OliverBooth/Data/Web/WebContext.cs index f623cf4..1f5a914 100644 --- a/OliverBooth/Data/Web/WebContext.cs +++ b/OliverBooth/Data/Web/WebContext.cs @@ -19,6 +19,12 @@ internal sealed class WebContext : DbContext _configuration = configuration; } + /// + /// Gets the collection of projects in the database. + /// + /// The collection of projects. + public DbSet Projects { get; private set; } = null!; + /// /// Gets the set of site configuration items. /// @@ -41,6 +47,7 @@ internal sealed class WebContext : DbContext protected override void OnModelCreating(ModelBuilder modelBuilder) { + modelBuilder.ApplyConfiguration(new ProjectConfiguration()); modelBuilder.ApplyConfiguration(new TemplateConfiguration()); modelBuilder.ApplyConfiguration(new SiteConfigurationConfiguration()); } diff --git a/OliverBooth/Pages/Projects/Index.cshtml b/OliverBooth/Pages/Projects/Index.cshtml index 521b41c..1bf09b1 100644 --- a/OliverBooth/Pages/Projects/Index.cshtml +++ b/OliverBooth/Pages/Projects/Index.cshtml @@ -1,190 +1,105 @@ @page +@using OliverBooth.Data.Web +@using OliverBooth.Services +@inject IProjectService ProjectService @{ ViewData["Title"] = "Projects"; }

Projects

-
-
-
-
In Active Development
- X10D -
-
X10D
-

A NuGet offering dozens of extension methods for countless .NET types.

- View on GitHub +@foreach (IProject[] chunk in ProjectService.GetProjects(ProjectStatus.Ongoing).OrderBy(p => p.Rank).Chunk(2)) +{ +
+ @foreach (IProject project in chunk) + { +
+
+
In Active Development
+ @project.Name +
+
@project.Name
+

@Html.Raw(ProjectService.GetDescription(project))

+ @if (!string.IsNullOrWhiteSpace(project.RemoteUrl)) + { + + @if (string.IsNullOrWhiteSpace(project.RemoteTarget)) + { + View website + } + else + { + View on @project.RemoteTarget + } + + } +
+
-
+ }
-
-
-
In Active Development
- BrackeysBot -
-
BrackeysBot
-

A collection of self-contained Discord bots that power the Brackeys Community Discord server.

- View on GitHub -
-
-
-
-
-
-
-
In Active Development
- Project KW -
-
Project KW
-

A spiritual successor to the PlayStation cult classic Kula World (aka Roll Away).

-

No further information is available for this project at this time.

-
-
-
-
+} -
-
-
-
Past Work
- It's 5 O'Clock Somewhere -
-
It's 5 O'Clock Somewhere
-

An alcoholic's best friend: an Android app that tells you where in the world it's socially acceptable to drink.

- View on Play Store +@foreach (IProject[] chunk in ProjectService.GetProjects(ProjectStatus.Past).Chunk(2)) +{ +
+ @foreach (IProject project in chunk) + { +
+
+
Past Work
+ @project.Name +
+
@project.Name
+

@Html.Raw(ProjectService.GetDescription(project))

+ @if (!string.IsNullOrWhiteSpace(project.RemoteUrl)) + { + + @if (string.IsNullOrWhiteSpace(project.RemoteTarget)) + { + View website + } + else + { + View on @project.RemoteTarget + } + + } +
+
-
+ }
-
-
-
Past Work
- Is It My Birthday? -
-
Is It My Birthday?
-

An Android app to tell you whether or not today is your birthday.

- View on Play Store -
-
-
-
-
-
-
-
Past Work
- Scrolling Candy Apple Saga -
-
Scrolling Candy Apple Saga
-

My submission to the 2013 Candy Jam. Made with Unity.

- View on itch.io -
-
-
-
-
-
Retired
- SAMP.NET -
-
SAMP.NET
-

A .NET wrapper for the Pawn SAMP API, made during my time in college.

- View on GitHub -
-
-
-
-
-
-
-
Retired
- SAMP.NET -
-
HaloMCCForgePatcher
-

A tool to enable the Forge menu item in Halo: Master Chief Collection before it was officially released.

- View on GitHub -
-
-
-
-
-
Retired
- Unity API Docs -
-
Unity API Docs
-

A complete rewrite and redesign of the Unity API docs, inspired by the C# and .NET documentation on Microsoft Learn.

-

This project was cancelled following Unity's pricing change announcement.

-
-
-
-
+} -
-
-
-
On Hiatus
- Olive -
-
Olive
-

A game engine written in C#, powered by MonoGame.

- View on GitHub +@foreach (IProject[] chunk in ProjectService.GetProjects(ProjectStatus.Hiatus).Chunk(2)) +{ +
+ @foreach (IProject project in chunk) + { +
+
+
On Hiatus
+ @project.Name +
+
@project.Name
+

@Html.Raw(ProjectService.GetDescription(project))

+ @if (!string.IsNullOrWhiteSpace(project.RemoteUrl)) + { + + @if (string.IsNullOrWhiteSpace(project.RemoteTarget)) + { + View website + } + else + { + View on @project.RemoteTarget + } + + } +
+
-
+ }
-
-
-
On Hiatus
- TCP.NET -
-
TCP.NET
-

A TCP library for .NET with support for RSA/AES encryption.

- View on GitHub -
-
-
-
-
-
-
-
On Hiatus
- MelonSharp -
-
MelonSharp
-

A vanilla Minecraft server written in C#.

-

No further information is available for this project at this time.

-
-
-
-
-
-
On Hiatus
- SyntaxGen.NET -
-
SyntaxGen.NET
-

A NuGet to be used for documentation tools that can generate the declaration syntax for .NET types and their members.

- View on GitHub -
-
-
-
-
-
-
-
On Hiatus
- Mutation -
-
Mutation
-

A restoration of the classic AW RPG world, modernised for Virtual Paradise.

- View website -
-
-
-
-
-
On Hiatus
- Olive -
-
Astraeos
-

An open-universe sandbox game.

-

No further information is available for this project at this time.

-
-
-
-
\ No newline at end of file +} \ No newline at end of file diff --git a/OliverBooth/Program.cs b/OliverBooth/Program.cs index 0dffe03..71865f9 100644 --- a/OliverBooth/Program.cs +++ b/OliverBooth/Program.cs @@ -34,6 +34,7 @@ builder.Services.AddDbContextFactory(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddRazorPages().AddRazorRuntimeCompilation(); builder.Services.AddControllersWithViews(); builder.Services.AddRouting(options => options.LowercaseUrls = true); diff --git a/OliverBooth/Services/IProjectService.cs b/OliverBooth/Services/IProjectService.cs new file mode 100644 index 0000000..e891bf3 --- /dev/null +++ b/OliverBooth/Services/IProjectService.cs @@ -0,0 +1,57 @@ +using System.Diagnostics.CodeAnalysis; +using OliverBooth.Data.Web; + +namespace OliverBooth.Services; + +/// +/// Represents a service for interacting with projects. +/// +public interface IProjectService +{ + /// + /// Gets the description of the specified project. + /// + /// The project whose description to get. + /// The description of the specified project. + /// is . + string GetDescription(IProject project); + + /// + /// Gets all projects. + /// + /// A read-only list of projects. + IReadOnlyList GetAllProjects(); + + /// + /// Gets all projects with the specified status. + /// + /// The status of the projects to get. + /// A read-only list of projects with the specified status. + IReadOnlyList GetProjects(ProjectStatus status = ProjectStatus.Ongoing); + + /// + /// Attempts to find a project with the specified ID. + /// + /// The ID of the project. + /// + /// When this method returns, contains the project associated with the specified ID, if the project is found; + /// otherwise, . + /// + /// + /// if a project with the specified ID is found; otherwise, . + /// + bool TryGetProject(Guid id, [NotNullWhen(true)] out IProject? project); + + /// + /// Attempts to find a project with the specified slug. + /// + /// The slug of the project. + /// + /// When this method returns, contains the project associated with the specified slug, if the project is found; + /// otherwise, . + /// + /// + /// if a project with the specified slug is found; otherwise, . + /// + bool TryGetProject(string slug, [NotNullWhen(true)] out IProject? project); +} diff --git a/OliverBooth/Services/ProjectService.cs b/OliverBooth/Services/ProjectService.cs new file mode 100644 index 0000000..b8b3407 --- /dev/null +++ b/OliverBooth/Services/ProjectService.cs @@ -0,0 +1,65 @@ +using System.Diagnostics.CodeAnalysis; +using Markdig; +using Markdig.Parsers; +using Markdig.Renderers.Html; +using Markdig.Syntax; +using Microsoft.EntityFrameworkCore; +using OliverBooth.Data.Web; + +namespace OliverBooth.Services; + +/// +/// Represents a service for interacting with projects. +/// +internal sealed class ProjectService : IProjectService +{ + private readonly IDbContextFactory _dbContextFactory; + private readonly MarkdownPipeline _markdownPipeline; + + /// + /// Initializes a new instance of the class. + /// + /// The database context factory. + /// The Markdown pipeline. + public ProjectService(IDbContextFactory dbContextFactory, MarkdownPipeline markdownPipeline) + { + _dbContextFactory = dbContextFactory; + _markdownPipeline = markdownPipeline; + } + + /// + public string GetDescription(IProject project) + { + return Markdig.Markdown.ToHtml(project.Description, _markdownPipeline); + } + + /// + public IReadOnlyList GetAllProjects() + { + using WebContext context = _dbContextFactory.CreateDbContext(); + return context.Projects.OrderBy(p => p.Rank).ThenBy(p => p.Name).ToArray(); + } + + /// + public IReadOnlyList GetProjects(ProjectStatus status = ProjectStatus.Ongoing) + { + using WebContext context = _dbContextFactory.CreateDbContext(); + return context.Projects.Where(p => p.Status == status).OrderBy(p => p.Rank).ThenBy(p => p.Name).ToArray(); + } + + /// + public bool TryGetProject(Guid id, [NotNullWhen(true)] out IProject? project) + { + using WebContext context = _dbContextFactory.CreateDbContext(); + project = context.Projects.Find(id); + return project is not null; + } + + /// + public bool TryGetProject(string slug, [NotNullWhen(true)] out IProject? project) + { + using WebContext context = _dbContextFactory.CreateDbContext(); + project = context.Projects.FirstOrDefault(p => p.Slug == slug); + return project is not null; + } +} diff --git a/src/ts/UI.ts b/src/ts/UI.ts index f5444f5..d00fae2 100644 --- a/src/ts/UI.ts +++ b/src/ts/UI.ts @@ -79,6 +79,7 @@ class UI { UI.renderSpoilers(element); UI.renderTeX(element); UI.renderTimestamps(element); + UI.updateProjectCards(element); } /** @@ -224,6 +225,13 @@ class UI { block.innerHTML = content; }); } + + private static updateProjectCards(element?: Element) { + element = element || document.body; + element.querySelectorAll(".project-card .card-body p").forEach((p: HTMLParagraphElement) => { + p.classList.add("card-text"); + }); + } } export default UI; \ No newline at end of file