transition

This commit is contained in:
2024-08-14 22:04:34 +03:00
parent 1ab3f678b7
commit 1f3ebafe69
50 changed files with 924 additions and 172 deletions
+3 -2
View File
@@ -8,13 +8,14 @@
"ext-zip": "*",
"ext-sqlite3": "*",
"ext-imagick": "*",
"php": "^8.2",
"php": "^8.3",
"guzzlehttp/guzzle": "^7.2",
"intervention/image": "^2.7",
"phpoption/phpoption": "^1.9",
"spatie/image-optimizer": "^1.6",
"staudenmeir/laravel-cte": "^1.0",
"ext-pdo": "*"
"ext-pdo": "*",
"mustache/mustache": "^2.14"
},
"require-dev": {
"phpstan/phpstan": "^1.8"
Generated
+56 -2
View File
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "351290446963296c5fabf750b3077a95",
"content-hash": "cd811774f135eb0a9c0338113aa84d8e",
"packages": [
{
"name": "brick/math",
@@ -947,6 +947,56 @@
],
"time": "2022-05-21T17:30:32+00:00"
},
{
"name": "mustache/mustache",
"version": "v2.14.2",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/mustache.php.git",
"reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/e62b7c3849d22ec55f3ec425507bf7968193a6cb",
"reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb",
"shasum": ""
},
"require": {
"php": ">=5.2.4"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~1.11",
"phpunit/phpunit": "~3.7|~4.0|~5.0"
},
"type": "library",
"autoload": {
"psr-0": {
"Mustache": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
}
],
"description": "A Mustache implementation in PHP.",
"homepage": "https://github.com/bobthecow/mustache.php",
"keywords": [
"mustache",
"templating"
],
"support": {
"issues": "https://github.com/bobthecow/mustache.php/issues",
"source": "https://github.com/bobthecow/mustache.php/tree/v2.14.2"
},
"time": "2022-08-23T13:07:01+00:00"
},
{
"name": "nesbot/carbon",
"version": "2.71.0",
@@ -2261,7 +2311,11 @@
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": "^8.2",
"ext-xml": "*",
"ext-zip": "*",
"ext-sqlite3": "*",
"ext-imagick": "*",
"php": "^8.3",
"ext-pdo": "*"
},
"platform-dev": [],
+3
View File
@@ -5,6 +5,9 @@
*/
import axios from "axios";
import {loadHtmxFormsBehaviour} from "./htmx-form.js";
loadHtmxFormsBehaviour();
window.axios = axios;
export const axiosInstance = axios;
+24
View File
@@ -0,0 +1,24 @@
export function loadHtmxFormsBehaviour(){
document.querySelectorAll(".form").forEach(el => {
initHtmxForm(el);
})
}
function initHtmxForm(el){
el.addEventListener("htmx:responseError", (e) => {
el.querySelector(".form-errors").innerHTML = e.detail.xhr.response;
});
const formEl = el.querySelector("form");
if(!formEl.getAttribute("hx-redirect")){
return;
}
el.addEventListener("htmx:afterOnLoad", (e) => {
if(e.detail.successful){
return window.location.href = formEl.getAttribute("hx-redirect");
}
});
}
+1
View File
@@ -4,6 +4,7 @@ import Account from "./svelte/Account.svelte";
import Channel from "./svelte/Channel.svelte";
import * as bootstrap from "bootstrap";
import Mustache from "mustache";
import 'htmx.org';
Mustache.escape = function (value) {
return value;
+7 -1
View File
@@ -5,7 +5,8 @@
"packages": {
"": {
"dependencies": {
"@codemirror/lang-markdown": "^6.2.2"
"@codemirror/lang-markdown": "^6.2.2",
"htmx.org": "^2.0.1"
},
"devDependencies": {
"@codemirror/commands": "^6.1.2",
@@ -935,6 +936,11 @@
"node": ">= 0.4.0"
}
},
"node_modules/htmx.org": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.1.tgz",
"integrity": "sha512-wO/rWlveSLD2mzRS9Em0v5hlsi6r21iUvaS17GPMgehBbM7eoQmqGQCkscsM67poF24zONgq3gQv+q/cgCHn2w=="
},
"node_modules/immutable": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.4.tgz",
+2 -1
View File
@@ -29,6 +29,7 @@
"vite": "^3.2.3"
},
"dependencies": {
"@codemirror/lang-markdown": "^6.2.2"
"@codemirror/lang-markdown": "^6.2.2",
"htmx.org": "^2.0.1"
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 195 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><circle cx="4" cy="12" r="3" fill="currentColor"><animate id="svgSpinners3DotsBounce0" attributeName="cy" begin="0;svgSpinners3DotsBounce1.end+0.25s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle><circle cx="12" cy="12" r="3" fill="currentColor"><animate attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.1s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle><circle cx="20" cy="12" r="3" fill="currentColor"><animate id="svgSpinners3DotsBounce1" attributeName="cy" begin="svgSpinners3DotsBounce0.begin+0.2s" calcMode="spline" dur="0.6s" keySplines=".33,.66,.66,1;.33,0,.66,.33" values="12;6;12"/></circle></svg>

After

Width:  |  Height:  |  Size: 803 B

+22
View File
@@ -0,0 +1,22 @@
.scope-login {
display: flex;
height: 100vh;
.bg-image {
width: 50%;
background: url("/vendor/lucent/public/art.jpg");
background-repeat: no-repeat;
background-size: cover;
background-repeat: no-repeat;
background-position: center center;
}
.login-form{
width: 50%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
}
+85
View File
@@ -0,0 +1,85 @@
label {
display: block;
font-weight: 700;
margin-bottom: 4px;
}
input{
width: 100%;
}
input,textarea{
background: var(--input-bg);
border: 1px solid $border-color;
border-radius: 5px;
padding: 5px 7px;
font-size: 16px;
filter: brightness(95%);
}
input:focus,textarea:focus{
filter: brightness(101%);
}
textarea{
resize: none;
}
.htmx-indicator {
display: none;
}
.htmx-request .htmx-indicator {
display: inline;
}
.htmx-request.htmx-indicator {
display: inline;
}
.bt {
appearance: none;
background-color: #000;
background-image: none;
border: 1px solid #000;
border-radius: 4px;
box-shadow: #fff 4px 4px 0 0, #000 4px 4px 0 1px;
box-sizing: border-box;
color: #fff;
cursor: pointer;
display: inline-block;
font-family: ITCAvantGardeStd-Bk, Arial, sans-serif;
font-size: 16px;
font-weight: 400;
line-height: 20px;
margin: 0 5px 10px 0;
overflow: visible;
padding: 8px 40px;
text-align: center;
text-transform: none;
touch-action: manipulation;
user-select: none;
-webkit-user-select: none;
vertical-align: middle;
white-space: nowrap;
}
.bt:focus {
text-decoration: none;
}
.bt:hover {
text-decoration: none;
}
.bt:active {
box-shadow: rgba(0, 0, 0, .125) 0 3px 5px inset;
outline: 0;
}
.bt:not([disabled]):active {
box-shadow: #fff 2px 2px 0 0, #000 2px 2px 0 1px;
transform: translate(2px, 2px);
}
+49
View File
@@ -0,0 +1,49 @@
.mt-1{margin-top: 4px}
.mt-2{margin-top: 8px}
.mt-3{margin-top: 12px}
.mt-4{margin-top: 16px}
.mt-5{margin-top: 20px}
.mb-1{margin-bottom: 4px}
.mb-2{margin-bottom: 8px}
.mb-3{margin-bottom: 12px}
.mb-4{margin-bottom: 16px}
.mb-5{margin-bottom: 20px}
.pt-1{padding-top: 4px}
.pt-2{padding-top: 8px}
.pt-3{padding-top: 12px}
.pt-4{padding-top: 16px}
.pt-5{padding-top: 20px}
.pb-1{padding-bottom: 4px}
.pb-2{padding-bottom: 8px}
.pb-3{padding-bottom: 12px}
.pb-4{padding-bottom: 16px}
.pb-5{padding-bottom: 20px}
.gap-1{gap: 4px}
.gap-2{gap: 8px}
.gap-3{gap: 12px}
.gap-4{gap: 16px}
.gap-5{gap: 20px}
.hide{
display: none;
}
.d-block{
display: block;
}
.d-inline-block{
display: inline-block;
}
.is-bold{
font-weight: 700;
}
.in-place{
padding: 36px;
}
-3
View File
@@ -1,3 +0,0 @@
.in-place{
padding: 36px;
}
+14
View File
@@ -0,0 +1,14 @@
.sidebar {
}
.main-content {
width: 100%;
}
.main-wrapper {
display: flex;
gap: 40px;
padding: 20px;
}
-16
View File
@@ -1,16 +0,0 @@
.lx-nav{
display: flex;
justify-content: space-between;
align-items: center;
background-color: rgba(255,255,255,1);
margin-bottom:0px ;
.nav-item{
padding:16px 0;
margin: 0 16px;
color: $primary;
}
a{
text-decoration: none;
}
}
+32
View File
@@ -0,0 +1,32 @@
.notice {
background-color: $background;
padding: 25px 14px 14px;
margin: 2rem 0;
filter: brightness(1.03);
position: relative;
font-size: 16px;
line-height: 24px;
border: 3px solid $border-color;
}
.notice .title {
content: "NOTE";
position: absolute;
background: $background;
min-width: 90px;
border: 3px solid $border-color;
color: $text;
display: block;
text-align: center;
left: 14px;
top: -18px;
padding: 2px 10px;
font-weight: bold;
}
.notice.success{
border-color: $success;
& .title{
border-color: $success;
}
}
+46
View File
@@ -0,0 +1,46 @@
/*
1. Use a more-intuitive box-sizing model.
*/
*, *::before, *::after {
box-sizing: border-box;
}
/*
2. Remove default margin
*/
* {
margin: 0;
}
/*
Typographic tweaks!
3. Add accessible line-height
4. Improve text rendering
*/
body {
line-height: 1.5;
-webkit-font-smoothing: antialiased;
}
/*
5. Improve media defaults
*/
img, picture, video, canvas, svg {
display: block;
max-width: 100%;
}
/*
6. Remove built-in form typography styles
*/
input, button, textarea, select {
font: inherit;
}
/*
7. Avoid text overflows
*/
p, h1, h2, h3, h4, h5, h6 {
overflow-wrap: break-word;
}
/*
8. Create a root stacking context
*/
#root, #__next {
isolation: isolate;
}
+29 -69
View File
@@ -1,89 +1,49 @@
.sidebar {
background-color: $dark;
border: 1px solid $border-color;
border-radius: 12px;
min-height: 100vh;
font-size: 15px;
line-height: 28px;
min-width: 300px;
max-width: 400px;
background: $background;
filter: brightness(104%);
}
.sidebar-header {
color: $white;
position: relative;
padding: 0 10px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
&:hover .actions {
visibility: visible;
}
}
background: $background;
font-size: 16px;
padding: 12px 12px 6px;
color: $text;
filter: brightness(94%);
.sidebar .actions {
position: absolute;
top: 0px;
right: 8px;
background-color: $dark;
visibility: hidden;
&:first-child {
border-bottom: none;
border-radius: 12px 12px 0 0;
}
.sidebar .sidebar-header .actions {
background-color: $dark;
}
.sidebar .sidebar-item .actions {
background-color: $white;
}
.sidebar .sidebar-item.active .actions {
background-color: $primary;
}
.sidebar .sidebar-header .actions a,
.sidebar .sidebar-header .actions span {
color: $white !important;
cursor: pointer;
}
.sidebar .sidebar-item .actions a {
color: $dark !important;
cursor: pointer;
}
.sidebar .sidebar-item.active .actions a {
color: $white !important;
cursor: pointer;
}
.sidebar-item {
a {
color: $white;
color: $text;
display: block;
font-size: 14px;
padding: 6px 12px;
text-decoration: none;
padding: 0 0px;
border-bottom: 1px solid $border-color;
transition: 600ms;
&:last-child {
border-bottom: none;
border-radius: 0 0 12px 12px;
}
&:hover {
background-color: $light;
a {
color: $dark;
}
}
position: relative;
padding: 0 10px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
color: $white;
&:hover .actions {
visibility: visible;
background: $secondary;
}
}
.sidebar hr {
color: $white;
line-height: 30px;
}
.sidebar .active {
background-color: $primary;
a {
color: $white;
&.active {
background: $secondary;
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
.lx-table {
min-width: 600px;
overflow: auto;
background-color: #f9f9f9;
table {
width: 100%;
min-width: 600px;
}
th {
-4
View File
@@ -1,4 +0,0 @@
.lx-small-text {
font-size: 12px;
line-height: 15px;
}
+56
View File
@@ -0,0 +1,56 @@
.content{
font-size: 16px;
line-height: 20px;
p{
margin-bottom: 14px;
&:last-child{
margin-bottom: 0;
}
}
ul {
padding: 0 0 0 16px;
list-style: none outside none;
li::before {
content: "";
opacity: .5;
font-size: 12px;
padding-right: 6px;
vertical-align: 10%;
}
li {
list-style: none;
padding: 0;
}
}
}
.title-separator{
font-size: 20px;
text-align: center;
padding: 30px 0 10px;
margin-bottom: 30px;
position: relative;
border-bottom: 1px solid var(--border-color);
//&:after {
// position: absolute;
// left: 0;
// right: 0;
// top: 70px;
// margin: auto;
// display: block;
// width: 270px;
// border-bottom: 1px solid var(--border-color);
// content: "";
//}
}
.lx-small-text {
font-size: 12px;
line-height: 15px;
}
+1 -1
View File
@@ -120,6 +120,6 @@
}
.header-small {
text-align: center;
text-align: left;
font-size: 26px;
}
+58 -8
View File
@@ -6,6 +6,10 @@ $green-pigment: #139a43ff;
$lincoln-green: #0b5d1eff;
$forest-green-traditional: #053b06ff;
$black: #000000ff;
$dark: #000000ff;
$white: #ffffff;
$light: #eee;
$danger: red;
/* SCSS Gradient */
$gradient-top: linear-gradient(
@@ -80,25 +84,71 @@ $gradient-radial: radial-gradient(
#000000ff
);
$primary: $lincoln-green;
$primary: #5b86be;
$secondary: #d9cca1;
$success: #80c671;
$background: #f4f6fa;
$table-striped-bg-factor: 0.03;
$dropdown-bg: rgb(206, 223, 210);
$border-color: #000;
$text: #04060b;
$accent: #80c671;
@import "../node_modules/bootstrap/scss/bootstrap";
//https://www.realtimecolors.com/?colors=04060b-f4f6fa-5b86be-d9cca1-80c671&fonts=Anek Telugu-Anek Telugu
$themes: (
light: (
text: #04060b,
background: #f4f6fa,
primary: #5b86be,
secondary: #d9cca1,
accent: #80c671,
),
dark: (
text: #f4f6fb,
background: #05070a,
primary: #416ca4,
secondary: #5e5126,
accent: #488e39,
),
);
//@import "../node_modules/bootstrap/scss/bootstrap";
@import "./reset";
@import "./helpers";
@import "./notice";
@import "./auth";
@import "./typography";
@import "./sidebar";
@import "./form";
@import "./table";
@import "./avatar";
@import "./codemirror";
@import "./layout";
@import "./wrappers";
@import "./in-place";
@import "./text";
@import "./card";
@import "./nav";
@import "./files";
@import "./revisions";
:root{
--linearPrimarySecondary: linear-gradient(#5b86be, #d9cca1);
--linearPrimaryAccent: linear-gradient(#5b86be, #80c671);
--linearSecondaryAccent: linear-gradient(#d9cca1, #80c671);
--radialPrimarySecondary: radial-gradient(#5b86be, #d9cca1);
--radialPrimaryAccent: radial-gradient(#5b86be, #80c671);
--radialSecondaryAccent: radial-gradient(#d9cca1, #80c671);
--border-color: $border-color;
--main-font: Open Sans, Arial, Helvetica, sans-serif;
--main-font-color: #444;
--input-bg: rgb(245,245,249);
}
body {
background-color: rgba(11, 93, 30, 0.04);
background-color: $background;
}
.btn-spinner .spinner-border {
@@ -119,8 +169,8 @@ body {
}
.dropdown-menu {
border: 0px;
border: 0;
border-radius: 15px;
box-shadow: 0px 0px 4px #ccc;
box-shadow: 0 0 4px #ccc;
padding: 30px 15px;
}
+9
View File
@@ -0,0 +1,9 @@
<x-lucent::notice type="success" title="Success">
<p>
If you have provided a valid email you should receive in the following seconds a login email
</p>
<p>You can safely close this tab</p>
</x-lucent::notice>
+36
View File
@@ -0,0 +1,36 @@
@extends("lucent::layouts.account")
@section("content")
<div class="scope-login">
<div class="bg-image">
</div>
<div class="login-form">
<div class="form">
<h2 class="mb-5">Enter Lucent</h2>
<form hx-post="/lucent/login" >
<p>Submit your email address and you will receive a <b>login link</b> to your email</p>
<p>Don't forget to check your spam folder</p>
<div class="mt-5 mb-3">
<label for="emailaddress" class="form-label">Email address</label>
<input
type="email"
name="email"
class="form-control"
id="emailaddress"
required
/>
</div>
<x-lucent::button-indicator>
Send email
</x-lucent::button-indicator>
</form>
</div>
</div>
</div>
@endsection
+24
View File
@@ -0,0 +1,24 @@
@extends("lucent::layouts.account")
@section("content")
<div class="scope-login">
<div class="bg-image">
</div>
<div class="login-form">
<div class="form">
<h2 class="mb-5">Welcome to Lucent</h2>
<form hx-post="/lucent/verify" hx-redirect="/lucent" hx-target-error=".form-errors" >
<input type="hidden" value="{{$email}}" name="email" />
<input type="hidden" value="{{$token}}" name="token" />
@csrf
<x-lucent::button-indicator>
Enter as {{$email}}
</x-lucent::button-indicator>
</form>
<div class="form-errors"></div>
</div>
</div>
</div>
@endsection
+43
View File
@@ -0,0 +1,43 @@
@php
$side = $side ?? 48;
$colors = [
"#00AA55",
"#009FD4",
"#B381B3",
"#939393",
"#E3BC00",
"#D47500",
"#DC2A2A",
"#3ede91",
"#377dd4",
"#0256b0",
"#053d82",
"#3d026e",
"#b378e3",
"#c4065c",
"#543208",
"#d97811",
"#0c6b40",
];
$initials = function($name){
$segs = explode(" ",$name);
if(count($segs) > 1){
return strtoupper($segs[0][0]).strtoupper($segs[1][0]);
}
return strtoupper($segs[0][0]).strtoupper($segs[0][1]);
};
$name = $user["name"];
$charIndex = ord($name[1]) + strlen($name);
$colorIndex = $charIndex % 19;
$bgColor = $colors[$colorIndex];
@endphp
<div
class="avatar"
title="{{$name}}"
style="background-color:{{$bgColor}};height: {{$side}}px;width: {{$side}}px; font-size:{{$side / 2}}px"
>
<div class="avatar__letters">{{$initials($user["name"])}}</div>
</div>
@@ -0,0 +1,4 @@
<button class="bt bt-primary">
{{$slot}}
<img alt="indicator" id="indicator" class="htmx-indicator" src="/img/spinner.svg"/>
</button>
+4
View File
@@ -0,0 +1,4 @@
<div class="notice {{$type ?? "info"}}">
<div class="title">{{$title}}</div>
<div class="content">{{ $slot }}</div>
</div>
+9
View File
@@ -0,0 +1,9 @@
@if (count($errors) > 0)
<x-lucent::notice type="error" title="🛑 Submission failed">
<ul>
@foreach ($errors as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</x-lucent::notice>
@endif
+23
View File
@@ -0,0 +1,23 @@
@extends("lucent::layouts.channel")
@section("content")
<h3 class="header-small mb-4">Latest Content changes</h3>
@if($records->isNotEmpty())
<div class="lx-card mb-4">
<div class="lx-table p-0">
<table class="">
<tbody>
@foreach($records as $record)
<tr>
@include("lucent::records.card-row")
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
@endif
@endsection
+21
View File
@@ -0,0 +1,21 @@
<div class="d-flex align-items-center ">
<a class="nav-item" href="/lucent">{{$channel->name}}</a>
<a class="nav-item" href="{channel.lucentUrl}/members">Members</a>
@if($channel->generateCommand)
<a href="{channel.lucentUrl}/build-report" class="btn btn-outline-primary btn-sm d-">Build website</a>
@endif
<!-- <div>-->
<!-- <form method="GET">-->
<!-- <input type="search" name="filter[search_regex]" placeholder="Search"-->
<!-- class="form-control" required/>-->
<!-- </form>-->
<!-- </div>-->
</div>
<div>
<a class="nav-item" href="/lucent/profile">
<x-lucent::avatar side="28" :user="$user"></x-lucent::avatar>
</a>
</div>
@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<html lang="en">
<head>
<meta charset="utf-8">
@@ -7,7 +7,7 @@
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>@yield('title') - Lucent Data Platform</title>
@if(config("lucent.env") == "production")
@if(config("lucent.env") === "production")
<!-- if production -->
<link rel="stylesheet" href="/vendor/lucent/dist/{{ $manifest['main.css']["file"] }}"/>
<script type="module" src="/vendor/lucent/dist/{{ $manifest['main.js']["file"] }}"></script>
@@ -20,14 +20,13 @@
@endif
<link rel="icon" type="image/x-icon" href="/favicon.ico">
{{-- <link rel="icon" type="image/x-icon" href="/favicon.ico"/>--}}
</head>
<body class="view-{{ $view }}">
<div class="mt-5">
<body>
<div>
@yield('content')
</div>
</body>
@@ -22,9 +22,14 @@
</head>
<body class="view-{{ $view }}">
<body>
@include("lucent::includes.header")
<div class="main-wrapper">
@include("lucent::sidebar.sidebar")
<div class="main-content">
@yield('content')
</div>
</div>
</body>
</html>
+33
View File
@@ -0,0 +1,33 @@
@php
$schema = $schemas->where("name",$record->schema)->first();
@endphp
<td>
{{-- {#if schema.type === "files"}--}}
{{-- <Preview {record} size="tiny"/>--}}
{{-- {:else}--}}
<a
href="/lucent/records/{{$record->id}}"
class="text-decoration-none text-dark d-block"
>
{{$viewModel->getRecordName($record, $schemas)}}
</a>
{{$record->status->value === "draft" ? "Draft" : ""}}
{{-- {/if}--}}
</td>
<td><a
class="text-decoration-none lx-small-text"
href="/lucent/content/{{$schema->name}}">{{$schema->label}}</a
>
</td>
<td>
{{-- <div class="d-flex">--}}
{{-- <Avatar name={usernameById(users, record._sys.updatedBy)} side={24}/>--}}
{{-- <div class="ms-2">--}}
{{-- {frieldlyUpdatedAt}--}}
{{-- </div>--}}
{{-- </div>--}}
</td>
@@ -0,0 +1,7 @@
@php
$currentSchema = $currentSchema ?? null;
$activeClass = $schema->name === $currentSchema?->name ? "active" : "";
@endphp
<a class="sidebar-item {{$activeClass}}" aria-current="page"
href="/lucent/content/{{$schema->name}}">{{$schema->label}}</a>
+20
View File
@@ -0,0 +1,20 @@
<div class="sidebar">
<div class="sidebar-header">
Content
</div>
@foreach($schemas->where("type.value", "collection")->where("isEntry",true) as $schema)
@include("lucent::sidebar.sidebar-item", ["schema" => $schema])
@endforeach
<div class="sidebar-header">
Files
</div>
@foreach($schemas->where("type.value", "files") as $schema)
@include("lucent::sidebar.sidebar-item", ["schema" => $schema])
@endforeach
<div class="sidebar-header">
Other
</div>
@foreach($schemas->where("type.value", "collection")->where("isEntry",false) as $schema)
@include("lucent::sidebar.sidebar-item", ["schema" => $schema])
@endforeach
</div>
+4 -8
View File
@@ -119,24 +119,20 @@ readonly class AuthService
}
/**
* @throws LucentException
*/
public
function sendLoginEmail(string $email): void
public function sendLoginEmail(string $email): void
{
$emailAddress = (new Email($email));
$user = $this->userRepo->findByEmail($emailAddress);
if ($user->isEmpty()) {
throw new LucentException("User not found");
return;
}
if ($user->get()->isRemoved()) {
throw new LucentException("Cannot reset email if the user is not active");
return;
}
$newToken = $this->userRepo->updateLoginToken($user->get()->id);
Mail::to($email)->send(
+13 -24
View File
@@ -13,11 +13,13 @@ use Lucent\Account\AuthService;
use Lucent\Channel\ChannelService;
use Lucent\LucentException;
use Lucent\Svelte\Svelte;
use Lucent\Util\Form\FormException;
use Lucent\Util\Form\ResponseFormError;
use function Lucent\Response\fail;
use function Lucent\Response\ok;
class AuthController extends Controller
class AuthController
{
public function __construct(
private readonly AuthService $authService,
@@ -65,52 +67,39 @@ class AuthController extends Controller
return ok();
}
public function login(): View|RedirectResponse
public function login()
{
if ($this->accountService->countUsers() == 0) {
return redirect($this->channelService->channel->lucentUrl . "/register");
}
return $this->svelte->render(
layout: "account",
view: "login",
title: "Log in"
);
return view("lucent::auth.login");
}
public function postLogin(Request $request): Response
public function postLogin(Request $request)
{
try {
$this->authService->sendLoginEmail($request->input("email"));
} catch (LucentException $th) {
return fail($th);
}
return ok();
return view("lucent::auth.login-success");
}
public function verify(Request $request): View
{
return $this->svelte->render(
layout: "account",
view: "verify",
title: "Verify and enter",
data: [
return view("lucent::auth.verify", [
"email" => $request->input("email"),
"token" => $request->input("token"),
]
);
]);
}
public function postVerify(Request $request): Response
public function postVerify(Request $request)
{
try {
$this->authService->login($request->input("email"), $request->input("token"));
} catch (LucentException $th) {
return fail($th);
return ResponseFormError::fromException($th);
}
return ok();
return [];
}
+28 -6
View File
@@ -21,14 +21,36 @@ class HomeController extends Controller
{
}
public function home(): View
public function home(Request $request): View
{
$urlParams = $request->all();
$users = $this->accountService->all();
$sort = data_get($urlParams, "sort") ?? "-_sys.updatedAt";
$filter = data_get($urlParams, "filter") ?? [];
$arguments = array_merge([
"schema_in" => $this->accountService->currentReadableSchemas(),
"status_in" => ["draft", "published"]
], $filter);
$limit = 10;
$graph = $this->query
->filter($arguments)
->limit($limit)
->childrenDepth(1)
->parentsDepth(0)
->sort($sort)
->run();
return view("lucent::home", [
"users" => $users,
"records" => $graph->getRootRecords(),
"graph" => toArray($graph),
"modalUrl" => $request->fullUrl(),
]);
return $this->svelte->render(
layout: "channel",
view: "homeIndex",
title: "Records",
);
}
public function records(Request $request): Response
+9 -3
View File
@@ -4,15 +4,19 @@ namespace Lucent\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\View;
use Lucent\Account\AccountService;
use Lucent\Account\AuthService;
use Lucent\Channel\ChannelService;
use Lucent\ViewModel\ViewModel;
readonly class AuthMiddleware
{
public function __construct(
private AuthService $authService,
private ChannelService $channelService
private AccountService $accountService,
private ChannelService $channelService,
private ViewModel $viewModel
)
{
}
@@ -22,9 +26,11 @@ readonly class AuthMiddleware
if (!$this->authService->isLoggedIn()) {
return redirect($this->channelService->channel->lucentUrl . "/login");
}
$this->authService->refreshSession();
View::share("channel",$this->channelService->channel);
View::share("user",session("user"));
View::share("schemas",$this->channelService->channel->schemas->whereIn("name",$this->accountService->currentReadableSchemas())->values());
View::share("viewModel",$this->viewModel);
return $next($request);
}
+4 -1
View File
@@ -3,6 +3,7 @@
namespace Lucent;
use Illuminate\Routing\Router;
use Illuminate\Support\Facades\Blade;
use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
use Intervention\Image\ImageManager;
@@ -58,7 +59,7 @@ class LucentServiceProvider extends ServiceProvider
$router->aliasMiddleware('lucent.auth', \Lucent\Http\Middleware\AuthMiddleware::class);
$router->aliasMiddleware('lucent.guest', \Lucent\Http\Middleware\GuestMiddleware::class);
$this->loadViewsFrom(__DIR__ . '/Views', 'lucent');
$this->loadViewsFrom(__DIR__ . '/../front/views', 'lucent');
$this->loadRoutesFrom(__DIR__ . '/Http/web.php');
$this->loadRoutesFrom(__DIR__ . '/Http/api.php');
@@ -76,6 +77,7 @@ class LucentServiceProvider extends ServiceProvider
View::share('manifest', $manifest);
View::share('image', app()->make(ImageService::class));
View::share('file', app()->make(FileService::class));
Blade::anonymousComponentPath(__DIR__.'../front/views/components',"lucent");
$this->publishes([
__DIR__ . '/Config/main.php' => config_path('lucent.php'),
@@ -83,6 +85,7 @@ class LucentServiceProvider extends ServiceProvider
$this->publishes([
__DIR__ . '/../front/dist' => public_path('vendor/lucent/dist'),
__DIR__ . '/../front/public' => public_path('vendor/lucent/public'),
], 'lucent');
}
}
+17
View File
@@ -0,0 +1,17 @@
<?php
namespace Lucent\Util\Form;
use Exception;
final class FormException extends Exception
{
// Redefine the exception so message isn't optional
public function __construct(string $message, int $code = 0, Exception $previous = null)
{
// make sure everything is assigned properly
parent::__construct($message, $code, $previous);
}
}
+59
View File
@@ -0,0 +1,59 @@
<?php
namespace Lucent\Util\Form;
use Exception;
use Illuminate\Contracts\Foundation\Application;
use Illuminate\Contracts\Routing\ResponseFactory;
use Illuminate\Http\Response;
use Illuminate\Validation\Validator;
use Lucent\LucentException;
class ResponseFormError
{
/**
* @param $errors list<string>
*/
public static function render(array $errors): ResponseFactory|Application|Response
{
return response(view("lucent::forms.errors", ["errors" => $errors])->render(), 400);
}
/**
* @param Validator $validator
* @return ResponseFactory|Application|Response
*/
public static function fromValidator(Validator $validator): ResponseFactory|Application|Response
{
return self::render($validator->errors()->all());
}
/**
* @param array $errors
* @return ResponseFactory|Application|Response
*/
public static function fromArray(array $errors): ResponseFactory|Application|Response
{
return self::render($errors);
}
/**
* @param string $errorString
* @return ResponseFactory|Application|Response
*/
public static function fromMessage(string $errorString): ResponseFactory|Application|Response
{
return self::render([$errorString]);
}
/**
* @param LucentException|FormException $exception
* @return ResponseFactory|Application|Response
*/
public static function fromException(LucentException|FormException $exception): ResponseFactory|Application|Response
{
return self::render([$exception->getMessage()]);
}
}
+41
View File
@@ -0,0 +1,41 @@
<?php
namespace Lucent\ViewModel;
use Lucent\Channel\ChannelService;
use Lucent\Record\QueryRecord;
use Lucent\Record\Status;
use Lucent\Schema\CollectionSchema;
use Lucent\Schema\FieldInterface;
use Lucent\Schema\FilesSchema;
use Mustache_Engine;
class ViewModel
{
public function __construct(
public ChannelService $channelService
)
{
}
public function getRecordName(QueryRecord $record): string
{
$schema = $this->channelService->getSchema($record->schema)->get();
if (empty($schema->titleTemplate)) {
$title = match (get_class($schema)) {
CollectionSchema::class => $record->data[$schema->fields->filter(fn(FieldInterface $f) => $f->info->name === "text")->first()->name],
FilesSchema::class => $record->_file->path,
};
if (empty(trim($title))) {
return "~Untitled~";
}
return $title;
}
$m = new Mustache_Engine(array('entity_flags' => ENT_QUOTES));
return $m->render($schema->titleTemplate, $record->data);
}
}