Compare commits
20
Commits
v1.0.0
...
5f6dbcf585
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f6dbcf585 | ||
|
|
eb409d9778 | ||
|
|
d17fd89e40 | ||
|
|
53a33b558a | ||
|
|
64b0a5e62f | ||
|
|
3bf91892f6 | ||
|
|
4fffec4919 | ||
|
|
5c3d3805d1 | ||
|
|
29a7af4950 | ||
|
|
7da0aed466 | ||
|
|
dddd3188ce | ||
|
|
88d70926f4 | ||
|
|
f4aec96ce0 | ||
|
|
58c5f30b0d | ||
|
|
a060469adc | ||
|
|
c154ea86a2 | ||
|
|
37785c48e0 | ||
|
|
c0acb067b3 | ||
|
|
9b4e2677dc | ||
|
|
c7e82a47e9 |
@@ -0,0 +1,63 @@
|
|||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace NoticeBoard.Controllers;
|
||||||
|
|
||||||
|
public class AccountController : Controller
|
||||||
|
{
|
||||||
|
private readonly IConfiguration _config;
|
||||||
|
|
||||||
|
public AccountController(IConfiguration config)
|
||||||
|
{
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public IActionResult Login(string? returnUrl = null)
|
||||||
|
{
|
||||||
|
if (User.Identity?.IsAuthenticated == true)
|
||||||
|
return RedirectToAction("Index", "Admin");
|
||||||
|
|
||||||
|
ViewBag.ReturnUrl = returnUrl;
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost]
|
||||||
|
public async Task<IActionResult> Login(string username, string password, string? returnUrl = null)
|
||||||
|
{
|
||||||
|
var adminUser = _config["Admin:Username"] ?? "admin";
|
||||||
|
var adminPass = _config["Admin:Password"] ?? "admin";
|
||||||
|
|
||||||
|
if (username == adminUser && password == adminPass)
|
||||||
|
{
|
||||||
|
var claims = new List<Claim>
|
||||||
|
{
|
||||||
|
new Claim(ClaimTypes.Name, username),
|
||||||
|
new Claim(ClaimTypes.Role, "Admin")
|
||||||
|
};
|
||||||
|
|
||||||
|
var identity = new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
var principal = new ClaimsPrincipal(identity);
|
||||||
|
|
||||||
|
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, principal);
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(returnUrl) && Url.IsLocalUrl(returnUrl))
|
||||||
|
return Redirect(returnUrl);
|
||||||
|
|
||||||
|
return RedirectToAction("Index", "Admin");
|
||||||
|
}
|
||||||
|
|
||||||
|
ViewBag.Error = "Invalid username or password.";
|
||||||
|
ViewBag.ReturnUrl = returnUrl;
|
||||||
|
return View();
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
public async Task<IActionResult> Logout()
|
||||||
|
{
|
||||||
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
||||||
|
return RedirectToAction("Login");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NoticeBoard.Data;
|
using NoticeBoard.Data;
|
||||||
|
|
||||||
namespace NoticeBoard.Controllers;
|
namespace NoticeBoard.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public class AdminController : Controller
|
public class AdminController : Controller
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ public class DisplayController : Controller
|
|||||||
icsSource = ds.Slide.IcsSource,
|
icsSource = ds.Slide.IcsSource,
|
||||||
backgroundColor = ds.Slide.BackgroundColor,
|
backgroundColor = ds.Slide.BackgroundColor,
|
||||||
backgroundImage = ds.Slide.BackgroundImage,
|
backgroundImage = ds.Slide.BackgroundImage,
|
||||||
|
backgroundSize = ds.Slide.BackgroundSize,
|
||||||
customCss = ds.Slide.CustomCss,
|
customCss = ds.Slide.CustomCss,
|
||||||
duration = ds.DurationSeconds,
|
duration = ds.DurationSeconds,
|
||||||
updatedAt = ds.Slide.UpdatedAt
|
updatedAt = ds.Slide.UpdatedAt
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using NoticeBoard.Data;
|
using NoticeBoard.Data;
|
||||||
@@ -5,6 +6,7 @@ using NoticeBoard.Models;
|
|||||||
|
|
||||||
namespace NoticeBoard.Controllers;
|
namespace NoticeBoard.Controllers;
|
||||||
|
|
||||||
|
[Authorize]
|
||||||
public class SlidesController : Controller
|
public class SlidesController : Controller
|
||||||
{
|
{
|
||||||
private readonly AppDbContext _db;
|
private readonly AppDbContext _db;
|
||||||
@@ -70,6 +72,7 @@ public class SlidesController : Controller
|
|||||||
existing.CustomCss = slide.CustomCss;
|
existing.CustomCss = slide.CustomCss;
|
||||||
existing.BackgroundColor = slide.BackgroundColor;
|
existing.BackgroundColor = slide.BackgroundColor;
|
||||||
existing.BackgroundImage = slide.BackgroundImage;
|
existing.BackgroundImage = slide.BackgroundImage;
|
||||||
|
existing.BackgroundSize = slide.BackgroundSize;
|
||||||
existing.UpdatedAt = DateTime.UtcNow;
|
existing.UpdatedAt = DateTime.UtcNow;
|
||||||
|
|
||||||
await _db.SaveChangesAsync();
|
await _db.SaveChangesAsync();
|
||||||
|
|||||||
@@ -40,6 +40,10 @@ public class Slide
|
|||||||
[MaxLength(500)]
|
[MaxLength(500)]
|
||||||
public string? BackgroundImage { get; set; }
|
public string? BackgroundImage { get; set; }
|
||||||
|
|
||||||
|
/// <summary>Background image sizing: cover, contain, fill, scale-down, auto</summary>
|
||||||
|
[MaxLength(20)]
|
||||||
|
public string BackgroundSize { get; set; } = "cover";
|
||||||
|
|
||||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||||
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
public DateTime UpdatedAt { get; set; } = DateTime.UtcNow;
|
||||||
|
|
||||||
|
|||||||
+19
-4
@@ -1,4 +1,5 @@
|
|||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using NoticeBoard.Data;
|
using NoticeBoard.Data;
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
@@ -11,9 +12,19 @@ builder.Services.AddDbContext<AppDbContext>(options =>
|
|||||||
builder.Services.AddControllersWithViews();
|
builder.Services.AddControllersWithViews();
|
||||||
builder.Services.AddHttpClient();
|
builder.Services.AddHttpClient();
|
||||||
|
|
||||||
|
// Cookie authentication for admin panel
|
||||||
|
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||||
|
.AddCookie(options =>
|
||||||
|
{
|
||||||
|
options.LoginPath = "/account/login";
|
||||||
|
options.LogoutPath = "/account/logout";
|
||||||
|
options.ExpireTimeSpan = TimeSpan.FromHours(12);
|
||||||
|
options.SlidingExpiration = true;
|
||||||
|
});
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
|
||||||
// Auto-create database on startup (use Migrate() if using EF migrations)
|
// Auto-create database on startup
|
||||||
using (var scope = app.Services.CreateScope())
|
using (var scope = app.Services.CreateScope())
|
||||||
{
|
{
|
||||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||||
@@ -28,12 +39,19 @@ if (!app.Environment.IsDevelopment())
|
|||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
|
||||||
// Ensure uploads directory exists
|
// Ensure uploads directory exists
|
||||||
var uploadsPath = Path.Combine(app.Environment.WebRootPath, "uploads");
|
var uploadsPath = Path.Combine(app.Environment.WebRootPath, "uploads");
|
||||||
if (!Directory.Exists(uploadsPath))
|
if (!Directory.Exists(uploadsPath))
|
||||||
Directory.CreateDirectory(uploadsPath);
|
Directory.CreateDirectory(uploadsPath);
|
||||||
|
|
||||||
|
app.MapControllerRoute(
|
||||||
|
name: "account",
|
||||||
|
pattern: "account/{action=Login}",
|
||||||
|
defaults: new { controller = "Account" });
|
||||||
|
|
||||||
app.MapControllerRoute(
|
app.MapControllerRoute(
|
||||||
name: "admin",
|
name: "admin",
|
||||||
pattern: "admin/{action=Index}/{id?}",
|
pattern: "admin/{action=Index}/{id?}",
|
||||||
@@ -54,20 +72,17 @@ app.MapControllerRoute(
|
|||||||
pattern: "api/{action}/{id?}",
|
pattern: "api/{action}/{id?}",
|
||||||
defaults: new { controller = "Api" });
|
defaults: new { controller = "Api" });
|
||||||
|
|
||||||
// Display route: /{slug} — must be last to act as catch-all
|
|
||||||
app.MapControllerRoute(
|
app.MapControllerRoute(
|
||||||
name: "display",
|
name: "display",
|
||||||
pattern: "d/{slug}",
|
pattern: "d/{slug}",
|
||||||
defaults: new { controller = "Display", action = "Show" });
|
defaults: new { controller = "Display", action = "Show" });
|
||||||
|
|
||||||
// Also support root-level slugs
|
|
||||||
app.MapControllerRoute(
|
app.MapControllerRoute(
|
||||||
name: "display-root",
|
name: "display-root",
|
||||||
pattern: "{slug}",
|
pattern: "{slug}",
|
||||||
defaults: new { controller = "Display", action = "Show" },
|
defaults: new { controller = "Display", action = "Show" },
|
||||||
constraints: new { slug = new NoticeBoard.Routing.DeviceSlugConstraint() });
|
constraints: new { slug = new NoticeBoard.Routing.DeviceSlugConstraint() });
|
||||||
|
|
||||||
// Default route goes to admin
|
|
||||||
app.MapControllerRoute(
|
app.MapControllerRoute(
|
||||||
name: "default",
|
name: "default",
|
||||||
pattern: "",
|
pattern: "",
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
@{
|
||||||
|
Layout = null;
|
||||||
|
}
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Sign In — Scratching Post</title>
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet" />
|
||||||
|
<style>
|
||||||
|
body { background: #f4f6f9; min-height: 100vh; display: flex; align-items: center; justify-content: center; font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif; }
|
||||||
|
.login-card { width: 100%; max-width: 400px; }
|
||||||
|
.login-brand { text-align: center; margin-bottom: 2em; }
|
||||||
|
.login-brand i { font-size: 2.5em; color: #d48806; }
|
||||||
|
.login-brand h1 { font-size: 1.6em; font-weight: 700; margin: 0.3em 0 0; color: #1a1a2e; }
|
||||||
|
.login-brand p { color: #6b7280; font-size: 0.85em; }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="login-card">
|
||||||
|
<div class="login-brand">
|
||||||
|
<i class="bi bi-sun"></i>
|
||||||
|
<h1>Sunbeam</h1>
|
||||||
|
<p>Scratching Post Admin</p>
|
||||||
|
</div>
|
||||||
|
<div class="card shadow-sm">
|
||||||
|
<div class="card-body p-4">
|
||||||
|
@if (ViewBag.Error != null)
|
||||||
|
{
|
||||||
|
<div class="alert alert-danger py-2"><i class="bi bi-exclamation-circle me-1"></i>@ViewBag.Error</div>
|
||||||
|
}
|
||||||
|
<form method="post" asp-action="Login">
|
||||||
|
@if (ViewBag.ReturnUrl != null) { <input type="hidden" name="returnUrl" value="@ViewBag.ReturnUrl" /> }
|
||||||
|
<div class="mb-3"><label class="form-label">Username</label><input type="text" name="username" class="form-control" required autofocus /></div>
|
||||||
|
<div class="mb-4"><label class="form-label">Password</label><input type="password" name="password" class="form-control" required /></div>
|
||||||
|
<button type="submit" class="btn btn-primary w-100"><i class="bi bi-box-arrow-in-right me-1"></i>Sign In</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<p class="text-center text-muted mt-3" style="font-size:0.8em;">© Jess Rogerson — 2026</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -25,6 +25,7 @@
|
|||||||
id = ds.Slide.Id, name = ds.Slide.Name, type = ds.Slide.SlideType.ToString().ToLower(),
|
id = ds.Slide.Id, name = ds.Slide.Name, type = ds.Slide.SlideType.ToString().ToLower(),
|
||||||
content = ds.Slide.Content, embedUrl = ds.Slide.EmbedUrl, icsSource = ds.Slide.IcsSource,
|
content = ds.Slide.Content, embedUrl = ds.Slide.EmbedUrl, icsSource = ds.Slide.IcsSource,
|
||||||
backgroundColor = ds.Slide.BackgroundColor, backgroundImage = ds.Slide.BackgroundImage,
|
backgroundColor = ds.Slide.BackgroundColor, backgroundImage = ds.Slide.BackgroundImage,
|
||||||
|
backgroundSize = ds.Slide.BackgroundSize,
|
||||||
customCss = ds.Slide.CustomCss, duration = ds.DurationSeconds
|
customCss = ds.Slide.CustomCss, duration = ds.DurationSeconds
|
||||||
})));
|
})));
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" />
|
||||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet" />
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet" />
|
||||||
<link href="/css/admin.css" rel="stylesheet" />
|
<link href="/css/admin.css" rel="stylesheet" />
|
||||||
|
<script>
|
||||||
|
// Apply saved theme immediately to prevent flash
|
||||||
|
(function() {
|
||||||
|
var t = localStorage.getItem('sb-theme') || 'dark';
|
||||||
|
document.documentElement.setAttribute('data-theme', t);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@RenderSection("Styles", required: false)
|
@RenderSection("Styles", required: false)
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
@@ -42,7 +49,10 @@
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="sidebar-footer">
|
<div class="sidebar-footer">
|
||||||
<small>Sunbeam Framework v1.0</small>
|
<small>Sunbeam v1.0</small>
|
||||||
|
<button id="themeToggle" class="theme-toggle" title="Toggle theme">
|
||||||
|
<i class="bi bi-sun"></i>
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
|
|||||||
+18
-12
@@ -62,6 +62,17 @@
|
|||||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="uploadBackgroundImage()"><i class="bi bi-upload me-1"></i>Upload</button>
|
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="uploadBackgroundImage()"><i class="bi bi-upload me-1"></i>Upload</button>
|
||||||
<input type="file" id="bgImageUpload" class="d-none" accept="image/*" />
|
<input type="file" id="bgImageUpload" class="d-none" accept="image/*" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label asp-for="BackgroundSize" class="form-label">Image Sizing</label>
|
||||||
|
<select asp-for="BackgroundSize" class="form-select">
|
||||||
|
<option value="cover">Cover — fill entire slide, crop if needed</option>
|
||||||
|
<option value="contain">Contain — fit whole image, may show background</option>
|
||||||
|
<option value="fill">Fill — stretch to fit exactly</option>
|
||||||
|
<option value="scale-down">Scale Down — shrink to fit, never enlarge</option>
|
||||||
|
<option value="auto">Auto — original size</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">How the background image fills the slide area.</div>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label asp-for="CustomCss" class="form-label">Custom CSS</label>
|
<label asp-for="CustomCss" class="form-label">Custom CSS</label>
|
||||||
<textarea asp-for="CustomCss" class="form-control font-monospace" rows="4" placeholder="color: white; font-family: Arial;"></textarea>
|
<textarea asp-for="CustomCss" class="form-control font-monospace" rows="4" placeholder="color: white; font-family: Arial;"></textarea>
|
||||||
@@ -81,7 +92,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
|
<script src="/lib/tinymce/tinymce.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('slideType').addEventListener('change', function () {
|
document.getElementById('slideType').addEventListener('change', function () {
|
||||||
document.getElementById('contentSection').style.display = this.value === '0' ? '' : 'none';
|
document.getElementById('contentSection').style.display = this.value === '0' ? '' : 'none';
|
||||||
@@ -94,17 +105,14 @@
|
|||||||
if (tinymce.get('contentEditor')) return;
|
if (tinymce.get('contentEditor')) return;
|
||||||
tinymce.init({
|
tinymce.init({
|
||||||
selector: '#contentEditor', height: 500,
|
selector: '#contentEditor', height: 500,
|
||||||
|
license_key: 'gpl',
|
||||||
menubar: 'file edit view insert format table',
|
menubar: 'file edit view insert format table',
|
||||||
plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount',
|
plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount',
|
||||||
toolbar: 'undo redo | blocks | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image media table | removeformat code fullscreen',
|
toolbar: 'undo redo | blocks | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image media table | removeformat code fullscreen',
|
||||||
images_upload_url: '/api/upload',
|
images_upload_handler: function (blobInfo) {
|
||||||
images_upload_handler: function (blobInfo, progress) {
|
|
||||||
return new Promise(function (resolve, reject) {
|
return new Promise(function (resolve, reject) {
|
||||||
var formData = new FormData();
|
var fd = new FormData(); fd.append('file', blobInfo.blob(), blobInfo.filename());
|
||||||
formData.append('file', blobInfo.blob(), blobInfo.filename());
|
fetch('/api/upload', { method: 'POST', body: fd }).then(r => r.json()).then(d => resolve(d.location)).catch(e => reject(e));
|
||||||
fetch('/api/upload', { method: 'POST', body: formData })
|
|
||||||
.then(r => r.json()).then(data => resolve(data.location))
|
|
||||||
.catch(err => reject('Upload failed: ' + err));
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 16px; color: #fff; background: #1a1a2e; }',
|
content_style: 'body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; font-size: 16px; color: #fff; background: #1a1a2e; }',
|
||||||
@@ -115,13 +123,11 @@
|
|||||||
document.getElementById('bgColorPicker').addEventListener('input', function () { document.getElementById('BackgroundColor').value = this.value; });
|
document.getElementById('bgColorPicker').addEventListener('input', function () { document.getElementById('BackgroundColor').value = this.value; });
|
||||||
function uploadBackgroundImage() { document.getElementById('bgImageUpload').click(); }
|
function uploadBackgroundImage() { document.getElementById('bgImageUpload').click(); }
|
||||||
document.getElementById('bgImageUpload').addEventListener('change', function () {
|
document.getElementById('bgImageUpload').addEventListener('change', function () {
|
||||||
if (!this.files[0]) return;
|
if (!this.files[0]) return; var fd = new FormData(); fd.append('file', this.files[0]);
|
||||||
var fd = new FormData(); fd.append('file', this.files[0]);
|
|
||||||
fetch('/api/upload', { method: 'POST', body: fd }).then(r => r.json()).then(d => { document.getElementById('BackgroundImage').value = d.location; });
|
fetch('/api/upload', { method: 'POST', body: fd }).then(r => r.json()).then(d => { document.getElementById('BackgroundImage').value = d.location; });
|
||||||
});
|
});
|
||||||
document.getElementById('icsFileUpload')?.addEventListener('change', function () {
|
document.getElementById('icsFileUpload')?.addEventListener('change', function () {
|
||||||
if (!this.files[0]) return;
|
if (!this.files[0]) return; var fd = new FormData(); fd.append('file', this.files[0]);
|
||||||
var fd = new FormData(); fd.append('file', this.files[0]);
|
|
||||||
fetch('/api/uploadfile', { method: 'POST', body: fd }).then(r => r.json()).then(d => { document.getElementById('IcsSource').value = d.url; });
|
fetch('/api/uploadfile', { method: 'POST', body: fd }).then(r => r.json()).then(d => { document.getElementById('IcsSource').value = d.url; });
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -62,6 +62,17 @@
|
|||||||
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="uploadBackgroundImage()"><i class="bi bi-upload me-1"></i>Upload</button>
|
<button type="button" class="btn btn-sm btn-outline-secondary mt-1" onclick="uploadBackgroundImage()"><i class="bi bi-upload me-1"></i>Upload</button>
|
||||||
<input type="file" id="bgImageUpload" class="d-none" accept="image/*" />
|
<input type="file" id="bgImageUpload" class="d-none" accept="image/*" />
|
||||||
</div>
|
</div>
|
||||||
|
<div class="mb-3">
|
||||||
|
<label asp-for="BackgroundSize" class="form-label">Image Sizing</label>
|
||||||
|
<select asp-for="BackgroundSize" class="form-select">
|
||||||
|
<option value="cover">Cover — fill entire slide, crop if needed</option>
|
||||||
|
<option value="contain">Contain — fit whole image, may show background</option>
|
||||||
|
<option value="fill">Fill — stretch to fit exactly</option>
|
||||||
|
<option value="scale-down">Scale Down — shrink to fit, never enlarge</option>
|
||||||
|
<option value="auto">Auto — original size</option>
|
||||||
|
</select>
|
||||||
|
<div class="form-text">How the background image fills the slide area.</div>
|
||||||
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label asp-for="CustomCss" class="form-label">Custom CSS</label>
|
<label asp-for="CustomCss" class="form-label">Custom CSS</label>
|
||||||
<textarea asp-for="CustomCss" class="form-control font-monospace" rows="4"></textarea>
|
<textarea asp-for="CustomCss" class="form-control font-monospace" rows="4"></textarea>
|
||||||
@@ -81,7 +92,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@section Scripts {
|
@section Scripts {
|
||||||
<script src="https://cdn.tiny.cloud/1/no-api-key/tinymce/6/tinymce.min.js" referrerpolicy="origin"></script>
|
<script src="/lib/tinymce/tinymce.min.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.getElementById('slideType').addEventListener('change', function () {
|
document.getElementById('slideType').addEventListener('change', function () {
|
||||||
document.getElementById('contentSection').style.display = this.value === '0' ? '' : 'none';
|
document.getElementById('contentSection').style.display = this.value === '0' ? '' : 'none';
|
||||||
@@ -92,7 +103,9 @@
|
|||||||
function initTinyMCE() {
|
function initTinyMCE() {
|
||||||
if (tinymce.get('contentEditor')) return;
|
if (tinymce.get('contentEditor')) return;
|
||||||
tinymce.init({
|
tinymce.init({
|
||||||
selector: '#contentEditor', height: 500, menubar: 'file edit view insert format table',
|
selector: '#contentEditor', height: 500,
|
||||||
|
license_key: 'gpl',
|
||||||
|
menubar: 'file edit view insert format table',
|
||||||
plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount',
|
plugins: 'advlist autolink lists link image charmap preview anchor searchreplace visualblocks code fullscreen insertdatetime media table help wordcount',
|
||||||
toolbar: 'undo redo | blocks | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image media table | removeformat code fullscreen',
|
toolbar: 'undo redo | blocks | bold italic forecolor backcolor | alignleft aligncenter alignright alignjustify | bullist numlist outdent indent | image media table | removeformat code fullscreen',
|
||||||
images_upload_handler: function (blobInfo) {
|
images_upload_handler: function (blobInfo) {
|
||||||
|
|||||||
+56
-14
@@ -1,6 +1,11 @@
|
|||||||
@model Slide
|
@model Slide
|
||||||
@{
|
@{
|
||||||
Layout = null;
|
Layout = null;
|
||||||
|
var bgStyle = $"background: {Model.BackgroundColor ?? "#1a1a2e"};";
|
||||||
|
if (!string.IsNullOrEmpty(Model.BackgroundImage))
|
||||||
|
{
|
||||||
|
bgStyle += $" background-image: url('{Model.BackgroundImage}'); background-size: {Model.BackgroundSize ?? "cover"}; background-position: center; background-repeat: no-repeat;";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
@@ -11,32 +16,69 @@
|
|||||||
<style>
|
<style>
|
||||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
html, body { width: 100%; height: 100%; overflow: hidden; }
|
html, body { width: 100%; height: 100%; overflow: hidden; }
|
||||||
body { background: @(Model.BackgroundColor ?? "#1a1a2e"); color: #fff; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
body {
|
||||||
@if (!string.IsNullOrEmpty(Model.BackgroundImage)) { @:background-image: url('@Model.BackgroundImage'); @:background-size: cover; @:background-position: center; }
|
@Html.Raw(bgStyle)
|
||||||
|
color: #fff;
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
||||||
|
}
|
||||||
|
.slide-content {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
@Html.Raw(Model.CustomCss ?? "")
|
||||||
}
|
}
|
||||||
.slide-content { width: 100%; height: 100%; @Html.Raw(Model.CustomCss ?? "") }
|
|
||||||
.slide-content img { max-width: 100%; height: auto; }
|
.slide-content img { max-width: 100%; height: auto; }
|
||||||
.slide-content table { border-collapse: collapse; }
|
.slide-content table { border-collapse: collapse; }
|
||||||
.slide-content table td, .slide-content table th { border: 1px solid rgba(255,255,255,0.3); padding: 8px 12px; }
|
.slide-content table td, .slide-content table th { border: 1px solid rgba(255,255,255,0.3); padding: 8px 12px; }
|
||||||
iframe.embed-frame { width: 100%; height: 100%; border: none; }
|
iframe.embed-frame { width: 100%; height: 100%; border: none; }
|
||||||
.ics-events { padding: 2em; } .ics-events h2 { margin-bottom: 1em; font-size: 2em; }
|
.ics-events { padding: 2em; }
|
||||||
|
.ics-events h2 { margin-bottom: 1em; font-size: 2em; }
|
||||||
.event-card { background: rgba(255,255,255,0.1); border-radius: 12px; padding: 1.2em; margin-bottom: 1em; }
|
.event-card { background: rgba(255,255,255,0.1); border-radius: 12px; padding: 1.2em; margin-bottom: 1em; }
|
||||||
|
.event-card h3 { font-size: 1.3em; margin-bottom: 0.3em; }
|
||||||
|
.event-meta { opacity: 0.7; font-size: 0.9em; }
|
||||||
.back-link { position: fixed; top: 10px; right: 10px; z-index: 999; }
|
.back-link { position: fixed; top: 10px; right: 10px; z-index: 999; }
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<a href="/admin/slides" class="back-link btn btn-sm btn-light"><i class="bi bi-x-lg"></i> Close</a>
|
<a href="/admin/slides" class="back-link btn btn-sm btn-light"><i class="bi bi-x-lg"></i> Close</a>
|
||||||
@if (Model.SlideType == SlideType.Content) { <div class="slide-content">@Html.Raw(Model.Content ?? "")</div> }
|
|
||||||
else if (Model.SlideType == SlideType.Embed) { <iframe class="embed-frame" src="@Model.EmbedUrl"></iframe> }
|
@if (Model.SlideType == SlideType.Content)
|
||||||
else if (Model.SlideType == SlideType.IcsCalendar) {
|
{
|
||||||
<div class="ics-events" id="icsContainer"><h2>Upcoming Events</h2><p>Loading...</p></div>
|
<div class="slide-content">@Html.Raw(Model.Content ?? "")</div>
|
||||||
|
}
|
||||||
|
else if (Model.SlideType == SlideType.Embed)
|
||||||
|
{
|
||||||
|
<iframe class="embed-frame" src="@Model.EmbedUrl"></iframe>
|
||||||
|
}
|
||||||
|
else if (Model.SlideType == SlideType.IcsCalendar)
|
||||||
|
{
|
||||||
|
<div class="ics-events" id="icsContainer">
|
||||||
|
<h2><i class="bi bi-calendar-event me-2"></i>Upcoming Events</h2>
|
||||||
|
<p>Loading calendar...</p>
|
||||||
|
</div>
|
||||||
<script>
|
<script>
|
||||||
fetch('/api/parseics?url=' + encodeURIComponent('@Model.IcsSource')).then(r => r.json()).then(data => {
|
fetch('/api/parseics?url=' + encodeURIComponent('@Model.IcsSource'))
|
||||||
var html = '<h2>Upcoming Events</h2>';
|
.then(r => r.json())
|
||||||
if (data.events && data.events.length > 0) { data.events.forEach(function(e) { html += '<div class="event-card"><h3>' + (e.summary||'Event') + '</h3><div style="opacity:0.7;">' + (e.start||'') + '</div></div>'; }); }
|
.then(data => {
|
||||||
else { html += '<p>No upcoming events.</p>'; }
|
var html = '<h2><i class="bi bi-calendar-event" style="margin-right:0.5em;"></i>Upcoming Events</h2>';
|
||||||
document.getElementById('icsContainer').innerHTML = html;
|
if (data.events && data.events.length > 0) {
|
||||||
});
|
data.events.forEach(function(e) {
|
||||||
|
html += '<div class="event-card"><h3>' + (e.summary || 'Event') + '</h3>';
|
||||||
|
html += '<div class="event-meta">';
|
||||||
|
if (e.start) html += '<i class="bi bi-clock"></i> ' + e.start;
|
||||||
|
if (e.end) html += ' — ' + e.end;
|
||||||
|
if (e.location) html += ' | <i class="bi bi-geo-alt"></i> ' + e.location;
|
||||||
|
html += '</div>';
|
||||||
|
if (e.description) html += '<p style="margin-top:0.5em;opacity:0.8;">' + e.description + '</p>';
|
||||||
|
html += '</div>';
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
html += '<p>No upcoming events found.</p>';
|
||||||
|
}
|
||||||
|
document.getElementById('icsContainer').innerHTML = html;
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
document.getElementById('icsContainer').innerHTML = '<h2>Calendar</h2><p>Error loading calendar: ' + err + '</p>';
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
}
|
}
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
+5
-1
@@ -6,5 +6,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"MaxUploadSizeMB": 20
|
"MaxUploadSizeMB": 20,
|
||||||
|
"Admin": {
|
||||||
|
"Username": "admin",
|
||||||
|
"Password": "ScratchingPost2026!"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+104
-27
@@ -1,67 +1,144 @@
|
|||||||
/* === Scratching Post Admin — Sunbeam Theme === */
|
/* === Scratching Post Admin — Sunbeam Theme === */
|
||||||
:root {
|
/* Dark mode (default) */
|
||||||
--sb-bg: #0f0f1a; --sb-surface: #1a1a2e; --sb-surface2: #232340;
|
:root, [data-theme="dark"] {
|
||||||
--sb-border: rgba(255, 255, 255, 0.08); --sb-text: #e0e0e0; --sb-text-muted: #8888aa;
|
--sb-bg: #0f0f1a;
|
||||||
--sb-accent: #f0a030; --sb-accent2: #ff6b6b; --sb-primary: #5b8def; --sidebar-width: 240px;
|
--sb-surface: #1a1a2e;
|
||||||
|
--sb-surface2: #232340;
|
||||||
|
--sb-border: rgba(255, 255, 255, 0.08);
|
||||||
|
--sb-text: #e0e0e0;
|
||||||
|
--sb-text-muted: #8888aa;
|
||||||
|
--sb-accent: #f0a030;
|
||||||
|
--sb-accent2: #ff6b6b;
|
||||||
|
--sb-primary: #5b8def;
|
||||||
|
--sb-btn-text: #fff;
|
||||||
|
--sb-input-bg: #0f0f1a;
|
||||||
|
--sb-card-header-bg: rgba(255, 255, 255, 0.02);
|
||||||
|
--sb-hover-bg: rgba(255, 255, 255, 0.04);
|
||||||
|
--sb-active-bg: rgba(240, 160, 48, 0.08);
|
||||||
|
--sidebar-width: 240px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Light mode */
|
||||||
|
[data-theme="light"] {
|
||||||
|
--sb-bg: #f4f6f9;
|
||||||
|
--sb-surface: #ffffff;
|
||||||
|
--sb-surface2: #e9ecf0;
|
||||||
|
--sb-border: rgba(0, 0, 0, 0.1);
|
||||||
|
--sb-text: #1a1a2e;
|
||||||
|
--sb-text-muted: #6b7280;
|
||||||
|
--sb-accent: #d48806;
|
||||||
|
--sb-accent2: #e04040;
|
||||||
|
--sb-primary: #3b6fd4;
|
||||||
|
--sb-btn-text: #fff;
|
||||||
|
--sb-input-bg: #ffffff;
|
||||||
|
--sb-card-header-bg: rgba(0, 0, 0, 0.02);
|
||||||
|
--sb-hover-bg: rgba(0, 0, 0, 0.03);
|
||||||
|
--sb-active-bg: rgba(212, 136, 6, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body { margin: 0; font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif; background: var(--sb-bg); color: var(--sb-text); min-height: 100vh; }
|
body { margin: 0; font-family: 'Segoe UI', -apple-system, BlinkMacSystemFont, Roboto, sans-serif; background: var(--sb-bg); color: var(--sb-text); min-height: 100vh; transition: background 0.3s, color 0.3s; }
|
||||||
|
|
||||||
|
/* === Layout === */
|
||||||
.admin-wrapper { display: flex; min-height: 100vh; }
|
.admin-wrapper { display: flex; min-height: 100vh; }
|
||||||
.admin-sidebar { width: var(--sidebar-width); background: var(--sb-surface); border-right: 1px solid var(--sb-border); display: flex; flex-direction: column; position: fixed; top: 0; left: 0; bottom: 0; z-index: 100; }
|
|
||||||
|
/* === Sidebar === */
|
||||||
|
.admin-sidebar { width: var(--sidebar-width); background: var(--sb-surface); border-right: 1px solid var(--sb-border); display: flex; flex-direction: column; position: fixed; top: 0; left: 0; bottom: 0; z-index: 100; transition: background 0.3s; }
|
||||||
.sidebar-brand { padding: 1.5em 1.2em 0.3em; font-size: 1.4em; font-weight: 700; color: var(--sb-accent); display: flex; align-items: center; gap: 0.5em; }
|
.sidebar-brand { padding: 1.5em 1.2em 0.3em; font-size: 1.4em; font-weight: 700; color: var(--sb-accent); display: flex; align-items: center; gap: 0.5em; }
|
||||||
.sidebar-brand i { font-size: 1.2em; }
|
.sidebar-brand i { font-size: 1.2em; }
|
||||||
.sidebar-subtitle { padding: 0 1.2em 1.2em; font-size: 0.75em; color: var(--sb-text-muted); text-transform: uppercase; letter-spacing: 0.1em; }
|
.sidebar-subtitle { padding: 0 1.2em 1.2em; font-size: 0.75em; color: var(--sb-text-muted); text-transform: uppercase; letter-spacing: 0.1em; }
|
||||||
.sidebar-nav { list-style: none; padding: 0; margin: 0; flex: 1; }
|
.sidebar-nav { list-style: none; padding: 0; margin: 0; flex: 1; }
|
||||||
.sidebar-nav li a { display: flex; align-items: center; gap: 0.75em; padding: 0.8em 1.2em; color: var(--sb-text-muted); text-decoration: none; border-left: 3px solid transparent; transition: all 0.15s ease; font-size: 0.95em; }
|
.sidebar-nav li a { display: flex; align-items: center; gap: 0.75em; padding: 0.8em 1.2em; color: var(--sb-text-muted); text-decoration: none; border-left: 3px solid transparent; transition: all 0.15s ease; font-size: 0.95em; }
|
||||||
.sidebar-nav li a:hover { background: rgba(255, 255, 255, 0.04); color: var(--sb-text); }
|
.sidebar-nav li a:hover { background: var(--sb-hover-bg); color: var(--sb-text); }
|
||||||
.sidebar-nav li a.active { background: rgba(240, 160, 48, 0.08); color: var(--sb-accent); border-left-color: var(--sb-accent); }
|
.sidebar-nav li a.active { background: var(--sb-active-bg); color: var(--sb-accent); border-left-color: var(--sb-accent); }
|
||||||
.sidebar-nav li a i { font-size: 1.1em; width: 1.2em; text-align: center; }
|
.sidebar-nav li a i { font-size: 1.1em; width: 1.2em; text-align: center; }
|
||||||
.nav-hint { margin-left: auto; font-size: 0.75em; opacity: 0.5; }
|
.nav-hint { margin-left: auto; font-size: 0.75em; opacity: 0.5; }
|
||||||
.sidebar-footer { padding: 1em 1.2em; border-top: 1px solid var(--sb-border); color: var(--sb-text-muted); font-size: 0.75em; }
|
.sidebar-footer { padding: 1em 1.2em; border-top: 1px solid var(--sb-border); color: var(--sb-text-muted); font-size: 0.75em; display: flex; align-items: center; justify-content: space-between; }
|
||||||
|
|
||||||
|
/* === Theme Toggle === */
|
||||||
|
.theme-toggle { background: none; border: 1px solid var(--sb-border); color: var(--sb-text-muted); border-radius: 6px; padding: 0.3em 0.5em; cursor: pointer; font-size: 1em; transition: all 0.2s; display: flex; align-items: center; }
|
||||||
|
.theme-toggle:hover { color: var(--sb-accent); border-color: var(--sb-accent); }
|
||||||
|
|
||||||
|
/* === Main Content === */
|
||||||
.admin-main { margin-left: var(--sidebar-width); flex: 1; display: flex; flex-direction: column; min-height: 100vh; }
|
.admin-main { margin-left: var(--sidebar-width); flex: 1; display: flex; flex-direction: column; min-height: 100vh; }
|
||||||
.admin-header { padding: 1.5em 2em 1em; border-bottom: 1px solid var(--sb-border); display: flex; justify-content: space-between; align-items: center; }
|
.admin-header { padding: 1.5em 2em 1em; border-bottom: 1px solid var(--sb-border); display: flex; justify-content: space-between; align-items: center; }
|
||||||
.admin-header h1 { font-size: 1.5em; font-weight: 600; margin: 0; }
|
.admin-header h1 { font-size: 1.5em; font-weight: 600; margin: 0; }
|
||||||
.admin-content { padding: 1.5em 2em 3em; flex: 1; }
|
.admin-content { padding: 1.5em 2em 3em; flex: 1; }
|
||||||
.card { background: var(--sb-surface); border: 1px solid var(--sb-border); border-radius: 10px; color: var(--sb-text); }
|
|
||||||
.card-header { background: rgba(255, 255, 255, 0.02); border-bottom: 1px solid var(--sb-border); padding: 0.9em 1.2em; }
|
/* === Cards === */
|
||||||
|
.card { background: var(--sb-surface); border: 1px solid var(--sb-border); border-radius: 10px; color: var(--sb-text); transition: background 0.3s; }
|
||||||
|
.card-header { background: var(--sb-card-header-bg); border-bottom: 1px solid var(--sb-border); padding: 0.9em 1.2em; }
|
||||||
.card-body { padding: 1.2em; }
|
.card-body { padding: 1.2em; }
|
||||||
.card-footer { background: rgba(255, 255, 255, 0.02); border-top: 1px solid var(--sb-border); padding: 0.8em 1.2em; }
|
.card-footer { background: var(--sb-card-header-bg); border-top: 1px solid var(--sb-border); padding: 0.8em 1.2em; }
|
||||||
.stat-card { background: var(--sb-surface); border: 1px solid var(--sb-border); border-radius: 12px; padding: 1.5em; display: flex; align-items: center; gap: 1.2em; transition: transform 0.2s; }
|
|
||||||
|
/* === Stat Cards === */
|
||||||
|
.stat-card { background: var(--sb-surface); border: 1px solid var(--sb-border); border-radius: 12px; padding: 1.5em; display: flex; align-items: center; gap: 1.2em; transition: transform 0.2s, background 0.3s; }
|
||||||
.stat-card:hover { transform: translateY(-2px); }
|
.stat-card:hover { transform: translateY(-2px); }
|
||||||
.stat-card.accent { border-color: var(--sb-accent); background: rgba(240, 160, 48, 0.06); }
|
.stat-card.accent { border-color: var(--sb-accent); background: var(--sb-active-bg); }
|
||||||
.stat-icon { font-size: 2em; color: var(--sb-accent); width: 1.5em; text-align: center; }
|
.stat-icon { font-size: 2em; color: var(--sb-accent); width: 1.5em; text-align: center; }
|
||||||
.stat-value { font-size: 1.8em; font-weight: 700; line-height: 1; }
|
.stat-value { font-size: 1.8em; font-weight: 700; line-height: 1; }
|
||||||
.stat-label { color: var(--sb-text-muted); font-size: 0.85em; margin-top: 0.2em; }
|
.stat-label { color: var(--sb-text-muted); font-size: 0.85em; margin-top: 0.2em; }
|
||||||
.table { color: var(--sb-text); --bs-table-bg: transparent; --bs-table-hover-bg: rgba(255, 255, 255, 0.03); }
|
|
||||||
|
/* === Tables === */
|
||||||
|
.table { color: var(--sb-text); --bs-table-bg: transparent; --bs-table-hover-bg: var(--sb-hover-bg); }
|
||||||
.table thead th { border-bottom-color: var(--sb-border); color: var(--sb-text-muted); font-weight: 500; font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.05em; }
|
.table thead th { border-bottom-color: var(--sb-border); color: var(--sb-text-muted); font-weight: 500; font-size: 0.85em; text-transform: uppercase; letter-spacing: 0.05em; }
|
||||||
.table td { border-color: var(--sb-border); }
|
.table td { border-color: var(--sb-border); }
|
||||||
.form-control, .form-select { background: var(--sb-bg); border-color: var(--sb-border); color: var(--sb-text); }
|
|
||||||
.form-control:focus, .form-select:focus { background: var(--sb-bg); border-color: var(--sb-accent); color: var(--sb-text); box-shadow: 0 0 0 0.2rem rgba(240, 160, 48, 0.15); }
|
/* === Forms === */
|
||||||
|
.form-control, .form-select { background: var(--sb-input-bg); border-color: var(--sb-border); color: var(--sb-text); }
|
||||||
|
.form-control:focus, .form-select:focus { background: var(--sb-input-bg); border-color: var(--sb-accent); color: var(--sb-text); box-shadow: 0 0 0 0.2rem rgba(240, 160, 48, 0.15); }
|
||||||
.form-label { font-weight: 500; font-size: 0.9em; }
|
.form-label { font-weight: 500; font-size: 0.9em; }
|
||||||
.form-text { color: var(--sb-text-muted); }
|
.form-text { color: var(--sb-text-muted); }
|
||||||
.input-group-text { background: var(--sb-surface2); border-color: var(--sb-border); color: var(--sb-text-muted); }
|
.input-group-text { background: var(--sb-surface2); border-color: var(--sb-border); color: var(--sb-text-muted); }
|
||||||
|
.form-check-input { background-color: var(--sb-input-bg); border-color: var(--sb-border); }
|
||||||
.form-check-input:checked { background-color: var(--sb-accent); border-color: var(--sb-accent); }
|
.form-check-input:checked { background-color: var(--sb-accent); border-color: var(--sb-accent); }
|
||||||
.btn-primary { background: var(--sb-primary); border-color: var(--sb-primary); }
|
|
||||||
.btn-primary:hover { background: #4a7ddf; border-color: #4a7ddf; }
|
/* === Buttons === */
|
||||||
|
.btn-primary { background: var(--sb-primary); border-color: var(--sb-primary); color: var(--sb-btn-text); }
|
||||||
|
.btn-primary:hover { filter: brightness(0.9); }
|
||||||
.btn-outline-secondary { color: var(--sb-text-muted); border-color: var(--sb-border); }
|
.btn-outline-secondary { color: var(--sb-text-muted); border-color: var(--sb-border); }
|
||||||
.btn-outline-secondary:hover { background: rgba(255, 255, 255, 0.05); color: var(--sb-text); border-color: var(--sb-text-muted); }
|
.btn-outline-secondary:hover { background: var(--sb-hover-bg); color: var(--sb-text); border-color: var(--sb-text-muted); }
|
||||||
|
|
||||||
|
/* === List Groups === */
|
||||||
.list-group-item { background: transparent; border-color: var(--sb-border); color: var(--sb-text); }
|
.list-group-item { background: transparent; border-color: var(--sb-border); color: var(--sb-text); }
|
||||||
.list-group-item:hover { background: rgba(255, 255, 255, 0.02); }
|
.list-group-item:hover { background: var(--sb-hover-bg); }
|
||||||
.alert-success { background: rgba(40, 167, 69, 0.15); border-color: rgba(40, 167, 69, 0.3); color: #6bdb8a; }
|
|
||||||
.alert-danger { background: rgba(220, 53, 69, 0.15); border-color: rgba(220, 53, 69, 0.3); color: #f08090; }
|
/* === Alerts === */
|
||||||
.alert-warning { background: rgba(255, 193, 7, 0.15); border-color: rgba(255, 193, 7, 0.3); color: #ffd54f; }
|
.alert-success { background: rgba(40, 167, 69, 0.12); border-color: rgba(40, 167, 69, 0.25); color: #28a745; }
|
||||||
.badge { font-weight: 500; } .badge-sm { font-size: 0.7em; padding: 0.2em 0.5em; }
|
.alert-danger { background: rgba(220, 53, 69, 0.12); border-color: rgba(220, 53, 69, 0.25); color: #dc3545; }
|
||||||
|
.alert-warning { background: rgba(255, 193, 7, 0.12); border-color: rgba(255, 193, 7, 0.25); color: #d48806; }
|
||||||
|
[data-theme="dark"] .alert-success { color: #6bdb8a; }
|
||||||
|
[data-theme="dark"] .alert-danger { color: #f08090; }
|
||||||
|
[data-theme="dark"] .alert-warning { color: #ffd54f; }
|
||||||
|
|
||||||
|
/* === Badges === */
|
||||||
|
.badge { font-weight: 500; }
|
||||||
|
.badge-sm { font-size: 0.7em; padding: 0.2em 0.5em; }
|
||||||
|
|
||||||
|
/* === Playlist Items === */
|
||||||
.playlist-item { transition: background 0.15s; }
|
.playlist-item { transition: background 0.15s; }
|
||||||
.playlist-item.sortable-ghost { background: rgba(240, 160, 48, 0.1); opacity: 0.6; }
|
.playlist-item.sortable-ghost { background: var(--sb-active-bg); opacity: 0.6; }
|
||||||
.drag-handle { cursor: grab; color: var(--sb-text-muted); font-size: 1.2em; padding: 0.2em; }
|
.drag-handle { cursor: grab; color: var(--sb-text-muted); font-size: 1.2em; padding: 0.2em; }
|
||||||
.drag-handle:hover { color: var(--sb-accent); }
|
.drag-handle:hover { color: var(--sb-accent); }
|
||||||
.duration-input input { text-align: center; }
|
.duration-input input { text-align: center; }
|
||||||
|
|
||||||
|
/* === Device Cards === */
|
||||||
.device-card { transition: transform 0.2s, border-color 0.2s; }
|
.device-card { transition: transform 0.2s, border-color 0.2s; }
|
||||||
.device-card:hover { transform: translateY(-2px); border-color: var(--sb-accent); }
|
.device-card:hover { transform: translateY(-2px); border-color: var(--sb-accent); }
|
||||||
|
|
||||||
|
/* === Empty State === */
|
||||||
.empty-state { text-align: center; padding: 4em 2em; color: var(--sb-text-muted); }
|
.empty-state { text-align: center; padding: 4em 2em; color: var(--sb-text-muted); }
|
||||||
|
|
||||||
|
/* === Code in light mode === */
|
||||||
|
[data-theme="light"] code { background: rgba(0,0,0,0.06); padding: 0.15em 0.4em; border-radius: 3px; font-size: 0.9em; }
|
||||||
|
|
||||||
|
/* === Modal overrides === */
|
||||||
|
.modal-content { background: var(--sb-surface); color: var(--sb-text); border-color: var(--sb-border); }
|
||||||
|
|
||||||
|
/* === Responsive === */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
.admin-sidebar { width: 60px; overflow: hidden; }
|
.admin-sidebar { width: 60px; overflow: hidden; }
|
||||||
.admin-sidebar span, .admin-sidebar .sidebar-subtitle, .admin-sidebar .sidebar-footer, .admin-sidebar .nav-hint { display: none; }
|
.admin-sidebar span, .admin-sidebar .sidebar-subtitle, .admin-sidebar .sidebar-footer small, .admin-sidebar .nav-hint { display: none; }
|
||||||
.sidebar-brand { justify-content: center; padding: 1em 0.5em; }
|
.sidebar-brand { justify-content: center; padding: 1em 0.5em; }
|
||||||
.sidebar-nav li a { justify-content: center; padding: 0.8em; }
|
.sidebar-nav li a { justify-content: center; padding: 0.8em; }
|
||||||
.admin-main { margin-left: 60px; }
|
.admin-main { margin-left: 60px; }
|
||||||
|
|||||||
+41
-2
@@ -1,7 +1,46 @@
|
|||||||
// === Scratching Post Admin JS ===
|
// === Scratching Post Admin JS ===
|
||||||
|
|
||||||
|
// === Theme Toggle ===
|
||||||
|
(function () {
|
||||||
|
var saved = localStorage.getItem('sb-theme') || 'dark';
|
||||||
|
document.documentElement.setAttribute('data-theme', saved);
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
updateToggleIcon(saved);
|
||||||
|
|
||||||
|
var btn = document.getElementById('themeToggle');
|
||||||
|
if (btn) {
|
||||||
|
btn.addEventListener('click', function () {
|
||||||
|
var current = document.documentElement.getAttribute('data-theme') || 'dark';
|
||||||
|
var next = current === 'dark' ? 'light' : 'dark';
|
||||||
|
document.documentElement.setAttribute('data-theme', next);
|
||||||
|
localStorage.setItem('sb-theme', next);
|
||||||
|
updateToggleIcon(next);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateToggleIcon(theme) {
|
||||||
|
var btn = document.getElementById('themeToggle');
|
||||||
|
if (!btn) return;
|
||||||
|
btn.innerHTML = theme === 'dark'
|
||||||
|
? '<i class="bi bi-sun"></i>'
|
||||||
|
: '<i class="bi bi-moon-stars"></i>';
|
||||||
|
btn.title = theme === 'dark' ? 'Switch to Light Mode' : 'Switch to Dark Mode';
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
// Auto-dismiss alerts after 5 seconds
|
||||||
document.querySelectorAll('.alert-dismissible').forEach(function (alert) {
|
document.querySelectorAll('.alert-dismissible').forEach(function (alert) {
|
||||||
setTimeout(function () { var bsAlert = bootstrap.Alert.getOrCreateInstance(alert); bsAlert.close(); }, 5000);
|
setTimeout(function () {
|
||||||
|
var bsAlert = bootstrap.Alert.getOrCreateInstance(alert);
|
||||||
|
bsAlert.close();
|
||||||
|
}, 5000);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Confirm dangerous actions
|
||||||
document.querySelectorAll('[data-confirm]').forEach(function (el) {
|
document.querySelectorAll('[data-confirm]').forEach(function (el) {
|
||||||
el.addEventListener('click', function (e) { if (!confirm(this.dataset.confirm)) e.preventDefault(); });
|
el.addEventListener('click', function (e) {
|
||||||
|
if (!confirm(this.dataset.confirm)) e.preventDefault();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
+19
-7
@@ -19,19 +19,31 @@
|
|||||||
layerA.classList.add('active'); return;
|
layerA.classList.add('active'); return;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSlide(slide, layer) {
|
function buildBgStyle(slide) {
|
||||||
var bg = slide.backgroundColor || '#0a0a14';
|
var bg = slide.backgroundColor || '#0a0a14';
|
||||||
var bgImg = slide.backgroundImage ? 'url(' + slide.backgroundImage + ')' : 'none';
|
var bgSize = slide.backgroundSize || 'cover';
|
||||||
var css = slide.customCss || '';
|
var style = 'background-color:' + bg + ';';
|
||||||
|
if (slide.backgroundImage) {
|
||||||
|
style += 'background-image:url(' + slide.backgroundImage + ');';
|
||||||
|
style += 'background-size:' + bgSize + ';';
|
||||||
|
style += 'background-position:center;';
|
||||||
|
style += 'background-repeat:no-repeat;';
|
||||||
|
}
|
||||||
|
if (slide.customCss) style += slide.customCss;
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSlide(slide, layer) {
|
||||||
|
var style = buildBgStyle(slide);
|
||||||
if (slide.type === 'content') {
|
if (slide.type === 'content') {
|
||||||
layer.innerHTML = '<div class="slide-inner" style="background-color:' + bg + ';background-image:' + bgImg + ';' + css + '"><div class="content-wrap">' + (slide.content || '') + '</div></div>';
|
layer.innerHTML = '<div class="slide-inner" style="' + style + '"><div class="content-wrap">' + (slide.content || '') + '</div></div>';
|
||||||
} else if (slide.type === 'embed') {
|
} else if (slide.type === 'embed') {
|
||||||
layer.innerHTML = '<div class="slide-inner" style="background-color:' + bg + ';' + css + '"><iframe class="embed-frame" src="' + escapeHtml(slide.embedUrl || '') + '" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe></div>';
|
layer.innerHTML = '<div class="slide-inner" style="' + style + '"><iframe class="embed-frame" src="' + escapeHtml(slide.embedUrl || '') + '" sandbox="allow-scripts allow-same-origin allow-popups" loading="lazy"></iframe></div>';
|
||||||
} else if (slide.type === 'icscalendar') {
|
} else if (slide.type === 'icscalendar') {
|
||||||
layer.innerHTML = '<div class="slide-inner" style="background-color:' + bg + ';' + css + '"><div class="ics-display"><h2>Upcoming Events</h2><div class="events-grid"><div class="loading-state">Loading calendar</div></div></div></div>';
|
layer.innerHTML = '<div class="slide-inner" style="' + style + '"><div class="ics-display"><h2>Upcoming Events</h2><div class="events-grid"><div class="loading-state">Loading calendar</div></div></div></div>';
|
||||||
loadIcsEvents(slide.icsSource, layer);
|
loadIcsEvents(slide.icsSource, layer);
|
||||||
} else {
|
} else {
|
||||||
layer.innerHTML = '<div class="slide-inner" style="background-color:' + bg + ';"><div class="content-wrap">' + (slide.content || '') + '</div></div>';
|
layer.innerHTML = '<div class="slide-inner" style="' + style + '"><div class="content-wrap">' + (slide.content || '') + '</div></div>';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user