A lot of things.

This commit is contained in:
2026-05-27 21:24:38 +02:00
parent b361f07954
commit d02baa89d6
43 changed files with 1340 additions and 231 deletions

View File

@@ -15,6 +15,7 @@
@import './components/database.css';
@import './components/hovercard.css';
@import './components/notifications.css';
@import './components/settings.css';
@import './components/easymde.css';

View File

@@ -29,3 +29,29 @@
--menu-size: 260px;
--menu-user-avatar-bg: #555;
}
.light-mode {
/* RHPZ color */
--rhpz-orange: #ff7300;
--rhpz-orange-hover: #e56700;
/* Background colors */
--bg: #f0f0f0;
--bg2: #ffffff;
--bg3: #e8e8e8;
--bg4: #dcdcdc;
/* Text */
--text: #454545;
--text2: #737373;
--text3: #111111;
/* Elements */
--border: #d0d0d0;
--error: #e57373;
--info: #1976d2;
--success: #81c784;
--success2: #388e3c;
}

View File

@@ -64,6 +64,13 @@
border-bottom: 1px solid var(--border);
padding-bottom: 10px;
}
.block-success {
background-color: var(--success);
border: 1px solid var(--success);
color: var(--text);
padding: 20px;
margin-bottom: 20px;
}
.block-error {
background-color: var(--error);
border: 1px solid var(--error);

View File

@@ -0,0 +1,156 @@
.settings-dropdown {
position: absolute;
top: calc(100% + 8px);
right: 0;
width: 240px;
background-color: var(--bg2);
border: 1px solid var(--border);
box-shadow: 0 8px 24px rgba(0,0,0,0.5);
z-index: 2000;
}
.settings-header {
padding: 12px 16px;
border-bottom: 1px solid var(--border);
background-color: var(--bg3);
font-weight: 600;
font-size: 0.9rem;
color: var(--text);
}
.settings-section {
padding: 12px 16px;
}
.settings-section-title {
display: flex;
align-items: center;
gap: 7px;
font-size: 0.78rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.7px;
color: var(--text2);
margin-bottom: 10px;
}
.settings-separator {
border-top: 1px solid var(--border);
}
.settings-themes {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.settings-theme-btn {
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid transparent;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: var(--text);
transition: transform 0.15s, border-color 0.15s;
padding: 0;
&:hover {
transform: scale(1.15);
}
.active {
border-color: var(--text);
transform: scale(1.1);
}
}
.settings-theme-toggle {
width: 100%;
background-color: var(--bg3);
border: 1px solid var(--border);
color: var(--text);
padding: 8px 12px;
cursor: pointer;
font-family: var(--typography);
font-size: 0.88rem;
transition: background-color 0.1s;
text-align: left;
&:hover {
background-color: var(--bg4);
}
}
.settings-theme-toggle-inner {
display: flex;
align-items: center;
gap: 8px;
}
.settings-theme-toggle-badge {
margin-left: auto;
background-color: var(--rhpz-orange);
color: #111;
font-size: 0.65rem;
font-weight: 700;
padding: 2px 6px;
}
.settings-perpage {
display: flex;
gap: 6px;
}
.settings-perpage-btn {
flex: 1;
padding: 6px 4px;
background-color: var(--bg3);
border: 1px solid var(--border);
color: var(--text2);
font-size: 0.85rem;
cursor: pointer;
font-family: var(--typography);
transition: all 0.1s;
&:hover {
background-color: var(--bg4);
color: var(--text);
}
.active {
background-color: var(--rhpz-orange);
border-color: var(--rhpz-orange);
color: var(--text3);
font-weight: 600;
}
}
.settings-link {
display: flex;
align-items: center;
gap: 9px;
padding: 8px 10px;
color: var(--text);
text-decoration: none;
font-size: 0.88rem;
transition: background-color 0.1s;
border: 1px solid transparent;
&:hover {
background-color: var(--bg3);
border-color: var(--border);
text-decoration: none;
}
}
.settings-link--danger {
color: var(--error);
&:hover {
background-color: rgba(229, 115, 115, 0.08);
border-color: rgba(229, 115, 115, 0.3);
}
}

View File

@@ -1,4 +1,4 @@
#entry-container {
#entry-container, #comments-section, #reviews-section {
background-color: var(--bg2);
border: 1px solid var(--border);
display: flex;
@@ -128,3 +128,112 @@
text-align: center;
color: var( --text2 );
}
.comment-block {
display: flex;
gap: 16px;
padding: 20px 0;
border-bottom: 1px solid var(--border);
&:last-child {
border-bottom: none;
}
.comment-avatar {
flex-shrink: 0;
width: 48px;
height: 48px;
border-radius: 50%;
overflow: hidden;
background-color: var(--bg4);
border: 1px solid var(--border);
img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
}
.comment-content {
flex: 1;
min-width: 0;
.comment-meta {
font-size: 0.88rem;
color: var(--text2);
margin-bottom: 6px;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
.comment-author {
font-weight: 600;
color: var(--text);
text-decoration: none;
transition: color 0.2s;
&:hover {
color: var(--rhpz-orange);
}
}
.comment-date {
color: var(--text2);
}
.comment-separator {
color: var(--border);
user-select: none;
}
}
.comment-body {
font-size: 0.95rem;
color: var(--text);
line-height: 1.5;
word-wrap: break-word;
p {
margin-bottom: 10px;
&:last-child { margin-bottom: 0; }
}
a {
color: var(--rhpz-orange);
&:hover {
color: var(--rhpz-orange-hover);
text-decoration: underline;
}
}
blockquote, .bbCodeBlock-blockquote {
background-color: var(--bg);
border-left: 3px solid var(--info);
padding: 12px 16px;
margin: 12px 0;
font-style: italic;
color: var(--text2);
}
code {
font-family: monospace;
background-color: var(--bg3);
border: 1px solid var(--border);
padding: 2px 5px;
font-size: 0.9rem;
}
}
}
}
.comments-empty {
text-align: center;
padding: 40px 20px;
color: var(--text2);
font-style: italic;
background-color: var(--bg);
border: 1px dashed var(--border);
}

View File

@@ -72,8 +72,9 @@ export function GalleryManager() {
break;
const IMG = GalleryImage();
IMG.getOldImage( PATH );
IMG.serverFilePath = PATH;
this.images.push(IMG);
this.images[this.images.length - 1].getOldImage( PATH );
}
},

View File

@@ -2,6 +2,12 @@ export function MainImageManager() {
return {
/**
* Used for gallery managament and indexation.
* @type {string}
*/
key: crypto.randomUUID(),
/**
* If an image has been uploaded or not.
* @type {boolean}

View File

@@ -6,7 +6,16 @@ import { calculate as calculateHashes } from "./hashes.js";
import hovercard from "./hovercard.js";
import notifications from "./notifications.js";
import conversations from "./conversations.js";
import settings from "./settings.js";
/**
* Get config defined in meta.blade.php
* @param {string} key
* @return {string|null}
*/
window.getConfig = function( key ){
return document.querySelector('meta[name="config-' + key + '"]').getAttribute('content') ?? null;
}
// Lucide icons.
createIcons({ icons });
@@ -31,3 +40,6 @@ Alpine.store('notifications', notifications() );
// Conversations
Alpine.store('conversations', conversations() );
// Settings
Alpine.store('settings', settings() );

83
resources/js/settings.js Normal file
View File

@@ -0,0 +1,83 @@
import Cookies from 'js-cookie';
export default function settings() {
return {
/**
* @type {boolean}
*/
start: false,
/**
* Two keys, default and alternate.
* @type {Object}
*/
xfUrls: {},
/**
* @type {number[]}
*/
entriesPerPage: [ 12, 30, 48 ],
/**
* @type {string}
*/
currentTheme: Cookies.get("theme") ?? 'default',
/**
* @type {number}
*/
currentEntriesPerPage: Cookies.get("entries_per_page") ?? 30,
/**
*
* @param {string} newTheme default|alternate
*/
themeChanged( newTheme ){
if( newTheme !== "default" && newTheme !== "alternate" )
return;
if( newTheme === this.currentTheme )
return;
this.currentTheme = newTheme;
document.documentElement.classList.toggle('light-mode', this.currentTheme === 'alternate');
Cookies.set('theme', this.currentTheme, { expires: 365, path: '/', domain: window.getConfig('session-domain') } );
this.syncXF();
},
/**
*
* @return {Promise<void>}
*/
async syncXF(){
await fetch(this.xfUrls[this.currentTheme ?? 'default'], { method: "GET", credentials: "include", mode: "no-cors" });
},
/**
*
*/
toggleTheme(){
this.themeChanged(this.currentTheme === 'default' ? 'alternate' : 'default');
},
/**
*
* @param n
*/
entriesPerPageChanged( n ){
if( !this.entriesPerPage.includes(n) )
return;
this.entriesPerPage = n;
Cookies.set('entries_per_page', this.entriesPerPage, { expires: 365, path: '/', domain: window.getConfig('session-domain') } );
if( window.Livewire ){
Livewire.dispatch('entriesPerPageChanged', {n});
}
},
open(){ this.start = !this.start; },
close(){ this.start = false; },
}
}

View File

@@ -8,7 +8,7 @@
@endif
</div>
<div class="entry-card-info">
<div class="entry-card-title">{{ $entry->title }}</div>
<a href="{{ route('entries.show', [ 'section' => $entry->type, 'entry' => $entry ] ) }}" class="entry-card-title">{{ $entry->title }}</a>
<div class="entry-card-author">
@forelse( $entry->authors as $author)
@if($loop->first)By @endif
@@ -24,13 +24,13 @@
@foreach( $entry->modifications as $modif )
<span class="badge orange">{{ $modif->name }}</span>
@endforeach
@if( $entry->status_id )
<span class="badge">{{ $entry->status->name }}</span>
@endif
@foreach( $entry->languages as $lang )
<span class="badge">{{ $lang->name }}</span>
@endforeach
@endif
@if( $entry->status_id )
<span class="badge">{{ $entry->status->name }}</span>
@endif
@foreach( $entry->languages as $lang )
<span class="badge">{{ $lang->name }}</span>
@endforeach
</div>
<div class="entry-card-meta">
<span><i data-lucide="download" size="12"></i> x</span>

View File

@@ -14,7 +14,7 @@
</div>
</div>
<div class="form-gallery form-group level" style="flex:4;">
<template x-for="(image,i) in images" :key="image.serverFilePath">
<template x-for="(image,i) in images" :key="image.key">
<div class="gallery-item">
<div class="form-image-preview-wrap">
<img :src="image.preview" :alt="image.name">
@@ -27,7 +27,7 @@
</div>
</div>
<template x-for="(image, i) in images" :key="image.serverFilePath">
<template x-for="(image, i) in images" :key="image.key">
<input type="hidden" name="gallery[]" :value="image.serverFilePath">
</template>

View File

@@ -1,7 +1,7 @@
<?php /** @var \App\Models\Language $language */ ?>
<div class="languages-selector form-group level" x-data="{
search: '',
selected: {{ JS::from( (array) $selected ) }},
selected: @js((array) $selected),
toggle(value){
const i = this.selected.indexOf(value);
i === -1 ? this.selected.push(value) : this.selected.splice(i,1);

View File

@@ -0,0 +1,72 @@
<div
x-data
x-show="$store.settings.start"
x-cloak
class="settings-dropdown"
@keydown.escape.window="$store.settings.close()"
>
<div class="settings-header">
<span>Settings</span>
</div>
<div class="settings-section">
<div class="settings-section-title">
<i data-lucide="sun-moon" size="14"></i>
Theme
</div>
<button type="button" class="settings-theme-toggle" @click="$store.settings.toggleTheme()">
<template x-if="$store.settings.currentTheme === 'default'">
<span class="settings-theme-toggle-inner">
<i data-lucide="moon" size="15"></i>
Dark
<span class="settings-theme-toggle-badge">ON</span>
</span>
</template>
<template x-if="$store.settings.currentTheme === 'alternate'">
<span class="settings-theme-toggle-inner">
<i data-lucide="sun" size="15"></i>
Light
<span class="settings-theme-toggle-badge">ON</span>
</span>
</template>
</button>
</div>
<div class="settings-separator"></div>
<div class="settings-section">
<div class="settings-section-title">
<i data-lucide="layout-grid" size="14"></i>
Entries per page
</div>
<div class="settings-perpage">
<template x-for="n in $store.settings.entriesPerPage" :key="n">
<button
type="button"
class="settings-perpage-btn"
:class="{ 'active': $store.settings.currentEntriesPerPage == n }"
@click="$store.settings.entriesPerPageChanged(n)"
x-text="n"
></button>
</template>
</div>
<div class="settings-separator"></div>
@auth
<div class="settings-section">
<div class="settings-section-title">
<i data-lucide="user-cog" size="14"></i>
Account
</div>
<a href="{{ xfRoute('account/account-details') }}" class="settings-link">
<i data-lucide="settings-2" size="14"></i>
XenForo settings
<i data-lucide="external-link" size="12" style="margin-left:auto"></i>
</a>
</div>
@endauth
</div>
</div>

View File

@@ -1,4 +1,4 @@
@php $topbarModSeparator = false; $topBarAdminSeparator = false; @endphp
@php $topbarModSeparator = false; $topbarAdminSeparator = false; @endphp
<header id="topbar">
<button class="mobile-toggle">
<i data-lucide="menu"></i>
@@ -43,11 +43,11 @@
@endif
{{-- Users --}}
@if( !\Auth::guest() && \Auth::user()->can('romhackplaza', 'canSubmitEntry') )
@can('create','\App\Models\Entry')
<a href="#" class="btn">
<i data-lucide="hard-drive-upload" size="18"></i>
</a>
@endif
@endcan
@if( !\Auth::guest() )
<div x-data x-init="$store.notifications.unviewed = {{ \Auth::user()->alerts_unviewed }}" style="position:relative">
<button type="button" class="btn" :class="{ 'active': $store.notifications.start }" @click="$store.notifications.open($el)" @click.outside="$store.notifications.close()">
@@ -76,9 +76,19 @@
@include('components.conversations')
</div>
@endif
<button class="btn">
<i data-lucide="settings" size="18"></i>
</button>
<div x-data style="position: relative;" x-init="$store.settings.xfUrls = { 'default': '{{ xfStyleVariationUrl( 'default' ) }}', 'alternate': '{{ xfStyleVariationUrl( 'alternate' ) }}' }">
<button
type="button"
class="btn"
:class="{ 'active': $store.settings.start }"
@click="$store.settings.open()"
@click.outside="$store.settings.close()"
>
<i data-lucide="settings" size="18"></i>
</button>
@include('components.settings-dropdown')
</div>
</div>
</header>

View File

@@ -0,0 +1,38 @@
<div class="grid-c2" style="margin-top:1%;">
<div id="reviews-section">
<div class="entry-content">
<x-entry-section-title label="Reviews" icon="star" />
</div>
</div>
<div id="comments-section">
<div class="entry-content">
<x-entry-section-title label="Last comments" icon="message-circle" />
@forelse( $comments as $comment )
@if($comment['user_id'] === config('xenforo.bot_user_id') && $comment['position'] == 0)
@continue
@else
<div class="comment-block">
<x-xen-foro-avatar :user="$comment['user_id']" />
<div class="comment-content">
<div class="comment-meta">
<span class="comment-author">{{ $comment['User']['username'] }}</span>
<span class="comment-separator"></span>
<span class="comment-date">{{ date('Y-m-d', $comment['post_date']) }}</span>
</div>
<div class="comment-body">
{!! $comment['message'] !!}
</div>
</div>
</div>
@endif
@empty
<span class="whisper">Be the first to post a comment</span>
@endforelse
<a href="{{ xfRoute("threads/{$entry->comments_thread_id}/latest") }}" class="btn primary" style="margin-top: 1%;">
<i data-lucide="pen"></i> Post a comment
</a>
</div>
</div>
</div>

View File

@@ -59,9 +59,19 @@
<button class="btn primary" onclick="Livewire.dispatch('entryOpenFilesModal', { entryId: {{ $entry->id }} })">
<i data-lucide="download"></i> Download
</button>
@can('update',$entry)
<a href="{{ route('submit.edit', ['section' => $entry->type, 'entry' => $entry ] ) }}" class="btn primary">
<i data-lucide="edit"></i> Edit
</a>
@endcan
<button class="btn">
<i data-lucide="message-square"></i> Comments
</button>
@auth
<a href="{{ xfRoute("romhackplaza_entry/{$entry->id}/report") }}" class="btn">
<i data-lucide="flag"></i> Report / Claim Ownership
</a>
@endauth
</div>
</div>
</div>
@@ -94,5 +104,6 @@
@endif
</div>
</article>
@include('entries.comments')
@livewire('entry-files-modal')
@endsection

View File

@@ -1,8 +1,9 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="{{ \Illuminate\Support\Facades\Cookie::get('theme', 'default') === 'alternate' ? 'light-mode' : '' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
@include('meta')
@vite(['resources/css/app.css', 'resources/js/app.js'])
@livewireStyles
@stack('styles')
@@ -15,14 +16,14 @@
<main id="main-wrapper">
@include('components.topbar')
@if(session('success'))
<x-success-block success-type="custom" :message="session('success')" />
@endif
@if(session('error'))
<x-error-block error-type="custom" :message="session('error')" />
@endif
<div id="content">
@if(session('success'))
<x-success-block success-type="custom" :message="session('success')" />
@endif
@if(session('error'))
<x-error-block error-type="custom" :message="session('error')" />
@endif
@yield('content')
</div>
</main>

View File

@@ -0,0 +1 @@
<meta name="config-session-domain" content="{{ config('session.domain') }}">

View File

@@ -84,6 +84,12 @@
@if( section_must_be( 'translations', $section ) )
<x-form-field-title name="Languages" required="true" />
<x-languages-selector :selected="$oldLanguages" />
@error('languages')
<x-form-error-text message="{{ $message }}" />
@enderror
@error('languages.*')
<x-form-error-text message="{{ $message }}" />
@enderror
@endif
<div class="form-group" x-ref="descriptionField">