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

View File

@ -10,10 +10,11 @@
ViewData["Title"] = "Reading List"; ViewData["Title"] = "Reading List";
} }
<main class="container">
<h1 class="display-4">Reading List</h1> <h1 class="display-4">Reading List</h1>
<p> <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, 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
but I will update this list as I try to remember what it is I've read in the past. here, but I will update this list as I try to remember what it is I've read in the past.
</p> </p>
<p> <p>
This list is also available on <a href="https://www.goodreads.com/review/list/145592619">Goodreads</a>. This list is also available on <a href="https://www.goodreads.com/review/list/145592619">Goodreads</a>.
@ -38,7 +39,9 @@
@book.Title.Trim() @book.Title.Trim()
</td> </td>
<td>@book.Author.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> </tr>
} }
</tbody> </tbody>
@ -63,7 +66,9 @@
@book.Title.Trim() @book.Title.Trim()
</td> </td>
<td>@book.Author.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> </tr>
} }
</tbody> </tbody>
@ -88,8 +93,11 @@
@book.Title.Trim() @book.Title.Trim()
</td> </td>
<td>@book.Author.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> </tr>
} }
</tbody> </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,48 +3,35 @@
ViewData["Title"] = "Contact"; ViewData["Title"] = "Contact";
} }
<main class="container">
<h1 class="display-4">Contact</h1> <h1 class="display-4">Contact</h1>
<p> <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 Thanks for getting in touch! While I do my best to read to all inquiries, I cannot guarantee that I will be able
respond to your message. Nevertheless, I appreciate you taking the time to reach out to me and I will respond if I to respond to your message. Nevertheless, I appreciate you taking the time to reach out to me and I will respond
can. if I can.
</p> </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 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> <p>
I am politely asking that you respect my inbox and keep your unsolicited advertising away. This is a simple 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
</p> human decency to respect my wishes or my privacy, and you have lost the privilege for me to respect yours. I
<p> <strong>will</strong> block your email address, and I will add your name to my public blacklist of spammers,
If you send me any kind of spam after this, you have demonstrated that you do not have the basic human decency which you can find <a asp-page="/Contact/Blacklist">here</a>.
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> </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 class="lead"><i class="fa-solid fa-circle-info"></i> Dear SEO marketing teams</p>
<p> <p>
While I don't necessarily consider receiving legitimate offers for SEO services to be spam, I am not interested While I don't necessarily consider receiving legitimate offers for SEO services to be spam, I am not
in your services at this time. I understand that you are just doing your job, but hopefully you can do it more interested in your services at this time. I understand that you are just doing your job, but hopefully you
efficiently by going to others who are more receptive to your services. can do it more efficiently by going to others who are more receptive to your services.
</p> </p>
<p> <p>
Do not contact me about SEO services. If you do, while I will not publicly blacklist your address, you will - Do not contact me about SEO services. If you do, while I will not publicly blacklist your address, you
however - be blocked. will - however - be blocked.
</p> </p>
</div> </div>
@ -75,3 +62,4 @@
<button class="btn btn-primary" style="margin-top: 10px;">Submit</button> <button class="btn btn-primary" style="margin-top: 10px;">Submit</button>
</form> </form>
</main>

View File

@ -3,21 +3,22 @@
ViewData["Title"] = "Donate"; ViewData["Title"] = "Donate";
} }
<main class="container">
<h1 class="display-4">Donate</h1> <h1 class="display-4">Donate</h1>
<p> <p>
I believe in free and open exchange of information, and I want to keep my educational content free for everyone to I believe in free and open exchange of information, and I want to keep my educational content free for everyone
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 access. I will never put ads on my site, and I don't want to put behind a paywall resources that should be
to everybody, regardless of their financial situation. available to everybody, regardless of their financial situation.
</p> </p>
<p> <p>
However, writing tutorials takes time, and I do have to pay for hosting. While I will never ask you for money, I 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 will always appreciate it if you do decide to donate. It also helps me to know that people are finding my
useful, and that I should continue to make more. content useful, and that I should continue to make more.
</p> </p>
<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 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
the links below. Thank you for your support! using the links below. Thank you for your support!
</p> </p>
<p> <p>
@ -35,3 +36,4 @@
<li>BTC: 1LmXvavJr1omscfkXjp7A4VyNf3XhKP9JK</li> <li>BTC: 1LmXvavJr1omscfkXjp7A4VyNf3XhKP9JK</li>
<li>ETH: 0x972C6641e36e2736823A6B1e6BA4D2A814b69fD2</li> <li>ETH: 0x972C6641e36e2736823A6B1e6BA4D2A814b69fD2</li>
</ul> </ul>
</main>

View File

@ -1,5 +1,6 @@
@page @page
<main class="container">
<h1 class="display-4">Hi, I'm Oliver.</h1> <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="lead">I'm a tech enthusiast, coffee drinker, and software developer.</p>
@ -8,10 +9,11 @@
</p> </p>
<p> <p>
My primary focus is C#, though I have dabbled in several other languages such as Java, Kotlin, VB, C/C++, Python, My primary focus is C#, though I have dabbled in several other languages such as Java, Kotlin, VB, C/C++,
and others. Over the years I've built up a collection of projects. Some of which I'm extremely proud of, and others Python, and others. Over the years I've built up a collection of projects. Some of which I'm extremely proud of,
I've quietly abandoned and buried. I'm currently working on a few projects that I hope to release in the near and others I've quietly abandoned and buried. I'm currently working on a few projects that I hope to release in
future, but in the meantime, feel free to check out some of my <a asp-page="/Projects/Index">previous work</a>. 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> <p>
@ -22,9 +24,10 @@
</p> </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>, If you want a general overview of stuff I've made, check out my
<a href="https://oliverbooth.itch.io">itch.io</a>, and <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>. <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,6 +3,7 @@
ViewData["Title"] = "It's 5 O'Clock Somewhere Privacy Policy"; ViewData["Title"] = "It's 5 O'Clock Somewhere Privacy Policy";
} }
<main class="container">
<h1 class="display-4">@ViewData["Title"]</h1> <h1 class="display-4">@ViewData["Title"]</h1>
<p class="lead">Last Updated: 24 September 2023</p> <p class="lead">Last Updated: 24 September 2023</p>
<div class="alert alert-primary"> <div class="alert alert-primary">
@ -17,39 +18,39 @@
<h2>Introduction</h2> <h2>Introduction</h2>
<p> <p>
I am committed to protecting your privacy and ensuring the security of any information you provide to me when using I am committed to protecting your privacy and ensuring the security of any information you provide to me when
my applications. This Privacy Policy outlines my practices regarding the collection, use, and disclosure of using my applications. This Privacy Policy outlines my practices regarding the collection, use, and disclosure
information I may gather from users of the Application. of information I may gather from users of the Application.
</p> </p>
<h2>Information I Collect</h2> <h2>Information I Collect</h2>
<p> <p>
The Application will temporarily read your device's clock to determine the current time. This information is sent to The Application will temporarily read your device's clock to determine the current time. This information is
the Application's server to determine an accurate and time-zone aware response to you, the user. This information is sent to the Application's server to determine an accurate and time-zone aware response to you, the user. This
not stored or retained on the server. I do not use cookies, tracking technologies, or any other means to collect or information is not stored or retained on the server. I do not use cookies, tracking technologies, or any other
track your usage behavior within the Application. I do not have access to any personal data, including your name, means to collect or track your usage behavior within the Application. I do not have access to any personal data,
email address, or any other personally identifiable information. including your name, email address, or any other personally identifiable information.
</p> </p>
<h2>Use of Information</h2> <h2>Use of Information</h2>
<p> <p>
The Application uses the little information it collects from you to provide the Application's functionality to you. The Application uses the little information it collects from you to provide the Application's functionality to
This information is not used for any other purpose. you. This information is not used for any other purpose.
</p> </p>
<h2>Disclosure of Information</h2> <h2>Disclosure of Information</h2>
<p> <p>
I do not disclose your personal information to any third parties unless required by law or with your explicit 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 consent. I maintain strict confidentiality and take reasonable precautions to protect your personal information
unauthorized access, use, or disclosure. from unauthorized access, use, or disclosure.
</p> </p>
<h2>Security</h2> <h2>Security</h2>
<p> <p>
I prioritize the security of your personal information and take reasonable precautions to protect it. However, 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 please be aware that no method of transmission over the internet or electronic storage is 100% secure, and I
guarantee absolute security. cannot guarantee absolute security.
</p> </p>
<h2>Changes to this Privacy Policy</h2> <h2>Changes to this Privacy Policy</h2>
@ -64,3 +65,4 @@
If you have any questions or concerns about this Privacy Policy or my privacy practices, please 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>. <a asp-page="/Contact/Index">get in touch</a>.
</p> </p>
</main>

View File

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

View File

@ -3,6 +3,7 @@
ViewData["Title"] = "Privacy Policy"; ViewData["Title"] = "Privacy Policy";
} }
<main class="container">
<h1 class="display-4">@ViewData["Title"]</h1> <h1 class="display-4">@ViewData["Title"]</h1>
<p class="lead">Last Updated: 26 May 2023</p> <p class="lead">Last Updated: 26 May 2023</p>
<div class="alert alert-primary"> <div class="alert alert-primary">
@ -17,9 +18,9 @@
<h2>Introduction</h2> <h2>Introduction</h2>
<p> <p>
I am committed to protecting your privacy and ensuring the security of any information you provide to me when using I am committed to protecting your privacy and ensuring the security of any information you provide to me when
my website. This Privacy Policy outlines my practices regarding the collection, use, and disclosure of information I using my website. This Privacy Policy outlines my practices regarding the collection, use, and disclosure of
may gather from users of my website. information I may gather from users of my website.
</p> </p>
<h2>Information I Collect</h2> <h2>Information I Collect</h2>
@ -32,9 +33,9 @@
<p>I do not use any cookies or similar tracking technologies that can identify individual users.</p> <p>I do not use any cookies or similar tracking technologies that can identify individual users.</p>
<p> <p>
Please note that my website includes a Disqus integration for commenting on blog posts. Disqus is a third-party 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. service, and their use of cookies and collection of personal information are governed by their own privacy
I have no control over the information collected by Disqus, and I encourage you to review their privacy policy to policies. I have no control over the information collected by Disqus, and I encourage you to review their
understand how your information may be used by them. privacy policy to understand how your information may be used by them.
</p> </p>
<h2>Use of Information</h2> <h2>Use of Information</h2>
@ -44,28 +45,28 @@
sell, or share it with any third parties unless required by law or with your explicit consent. sell, or share it with any third parties unless required by law or with your explicit consent.
</p> </p>
<p> <p>
I do not use your personal information for marketing purposes or send you any unsolicited communications. Once our I do not use your personal information for marketing purposes or send you any unsolicited communications. Once
conversation is complete and no longer necessary, I will securely delete your personal information unless otherwise our conversation is complete and no longer necessary, I will securely delete your personal information unless
required to retain it by applicable laws or regulations. otherwise required to retain it by applicable laws or regulations.
</p> </p>
<h2>Disclosure of Information</h2> <h2>Disclosure of Information</h2>
<p> <p>
I do not disclose your personal information to any third parties unless required by law or with your explicit 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 consent. I maintain strict confidentiality and take reasonable precautions to protect your personal information
unauthorized access, use, or disclosure. from unauthorized access, use, or disclosure.
</p> </p>
<p> <p>
However, please be aware that my website contains links to third-party websites and services. I am not responsible However, please be aware that my website contains links to third-party websites and services. I am not
for the privacy practices or content of these third-party sites. I encourage you to review the privacy policies of responsible for the privacy practices or content of these third-party sites. I encourage you to review the
those third-party sites before providing any personal information. privacy policies of those third-party sites before providing any personal information.
</p> </p>
<h2>Security</h2> <h2>Security</h2>
<p> <p>
I prioritize the security of your personal information and take reasonable precautions to protect it. However, 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 please be aware that no method of transmission over the internet or electronic storage is 100% secure, and I
guarantee absolute security. cannot guarantee absolute security.
</p> </p>
<h2>Changes to this Privacy Policy</h2> <h2>Changes to this Privacy Policy</h2>
@ -84,3 +85,4 @@
<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,6 +6,7 @@
ViewData["Title"] = "Projects"; ViewData["Title"] = "Projects";
} }
<main class="container">
<h1 class="display-4">Projects</h1> <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))
@ -103,3 +104,4 @@
} }
</div> </div>
} }
</main>

View File

@ -3,13 +3,14 @@
ViewData["Title"] = "Tutorials"; ViewData["Title"] = "Tutorials";
} }
<main class="container">
<h1 class="display-4">Tutorials</h1> <h1 class="display-4">Tutorials</h1>
<p class="lead">Coming Soon</p> <p class="lead">Coming Soon</p>
<p> <p>
Due to Unity's poor corporate decision-making, I'm left in a position where I find it infeasible to write Unity 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 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 still on the table for sure. But tutorials take a lot of time and effort, so unfortunately it may be a while
I can get around to publishing them. before I can get around to publishing them.
</p> </p>
<p> <p>
However, in the meantime, I do have various blog posts that contain some tutorials and guides. You can find them However, in the meantime, I do have various blog posts that contain some tutorials and guides. You can find them
@ -19,3 +20,4 @@
I'm sorry for the inconvenience, but I hope you understand my position. Watch this space! New tutorials will be 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>. 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<BlogContext>();
builder.Services.AddDbContextFactory<WebContext>(); builder.Services.AddDbContextFactory<WebContext>();
builder.Services.AddSingleton<IContactService, ContactService>();
builder.Services.AddSingleton<ITemplateService, TemplateService>(); builder.Services.AddSingleton<ITemplateService, TemplateService>();
builder.Services.AddSingleton<IBlogPostService, BlogPostService>(); builder.Services.AddSingleton<IBlogPostService, BlogPostService>();
builder.Services.AddSingleton<IBlogUserService, BlogUserService>(); 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 { html, body {
background: #121212; background: #121212;
color: #f5f5f5; color: #f5f5f5;
font-family: 'Gabarito', sans-serif;
font-size: 16px; font-size: 16px;
} }
@ -24,6 +23,11 @@ body {
margin-bottom: 60px; margin-bottom: 60px;
} }
main.container {
background: #333;
padding: 20px;
}
a { a {
&:link, &:visited, &:hover, &:active { &:link, &:visited, &:hover, &:active {
text-decoration: none; text-decoration: none;