feat: initial core/php-client — SaaS customer dashboard

Extracts Core\Front\Client (authenticated dashboard, client routes,
Livewire components) from core/php.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Snider 2026-03-09 18:55:40 +00:00
commit 97909bec75
8 changed files with 335 additions and 0 deletions

22
LICENSE Normal file
View file

@ -0,0 +1,22 @@
European Union Public Licence
Version 1.2
EUPL (c) the European Union 2007, 2016
This European Union Public Licence (the "EUPL") applies to the Work (as
defined below) which is provided under the terms of this Licence. Any use
of the Work, other than as authorised under this Licence is prohibited (to
the extent such use is covered by a right of the copyright holder of the
Work).
The Work is provided under the terms of this Licence when the Licensor (as
defined below) has placed the following notice immediately following the
copyright notice for the Work:
Licensed under the EUPL
or has expressed by any other means his willingness to license under the
EUPL.
For the full licence text, see:
https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12

24
composer.json Normal file
View file

@ -0,0 +1,24 @@
{
"name": "lthn/client",
"description": "SaaS customer dashboard — authenticated client frontend",
"license": "EUPL-1.2",
"require": {
"php": "^8.2",
"lthn/php": "*"
},
"autoload": {
"psr-4": {
"Core\\Front\\Client\\": "src/Core/Front/Client/"
}
},
"replace": {
"core/php-client": "self.version"
},
"extra": {
"laravel": {
"dont-discover": []
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View file

@ -0,0 +1,24 @@
<div class="max-w-4xl mx-auto px-6 py-12">
<h1 class="text-3xl font-bold text-white mb-4">Your Namespace</h1>
<p class="text-zinc-400 mb-8">Manage your space on the internet.</p>
<div class="grid gap-6">
{{-- Namespace Overview --}}
<div class="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
<h2 class="text-lg font-semibold text-white mb-2">Overview</h2>
<p class="text-zinc-500 text-sm">Your namespace details will appear here.</p>
</div>
{{-- Quick Actions --}}
<div class="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
<h2 class="text-lg font-semibold text-white mb-2">Quick Actions</h2>
<p class="text-zinc-500 text-sm">Edit your bio, view analytics, and more.</p>
</div>
{{-- Recent Activity --}}
<div class="bg-zinc-900 border border-zinc-800 rounded-xl p-6">
<h2 class="text-lg font-semibold text-white mb-2">Recent Activity</h2>
<p class="text-zinc-500 text-sm">Your recent activity will appear here.</p>
</div>
</div>
</div>

View file

@ -0,0 +1,85 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}" class="dark">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ $title ?? 'Dashboard' }} - lt.hn</title>
{{-- Fonts --}}
<link rel="preconnect" href="https://fonts.bunny.net">
<link href="https://fonts.bunny.net/css?family=inter:400,500,600,700&display=swap" rel="stylesheet">
{{-- Font Awesome --}}
<link rel="stylesheet" href="/vendor/fontawesome/css/all.min.css">
@vite(['resources/css/admin.css', 'resources/js/app.js'])
@fluxAppearance
</head>
<body class="font-inter antialiased bg-[#070b0b] text-[#cccccb] min-h-screen">
{{-- Header --}}
<header class="sticky top-0 z-50 border-b border-[#40c1c5]/10 bg-[#070b0b]/95 backdrop-blur-sm">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="flex items-center justify-between h-14">
{{-- Logo + Bio link --}}
<div class="flex items-center gap-4">
<a href="{{ url('/') }}" class="text-xl font-bold text-white">lt.hn</a>
@if(isset($bioUrl))
<span class="text-[#cccccb]/40">/</span>
<a href="{{ url('/' . $bioUrl) }}" class="text-sm text-[#40c1c5] hover:text-[#5dd1d5] transition">
{{ $bioUrl }}
</a>
@endif
</div>
{{-- Nav + User menu --}}
<div class="flex items-center gap-6">
@if(isset($bioUrl))
<nav class="flex items-center gap-1">
@php
$currentPath = request()->path();
$navItems = [
['url' => "/{$bioUrl}/settings", 'label' => 'Editor', 'icon' => 'fa-pen-to-square'],
['url' => "/{$bioUrl}/analytics", 'label' => 'Analytics', 'icon' => 'fa-chart-line'],
['url' => "/{$bioUrl}/submissions", 'label' => 'Submissions', 'icon' => 'fa-inbox'],
['url' => "/{$bioUrl}/qr", 'label' => 'QR Code', 'icon' => 'fa-qrcode'],
];
@endphp
@foreach($navItems as $item)
<a href="{{ url($item['url']) }}"
class="flex items-center gap-2 px-3 py-1.5 rounded-md text-sm transition
{{ $currentPath === ltrim($item['url'], '/')
? 'bg-[#40c1c5]/10 text-[#40c1c5]'
: 'text-[#cccccb]/60 hover:text-white hover:bg-white/5' }}">
<i class="fa-solid {{ $item['icon'] }} text-xs"></i>
<span class="hidden sm:inline">{{ $item['label'] }}</span>
</a>
@endforeach
</nav>
@endif
@auth
<div class="flex items-center gap-4 pl-4 border-l border-[#40c1c5]/10">
<a href="{{ url('/dashboard') }}" class="text-sm text-[#cccccb]/60 hover:text-[#40c1c5] transition hidden md:inline">
Dashboard
</a>
<a href="{{ url('/logout') }}" class="text-sm text-[#cccccb]/60 hover:text-white transition">
Sign out
</a>
</div>
@endauth
</div>
</div>
</div>
</header>
{{-- Main content --}}
<main class="min-h-[calc(100vh-3.5rem)]">
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
{{ $slot }}
</div>
</main>
@fluxScripts
</body>
</html>

View file

@ -0,0 +1,69 @@
<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Front\Client;
use Core\Headers\SecurityHeaders;
use Core\LifecycleEventProvider;
use Illuminate\Foundation\Configuration\Middleware;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\ServiceProvider;
/**
* Client frontage - namespace owner dashboard.
*
* For SaaS customers managing their namespace (personal workspace).
* Not the full Hub/Admin - just YOUR space on the internet.
*
* Hierarchy:
* - Core/Front/Web = Public (anonymous, read-only)
* - Core/Front/Client = SaaS customer (authenticated, namespace owner)
* - Core/Front/Admin = Backend admin (privileged)
* - Core/Hub = SaaS operator (Host.uk.com control plane)
*
* A namespace is tied to a URI/handle (lt.hn/you, you.lthn).
* A workspace (org) can manage multiple namespaces.
* A personal workspace IS your namespace.
*/
class Boot extends ServiceProvider
{
/**
* Configure client middleware group.
*/
public static function middleware(Middleware $middleware): void
{
$middleware->group('client', [
\Illuminate\Cookie\Middleware\EncryptCookies::class,
\Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse::class,
\Illuminate\Session\Middleware\StartSession::class,
\Illuminate\View\Middleware\ShareErrorsFromSession::class,
\Illuminate\Foundation\Http\Middleware\ValidateCsrfToken::class,
\Illuminate\Routing\Middleware\SubstituteBindings::class,
SecurityHeaders::class,
'auth',
]);
}
public function register(): void
{
//
}
public function boot(): void
{
// Register client:: namespace for client dashboard components
$this->loadViewsFrom(__DIR__.'/Blade', 'client');
Blade::anonymousComponentPath(__DIR__.'/Blade', 'client');
// Fire ClientRoutesRegistering event for lazy-loaded modules
LifecycleEventProvider::fireClientRoutes();
}
}

View file

@ -0,0 +1,52 @@
# Core/Front/Client
SaaS customer dashboard for namespace owners.
## Concept
```
Core/Front/Web → Public (anonymous, read-only)
Core/Front/Client → SaaS customer (authenticated, namespace owner) ← THIS
Core/Front/Admin → Backend admin (privileged)
Core/Hub → SaaS operator (Host.uk.com control plane)
```
## Namespace vs Workspace
- **Namespace** = your identity, tied to a URI/handle (lt.hn/you, you.lthn)
- **Workspace** = management container (org/agency that can own multiple namespaces)
- **Personal workspace** = IS your namespace (1:1 for solo users)
A user with just a personal workspace uses **Client** to manage their namespace.
An org workspace with multiple namespaces uses **Hub** for team management.
## Use Cases
- Bio page editor (lt.hn/you)
- Analytics dashboard (your stats)
- Domain management (custom domains, web3)
- Settings (profile, notifications)
- Boost purchases (expand namespace entitlements)
## Not For
- Team/org management (use Hub)
- Multi-namespace management (use Hub)
- Backend admin tasks (use Admin)
- Public viewing (use Web)
## Middleware
```php
Route::middleware('client')->group(function () {
// Namespace owner routes
});
```
## Views
```blade
@extends('client::layouts.app')
<x-client::component />
```

View file

@ -0,0 +1,30 @@
<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
use Core\Front\Client\View\Dashboard;
use Illuminate\Support\Facades\Route;
/*
|--------------------------------------------------------------------------
| Client Routes
|--------------------------------------------------------------------------
|
| Routes for namespace owners managing their personal workspace.
| Uses 'client' middleware group (authenticated, namespace owner).
|
*/
Route::middleware('client')->group(function () {
// Dashboard
Route::get('/dashboard', Dashboard::class)->name('client.dashboard');
// Additional routes can be registered via ClientRoutesRegistering event
});

View file

@ -0,0 +1,29 @@
<?php
/*
* Core PHP Framework
*
* Licensed under the European Union Public Licence (EUPL) v1.2.
* See LICENSE file for details.
*/
declare(strict_types=1);
namespace Core\Front\Client\View;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Title;
use Livewire\Component;
/**
* Client dashboard - namespace owner home.
*/
#[Title('Dashboard')]
class Dashboard extends Component
{
public function render(): View
{
return view('client::dashboard')
->layout('client::layouts.app');
}
}