A lot of things

This commit is contained in:
2026-06-16 16:21:43 +02:00
parent 4f9f6c63b3
commit 7e1e26f20b
126 changed files with 7917 additions and 204 deletions

View File

@@ -5,6 +5,8 @@
@import './layout/content.css';
@import './layout/entry.css';
@import './layout/news.css';
@import './layout/activity.css';
@import './layout/submit.css';
@import './components/common.css';
@import './components/grid.css';

View File

@@ -349,6 +349,50 @@
flex-direction: column;
gap: 5px;
}
.gallery-item {
position: relative;
cursor: grab;
transition: opacity 0.2s, transform 0.15s;
user-select: none;
}
.gallery-item:active { cursor: grabbing; }
.gallery-item--dragging {
opacity: 0.4;
transform: scale(0.97);
}
.gallery-drag-handle {
position: absolute;
top: 4px;
left: 4px;
z-index: 10;
background-color: rgba(0,0,0,0.6);
color: #fff;
padding: 3px 4px;
display: flex;
align-items: center;
cursor: grab;
opacity: 0;
transition: opacity 0.15s;
}
.gallery-item:hover .gallery-drag-handle { opacity: 1; }
.gallery-order-badge {
position: absolute;
bottom: 4px;
left: 4px;
z-index: 10;
background-color: rgba(0,0,0,0.7);
color: #fff;
font-size: 0.7rem;
font-weight: 700;
padding: 2px 6px;
min-width: 20px;
text-align: center;
}
.authors-list {
display: grid;
grid-template-columns: repeat(4, 1fr);

View File

@@ -48,6 +48,6 @@
}
.modal-content {
.modal-content, .modal-body {
padding: 20px;
}

View File

@@ -318,3 +318,232 @@
.modcp-list-item-edit--game .form-input { min-width: 180px; flex: 2; }
.modcp-list-item-edit--game .form-select { flex: 1; min-width: 120px; }
.log-filters {
margin-bottom: 16px;
background-color: var(--bg3);
border: 1px solid var(--border);
}
.log-filters-main {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 14px;
flex-wrap: wrap;
}
.log-search-wrap {
flex: 1;
min-width: 200px;
position: relative;
display: flex;
align-items: center;
}
.log-search-wrap i {
position: absolute;
left: 10px;
color: var(--text2);
pointer-events: none;
}
.log-search-wrap .form-input { padding-left: 30px; }
.log-select { min-width: 130px; }
.log-filter-dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: var(--rhpz-orange);
flex-shrink: 0;
}
.log-filters-extra {
border-top: 1px solid var(--border);
padding: 12px 14px;
}
.log-filters-extra-inner {
display: flex;
align-items: flex-end;
gap: 12px;
flex-wrap: wrap;
}
.log-filter-field {
display: flex;
flex-direction: column;
gap: 4px;
}
.log-filter-label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.6px;
color: var(--text2);
}
.log-transition-enter { transition: all .15s ease; }
.log-transition-leave { transition: all .1s ease; }
.log-results-bar {
display: flex;
align-items: center;
justify-content: space-between;
font-size: 0.78rem;
color: var(--text2);
margin-bottom: 10px;
padding: 0 2px;
}
.log-loading { opacity: 0.5; }
.log-item { align-items: flex-start; padding: 11px 14px; }
.log-item--open { background-color: var(--bg3); }
.log-event-dot {
width: 26px;
height: 26px;
flex-shrink: 0;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
margin-top: 2px;
border: 1px solid var(--border);
background-color: var(--bg3);
color: var(--text2);
}
.log-event-dot--created {
background-color: rgba(129,199,132,.1);
border-color: rgba(129,199,132,.35);
color: var(--success);
}
.log-event-dot--updated {
background-color: rgba(255,115,0,.1);
border-color: rgba(255,115,0,.35);
color: var(--rhpz-orange);
}
.log-event-dot--deleted {
background-color: rgba(229,115,115,.1);
border-color: rgba(229,115,115,.35);
color: var(--error);
}
.log-channel-badge {
display: inline-flex;
align-items: center;
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 1px 6px;
background-color: rgba(255,255,255,.05);
border: 1px solid var(--border);
color: var(--text2);
}
.log-id { color: var(--text2); font-size: 0.78rem; }
.log-sep { color: var(--border); }
.log-item-right {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
margin-left: auto;
}
.log-timestamp {
font-size: 0.75rem;
color: var(--text2);
white-space: nowrap;
}
.log-expand-btn { padding: 4px 7px; }
.log-properties {
background-color: var(--bg);
border-bottom: 1px solid var(--border);
padding: 14px 14px 14px 54px;
}
.log-diff-label {
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.7px;
color: var(--text2);
margin-bottom: 8px;
}
.log-diff {
width: 100%;
border-collapse: collapse;
font-size: 0.8rem;
}
.log-diff th {
text-align: left;
padding: 5px 10px;
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text2);
border-bottom: 1px solid var(--border);
}
.log-diff td {
padding: 5px 10px;
border-bottom: 1px solid var(--border);
vertical-align: top;
max-width: 300px;
overflow-wrap: break-word;
}
.log-diff tr:last-child td { border-bottom: none; }
.log-diff-key {
font-size: 0.78rem;
font-weight: 600;
color: var(--text);
width: 160px;
white-space: nowrap;
}
.log-diff-old-head { color: var(--error) !important; }
.log-diff-new-head { color: var(--success) !important; }
.log-diff-old {
color: var(--error);
background-color: rgba(229,115,115,.05);
}
.log-diff-new {
color: var(--success);
background-color: rgba(129,199,132,.05);
}
.log-raw {
font-family: monospace;
font-size: 0.78rem;
color: var(--text2);
background-color: var(--bg2);
border: 1px solid var(--border);
padding: 10px 12px;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
}
.log-pagination {
padding: 14px 0 4px;
border-top: 1px solid var(--border);
}

View File

@@ -0,0 +1,485 @@
.activity-hero-excerpt {
font-size: 0.9rem;
color: rgba(255,255,255,0.75);
margin-bottom: 12px;
line-height: 1.5;
max-width: 600px;
}
.activity-tl-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding-bottom: 15px;
border-bottom: 1px solid var(--border);
gap: 15px;
flex-wrap: wrap;
}
.activity-tl-title {
display: flex;
align-items: center;
gap: 10px;
font-size: 1.15rem;
font-weight: 600;
color: var(--text);
margin: 0;
}
.activity-tl-filters {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.activity-tl-filter {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 5px 12px;
background: none;
border: 1px solid var(--border);
color: var(--text2);
font-size: 0.8rem;
cursor: pointer;
font-family: var(--typography);
transition: all 0.1s;
}
.activity-tl-filter:hover { background-color: var(--bg3); color: var(--text); }
.activity-tl-filter.active {
background-color: var(--bg3);
border-color: var(--rhpz-orange);
color: var(--rhpz-orange);
}
.activity-day-sep {
display: flex;
align-items: center;
gap: 10px;
padding-left: 54px;
margin: 20px 0 12px;
}
.activity-day-label {
font-size: 0.72rem;
font-weight: 600;
color: var(--text2);
text-transform: uppercase;
letter-spacing: 0.8px;
white-space: nowrap;
}
.activity-day-line {
flex: 1;
height: 1px;
background-color: var(--border);
}
.activity-tl-item {
display: flex;
gap: 0;
margin-bottom: 2px;
}
.activity-tl-left {
width: 54px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
padding-top: 12px;
}
.activity-tl-dot {
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid var(--border);
background-color: var(--bg2);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
z-index: 1;
}
.activity-tl-dot--entry {
background-color: rgba(255,115,0,0.1);
border-color: rgba(255,115,0,0.4);
color: var(--rhpz-orange);
}
.activity-tl-dot--news {
background-color: rgba(129,199,132,0.1);
border-color: rgba(129,199,132,0.4);
color: var(--success);
}
.activity-tl-dot--message, .activity-tl-dot--thread, .activity-tl-dot--club {
background-color: rgba(25,118,210,0.1);
border-color: rgba(25,118,210,0.4);
color: var(--info);
}
.activity-tl-line {
width: 1px;
flex: 1;
background-color: var(--border);
margin-top: 4px;
min-height: 16px;
}
.activity-tl-item:last-of-type .activity-tl-line { display: none; }
.activity-tl-card {
flex: 1;
display: flex;
align-items: center;
gap: 12px;
background-color: var(--bg2);
border: 1px solid var(--border);
padding: 10px 14px;
margin-bottom: 8px;
text-decoration: none;
transition: border-color 0.15s, background-color 0.1s;
min-width: 0;
}
.activity-tl-card:hover {
border-color: var(--rhpz-orange);
background-color: var(--bg3);
text-decoration: none;
}
.activity-tl-thumb {
width: 52px;
height: 52px;
flex-shrink: 0;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background-color: var(--bg3);
border: 1px solid var(--border);
}
.activity-tl-thumb img {
width: 100%;
height: 100%;
object-fit: cover;
}
.activity-tl-body {
flex: 1;
min-width: 0;
display: flex;
flex-direction: column;
gap: 4px;
}
.activity-tl-badge {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.6px;
padding: 2px 7px;
width: fit-content;
}
.activity-tl-badge--entry {
background-color: rgba(255,115,0,0.1);
color: var(--rhpz-orange);
border: 1px solid rgba(255,115,0,0.25);
}
.activity-tl-badge--news {
background-color: rgba(129,199,132,0.1);
color: var(--success);
border: 1px solid rgba(129,199,132,0.25);
}
.activity-tl-badge--message, .activity-tl-badge--thread, .activity-tl-dot--club {
background-color: rgba(25,118,210,0.1);
color: var(--info);
border: 1px solid rgba(25,118,210,0.25);
}
.activity-tl-card-title {
font-size: 0.92rem;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
line-height: 1.3;
}
.activity-tl-card-description {
font-size: 0.8rem;
color: var(--text2);
white-space: nowrap;
text-overflow: ellipsis;
line-height: 1.3;
}
.activity-tl-meta {
display: flex;
align-items: center;
gap: 10px;
font-size: 0.75rem;
color: var(--text2);
flex-wrap: wrap;
}
.activity-tl-meta span {
display: flex;
align-items: center;
gap: 3px;
}
.activity-tl-time {
font-size: 0.72rem;
color: var(--text2);
white-space: nowrap;
flex-shrink: 0;
align-self: center;
}
.activity-tl-empty {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
padding: 60px;
color: var(--text2);
text-align: center;
padding-left: 54px;
}
@media (max-width: 600px) {
.activity-tl-header { flex-direction: column; align-items: flex-start; }
.activity-tl-thumb { display: none; }
.activity-day-sep { padding-left: 44px; }
.activity-tl-left { width: 44px; }
}
.home-section {
margin-bottom: 30px;
}
.home-section-header {
display: flex;
align-items: center;
justify-content: space-between;
padding-bottom: 12px;
border-bottom: 1px solid var(--border);
margin-bottom: 14px;
}
.home-section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 1.05rem;
font-weight: 600;
color: var(--text);
margin: 0;
}
.home-section-more {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 0.75rem;
color: var(--text2);
border: 1px solid var(--border);
padding: 4px 10px;
text-decoration: none;
transition: color 0.1s, border-color 0.1s;
}
.home-section-more:hover {
color: var(--rhpz-orange);
border-color: var(--rhpz-orange);
}
.news-strip {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 10px;
}
.news-strip-card {
display: flex;
flex-direction: column;
background-color: var(--bg2);
border: 1px solid var(--border);
text-decoration: none;
overflow: hidden;
transition: border-color 0.15s;
}
.news-strip-card:hover { border-color: var(--rhpz-orange); text-decoration: none; }
.news-strip-cover {
height: 110px;
background-color: var(--bg3);
background-size: cover;
background-position: center;
position: relative;
flex-shrink: 0;
}
.news-strip-date {
position: absolute;
bottom: 6px;
left: 8px;
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(255,255,255,0.8);
background: rgba(0,0,0,0.55);
padding: 2px 6px;
border: 1px solid rgba(255,255,255,0.07);
}
.news-strip-body {
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
}
.news-strip-badge {
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--success);
background: rgba(129,199,132,0.1);
border: 1px solid rgba(129,199,132,0.25);
padding: 1px 6px;
width: fit-content;
}
.news-strip-title {
font-size: 0.85rem;
font-weight: 600;
color: var(--text);
line-height: 1.35;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
margin: 0;
}
.news-strip-meta {
font-size: 0.72rem;
color: var(--text2);
margin-top: auto;
}
.featured-entries-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.featured-entry-card {
display: flex;
flex-direction: column;
background-color: var(--bg2);
border: 1px solid var(--border);
text-decoration: none;
overflow: hidden;
transition: border-color 0.15s;
}
.featured-entry-card:hover { border-color: var(--rhpz-orange); text-decoration: none; }
.featured-entry-cover {
height: 80px;
background-color: var(--bg3);
position: relative;
flex-shrink: 0;
overflow: hidden;
}
.featured-entry-cover img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.85;
}
.featured-entry-star {
position: absolute;
top: 6px;
right: 6px;
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
background: rgba(255,115,0,0.9);
color: #111;
padding: 2px 6px;
border: 1px solid rgba(255,115,0,0.5);
display: flex;
align-items: center;
gap: 3px;
}
.featured-entry-body {
padding: 10px 12px;
display: flex;
flex-direction: column;
gap: 5px;
flex: 1;
}
.featured-entry-platform {
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--rhpz-orange);
background: rgba(255,115,0,0.1);
border: 1px solid rgba(255,115,0,0.25);
padding: 1px 6px;
width: fit-content;
}
.featured-entry-title {
font-size: 0.88rem;
font-weight: 600;
color: var(--text);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.featured-entry-meta {
font-size: 0.72rem;
color: var(--text2);
margin-top: auto;
}
@media (max-width: 900px) {
.news-strip { grid-template-columns: repeat(3, 1fr); }
.featured-entries-grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
.news-strip { grid-template-columns: repeat(2, 1fr); }
.featured-entries-grid { grid-template-columns: repeat(2, 1fr); }
.news-strip-cover { height: 80px; }
}

View File

@@ -9,12 +9,18 @@
z-index: 100;
.menu-header {
padding: 20px;
padding: 10px;
display: flex;
align-items: center;
gap: 12px;
border-bottom: 1px solid var(--border);
img {
width: 100%;
height: 100%;
object-fit: contain;
}
.menu-logo {
width: 32px;
height: 32px;

View File

@@ -46,3 +46,441 @@
margin-bottom: 20px;
}
}
#news-container {
background-color: var(--bg2);
border: 1px solid var(--border);
display: flex;
flex-direction: column;
}
.news-header {
width: 100%;
height: 300px;
background-size: cover;
background-position: center;
display: flex;
align-items: flex-end;
padding: 40px 30px;
border-bottom: 1px solid var(--border);
position: relative;
}
.news-header-content {
position: relative;
z-index: 2;
}
.news-header .news-title {
font-size: 2.5rem;
font-weight: 600;
color: var(--text);
margin-bottom: 12px;
text-shadow: 0 2px 4px rgba(0,0,0,0.6);
}
.news-header .news-meta {
color: var(--text2);
display: flex;
gap: 20px;
font-size: 0.9rem;
align-items: center;
}
.news-header .meta-item {
display: flex;
align-items: center;
gap: 6px;
background-color: rgba(0, 0, 0, 0.4);
padding: 4px 10px;
border-radius: 2px;
border: 1px solid rgba(255,255,255,0.05);
}
.news-layout {
display: flex;
flex-direction: row;
gap: 30px;
padding: 30px;
}
@media (max-width: 992px) {
.news-layout {
flex-direction: column;
}
}
.news-main-content {
flex-grow: 1;
flex-basis: 0;
min-width: 0;
}
.news-body-text {
line-height: 1.75;
color: var(--text);
font-size: 1.05rem;
margin-bottom: 15px;
}
.news-body-text p {
margin-bottom: 20px;
}
.news-sidebar {
width: 320px;
flex-shrink: 0;
display: flex;
flex-direction: column;
gap: 25px;
}
@media (max-width: 992px) {
.news-sidebar {
width: 100%;
}
}
.sidebar-block {
background-color: var(--bg);
border: 1px solid var(--border);
padding: 20px;
border-radius: 4px;
}
.sidebar-title {
font-size: 1rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--text);
margin-bottom: 15px;
display: flex;
align-items: center;
gap: 8px;
border-bottom: 1px solid var(--border);
padding-bottom: 8px;
}
.btn-sidebar {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
width: 100%;
padding: 10px 15px;
font-size: 0.95rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
border-radius: 2px;
transition: background-color 0.2s ease, border-color 0.2s ease;
text-align: center;
}
.btn-orange {
background-color: var(--rhpz-orange);
color: #fff;
border: 1px solid transparent;
}
.btn-orange:hover {
background-color: var(--rhpz-orange-hover);
}
.related-card {
display: flex;
flex-direction: column;
gap: 12px;
}
.related-card-cover {
width: 100%;
height: 150px;
background-color: var(--bg2);
border: 1px solid var(--border);
overflow: hidden;
border-radius: 2px;
}
.related-card-cover img {
width: 100%;
height: 100%;
object-fit: contain;
padding: 5px;
}
.related-card-info h4 {
font-size: 1.1rem;
color: var(--text);
margin-bottom: 10px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.news-sidebar .video-thumbnail-wrapper {
position: relative;
width: 100%;
aspect-ratio: 16/9;
background-color: #000;
border: 1px solid var(--border);
cursor: pointer;
overflow: hidden;
border-radius: 2px;
}
.news-sidebar .video-thumbnail-wrapper img {
width: 100%;
height: 100%;
object-fit: cover;
opacity: 0.7;
transition: transform 0.3s ease, opacity 0.3s ease;
}
.news-sidebar .video-thumbnail-wrapper:hover img {
transform: scale(1.03);
opacity: 0.9;
}
.news-sidebar .play-trigger {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 50px;
height: 50px;
background-color: rgba(0, 0, 0, 0.75);
border: 2px solid #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
transition: background-color 0.2s, transform 0.2s ease-out;
}
.news-sidebar .video-thumbnail-wrapper:hover .play-trigger {
background-color: var(--rhpz-orange);
transform: translate(-50%, -50%) scale(1.1);
}
.news-actions {
display: flex;
align-items: center;
gap: 8px;
margin-top: 15px;
flex-wrap: wrap;
}
.news-header .news-actions .btn {
background-color: rgba(0, 0, 0, 0.5);
border-color: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(4px);
color: var(--text);
transition: background-color 0.15s, border-color 0.15s;
}
.news-header .news-actions .btn:hover {
background-color: rgba(0, 0, 0, 0.7);
border-color: rgba(255, 255, 255, 0.3);
}
.news-header .news-actions .btn.success {
background-color: rgba(56, 142, 60, 0.6);
border-color: rgba(129, 199, 132, 0.4);
color: #81c784;
}
.news-header .news-actions .btn.danger {
background-color: rgba(183, 28, 28, 0.5);
border-color: rgba(229, 115, 115, 0.4);
color: #e57373;
}
/* ── Hero ────────────────────────────────────────────────── */
.news-hero {
display: block;
position: relative;
width: 100%;
height: 360px;
margin-bottom: 20px;
border: 1px solid var(--border);
overflow: hidden;
text-decoration: none;
transition: border-color 0.2s;
}
.news-hero:hover {
border-color: var(--rhpz-orange);
text-decoration: none;
}
.news-hero-bg {
position: absolute;
inset: 0;
background-size: cover;
background-position: center;
background-color: var(--bg3);
transition: transform 0.4s ease;
}
.news-hero:hover .news-hero-bg {
transform: scale(1.02);
}
.news-hero-content {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 30px;
z-index: 2;
}
.news-hero-badge {
display: inline-flex;
align-items: center;
gap: 5px;
background-color: var(--rhpz-orange);
color: #111;
font-size: 0.72rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
padding: 4px 10px;
margin-bottom: 12px;
}
.news-hero-title {
font-size: 2rem;
font-weight: 600;
color: #fff;
margin-bottom: 12px;
text-shadow: 0 2px 8px rgba(0,0,0,0.5);
line-height: 1.2;
}
.news-hero-meta {
display: flex;
align-items: center;
gap: 15px;
font-size: 0.85rem;
color: rgba(255,255,255,0.75);
}
.news-hero-meta span {
display: flex;
align-items: center;
gap: 5px;
background-color: rgba(0,0,0,0.4);
padding: 3px 10px;
border: 1px solid rgba(255,255,255,0.08);
}
.news-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.news-card {
display: flex;
flex-direction: column;
background-color: var(--bg2);
border: 1px solid var(--border);
text-decoration: none;
overflow: hidden;
transition: border-color 0.15s, transform 0.15s;
}
.news-card:hover {
border-color: var(--rhpz-orange);
transform: translateY(-2px);
text-decoration: none;
}
.news-card-cover {
height: 160px;
background-size: cover;
background-position: center;
background-color: var(--bg3);
position: relative;
flex-shrink: 0;
transition: transform 0.3s ease;
}
.news-card:hover .news-card-cover {
transform: scale(1.03);
}
.news-card-state-badge {
position: absolute;
top: 10px;
right: 10px;
display: inline-flex;
align-items: center;
gap: 4px;
background-color: rgba(0,0,0,0.7);
color: var(--rhpz-orange);
font-size: 0.72rem;
font-weight: 600;
padding: 3px 8px;
border: 1px solid rgba(255,115,0,0.3);
}
.news-card-body {
padding: 16px;
display: flex;
flex-direction: column;
flex: 1;
gap: 8px;
}
.news-card-title {
font-size: 1rem;
font-weight: 600;
color: var(--text);
line-height: 1.3;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
}
.news-card-excerpt {
font-size: 0.82rem;
color: var(--text2);
line-height: 1.5;
margin: 0;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
flex: 1;
}
.news-card-meta {
display: flex;
align-items: center;
gap: 12px;
font-size: 0.75rem;
color: var(--text2);
margin-top: auto;
padding-top: 8px;
border-top: 1px solid var(--border);
}
.news-card-meta span {
display: flex;
align-items: center;
gap: 4px;
}
@media (max-width: 600px) {
.news-hero { height: 240px; }
.news-hero-title { font-size: 1.4rem; }
.news-grid { grid-template-columns: 1fr; }
}

View File

@@ -0,0 +1,297 @@
.submit-hero {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 24px;
background-color: var(--bg2);
border-bottom: 1px solid var(--border);
padding: 40px 36px 32px;
}
.submit-eyebrow {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--rhpz-orange);
background: rgba(255,115,0,.1);
border: 1px solid rgba(255,115,0,.3);
padding: 3px 10px;
margin-bottom: 16px;
}
.submit-hero-title {
font-size: 1.9rem;
font-weight: 300;
color: var(--text);
margin-bottom: 10px;
line-height: 1.25;
}
.submit-hero-sub {
font-size: 0.9rem;
color: var(--text2);
max-width: 460px;
line-height: 1.65;
}
.submit-review-note {
font-size: 0.8rem;
color: var(--text2);
border: 1px solid var(--border);
background: var(--bg3);
padding: 14px 18px;
max-width: 210px;
line-height: 1.6;
flex-shrink: 0;
}
.submit-review-note strong { color: var(--rhpz-orange); }
.submit-body { padding: 28px 36px 40px; }
.submit-section-label {
font-size: 0.68rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 1px;
color: var(--text2);
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.submit-section-label::after {
content: '';
flex: 1;
height: 1px;
background: var(--border);
}
.submit-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 30px;
}
.submit-card {
display: flex;
flex-direction: column;
background: var(--bg2);
border: 1px solid var(--border);
text-decoration: none;
transition: border-color .15s, background .1s;
}
.submit-card:hover {
border-color: var(--card-color);
background: var(--bg3);
text-decoration: none;
}
.submit-card-top {
display: flex;
align-items: flex-start;
gap: 14px;
padding: 20px 20px 16px;
border-bottom: 1px solid var(--border);
}
.submit-card-icon {
width: 38px;
height: 38px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: var(--card-bg);
border: 1px solid var(--card-border);
color: var(--card-color);
}
.submit-card-title {
font-size: 0.95rem;
font-weight: 600;
color: var(--text);
margin-bottom: 4px;
}
.submit-card-desc {
font-size: 0.78rem;
color: var(--text2);
line-height: 1.55;
}
.submit-card-bottom {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 20px;
}
.submit-card-tag {
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--card-color);
background: var(--card-bg);
border: 1px solid var(--card-border);
padding: 2px 7px;
}
.submit-card-cta {
font-size: 0.75rem;
color: var(--text2);
display: flex;
align-items: center;
gap: 4px;
transition: color .15s;
}
.submit-card:hover .submit-card-cta { color: var(--card-color); }
.submit-news-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
margin-bottom: 28px;
}
.submit-news-card {
display: flex;
align-items: center;
gap: 14px;
background: var(--bg2);
border: 1px solid var(--border);
padding: 18px 22px;
text-decoration: none;
transition: border-color .15s, background .1s;
}
.submit-news-card:hover {
border-color: var(--success);
background: var(--bg3);
}
.submit-news-card--disabled { opacity: .5; cursor: not-allowed; }
.submit-news-card--disabled:hover { border-color: var(--border); background: var(--bg2); }
.submit-news-icon {
width: 38px;
height: 38px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(129,199,132,.1);
border: 1px solid rgba(129,199,132,.3);
color: var(--success);
}
.submit-news-title {
font-size: 0.95rem;
font-weight: 600;
color: var(--text);
margin-bottom: 3px;
}
.submit-news-desc { font-size: 0.78rem; color: var(--text2); line-height: 1.55; }
.submit-news-cta {
flex-shrink: 0;
font-size: 0.75rem;
color: var(--text2);
display: flex;
align-items: center;
gap: 4px;
transition: color .15s;
}
.submit-news-card:hover .submit-news-cta { color: var(--success); }
.submit-news-staff-note {
background: var(--bg2);
border: 1px solid var(--border);
padding: 18px 22px;
font-size: 0.8rem;
color: var(--text2);
line-height: 1.65;
display: flex;
flex-direction: column;
gap: 10px;
}
.submit-news-staff-badge {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 0.65rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .6px;
color: var(--rhpz-orange);
background: rgba(255,115,0,.1);
border: 1px solid rgba(255,115,0,.3);
padding: 2px 8px;
width: fit-content;
}
.submit-news-staff-note a { color: var(--text2); text-decoration: underline; }
.submit-news-staff-note a:hover { color: var(--text); }
.submit-rules {
display: flex;
gap: 0;
background: var(--bg2);
border: 1px solid var(--border);
}
.submit-rule {
flex: 1;
display: flex;
align-items: flex-start;
gap: 12px;
padding: 18px 22px;
border-right: 1px solid var(--border);
font-size: 0.8rem;
color: var(--text2);
line-height: 1.6;
}
.submit-rule:last-child { border-right: none; }
.submit-rule strong { color: var(--text); }
.submit-rule-num {
width: 22px;
height: 22px;
flex-shrink: 0;
display: flex;
align-items: center;
justify-content: center;
background: rgba(255,115,0,.1);
border: 1px solid rgba(255,115,0,.3);
color: var(--rhpz-orange);
font-size: 0.72rem;
font-weight: 700;
}
@media (max-width: 900px) {
.submit-hero { flex-direction: column; align-items: flex-start; }
.submit-grid { grid-template-columns: repeat(2, 1fr); }
.submit-rules { flex-direction: column; }
.submit-rule { border-right: none; border-bottom: 1px solid var(--border); }
.submit-rule:last-child { border-bottom: none; }
}
@media (max-width: 600px) {
.submit-hero, .submit-body { padding-left: 20px; padding-right: 20px; }
.submit-grid { grid-template-columns: 1fr; }
.submit-news-row { grid-template-columns: 1fr; }
.submit-review-note { max-width: 100%; }
}

View File

@@ -0,0 +1,85 @@
import { RomPatcher } from './RomPatcher.js';
window.PlayOnline = function( initialPatches = {}, emulatorJsConfig = {} ){
const parent = RomPatcher( initialPatches );
return {
...parent,
currentBlobUrl: null,
emuConfig: emulatorJsConfig,
launchGame: false,
init(){
parent.init({
language: 'en',
requireValidation: false,
onpatch: this.handlePatchedRomFile.bind(this),
});
},
cleanEmulatorJsVars() {
['EJS_player','EJS_core','EJS_gameUrl','EJS_pathtodata',
'EJS_startOnLoaded','EJS_threads']
.forEach(k => delete window[k]);
},
prepareEmulatorJs(){
window.EJS_player = '#game';
window.EJS_core = this.emuConfig.core;
window.EJS_gameUrl = this.currentBlobUrl;
window.EJS_pathtodata = "https://cdn.emulatorjs.org/stable/data/";
window.EJS_startOnLoaded = true;
window.EJS_threads = this.emuConfig.threads ?? false;
},
launchEmulatorJs(){
if(!this.currentBlobUrl){
console.error("EmulatorJS: Empty Blob field");
return;
}
console.log(this.currentBlobUrl);
this.cleanEmulatorJsVars();
this.prepareEmulatorJs();
const script = document.createElement('script');
script.id = 'ejs-loader';
script.src = 'https://cdn.emulatorjs.org/stable/data/loader.js';
document.body.appendChild(script);
this.launchGame = true;
},
/**
* @param {BinFile} patchedRomFile
*/
handlePatchedRomFile( patchedRomFile ){
patchedRomFile.save = function(){
// Remove save.
return;
}
const u8 = patchedRomFile._u8array;
if( !u8 || u8.byteLength === 0 ){
console.error("Patch error: Empty ROM file");
return;
}
if(this.currentBlobUrl){
URL.revokeObjectURL(this.currentBlobUrl);
}
const blob = new Blob([u8], { type: 'application/octet-stream' });
this.currentBlobUrl = URL.createObjectURL(blob);
this.launchEmulatorJs()
}
}
}

View File

@@ -1,4 +1,4 @@
export function RomPatcher( initialPatches = {} ) {
export const RomPatcher = function( initialPatches = {} ) {
let patchesArray = [];
if (initialPatches) {
@@ -39,9 +39,9 @@ export function RomPatcher( initialPatches = {} ) {
patchesData: patchesArray,
hasEmbedded: patchesArray.length > 0,
init() {
init( config = {language: 'en', requireValidation: false} ) {
const CONFIG = {language: 'en', requireValidation: false};
const CONFIG = config;
if (!RomPatcherWeb.isInitialized()){
if (this.hasEmbedded) {
@@ -112,3 +112,7 @@ export function RomPatcher( initialPatches = {} ) {
}
}
}
window.RomPatcher = RomPatcher;

View File

@@ -2,6 +2,10 @@
export const CHUNK_SIZE = 8192;
const PATCH_EXTENSIONS = new Set([
'ips', 'bps', 'ups', 'aps', 'ppf', 'xdelta', "zip"
]);
/**
* An uploaded file instance.
* Create a new file data.
@@ -12,6 +16,8 @@ export const CHUNK_SIZE = 8192;
*/
export function FSFileData(name, totalChunks, rawFile ) {
const extension = name.split('.').pop().toLowerCase();
return {
/**
@@ -66,6 +72,8 @@ export function FSFileData(name, totalChunks, rawFile ) {
*/
state: 'public',
can_be_online_patched: PATCH_EXTENSIONS.has(extension),
/**
* If the online patcher is enabled
*/
@@ -76,6 +84,21 @@ export function FSFileData(name, totalChunks, rawFile ) {
*/
meta_secondary_online_patcher: false,
/**
* If this patch can be played online.
*/
meta_play_online: false,
/**
* Selected core for play online
*/
meta_play_online_core: null,
/**
* If the threads are enabled for playing online.
*/
meta_play_online_threads: null,
/**
* Look if this file is currently uploading.
* @returns {boolean}
@@ -164,6 +187,6 @@ export function FSFileData(name, totalChunks, rawFile ) {
}
}
}
}
}

View File

@@ -12,6 +12,8 @@ export function GalleryManager() {
*/
images: [],
dragSrcI: null,
/**
* Forward to this.images.length
* @returns {number}
@@ -123,6 +125,25 @@ export function GalleryManager() {
handleRemoveFile(index){
this.images[index].handleRemoveFile(null);
this.images.splice(index, 1);
},
dragStart(index){
this.dragSrcI = index;
},
dragOver(e, index){
e.preventDefault();
if( this.dragSrcI === null || this.dragSrcI === index )
return;
const moved = this.images.splice(this.dragSrcI, 1)[0];
this.images.splice(index, 0, moved);
this.dragSrcI = index;
},
dragEnd(){
this.dragSrcI = null;
}
}
}

View File

@@ -7,7 +7,6 @@ import hovercard from "./hovercard.js";
import notifications from "./notifications.js";
import conversations from "./conversations.js";
import settings from "./settings.js";
import {RomPatcher} from "./RomPatcher.js";
/**
* Get config defined in meta.blade.php
@@ -15,7 +14,7 @@ import {RomPatcher} from "./RomPatcher.js";
* @return {string|null}
*/
window.getConfig = function( key ){
return document.querySelector('meta[name="config-' + key + '"]').getAttribute('content') ?? null;
return document.querySelector('meta[name="config-' + key + '"]')?.getAttribute('content') ?? null;
}
// Lucide icons.
@@ -44,6 +43,3 @@ Alpine.store('conversations', conversations() );
// Settings
Alpine.store('settings', settings() );
// ROMPatcher
window.RomPatcher = RomPatcher;

View File

@@ -0,0 +1,169 @@
import { GalleryManager } from "./SubmissionsClass/GalleryManager.js";
/**
* If there is some server side errors.
* We may need reload some things.
* @type {boolean}
*/
const SERVER_SIDE_ERRORS = document.querySelector('meta[name="submission-has-errors"]')?.content === '1';
/**
* Object map of errors messages
* @type {Object<string,string>}
*/
const ERROR_TABLE = {
noDescription: "Please provide a description.",
noGalleryImages: "Please select at least a gallery image.",
isSubmitting: "The entry is already during submission."
}
window.GalleryManager = GalleryManager;
/**
* Verify if an EasyMDE field is filled.
*
* @param {string} fieldName
* @returns {boolean}
*/
function verifyMDE( fieldName ){
const textarea = document.querySelector('#field_' + fieldName);
if( textarea && textarea.value.trim().length > 0 ) {
return true;
}
const field = window['mde_' + fieldName] || null;
return field && typeof field.value === 'function' && field.value().trim().length > 0;
}
window.SubmissionVerifications = {
/**
* Verify if the description field has at least one character.
* @returns {boolean}
*/
step1_VerifyDescription: function(){
return verifyMDE('description');
},
/**
* Verify if at least one image is uploaded in the gallery.
* @param element this.$el
* @return {boolean}
*/
step2_verifyGallery: function( element){
let GalleryData = element.querySelector('[x-data="GalleryManager()"]');
GalleryData = GalleryData ? Alpine.$data(GalleryData) : null;
if( ! GalleryData ){
return false;
}
return GalleryData.number > 0 && GalleryData.allUploaded;
}
}
/**
* Handle entire submission process.
*/
window.NewsSubmission = function(){
return {
/**
* If the script is during a try of submission process.
* @type {boolean}
*/
duringSubmissionProcess: false,
/**
* Error checked.
* @type {string|null}
*/
errorKey: null,
/**
* Return error message.
* @return {string}
*/
get errorMessage(){
return ERROR_TABLE[this.errorKey] ?? "Unknown error";
},
init(){
},
/**
* Do each form verifications.
* Update also this.errorKey.
*
* @returns {boolean}
*/
verifyForm(){
console.log( "Step 1" );
if( !SubmissionVerifications.step1_VerifyDescription() ){
this.errorKey = "noDescription";
return false;
}
console.log( "Step 2" );
if( !SubmissionVerifications.step2_verifyGallery( this.$el )){
this.errorKey = "noGalleryImages";
return false;
}
return true;
},
/**
* Scroll to the specific error field.
*/
scrollToError(){
const refMap = {
noDescription: 'descriptionField',
noGalleryImages: 'gallery-field',
isSubmitting: 'submitButton'
};
const target = this.$refs[refMap[this.errorKey]]
|| this.$el.querySelector('.upload-list')
|| this.$el.querySelector('.form-upload');
if (target) {
target.scrollIntoView({behavior: 'smooth', block: 'center'});
return;
}
},
/**
* If you want to submit the form.
* @param {Event} e
*/
submitForm( e ){
if( this.duringSubmissionProcess )
return; // Don't submit two times.
this.errorKey = null; // Reset.
this.duringSubmissionProcess = true;
const STATE = document.querySelector('select[name="submit-state"]')?.value;
if( STATE === 'draft' ){
e.target.submit();
return;
}
if( !this.verifyForm() ){
this.scrollToError();
this.duringSubmissionProcess = false;
return;
}
e.target.submit();
}
}
}

View File

@@ -14,20 +14,15 @@ export default function settings() {
*/
xfUrls: {},
/**
* @type {number[]}
*/
entriesPerPage: [ 12, 30, 48 ],
/**
* @type {string}
*/
currentTheme: Cookies.get("theme") ?? 'default',
currentTheme: 'default',
/**
* @type {number}
* @type {list|null}
*/
currentEntriesPerPage: Cookies.get("entries_per_page") ?? 30,
currentActivityFilters: null,
/**
*
@@ -43,7 +38,7 @@ export default function settings() {
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') } );
// Cookies.set('theme', this.currentTheme, { expires: 365, path: '/', domain: window.getConfig('session-domain') } );
this.syncXF();
},
@@ -62,22 +57,48 @@ export default function settings() {
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; },
async toggleActivityFilter( type ){
if( this.currentActivityFilters === null )
return;
const i = this.currentActivityFilters.indexOf( type );
if( i !== -1 && this.currentActivityFilters.length === 1)
return;
if( i === -1 )
this.currentActivityFilters.push( type );
else
this.currentActivityFilters.splice( i, 1 );
Cookies.set( 'activity_filters', JSON.stringify(this.currentActivityFilters), { expires: 365, path: '/', domain: window.getConfig('session-domain') } );
await this.syncTimeline();
},
async syncTimeline(){
const tl = document.getElementById('activity-timeline');
if( !tl )
return;
tl.style.opacity = 0.5;
const params = this.currentActivityFilters.join(',');
const response = await fetch(`/api/dynamic/activity/feed?filters=${params}`);
const data = await response.json();
if( !data.html )
return;
tl.innerHTML = data.html;
tl.style.opacity = 1;
refreshIcons(tl);
}
}
}

View File

@@ -38,6 +38,7 @@ const ERROR_TABLE = {
* @constructor
*/
const SECTION = () => document.querySelector("meta[name='fs-section']")?.content ?? '';
const CSRF = () => document.querySelector("meta[name='csrf-token']")?.content ?? '';
window.FSUploader = FSUploader;
window.HashesManager = HashesManager;
@@ -403,6 +404,31 @@ window.Submission = function(){
}
e.target.submit();
},
async requestFeatured( entryId ){
const csrf = CSRF();
const response = await fetch(`/api/entry/${entryId}/featured`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrf
}
});
const json = await response.json();
const entry_featured_button = document.querySelector('#entry-featured-button');
const entry_featured_body = document.querySelector('#entry-featured-body');
if( json.success ){
entry_featured_body.innerHTML = '<p>Request submitted</p>';
entry_featured_button.style.display = 'none';
} else {
entry_featured_body.innerHTML = '<p>Request failed. Please refresh the page and retry.</p>';
entry_featured_button.style.display = 'none';
}
}
}

View File

@@ -0,0 +1,40 @@
<?php /** @var \App\Models\Entry $entry */ ?>
@if($featuredEntries->isNotEmpty())
<section class="home-section">
<div class="home-section-header">
<h2 class="home-section-title">
<i data-lucide="star" size="16"></i>
Featured entries
</h2>
</div>
<div class="featured-entries-grid">
@foreach($featuredEntries as $entry)
<a href="{{ route('entries.show', [ 'section' => $entry->type, 'entry' => $entry ]) }}" class="featured-entry-card">
<div class="featured-entry-cover">
@if($entry->main_image)
<img src="{{ Storage::url($entry->main_image) }}"
alt="{{ $entry->title }}"
loading="lazy">
@endif
<span class="featured-entry-star">
{{ \App\View\Components\EntryCard::ENTRY_TYPES_BADGE[ $entry->type ] }}
</span>
</div>
<div class="featured-entry-body">
@if(!empty($entry->getRealPlatform()))
<span class="featured-entry-platform">{{ $entry->getRealPlatform()->name }}</span>
@endif
<div class="featured-entry-title">{{ $entry->title }}</div>
<div class="featured-entry-meta">
@if($entry->user_id)
<x-xf-username-link :user-id="$entry->user_id"/>
@endif
· {{ $entry->featured_at->format('M Y') }}
</div>
</div>
</a>
@endforeach
</div>
</section>
@endif

View File

@@ -0,0 +1,34 @@
<?php /** @var \App\Models\News $news */ ?>
<section class="home-section">
<div class="home-section-header">
<h2 class="home-section-title">
<i data-lucide="newspaper" size="16"></i>
Latest news
</h2>
<a href="{{ route('news.index') }}" class="home-section-more">
See all <i data-lucide="arrow-right" size="12"></i>
</a>
</div>
<div class="news-strip">
@foreach($latestNews as $news)
<a href="{{ route('news.show', $news) }}" class="news-strip-card">
<div class="news-strip-cover"
@if($news->gallery->first())
style="background-image: url('{{ Storage::url($news->gallery()->first()->image) }}')"
@endif>
<span class="news-strip-date">
{{ $news->created_at->format('M j') }}
</span>
</div>
<div class="news-strip-body">
<span class="news-strip-badge">News</span>
<h3 class="news-strip-title">{{ $news->title }}</h3>
<span class="news-strip-meta">
{{ $news->created_at->diffForHumans() }}
</span>
</div>
</a>
@endforeach
</div>
</section>

View File

@@ -0,0 +1,103 @@
@php $currentDay = null; @endphp
<div class="activity-timeline" id="activity-timeline">
@forelse($items as $item)
@php
$day = $item->date->format('Y-m-d');
$dayLabel = $item->date->isToday() ? 'Today'
: ($item->date->isYesterday() ? 'Yesterday'
: $item->date->format('M d, Y'));
@endphp
@if($day !== $currentDay)
@php $currentDay = $day; @endphp
<div class="activity-day-sep">
<span class="activity-day-label">{{ $dayLabel }}</span>
<div class="activity-day-line"></div>
</div>
@endif
<div class="activity-tl-item" data-type="{{ $item->type }}">
<div class="activity-tl-left">
<div class="activity-tl-dot activity-tl-dot--{{ $item->type }}">
@if($item->type === 'entry')
<i data-lucide="database" size="14"></i>
@elseif($item->type === 'news')
<i data-lucide="newspaper" size="14"></i>
@elseif($item->type === 'message')
<i data-lucide="message-square" size="14"></i>
@elseif($item->type === 'thread')
<i data-lucide="messages-square" size="14"></i>
@elseif($item->type === 'club')
<i data-lucide="balloon" size="14"></i>
@else
<i data-lucide="target" size="14"></i>
@endif
</div>
<div class="activity-tl-line"></div>
</div>
<a
href="{{ $item->url }}"
class="activity-tl-card"
>
@if( !empty($item->image) )
<div class="activity-tl-thumb activity-tl-thumb--{{ $item->type }}">
<img src="{{ $item->image }}" alt="{{ $item->title }}" loading="lazy">
</div>
@endif
<div class="activity-tl-body">
<span class="activity-tl-badge activity-tl-badge--{{ $item->type }}">
{{ $item->badge }}
</span>
<div class="activity-tl-card-title">{{ $item->title }}</div>
@if(!empty($item->excerpt))
<div class="activity-tl-card-description">
{{ $item->excerpt }}
</div>
@endif
<div class="activity-tl-meta">
@if(!empty($item->user_id))
<span>
<i data-lucide="user" size="11"></i>
<x-xf-username-link :user-id="$item->user_id"/>
</span>
@elseif(!empty($item->author))
<span>
<i data-lucide="users" size="11"></i>
{{ $item->author }}
</span>
@endif
@if(!empty($item->meta))
<span>
<i data-lucide="monitor" size="11"></i>
{{ $item->meta }}
</span>
@endif
</div>
</div>
<div class="activity-tl-time">
@if($item->date->isToday())
{{ $item->date->format('g:i A') }}
@elseif($item->date->isYesterday())
Yesterday {{ $item->date->format('g:i A') }}
@elseif($item->date->diffInDays() < 7)
{{ $item->date->format('D g:i A') }}
@else
{{ $item->date->format('M j, g:i A') }}
@endif
</div>
</a>
</div>
@empty
<div class="activity-tl-empty">
<i data-lucide="inbox" size="36"></i>
<p>No recent activity.</p>
</div>
@endforelse
</div>

View File

@@ -15,8 +15,14 @@
</div>
<div class="form-gallery form-group level" style="flex:4;">
<template x-for="(image,i) in images" :key="image.key">
<div class="gallery-item">
<div class="gallery-item" :class="{ 'gallery-item--dragging': dragSrcI === i }" draggable="true" @dragstart="dragStart(i)" @dragover="dragOver($event, i)" @dragend="dragEnd()">
<div class="form-image-preview-wrap">
<div class="gallery-drag-handle" title="Drag to reorder">
<i data-lucide="grip-vertical" size="14"></i>
</div>
<span class="gallery-order-badge" x-text="i + 1"></span>
<img :src="image.preview" :alt="image.name">
<button type="button" class="form-image-remove" @click="handleRemoveFile(i)">
X

View File

@@ -1,12 +1,7 @@
<nav id="menu">
<div class="menu-header">
<div class="menu-logo">
RP
</div>
<div class="menu-title">
Romhack Plaza
</div>
<img src="{{ asset('logo/plaza-logo-wide.png') }}">
</div>
<div class="menu-navigation">
@@ -15,7 +10,7 @@
<div class="menu-group">
<div class="menu-group-title">{{ $menu['name'] }}</div>
@foreach( $menu['items'] as $item )
@if( !isset( $item['requires_auth'] ) || $item['requires_auth'] && !\Auth::guest() )
@if( !isset( $item['requires_auth'] ) || $item['requires_auth'] === true && !\Auth::guest() )
<a href="{{ isset($item['xf_route']) ? xfRoute($item['xf_route']) : route($item['route']) }}"
@class(['menu-item', 'active' => request()->routeIs( $item['route'] ?? '' )]) >
<i data-lucide="{{ $item['icon'] }}"></i><span>{{ $item['name'] }}</span>

View File

@@ -0,0 +1,41 @@
<a href="{{ route('news.show', $news->slug) }}" class="news-card">
<div class="news-card-cover"
style="background-image:
linear-gradient(rgba(0,0,0,0.1), rgba(20,20,20,0.9)),
url('{{ $news->gallery()->first() ? Storage::url($news->gallery()->first()->image) : '' }}')">
@if($news->state !== 'published')
<span class="news-card-state-badge">
@if($news->state === 'pending')
<i data-lucide="clock" size="11"></i> Pending
@elseif($news->state === 'draft')
<i data-lucide="scissors" size="11"></i> Draft
@elseif($news->state === 'hidden')
<i data-lucide="eye-off" size="11"></i> Hidden
@endif
</span>
@endif
</div>
<div class="news-card-body">
<h2 class="news-card-title">{{ $news->title }}</h2>
@if($news->description)
<p class="news-card-excerpt">
{{ Str::limit(strip_tags($news->description), 100) }}
</p>
@endif
<div class="news-card-meta">
@if($news->user_id)
<span><i data-lucide="user" size="12"></i>
<x-xf-username-link :user-id="$news->user_id"/>
</span>
@endif
<span><i data-lucide="calendar" size="12"></i>
{{ $news->created_at->format('M d, Y') }}
</span>
@if($news->gallery->isNotEmpty())
<span><i data-lucide="images" size="12"></i>
{{ $news->gallery->count() }}
</span>
@endif
</div>
</div>
</a>

View File

@@ -0,0 +1,13 @@
<?php /** @var \App\Models\Category $category */ ?>
<div class="form-group">
<select class="form-select" name="category">
<option value="" disabled {{ empty($selected) ? 'selected' : '' }} hidde*>
Select a category...
</option>
@foreach($categories as $category)
<option value="{{ $category->id }}" {{ in_array($category->id, (array) $selected) ? 'selected' : '' }}>
{{ $category->name }}
</option>
@endforeach
</select>
</div>

View File

@@ -32,25 +32,6 @@
</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
@@ -67,6 +48,4 @@
</div>
@endauth
</div>
</div>

View File

@@ -2,6 +2,7 @@
nsfw: null,
state: '{{ old('submit-state', $defaultState) }}',
deleteOpen: false,
featuredOpen: false,
init(){
this.$watch('nsfw', (val) => {
if( val && this.state === 'published' ) {
@@ -11,6 +12,13 @@
}
}" x-init="init()">
@if($isEdit)
@if(!$news && $entry && !$entry->featured )
<div>
<button type="button" id="entry-featured-button" class="btn" @click="featuredOpen = true; $dispatch('modal:opened')">
<i data-lucide="star" size="13"></i> Featured
</button>
</div>
@endif
<div>
<button type="button" class="btn danger" @click="deleteOpen = true; $dispatch('modal:opened')">
<i data-lucide="trash-2" size="13"></i> Delete
@@ -35,6 +43,46 @@
</select>
@if($isEdit)
@if(!$news && $entry && !$entry->featured )
<template x-teleport="body">
<div
class="modal-overlay"
x-cloak
x-show="featuredOpen"
x-transition.opacity
@click.self="featuredOpen = false"
@keydown.escape.window="featuredOpen = false"
@modal:opened.window="refreshIcons($el)"
>
<div class="modal-window" x-show="featuredOpen" x-transition>
<div class="modal-header">
<span class="modal-title">Request as featured</span>
<button type="button" class="modal-close" @click="featuredOpen = false">
<i data-lucide="x" size="20"></i>
</button>
</div>
<div class="modal-body" id="entry-featured-body">
<p style="margin-bottom: 1.5rem; color: var(--text, #333);">
Please do not overuse this feature. Send only one request at a time.
</p>
<div class="queue-mod-actions" id="entry-featured-actions">
<button type="button" class="btn" @click="featuredOpen = false">
Cancel
</button>
<button type="button" class="btn" @click="Submission().requestFeatured({{ $entry->id }})">
<i data-lucide="star" size="14"></i>
Request as featured
</button>
</div>
</div>
</div>
</div>
</template>
@endif
<template x-teleport="body">
<div
class="modal-overlay"
@@ -58,7 +106,7 @@
Are you sure you want to delete this entry? This action cannot be undone.
</p>
<form action="{{ route('submit.destroy', ['section' => $section, 'entry' => $entry ]) }}" method="POST">
<form action="{{ $news ? route('news.destroy', [ 'news' => $news ] ) : route('submit.destroy', ['section' => $section, 'entry' => $entry ]) }}" method="POST">
@csrf
@method('DELETE')

View File

@@ -46,7 +46,7 @@
{{-- Users --}}
@can('create','\App\Models\Entry')
<a href="#" class="btn">
<a href="{{ route('submit.index') }}" class="btn">
<i data-lucide="hard-drive-upload" size="18"></i>
</a>
@endcan
@@ -78,7 +78,7 @@
@include('components.conversations')
</div>
@endif
<div x-data style="position: relative;" x-init="$store.settings.xfUrls = { 'default': '{{ xfStyleVariationUrl( 'default' ) }}', 'alternate': '{{ xfStyleVariationUrl( 'alternate' ) }}' }">
<div x-data style="position: relative;" x-init="$store.settings.xfUrls = { 'default': '{{ xfStyleVariationUrl( 'default' ) }}', 'alternate': '{{ xfStyleVariationUrl( 'alternate' ) }}' }; $store.settings.currentTheme = '{{ userTheme() }}'">
<button
type="button"
class="btn"

View File

@@ -1,9 +1,12 @@
<div class="grid-c2" style="margin-top:1%;">
<div id="reviews-section">
<div class="entry-content">
<x-entry-section-title label="Reviews" icon="star" />
@php if( !isset($entry) && isset($news) ){ $entry = $news; $newsMode = true; } else { $newsMode = false; } @endphp
<div class="{{ !$newsMode ? 'grid-c2' : '' }}" style="margin-top:1%;">
@if( !$newsMode )
<div id="reviews-section">
<div class="entry-content">
<x-entry-section-title label="Reviews" icon="star" />
</div>
</div>
</div>
@endif
<div id="comments-section">
<div class="entry-content">
<x-entry-section-title label="Last comments" icon="message-circle" />

View File

@@ -3,6 +3,7 @@
@section('page-title', "My Drafts - " . config('app.name'))
@section('content')
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
<div class="page-title">
My Drafts
</div>
@@ -16,6 +17,9 @@
<span>{{ $drafts->total() }} draft{{ $drafts->total() > 1 ? 's' : '' }}</span>
</div>
<div class="drafts-list">
@foreach($newsDrafts as $draft)
@include('news.draft_item', ['news' => $draft])
@endforeach
@foreach($drafts as $draft)
@include('entries.draft_item', ['entry' => $draft])
@endforeach

View File

@@ -3,5 +3,9 @@
@section('page-title', "Database - " . config('app.name') )
@section('content')
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
<div class="page-title">
Database
</div>
@livewire('database')
@endsection

View File

@@ -279,7 +279,7 @@
<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">
<img src="https://img.youtube.com/vi/{{ $entry->getYoutubeVideoId() }}/maxresdefault.jpg">
<div class="play-trigger">
<i data-lucide="play"></i>
</div>

View File

@@ -3,10 +3,28 @@
@section('page-title', "Home - " . config('app.name') )
@section('content')
<div class="block">
Ceci est un block !
</div>
<x-error-block error-type="page-not-allowed" />
<x-xf-username-link user-id="2" />
@include('activity.latest-news')
@include('activity.featured-entries')
<div class="activity-tl-header" x-data x-init="$store.settings.currentActivityFilters = {{ Js::from($activeFilters) }};">
<h2 class="activity-tl-title">
<i data-lucide="radio-tower" size="18"></i>
Latest activity
</h2>
<div class="activity-tl-filters">
@foreach($viewFilters as $key => $config)
<button
class="activity-tl-filter {{ in_array($key, $activeFilters) ? 'active' : '' }}"
data-filter="{{ $key }}"
x-data
@click="$store.settings.toggleActivityFilter('{{$key}}');$el.classList.toggle('active');">
<i data-lucide="{{ $config['icon'] }}" size="12"></i>
{{ $config['label'] }}
</button>
@endforeach
</div>
</div>
@include('activity.timeline')
@endsection

View File

@@ -1,5 +1,5 @@
<!DOCTYPE html>
<html lang="en" class="{{ \Illuminate\Support\Facades\Cookie::get('theme', 'default') === 'alternate' ? 'light-mode' : '' }}">
<html lang="en" class="{{ userTheme() === 'alternate' ? 'light-mode' : '' }}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">

View File

@@ -44,6 +44,10 @@
<i data-lucide="trash-2" size="15"></i>
Deleted entries
</a>
<a href="{{ route('modcp.logs') }}" class="modcp-nav-item" {{ request()->routeIs('modcp.logs') ? 'active' : '' }}>
<i data-lucide="logs" size="15"></i>
Logs
</a>
@endcan
</div>

View File

@@ -0,0 +1,210 @@
<div>
<div class="log-filters" x-data="{ open: {{ $hasFilters ? 'true' : 'false' }} }">
<div class="log-filters-main">
<div class="log-search-wrap">
<i data-lucide="search" size="13"></i>
<input
wire:model.live.debounce.300ms="search"
type="text"
class="form-input"
placeholder="Search description, subject, channel…"
>
</div>
<select wire:model.live="event" class="form-select log-select">
<option value="">All events</option>
@foreach($events as $e)
<option value="{{ $e }}">{{ ucfirst($e) }}</option>
@endforeach
</select>
<select wire:model.live="logName" class="form-select log-select">
<option value="">All channels</option>
@foreach($logNames as $name)
<option value="{{ $name }}">{{ $name }}</option>
@endforeach
</select>
<button @click="open = !open" class="btn {{ $hasFilters ? 'btn--active' : '' }}">
<i data-lucide="sliders-horizontal" size="13"></i>
More filters
@if($hasFilters)
<span class="log-filter-dot"></span>
@endif
</button>
@if($search || $hasFilters)
<button wire:click="clearFilters" class="btn">
<i data-lucide="x" size="13"></i> Clear
</button>
@endif
</div>
<div
x-show="open"
x-transition:enter="log-transition-enter"
x-transition:leave="log-transition-leave"
class="log-filters-extra"
>
<div class="log-filters-extra-inner">
<div class="log-filter-field">
<label class="log-filter-label">From</label>
<input wire:model.live="dateFrom" type="date" class="form-input">
</div>
<div class="log-filter-field">
<label class="log-filter-label">To</label>
<input wire:model.live="dateTo" type="date" class="form-input">
</div>
<div class="log-filter-field">
<label class="log-filter-label">User ID</label>
<input
wire:model.live.debounce.400ms="causerId"
type="text"
class="form-input"
placeholder="XenForo user ID"
>
</div>
</div>
</div>
</div>
<div class="log-results-bar">
<span class="log-results-count">
<span wire:loading.class="log-loading">
{{ number_format($logs->total()) }} log entries
</span>
</span>
<span class="log-results-pages">
Page {{ $logs->currentPage() }} / {{ $logs->lastPage() }}
</span>
</div>
@if($logs->isEmpty())
<div class="modcp-empty">
<i data-lucide="search-x" size="36"></i>
<p>No logs match your search.</p>
</div>
@else
<div class="modcp-list">
@foreach($logs as $log)
@php
$old = $log->properties->get('old', []);
$attrs = $log->properties->get('attributes', []);
$extra = array_diff_key($log->properties->toArray(), array_flip(['old', 'attributes']));
$hasDiff = !empty($old) || !empty($attrs) || !empty($extra);
@endphp
<div
class="modcp-list-item log-item"
x-data="{ open: false }"
:class="open && 'log-item--open'"
>
<div class="log-event-dot log-event-dot--{{ $log->event ?? 'custom' }}">
@switch($log->event)
@case('created') <i data-lucide="plus" size="12"></i> @break
@case('updated') <i data-lucide="pen-line" size="12"></i> @break
@case('deleted') <i data-lucide="trash-2" size="12"></i> @break
@default <i data-lucide="zap" size="12"></i>
@endswitch
</div>
<div class="modcp-list-item-info">
<span class="modcp-list-item-title">{{ $log->description }}</span>
<div class="modcp-list-item-meta">
@if($log->log_name)
<span class="log-channel-badge">{{ $log->log_name }}</span>
<span class="log-sep">·</span>
@endif
@if($log->subject_type)
<span>
{{ class_basename($log->subject_type) }}
<span class="log-id">#{{ $log->subject_id }}</span>
</span>
<span class="log-sep">·</span>
@endif
@if($log->causer_id)
<span>
<i data-lucide="user" size="10"></i>
<x-xf-username-link :user-id="$log->causer_id" />
</span>
<span class="log-sep">·</span>
@endif
<span title="{{ $log->created_at->format('d M Y, H:i:s') }}">
{{ $log->created_at->diffForHumans() }}
</span>
</div>
</div>
<div class="log-item-right">
<span class="log-timestamp">{{ $log->created_at->format('d M Y, H:i') }}</span>
@if($hasDiff)
<button
@click="open = !open"
class="btn btn--sm log-expand-btn"
:class="open && 'btn--active'"
:aria-label="open ? 'Hide details' : 'Show details'"
>
<i data-lucide="chevron-down" size="12"
style="transition: transform .15s"
:style="open ? 'transform:rotate(180deg)' : ''">
</i>
</button>
@endif
</div>
</div>
@if($hasDiff)
<div x-show="open" x-transition class="log-properties">
@if(!empty($old) || !empty($attrs))
<div class="log-diff-label">Changes</div>
<table class="log-diff">
<thead>
<tr>
<th>Field</th>
<th class="log-diff-old-head">Before</th>
<th class="log-diff-new-head">After</th>
</tr>
</thead>
<tbody>
@foreach(array_unique(array_merge(array_keys((array)$old), array_keys((array)$attrs))) as $key)
<tr>
<td class="log-diff-key">{{ $key }}</td>
<td class="log-diff-old">
{{ is_array($old[$key] ?? null) ? json_encode($old[$key]) : ($old[$key] ?? '—') }}
</td>
<td class="log-diff-new">
{{ is_array($attrs[$key] ?? null) ? json_encode($attrs[$key]) : ($attrs[$key] ?? '—') }}
</td>
</tr>
@endforeach
</tbody>
</table>
@endif
@if(!empty($extra))
<div class="log-diff-label" style="margin-top: 10px">Properties</div>
<pre class="log-raw">{{ json_encode($extra, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE) }}</pre>
@endif
</div>
@endif
@endforeach
</div>
<div class="log-pagination">
{{ $logs->links() }}
</div>
@endif
</div>

View File

@@ -5,7 +5,7 @@
<div class="game-selector">
<div class="game-selector-level2">
<x-form-field-title name="Authors" helper="Person or Group who created this, if it's you add/select yourself." required="true" />
<input id="author-search" class="form-input" type="text" wire:model.live.debounce="search" placeholder="Search a game..." autocomplete="off"
<input id="author-search" class="form-input" type="text" wire:model.live.debounce="search" placeholder="Search an author..." autocomplete="off"
@focus="$wire.dropdown = $wire.search.length >= 2" @click.outside="$wire.dropdown = false" >
</div>
@if( $dropdown )

View File

@@ -6,7 +6,7 @@
placeholder="Search..."
class="form-input filter-bar-search"
>
@if( $search || $types || $platforms || $games || $statuses || $authors || $languages || $modifications )
@if( $search || $types || $platforms || $games || $statuses || $authors || $languages || $modifications || $categories || $levels || $systems )
<button type="button" wire:click="clearFilters" class="btn">
<i data-lucide="x"></i> Clear filters
</button>

View File

@@ -20,13 +20,18 @@
<div class="file-item">
<div class="file-info">
<span class="file-name">{{ $file->filename }}</span>
<span class="file-meta">{{ $file->filesize }} - 0 downloads</span>
<span class="file-meta">{{ $file->filesize }} - {{ $file->download_count }} downloads</span>
</div>
<div style="display:flex;flex-direction:column;gap:15px;">
<a href="{{ route('fs.download', ['entry_id' => $entryId, 'file' => $file->file_uuid] ) }}" class="btn primary"><i data-lucide="download"></i> Download</a>
@if( $file->online_patcher )
<a href="{{ route('tools.direct-patch', ['entry_id' => $entryId, 'file' => $file->file_uuid] ) }}" class="btn"><i data-lucide="stamp"></i> Patch</a>
@endif
@if( $file->playOnlineSetting )
@auth
<a href="{{ route('tools.play', ['entry_id' => $entryId, 'file' => $file->file_uuid] ) }}" class="btn"><i data-lucide="gamepad-2"></i> Try it online</a>
@endauth
@endif
</div>
</div>
@empty

View File

@@ -0,0 +1,41 @@
<div class="form-group">
<div class="game-selector">
<div class="game-selector-level2">
<x-form-field-title name="Related Entry" required="" />
<input id="entry-search" class="form-input" type="text" wire:model.live.debounce="search" placeholder="Search an entry..." autocomplete="off"
@focus="$wire.dropdown = $wire.search.length > 2" @click.outside="$wire.dropdown = false"
>
<input type="hidden" name="entry_id" value="{{ $selectedEntryId ?? '' }}" />
</div>
@if( $dropdown )
<ul class="game-selector-dropdown">
@forelse( $entries as $entry )
<li>
<button type="button"
wire:click="selectEntry({{ $entry->id }})" class="dropdown-item" {{ $selectedEntryId === $entry->id ? 'selected' : '' }}>
<span class="dropdown-item-name">
{{ $entry->complete_title ?? $entry->title }}
</span>
<span class="badge {{ $entry->type }}">
{{ \App\View\Components\EntryCard::ENTRY_TYPES_BADGE[$entry->type] ?? $entry->type }}
</span>
</button>
</li>
@empty
<li class="dropdown-empty">No entry found</li>
@endforelse
</ul>
@endif
</div>
<div style="display:flex;align-items: flex-end;justify-content: right;gap:15px;margin-top: 15px;">
@if($selectedEntryId)
<button type="button" class="btn" wire:click="clearEntry">
Remove
</button>
@endif
</div>
</div>

View File

@@ -0,0 +1,43 @@
<div @filters-updated.window="refreshIcons($el)">
<div class="database-search filter-bar">
<input type="text" wire:model.live.debounce="search"
placeholder="Search..." class="form-input filter-bar-search"
>
@if($search || $categories )
<button type="button" wire:click="clearFilters" class="btn">
<i data-lucide="x"></i> Clear filters
</button>
@endif
</div>
<div class="database-wrapper">
<aside class="database-filters">
{{-- Categories --}}
<x-database-filter-without-mode title="Categories" :items="$allCategories" model="categories" />
</aside>
<div class="database-results">
<div class="database-sort">
@foreach( \App\Livewire\NewsDatabase::SORT_OPTIONS as $k => $v )
<button type="button" wire:click="setSort('{{ $k }}')" class="btn {{ $sortBy === $k ? 'active' : '' }}">
{{ $v }}
</button>
@if( $sortBy === $k )
<i data-lucide="{{ $sortDir === 'asc' ? 'arrow-up' : 'arrow-down' }}"></i>
@endif
@endforeach
<span class="database-results-count">{{ $news->total() }} results</span>
</div>
<div class="news-grid">
@forelse($news as $item)
<x-news-card :news="$item" />
@empty
<p>No news found.</p>
@endforelse
</div>
{{ $news->links() }}
</div>
</div>
</div>

View File

@@ -0,0 +1,14 @@
@extends('layouts.modcp')
@section('page-title', 'Logs — ' . config('app.name'))
@section('modcp-content')
<div class="modcp-page-title">
<i data-lucide="activity" size="20"></i>
Logs
</div>
@livewire('activity-logs')
@endsection

View File

@@ -0,0 +1,9 @@
@extends('layouts.app')
@section('page-title', "Submit News - " . config('app.name') )
@section('content')
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
<div class="page-title">Submit a news</div>
@include('news.form')
@endsection

View File

@@ -0,0 +1,51 @@
<div class="drafts-item">
<div class="drafts-cover">
@if($news->gallery()->first())
<img src="{{ Storage::url($news->gallery()->first()->image) }}">
@else
<div class="drafts-cover-placeholder">
<i data-lucide="image" size="24"></i>
</div>
@endif
</div>
<div class="drafts-info">
<div class="drafts-top">
<div>
<h3 class="drafts-title">
{{ $news->title }}
</h3>
<div class="drafts-meta">
<span class="badge news">
News
</span>
@if( $news->category_id )
<span class="badge">{{ $news->category->name }}</span>
@endif
</div>
</div>
<div class="drafts-dates">
<span>
<i data-lucide="pencil" size="12"></i>
Last edited {{ $draft->updated_at->diffForHumans() }}
</span>
<span>
<i data-lucide="calendar" size="12"></i>
Created {{ $draft->created_at->format('d M Y') }}
</span>
</div>
</div>
<div class="drafts-actions">
<a href="{{ route('news.edit', ['news' => $news]) }}" class="btn primary">
<i data-lucide="pen" size="13"></i>
Continue editing
</a>
<a href="{{ route('news.show', ['news' => $news] ) }}" class="btn" target="_blank">
<i data-lucide="eye" size="13"></i>
Preview
</a>
</div>
</div>
</div>

View File

@@ -0,0 +1,9 @@
@extends('layouts.app')
@section('page-title', "Edit $news->title - " . config('app.name') )
@section('content')
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
<div class="page-title">Submit a news</div>
@include('news.form')
@endsection

View File

@@ -0,0 +1,104 @@
@push('styles')
<meta name="csrf-token" content="{{ csrf_token() }}">
<meta name="submission-has-errors" content="{{ $errors->any() ? '1' : '0' }}">
@endpush
@push('scripts')
@vite('resources/js/news-submissions.js')
@endpush
{{-- Server side errors summary --}}
@if($errors->any())
@foreach( $errors->all() as $error )
<x-form-error-text message="{{ $error }}" />
@endforeach
@endif
<div class="block">
<form action="{{ $isEdit ? route('news.update', [$news->id ]) : route('news.store') }}" method="POST" x-data="NewsSubmission()" x-init="init()" @submit.prevent="submitForm($event)">
<x-form-group-title label="About the news" icon="puzzle" />
<div class="form-group grid-c2">
<div>
<x-form-field-title name="Title" required="true" />
<input class="form-input" type="text" name="title" value="{{ old('title', $news->title ?? '' ) }}" required>
@error('title')
<x-form-error-text message="{{ $message }}" />
@enderror
</div>
<div>
<x-form-field-title name="Categories" required="true" />
<x-category-selector section="news" :selected="$oldCategory" :required="true" :news="true" />
</div>
</div>
<div class="form-group" x-ref="descriptionField">
<x-form-field-title name="Content" required="true" />
<x-markdown-textarea name="description" value="{{ old('description', $news->description ?? '') }}" />
<div class="form-error-text" x-show="errorKey === 'noDescription'" x-text="errorMessage"></div>
@error('description')
<x-form-error-text message="{{ $message }}" />
@enderror
</div>
<x-form-group-title label="Attachments" icon="paperclip" />
<x-gallery-field :old-paths="old('gallery', $news->gallery->pluck('image')->toArray() ?? [] )"/>
@error('gallery')
<x-form-error-text message="{{ $message }}" />
@enderror
@error('gallery.*')
<x-form-error-text message="{{ $message }}" />
@enderror
<x-form-group-title label="Related Links" icon="link" />
<livewire:entry-selector :old-entry-id="old('entry_id', $news->entry_id )" />
<div class="form-group grid-c2">
<div>
<x-form-field-title name="Release Site" required="" />
<input class="form-input" type="url" name="release_site" value="{{ old( 'release_site', $news->relevant_link ?? '' ) }}">
</div>
<div>
<x-form-field-title name="Youtube Video" required="" />
<input class="form-input" type="url" name="youtube_video" value="{{ old( 'youtube_video', $news->youtube_link ?? '' ) }}">
</div>
</div>
@if($isEdit)
<x-form-group-title label="News Management" icon="wrench" />
@can('moderate',$news)
<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="old('owner_user_id', $news->user_id)" />
</div>
</div>
<div class="form-group grid-c2">
<div>
<x-form-field-title name="XenForo Comments Thread ID" />
<input type="text" name="comments_thread_id" class="form-input" value="{{ old('comments_thread_id', $news->comments_thread_id) }}">
</div>
</div>
<div class="form-group">
<x-form-field-title name="Metadata" required="true" />
<div class="form-type-of-checkboxes form-group level" id="entry-metadata">
</div>
</div>
@endcan
@cannot('moderate', $news)
@endcannot
@endif
@csrf
<div class="submit">
<x-submit-entry-status section="news" :is-edit="$isEdit" :current-state="$news->state ?? null" :entry="$news" :news="true" />
<button id="submit-button" type="submit" class="btn primary" style="padding:1%;">Submit</button>
</div>
</form>
</div>

View File

@@ -0,0 +1,11 @@
@extends('layouts.app')
@section('page-title', "News - " . config('app.name') )
@section('content')
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
<div class="page-title">
News
</div>
@livewire('news-database')
@endsection

View File

@@ -0,0 +1,225 @@
@extends('layouts.app')
@section('page-title', $news->title . " - " . config('app.name') )
@section('content')
{{ Diglactic\Breadcrumbs\Breadcrumbs::render() }}
<article id="news-container">
<div class="news-header"
style="background-image: linear-gradient(rgba(0,0,0,0.4), var(--bg2)), url('{{ $news->gallery()->first() ? Storage::url($news->gallery()->first()->image ) : '' }}')">
<div class="news-header-content">
<h1 class="news-title">
{{ $news->title }}
@if( $news->state === 'pending' )
<div style="display:inline;color:var(--rhpz-orange);">
-
<i data-lucide="clock" size="24"></i>
Pending approval
</div>
@elseif( $news->state === 'rejected' )
<div style="display:inline;color:var(--error);">
-
<i data-lucide="x-circle" size="24"></i>
Rejected
</div>
@elseif( $news->state === 'locked' )
<div style="display:inline;color:var(--error);">
-
<i data-lucide="lock" size="24"></i>
Locked
</div>
@elseif( $news->state === 'draft' )
<div style="display:inline;color:var(--rhpz-orange);">
-
<i data-lucide="scissors" size="24"></i>
Draft
</div>
@elseif( $news->state === 'hidden' )
<div style="display:inline;color:var(--rhpz-orange);">
-
<i data-lucide="hide" size="24"></i>
Hidden
</div>
@endif
</h1>
<div class="news-meta">
@if($news->category_id)
<a href="#" class="meta-item">
<i data-lucide="book" size="16"></i>
{{ $news->category->name }}
</a>
@endif
@if($news->user_id)
<span class="meta-item">
<i data-lucide="user" size="16"></i>
Posted by <x-xf-username-link :user-id="$news->user_id"/>
</span>
@endif
<span class="meta-item">
<i data-lucide="calendar" size="16"></i>
{{ $news->created_at->format('M d, Y') }}
</span>
@if($news->updated_at && $news->updated_at->gt($news->created_at))
<span class="meta-item">
<i data-lucide="file-edit" size="16"></i>
Updated {{ $news->updated_at->diffForHumans() }}
</span>
@endif
</div>
<div class="news-actions">
@if($news->state === 'pending')
@can('approve', $news)
<div x-data="{ rejectOpen: false }">
<form action="{{ route('queue.news.approve', $news) }}" method="POST" style="display:inline">
@csrf @method('PATCH')
<button type="submit" class="btn success" onclick="return confirm('Approve this news?')">
<i data-lucide="check-circle" size="14"></i> Approve
</button>
</form>
<button type="button" class="btn danger" @click="rejectOpen = true">
<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">
<div class="modal-window" x-show="rejectOpen" x-transition>
<div class="modal-header">
<span class="modal-title">Reject news</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.news.reject', $news) }}" 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" 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
@can('update', $news)
<a href="{{ route('news.edit', $news) }}" class="btn">
<i data-lucide="edit" size="14"></i> Edit
</a>
@endcan
@auth
<a href="{{ xfRoute("romhackplaza_news/{$news->id}/report") }}" class="btn">
<i data-lucide="flag" size="14"></i> Report
</a>
@endauth
</div>
</div>
</div>
<div class="news-layout">
<div class="news-main-content entry-content">
@if( $news->description )
<div class="news-body-text">
{{ $news->description }}
</div>
@endif
@if( $news->gallery->isNotEmpty() )
<div x-data="{ open: false, currentImage: ''}" x-cloak>
<x-entry-section-title label="Gallery" icon="images"/>
<div class="entry-gallery">
@foreach( $news->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
</div>
<aside class="news-sidebar">
@if($news->entry->exists())
<div class="sidebar-block">
<h3 class="sidebar-title">
<i data-lucide="content" size="18"></i>
Related entry
</h3>
<div class="related-card">
@if( $news->entry->main_image )
<div class="related-card-cover">
<img src="{{ Storage::url($news->entry->main_image) }}">
</div>
@endif
<div class="related-card-info">
<h4>{{ $news->entry->title }}</h4>
<a href="{{ route('entries.show', ['section' => $news->entry->type, 'entry' => $news->entry ]) }}" class="btn-sidebar" class="btn-orange">
Go to the entry
</a>
</div>
</div>
</div>
@endif
@if($news->relevant_link)
<div class="sidebar-block">
<h3 class="sidebar-title">
<i data-lucide="link" size="18"></i>
Relevant link
</h3>
<a href="{{ $news->relevant_link }}" target="_blank" rel="noopener">
{{ $news->relevant_link }}
</a>
</div>
@endif
@if( $news->youtube_link )
<div x-data="{open: false, src: ''}" x-cloak class="sidebar-block youtube-section">
<h3 class="sidebar-title">
<i data-lucide="play" size="18"></i>
Youtube video
</h3>
<div class="video-thumbnail-wrapper"
@click="src = 'https://www.youtube.com/embed/{{ $news->getYoutubeVideoId() }}?autoplay=1'; open = true">
<img src="https://img.youtube.com/vi/{{ $news->getYoutubeVideoId() }}/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
</aside>
</div>
</article>
@include('entries.comments')
@endsection

View File

@@ -3,6 +3,7 @@
@section('page-title', "Submissions Queue - " . config('app.name') )
@section('content')
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
<div class="page-title">
Submissions Queue
</div>

View File

@@ -7,7 +7,7 @@
>
<div class="queue-item-header">
<div class="queue-item-info">
<h3 class="queue-item-title">{{ $entry->complete_title }}</h3>
<h3 class="queue-item-title">{{ $entry->complete_title ?? $entry->title }}</h3>
@if($entry->state === 'rejected')
<span class="badge badge--danger">
<i data-lucide="x-circle" size="12"></i>
@@ -23,20 +23,35 @@
<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>
@if($entry->queue_type === 'entry' )
<span class="badge {{ $entry->type }}">{{ \App\View\Components\EntryCard::ENTRY_TYPES_BADGE[$entry->type] ?? $entry->type }}</span>
@else
<span class="badge news">News</span>
@endif
</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>
@if( $entry->queue_type === 'entry' )
<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>
@else
<a href="{{ route('news.show', ['news' => $entry ]) }}" class="btn" target="_blank">
<i data-lucide="external-link" size="14"></i>
View Entry
</a>
<a href="{{ route('news.edit', ['news' => $entry ] ) }}" class="btn" target="_blank">
<i data-lucide="pen" size="14"></i>
Edit
</a>
@endif
</div>
@endcan
</div>
@@ -97,7 +112,7 @@
@can('approve',$entry)
@if($entry->state === 'pending')
<div class="queue-mod-zone">
<form action="{{ route('queue.comment', $entry ) }}" method="POST">
<form action="{{ $entry->queue_type === 'entry' ? route('queue.comment', $entry ) : route('queue.news.comment', $entry ) }}" method="POST">
@csrf
@method('PATCH')
<div class="form-group">
@@ -114,7 +129,7 @@
<div class="queue-mod-separator"></div>
<form action="{{ route('queue.approve', $entry) }}" method="POST" style="display:inline">
<form action="{{ $entry->queue_type === 'entry' ? route('queue.approve', $entry) : route('queue.news.approve', $entry) }}" method="POST" style="display:inline">
@csrf
@method('PATCH')
<button type="submit" class="btn success" onclick="return confirm('Approve this entry?')">
@@ -130,7 +145,7 @@
</button>
<div x-show="open" x-cloak class="queue-reject-form">
<form action="{{ route('queue.reject', $entry) }}" method="POST">
<form action="{{ $entry->queue_type === 'entry' ? route('queue.reject', $entry) : route('queue.news.reject', $entry) }}" method="POST">
@csrf
@method('PATCH')
<div class="form-group">

View File

@@ -28,7 +28,7 @@
{{--
File listing. Used for editions or server-side errors.
--}}
<div class="upload-list" x-show="numberOfFiles > 0" x-cloak>
<div class="upload-list" x-show="numberOfFiles > 0" x-cloak style="margin-bottom: 15px">
<template x-for="(file,i) in files" :key="i">
<div class="upload-item" :class="
{
@@ -132,17 +132,33 @@
<div class="modal-content">
@if( section_must_be( ['translations', 'romhacks'], $section ) )
<div class="form-group">
<x-form-group-title label="Online patcher" />
<label>
<input type="checkbox" class="form-checkbox" x-model="file.meta_online_patcher">
Enable it
</label>
<label x-show="file.meta_online_patcher">
<input type="checkbox" class="form-checkbox" x-model="file.meta_secondary_online_patcher">
Mark as a secondary patch
</label>
</div>
<template x-if="file.can_be_online_patched">
<div class="form-group">
<x-form-group-title label="Online patcher" />
<label>
<input type="checkbox" class="form-checkbox" x-model="file.meta_online_patcher">
Enable it
</label>
</div>
</template>
@endif
@if( section_must_be( ['translations', 'romhacks', 'homebrew' ], $section ) )
<template x-if="file.can_be_online_patched">
<div class="form-group">
<x-form-group-title label="Play online" />
<label>
<input type="checkbox" class="form-checkbox" x-model="file.meta_play_online">
Enable it
</label>
<div class="form-group level" x-show="file.meta_play_online">
@include('submissions.play-online-core-select')
<label>
<input type="checkbox" class="form-checkbox" x-model="file.meta_play_online_threads">
Enable Threads
</label>
</div>
</div>
</template>
@endif
</div>
</div>
@@ -163,6 +179,17 @@
:name="'files_metadata[' + file.uuid + '][secondary_online_patcher]'"
:value="(file.meta_online_patcher && file.meta_secondary_online_patcher) ? 1 : 0">
@endif
@if( section_must_be( ['translations', 'romhacks', 'homebrew'], $section ) )
<input type="hidden"
:name="'files_metadata[' + file.uuid + '][play_online]'"
:value="file.meta_play_online ? 1 : 0">
<input type="hidden"
:name="'files_metadata[' + file.uuid + '][play_online_core]'"
:value="file.meta_play_online_core ? file.meta_play_online_core : ''">
<input type="hidden"
:name="'files_metadata[' + file.uuid + '][play_online_threads]'"
:value="file.meta_play_online_threads ? 1 : 0">
@endif
</div>
</template>
@endif

View File

@@ -0,0 +1,73 @@
@extends('layouts.app')
@section('page-title', 'Submit - ' . config('app.name'))
@section('content')
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
<div class="page-title">Submit content</div>
<div class="submit-body">
<div class="submit-section-label">Entries</div>
<div class="submit-grid">
@foreach($entryTypes as $type)
<a href="{{ route('submit.create', ['section' => $type['slug']]) }}"
class="submit-card"
style="--card-color: {{ $type['color'] }}; --card-bg: {{ $type['bg'] }}; --card-border: {{ $type['border'] }}">
<div class="submit-card-top">
<div class="submit-card-icon">
<i data-lucide="{{ $type['icon'] }}" size="17"></i>
</div>
<div class="submit-card-info">
<div class="submit-card-title">{{ $type['label'] }}</div>
</div>
</div>
<div class="submit-card-bottom">
<span class="submit-card-cta">
Submit <i data-lucide="arrow-right" size="11"></i>
</span>
</div>
</a>
@endforeach
</div>
<div class="submit-section-label">News</div>
<div class="submit-news-row" style="display:flex;align-items: center;justify-content: center">
<a href="{{ route('news.create') }}" class="submit-news-card">
<div class="submit-news-icon">
<i data-lucide="newspaper" size="17"></i>
</div>
<div class="submit-news-info">
<div class="submit-news-title">News article</div>
</div>
<span class="submit-news-cta">
Submit <i data-lucide="arrow-right" size="11"></i>
</span>
</a>
</div>
<div class="submit-section-label">Rules</div>
<div class="submit-rules">
<div class="submit-rule">
<span class="submit-rule-num">1</span>
<p><strong>One entry per submission.</strong> Do not bundle multiple hacks or versions in a single form.</p>
</div>
<div class="submit-rule">
<span class="submit-rule-num">2</span>
<p><strong>You must be the author</strong> or have explicit permission from the original creator.</p>
</div>
<div class="submit-rule">
<span class="submit-rule-num">3</span>
<p><strong>Patch files only.</strong> Never upload ROM files attach an IPS, BPS or xdelta patch.</p>
</div>
</div>
</div>
@endsection

View File

@@ -0,0 +1,8 @@
<div x-data>
<x-form-field-title name="Core" required="true" />
<select x-model="file.meta_play_online_core" class="form-select" style="margin-bottom:15px;">
@foreach( \App\Helpers\PlayOnlineHelpers::getCoreLists() as $core )
<option value="{{ $core }}" :selected="file.meta_play_online_core === '{{$core}}'">{{ $core }}</option>
@endforeach
</select>
</div>

View File

@@ -4,6 +4,7 @@
@push('scripts')
<script type="text/javascript" src="{{ asset('rom-patcher-js/RomPatcher.webapp.js') }}"></script>
@vite('resources/js/RomPatcher.js')
@endpush
@section('content')
@@ -88,5 +89,9 @@
<i data-lucide="wrench" size="16"></i> Apply patch
</button>
</div>
<div id="rom-patcher-row-error-message" class="block-error" style="display:none">
<span id="rom-patcher-error-message"></span>
</div>
</div>
@endsection

View File

@@ -0,0 +1,102 @@
@extends('layouts.app')
@section('page-title', "Play Online - " . config('app.name'))
@push('scripts')
<script type="text/javascript" src="{{ asset('rom-patcher-js/RomPatcher.webapp.js') }}"></script>
@vite('resources/js/PlayOnlineAndPatcher.js')
@endpush
@section('content')
<div class="page-title">
<span>Play Online</span>
</div>
<div id="rom-patcher-container" class="patcher-container" x-data="PlayOnline({{ Js::from($patches ?? []) }}, {{ JS::from($emuConfig ?? [])}})">
<div x-show="!launchGame">
<div class="patcher-grid">
<div class="form-group">
<label class="form-label">1. Original ROM</label>
<div class="patcher-dropzone" id="rom-dropzone"
:class="{ 'dragover': isRomDragOver, 'has-file': romFileName !== '' }"
@click="triggerFileInput('rom-patcher-input-file-rom')"
@dragover.prevent="isRomDragOver = true"
@dragleave.prevent="isRomDragOver = false"
@drop.prevent="isRomDragOver = false; handleDrop($event, 'rom')">
<input type="file" id="rom-patcher-input-file-rom" style="display: none;" @change="handleInputChange($event, 'rom')" disabled />
<i data-lucide="gamepad-2" size="40" :style="romFileName ? 'color: var(--rhpz-orange)' : 'color: var(--text2)'"></i>
<span style="color: var(--text); font-weight: 500;" x-text="romFileName ? romFileName : 'Drag n\'drop your file here or click'"></span>
</div>
</div>
<div class="form-group">
<label class="form-label">2. Patch file</label>
<div class="patcher-dropzone" id="patch-dropzone"
x-show="!hasEmbedded"
:class="{ 'dragover': isPatchDragOver, 'has-file': patchFileName !== '' }"
@click="triggerFileInput('rom-patcher-input-file-patch')"
@dragover.prevent="isPatchDragOver = true"
@dragleave.prevent="isPatchDragOver = false"
@drop.prevent="isPatchDragOver = false; handleDrop($event, 'patch')">
<input type="file" id="rom-patcher-input-file-patch" style="display: none;" @change="handleInputChange($event, 'patch')" disabled />
<i data-lucide="file-archive" size="40" :style="patchFileName ? 'color: var(--rhpz-orange)' : 'color: var(--text2)'"></i>
<span style="color: var(--text); font-weight: 500;" x-text="patchFileName ? patchFileName : 'Drag n\'drop your file here or click'"></span>
</div>
<div x-show="hasEmbedded" class="embed-patch-box">
<div class="embed-patch-box-icon">
<div class="embed-patch-box-icon-block">
<i data-lucide="package" size="24" color="var(--rhpz-orange)"></i>
</div>
<div>
<div style="font-weight: 600; color: var(--text); font-size: 1.1rem;">Patch file</div>
<div style="font-size: 0.85rem; color: var(--text2);">Select if there is multiple patch files</div>
</div>
</div>
<div class="rom-patcher-container-input" style="margin-top: 10px;">
<select id="rom-patcher-select-patch" class="form-select" style="width: 100%; cursor: pointer;"></select>
</div>
</div>
</div>
</div>
<div class="patcher-status-box" id="patcher-status" x-show="showStatusBox" x-transition x-cloak style="margin-top: 20px;">
<div class="rom-patcher-row" style="color: var(--text2);">
<div style="color: var(--rhpz-orange); font-weight: bold;">Checksums:</div>
<ul style="margin-bottom: 0">
<li>CRC32: <span id="rom-patcher-span-crc32"></span></li>
<li>MD5: <span id="rom-patcher-span-md5"></span></li>
<li>SHA-1: <span id="rom-patcher-span-sha1"></span></li>
</ul>
</div>
<span id="rom-patcher-span-rom-info"></span>
<div class="rom-patcher-row margin-bottom" id="rom-patcher-row-patch-description">
<div style="color: var(--rhpz-orange); font-weight: bold;">Description:</div>
<div id="rom-patcher-patch-description"></div>
</div>
<div class="rom-patcher-row margin-bottom" id="rom-patcher-row-patch-requirements">
<div id="rom-patcher-patch-requirements-type" style="color: var(--rhpz-orange); font-weight: bold;">ROM requirements:</div>
<div id="rom-patcher-patch-requirements-value"></div>
</div>
</div>
<div style="margin-top: 25px; border-top: 1px solid var(--border); padding-top: 20px; text-align: right;">
<button type="button" class="btn primary" id="rom-patcher-button-apply" disabled>
<i data-lucide="wrench" size="16"></i> Launch Game
</button>
</div>
<div id="rom-patcher-row-error-message" class="block-error" style="display:none">
<span id="rom-patcher-error-message"></span>
</div>
</div>
<div id="game" x-show="launchGame"></div>
</div>
@endsection