Club System

This commit is contained in:
2026-06-02 20:54:10 +02:00
parent c68c4d18b5
commit 0b18d289ef
38 changed files with 1464 additions and 118 deletions

View File

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

View File

@@ -35,3 +35,5 @@ ul {
margin-bottom: 20px;
list-style-type: square;
}
[x-cloak] {display: none !important;}

View File

@@ -459,3 +459,72 @@
color: var(--text2);
white-space: nowrap;
}
.upload-item-actions {
display: flex;
flex-direction: row;
gap: 15px;
}
.file-state-icon { width: 18px; height: 18px; }
.file-state-icon--public { color: var(--success); }
.file-state-icon--private { color: var(--text2); }
.file-state-icon--archived { color: var(--error); }
.upload-item-state { display: flex; align-items: center; gap: 8px; }
.author-search { position: relative; }
.author-search-input {
display: flex;
align-items: center;
gap: 8px;
background-color: var(--bg);
border: 1px solid var(--border);
padding: 8px 12px;
}
.author-search-input .form-input {
border: none;
padding: 0;
background: none;
flex: 1;
}
.author-search-selected {
display: flex;
align-items: center;
gap: 5px;
color: var(--success);
font-size: 0.85rem;
white-space: nowrap;
}
.author-search-dropdown {
position: absolute;
top: 100%;
left: 0;
right: 0;
background-color: var(--bg2);
border: 1px solid var(--border);
border-top: none;
z-index: 100;
max-height: 200px;
overflow-y: auto;
}
.author-search-item {
display: flex;
align-items: center;
gap: 9px;
width: 100%;
padding: 9px 12px;
background: none;
border: none;
color: var(--text);
font-size: 0.88rem;
cursor: pointer;
text-align: left;
font-family: var(--typography);
transition: background-color 0.1s;
}
.author-search-item:hover { background-color: var(--bg3); }

View File

@@ -0,0 +1,187 @@
.queue-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 15px;
padding: 80px 20px;
color: var(--text2);
font-size: 0.95rem;
}
.queue-item {
background-color: var(--bg2);
border: 1px solid var(--border);
border-left-width: 4px;
margin-bottom: 20px;
padding: 20px;
}
.queue-item--pending {
border-left-color: var(--rhpz-orange);
}
.queue-item--rejected {
position: relative;
overflow: hidden;
border-left-color: var(--error);
&::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
height: 2px;
background-color: var(--error);
width: var(--reject-progress, 100%);
opacity: 0.5;
transition: width 0.3s;
}
}
.queue-item-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
gap: 15px;
margin-bottom: 20px;
}
.queue-item-title {
font-size: 1.15rem;
font-weight: 600;
color: var(--text);
margin-bottom: 6px;
}
.queue-item-meta {
font-size: 0.85rem;
color: var(--text2);
display: flex;
align-items: center;
gap: 6px;
flex-wrap: wrap;
}
.queue-item-actions-header {
display: flex;
gap: 8px;
flex-shrink: 0;
}
.timeline-container {
padding: 15px 20px;
background-color: var(--bg);
border: 1px solid var(--border);
margin-bottom: 20px;
}
.timeline {
display: flex;
justify-content: space-between;
position: relative;
&::before {
content: '';
position: absolute;
top: 15px;
left: 30px;
right: 30px;
height: 2px;
background-color: var(--border);
z-index: 0;
}
}
.timeline-step {
display: flex;
flex-direction: column;
align-items: center;
position: relative;
z-index: 1;
width: 100px;
}
.timeline-dot {
width: 32px;
height: 32px;
border-radius: 50%;
background-color: var(--bg);
border: 2px solid var(--border);
display: flex;
align-items: center;
justify-content: center;
color: var(--text2);
margin-bottom: 10px;
transition: all 0.2s;
}
.timeline-step--active .timeline-dot {
border-color: var(--rhpz-orange);
background-color: var(--rhpz-orange);
color: #111;
}
.timeline-step--validated .timeline-dot {
border-color: var(--success);
background-color: var(--success);
color: #111;
}
.timeline-step--rejected .timeline-dot {
border-color: var(--error);
background-color: var(--error);
color: #fff;
}
.timeline-label {
font-size: 0.78rem;
text-align: center;
color: var(--text2);
font-weight: 500;
}
.timeline-step--active .timeline-label { color: var(--rhpz-orange); font-weight: 600; }
.timeline-step--validated .timeline-label { color: var(--success); }
.timeline-step--rejected .timeline-label { color: var(--error); }
.queue-reject-reason {
display: flex;
gap: 10px;
align-items: flex-start;
padding: 12px 15px;
background-color: rgba(229, 115, 115, 0.08);
border-left: 2px solid var(--error);
color: var(--text);
font-size: 0.88rem;
margin-bottom: 15px;
line-height: 1.5;
}
.queue-mod-zone {
border-top: 1px solid var(--border);
padding-top: 15px;
margin-top: 5px;
display: flex;
flex-direction: column;
gap: 15px;
}
.queue-mod-separator {
border-top: 1px solid var(--border);
}
.queue-mod-actions {
display: flex;
gap: 8px;
justify-content: flex-end;
margin-top: 10px;
}
.queue-reject-form {
margin-top: 12px;
padding: 15px;
background-color: var(--bg3);
border: 1px solid var(--border);
}

View File

@@ -106,6 +106,7 @@
display: grid;
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
gap: 15px;
margin-bottom: 30px;
}
.entry-gallery-item {
@@ -116,10 +117,81 @@
align-items: center;
justify-content: center;
cursor: pointer;
overflow: hidden;
transition: border-color 0.2s;
&:hover {
border-color: var(--rhpz-orange);
img {
transform: scale(1.05);
}
}
img {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.3s ease;
}
}
}
.gallery-modal {
position: fixed;
z-index: 3000;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
display: flex;
align-items: center;
justify-content: center;
.gallery-modal-content {
max-width: 90%;
max-height: 90%;
position: relative;
img {
max-width: 100%;
max-height: 90vh;
object-fit: contain;
border: 2px solid var(--border);
background-color: var(--bg);
box-shadow: 0 10px 25px rgba(0,0,0,0.5);
}
}
.gallery-modal-close {
position: absolute;
top: 20px;
right: 30px;
color: #fff;
font-size: 40px;
font-weight: bold;
cursor: pointer;
user-select: none;
&:hover {
color: var(--rhpz-orange);
}
}
.gallery-modal-video {
width: 90%;
max-width: 960px;
aspect-ratio: 16/9;
box-shadow: 0 10px 30px rgba(0,0,0,0.6);
border: 1px solid var(--border);
background-color: #000;
iframe {
width: 100%;
height: 100%;
border: none;
display: block;
}
}
}
@@ -237,3 +309,74 @@
background-color: var(--bg);
border: 1px dashed var(--border);
}
.entry-video-section {
margin-bottom: 30px;
}
.video-thumbnail-wrapper {
position: relative;
width: 100%;
max-width: 500px;
aspect-ratio: 16/9;
background-color: #000;
border: 1px solid var(--border);
cursor: pointer;
overflow: hidden;
border-radius: 4px;
img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.8;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.play-trigger {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 65px;
height: 65px;
background-color: rgba(0, 0, 0, 0.7);
border: 2px solid #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 1.5rem;
transition: background-color 0.2s, transform 0.2s ease-out;
i { margin-left: 4px; }
}
&:hover {
img {
transform: scale(1.03);
opacity: 1;
}
.play-trigger {
background-color: var(--rhpz-orange);
border-color: var(--rhpz-orange);
transform: translate(-50%, -50%) scale(1.1);
box-shadow: 0 0 15px rgba(255, 115, 0, 0.5);
}
}
}
.entry-submission-byline {
font-size: 0.85rem;
color: var(--text2);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 15px;
i {
vertical-align: middle;
margin-right: 4px;
}
}

View File

@@ -61,93 +61,98 @@ export function FSFileData(name, totalChunks, rawFile ) {
*/
uuid: crypto.randomUUID(),
/**
* Look if this file is currently uploading.
* @returns {boolean}
*/
get isUploading()
{
return !this.done && !this.error;
},
/**
* Build API url.
* @param {string} section
* @returns {string} The API url.
*/
buildUrl(section)
{
return `/api/fs/upload-chunk/${section}`;
},
/**
* Upload the file.
* @param {string} section section of the file.
* @returns {Promise<void>}
*/
async upload(section)
{
if (!this.rawFile)
return; // Can't upload in that case.
/**
* Current file state
*/
state: 'public',
/**
* Get CSRF token for uploading request.
* @type {string}
* Look if this file is currently uploading.
* @returns {boolean}
*/
const CSRF = document.querySelector('meta[name=csrf-token]')?.content ?? '';
get isUploading()
{
return !this.done && !this.error;
},
for (let i = 0; i < this.totalChunks; i++) {
/**
* Build API url.
* @param {string} section
* @returns {string} The API url.
*/
buildUrl(section)
{
return `/api/fs/upload-chunk/${section}`;
},
if (this.error)
return; // Abort the process.
/**
* Upload the file.
* @param {string} section section of the file.
* @returns {Promise<void>}
*/
async upload(section)
{
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, this.rawFile.size);
const chunk = this.rawFile.slice(start, end);
if (!this.rawFile)
return; // Can't upload in that case.
const formData = new FormData();
/**
* Get CSRF token for uploading request.
* @type {string}
*/
const CSRF = document.querySelector('meta[name=csrf-token]')?.content ?? '';
formData.append('file', chunk);
formData.append('file_uuid', this.uuid);
formData.append('current_chunk', i);
formData.append('total_chunks', this.totalChunks);
formData.append('filename', this.rawFile.name);
formData.append('_token', CSRF);
for (let i = 0; i < this.totalChunks; i++) {
// -----
// UPLOAD TIME !
// -----
if (this.error)
return; // Abort the process.
try {
const RESPONSE = await fetch(this.buildUrl(section), {method: 'POST', body: formData});
const start = i * CHUNK_SIZE;
const end = Math.min(start + CHUNK_SIZE, this.rawFile.size);
const chunk = this.rawFile.slice(start, end);
if (!RESPONSE.ok) // Problem with the request.
throw new Error(`${RESPONSE.status} ${RESPONSE.statusText}`);
const formData = new FormData();
/** @type {UploadchunkResponse} */
const DATA = await RESPONSE.json();
formData.append('file', chunk);
formData.append('file_uuid', this.uuid);
formData.append('current_chunk', i);
formData.append('total_chunks', this.totalChunks);
formData.append('filename', this.rawFile.name);
formData.append('_token', CSRF);
if (DATA.success !== true || DATA.uploaded !== true)
// The request reached the file server but could not be sent.
throw new Error(`${DATA.error}`);
// -----
// UPLOAD TIME !
// -----
this.currentChunk = i + 1;
this.progressValue = Math.round(((i + 1) / this.totalChunks) * 100);
try {
const RESPONSE = await fetch(this.buildUrl(section), {method: 'POST', body: formData});
if (DATA.finished === true) {
this.done = true;
if (!RESPONSE.ok) // Problem with the request.
throw new Error(`${RESPONSE.status} ${RESPONSE.statusText}`);
/** @type {UploadchunkResponse} */
const DATA = await RESPONSE.json();
if (DATA.success !== true || DATA.uploaded !== true)
// The request reached the file server but could not be sent.
throw new Error(`${DATA.error}`);
this.currentChunk = i + 1;
this.progressValue = Math.round(((i + 1) / this.totalChunks) * 100);
if (DATA.finished === true) {
this.done = true;
return;
}
} catch (err) {
this.error = 'Error on chunk ' + (i + 1) + '. ' + err.message;
this.progressValue = 0;
return;
}
} catch (err) {
this.error = 'Error on chunk ' + (i + 1) + '. ' + err.message;
this.progressValue = 0;
return;
}
}
}
}

View File

@@ -127,6 +127,10 @@ export function FSUploader(){
*/
handleRemoveFile( index ){
this.files.splice(index, 1);
},
changeFileState( index, newState ){
this.files[index].state = newState;
}
}

View File

@@ -10,7 +10,7 @@
}
}" x-init="init()">
<div>
@if( section_must_be( [ 'romhacks', 'homebrew' ], $section ) )
@if( section_must_be( [ 'romhacks', 'homebrew' ], $section ) && !$isEdit )
<label class="nsfw-label"><input id="nsfw-checkbox" type="checkbox" name="nsfw-entry" x-model="nsfw" style="transform: scale(1.5)"> NSFW</label>
@endif
</div>

View File

@@ -1,3 +1,4 @@
<?php /** @var \App\Models\EntryGallery $galleryItem */ ?>
@extends('layouts.app')
@section('page-title', $entry->title . " - " . config('app.name') )
@@ -19,6 +20,37 @@
<div class="entry-info">
<h1 class="entry-title">
{{ $entry->title }}
@if( $entry->state === 'pending' )
<div style="display:inline;color:var(--rhpz-orange);">
-
<i data-lucide="clock" size="24"></i>
Pending approval
</div>
@elseif( $entry->state === 'rejected' )
<div style="display:inline;color:var(--error);">
-
<i data-lucide="x-circle" size="24"></i>
Rejected
</div>
@elseif( $entry->state === 'locked' )
<div style="display:inline;color:var(--error);">
-
<i data-lucide="lock" size="24"></i>
Locked
</div>
@elseif( $entry->state === 'draft' )
<div style="display:inline;color:var(--rhpz-orange);">
-
<i data-lucide="scissors" size="24"></i>
Draft
</div>
@elseif( $entry->state === 'hidden' )
<div style="display:inline;color:var(--rhpz-orange);">
-
<i data-lucide="hide" size="24"></i>
Hidden
</div>
@endif
</h1>
<div class="entry-authors">
@forelse( $entry->authors as $author)
@@ -29,6 +61,26 @@
No authors
@endforelse
</div>
<div class="entry-submission-byline">
@if($entry->user_id)
<span>
<i data-lucide="user" size="14"></i>
Posted by <x-xf-username-link :user-id="$entry->user_id" />
</span>
@endif
<span>
<i data-lucide="calendar" size="14"></i>
{{ $entry->created_at->format('M d, Y') }}
</span>
@if($entry->updated_at && $entry->updated_at->gt($entry->created_at))
<span>
<i data-lucide="file-edit" size="14"></i>
Updated {{ $entry->updated_at->diffForHumans() }}
</span>
@endif
</div>
<div class="entry-meta-grid">
@if( $entry->game )
<x-entry-meta-item label="Game Name" value="{{ $entry->game->name }}" />
@@ -55,18 +107,78 @@
<x-entry-meta-item label="Type of hack" value="{{ $entry->modifications->pluck('name')->implode(', ') }}" route="none" />
@endif
</div>
<div class="hack-actions">
<div class="hack-actions" style="display:flex;gap:10px;">
@if($entry->state === 'pending')
@can('approve', $entry)
<div x-data="{ rejectOpen: false }">
<form action="{{ route('queue.approve', $entry) }}" method="POST" style="display:inline">
@csrf
@method('PATCH')
<button type="submit" class="btn success" onclick="return confirm('Approve this entry?')">
<i data-lucide="check-circle" size="14"></i>
Approve
</button>
</form>
<button type="button" class="btn danger" style="margin-right:15px;" @click="rejectOpen = !rejectOpen">
<i data-lucide="x-circle" size="14"></i>
Reject
</button>
<div
class="modal-overlay"
x-cloak
x-show="rejectOpen"
x-transition.opacity
@click.self="rejectOpen = false"
@keydown.escape.window="rejectOpen = false"
@modal:opened.window="refreshIcons($el)"
>
<div class="modal-window" x-show="rejectOpen" x-transition>
<div class="modal-header">
<span class="modal-title">Reject entry</span>
<button type="button" class="modal-close" @click="rejectOpen = false">
<i data-lucide="x" size="20"></i>
</button>
</div>
<div class="modal-body">
<form action="{{ route('queue.reject', $entry) }}" method="POST">
@csrf
@method('PATCH')
<div class="form-group">
<x-form-field-title name="Rejection reason" required="true" />
<textarea
class="form-input"
name="reason"
rows="4"
placeholder="Explain why this entry is being rejected..."
required
></textarea>
</div>
<div class="queue-mod-actions">
<button type="button" class="btn" @click="rejectOpen = false">
Cancel
</button>
<button type="submit" class="btn danger">
<i data-lucide="x-circle" size="14"></i>
Confirm rejection
</button>
</div>
</form>
</div>
</div>
</div>
</div>
@endcan
@endif
<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">
<a href="{{ route('submit.edit', ['section' => $entry->type, 'entry' => $entry ] ) }}" class="btn">
<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
@@ -95,11 +207,55 @@
</div>
@endif
@if( $entry->staff_credits )
<h2 class="entry-section-title">
<i data-lucide="users-round"></i> Staff credits
</h2>
<x-entry-section-title label="Staff Credits" icon="users-round" />
<div class="entry-description">
{{ $entry->staff_credits }}
<ul>
@foreach( $entry->parseStaffCredits() as $item )
<li>{{ $item['name'] }}: {{ $item['description'] }}</li>
@endforeach
</ul>
</div>
@endif
@if( $entry->gallery->isNotEmpty() )
<div x-data="{ open: false, currentImage: ''}" x-cloak>
<x-entry-section-title label="Gallery" icon="images" />
<div class="entry-gallery">
@foreach( $entry->gallery as $galleryItem )
<div class="entry-gallery-item" @click="currentImage = '{{ Storage::url($galleryItem->image) }}'; open = true; "><img src="{{ Storage::url($galleryItem->image) }}"></div>
@endforeach
</div>
<div class="gallery-modal" x-show="open" x-transition.opacity.duration.300ms @click="open = false" @keydown.escape.window="open = false">
<span class="gallery-modal-close" @click="open = false;"><i data-lucide="x"></i></span>
<div class="gallery-modal-content" @click.stop>
<img :src="currentImage">
</div>
</div>
</div>
@endif
@if( $entry->relevant_link )
<x-entry-section-title label="Relevant Link" icon="link" />
<div class="entry-description">
<a href="{{ $entry->relevant_link }}" target="_blank">{{ $entry->relevant_link }}</a>
</div>
@endif
@if( $entry->getYoutubeVideoId() )
<div x-data="{open: false, src: ''}" x-cloak class="youtube-section">
<x-entry-section-title label="Youtube Video" icon="play" />
<div class="video-thumbnail-wrapper" @click="src = 'https://www.youtube.com/embed/{{ $entry->getYoutubeVideoId() }}?autoplay=1'; open = true">
<img src="https://img.youtube.com/vi/{{ $entry->youtube_id }}/maxresdefault.jpg">
<div class="play-trigger">
<i data-lucide="play"></i>
</div>
</div>
<div class="gallery-modal" x-show="open" x-transition.opacity.duration.300ms @click="open = false; src = ''" @keydown.escape.window="open = false; src = ''">
<span class="gallery-modal-close" @click="open = false; src = '';"><i data-lucide="x"></i></span>
<div class="gallery-modal-video" @click.stop>
<iframe :src="src" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>
</div>
</div>
</div>
@endif
</div>

View File

@@ -0,0 +1,31 @@
<div class="author-search" x-data>
<div class="author-search-input">
<i data-lucide="search" size="14"></i>
<input type="text" class="form-input" wire:model.live.debounce.300ms="search" autocomplete="off">
@if($selected)
<span class="author-search-selected">
<i data-lucide="check" size="13"></i>
{{ $selectedUsername }}
</span>
<button type="button" class="btn" wire:click="$set('selected', null); $set('search', '')">
Remove
</button>
@endif
@if($selected)
<input type="hidden" name="owner_user_id" value="{{ $selected }}">
@endif
@if(count($this->results) > 0)
<div class="author-search-dropdown">
@foreach($this->results as $user)
<button
type="button"
class="author-search-item"
wire:click="selectUser({{ $user->user_id }}, '{{ addslashes($user->username) }}')"
>
{{ $user->username }}
</button>
@endforeach
</div>
@endif
</div>
</div>

View File

@@ -0,0 +1,19 @@
@extends('layouts.app')
@section('page-title', "Submissions Queue - " . config('app.name') )
@section('content')
<div class="page-title">
Submissions Queue
</div>
@if($entries->isEmpty())
<div class="queue-empty">
<i data-lucide="inbox" size="48"></i>
<p>No pending submissions.</p>
</div>
@else
@foreach($entries as $entry)
@include('queue.item', ['entry' => $entry ] )
@endforeach
@endif
@endsection

View File

@@ -0,0 +1,155 @@
<div class="queue-item
{{ $entry->state === 'rejected' ? 'queue-item--rejected' : 'queue-item--pending' }}
"
@if($entry->state === 'rejected')
style="--reject-progress: {{ min(100, (now()->diffInDays($entry->rejected_at) / 7) * 100) }}%"
@endif
>
<div class="queue-item-header">
<div class="queue-item-info">
<h3 class="queue-item-title">{{ $entry->complete_title }}</h3>
@if($entry->state === 'rejected')
<span class="badge badge--danger">
<i data-lucide="x-circle" size="12"></i>
Rejected
@php
$daysLeft = intval(7 - now()->diffInDays($entry->rejected_at));
@endphp
@if($daysLeft > 0)
- deleted in {{ $daysLeft }} days
@endif
</span>
@endif
<div class="queue-item-meta">
Submitted by <x-xf-username-link :user-id="$entry->user_id" />
on {{ $entry->created_at->format('Y-m-d') }}
<span class="badge {{ $entry->type }}">{{ \App\View\Components\EntryCard::ENTRY_TYPES_BADGE[$entry->type] ?? $entry->type }}</span>
</div>
</div>
@can('manageButtonsInQueue',$entry)
<div class="queue-item-actions-header">
<a href="{{ route('entries.show', ['section' => $entry->type, 'entry' => $entry ]) }}" class="btn" target="_blank">
<i data-lucide="external-link" size="14"></i>
View Entry
</a>
<a href="{{ route('submit.edit', ['section' => $entry->type, 'entry' =>$entry ] ) }}" class="btn" target="_blank">
<i data-lucide="pen" size="14"></i>
Edit
</a>
</div>
@endcan
</div>
<div class="timeline-container">
<div class="timeline">
<div class="timeline-step timeline-step--validated">
<div class="timeline-dot">
<i data-lucide="check" size="16"></i>
</div>
<span class="timeline-label">Submitted</span>
</div>
<div class="timeline-step {{ $entry->state === 'pending' ? 'timeline-step--active' : 'timeline-step--validated' }}">
<div class="timeline-dot">
@if($entry->state === 'pending')
<i data-lucide="search" size="16"></i>
@else
<i data-lucide="check" size="16"></i>
@endif
</div>
<span class="timeline-label">Under review</span>
</div>
@if($entry->state === 'rejected')
<div class="timeline-step timeline-step--rejected">
<div class="timeline-dot">
<i data-lucide="x" size="16"></i>
</div>
<span class="timeline-label">Rejected</span>
</div>
@else
<div class="timeline-step {{ $entry->state === 'published' ? 'timeline-step--validated' : '' }}">
<div class="timeline-dot">
@if($entry->state === 'published')
<i data-lucide="check" size="16"></i>
@endif
</div>
<span class="timeline-label">Approved</span>
</div>
@endif
</div>
</div>
@if($entry->state === 'rejected' && $entry->staff_comment)
<div class="queue-reject-reason">
<i data-lucide="alert-circle" size="14"></i>
<div>
<strong>Rejection reason :</strong>
{{ $entry->staff_comment }}
</div>
</div>
@endif
@can('approve',$entry)
@if($entry->state === 'pending')
<div class="queue-mod-zone">
<form action="{{ route('queue.comment', $entry ) }}" method="POST">
@csrf
@method('PATCH')
<div class="form-group">
<x-form-field-title name="Comment" />
<textarea class="form-input" name="comment" rows="3">{{ $entry->staff_comment }}</textarea>
</div>
<div class="queue-mod-actions">
<button type="submit" class="btn">
<i data-lucide="save" size="14"></i>
Save comment
</button>
</div>
</form>
<div class="queue-mod-separator"></div>
<form action="{{ route('queue.approve', $entry) }}" method="POST" style="display:inline">
@csrf
@method('PATCH')
<button type="submit" class="btn success" onclick="return confirm('Approve this entry?')">
<i data-lucide="check-circle" size="14"></i>
Approve
</button>
</form>
<div x-data="{open: false}">
<button type="button" class="btn danger" @click="open = !open">
<i data-lucide="x-circle" size="14"></i>
Reject
</button>
<div x-show="open" x-cloak class="queue-reject-form">
<form action="{{ route('queue.reject', $entry) }}" method="POST">
@csrf
@method('PATCH')
<div class="form-group">
<x-form-field-title name="Rejection reason" required="true" />
<textarea class="form-input" name="reason" rows="3" required></textarea>
</div>
<div class="queue-mod-actions">
<button type="button" class="btn" @click="open = false">Cancel</button>
<button type="submit" class="btn btn--danger">
<i data-lucide="x-circle" size="14"></i>
Confirm rejection
</button>
</div>
</form>
</div>
</div>
</div>
@endif
@endcan
</div>

View File

@@ -168,10 +168,29 @@
</div>
</div>
@if($isEdit)
<x-form-group-title label="Entry Management" icon="wrench" />
@can('moderate',$entry)
<div class="form-group grid-c2">
<div>
<x-form-field-title name="Staff comment" />
<textarea class="form-textarea" name="staff_comment" rows="3">{{ old('staff_comment', $entry->staff_comment ?? '' ) }}</textarea>
</div>
<div>
<x-form-field-title name="Owner" required="true" />
<livewire:xf-user-selector :initial-user-id="$entry->user_id" />
</div>
</div>
@endcan
@cannot('moderate', $entry)
@endcannot
@endif
@csrf
<div class="submit">
<x-submit-entry-status :section="$section" />
<x-submit-entry-status :section="$section" :is-edit="$isEdit" :current-state="$entry->state ?? null" :entry="$entry" />
<button id="submit-button" type="submit" class="btn primary" style="padding:1%;">Submit</button>
</div>
</form>

View File

@@ -39,12 +39,18 @@
<template x-if="!file.done && !file.error">
<i data-lucide="loader-2" class="spin"></i>
</template>
<template x-if="file.done">
<i data-lucide="check-circle"></i>
</template>
<template x-if="file.error">
<i data-lucide="alert-circle"></i>
</template>
<template x-if="file.done && file.state === 'public'">
<i data-lucide="eye" class="file-state-icon file-state-icon--public"></i>
</template>
<template x-if="file.done && file.state === 'private'">
<i data-lucide="eye-off" class="file-state-icon file-state-icon--private"></i>
</template>
<template x-if="file.done && file.state === 'archived'">
<i data-lucide="archive" class="file-state-icon file-state-icon--archived"></i>
</template>
<div class="upload-item-info">
<span class="upload-item-name" x-text="file.name"></span>
@@ -55,6 +61,48 @@
</div>
<span class="upload-item-error" x-show="file.error" x-text="file.error"></span>
</div>
@if($isEdit)
<div class="upload-item-state" x-show="file.done" x-data="{ confirmArchive: false }">
<select class="form-select" :disabled="file.state === 'archived'" @change="
if( $event.target.value === 'archived'){
confirmArchive = true;
$event.target.value = file.state;
} else {
$wire ? $wire.dispatch('fileStateChanged') : null;
changeFileState(i, $event.target.value);
}
" :value="file.state">
<option value="public" :selected="file.state === 'public'">Public</option>
<option value="private" :selected="file.state === 'private'">Private</option>
<option value="archived" :selected="file.state === 'archived'">Archived</option>
</select>
<div class="modal-overlay" x-cloak x-show="confirmArchive" x-transition.opacity.duration.300ms @click.self="confirmArchive = false" @keydown.escape.window="confirmArchive = false">
<div class="modal-window" x-show="confirmArchive" x-transition>
<div class="modal-header">
<span class="modal-title">
<i data-lucide="archive" size="16"></i>
Archive file
</span>
<button type="button" class="modal-close" @click="confirmArchive = false">
<i data-lucide="x" size="20"></i>
</button>
</div>
<div class="modal-body">
<p>Archiving a file is <b>irreversible</b>.</p>
<div class="queue-mod-actions" style="margin-top: 15px">
<button type="button" class="btn" @click="confirmArchive = false">
Cancel
</button>
<button type="button" class="btn danger" @click="changeFileState(i,'archived'); confirmArchive = false;">
<i data-lucide="archive" size="14"></i>
Confirm archive
</button>
</div>
</div>
</div>
</div>
</div>
@endif
<div class="upload-item-actions">
<button type="button" class="btn" x-show="file.error" @click="handleRetryFile(i)">
<i data-lucide="refresh-cw"></i>
@@ -64,6 +112,9 @@
</button>
</div>
<input type="hidden" name="files_uuid[]" :value="file.uuid" x-show="file.done">
@if($isEdit)
<input type="hidden" name="files_state[]" :value="file.state" x-show="file.done">
@endif
</div>
</template>
</div>