Compare commits

...

11 Commits

Author SHA1 Message Date
db097acc3c
feat: add Blazor to project 2024-02-27 19:49:42 +00:00
fa87d808dc
fix: fix rare assertion error 2024-02-27 19:00:10 +00:00
71b1ff32c4
feat: add support for maintaining indentation on newline 2024-02-27 16:08:55 +00:00
5b236da2e3
style: add visual feedback for table hovering 2024-02-27 14:53:32 +00:00
8f09197de6
build: support nested directories for js bundling 2024-02-27 14:49:29 +00:00
8925f07f31
Merge branch 'main' into feature/admin 2024-02-27 13:42:09 +00:00
521d202824
build: skip dotnet restore and dotnet build
These steps are executed as part of dotnet publish
2024-02-27 13:39:11 +00:00
b9de8205f6
chore: update dependencies
.NET project:
* AspNetCore.ReCaptcha 1.8.1
* MailKitSimplified.Sender 2.9.0
* Markdig 0.35.0
* Microsoft.AspNetCore.Components.Web 8.0.2
* Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation 8.0.2
* Microsoft.Extensions.FileProviders.Embedded 8.0.2
* Pomelo.EntityFrameworkCore.MySql 8.0.0
* Serilog.AspNetCore 8.0.1
* SmartFormat.NET 3.3.2

npm project:
No front-facing dependencies updated; however some child packages were updated in the process of npm update.
2024-02-27 13:27:28 +00:00
783265e6d0
style: reword tagline to match what it really is 2024-02-27 13:11:05 +00:00
e7e4491002
style: use Titillium Web for index intro 2024-02-27 13:08:36 +00:00
f1f711fa1f
style: reduce in-your-face-ness of headshot photo 2024-02-27 13:01:10 +00:00
15 changed files with 774 additions and 724 deletions

View File

@ -1,4 +1,5 @@
const gulp = require("gulp"); const gulp = require("gulp");
const fs = require("fs");
const sass = require('gulp-sass')(require("sass")); const sass = require('gulp-sass')(require("sass"));
const cleanCSS = require("gulp-clean-css"); const cleanCSS = require("gulp-clean-css");
const rename = require("gulp-rename"); const rename = require("gulp-rename");
@ -26,11 +27,17 @@ function compileTS() {
.pipe(gulp.dest(`tmp/js`)); .pipe(gulp.dest(`tmp/js`));
} }
function bundleJS() { function bundleJS(done) {
return gulp.src(["tmp/js/*.js", "tmp/js/app/app.js", "tmp/js/admin/admin.js"]) const tasks = fs.readdirSync("tmp/js", {withFileTypes: true})
.pipe(named()) .filter(dirent => dirent.isDirectory())
.pipe(webpack({ mode: "production", output: { filename: "[name].min.js" } })) .map(d => bundleDir(d.name));
.pipe(gulp.dest(`${destDir}/js`)); return gulp.parallel(...tasks)(done);
function bundleDir(directory) {
return () => gulp.src(`tmp/js/${directory}/${directory}.js`)
.pipe(webpack({mode: "production", output: {filename: `${directory}.min.js`}}))
.pipe(gulp.dest(`${destDir}/js`));
}
} }
function copyJS() { function copyJS() {

View File

@ -15,11 +15,9 @@ FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build
COPY --from=build-deps /src/OliverBooth/wwwroot /src/OliverBooth/wwwroot COPY --from=build-deps /src/OliverBooth/wwwroot /src/OliverBooth/wwwroot
WORKDIR /src WORKDIR /src
COPY ["OliverBooth/OliverBooth.csproj", "OliverBooth/"] COPY ["OliverBooth/OliverBooth.csproj", "OliverBooth/"]
RUN dotnet restore "OliverBooth/OliverBooth.csproj"
COPY . . COPY . .
WORKDIR "/src/OliverBooth" WORKDIR "/src/OliverBooth"
RUN dotnet build "OliverBooth.csproj" -c Release -o /app/build
FROM build AS publish FROM build AS publish
RUN dotnet publish "OliverBooth.csproj" -c Release -o /app/publish /p:UseAppHost=false RUN dotnet publish "OliverBooth.csproj" -c Release -o /app/publish /p:UseAppHost=false

View File

@ -10,25 +10,25 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Alexinea.Extensions.Configuration.Toml" Version="7.0.0"/> <PackageReference Include="Alexinea.Extensions.Configuration.Toml" Version="7.0.0"/>
<PackageReference Include="Asp.Versioning.Mvc" Version="8.0.0"/> <PackageReference Include="Asp.Versioning.Mvc" Version="8.0.0"/>
<PackageReference Include="AspNetCore.ReCaptcha" Version="1.7.0"/> <PackageReference Include="AspNetCore.ReCaptcha" Version="1.8.1"/>
<PackageReference Include="BCrypt.Net-Core" Version="1.6.0"/> <PackageReference Include="BCrypt.Net-Core" Version="1.6.0"/>
<PackageReference Include="HtmlAgilityPack" Version="1.11.59"/> <PackageReference Include="HtmlAgilityPack" Version="1.11.59"/>
<PackageReference Include="Humanizer.Core" Version="2.14.1"/> <PackageReference Include="Humanizer.Core" Version="2.14.1"/>
<PackageReference Include="MailKit" Version="4.3.0"/> <PackageReference Include="MailKit" Version="4.3.0"/>
<PackageReference Include="MailKitSimplified.Sender" Version="2.8.0"/> <PackageReference Include="MailKitSimplified.Sender" Version="2.9.0"/>
<PackageReference Include="Markdig" Version="0.34.0"/> <PackageReference Include="Markdig" Version="0.35.0"/>
<PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.0"/> <PackageReference Include="Microsoft.AspNetCore.Components.Web" Version="8.0.2"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.0"/> <PackageReference Include="Microsoft.AspNetCore.Mvc.Razor.RuntimeCompilation" Version="8.0.2"/>
<PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.0"/> <PackageReference Include="Microsoft.Extensions.FileProviders.Embedded" Version="8.0.2"/>
<PackageReference Include="NetBarcode" Version="1.7.0"/> <PackageReference Include="NetBarcode" Version="1.7.0"/>
<PackageReference Include="Otp.NET" Version="1.3.0"/> <PackageReference Include="Otp.NET" Version="1.3.0"/>
<PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="7.0.0"/> <PackageReference Include="Pomelo.EntityFrameworkCore.MySql" Version="8.0.0"/>
<PackageReference Include="Serilog" Version="3.1.1"/> <PackageReference Include="Serilog" Version="3.1.1"/>
<PackageReference Include="Serilog.AspNetCore" Version="8.0.0"/> <PackageReference Include="Serilog.AspNetCore" Version="8.0.1"/>
<PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0"/> <PackageReference Include="Serilog.Extensions.Logging" Version="8.0.0"/>
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/> <PackageReference Include="Serilog.Sinks.Console" Version="5.0.1"/>
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/> <PackageReference Include="Serilog.Sinks.File" Version="5.0.0"/>
<PackageReference Include="SmartFormat.NET" Version="3.3.0"/> <PackageReference Include="SmartFormat.NET" Version="3.3.2"/>
<PackageReference Include="X10D" Version="3.3.1"/> <PackageReference Include="X10D" Version="3.3.1"/>
<PackageReference Include="X10D.Hosting" Version="3.3.1"/> <PackageReference Include="X10D.Hosting" Version="3.3.1"/>
<PackageReference Include="ZString" Version="2.5.1"/> <PackageReference Include="ZString" Version="2.5.1"/>

View File

@ -31,7 +31,7 @@
<div class="d-flex flex-row flex-fill"> <div class="d-flex flex-row flex-fill">
<div class="flex-fill mb-0 highlighting-container" style="border-right: 2px dashed #FFFFFF;"> <div class="flex-fill mb-0 highlighting-container" style="border-right: 2px dashed #FFFFFF;">
<textarea id="content" spellcheck="false">@post.Body</textarea> <textarea id="content" class="tab-support" spellcheck="false">@post.Body</textarea>
<pre id="highlighting" aria-hidden="true"><code id="highlighting-content" class="language-markdown">@post.Body</code></pre> <pre id="highlighting" aria-hidden="true"><code id="highlighting-content" class="language-markdown">@post.Body</code></pre>
</div> </div>
<div class="flex-fill mb-0" style="overflow-y: scroll; background: #1E1E1E; max-height: calc(100vh - 35px)"> <div class="flex-fill mb-0" style="overflow-y: scroll; background: #1E1E1E; max-height: calc(100vh - 35px)">

View File

@ -1,12 +1,17 @@
@page @page
<main class="container"> <main class="container">
<h1 class="display-4">Hi, I'm Oliver.</h1> <div class="row align-items-center mb-3">
<p class="lead">I'm a tech enthusiast, coffee drinker, and software developer.</p> <div id="landing-page-intro" class="col-sm-12 col-md-6">
<h1 class="display-4">Hi, I'm Oliver.</h1>
<p class="text-center"> <p class="lead">
<img src="~/img/headshot_512x512_2023.jpg" style="width: 512px; max-width: 100%;"> Coffee enthusiast with a love for all things tech. Tech enthusiast with a love for all things coffee.
</p> </p>
</div>
<div id="landing-page-headshot" class="col-sm-12 col-md-6 justify-content-right">
<img src="~/img/headshot_512x512_2023.jpg" class="rounded-circle" style="width: 50%; max-width: 512px;" alt="Headshot">
</div>
</div>
<p> <p>
My primary focus is C#, though I have dabbled in several other languages such as Java, Kotlin, VB, C/C++, My primary focus is C#, though I have dabbled in several other languages such as Java, Kotlin, VB, C/C++,

View File

@ -1,4 +1,3 @@
@using System.Diagnostics
@using OliverBooth.Data.Blog @using OliverBooth.Data.Blog
@using OliverBooth.Data.Web @using OliverBooth.Data.Web
@using OliverBooth.Services @using OliverBooth.Services
@ -17,7 +16,6 @@
{ {
UserService.TryGetUser(session.UserId, out user); UserService.TryGetUser(session.UserId, out user);
} }
Debug.Assert(user is not null);
} }
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en" data-bs-theme="dark"> <html lang="en" data-bs-theme="dark">
@ -75,6 +73,7 @@
<link rel="stylesheet" href="~/css/ribbon.min.css" asp-append-version="true"> <link rel="stylesheet" href="~/css/ribbon.min.css" asp-append-version="true">
<link rel="stylesheet" href="~/css/admin.min.css" asp-append-version="true"> <link rel="stylesheet" href="~/css/admin.min.css" asp-append-version="true">
@await RenderSectionAsync("Styles", required: false) @await RenderSectionAsync("Styles", required: false)
<base href="~/">
</head> </head>
<body> <body>
<main class="d-flex flex-nowrap"> <main class="d-flex flex-nowrap">
@ -98,7 +97,7 @@
<i class="fa-solid fa-newspaper fa-fw"></i> Blog Posts <i class="fa-solid fa-newspaper fa-fw"></i> Blog Posts
</a> </a>
</li> </li>
@if (user.HasPermission("projects:read")) @if (user?.HasPermission("projects:read") != true)
{ {
<li> <li>
<a asp-page="Projects" class="nav-link @(currentPage == "/Admin/Projects" ? "active" : "text-white")" aria-current="page"> <a asp-page="Projects" class="nav-link @(currentPage == "/Admin/Projects" ? "active" : "text-white")" aria-current="page">
@ -106,7 +105,7 @@
</a> </a>
</li> </li>
} }
@if (user.HasPermission("users:read")) @if (user?.HasPermission("users:read") != true)
{ {
<li> <li>
<a asp-page="Users" class="nav-link @(currentPage == "/Admin/Users" ? "active" : "text-white")" aria-current="page"> <a asp-page="Users" class="nav-link @(currentPage == "/Admin/Users" ? "active" : "text-white")" aria-current="page">
@ -118,8 +117,8 @@
<hr> <hr>
<div class="dropdown"> <div class="dropdown">
<a href="#" class="d-flex align-items-center text-white text-decoration-none dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false"> <a href="#" class="d-flex align-items-center text-white text-decoration-none dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false">
<img src="@user.AvatarUrl" alt="" width="32" height="32" class="rounded-circle me-2"> <img src="@user?.AvatarUrl" alt="" width="32" height="32" class="rounded-circle me-2">
<strong>@user.DisplayName</strong> <strong>@user?.DisplayName</strong>
</a> </a>
<ul class="dropdown-menu dropdown-menu-dark text-small shadow"> <ul class="dropdown-menu dropdown-menu-dark text-small shadow">
<li><a class="dropdown-item" href="#">New project...</a></li> <li><a class="dropdown-item" href="#">New project...</a></li>
@ -146,6 +145,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.8/katex.min.js" integrity="sha512-aoZChv+8imY/U1O7KIHXvO87EOzCuKO0GhFtpD6G2Cyjo/xPeTgdf3/bchB10iB+AojMTDkMHDPLKNxPJVqDcw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.8/katex.min.js" integrity="sha512-aoZChv+8imY/U1O7KIHXvO87EOzCuKO0GhFtpD6G2Cyjo/xPeTgdf3/bchB10iB+AojMTDkMHDPLKNxPJVqDcw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/js/all.min.js" integrity="sha512-uKQ39gEGiyUJl4AI6L+ekBdGKpGw4xJ55+xyJG7YFlJokPNYegn9KwQ3P8A7aFQAUtUsAQHep+d/lrGqrbPIDQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/js/all.min.js" integrity="sha512-uKQ39gEGiyUJl4AI6L+ekBdGKpGw4xJ55+xyJG7YFlJokPNYegn9KwQ3P8A7aFQAUtUsAQHep+d/lrGqrbPIDQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.8/handlebars.min.js" integrity="sha512-E1dSFxg+wsfJ4HKjutk/WaCzK7S2wv1POn1RRPGh8ZK+ag9l244Vqxji3r6wgz9YBf6+vhQEYJZpSjqWFPg9gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.8/handlebars.min.js" integrity="sha512-E1dSFxg+wsfJ4HKjutk/WaCzK7S2wv1POn1RRPGh8ZK+ag9l244Vqxji3r6wgz9YBf6+vhQEYJZpSjqWFPg9gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="~/_framework/blazor.server.js"></script>
<script src="~/js/prism.min.js" asp-append-version="true" data-manual></script> <script src="~/js/prism.min.js" asp-append-version="true" data-manual></script>
<script src="~/js/app.min.js" asp-append-version="true"></script> <script src="~/js/app.min.js" asp-append-version="true"></script>
<script src="~/js/admin.min.js" asp-append-version="true"></script> <script src="~/js/admin.min.js" asp-append-version="true"></script>

View File

@ -60,11 +60,13 @@
<link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100;400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Roboto+Mono:wght@100;400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@200;400;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@200;400;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Gabarito:wght@400;500;600;700;800;900&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Gabarito:wght@400;500;600;700;800;900&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Titillium+Web:ital,wght@0,200;0,300;0,400;0,600;0,700;0,900;1,200;1,300;1,400;1,600;1,700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="~/css/prism.min.css" asp-append-version="true"> <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/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/app.min.css" asp-append-version="true">
<link rel="stylesheet" href="~/css/ribbon.min.css" asp-append-version="true"> <link rel="stylesheet" href="~/css/ribbon.min.css" asp-append-version="true">
@await RenderSectionAsync("Styles", required: false) @await RenderSectionAsync("Styles", required: false)
<base href="~/">
</head> </head>
<body> <body>
<header class="container" style="margin-top: 20px;"> <header class="container" style="margin-top: 20px;">
@ -150,6 +152,7 @@
<script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.8/katex.min.js" integrity="sha512-aoZChv+8imY/U1O7KIHXvO87EOzCuKO0GhFtpD6G2Cyjo/xPeTgdf3/bchB10iB+AojMTDkMHDPLKNxPJVqDcw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/KaTeX/0.16.8/katex.min.js" integrity="sha512-aoZChv+8imY/U1O7KIHXvO87EOzCuKO0GhFtpD6G2Cyjo/xPeTgdf3/bchB10iB+AojMTDkMHDPLKNxPJVqDcw==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/js/all.min.js" integrity="sha512-uKQ39gEGiyUJl4AI6L+ekBdGKpGw4xJ55+xyJG7YFlJokPNYegn9KwQ3P8A7aFQAUtUsAQHep+d/lrGqrbPIDQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.2/js/all.min.js" integrity="sha512-uKQ39gEGiyUJl4AI6L+ekBdGKpGw4xJ55+xyJG7YFlJokPNYegn9KwQ3P8A7aFQAUtUsAQHep+d/lrGqrbPIDQ==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.8/handlebars.min.js" integrity="sha512-E1dSFxg+wsfJ4HKjutk/WaCzK7S2wv1POn1RRPGh8ZK+ag9l244Vqxji3r6wgz9YBf6+vhQEYJZpSjqWFPg9gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.7.8/handlebars.min.js" integrity="sha512-E1dSFxg+wsfJ4HKjutk/WaCzK7S2wv1POn1RRPGh8ZK+ag9l244Vqxji3r6wgz9YBf6+vhQEYJZpSjqWFPg9gg==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="~/_framework/blazor.server.js"></script>
<script src="~/js/prism.min.js" asp-append-version="true" data-manual></script> <script src="~/js/prism.min.js" asp-append-version="true" data-manual></script>
<script src="~/js/app.min.js" asp-append-version="true"></script> <script src="~/js/app.min.js" asp-append-version="true"></script>

View File

@ -0,0 +1,5 @@
@using System.Net.Http
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.JSInterop
@using Microsoft.AspNetCore.Components.Web

View File

@ -53,6 +53,7 @@ builder.Services.AddHostedSingleton<IUserService, UserService>();
builder.Services.AddHostedSingleton<ISessionService, SessionService>(); builder.Services.AddHostedSingleton<ISessionService, SessionService>();
builder.Services.AddRazorPages().AddRazorRuntimeCompilation(); builder.Services.AddRazorPages().AddRazorRuntimeCompilation();
builder.Services.AddControllersWithViews(); builder.Services.AddControllersWithViews();
builder.Services.AddServerSideBlazor().AddInteractiveServerComponents();
builder.Services.AddRouting(options => options.LowercaseUrls = true); builder.Services.AddRouting(options => options.LowercaseUrls = true);
builder.Services.AddReCaptcha(builder.Configuration.GetSection("ReCaptcha")); builder.Services.AddReCaptcha(builder.Configuration.GetSection("ReCaptcha"));
@ -78,5 +79,6 @@ app.UseAuthorization();
app.MapControllers(); app.MapControllers();
app.MapRazorPages(); app.MapRazorPages();
app.MapBlazorHub();
app.Run(); app.Run();

1135
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -127,3 +127,7 @@ textarea {
display: none; display: none;
} }
} }
table.table tr:hover td {
background-color: rgba(#03A9F4, 10%);
}

View File

@ -367,6 +367,7 @@ td.trim-p p:last-child {
background-size: cover; background-size: cover;
border-radius: 10px; border-radius: 10px;
cursor: pointer; cursor: pointer;
* { * {
cursor: pointer; cursor: pointer;
} }
@ -384,6 +385,7 @@ td.trim-p p:last-child {
&:first-child { &:first-child {
border-left: none; border-left: none;
} }
&:last-child { &:last-child {
border-right: none; border-right: none;
} }
@ -416,3 +418,41 @@ td.trim-p p:last-child {
background-color: #6364FF; background-color: #6364FF;
} }
} }
#landing-page-intro {
text-align: left;
h1.display-4 {
font-family: "Titillium Web", sans-serif;
font-weight: 600;
font-style: normal;
}
p.lead {
font-family: "Titillium Web", sans-serif;
font-weight: 400;
font-style: normal;
}
}
#landing-page-headshot {
text-align: right;
img {
transition: border-radius .4s;
&:hover {
border-radius: 5px !important;
}
}
}
@media (max-width: 768px) {
#landing-page-intro {
text-align: center;
}
#landing-page-headshot {
text-align: center;
}
}

62
src/ts/admin/AdminUI.ts Normal file
View File

@ -0,0 +1,62 @@
import adminUI from "./AdminUI";
declare const Prism: any;
class AdminUI {
static highlightingContent: HTMLElement;
static highlighting: HTMLElement;
static _content: HTMLTextAreaElement;
static _saveButton: HTMLButtonElement;
static init() {
const content = AdminUI.content;
AdminUI.highlightingContent = document.getElementById("highlighting-content");
AdminUI.highlighting = document.getElementById("highlighting");
content.addEventListener("input", () => AdminUI.updateEditView());
content.addEventListener("scroll", () => AdminUI.syncEditorScroll());
}
public static get content() {
if (!AdminUI._content) {
AdminUI._content = document.getElementById("content") as HTMLTextAreaElement;
}
return AdminUI._content;
}
public static get saveButton() {
if (!AdminUI._saveButton) {
AdminUI._saveButton = document.getElementById("save-button") as HTMLButtonElement;
}
return AdminUI._saveButton;
}
public static updateEditView() {
AdminUI.highlightingContent.innerHTML = Prism.highlight(AdminUI.content.value, Prism.languages.markdown);
document.querySelectorAll("#highlighting-content span.token.code").forEach(el => {
const languageSpan = el.querySelector(".code-language") as HTMLSpanElement;
if (!languageSpan) {
return;
}
const language = languageSpan.innerText;
const span = el.querySelector(".code-block");
if (!span) {
return;
}
span.outerHTML = `<code class="${span.className} language-${language}" style="padding:0;">${span.innerHTML}</code>`;
Prism.highlightAllUnder(languageSpan.parentElement);
});
AdminUI.syncEditorScroll();
}
static syncEditorScroll() {
AdminUI.highlighting.scrollTop = AdminUI._content.scrollTop;
AdminUI.highlighting.scrollLeft = AdminUI._content.scrollLeft;
}
}
export default AdminUI;

View File

@ -1,8 +1,8 @@
import BlogPost from "../app/BlogPost"; import BlogPost from "../app/BlogPost";
import API from "../app/API"; import API from "../app/API";
import UI from "../app/UI"; import UI from "../app/UI";
import AdminUI from "./AdminUI";
declare const Prism: any; import "./TabSupport"
(() => { (() => {
getCurrentBlogPost().then(post => { getCurrentBlogPost().then(post => {
@ -10,14 +10,12 @@ declare const Prism: any;
return; return;
} }
const saveButton = document.getElementById("save-button") as HTMLButtonElement; AdminUI.init();
const preview = document.getElementById("article-preview") as HTMLAnchorElement;
const content = document.getElementById("content") as HTMLTextAreaElement;
const title = document.getElementById("post-title") as HTMLInputElement;
const highlighting = document.getElementById("highlighting");
const highlightingContent = document.getElementById("highlighting-content");
saveButton.addEventListener("click", async (e: MouseEvent) => { const preview = document.getElementById("article-preview") as HTMLAnchorElement;
const title = document.getElementById("post-title") as HTMLInputElement;
AdminUI.saveButton.addEventListener("click", async (e: MouseEvent) => {
await savePost(); await savePost();
}); });
@ -31,28 +29,15 @@ declare const Prism: any;
} }
}); });
content.addEventListener("keydown", async (e: KeyboardEvent) => {
if (e.key === "Tab") {
e.preventDefault();
const start = content.selectionStart;
const end = content.selectionEnd;
const text = content.value;
content.value = `${text.slice(0, start)} ${text.slice(start, end)}`;
updateEditView();
content.selectionStart = start + 4;
content.selectionEnd = end ? end + 4 : start + 4;
}
});
async function savePost(): Promise<void> { async function savePost(): Promise<void> {
const saveButton = AdminUI.saveButton;
saveButton.classList.add("btn-primary"); saveButton.classList.add("btn-primary");
saveButton.classList.remove("btn-success"); saveButton.classList.remove("btn-success");
saveButton.setAttribute("disabled", "disabled"); saveButton.setAttribute("disabled", "disabled");
saveButton.innerHTML = '<i class="fa-solid fa-spinner fa-spin fa-fw"></i> Saving ...'; saveButton.innerHTML = '<i class="fa-solid fa-spinner fa-spin fa-fw"></i> Saving ...';
post = await API.updatePost(post, {content: content.value, title: title.value}); post = await API.updatePost(post, {content: AdminUI.content.value, title: title.value});
saveButton.classList.add("btn-success"); saveButton.classList.add("btn-success");
saveButton.classList.remove("btn-primary"); saveButton.classList.remove("btn-primary");
@ -66,33 +51,7 @@ declare const Prism: any;
}, 2000); }, 2000);
} }
updateEditView(); AdminUI.updateEditView();
content.addEventListener("input", () => updateEditView());
content.addEventListener("scroll", () => syncEditorScroll());
function updateEditView() {
highlightingContent.innerHTML = Prism.highlight(content.value, Prism.languages.markdown);
document.querySelectorAll("#highlighting-content span.token.code").forEach(el => {
const languageSpan = el.querySelector(".code-language") as HTMLSpanElement;
if (!languageSpan) {
return;
}
const language = languageSpan.innerText;
const span = el.querySelector(".code-block");
if (!span) {
return;
}
span.outerHTML = `<code class="${span.className} language-${language}" style="padding:0;">${span.innerHTML}</code>`;
Prism.highlightAllUnder(highlightingContent);
});
syncEditorScroll();
}
function syncEditorScroll() {
highlighting.scrollTop = content.scrollTop;
highlighting.scrollLeft = content.scrollLeft;
}
}); });
async function getCurrentBlogPost(): Promise<BlogPost> { async function getCurrentBlogPost(): Promise<BlogPost> {

112
src/ts/admin/TabSupport.ts Normal file
View File

@ -0,0 +1,112 @@
import AdminUI from "./AdminUI";
import adminUI from "./AdminUI";
(() => {
const textareas = document.querySelectorAll("textarea.tab-support");
textareas.forEach((textarea: HTMLTextAreaElement) => {
textarea.addEventListener("keydown", (e: KeyboardEvent) => {
let text: string;
// Enter Key?
if (e.key === "Enter") {
const selStart = textarea.selectionStart;
const selEnd = textarea.selectionEnd;
let sel = selStart;
// selection?
if (sel == selEnd) {
// find start of the current line
let text = textarea.value;
while (sel > 0 && text[sel - 1] !== '\n')
sel--;
console.log(`Line starts at index ${sel}`);
const lineStart = sel;
while (text[sel] === ' ' || text[sel] === '\t')
sel++;
console.log(`Identation ends at ${sel} (sel + ${sel - lineStart})`);
if (sel > lineStart) {
const lineEnd = lineStart + text.indexOf('\n', lineStart);
console.log(`Line starts at index ${lineEnd}`);
e.preventDefault();
const indentStr = text.slice(lineStart, sel);
console.log(`Indent string is "${indentStr}"`);
// insert carriage return and indented text
textarea.value = `${text.slice(0, selStart)}\n${indentStr}${text.slice(selStart)}`;
// Scroll caret visible
textarea.blur();
textarea.focus();
textarea.selectionEnd = selEnd + indentStr.length + 1; // +1 for \n
AdminUI.updateEditView();
return false;
}
}
}
// Tab key?
if (e.key === "Tab") {
e.preventDefault();
let selStart = textarea.selectionStart;
// selection?
if (selStart == textarea.selectionEnd) {
// These single character operations are undoable
if (e.shiftKey) {
text = textarea.value;
if (selStart > 0 && (text[selStart - 1] === '\t' || text.slice(selStart - 4, selStart) === " ")) {
document.execCommand("delete");
}
} else {
document.execCommand("insertText", false, " ");
}
} else {
// Block indent/unindent trashes undo stack.
// Select whole lines
let selEnd = textarea.selectionEnd;
text = textarea.value;
while (selStart > 0 && text[selStart - 1] !== '\n')
selStart--;
while (selEnd > 0 && text[selEnd - 1] !== '\n' && selEnd < text.length)
selEnd++;
// Get selected text
let lines = text.slice(selStart, selEnd - selStart).split('\n');
// Insert tabs
for (let i = 0; i < lines.length; i++) {
// Don't indent last line if cursor at start of line
if (i == lines.length - 1 && lines[i].length == 0)
continue;
// Tab or Shift+Tab?
if (e.shiftKey) {
if (lines[i].startsWith('\t'))
lines[i] = lines[i].slice(1);
else if (lines[i].startsWith(" "))
lines[i] = lines[i].slice(4);
} else {
lines[i] = ` ${lines[i]}`;
}
}
const result = lines.join('\n');
// Update the text area
textarea.value = text.slice(0, selStart) + result + text.slice(selEnd);
textarea.selectionStart = selStart;
textarea.selectionEnd = selStart + lines.length;
}
AdminUI.updateEditView();
return false;
}
return true;
});
});
})();