Feat: Adding Stoic, Updating Core Service Provider to declare that the lunar page is called Boboko
This commit is contained in:
+2
-1
@@ -12,7 +12,8 @@
|
|||||||
"php": "^8.5",
|
"php": "^8.5",
|
||||||
"lunarphp/lunar": "1.3.0",
|
"lunarphp/lunar": "1.3.0",
|
||||||
"laravel/framework": "^12.0",
|
"laravel/framework": "^12.0",
|
||||||
"laravel/tinker": "^3.0"
|
"laravel/tinker": "^3.0",
|
||||||
|
"symfony/yaml": "^7.0"
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"fakerphp/faker": "^1.23",
|
"fakerphp/faker": "^1.23",
|
||||||
|
|||||||
@@ -0,0 +1,221 @@
|
|||||||
|
/**
|
||||||
|
* Stoic CMS — in-place editor script
|
||||||
|
*
|
||||||
|
* Vendor this file into your website's asset pipeline.
|
||||||
|
*
|
||||||
|
* Configuration (set before this script runs):
|
||||||
|
* window.STOIC_HOST = "https://your-cms-domain.com"; // default: same origin
|
||||||
|
*
|
||||||
|
* Usage on your HTML elements:
|
||||||
|
* data-stoic="schema/slug" open editor for this entry
|
||||||
|
* data-stoic="schema/slug#field" open editor and focus a specific field
|
||||||
|
* data-stoic="singleton#field" open editor for a singleton schema
|
||||||
|
*
|
||||||
|
* SSO: if STOIC_SSO_SECRET is configured on the server, the admin link
|
||||||
|
* will generate a signed token so the user lands directly in the CMS.
|
||||||
|
* Set window.STOIC_SSO_TOKEN before loading this script:
|
||||||
|
* window.STOIC_SSO_TOKEN = "<?= $ssoToken ?>";
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
const HOST = (window.STOIC_HOST || "").replace(/\/$/, "");
|
||||||
|
|
||||||
|
// ── State ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
let editMode = false;
|
||||||
|
let activePane = null;
|
||||||
|
let messageListener = null;
|
||||||
|
|
||||||
|
// ── Toolbar ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const bar = document.createElement("div");
|
||||||
|
bar.className = "stoic-bar";
|
||||||
|
|
||||||
|
const btn = document.createElement("button");
|
||||||
|
btn.className = "stoic-edit-toggle";
|
||||||
|
btn.setAttribute("aria-label", "Toggle edit mode");
|
||||||
|
btn.innerHTML =
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg> Edit';
|
||||||
|
|
||||||
|
btn.addEventListener("click", function () {
|
||||||
|
editMode ? exitEditMode() : enterEditMode();
|
||||||
|
});
|
||||||
|
|
||||||
|
const adminLink = document.createElement("a");
|
||||||
|
adminLink.className = "stoic-admin-link";
|
||||||
|
adminLink.target = "_blank";
|
||||||
|
adminLink.rel = "noopener noreferrer";
|
||||||
|
adminLink.setAttribute("aria-label", "Open CMS admin");
|
||||||
|
adminLink.innerHTML =
|
||||||
|
'<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>';
|
||||||
|
|
||||||
|
// If a pre-generated SSO token is provided, use the SSO endpoint directly.
|
||||||
|
// Otherwise link to the standard login page.
|
||||||
|
if (window.STOIC_SSO_TOKEN) {
|
||||||
|
adminLink.href = HOST + "/cms/sso?" + window.STOIC_SSO_TOKEN;
|
||||||
|
} else {
|
||||||
|
adminLink.href = HOST;
|
||||||
|
}
|
||||||
|
|
||||||
|
bar.appendChild(btn);
|
||||||
|
bar.appendChild(adminLink);
|
||||||
|
|
||||||
|
// ── Edit mode ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function enterEditMode() {
|
||||||
|
editMode = true;
|
||||||
|
btn.classList.add("active");
|
||||||
|
document.querySelectorAll("[data-stoic]").forEach(function (el) {
|
||||||
|
el.classList.add("stoic-target");
|
||||||
|
el.addEventListener("click", onTargetClick, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitEditMode() {
|
||||||
|
editMode = false;
|
||||||
|
btn.classList.remove("active");
|
||||||
|
document.querySelectorAll("[data-stoic]").forEach(function (el) {
|
||||||
|
el.classList.remove("stoic-target", "stoic-target--active");
|
||||||
|
el.removeEventListener("click", onTargetClick, true);
|
||||||
|
});
|
||||||
|
closePane();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onTargetClick(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll(".stoic-target--active")
|
||||||
|
.forEach(function (el) {
|
||||||
|
el.classList.remove("stoic-target--active");
|
||||||
|
});
|
||||||
|
this.classList.add("stoic-target--active");
|
||||||
|
|
||||||
|
const attr = this.getAttribute("data-stoic");
|
||||||
|
const hashIdx = attr.indexOf("#");
|
||||||
|
const path = hashIdx === -1 ? attr : attr.slice(0, hashIdx);
|
||||||
|
const field = hashIdx === -1 ? "" : attr.slice(hashIdx + 1);
|
||||||
|
openPane(path, field);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Side pane ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
function openPane(path, field) {
|
||||||
|
closePane();
|
||||||
|
|
||||||
|
const overlay = document.createElement("div");
|
||||||
|
overlay.className = "stoic-overlay";
|
||||||
|
overlay.addEventListener("click", closePane);
|
||||||
|
|
||||||
|
const pane = document.createElement("div");
|
||||||
|
pane.className = "stoic-pane";
|
||||||
|
|
||||||
|
const closeBtn = document.createElement("button");
|
||||||
|
closeBtn.className = "stoic-pane-close";
|
||||||
|
closeBtn.setAttribute("aria-label", "Close");
|
||||||
|
closeBtn.innerHTML =
|
||||||
|
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>';
|
||||||
|
closeBtn.addEventListener("click", closePane);
|
||||||
|
|
||||||
|
const iframe = document.createElement("iframe");
|
||||||
|
iframe.className = "stoic-pane-iframe";
|
||||||
|
let src = HOST + "/cms/embed/" + path;
|
||||||
|
if (field) src += "?focus=" + encodeURIComponent(field);
|
||||||
|
iframe.src = src;
|
||||||
|
|
||||||
|
pane.appendChild(closeBtn);
|
||||||
|
pane.appendChild(iframe);
|
||||||
|
|
||||||
|
document.body.appendChild(overlay);
|
||||||
|
document.body.appendChild(pane);
|
||||||
|
activePane = { pane: pane, overlay: overlay };
|
||||||
|
|
||||||
|
messageListener = function (e) {
|
||||||
|
if (e.data && e.data.type === "stoic:saved") {
|
||||||
|
onSaved();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
window.addEventListener("message", messageListener);
|
||||||
|
|
||||||
|
requestAnimationFrame(function () {
|
||||||
|
pane.classList.add("open");
|
||||||
|
overlay.classList.add("open");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closePane() {
|
||||||
|
if (!activePane) return;
|
||||||
|
var pane = activePane.pane;
|
||||||
|
var overlay = activePane.overlay;
|
||||||
|
|
||||||
|
pane.classList.remove("open");
|
||||||
|
overlay.classList.remove("open");
|
||||||
|
|
||||||
|
setTimeout(function () {
|
||||||
|
pane.remove();
|
||||||
|
overlay.remove();
|
||||||
|
}, 220);
|
||||||
|
|
||||||
|
if (messageListener) {
|
||||||
|
window.removeEventListener("message", messageListener);
|
||||||
|
messageListener = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
document
|
||||||
|
.querySelectorAll(".stoic-target--active")
|
||||||
|
.forEach(function (el) {
|
||||||
|
el.classList.remove("stoic-target--active");
|
||||||
|
});
|
||||||
|
|
||||||
|
activePane = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSaved() {
|
||||||
|
var activeEl = document.querySelector(".stoic-target--active");
|
||||||
|
if (activeEl) {
|
||||||
|
activeEl.dispatchEvent(
|
||||||
|
new CustomEvent("stoic:saved", {
|
||||||
|
bubbles: true,
|
||||||
|
composed: true,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
closePane();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Styles ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
var style = document.createElement("style");
|
||||||
|
style.textContent = [
|
||||||
|
".stoic-bar{position:fixed;bottom:1.25rem;right:1.25rem;z-index:2147483646;display:inline-flex;align-items:center;gap:0.375rem;background:#1a1a2e;border-radius:9999px;padding:0.25rem 0.25rem 0.25rem 0.5rem;box-shadow:0 2px 12px rgba(0,0,0,.35);}",
|
||||||
|
".stoic-edit-toggle{display:inline-flex;align-items:center;gap:0.4rem;padding:0.35rem 0.75rem;border:none;border-radius:9999px;background:transparent;color:#fff;font-size:0.8rem;font-weight:600;font-family:system-ui,sans-serif;cursor:pointer;transition:background .15s;}",
|
||||||
|
".stoic-edit-toggle:hover{background:rgba(255,255,255,.1);}",
|
||||||
|
".stoic-edit-toggle.active{background:#4f46e5;}",
|
||||||
|
".stoic-admin-link{display:inline-flex;align-items:center;justify-content:center;width:1.75rem;height:1.75rem;border-radius:50%;color:rgba(255,255,255,.6);text-decoration:none;transition:color .15s,background .15s;}",
|
||||||
|
".stoic-admin-link:hover{color:#fff;background:rgba(255,255,255,.1);}",
|
||||||
|
".stoic-target{outline:2px dashed rgba(79,70,229,.5)!important;outline-offset:2px!important;cursor:pointer!important;transition:outline-color .15s;}",
|
||||||
|
".stoic-target:hover{outline-color:rgba(79,70,229,.9)!important;}",
|
||||||
|
".stoic-target--active{outline:2px solid #4f46e5!important;outline-offset:2px!important;}",
|
||||||
|
".stoic-overlay{position:fixed;inset:0;z-index:2147483644;background:rgba(0,0,0,.25);opacity:0;transition:opacity .2s;}",
|
||||||
|
".stoic-overlay.open{opacity:1;}",
|
||||||
|
".stoic-pane{position:fixed;top:0;right:0;bottom:0;z-index:2147483645;width:min(480px,100vw);background:#fff;box-shadow:-4px 0 24px rgba(0,0,0,.18);transform:translateX(100%);transition:transform .22s cubic-bezier(.4,0,.2,1);display:flex;flex-direction:column;}",
|
||||||
|
".stoic-pane.open{transform:translateX(0);}",
|
||||||
|
".stoic-pane-close{position:absolute;top:.75rem;right:.75rem;z-index:1;width:1.75rem;height:1.75rem;border:none;border-radius:50%;background:rgba(0,0,0,.07);color:#555;cursor:pointer;display:flex;align-items:center;justify-content:center;padding:0;}",
|
||||||
|
".stoic-pane-close:hover{background:rgba(0,0,0,.14);}",
|
||||||
|
".stoic-pane-iframe{flex:1;width:100%;border:none;}",
|
||||||
|
].join("");
|
||||||
|
|
||||||
|
// ── Init ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
document.head.appendChild(style);
|
||||||
|
|
||||||
|
if (document.readyState === "loading") {
|
||||||
|
document.addEventListener("DOMContentLoaded", function () {
|
||||||
|
document.body.appendChild(bar);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
document.body.appendChild(bar);
|
||||||
|
}
|
||||||
|
})();
|
||||||
+2
-2
@@ -24,8 +24,8 @@ class CorePlugin implements Plugin
|
|||||||
{
|
{
|
||||||
$panel->path('boboko')
|
$panel->path('boboko')
|
||||||
->brandName('Boboko')
|
->brandName('Boboko')
|
||||||
->brandLogo(asset('logos/core/boboko-logo.svg'))
|
->brandLogo(asset('static/logos/core/boboko-logo.svg'))
|
||||||
->darkModeBrandLogo(asset('logos/core/boboko-logo-white.svg'))
|
->darkModeBrandLogo(asset('static/logos/core/boboko-logo-white.svg'))
|
||||||
->login(Login::class)
|
->login(Login::class)
|
||||||
->navigationItems([
|
->navigationItems([
|
||||||
NavigationItem::make("Lucent")
|
NavigationItem::make("Lucent")
|
||||||
|
|||||||
@@ -18,7 +18,8 @@ class CoreServiceProvider extends ServiceProvider
|
|||||||
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
|
$this->loadMigrationsFrom(__DIR__ . '/../../database/migrations');
|
||||||
|
|
||||||
$this->publishes([
|
$this->publishes([
|
||||||
__DIR__ . '/../../resources/logos' => public_path('logos/core'),
|
__DIR__ . '/../../resources/logos' => public_path('static/logos/core'),
|
||||||
|
__DIR__ . '/../../resources/js/stoic_embed.js' => public_path('js/stoic_embed.js'),
|
||||||
], 'core-assets');
|
], 'core-assets');
|
||||||
|
|
||||||
if ($this->app->runningInConsole()) {
|
if ($this->app->runningInConsole()) {
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Modules\Core\Stoic;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Symfony\Component\Yaml\Yaml;
|
||||||
|
|
||||||
|
class Stoic
|
||||||
|
{
|
||||||
|
public static array $config = [];
|
||||||
|
public static array $presets = [];
|
||||||
|
public static string $thumbs_path;
|
||||||
|
|
||||||
|
private static function loadConfig(): void
|
||||||
|
{
|
||||||
|
if (!empty(static::$config)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$config = Yaml::parseFile(resource_path("stoic/stoic_config.yml"));
|
||||||
|
static::$config = $config;
|
||||||
|
static::$thumbs_path = $config["thumbs_path"];
|
||||||
|
static::$presets = $config["presets"];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function presets(): array
|
||||||
|
{
|
||||||
|
static::loadConfig();
|
||||||
|
return static::$presets;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns the public URL for a stoic image at a given preset.
|
||||||
|
public static function image(string $filename, string $preset): string
|
||||||
|
{
|
||||||
|
static::loadConfig();
|
||||||
|
|
||||||
|
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
|
||||||
|
if ($ext === "svg") {
|
||||||
|
$rel = static::thumbsRelPath() . "images/" . $filename;
|
||||||
|
return Storage::disk("public")->url($rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
$webp = substr($filename, 0, -strlen($ext)) . "webp";
|
||||||
|
$rel = static::thumbsRelPath() . "presets/" . $preset . "/" . $webp;
|
||||||
|
|
||||||
|
return Storage::disk("public")->url($rel);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generates the full srcset string for all configured presets.
|
||||||
|
public static function srcset(string $filename): string
|
||||||
|
{
|
||||||
|
static::loadConfig();
|
||||||
|
|
||||||
|
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
if ($ext === "svg") {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
return collect(static::$presets)
|
||||||
|
->map(
|
||||||
|
fn($p) => static::image($filename, $p["code"]) .
|
||||||
|
" " .
|
||||||
|
$p["width"] .
|
||||||
|
"w",
|
||||||
|
)
|
||||||
|
->implode(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns [width, height] for a given preset from the sidecar JSON.
|
||||||
|
public static function dimensions(string $filename, string $preset): array
|
||||||
|
{
|
||||||
|
static::loadConfig();
|
||||||
|
|
||||||
|
$ext = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
|
||||||
|
if ($ext === "svg") {
|
||||||
|
return [null, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
$rel = static::thumbsRelPath() . "images/" . $filename . ".json";
|
||||||
|
$path = Storage::disk("public")->path($rel);
|
||||||
|
$meta = json_decode(@file_get_contents($path), true);
|
||||||
|
|
||||||
|
$p = $meta["presets"][$preset] ?? null;
|
||||||
|
if (!$p) {
|
||||||
|
return [null, null];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [(int) $p["w"], (int) $p["h"]];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path of the thumbs directory relative to the public storage disk root.
|
||||||
|
// Uses a fixed marker so it works regardless of the app's base path (local vs Docker).
|
||||||
|
private static function thumbsRelPath(): string
|
||||||
|
{
|
||||||
|
$marker = "/storage/app/public/";
|
||||||
|
$pos = strpos(static::$thumbs_path, $marker);
|
||||||
|
|
||||||
|
if ($pos !== false) {
|
||||||
|
return substr(static::$thumbs_path, $pos + strlen($marker));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ltrim(static::$thumbs_path, "/");
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function token(string $email): string
|
||||||
|
{
|
||||||
|
$ttl = 60;
|
||||||
|
$payload = base64_encode(
|
||||||
|
json_encode([
|
||||||
|
"email" => $email,
|
||||||
|
"exp" => time() + $ttl,
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
|
||||||
|
$sig = hash_hmac(
|
||||||
|
"sha256",
|
||||||
|
$payload,
|
||||||
|
config("services.stoic.sso_secret"),
|
||||||
|
);
|
||||||
|
|
||||||
|
$token = "t=" . urlencode($payload) . "&s=" . $sig;
|
||||||
|
|
||||||
|
return $token;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function hasMany(
|
||||||
|
string $relation,
|
||||||
|
array $slugs,
|
||||||
|
string $prefix,
|
||||||
|
) {
|
||||||
|
$slugs = array_map(
|
||||||
|
fn($i) => str_replace($prefix . "/", "", $i),
|
||||||
|
$slugs,
|
||||||
|
);
|
||||||
|
|
||||||
|
return $relation::whereIn("slug", $slugs)->get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function hasOne(
|
||||||
|
string $relation,
|
||||||
|
string $slug,
|
||||||
|
string $prefix,
|
||||||
|
) {
|
||||||
|
$slug = str_replace($prefix . "/", "", $slug);
|
||||||
|
return $relation::firstWhere("slug", $slug);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user