Compare commits

...

3 Commits

19 changed files with 740 additions and 471 deletions

View File

@ -0,0 +1,47 @@
using System.Text;
using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using OliverBooth.Data.Web;
using OliverBooth.Services;
namespace OliverBooth.Controllers;
[Controller]
[Route("contact/blacklist/formatted")]
public class FormattedBlacklistController : Controller
{
private readonly IContactService _contactService;
/// <summary>
/// Initializes a new instance of the <see cref="FormattedBlacklistController" /> class.
/// </summary>
/// <param name="contactService">The <see cref="IContactService" />.</param>
public FormattedBlacklistController(IContactService contactService)
{
_contactService = contactService;
}
[HttpGet("{format}")]
public IActionResult OnGet([FromRoute] string format)
{
IReadOnlyCollection<IBlacklistEntry> blacklist = _contactService.GetBlacklist();
switch (format)
{
case "json":
return Json(blacklist);
case "csv":
var builder = new StringBuilder();
builder.AppendLine("EmailAddress,Name,Reason");
foreach (IBlacklistEntry entry in blacklist)
{
builder.AppendLine($"{entry.EmailAddress},{entry.Name},{entry.Reason}");
}
return Content(builder.ToString(), "text/csv", Encoding.UTF8);
}
return NotFound();
}
}

View File

@ -0,0 +1,75 @@
namespace OliverBooth.Data.Web;
/// <inheritdoc cref="IBlacklistEntry"/>
internal sealed class BlacklistEntry : IEquatable<BlacklistEntry>, IBlacklistEntry
{
/// <inheritdoc/>
public string EmailAddress { get; internal set; } = string.Empty;
/// <inheritdoc/>
public string Name { get; internal set; } = string.Empty;
/// <inheritdoc/>
public string Reason { get; internal set; } = string.Empty;
/// <summary>
/// Returns a value indicating whether two instances of <see cref="BlacklistEntry" /> are equal.
/// </summary>
/// <param name="left">The first instance of <see cref="BlacklistEntry" /> to compare.</param>
/// <param name="right">The second instance of <see cref="BlacklistEntry" /> 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 ==(BlacklistEntry? left, BlacklistEntry? right) => Equals(left, right);
/// <summary>
/// Returns a value indicating whether two instances of <see cref="BlacklistEntry" /> are not equal.
/// </summary>
/// <param name="left">The first instance of <see cref="BlacklistEntry" /> to compare.</param>
/// <param name="right">The second instance of <see cref="BlacklistEntry" /> 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 !=(BlacklistEntry? left, BlacklistEntry? right) => !(left == right);
/// <summary>
/// Returns a value indicating whether this instance of <see cref="BlacklistEntry" /> 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(BlacklistEntry? other)
{
if (ReferenceEquals(null, other)) return false;
if (ReferenceEquals(this, other)) return true;
return EmailAddress.Equals(other.EmailAddress);
}
/// <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="BlacklistEntry" /> and
/// equals the value of this instance; otherwise, <see langword="false" />.
/// </returns>
public override bool Equals(object? obj)
{
return ReferenceEquals(this, obj) || obj is BlacklistEntry 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 EmailAddress.GetHashCode();
}
}

View File

@ -0,0 +1,21 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
namespace OliverBooth.Data.Web.Configuration;
/// <summary>
/// Represents the configuration for the <see cref="BlacklistEntry" /> entity.
/// </summary>
internal sealed class BlacklistEntryConfiguration : IEntityTypeConfiguration<BlacklistEntry>
{
/// <inheritdoc />
public void Configure(EntityTypeBuilder<BlacklistEntry> builder)
{
builder.ToTable("ContactBlacklist");
builder.HasKey(entry => entry.EmailAddress);
builder.Property(entry => entry.EmailAddress).IsRequired();
builder.Property(entry => entry.Name).IsRequired();
builder.Property(entry => entry.Reason).IsRequired();
}
}

View File

@ -0,0 +1,25 @@
namespace OliverBooth.Data.Web;
/// <summary>
/// Represents an entry in the blacklist.
/// </summary>
public interface IBlacklistEntry
{
/// <summary>
/// Gets the email address of the entry.
/// </summary>
/// <value>The email address of the entry.</value>
string EmailAddress { get; }
/// <summary>
/// Gets the name of the entry.
/// </summary>
/// <value>The name of the entry.</value>
string Name { get; }
/// <summary>
/// Gets the reason for the entry.
/// </summary>
/// <value>The reason for the entry.</value>
string Reason { get; }
}

View File

@ -25,6 +25,12 @@ internal sealed class WebContext : DbContext
/// <value>The collection of books.</value>
public DbSet<Book> Books { get; private set; } = null!;
/// <summary>
/// Gets the collection of blacklist entries in the database.
/// </summary>
/// <value>The collection of blacklist entries.</value>
public DbSet<BlacklistEntry> ContactBlacklist { get; private set; } = null!;
/// <summary>
/// Gets the collection of projects in the database.
/// </summary>
@ -53,6 +59,7 @@ internal sealed class WebContext : DbContext
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new BlacklistEntryConfiguration());
modelBuilder.ApplyConfiguration(new BookConfiguration());
modelBuilder.ApplyConfiguration(new ProjectConfiguration());
modelBuilder.ApplyConfiguration(new TemplateConfiguration());

View File

@ -10,17 +10,18 @@
ViewData["Title"] = "Reading List";
}
<h1 class="display-4">Reading List</h1>
<p>
This is a list of the books I've read, I'm currently reading, or that I plan to read. Not every book is listed here,
but I will update this list as I try to remember what it is I've read in the past.
</p>
<p>
<main class="container">
<h1 class="display-4">Reading List</h1>
<p>
This is a list of the books I've read, I'm currently reading, or that I plan to read. Not every book is listed
here, but I will update this list as I try to remember what it is I've read in the past.
</p>
<p>
This list is also available on <a href="https://www.goodreads.com/review/list/145592619">Goodreads</a>.
</p>
</p>
<p class="lead">Currently Reading</p>
<table class="table reading-list">
<p class="lead">Currently Reading</p>
<table class="table reading-list">
<thead>
<tr>
<th style="width: 50%">Title</th>
@ -38,14 +39,16 @@
@book.Title.Trim()
</td>
<td>@book.Author.Trim()</td>
<td style="font-family: monospace">@book.Isbn.Trim()<br><img src="@book.GetBarcode()" alt="@book.Isbn"></td>
<td style="font-family: monospace">
@book.Isbn.Trim()<br><img src="@book.GetBarcode()" alt="@book.Isbn">
</td>
</tr>
}
</tbody>
</table>
</table>
<p class="lead">Plan to Read</p>
<table class="table reading-list">
<p class="lead">Plan to Read</p>
<table class="table reading-list">
<thead>
<tr>
<th style="width: 50%">Title</th>
@ -63,14 +66,16 @@
@book.Title.Trim()
</td>
<td>@book.Author.Trim()</td>
<td style="font-family: monospace">@book.Isbn.Trim()<br><img src="@book.GetBarcode()" alt="@book.Isbn"></td>
<td style="font-family: monospace">
@book.Isbn.Trim()<br><img src="@book.GetBarcode()" alt="@book.Isbn">
</td>
</tr>
}
</tbody>
</table>
</table>
<p class="lead">Read</p>
<table class="table reading-list">
<p class="lead">Read</p>
<table class="table reading-list">
<thead>
<tr>
<th style="width: 50%">Title</th>
@ -88,8 +93,11 @@
@book.Title.Trim()
</td>
<td>@book.Author.Trim()</td>
<td style="font-family: monospace">@book.Isbn.Trim()<br><img src="@book.GetBarcode()" alt="@book.Isbn"></td>
<td style="font-family: monospace">
@book.Isbn.Trim()<br><img src="@book.GetBarcode()" alt="@book.Isbn">
</td>
</tr>
}
</tbody>
</table>
</table>
</main>

View File

@ -0,0 +1,38 @@
@page
@using OliverBooth.Data.Web
@using OliverBooth.Services
@inject IContactService ContactService
@{
ViewData["Title"] = "Blacklist";
}
<main class="container">
<h1 class="display-4">Contact Blacklist</h1>
<p>
Below is a list of email addresses that have been blocked from contacting me. This list is public so that others
may also block these addresses if they wish. Any email address that contains an asterisk (*) is a wildcard,
meaning that any email address that matches the pattern will be blocked.
</p>
<p>
You can view this list in JSON format
<a asp-controller="FormattedBlacklist" asp-action="OnGet" asp-route-format="json">here</a>,
or in CSV format
<a asp-controller="FormattedBlacklist" asp-action="OnGet" asp-route-format="csv">here</a>.
</p>
<table class="table">
<tr>
<th>Name / Email</th>
<th>Reason</th>
</tr>
@foreach (IBlacklistEntry entry in ContactService.GetBlacklist())
{
<tr>
<td>@entry.Name &lt;@entry.EmailAddress&gt;</td>
<td>@entry.Reason</td>
</tr>
}
</table>
</main>

View File

@ -3,52 +3,39 @@
ViewData["Title"] = "Contact";
}
<h1 class="display-4">Contact</h1>
<p>
Thanks for getting in touch! While I do my best to read to all inquiries, I cannot guarantee that I will be able to
respond to your message. Nevertheless, I appreciate you taking the time to reach out to me and I will respond if I
can.
</p>
<main class="container">
<h1 class="display-4">Contact</h1>
<p>
Thanks for getting in touch! While I do my best to read to all inquiries, I cannot guarantee that I will be able
to respond to your message. Nevertheless, I appreciate you taking the time to reach out to me and I will respond
if I can.
</p>
<div class="alert alert-warning">
<div class="alert alert-warning">
<p class="lead"><i class="fa-solid fa-triangle-exclamation"></i> Spam warning</p>
<p>
In the short time that this site has been live, I have received a considerable amount of spam that I suspect is
automated. As such, I added background reCaptcha validation to this form to minimise the amount of
bot-written junk I receive. However, some spam still gets through - that means there are humans reading this.
</p>
<p>
I am politely asking that you respect my inbox and keep your unsolicited advertising away. This is a simple
request.
request. If you send me any kind of spam after this, you have demonstrated that you do not have the basic
human decency to respect my wishes or my privacy, and you have lost the privilege for me to respect yours. I
<strong>will</strong> block your email address, and I will add your name to my public blacklist of spammers,
which you can find <a asp-page="/Contact/Blacklist">here</a>.
</p>
<p>
If you send me any kind of spam after this, you have demonstrated that you do not have the basic human decency
to respect my wishes or my privacy, and you have lost the privilege for me to respect yours. I
<strong>will</strong> block your email address and report you to your email provider. I will also begin to
maintain a public blacklist of spammers, so that others may do the same. You will lose all credibility, and you
<strong>will</strong> be publicly shamed for it.
</p>
<p>
This <strong>does not apply</strong> to legitimate businesses who are offering something genuine
<span style="text-decoration: underline;">to me specifically</span>, nor to individuals who are interested in my
services.
</p>
</div>
</div>
<div class="alert alert-info">
<div class="alert alert-info">
<p class="lead"><i class="fa-solid fa-circle-info"></i> Dear SEO marketing teams</p>
<p>
While I don't necessarily consider receiving legitimate offers for SEO services to be spam, I am not interested
in your services at this time. I understand that you are just doing your job, but hopefully you can do it more
efficiently by going to others who are more receptive to your services.
While I don't necessarily consider receiving legitimate offers for SEO services to be spam, I am not
interested in your services at this time. I understand that you are just doing your job, but hopefully you
can do it more efficiently by going to others who are more receptive to your services.
</p>
<p>
Do not contact me about SEO services. If you do, while I will not publicly blacklist your address, you will -
however - be blocked.
Do not contact me about SEO services. If you do, while I will not publicly blacklist your address, you
will - however - be blocked.
</p>
</div>
</div>
<form method="post" asp-controller="Contact" asp-action="HandleForm">
<form method="post" asp-controller="Contact" asp-action="HandleForm">
<input type="hidden" name="contact-type" value="other">
<div class="form-group" style="margin-top: 10px;">
@ -74,4 +61,5 @@
<recaptcha/>
<button class="btn btn-primary" style="margin-top: 10px;">Submit</button>
</form>
</form>
</main>

View File

@ -3,35 +3,37 @@
ViewData["Title"] = "Donate";
}
<h1 class="display-4">Donate</h1>
<main class="container">
<h1 class="display-4">Donate</h1>
<p>
I believe in free and open exchange of information, and I want to keep my educational content free for everyone to
access. I will never put ads on my site, and I don't want to put behind a paywall resources that should be available
to everybody, regardless of their financial situation.
</p>
<p>
<p>
I believe in free and open exchange of information, and I want to keep my educational content free for everyone
to access. I will never put ads on my site, and I don't want to put behind a paywall resources that should be
available to everybody, regardless of their financial situation.
</p>
<p>
However, writing tutorials takes time, and I do have to pay for hosting. While I will never ask you for money, I
will always appreciate it if you do decide to donate. It also helps me to know that people are finding my content
useful, and that I should continue to make more.
</p>
<p>
If you like what I do and are both willing and able to donate money to me and fund all of this, you can do so using
the links below. Thank you for your support!
</p>
will always appreciate it if you do decide to donate. It also helps me to know that people are finding my
content useful, and that I should continue to make more.
</p>
<p>
If you like what I do and are both willing and able to donate money to me and fund all of this, you can do so
using the links below. Thank you for your support!
</p>
<p>
<p>
<script type="text/javascript" src="https://storage.ko-fi.com/cdn/widget/Widget_2.js"></script>
<script type="text/javascript">kofiwidget2.init('Support Me on Ko-fi', '#29abe0', 'C0C4DVDZX');kofiwidget2.draw();</script>
</p>
</p>
<p>
<p>
<script type="text/javascript" src="https://cdnjs.buymeacoffee.com/1.0.0/button.prod.min.js" data-name="bmc-button" data-slug="oliverbooth" data-color="#FFDD00" data-emoji="" data-font="Cookie" data-text="Buy me a coffee" data-outline-color="#000000" data-font-color="#000000" data-coffee-color="#ffffff"></script>
</p>
</p>
<p>I also accept cryptocurrency donations.</p>
<p>I also accept cryptocurrency donations.</p>
<ul>
<ul>
<li>BTC: 1LmXvavJr1omscfkXjp7A4VyNf3XhKP9JK</li>
<li>ETH: 0x972C6641e36e2736823A6B1e6BA4D2A814b69fD2</li>
</ul>
</ul>
</main>

View File

@ -1,30 +1,33 @@
@page
<h1 class="display-4">Hi, I'm Oliver.</h1>
<p class="lead">I'm a tech enthusiast, coffee drinker, and software developer.</p>
<main class="container">
<h1 class="display-4">Hi, I'm Oliver.</h1>
<p class="lead">I'm a tech enthusiast, coffee drinker, and software developer.</p>
<p class="text-center">
<p class="text-center">
<img src="~/img/headshot_512x512_2023.jpg" style="width: 512px; max-width: 100%;">
</p>
</p>
<p>
My primary focus is C#, though I have dabbled in several other languages such as Java, Kotlin, VB, C/C++, Python,
and others. Over the years I've built up a collection of projects. Some of which I'm extremely proud of, and others
I've quietly abandoned and buried. I'm currently working on a few projects that I hope to release in the near
future, but in the meantime, feel free to check out some of my <a asp-page="/Projects/Index">previous work</a>.
</p>
<p>
My primary focus is C#, though I have dabbled in several other languages such as Java, Kotlin, VB, C/C++,
Python, and others. Over the years I've built up a collection of projects. Some of which I'm extremely proud of,
and others I've quietly abandoned and buried. I'm currently working on a few projects that I hope to release in
the near future, but in the meantime, feel free to check out some of my
<a asp-page="/Projects/Index">previous work</a>.
</p>
<p>
<p>
I've also written a few <a asp-page="/Tutorials/Index">tutorials</a> on various topics, usually involving
information not readily available elsewhere. I hope you find them useful. On occasion, I also write about other
topics that I find interesting, such as
<a asp-page="/Blog/Index">my thoughts on the state of the world or the tech industry</a>.
</p>
</p>
<p>
If you want a general overview of stuff I've made, check out my <a href="https://github.com/oliverbooth">GitHub</a>,
<a href="https://oliverbooth.itch.io">itch.io</a>, and
<p>
If you want a general overview of stuff I've made, check out my
<a href="https://github.com/oliverbooth">GitHub</a>, <a href="https://oliverbooth.itch.io">itch.io</a>, and
<a href="https://play.google.com/store/apps/dev?id=9010459220239503006">Google Play</a>.
</p>
</p>
<p>If you'd like to get in touch, you can do so by <a asp-page="/Contact/Index">clicking here</a>.</p>
<p>If you'd like to get in touch, you can do so by <a asp-page="/Contact/Index">clicking here</a>.</p>
</main>

View File

@ -3,64 +3,66 @@
ViewData["Title"] = "It's 5 O'Clock Somewhere Privacy Policy";
}
<h1 class="display-4">@ViewData["Title"]</h1>
<p class="lead">Last Updated: 24 September 2023</p>
<div class="alert alert-primary">
<main class="container">
<h1 class="display-4">@ViewData["Title"]</h1>
<p class="lead">Last Updated: 24 September 2023</p>
<div class="alert alert-primary">
This Privacy Policy differs from the policy that applies to other applications that I published on Google Play. For
the generalised privacy policy, please <a asp-page="/Privacy/GooglePlay">click here</a>.
</div>
</div>
<p>
<p>
This Privacy Policy describes how your personal information is collected, used, and shared when you use or interact
with the application <em>It's 5 O'Clock Somewhere</em> (the "Application").
</p>
</p>
<h2>Introduction</h2>
<p>
I am committed to protecting your privacy and ensuring the security of any information you provide to me when using
my applications. This Privacy Policy outlines my practices regarding the collection, use, and disclosure of
information I may gather from users of the Application.
</p>
<h2>Introduction</h2>
<p>
I am committed to protecting your privacy and ensuring the security of any information you provide to me when
using my applications. This Privacy Policy outlines my practices regarding the collection, use, and disclosure
of information I may gather from users of the Application.
</p>
<h2>Information I Collect</h2>
<p>
The Application will temporarily read your device's clock to determine the current time. This information is sent to
the Application's server to determine an accurate and time-zone aware response to you, the user. This information is
not stored or retained on the server. I do not use cookies, tracking technologies, or any other means to collect or
track your usage behavior within the Application. I do not have access to any personal data, including your name,
email address, or any other personally identifiable information.
</p>
<h2>Information I Collect</h2>
<p>
The Application will temporarily read your device's clock to determine the current time. This information is
sent to the Application's server to determine an accurate and time-zone aware response to you, the user. This
information is not stored or retained on the server. I do not use cookies, tracking technologies, or any other
means to collect or track your usage behavior within the Application. I do not have access to any personal data,
including your name, email address, or any other personally identifiable information.
</p>
<h2>Use of Information</h2>
<p>
The Application uses the little information it collects from you to provide the Application's functionality to you.
This information is not used for any other purpose.
</p>
<h2>Use of Information</h2>
<p>
The Application uses the little information it collects from you to provide the Application's functionality to
you. This information is not used for any other purpose.
</p>
<h2>Disclosure of Information</h2>
<p>
<h2>Disclosure of Information</h2>
<p>
I do not disclose your personal information to any third parties unless required by law or with your explicit
consent. I maintain strict confidentiality and take reasonable precautions to protect your personal information from
unauthorized access, use, or disclosure.
</p>
consent. I maintain strict confidentiality and take reasonable precautions to protect your personal information
from unauthorized access, use, or disclosure.
</p>
<h2>Security</h2>
<p>
<h2>Security</h2>
<p>
I prioritize the security of your personal information and take reasonable precautions to protect it. However,
please be aware that no method of transmission over the internet or electronic storage is 100% secure, and I cannot
guarantee absolute security.
</p>
please be aware that no method of transmission over the internet or electronic storage is 100% secure, and I
cannot guarantee absolute security.
</p>
<h2>Changes to this Privacy Policy</h2>
<p>
<h2>Changes to this Privacy Policy</h2>
<p>
I may update this Privacy Policy from time to time to reflect changes in my practices or for other operational,
legal, or regulatory reasons. I encourage you to review this Privacy Policy periodically for any updates. The
revised policy will be effective immediately upon posting.
</p>
</p>
<h2>Contact Me</h2>
<p>
<h2>Contact Me</h2>
<p>
If you have any questions or concerns about this Privacy Policy or my privacy practices, please
<a asp-page="/Contact/Index">get in touch</a>.
</p>
</p>
</main>

View File

@ -3,68 +3,70 @@
ViewData["Title"] = "Google Play Privacy Policy";
}
<h1 class="display-4">@ViewData["Title"]</h1>
<p class="lead">Last Updated: 24 September 2023</p>
<div class="alert alert-primary">
This Privacy Policy differs from the policy that applies to this website. For this website's privacy policy, please
<a asp-page="/Privacy/Index">click here</a>.
</div>
<div class="alert alert-warning">
This Privacy Policy does not apply to the application <em>It's 5 O'Clock Somewhere</em>. For the privacy policy that
applies to that application, please <a asp-page="/Privacy/FiveOClockSomewhere">click here</a>.
</div>
<main class="container">
<h1 class="display-4">@ViewData["Title"]</h1>
<p class="lead">Last Updated: 24 September 2023</p>
<div class="alert alert-primary">
This Privacy Policy differs from the policy that applies to this website. For this website's privacy policy,
please <a asp-page="/Privacy/Index">click here</a>.
</div>
<div class="alert alert-warning">
This Privacy Policy does not apply to the application <em>It's 5 O'Clock Somewhere</em>. For the privacy policy
that applies to that application, please <a asp-page="/Privacy/FiveOClockSomewhere">click here</a>.
</div>
<p>
This Privacy Policy describes how your personal information is collected, used, and shared when you use or interact
with applications that I publish on the <a href="https://play.google.com/store/">Google Play Store</a>.
</p>
<p>
This Privacy Policy describes how your personal information is collected, used, and shared when you use or
interact with applications that I publish on the <a href="https://play.google.com/store/">Google Play Store</a>.
</p>
<h2>Introduction</h2>
<p>
I am committed to protecting your privacy and ensuring the security of any information you provide to me when using
my applications. This Privacy Policy outlines my practices regarding the collection, use, and disclosure of
information I may gather from users of my applications.
</p>
<h2>Introduction</h2>
<p>
I am committed to protecting your privacy and ensuring the security of any information you provide to me when
using my applications. This Privacy Policy outlines my practices regarding the collection, use, and disclosure
of information I may gather from users of my applications.
</p>
<h2>Information I Collect</h2>
<p>
<h2>Information I Collect</h2>
<p>
I do not collect any personally identifiable information about you when you use my applications. I do not use any
cookies or similar tracking technologies that can identify individual users or track your usage behavior within the
application.
</p>
<p>
However, please note that my applications may make use of third party integrations such as Google Play Services or
others. I am not responsible for the privacy practices or content of these third-party services. I encourage you to
review the privacy policies of those third-party services before providing any personal information.
</p>
cookies or similar tracking technologies that can identify individual users or track your usage behavior within
the application.
</p>
<p>
However, please note that my applications may make use of third party integrations such as Google Play Services
or others. I am not responsible for the privacy practices or content of these third-party services. I encourage
you to review the privacy policies of those third-party services before providing any personal information.
</p>
<h2>Use of Information</h2>
<p>Since I do not collect any personal information about you, I do not use it for any purpose.</p>
<h2>Use of Information</h2>
<p>Since I do not collect any personal information about you, I do not use it for any purpose.</p>
<h2>Disclosure of Information</h2>
<p>I do not share any personal information about you because I do not collect any such information.</p>
<p>
<h2>Disclosure of Information</h2>
<p>I do not share any personal information about you because I do not collect any such information.</p>
<p>
However, please be aware that my applications contains links to third-party websites and services. I am not
responsible for the privacy practices or content of these third-party sites. I encourage you to review the privacy
policies of those third-party sites before providing any personal information.
</p>
responsible for the privacy practices or content of these third-party sites. I encourage you to review the
privacy policies of those third-party sites before providing any personal information.
</p>
<h2>Security</h2>
<p>
<h2>Security</h2>
<p>
I prioritize the security of your personal information and take reasonable precautions to protect it. However,
please be aware that no method of transmission over the internet or electronic storage is 100% secure, and I cannot
guarantee absolute security.
</p>
please be aware that no method of transmission over the internet or electronic storage is 100% secure, and I
cannot guarantee absolute security.
</p>
<h2>Changes to this Privacy Policy</h2>
<p>
<h2>Changes to this Privacy Policy</h2>
<p>
I may update this Privacy Policy from time to time to reflect changes in my practices or for other operational,
legal, or regulatory reasons. I encourage you to review this Privacy Policy periodically for any updates. The
revised policy will be effective immediately upon posting.
</p>
</p>
<h2>Contact Me</h2>
<p>
<h2>Contact Me</h2>
<p>
If you have any questions or concerns about this Privacy Policy or my privacy practices, please
<a asp-page="/Contact/Index">get in touch</a>.
</p>
</p>
</main>

View File

@ -3,84 +3,86 @@
ViewData["Title"] = "Privacy Policy";
}
<h1 class="display-4">@ViewData["Title"]</h1>
<p class="lead">Last Updated: 26 May 2023</p>
<div class="alert alert-primary">
<main class="container">
<h1 class="display-4">@ViewData["Title"]</h1>
<p class="lead">Last Updated: 26 May 2023</p>
<div class="alert alert-primary">
This Privacy Policy differs from the policy that applies to my applications published to Google Play. For my
applications' privacy policy, please <a asp-page="/Privacy/GooglePlay">click here</a>.
</div>
</div>
<p>
<p>
This Privacy Policy describes how your personal information is collected, used, and shared when you visit or
interact with my website <a href="https://oliverbooth.dev/">oliverbooth.dev</a>.
</p>
</p>
<h2>Introduction</h2>
<p>
I am committed to protecting your privacy and ensuring the security of any information you provide to me when using
my website. This Privacy Policy outlines my practices regarding the collection, use, and disclosure of information I
may gather from users of my website.
</p>
<h2>Introduction</h2>
<p>
I am committed to protecting your privacy and ensuring the security of any information you provide to me when
using my website. This Privacy Policy outlines my practices regarding the collection, use, and disclosure of
information I may gather from users of my website.
</p>
<h2>Information I Collect</h2>
<p>
<h2>Information I Collect</h2>
<p>
When you choose to contact me via the contact form on my website, I collect your name and email address. This
information is provided voluntarily by you and is necessary for me to respond to your inquiries and engage in a
conversation. I do not collect any additional personally identifiable information about you through the contact
form.
</p>
<p>I do not use any cookies or similar tracking technologies that can identify individual users.</p>
<p>
</p>
<p>I do not use any cookies or similar tracking technologies that can identify individual users.</p>
<p>
Please note that my website includes a Disqus integration for commenting on blog posts. Disqus is a third-party
service, and their use of cookies and collection of personal information are governed by their own privacy policies.
I have no control over the information collected by Disqus, and I encourage you to review their privacy policy to
understand how your information may be used by them.
</p>
service, and their use of cookies and collection of personal information are governed by their own privacy
policies. I have no control over the information collected by Disqus, and I encourage you to review their
privacy policy to understand how your information may be used by them.
</p>
<h2>Use of Information</h2>
<p>
<h2>Use of Information</h2>
<p>
The name and email address you provide through the contact form are used solely for the purpose of responding to
your inquiries and engaging in relevant conversation. I keep this information confidential and do not disclose,
sell, or share it with any third parties unless required by law or with your explicit consent.
</p>
<p>
I do not use your personal information for marketing purposes or send you any unsolicited communications. Once our
conversation is complete and no longer necessary, I will securely delete your personal information unless otherwise
required to retain it by applicable laws or regulations.
</p>
</p>
<p>
I do not use your personal information for marketing purposes or send you any unsolicited communications. Once
our conversation is complete and no longer necessary, I will securely delete your personal information unless
otherwise required to retain it by applicable laws or regulations.
</p>
<h2>Disclosure of Information</h2>
<p>
<h2>Disclosure of Information</h2>
<p>
I do not disclose your personal information to any third parties unless required by law or with your explicit
consent. I maintain strict confidentiality and take reasonable precautions to protect your personal information from
unauthorized access, use, or disclosure.
</p>
<p>
However, please be aware that my website contains links to third-party websites and services. I am not responsible
for the privacy practices or content of these third-party sites. I encourage you to review the privacy policies of
those third-party sites before providing any personal information.
</p>
consent. I maintain strict confidentiality and take reasonable precautions to protect your personal information
from unauthorized access, use, or disclosure.
</p>
<p>
However, please be aware that my website contains links to third-party websites and services. I am not
responsible for the privacy practices or content of these third-party sites. I encourage you to review the
privacy policies of those third-party sites before providing any personal information.
</p>
<h2>Security</h2>
<p>
<h2>Security</h2>
<p>
I prioritize the security of your personal information and take reasonable precautions to protect it. However,
please be aware that no method of transmission over the internet or electronic storage is 100% secure, and I cannot
guarantee absolute security.
</p>
please be aware that no method of transmission over the internet or electronic storage is 100% secure, and I
cannot guarantee absolute security.
</p>
<h2>Changes to this Privacy Policy</h2>
<p>
<h2>Changes to this Privacy Policy</h2>
<p>
I may update this Privacy Policy from time to time to reflect changes in my practices or for other operational,
legal, or regulatory reasons. I encourage you to review this Privacy Policy periodically for any updates. The
revised policy will be effective immediately upon posting.
</p>
</p>
<h2>Contact Me</h2>
<p>
<h2>Contact Me</h2>
<p>
If you have any questions or concerns about this Privacy Policy or my privacy practices, please
<a asp-page="/Contact/Index">get in touch</a>.
</p>
</p>
<hr/>
<hr/>
<p>By using my website, you signify your acceptance of this Privacy Policy.</p>
<p>By using my website, you signify your acceptance of this Privacy Policy.</p>
</main>

View File

@ -6,10 +6,11 @@
ViewData["Title"] = "Projects";
}
<h1 class="display-4">Projects</h1>
<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 ProjectService.GetProjects(ProjectStatus.Ongoing).OrderBy(p => p.Rank).Chunk(2))
{
<div class="card-group row" style="margin-top: 20px;">
@foreach (IProject project in chunk)
{
@ -38,10 +39,10 @@
</div>
}
</div>
}
}
@foreach (IProject[] chunk in ProjectService.GetProjects(ProjectStatus.Past).Chunk(2))
{
@foreach (IProject[] chunk in ProjectService.GetProjects(ProjectStatus.Past).Chunk(2))
{
<div class="card-group row" style="margin-top: 20px;">
@foreach (IProject project in chunk)
{
@ -70,10 +71,10 @@
</div>
}
</div>
}
}
@foreach (IProject[] chunk in ProjectService.GetProjects(ProjectStatus.Hiatus).Chunk(2))
{
@foreach (IProject[] chunk in ProjectService.GetProjects(ProjectStatus.Hiatus).Chunk(2))
{
<div class="card-group row" style="margin-top: 20px;">
@foreach (IProject project in chunk)
{
@ -102,4 +103,5 @@
</div>
}
</div>
}
}
</main>

View File

@ -3,19 +3,21 @@
ViewData["Title"] = "Tutorials";
}
<h1 class="display-4">Tutorials</h1>
<p class="lead">Coming Soon</p>
<p>
<main class="container">
<h1 class="display-4">Tutorials</h1>
<p class="lead">Coming Soon</p>
<p>
Due to Unity's poor corporate decision-making, I'm left in a position where I find it infeasible to write Unity
tutorials. I plan to write tutorials for things like Unreal and MonoGame as I learn them, and C# tutorials are
still on the table for sure. But tutorials take a lot of time and effort, so unfortunately it may be a while before
I can get around to publishing them.
</p>
<p>
still on the table for sure. But tutorials take a lot of time and effort, so unfortunately it may be a while
before I can get around to publishing them.
</p>
<p>
However, in the meantime, I do have various blog posts that contain some tutorials and guides. You can find them
<a asp-page="/Blog/Index">here</a>!
</p>
<p>
</p>
<p>
I'm sorry for the inconvenience, but I hope you understand my position. Watch this space! New tutorials will be
coming. If you have any questions or requests, please feel free to <a asp-page="/Contact/Index">contact me</a>.
</p>
</p>
</main>

View File

@ -32,6 +32,7 @@ builder.Services.AddSingleton(provider => new MarkdownPipelineBuilder()
builder.Services.AddDbContextFactory<BlogContext>();
builder.Services.AddDbContextFactory<WebContext>();
builder.Services.AddSingleton<IContactService, ContactService>();
builder.Services.AddSingleton<ITemplateService, TemplateService>();
builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
builder.Services.AddSingleton<IBlogUserService, BlogUserService>();

View File

@ -0,0 +1,26 @@
using Microsoft.EntityFrameworkCore;
using OliverBooth.Data.Web;
namespace OliverBooth.Services;
/// <inheritdoc cref="IContactService" />
internal sealed class ContactService : IContactService
{
private readonly IDbContextFactory<WebContext> _dbContextFactory;
/// <summary>
/// Initializes a new instance of the <see cref="ContactService" /> class.
/// </summary>
/// <param name="dbContextFactory">The <see cref="IDbContextFactory{TContext}" />.</param>
public ContactService(IDbContextFactory<WebContext> dbContextFactory)
{
_dbContextFactory = dbContextFactory;
}
/// <inheritdoc />
public IReadOnlyCollection<IBlacklistEntry> GetBlacklist()
{
using WebContext context = _dbContextFactory.CreateDbContext();
return context.ContactBlacklist.OrderBy(b => b.EmailAddress).ToArray();
}
}

View File

@ -0,0 +1,14 @@
using OliverBooth.Data.Web;
namespace OliverBooth.Services;
/// <summary>
/// Represents a service for managing contact information.
/// </summary>
public interface IContactService
{
/// <summary>
/// Gets the blacklist.
/// </summary>
IReadOnlyCollection<IBlacklistEntry> GetBlacklist();
}

View File

@ -1,7 +1,6 @@
html, body {
background: #121212;
color: #f5f5f5;
font-family: 'Gabarito', sans-serif;
font-size: 16px;
}
@ -24,6 +23,11 @@ body {
margin-bottom: 60px;
}
main.container {
background: #333;
padding: 20px;
}
a {
&:link, &:visited, &:hover, &:active {
text-decoration: none;