Initial commit
This commit is contained in:
26
resources/css/app.css
Normal file
26
resources/css/app.css
Normal file
@@ -0,0 +1,26 @@
|
||||
@import './base/variables.css';
|
||||
@import './base/reset.css';
|
||||
|
||||
@import './layout/menu.css';
|
||||
@import './layout/content.css';
|
||||
@import './layout/entry.css';
|
||||
@import './layout/news.css';
|
||||
|
||||
@import './components/common.css';
|
||||
@import './components/grid.css';
|
||||
@import './components/forms.css';
|
||||
@import './components/cards.css';
|
||||
@import './components/modal.css';
|
||||
@import './components/files.css';
|
||||
|
||||
@import './components/easymde.css';
|
||||
|
||||
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
|
||||
@source '../../storage/framework/views/*.php';
|
||||
@source '../**/*.blade.php';
|
||||
@source '../**/*.js';
|
||||
|
||||
@theme {
|
||||
--font-sans: 'Instrument Sans', ui-sans-serif, system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
|
||||
'Segoe UI Symbol', 'Noto Color Emoji';
|
||||
}
|
||||
37
resources/css/base/reset.css
Normal file
37
resources/css/base/reset.css
Normal file
@@ -0,0 +1,37 @@
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: var(--typography);
|
||||
background-color: var(--bg);
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
a {
|
||||
color: var(--rhpz-orange);
|
||||
text-decoration: none;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
a:hover {
|
||||
color: var(--rhpz-orange-hover);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
#app {
|
||||
display: flex;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
ul {
|
||||
margin-left: 20px;
|
||||
margin-bottom: 20px;
|
||||
list-style-type: square;
|
||||
}
|
||||
31
resources/css/base/variables.css
Normal file
31
resources/css/base/variables.css
Normal file
@@ -0,0 +1,31 @@
|
||||
:root {
|
||||
|
||||
/* RHPZ color */
|
||||
--rhpz-orange: #ff7300;
|
||||
--rhpz-orange-hover: #e56700;
|
||||
|
||||
/* Background colors */
|
||||
--bg: #1e1e1e;
|
||||
--bg2: #252526;
|
||||
--bg3: #2d2d30;
|
||||
--bg4: #3e3e42;
|
||||
|
||||
/* Text */
|
||||
--text: #f1f1f1;
|
||||
--text2: #a1a1aa;
|
||||
--text3: #111111;
|
||||
|
||||
/* Elements */
|
||||
--border: #3f3f46;
|
||||
--error: #e57373;
|
||||
--info: #1976d2;
|
||||
--success: #81c784;
|
||||
--success2: #388e3c;
|
||||
|
||||
/* Typo */
|
||||
--typography: 'Segoe UI', 'San Francisco', 'Helvetica Neue', sans-serif;
|
||||
|
||||
/* Menu settings */
|
||||
--menu-size: 260px;
|
||||
--menu-user-avatar-bg: #555;
|
||||
}
|
||||
24
resources/css/components/cards.css
Normal file
24
resources/css/components/cards.css
Normal file
@@ -0,0 +1,24 @@
|
||||
/* STAT CARDS */
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 20px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
border-left: 3px solid var(--rhpz-orange);
|
||||
}
|
||||
|
||||
.stat-card i {
|
||||
color: var(--rhpz-orange);
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
145
resources/css/components/common.css
Normal file
145
resources/css/components/common.css
Normal file
@@ -0,0 +1,145 @@
|
||||
/* BUTTONS */
|
||||
|
||||
.btn {
|
||||
background-color: var(--bg3);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 8px 16px;
|
||||
font-size: 0.9rem;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
transition: all 0.1s;
|
||||
}
|
||||
.btn:hover {
|
||||
background-color: var(--bg4);
|
||||
border-color: var(--menu-user-avatar-bg);
|
||||
}
|
||||
.btn.primary {
|
||||
background-color: var(--rhpz-orange);
|
||||
color: var(--text3);
|
||||
border-color: var(--rhpz-orange);
|
||||
font-weight: 600;
|
||||
}
|
||||
.btn.primary:hover {
|
||||
background-color: var(--rhpz-orange-hover);
|
||||
border-color: var(--rhpz-orange-hover);
|
||||
}
|
||||
.btn.danger {
|
||||
background-color: transparent;
|
||||
color: var(--error);
|
||||
border-color: var(--error);
|
||||
}
|
||||
.btn.danger:hover {
|
||||
background-color: rgba(229, 115, 115, 0.1);
|
||||
}
|
||||
.btn.success {
|
||||
background-color: transparent;
|
||||
color: var(--success);
|
||||
border-color: var(--success);
|
||||
}
|
||||
.btn.success:hover {
|
||||
background-color: rgba(129, 199, 132, 0.1);
|
||||
}
|
||||
|
||||
/* BLOCK */
|
||||
|
||||
.block {
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.block.featured {
|
||||
border-left: 3px solid var(--rhpz-orange);
|
||||
}
|
||||
.block-header {
|
||||
font-size: 1.2rem;
|
||||
color: var(--text);
|
||||
margin-bottom: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
.block-error {
|
||||
background-color: var(--error);
|
||||
border: 1px solid var(--error);
|
||||
color: var(--text);
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
/* BADGES */
|
||||
.badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
font-size: 0.75rem;
|
||||
font-weight: bold;
|
||||
border-radius: 2px;
|
||||
background-color: var(--bg3);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.badge.orange {
|
||||
background-color: var(--rhpz-orange);
|
||||
color: var(--text3);
|
||||
border-color: var(--rhpz-orange);
|
||||
}
|
||||
.badge.blue {
|
||||
background-color: var(--info);
|
||||
color: var(--text);
|
||||
border-color: var(--info);
|
||||
}
|
||||
.badge.green {
|
||||
background-color: var(--success2);
|
||||
color: var(--text);
|
||||
border-color: var(--success2);
|
||||
}
|
||||
|
||||
/* BREADCRUMB */
|
||||
|
||||
.breadcrumb {
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
/* PAGE */
|
||||
|
||||
.page-title {
|
||||
font-size: 1.8rem;
|
||||
font-weight: 300;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
/* TEXTS */
|
||||
|
||||
.whisper {
|
||||
color: var(--text2);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.content-title {
|
||||
color: var(--text);
|
||||
margin: 30px 0 15px 0;
|
||||
border-left: 3px solid var(--rhpz-orange);
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.quote {
|
||||
background-color: var(--bg);
|
||||
border-left: 4px solid #1976d2;
|
||||
padding: 15px;
|
||||
margin-top: 30px;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* ANIMATIONS */
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
.spin {
|
||||
animation: spin 1s infinite linear;
|
||||
}
|
||||
80
resources/css/components/easymde.css
Normal file
80
resources/css/components/easymde.css
Normal file
@@ -0,0 +1,80 @@
|
||||
.EasyMDEContainer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.editor-toolbar {
|
||||
background-color: var(--bg3);
|
||||
border:1px solid var(--border);
|
||||
border-bottom: none;
|
||||
border-radius: var(--radius-md) var(--radius-md) 0 0;
|
||||
padding: 4px 8px;
|
||||
opacity: 1;
|
||||
|
||||
button {
|
||||
color: var(--text2);
|
||||
border: none;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg2);
|
||||
color: var(--text)
|
||||
}
|
||||
.active {
|
||||
background-color: var(--bg2);
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
i.separator {
|
||||
border-left: 1px solid var(--border);
|
||||
margin: 0 4px;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror {
|
||||
background-color: var(--bg2);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border) !important;
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--rhpz-orange);
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.CodeMirror-cursor {
|
||||
border-left: 2px solid var(--text2);
|
||||
}
|
||||
|
||||
.CodeMirror-selected {
|
||||
background: color-mix(in srgb, var(--rhpz-orange-hover) 25%, transparent);
|
||||
}
|
||||
|
||||
.cm-header {
|
||||
color: var(--rhpz-orange-hover);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cm-strong {
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.cm-em {
|
||||
color: var(--text2);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
&.cm-link, &.cm-url {
|
||||
color: var(--rhpz-orange);
|
||||
}
|
||||
|
||||
.cm-strikethrough, .cm-comment {
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.editor-preview, .editor-preview-side {
|
||||
background: var(--bg2);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
}
|
||||
35
resources/css/components/files.css
Normal file
35
resources/css/components/files.css
Normal file
@@ -0,0 +1,35 @@
|
||||
.file-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.file-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 12px 15px;
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--rhpz-orange);
|
||||
}
|
||||
|
||||
.file-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.file-name {
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.file-meta {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text2);
|
||||
}
|
||||
}
|
||||
}
|
||||
461
resources/css/components/forms.css
Normal file
461
resources/css/components/forms.css
Normal file
@@ -0,0 +1,461 @@
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.form-group.level {
|
||||
background-color: var(--bg3);
|
||||
padding: 25px;
|
||||
border: 1px dashed var(--border);
|
||||
margin-bottom: 35px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.form-group-title {
|
||||
color: var(--text);
|
||||
margin-bottom: 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
font-size: 1.15rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-group label, .form-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.form-group label span, .form-label span {
|
||||
color: var(--text2);
|
||||
font-weight: normal;
|
||||
font-size: 0.8rem;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.form-error-text {
|
||||
margin-top: 10px;
|
||||
font-size: 0.85rem;
|
||||
color: var(--error);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
|
||||
.form-input, .form-select, .form-textarea, .form-field {
|
||||
width: 100%;
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text);
|
||||
padding: 10px 12px;
|
||||
font-family: var(--typography);
|
||||
font-size: 0.95rem;
|
||||
transition: border-color 0.2s;
|
||||
outline: none;
|
||||
|
||||
&:focus {
|
||||
border-color: var(--rhpz-orange);
|
||||
}
|
||||
&:disabled {
|
||||
background-color: var(--bg3);
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
.form-textarea {
|
||||
resize: vertical;
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.form-checkbox, .form-radio {
|
||||
accent-color: var(--rhpz-orange);
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.game-selector {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.game-selector-level2 {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.game-selector-dropdown {
|
||||
position: absolute !important;
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
top: calc(100% + 0.35rem);
|
||||
left: 0;
|
||||
margin: 0;
|
||||
padding: 0.35rem 0;
|
||||
z-index: 9999;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: 0 18px 35px rgba(0, 0, 0, 0.08);
|
||||
list-style: none;
|
||||
will-change: transform, opacity;
|
||||
}
|
||||
|
||||
.game-selector-dropdown::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.game-selector-dropdown::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(255, 255, 255, 0.15);
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.game-selector-dropdown li {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.dropdown-item {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
padding: 0.85rem 1rem;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.18s ease, color 0.18s ease;
|
||||
}
|
||||
|
||||
.dropdown-item:hover,
|
||||
.dropdown-item:focus {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.dropdown-item[selected],
|
||||
.dropdown-item.selected,
|
||||
.dropdown-item.is-selected {
|
||||
background-color: rgba(255, 115, 0, 0.15);
|
||||
}
|
||||
|
||||
.dropdown-item-name {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dropdown-empty {
|
||||
padding: 0.85rem 1rem;
|
||||
color: var(--text2);
|
||||
}
|
||||
|
||||
.form-upload {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
input[type="file"] {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.form-upload-placeholder {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
background-color: var(--bg2);
|
||||
border: 1px dashed var(--border);
|
||||
padding: 12px;
|
||||
color: var(--text);
|
||||
transition: border-color 0.2s, background-color 0.2s;
|
||||
}
|
||||
.form-upload-placeholder.level {
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
display: block;
|
||||
background-color: var(--bg);
|
||||
}
|
||||
|
||||
.form-upload:hover .form-upload-placeholder {
|
||||
border-color: var(--rhpz-orange);
|
||||
background-color: rgba(255, 115, 0, 0.05);
|
||||
}
|
||||
|
||||
.form-type-of-checkboxes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 1rem;
|
||||
|
||||
label {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
transform: scale(1.5);
|
||||
margin-right: 1.33rem;
|
||||
}
|
||||
}
|
||||
|
||||
.form-status-radio {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
|
||||
label {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
transform: scale(1.5);
|
||||
margin-right: 1.33rem;
|
||||
}
|
||||
}
|
||||
|
||||
.languages-selector {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 1rem;
|
||||
}
|
||||
.language-search {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
background-color: var(--bg3);
|
||||
border: 1px solid var(--border);
|
||||
padding: 0 10px;
|
||||
height: 36px;
|
||||
}
|
||||
.language-search input {
|
||||
flex: 1;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text);
|
||||
}
|
||||
.language-search [data-lucide] {
|
||||
color: var(--text2);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.language-search .btn {
|
||||
padding: 0;
|
||||
background: transparent;
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.language-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
max-height: 260px;
|
||||
overflow-y: auto;
|
||||
background: var(--bg3);
|
||||
border: 1px solid var(--border);
|
||||
scrollbar-width: thin;
|
||||
scrollbar-color: var(--rhpz-orange);
|
||||
padding: 2%;
|
||||
}
|
||||
.language-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
accent-color: var(--rhpz-orange);
|
||||
}
|
||||
.main-image-grid {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
gap: 1rem;
|
||||
}
|
||||
.form-image-placeholder {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 15px;
|
||||
height: 100%;
|
||||
min-height: 120px;
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text2);
|
||||
}
|
||||
.form-image-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 15px;
|
||||
}
|
||||
.form-image-preview-wrap {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--border);
|
||||
aspect-ratio: 16/9;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
display: block;
|
||||
}
|
||||
|
||||
}
|
||||
.form-image-remove {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
right: 6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
background-color: var(--bg2);
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
color: var(--text);
|
||||
transition: background-color 0.15s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg3);
|
||||
}
|
||||
}
|
||||
.form-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
.gallery-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
}
|
||||
.authors-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 15px;
|
||||
button {
|
||||
padding: 5px;
|
||||
}
|
||||
}
|
||||
.submit {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 15px;
|
||||
}
|
||||
.submit-level {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
.nsfw-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.upload-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
.upload-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--border);
|
||||
background-color: var(--bg2);
|
||||
transition: background-color 0.15s, border-color 0.15s;
|
||||
|
||||
& > [data-lucide] {
|
||||
flex-shrink: 0;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
}
|
||||
.upload-item-uploading {
|
||||
border-color: var(--border);
|
||||
background-color: var(--bg2);
|
||||
|
||||
& > [data-lucide] {
|
||||
color: var(--text2);
|
||||
}
|
||||
}
|
||||
.upload-item-done {
|
||||
border-color: color-mix(in srgb, var(--rhpz-orange) 40%, transparent);
|
||||
background-color: color-mix(in srgb, var(--rhpz-orange) 6%, transparent);
|
||||
|
||||
& > [data-lucide] {
|
||||
color: var(--rhpz-orange);
|
||||
}
|
||||
}
|
||||
.upload-item-error {
|
||||
border-color: color-mix(in srgb, var(--error) 40%, transparent);
|
||||
background: color-mix(in srgb, var(--error) 6%, transparent);
|
||||
|
||||
& > [data-lucide] {
|
||||
color: var(--error);
|
||||
}
|
||||
}
|
||||
.upload-item-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
.upload-item-name {
|
||||
color: var(--text);
|
||||
font-weight: 500;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.progress {
|
||||
height: 6px;
|
||||
background-color: var(--bg3);
|
||||
border-radius: 99px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.progress-bar {
|
||||
height: 100%;
|
||||
background-color: var(--rhpz-orange);
|
||||
border-radius: 99px;
|
||||
transition: width 0.2s ease;
|
||||
min-width: 4px;
|
||||
}
|
||||
.progress-bar-label {
|
||||
position: absolute;
|
||||
top: -18px;
|
||||
right: 0;
|
||||
font-size: 11px;
|
||||
color: var(--text2);
|
||||
white-space: nowrap;
|
||||
}
|
||||
29
resources/css/components/grid.css
Normal file
29
resources/css/components/grid.css
Normal file
@@ -0,0 +1,29 @@
|
||||
.grid-c2 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.grid-c3 {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr 1fr;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
@media (max-width: 1100px) {
|
||||
.grid-c3 { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.grid-c2, .grid-c3 { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.grid-hashes {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4,0.5fr) 0.25fr;
|
||||
gap: 20px;
|
||||
}
|
||||
.grid-credits {
|
||||
display: grid;
|
||||
grid-template-columns: 0.5fr 1fr 0.25fr;
|
||||
gap: 20px;
|
||||
}
|
||||
53
resources/css/components/modal.css
Normal file
53
resources/css/components/modal.css
Normal file
@@ -0,0 +1,53 @@
|
||||
.modal-overlay {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0; left: 0; right: 0; bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(3px);
|
||||
animation: fadeIn 0.2s ease;
|
||||
}
|
||||
|
||||
.modal-window {
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
width: 100%;
|
||||
max-width: 500px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.5);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 15px 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
background-color: var(--bg3);
|
||||
|
||||
.modal-title {
|
||||
font-weight: 600;
|
||||
font-size: 1.1rem;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text2);
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: var(--text);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
padding: 20px;
|
||||
}
|
||||
54
resources/css/layout/content.css
Normal file
54
resources/css/layout/content.css
Normal file
@@ -0,0 +1,54 @@
|
||||
#main-wrapper {
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#topbar {
|
||||
|
||||
height: 60px;
|
||||
background-color: var(--bg2);
|
||||
border-bottom: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 20px;
|
||||
justify-content: space-between;
|
||||
|
||||
.mobile-toggle {
|
||||
display: none;
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2px;
|
||||
padding: 5px 10px;
|
||||
width: 300px;
|
||||
input {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text);
|
||||
outline: none;
|
||||
margin-left: 8px;
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
#content {
|
||||
flex-grow: 1;
|
||||
padding: 30px;
|
||||
overflow-y: auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
130
resources/css/layout/entry.css
Normal file
130
resources/css/layout/entry.css
Normal file
@@ -0,0 +1,130 @@
|
||||
#entry-container {
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.entry-header {
|
||||
|
||||
display: flex;
|
||||
padding: 30px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: linear-gradient(to right, rgba(255,115,0,0.05), transparent);
|
||||
gap: 30px;
|
||||
|
||||
.entry-cover {
|
||||
width: 200px;
|
||||
height: 280px;
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.entry-info {
|
||||
|
||||
flex-grow: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.entry-title {
|
||||
font-size: 2.2rem;
|
||||
font-weight: 600;
|
||||
margin-bottom: 5px;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.entry-authors {
|
||||
color: var(--rhpz-orange);
|
||||
font-size: 1.1rem;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.entry-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 15px;
|
||||
margin-bottom: 25px;
|
||||
background-color: var(--bg);
|
||||
padding: 15px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.entry-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.entry-meta-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text2);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.entry-actions {
|
||||
margin-top: auto;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
.entry-content {
|
||||
padding: 30px;
|
||||
|
||||
.entry-section-title {
|
||||
font-size: 1.2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 10px;
|
||||
margin-bottom: 20px;
|
||||
color: var(--text);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.entry-description {
|
||||
line-height: 1.6;
|
||||
color: var(--text);
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.entry-gallery {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.entry-gallery-item {
|
||||
aspect-ratio: 4/3;
|
||||
background-color: var(--bg);
|
||||
border: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.2s;
|
||||
|
||||
&:hover {
|
||||
border-color: var(--rhpz-orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.entry-cover-placeholder {
|
||||
text-align: center;
|
||||
color: var( --text2 );
|
||||
}
|
||||
128
resources/css/layout/menu.css
Normal file
128
resources/css/layout/menu.css
Normal file
@@ -0,0 +1,128 @@
|
||||
#menu {
|
||||
width: var(--menu-size);
|
||||
background-color: var(--bg2);
|
||||
border-right: 1px solid var(--border);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-shrink: 0;
|
||||
transition: transform 0.3s ease;
|
||||
z-index: 100;
|
||||
|
||||
.menu-header {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
.menu-logo {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
background-color: var(--rhpz-orange);
|
||||
color: var(--text3);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 4px;
|
||||
font-weight: bold;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.menu-title {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.5px;
|
||||
color: #fff; /* TODO Change */
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.menu-navigation {
|
||||
flex-grow: 1;
|
||||
padding: 10px 0;
|
||||
overflow-y: auto;
|
||||
|
||||
.menu-group {
|
||||
margin-bottom: 20px;
|
||||
|
||||
.menu-group-title {
|
||||
padding: 0 20px;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
color: var(--text2);
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 20px;
|
||||
color: var(--text);
|
||||
text-decoration: none;
|
||||
gap: 12px;
|
||||
border-left: 3px solid transparent;
|
||||
cursor: pointer;
|
||||
transition: background-color 0.1s, border-color 0.1s;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg4);
|
||||
text-decoration: none;
|
||||
}
|
||||
.active {
|
||||
background-color: var(--bg3);
|
||||
border-left-color: var(--rhpz-orange);
|
||||
font-weight: 600;
|
||||
}
|
||||
i {
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: var(--text2);
|
||||
}
|
||||
.active i, &:hover i {
|
||||
color: var( --rhpz-orange );
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.menu-user {
|
||||
padding: 15px 20px;
|
||||
border-top: 1px solid var(--border);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background-color: var(--bg3);
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--bg4);
|
||||
}
|
||||
.menu-user-avatar {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
background-color: var( --menu-user-avatar-bg );
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.menu-user-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&.username {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
&.user_role {
|
||||
font-size: 0.75rem;
|
||||
color: var(--rhpz-orange);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
48
resources/css/layout/news.css
Normal file
48
resources/css/layout/news.css
Normal file
@@ -0,0 +1,48 @@
|
||||
.news-header {
|
||||
width: 100%;
|
||||
height: 250px;
|
||||
background-color: #333;
|
||||
background-image: linear-gradient(rgba(0,0,0,0.2), rgba(30,30,30,1));
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
padding: 30px;
|
||||
border: 1px solid var(--border);
|
||||
border-bottom: none;
|
||||
position: relative;
|
||||
|
||||
.news-header-content {
|
||||
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
.news-title {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 300;
|
||||
color: var(--text);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.news-meta {
|
||||
color: var(--text2);
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.news-content {
|
||||
|
||||
background-color: var(--bg2);
|
||||
border: 1px solid var(--border);
|
||||
padding: 40px;
|
||||
line-height: 1.7;
|
||||
color: var(--text);
|
||||
font-size: 1.05rem;
|
||||
|
||||
p {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
37
resources/js/SubmissionsClass/Credits.js
Normal file
37
resources/js/SubmissionsClass/Credits.js
Normal file
@@ -0,0 +1,37 @@
|
||||
/** @typedef { import('types/CreditsObject.js').CreditsObject} CreditsObject */
|
||||
|
||||
export function Credits(){
|
||||
return {
|
||||
|
||||
/**
|
||||
* Credits. Preloaded at startup.
|
||||
* @type {CreditsObject[]}
|
||||
*/
|
||||
credits: [],
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string|null} jsonCredits
|
||||
*/
|
||||
init( jsonCredits= null ){
|
||||
if( jsonCredits !== null ){
|
||||
this.credits = JSON.parse(jsonCredits);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Add an empty credit.
|
||||
*/
|
||||
addEmptyCredits() {
|
||||
this.credits.push({name: '', description: ''});
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a specific credits.
|
||||
* @param {number} index index of credit.
|
||||
*/
|
||||
removeCredits( index ){
|
||||
this.credits.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
154
resources/js/SubmissionsClass/FSFileData.js
Normal file
154
resources/js/SubmissionsClass/FSFileData.js
Normal file
@@ -0,0 +1,154 @@
|
||||
/** @typedef { import('types/UploadchunkResponse.js').UploadchunkResponse} UploadchunkResponse */
|
||||
|
||||
export const CHUNK_SIZE = 8192;
|
||||
|
||||
/**
|
||||
* An uploaded file instance.
|
||||
* Create a new file data.
|
||||
*
|
||||
* @param {string} name Filename
|
||||
* @param {number} totalChunks Total number of chunks of the file.
|
||||
* @param rawFile The JS file element relation.
|
||||
*/
|
||||
export function FSFileData(name, totalChunks, rawFile ) {
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* Filename.
|
||||
* @type {string}
|
||||
*/
|
||||
name,
|
||||
|
||||
/**
|
||||
* Number of total chunks based on CHUNK_SIZE.
|
||||
* @type {number}
|
||||
*/
|
||||
totalChunks,
|
||||
|
||||
/**
|
||||
* The JS file element relation.
|
||||
*/
|
||||
rawFile,
|
||||
|
||||
/**
|
||||
* Upload progression value.
|
||||
* @type {number}
|
||||
*/
|
||||
progressValue: 0,
|
||||
|
||||
/**
|
||||
* Current chunk uploaded.
|
||||
* @type {number}
|
||||
*/
|
||||
currentChunk: 0,
|
||||
|
||||
/**
|
||||
* If the upload of the file is finished.
|
||||
* @type {boolean}
|
||||
*/
|
||||
done: false,
|
||||
|
||||
/**
|
||||
* If there is an error during file uploading.
|
||||
* @type {any|null}
|
||||
*/
|
||||
error: null,
|
||||
|
||||
/**
|
||||
* UUID v4 for the file.
|
||||
* @type {`${string}-${string}-${string}-${string}-${string}`}
|
||||
*/
|
||||
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.
|
||||
|
||||
/**
|
||||
* Get CSRF token for uploading request.
|
||||
* @type {string}
|
||||
*/
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content ?? '';
|
||||
|
||||
for (let i = 0; i < this.totalChunks; i++) {
|
||||
|
||||
if (this.error)
|
||||
return; // Abort the process.
|
||||
|
||||
const start = i * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, this.rawFile.size);
|
||||
const chunk = this.rawFile.slice(start, end);
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
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);
|
||||
|
||||
// -----
|
||||
// UPLOAD TIME !
|
||||
// -----
|
||||
|
||||
try {
|
||||
const RESPONSE = await fetch(this.buildUrl(section), {method: 'POST', body: formData});
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
133
resources/js/SubmissionsClass/FSUploader.js
Normal file
133
resources/js/SubmissionsClass/FSUploader.js
Normal file
@@ -0,0 +1,133 @@
|
||||
import { FSFileData, CHUNK_SIZE } from "./FSFileData.js";
|
||||
|
||||
/**
|
||||
* File uploader on FileServer.
|
||||
* @returns {{files: Array<FSFileData>, section: string, init(): void, readonly numberOfFiles: number, readonly isUploading: boolean, readonly hasErrors: boolean, readonly allFilesUploaded: boolean, totalChunksNumber(*): number, handleSubmitFile(Event): Promise<void>, handleRetryFile(number): Promise<void>, removeFile(number): void}|boolean|this is FSFileData[]|number|boolean}
|
||||
* @constructor
|
||||
*/
|
||||
export function FSUploader(){
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* Array of uploaded files.
|
||||
* @type {Array<FSFileData>}
|
||||
*/
|
||||
files: [],
|
||||
|
||||
/**
|
||||
* Section that files must be uploaded.
|
||||
* @type {string}
|
||||
*/
|
||||
section: document.querySelector("meta[name='fs-section']")?.content ?? '',
|
||||
|
||||
/**
|
||||
* Triggered in fs-upload.blade.php
|
||||
* Refresh icons.
|
||||
*
|
||||
* @param {Array<FSFileData>} oldFilesArray
|
||||
*/
|
||||
init( oldFilesArray ){
|
||||
this.$watch('files', () =>{
|
||||
this.$nextTick(() => window.refreshIcons(this.$el) )
|
||||
})
|
||||
|
||||
if( oldFilesArray !== undefined && oldFilesArray.length > 0)
|
||||
this.files = oldFilesArray;
|
||||
},
|
||||
|
||||
/**
|
||||
* Shortcut to files.length.
|
||||
* @returns {number}
|
||||
*/
|
||||
get numberOfFiles(){
|
||||
return this.files.length
|
||||
},
|
||||
|
||||
/**
|
||||
* Look if some files are currently in upload.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isUploading(){
|
||||
return this.files.some(
|
||||
file => file.isUploading
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if some files have an error or not.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get hasErrors(){
|
||||
return this.files.some(
|
||||
file => file.error
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if all files are uploaded.
|
||||
* True if all files are uploaded or no one.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get allFilesUploaded(){
|
||||
return ( this.numberOfFiles === 0 || this.files.every(file => file.done) );
|
||||
},
|
||||
|
||||
/**
|
||||
* Get total chunks size of a raw file.
|
||||
* @param rawFile
|
||||
* @returns {number}
|
||||
*/
|
||||
totalChunksNumber( rawFile ){
|
||||
return Math.ceil( rawFile.size / CHUNK_SIZE );
|
||||
},
|
||||
|
||||
/**
|
||||
* Handle file submission.
|
||||
* You can submit multiple files at once.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleSubmitFile( e ){
|
||||
|
||||
const RAW_FILES_FROM_EVENT = Array.from( e.target.files );
|
||||
e.target.value = ''; // Default.
|
||||
|
||||
for( const RAW_FILE of RAW_FILES_FROM_EVENT ){
|
||||
const TOTAL_CHUNKS = this.totalChunksNumber(RAW_FILE);
|
||||
let fsData = FSFileData( RAW_FILE.name, TOTAL_CHUNKS, RAW_FILE );
|
||||
|
||||
this.files.push( fsData );
|
||||
await this.files[this.files.length - 1].upload(this.section);
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Retry file uploading.
|
||||
*
|
||||
* @param {number} index FSFileData index in this.files.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
handleRetryFile( index ){
|
||||
|
||||
const OLDFSFILE = this.files[index];
|
||||
let fsData = FSFileData(OLDFSFILE.name, this.totalChunksNumber(OLDFSFILE.rawFile), OLDFSFILE.rawFile );
|
||||
this.files[index] = fsData;
|
||||
|
||||
this.files[index].upload(this.section);
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove a file.
|
||||
* @param {number} index FSFileData index in this.files.
|
||||
*/
|
||||
handleRemoveFile( index ){
|
||||
this.files.splice(index, 1);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
127
resources/js/SubmissionsClass/GalleryManager.js
Normal file
127
resources/js/SubmissionsClass/GalleryManager.js
Normal file
@@ -0,0 +1,127 @@
|
||||
import { MainImageManager as GalleryImage } from "./MainImageManager.js";
|
||||
|
||||
const MAX_GALLERY = 20;
|
||||
|
||||
export function GalleryManager() {
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* All images uploaded.
|
||||
* @type {Array<GalleryImage>}
|
||||
*/
|
||||
images: [],
|
||||
|
||||
/**
|
||||
* Forward to this.images.length
|
||||
* @returns {number}
|
||||
*/
|
||||
get number(){
|
||||
return this.images.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if all images has been uploaded or not.
|
||||
* @return {boolean}
|
||||
*/
|
||||
get allUploaded(){
|
||||
for(const IMG of this.images){
|
||||
if( IMG.serverFilePath == null ){
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* Return if there is an error on an image or not.
|
||||
*
|
||||
* @returns {null|Array<string>}
|
||||
*/
|
||||
get error(){
|
||||
const RESPONSE = [];
|
||||
for( const IMG of this.images ){
|
||||
if( IMG.error !== null )
|
||||
RESPONSE.push( IMG.error );
|
||||
}
|
||||
if( RESPONSE.length === 0 )
|
||||
return null;
|
||||
return RESPONSE;
|
||||
},
|
||||
|
||||
/**
|
||||
* Superior or equal than 20.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get isFull(){
|
||||
return this.images.length >= MAX_GALLERY;
|
||||
},
|
||||
|
||||
/**
|
||||
* Reload image if there is an error or edition.
|
||||
*
|
||||
* @param {Array<string>} oldPaths
|
||||
*/
|
||||
init( oldPaths = null ){
|
||||
if( oldPaths === null || oldPaths.length <= 0 )
|
||||
return;
|
||||
|
||||
for( const PATH of oldPaths ){
|
||||
|
||||
if( this.isFull )
|
||||
break;
|
||||
|
||||
const IMG = GalleryImage();
|
||||
IMG.getOldImage( PATH );
|
||||
this.images.push(IMG);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload images, refresh the preview and store images if there is a problem.
|
||||
*
|
||||
* @param {Event} e
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async handleSubmitFiles(e){
|
||||
const FILES = e.target.files;
|
||||
if( !FILES || FILES.length <= 0 )
|
||||
return; // No file uploaded.
|
||||
|
||||
for( const FILE of FILES ){
|
||||
|
||||
if( this.isFull )
|
||||
break;
|
||||
|
||||
const IMG = GalleryImage();
|
||||
IMG.name = FILE.name;
|
||||
IMG.type = FILE.type;
|
||||
this.images.push(IMG);
|
||||
|
||||
const IMG_FROM_LIST = this.images[this.images.length - 1];
|
||||
|
||||
await IMG_FROM_LIST.uploadImageToTemporary( FILE );
|
||||
|
||||
await new Promise( resolve => {
|
||||
const READER = new FileReader();
|
||||
READER.onload = (e2) => {
|
||||
IMG_FROM_LIST.preview = e2.target.result;
|
||||
resolve();
|
||||
}
|
||||
READER.onerror = () => resolve();
|
||||
READER.readAsDataURL(FILE);
|
||||
});
|
||||
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove specific file.
|
||||
* @param {number} index
|
||||
*/
|
||||
handleRemoveFile(index){
|
||||
this.images[index].handleRemoveFile(null);
|
||||
this.images.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
38
resources/js/SubmissionsClass/GameSelector.js
Normal file
38
resources/js/SubmissionsClass/GameSelector.js
Normal file
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* Handle new game already selected.
|
||||
*
|
||||
* @param {Object} initialContent With gameName, gamePlatformId and gameGenreId field.
|
||||
* @returns {Object}
|
||||
*/
|
||||
export function GameSelector(){
|
||||
return {
|
||||
|
||||
/**
|
||||
* Game Name
|
||||
* @type {string|null}
|
||||
*/
|
||||
name: null,
|
||||
|
||||
/**
|
||||
* Game Platform Id.
|
||||
* @type {number|null}
|
||||
*/
|
||||
platformId: null,
|
||||
|
||||
/**
|
||||
* Game genre Id.
|
||||
* @type {number|null}
|
||||
*/
|
||||
genreId: null,
|
||||
|
||||
/**
|
||||
* Initialize game selector.
|
||||
* @param initialContent
|
||||
*/
|
||||
init( initialContent = {} ){
|
||||
this.name = initialContent.name ?? null;
|
||||
this.platformId = Number(initialContent.platformId) ?? null;
|
||||
this.genreId = Number(initialContent.genreId) ?? null;
|
||||
}
|
||||
}
|
||||
}
|
||||
60
resources/js/SubmissionsClass/HashesManager.js
Normal file
60
resources/js/SubmissionsClass/HashesManager.js
Normal file
@@ -0,0 +1,60 @@
|
||||
import { calculate as calculateHashes } from "../hashes.js";
|
||||
|
||||
export function HashesManager( wire ) {
|
||||
return {
|
||||
|
||||
/**
|
||||
* Wire variable instance.
|
||||
*/
|
||||
$wire: wire,
|
||||
|
||||
/**
|
||||
* If a file hash is currently calculated or not.
|
||||
* @type {boolean}
|
||||
*/
|
||||
isCalculating: false,
|
||||
|
||||
/**
|
||||
* An error on hash calculation.
|
||||
* @type {any|null}
|
||||
*/
|
||||
error: null,
|
||||
|
||||
async handleSubmitFile(){
|
||||
|
||||
if( this.isCalculating === true ) // Calculation already done for another file.
|
||||
return;
|
||||
|
||||
this.error = null; // Reset.
|
||||
const FILE = await this.openFileInput();
|
||||
|
||||
if( !FILE )
|
||||
return; // No file sent.
|
||||
|
||||
this.isCalculating = true;
|
||||
|
||||
try {
|
||||
const RESULT = await calculateHashes(FILE);
|
||||
await this.$wire.addHash(RESULT.filename, RESULT.crc32, RESULT.sha1); // Send a signal to livewire.
|
||||
} catch(err) {
|
||||
this.error = err.message;
|
||||
} finally {
|
||||
this.isCalculating = false;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Open a specific file.
|
||||
* @returns {Promise<unknown>}
|
||||
*/
|
||||
async openFileInput(){
|
||||
return new Promise(resolve => {
|
||||
const input = document.createElement("input");
|
||||
input.type = "file";
|
||||
input.onchange = () => resolve(input.files[0] ?? null );
|
||||
input.oncancel = () => resolve(null);
|
||||
input.click();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
151
resources/js/SubmissionsClass/MainImageManager.js
Normal file
151
resources/js/SubmissionsClass/MainImageManager.js
Normal file
@@ -0,0 +1,151 @@
|
||||
export function MainImageManager() {
|
||||
|
||||
return {
|
||||
|
||||
/**
|
||||
* If an image has been uploaded or not.
|
||||
* @type {boolean}
|
||||
*/
|
||||
uploaded: false,
|
||||
|
||||
/**
|
||||
* Actual image path on the server.
|
||||
* @type {string|null}
|
||||
*/
|
||||
serverFilePath: null,
|
||||
|
||||
/**
|
||||
* Image filename.
|
||||
* @type {string|null}
|
||||
*/
|
||||
name: null,
|
||||
|
||||
/**
|
||||
* Image filetype.
|
||||
* @type {string|null}
|
||||
*/
|
||||
type: null,
|
||||
|
||||
/**
|
||||
* Handle preview.
|
||||
* @type {unknown}
|
||||
*/
|
||||
preview: null,
|
||||
|
||||
/**
|
||||
* Current error message.
|
||||
* @type {string}
|
||||
*/
|
||||
error: null,
|
||||
|
||||
/**
|
||||
* Reload image if there is an error.
|
||||
*
|
||||
* @param {string|null} oldPath If there is already a path.
|
||||
*/
|
||||
init( oldPath = null ){
|
||||
if(oldPath === "" || oldPath === null)
|
||||
return;
|
||||
|
||||
this.getOldImage(oldPath)
|
||||
},
|
||||
|
||||
/**
|
||||
* Get old image from old path and refresh preview.
|
||||
* @param {string} oldPath
|
||||
* @param {function|null} callback Used in the gallery to push the image at the right time.
|
||||
*/
|
||||
getOldImage( oldPath, callback = null ) {
|
||||
this.readOldImage( '/storage/' + oldPath ).then( blob => {
|
||||
|
||||
this.uploaded = true;
|
||||
this.serverFilePath = oldPath;
|
||||
|
||||
const READER = new FileReader();
|
||||
READER.onload = () => {
|
||||
this.preview = READER.result;
|
||||
}
|
||||
READER.readAsDataURL(blob);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Get old image data.
|
||||
* @param {string} url
|
||||
* @returns {Promise<Blob>}
|
||||
*/
|
||||
async readOldImage(url){
|
||||
const RESPONSE = await fetch(url);
|
||||
return await RESPONSE.blob();
|
||||
},
|
||||
|
||||
async uploadImageToTemporary( file ){
|
||||
|
||||
const CSRF = document.querySelector('meta[name=csrf-token]')?.content ?? '';
|
||||
const URL = `/api/tempfile/upload`;
|
||||
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('file', file);
|
||||
formData.append('_token', CSRF );
|
||||
|
||||
try {
|
||||
const RESPONSE = await fetch( URL, { method: 'POST', body: formData } );
|
||||
|
||||
if( !RESPONSE.ok ) // Problem with the request.
|
||||
throw new Error(`${RESPONSE.status} ${RESPONSE.statusText}`);
|
||||
|
||||
const DATA = await RESPONSE.json();
|
||||
|
||||
if( DATA.path === null ){
|
||||
throw new Error(`${RESPONSE.status} ${RESPONSE.statusText}`);
|
||||
}
|
||||
|
||||
this.serverFilePath = DATA.path;
|
||||
this.uploaded = true;
|
||||
|
||||
} catch (err){
|
||||
this.error = 'Error on main image uploading ' + ( i + 1 ) + '. ' + err.message;
|
||||
console.error( this.error );
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Upload image, refresh the preview and store image if there is a problem.
|
||||
*
|
||||
* @param {Event} e
|
||||
*/
|
||||
async handleSubmitFile(e) {
|
||||
const FILE = e.target.files[0];
|
||||
if (!FILE)
|
||||
return; // No file uploaded.
|
||||
|
||||
this.handleRemoveFile(null);
|
||||
await this.uploadImageToTemporary(FILE);
|
||||
|
||||
this.name = FILE.name;
|
||||
this.type = FILE.type;
|
||||
|
||||
const READER = new FileReader();
|
||||
READER.onload = (e2) => {
|
||||
this.preview = e2.target.result;
|
||||
}
|
||||
READER.readAsDataURL(FILE);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove main image.
|
||||
* @param {Event} e
|
||||
*/
|
||||
handleRemoveFile(e){
|
||||
this.uploaded = false;
|
||||
this.serverFilePath = null;
|
||||
this.preview = null;
|
||||
this.name = null;
|
||||
this.type = null;
|
||||
this.error = null;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
18
resources/js/SubmissionsClass/types/CreditsObject.js
Normal file
18
resources/js/SubmissionsClass/types/CreditsObject.js
Normal file
@@ -0,0 +1,18 @@
|
||||
/**
|
||||
* @typedef {Object} CreditsObject
|
||||
*
|
||||
* @property {string} name Credits name.
|
||||
* @property {string} description Credits description.
|
||||
*/
|
||||
|
||||
/**
|
||||
* If this object is a credit object or not.
|
||||
*
|
||||
* @param {object} object
|
||||
* @returns {boolean}
|
||||
*/
|
||||
export function isACreditsObject( object ){
|
||||
return typeof object === 'object' && object.name !== undefined && object.description !== undefined;
|
||||
}
|
||||
|
||||
export {}
|
||||
13
resources/js/SubmissionsClass/types/UploadchunkResponse.js
Normal file
13
resources/js/SubmissionsClass/types/UploadchunkResponse.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* @typedef {Object} UploadchunkResponse
|
||||
*
|
||||
* @see app/Http/FileServerController.php
|
||||
* @external RHPZFS::src/Endpoints/Uploadchunk
|
||||
*
|
||||
* @property {number} chunk The current chunk that has been uploaded.
|
||||
* @property {number} total_chunks Number of total chunks.
|
||||
* @property {boolean} uploaded If the chunk has been correctly uploaded.
|
||||
* @property {Object|boolean} file If the file has been entirely uploaded or not.
|
||||
* @property {boolean} finished Added by main server. Indicates if the file upload is finished or not.
|
||||
*/
|
||||
export {}
|
||||
23
resources/js/app.js
Normal file
23
resources/js/app.js
Normal file
@@ -0,0 +1,23 @@
|
||||
import { createIcons, icons } from "lucide";
|
||||
import EasyMDE from "easymde";
|
||||
import "easymde/dist/easymde.min.css";
|
||||
|
||||
import { calculate as calculateHashes } from "./hashes.js";
|
||||
|
||||
|
||||
// Lucide icons.
|
||||
createIcons({ icons });
|
||||
|
||||
window.refreshIcons = (container = document) => {
|
||||
const pending = container.querySelectorAll('[data-lucide]');
|
||||
if (pending.length === 0) return;
|
||||
createIcons({ icons });
|
||||
};
|
||||
|
||||
// EasyMDE.
|
||||
window.EasyMDE = EasyMDE;
|
||||
|
||||
// Hashes.
|
||||
window.calculateHashes = calculateHashes;
|
||||
|
||||
|
||||
56
resources/js/hashes.js
Normal file
56
resources/js/hashes.js
Normal file
@@ -0,0 +1,56 @@
|
||||
function createCrcTable() {
|
||||
const table = [];
|
||||
for (let n = 0; n < 256; n++) {
|
||||
let c = n;
|
||||
for (let k = 0; k < 8; k++) {
|
||||
c = (c & 1) ? (0xEDB88320 ^ (c >>> 1)) : (c >>> 1);
|
||||
}
|
||||
table[n] = c;
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
async function crc32(buffer) {
|
||||
let crc = 0 ^ (-1);
|
||||
const bytes = new Uint8Array(buffer);
|
||||
for (let i = 0; i < bytes.byteLength; i++) {
|
||||
crc = (crc >>> 8) ^ crcTable[(crc ^ bytes[i]) & 0xFF];
|
||||
}
|
||||
return ((crc ^ (-1)) >>> 0).toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
async function sha1(buffer) {
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-1', buffer);
|
||||
return Array.from(new Uint8Array(hashBuffer))
|
||||
.map(b => b.toString(16).padStart(2, '0'))
|
||||
.join('');
|
||||
}
|
||||
|
||||
const crcTable = createCrcTable();
|
||||
|
||||
export async function calculate( file ){
|
||||
return new Promise( ( resolve, reject ) => {
|
||||
const freader = new FileReader();
|
||||
|
||||
freader.onload = async e => {
|
||||
try {
|
||||
const buffer = e.target.result;
|
||||
const crc32V = await crc32(buffer);
|
||||
const sha1V = await sha1(buffer);
|
||||
|
||||
resolve({
|
||||
filename: file.name,
|
||||
crc32: crc32V,
|
||||
sha1: sha1V
|
||||
});
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
}
|
||||
|
||||
freader.onerror = () => {
|
||||
reject( new Error(`Could not parse file: ${file.name}`) );
|
||||
}
|
||||
freader.readAsArrayBuffer(file);
|
||||
})
|
||||
}
|
||||
387
resources/js/submissions.js
Normal file
387
resources/js/submissions.js
Normal file
@@ -0,0 +1,387 @@
|
||||
import { FSUploader } from "./SubmissionsClass/FSUploader.js";
|
||||
import { HashesManager } from "./SubmissionsClass/HashesManager.js";
|
||||
import { GameSelector } from "./SubmissionsClass/GameSelector.js";
|
||||
import { MainImageManager } from "./SubmissionsClass/MainImageManager.js";
|
||||
import { GalleryManager } from "./SubmissionsClass/GalleryManager.js";
|
||||
import { Credits } from "./SubmissionsClass/Credits.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 = {
|
||||
isUploading: "A file is uploading. Please wait.",
|
||||
noFiles: "Please select a file to upload",
|
||||
uploadError: "One or more files failed to upload.",
|
||||
notAllFilesDone: "Not all the files have finished uploading yet.",
|
||||
noModifications: "Please select at least a type of hack.",
|
||||
noDescription: "Please provide a description.",
|
||||
noGame: "Please provide a game or create a new one and fill all the required fields.",
|
||||
noLanguages: "Please select at least a language.",
|
||||
noAuthors: "Please provide at least an author or create a new one and fill all the required fields.",
|
||||
noMainImage: "Please select a main image.",
|
||||
noGalleryImages: "Please select at least a gallery image.",
|
||||
isSubmitting: "The entry is already during submission."
|
||||
}
|
||||
|
||||
/**
|
||||
* Current section.
|
||||
* @returns {string}
|
||||
* @constructor
|
||||
*/
|
||||
const SECTION = () => document.querySelector("meta[name='fs-section']")?.content ?? '';
|
||||
|
||||
window.FSUploader = FSUploader;
|
||||
window.HashesManager = HashesManager;
|
||||
window.GameSelector = GameSelector;
|
||||
window.MainImageManager = MainImageManager;
|
||||
window.GalleryManager = GalleryManager;
|
||||
window.Credits = Credits;
|
||||
|
||||
/**
|
||||
* Verify if at least one checkbox is checked in this element.
|
||||
* @param {HTMLElement} element
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function verifyCheckboxes( element ){
|
||||
if( !parent ) return false;
|
||||
return Array.from(element.querySelectorAll('input[type="checkbox"]')).some(el => el.checked);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 we are in an upload.
|
||||
* @param {FSUploader} Uploader
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step1_DuringFSUpload: function( Uploader ){
|
||||
return !Uploader.isUploading;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if at least one file is uploaded.
|
||||
* @param {FSUploader} Uploader
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step2_NoFilesFSUpload: function( Uploader ){
|
||||
return Uploader.numberOfFiles > 0;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if any files haven't error.
|
||||
* @param {FSUploader} Uploader
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step3_ErrorsFSUpload: function( Uploader ){
|
||||
return !Uploader.hasErrors;
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if all files are uploaded.
|
||||
* @param {FSUploader} Uploader
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step4_AllFilesUploadedFSUpload: function( Uploader ){
|
||||
return Uploader.allFilesUploaded;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if at least one checkbox of romhacks modifications is checked.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step5_RomhacksModificationsCheckboxes: function(){
|
||||
return verifyCheckboxes( document.querySelector( '#modifications-group' ) );
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if the description field has at least one character.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step6_VerifyDescription: function(){
|
||||
return verifyMDE('description');
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if a game is provided.
|
||||
* @param element this.$el
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step7_VerifyGame: function( element ){
|
||||
|
||||
// Check if we have an already existent selected game.
|
||||
const GAME_ID_INPUT = document.querySelector('input[name="game_id"]');
|
||||
if( GAME_ID_INPUT ){
|
||||
if( GAME_ID_INPUT.value !== '' && Number(GAME_ID_INPUT.value) > 0){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we have a new game.
|
||||
let gameSelector = element.querySelector('[x-data="GameSelector()"]');
|
||||
gameSelector = gameSelector ? Alpine.$data(gameSelector) : null;
|
||||
if( gameSelector !== null ){
|
||||
|
||||
if( !gameSelector.name || !gameSelector.name.toString().trim().length )
|
||||
return false;
|
||||
if( !gameSelector.platformId || gameSelector.platformId === '' || gameSelector.platformId === 0 )
|
||||
return false;
|
||||
if( !gameSelector.genreId || gameSelector.genreId === '' || gameSelector.genreId === 0 )
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if at least one checkbox of languages is checked.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
step8_LanguagesCheckboxes: function(){
|
||||
return verifyCheckboxes( document.querySelector( '#languages-group' ) );
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if at least one (new) author has been filled.
|
||||
* @return {boolean}
|
||||
*/
|
||||
step9_verifyAuthors: function(){
|
||||
const authorField = document.querySelectorAll('input[name="authors[]"]');
|
||||
const newAuthorField = document.querySelectorAll('input[name="new-authors[]"]');
|
||||
|
||||
return ( authorField.length > 0 || newAuthorField.length > 0 );
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if a main image has been uploaded.
|
||||
* @param element this.$el
|
||||
* @return {boolean}
|
||||
*/
|
||||
step10_verifyMainImage: function( element ){
|
||||
let MainImageData = element.querySelector('[x-data="MainImageManager()"]');
|
||||
MainImageData = MainImageData ? Alpine.$data(MainImageData) : null;
|
||||
|
||||
if( ! MainImageData ){
|
||||
return false;
|
||||
}
|
||||
|
||||
return MainImageData.uploaded;
|
||||
},
|
||||
|
||||
/**
|
||||
* Verify if at least one image is uploaded in the gallery.
|
||||
* @param element this.$el
|
||||
* @return {boolean}
|
||||
*/
|
||||
step11_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.Submission = 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(){
|
||||
},
|
||||
|
||||
/**
|
||||
* Get current FSUploader if initialized.
|
||||
*
|
||||
* @returns {FSUploader|null}
|
||||
* @constructor
|
||||
*/
|
||||
get Uploader(){
|
||||
const el = this.$el.querySelector('[x-data="FSUploader()"]');
|
||||
return el ? Alpine.$data(el) : null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Do each form verifications.
|
||||
* Update also this.errorKey.
|
||||
*
|
||||
* @returns {boolean}
|
||||
*/
|
||||
verifyForm(){
|
||||
|
||||
console.log( "Step 1" );
|
||||
if( !SubmissionVerifications.step1_DuringFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "isUploading";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 2" );
|
||||
if( !SubmissionVerifications.step2_NoFilesFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "noFiles";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 3" );
|
||||
if( !SubmissionVerifications.step3_ErrorsFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "uploadError";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 4" );
|
||||
if( !SubmissionVerifications.step4_AllFilesUploadedFSUpload( this.Uploader ) ){
|
||||
this.errorKey = "notAllFilesDone";
|
||||
return false;
|
||||
}
|
||||
|
||||
if( SECTION() === "romhacks" ){
|
||||
console.log( "Step 5" );
|
||||
if( !SubmissionVerifications.step5_RomhacksModificationsCheckboxes()){
|
||||
this.errorKey = "noModifications";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log( "Step 6" );
|
||||
if( !SubmissionVerifications.step6_VerifyDescription() ){
|
||||
this.errorKey = "noDescription";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 7" );
|
||||
if( !SubmissionVerifications.step7_VerifyGame( this.$el ) ){
|
||||
this.errorKey = "noGame";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 8" );
|
||||
if( !SubmissionVerifications.step8_LanguagesCheckboxes()){
|
||||
this.errorKey = "noLanguages";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 9" );
|
||||
if( !SubmissionVerifications.step9_verifyAuthors()){
|
||||
this.errorKey = "noAuthors";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 10" );
|
||||
if( !SubmissionVerifications.step10_verifyMainImage( this.$el )){
|
||||
this.errorKey = "noMainImage";
|
||||
return false;
|
||||
}
|
||||
|
||||
console.log( "Step 11" );
|
||||
if( !SubmissionVerifications.step11_verifyGallery( this.$el )){
|
||||
this.errorKey = "noGalleryImages";
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
},
|
||||
|
||||
/**
|
||||
* Scroll to the specific error field.
|
||||
*/
|
||||
scrollToError(){
|
||||
const refMap = {
|
||||
noFiles: 'uploadTarget',
|
||||
isUploading: 'uploadTarget',
|
||||
notAllFilesDone: 'uploadTarget',
|
||||
uploadError: 'uploadTarget',
|
||||
noModifications: 'modificationsGroup',
|
||||
noDescription: 'descriptionField',
|
||||
noGame: 'gameSelector',
|
||||
noLanguages: 'languagesGroup',
|
||||
noAuthors: 'authorsSelector',
|
||||
noMainImage: 'main-image-field',
|
||||
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;
|
||||
|
||||
if( !this.verifyForm() ){
|
||||
|
||||
this.scrollToError();
|
||||
|
||||
this.duringSubmissionProcess = false;
|
||||
return;
|
||||
}
|
||||
|
||||
e.target.submit();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
104
resources/js/uploader.js
Normal file
104
resources/js/uploader.js
Normal file
@@ -0,0 +1,104 @@
|
||||
import {createIcons, icons} from "lucide";
|
||||
export const CHUNK_SIZE = 8192;
|
||||
export function FSUploader(){
|
||||
return {
|
||||
files: [],
|
||||
rawFiles: [],
|
||||
section: document.querySelector("meta[name='fs-section']")?.content ?? '',
|
||||
init( oldFiles ){
|
||||
this.$watch('files', () =>{
|
||||
this.$nextTick(() => window.refreshIcons(this.$el) )
|
||||
})
|
||||
},
|
||||
get isUploading(){
|
||||
return this.files.some(f => !f.done && !f.error);
|
||||
},
|
||||
get hasErrors(){
|
||||
return this.files.some(f => f.error);
|
||||
},
|
||||
get allUploaded(){
|
||||
return this.files.length === 0 || this.files.every(f => f.done);
|
||||
},
|
||||
async submitFile(e){
|
||||
const selected = Array.from(e.target.files);
|
||||
e.target.value = '';
|
||||
|
||||
for( const raw of selected ){
|
||||
const totalChunks = Math.ceil(raw.size / CHUNK_SIZE );
|
||||
const index = this.files.length;
|
||||
this.files.push({
|
||||
name: raw.name,
|
||||
progress: 0,
|
||||
currentChunk: 0,
|
||||
totalChunks: totalChunks,
|
||||
done: false,
|
||||
error: null,
|
||||
uuid: crypto.randomUUID()
|
||||
});
|
||||
this.rawFiles.push(raw);
|
||||
this.uploadChunks(raw,index);
|
||||
}
|
||||
},
|
||||
async uploadChunks(rawFile, index){
|
||||
const file = this.files[index];
|
||||
const url = `/api/fs/upload-chunk/${this.section}`;
|
||||
const csrf = document.querySelector('meta[name=csrf-token]')?.content ?? '';
|
||||
|
||||
for( let i = 0; i < file.totalChunks; i++ ){
|
||||
if( file.error )
|
||||
return;
|
||||
|
||||
const start = i * CHUNK_SIZE;
|
||||
const end = Math.min(start + CHUNK_SIZE, rawFile.size);
|
||||
const chunk = rawFile.slice(start, end);
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append('file', chunk);
|
||||
formData.append('file_uuid', file.uuid);
|
||||
formData.append('current_chunk', i);
|
||||
formData.append('total_chunks', file.totalChunks);
|
||||
formData.append( 'filename', rawFile.name );
|
||||
formData.append('_token', csrf );
|
||||
|
||||
try {
|
||||
const response = await fetch(url, { method: 'POST', body: formData });
|
||||
if(!response.ok)
|
||||
throw new Error(`${response.status} ${response.statusText}`);
|
||||
|
||||
const data = await response.json();
|
||||
if( data.success !== true || data.uploaded !== true )
|
||||
throw new Error(`${data.error}`);
|
||||
|
||||
file.currentChunk = i + 1;
|
||||
file.progress = Math.round((( i + 1) / file.totalChunks ) * 100);
|
||||
|
||||
if( data.finished === true ){
|
||||
file.done = true;
|
||||
}
|
||||
} catch(err){
|
||||
file.error = 'Error on chunk ' + ( i + 1 ) + '. ' + err.message;
|
||||
file.progress = 0;
|
||||
return;
|
||||
}
|
||||
}
|
||||
},
|
||||
retry(index){
|
||||
const rawFile = this.rawFiles[index];
|
||||
const totalChunks = Math.ceil(rawFile.size / CHUNK_SIZE );
|
||||
this.files[index] = {
|
||||
name: rawFile.name,
|
||||
progress: 0,
|
||||
currentChunk: 0,
|
||||
totalChunks: totalChunks,
|
||||
done: false,
|
||||
error: null,
|
||||
uuid: crypto.randomUUID()
|
||||
};
|
||||
this.uploadChunks(raw,index);
|
||||
},
|
||||
remove(index){
|
||||
this.files.splice(index, 1);
|
||||
this.rawFiles.splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
resources/views/components/breadcrumbs.blade.php
Normal file
13
resources/views/components/breadcrumbs.blade.php
Normal file
@@ -0,0 +1,13 @@
|
||||
@unless ($breadcrumbs->isEmpty())
|
||||
<div class="breadcrumb">
|
||||
@foreach ($breadcrumbs as $breadcrumb)
|
||||
|
||||
@if ($breadcrumb->url && !$loop->last)
|
||||
<a href="{{ $breadcrumb->url }}">{{ $breadcrumb->title }}</a> <span>›</span>
|
||||
@else
|
||||
<span>{{ $breadcrumb->title }}</span>
|
||||
@endif
|
||||
|
||||
@endforeach
|
||||
</div>
|
||||
@endunless
|
||||
8
resources/views/components/entry-meta-item.blade.php
Normal file
8
resources/views/components/entry-meta-item.blade.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<div class="entry-meta-item">
|
||||
<span class="entry-meta-label">{{ $label }}</span>
|
||||
@if( $route !== "none" )
|
||||
<a href="{{ $route }}" class="entry-meta-value">{{ $value }}</a>
|
||||
@else
|
||||
<span class="entry-meta-value">{{ $value }}</span>
|
||||
@endif
|
||||
</div>
|
||||
3
resources/views/components/entry-section-title.blade.php
Normal file
3
resources/views/components/entry-section-title.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<h2 class="entry-section-title">
|
||||
@if( $icon != '' )<i data-lucide="{{ $icon }}"></i>@endif {{ $label }}
|
||||
</h2>
|
||||
3
resources/views/components/error-block.blade.php
Normal file
3
resources/views/components/error-block.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="block-error">
|
||||
<i data-lucide="{{ $errorArray['icon'] ?? '' }}"></i> {{ sprintf( $errorArray['message'], $message ) }}
|
||||
</div>
|
||||
6
resources/views/components/form-error-text.blade.php
Normal file
6
resources/views/components/form-error-text.blade.php
Normal file
@@ -0,0 +1,6 @@
|
||||
<span class="form-error-text">
|
||||
@if( $icon )
|
||||
<i data-lucide="alert-triangle" size="14"></i>
|
||||
@endif
|
||||
{{ $message }}
|
||||
</span>
|
||||
10
resources/views/components/form-field-title.blade.php
Normal file
10
resources/views/components/form-field-title.blade.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<label class="form-label">
|
||||
{{ $name }}
|
||||
@if( $required )
|
||||
<span style="color:red">*</span>
|
||||
@endif
|
||||
@if( $helper !== "" )
|
||||
<span>{{ $helper }}</span>
|
||||
@endif
|
||||
</label>
|
||||
{{ $slot }}
|
||||
7
resources/views/components/form-group-title.blade.php
Normal file
7
resources/views/components/form-group-title.blade.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<h3 class="form-group-title">
|
||||
@if( $icon !== "" )
|
||||
<i data-lucide="{{ $icon }}" color="var(--rhpz-orange"></i>
|
||||
@endif
|
||||
{{ $label }}
|
||||
</h3>
|
||||
{{ $slot }}
|
||||
34
resources/views/components/gallery-field.blade.php
Normal file
34
resources/views/components/gallery-field.blade.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<div x-data="GalleryManager()" x-init="init(@js($oldPaths))">
|
||||
<x-form-field-title name="Screenshots" helper="At least 1 Screenshot required, Maximum 20." required="{{ $required ? 'true' : 'false' }}" />
|
||||
<div class="form-group main-image-grid">
|
||||
<div class="form-upload" style="flex:1;" :class="{ 'disabled': isFull }">
|
||||
<input type="file" id="gallery-field" accept="image/png, image/jpeg, image/webp" multiple :disabled="isFull" @change="handleSubmitFiles($event)">
|
||||
<div class="form-upload-placeholder level">
|
||||
<i data-lucide="file-archive" size="36" style="margin-bottom:15px;color:var(--text2)"></i>
|
||||
<div style="font-size: 1.1rem;color:var(--text);margin-bottom:5px;">
|
||||
Click or drag'n drop files here.
|
||||
</div>
|
||||
<div style="font-size:0.85rem;color:var(--text2);">
|
||||
Accepted: PNG, JPG or WebP
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-gallery form-group level" style="flex:4;">
|
||||
<template x-for="(image,i) in images" :key="image.serverFilePath">
|
||||
<div class="gallery-item">
|
||||
<div class="form-image-preview-wrap">
|
||||
<img :src="image.preview" :alt="image.name">
|
||||
<button type="button" class="form-image-remove" @click="handleRemoveFile(i)">
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template x-for="(image, i) in images" :key="image.serverFilePath">
|
||||
<input type="hidden" name="gallery[]" :value="image.serverFilePath">
|
||||
</template>
|
||||
|
||||
</div>
|
||||
29
resources/views/components/languages-selector.blade.php
Normal file
29
resources/views/components/languages-selector.blade.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php /** @var \App\Models\Language $language */ ?>
|
||||
<div class="languages-selector form-group level" x-data="{
|
||||
search: '',
|
||||
selected: {{ JS::from( (array) $selected ) }},
|
||||
toggle(value){
|
||||
const i = this.selected.indexOf(value);
|
||||
i === -1 ? this.selected.push(value) : this.selected.splice(i,1);
|
||||
},
|
||||
valueSelect(value){
|
||||
return this.selected.includes(value);
|
||||
},
|
||||
get count(){ return this.selected.length; }
|
||||
}">
|
||||
<div class="language-search">
|
||||
<i data-lucide="search"></i>
|
||||
<input type="text" x-model="search" placeholder="Search language" autocomplete="off">
|
||||
<button class="btn" type="button" x-show="search !== ''" @click="search = ''" x-cloak>
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="language-list" id="languages-group">
|
||||
@foreach( $languages as $language )
|
||||
<label class="language-item" x-show="'{{ strtolower($language->name) }}'.includes(search.toLowerCase())">
|
||||
<input type="checkbox" name="languages[]" value="{{ $language->id }}" x-model="selected" :value="{{ $language->id }}" {{ in_array($language->id, $selected) ? 'checked' : '' }}> {{ $language->name }}
|
||||
</label>
|
||||
@endforeach
|
||||
</div>
|
||||
|
||||
</div>
|
||||
37
resources/views/components/main-image-field.blade.php
Normal file
37
resources/views/components/main-image-field.blade.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<div x-data="MainImageManager()" x-init="init('{{$oldPath}}')">
|
||||
<x-form-field-title name="Main image" helper="This will show up on the index and on top of the entry. A screenshot or custom cover is prefered else all entries of same game will look the same." required="{{ $required ? 'true' : 'false' }}" />
|
||||
<div class="form-group main-image-grid">
|
||||
<div class="form-upload" style="flex:4;">
|
||||
<input type="file" id="main-image-field" accept="image/png, image/jpeg, image/webp" @change="handleSubmitFile($event)">
|
||||
<div class="form-upload-placeholder level">
|
||||
<i data-lucide="file-archive" size="36" style="margin-bottom:15px;color:var(--text2)"></i>
|
||||
<div style="font-size: 1.1rem;color:var(--text);margin-bottom:5px;">
|
||||
Click or drag'n drop files here.
|
||||
</div>
|
||||
<div style="font-size:0.85rem;color:var(--text2);">
|
||||
Accepted: PNG, JPG or WebP
|
||||
</div>
|
||||
</div>
|
||||
<input type="hidden" name="main-image" x-model="serverFilePath">
|
||||
</div>
|
||||
<div class="form-image" style="flex:1">
|
||||
<div class="form-image-placeholder" x-show="!preview">
|
||||
<i data-lucide="image" size="48"></i>
|
||||
</div>
|
||||
<div class="form-image-preview" x-show="preview" x-cloak>
|
||||
<div class="form-image-preview-wrap">
|
||||
<img :src="preview" alt="Main Image">
|
||||
|
||||
<button type="button" class="form-image-remove" @click="handleRemoveFile()">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-image-info">
|
||||
<span x-text="name"></span>
|
||||
</div>
|
||||
<span class="form-error-text" x-show="error !== null" x-text="error"></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
16
resources/views/components/markdown-textarea.blade.php
Normal file
16
resources/views/components/markdown-textarea.blade.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<div x-data
|
||||
x-init="
|
||||
window.mde_{{ $name }} = new EasyMDE({
|
||||
element: $el.querySelector('#field_{{ $name }}'),
|
||||
minHeight: '{{ $minHeight }}',
|
||||
toolbar: {{ Js::from( $toolbar ) }},
|
||||
autosave: {
|
||||
enabled: true,
|
||||
uniqueId: '{{ $name }}',
|
||||
delay: 1000,
|
||||
},
|
||||
})
|
||||
"
|
||||
>
|
||||
<textarea class="form-textarea" id="field_{{ $name }}" name="{{ $name }}">{{ $value }}</textarea>
|
||||
</div>
|
||||
40
resources/views/components/menu.blade.php
Normal file
40
resources/views/components/menu.blade.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<nav id="menu">
|
||||
|
||||
<div class="menu-header">
|
||||
<div class="menu-logo">
|
||||
RP
|
||||
</div>
|
||||
<div class="menu-title">
|
||||
Romhack Plaza
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="menu-navigation">
|
||||
|
||||
|
||||
<div class="menu-group">
|
||||
<div class="menu-group-title">Website</div>
|
||||
<a href="{{ route('home') }}"
|
||||
@class(['menu-item', 'active' => request()->routeIs('home')]) >
|
||||
<i data-lucide="home"></i><span>Home</span>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="menu-user">
|
||||
<div class="menu-user-avatar">
|
||||
<x-xen-foro-avatar />
|
||||
</div>
|
||||
<div class="menu-user-info">
|
||||
<span class="username">
|
||||
{{ \Auth::user()?->username ?? "Guest" }}
|
||||
</span>
|
||||
<span class="user_role">
|
||||
Lorem
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</nav>
|
||||
31
resources/views/components/staff-credits-field.blade.php
Normal file
31
resources/views/components/staff-credits-field.blade.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<div x-data='Credits()' x-init="init(@js($oldStaffCredits))">
|
||||
<x-form-field-title name="Staff/Credits" />
|
||||
<template x-if="credits.length > 0">
|
||||
<div class="form-group grid-credits">
|
||||
<div><x-form-field-title name="Name" /></div>
|
||||
<div><x-form-field-title name="Description" /></div>
|
||||
<div><x-form-field-title name="Actions" /></div>
|
||||
<template x-for="(credit,i) in credits" :key="i">
|
||||
<div style="display:contents">
|
||||
<div>
|
||||
<input class="form-input" type="text" x-model="credit.name" autocomplete="off">
|
||||
</div>
|
||||
<div>
|
||||
<input class="form-input" type="text" x-model="credit.description" autocomplete="off">
|
||||
</div>
|
||||
<div>
|
||||
<button type="button" class="btn" @click="removeCredits(i)">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
<input type="hidden" name="staff_credits" x-model="JSON.stringify(credits)">
|
||||
<div style="display:flex;justify-content:flex-end;margin-top:10px;">
|
||||
<button type="button" class="btn primary" @click="addEmptyCredits()">
|
||||
Add a credit
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
28
resources/views/components/submit-entry-status.blade.php
Normal file
28
resources/views/components/submit-entry-status.blade.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<div class="submit-level" x-data="{
|
||||
nsfw: null,
|
||||
state: '{{ old('submit-state', $defaultState) }}',
|
||||
init(){
|
||||
this.$watch('nsfw', (val) => {
|
||||
if( val && this.state === 'published' ) {
|
||||
this.state = 'draft';
|
||||
}
|
||||
});
|
||||
}
|
||||
}" x-init="init()">
|
||||
<div>
|
||||
@if( section_must_be( [ 'romhacks', 'homebrew' ], $section ) )
|
||||
<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>
|
||||
<select class="form-select" name="submit-state" x-model="state">
|
||||
@foreach( $states as $k => $v )
|
||||
@if( $k == 'published' )
|
||||
<template x-if="!nsfw">
|
||||
<option value="{{ $k }}" {{ $defaultState == $k ? 'selected' : '' }}>{{ $v }}</option>
|
||||
</template>
|
||||
@else
|
||||
<option value="{{ $k }}" {{ $defaultState == $k ? 'selected' : '' }}>{{ $v }}</option>
|
||||
@endif
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
3
resources/views/components/success-block.blade.php
Normal file
3
resources/views/components/success-block.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<div class="block-success">
|
||||
<i data-lucide="{{ $successArray['icon'] ?? '' }}"></i> {{ sprintf( $successArray['message'], $message ) }}
|
||||
</div>
|
||||
16
resources/views/components/topbar.blade.php
Normal file
16
resources/views/components/topbar.blade.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<header id="topbar">
|
||||
<button class="mobile-toggle">
|
||||
<i data-lucide="menu"></i>
|
||||
</button>
|
||||
|
||||
<div class="search-bar">
|
||||
<i data-lucide="search" size="18" color="var(--text2)"></i>
|
||||
<input type="text">Search</input>
|
||||
</div>
|
||||
|
||||
<div class="topbar-actions">
|
||||
<button class="btn">
|
||||
<i data-lucide="bell" size="18"></i>
|
||||
</button>
|
||||
</div>
|
||||
</header>
|
||||
16
resources/views/components/xen-foro-avatar.blade.php
Normal file
16
resources/views/components/xen-foro-avatar.blade.php
Normal file
@@ -0,0 +1,16 @@
|
||||
@if(!$user)
|
||||
<i data-lucide="user"></i>
|
||||
@else
|
||||
@if($user->getAvatarUrl())
|
||||
<img src="{{ $user->getAvatarUrl('m') }}" alt="avatar">
|
||||
@else
|
||||
<div style="
|
||||
background: {{\App\Helpers\XenForoHelpers::getAvatarColor($user) }};
|
||||
width: 40px; height: 40px; border-radius: 50%;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color:var(--text); font-weight: bold;
|
||||
">
|
||||
{{ \App\Helpers\XenForoHelpers::getAvatarLetter($user) }}
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
11
resources/views/entries/index.blade.php
Normal file
11
resources/views/entries/index.blade.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>RomHack Plaza</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Bienvenue sur RomHack Plaza</h1>
|
||||
<p>Le catalogue est en construction.</p>
|
||||
</body>
|
||||
</html>
|
||||
97
resources/views/entries/show.blade.php
Normal file
97
resources/views/entries/show.blade.php
Normal file
@@ -0,0 +1,97 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('page-title', $entry->title . " - " . config('app.name') )
|
||||
|
||||
@section('content')
|
||||
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
|
||||
<article id="entry-container">
|
||||
<div class="entry-header">
|
||||
<div class="entry-cover">
|
||||
@if( $entry->main_image )
|
||||
<img src="{{ Storage::url($entry->main_image) }}">
|
||||
@else
|
||||
<div class="entry-cover-placeholder">
|
||||
<i data-lucide="image" size="48"></i>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="entry-info">
|
||||
<h1 class="entry-title">
|
||||
{{ $entry->title }}
|
||||
</h1>
|
||||
<div class="entry-authors">
|
||||
@forelse( $entry->authors as $author)
|
||||
@if($loop->first)By @endif
|
||||
{{ $author->name }}
|
||||
@if( !$loop->last ), @endif
|
||||
@empty
|
||||
No authors
|
||||
@endforelse
|
||||
</div>
|
||||
<div class="entry-meta-grid">
|
||||
@if( $entry->game )
|
||||
<x-entry-meta-item label="Game Name" value="{{ $entry->game->name }}" />
|
||||
@endif
|
||||
@if( $entry->getRealPlatform() )
|
||||
<x-entry-meta-item label="Platform" value="{{ ($entry->getRealPlatform())->name }}" />
|
||||
@endif
|
||||
@if( $entry->game && $entry->game->genre )
|
||||
<x-entry-meta-item label="Genre" value="{{ $entry->game->genre->name }}" />
|
||||
@endif
|
||||
@if( $entry->languages->isNotEmpty() )
|
||||
<x-entry-meta-item label="Language" value="{{ $entry->languages->pluck('name')->implode(', ') }}" route="none" />
|
||||
@endif
|
||||
@if( $entry->status_id )
|
||||
<x-entry-meta-item label="Status" value="{{ $entry->status->name }}" />
|
||||
@endif
|
||||
@if( $entry->version )
|
||||
<x-entry-meta-item label="Version" value="{{ $entry->version }}" route="none" />
|
||||
@endif
|
||||
@if( $entry->release_date )
|
||||
<x-entry-meta-item label="Release Date" value="{{ $entry->release_date }}" />
|
||||
@endif
|
||||
@if( $entry->modifications->isNotEmpty() )
|
||||
<x-entry-meta-item label="Type of hack" value="{{ $entry->modifications->pluck('name')->implode(', ') }}" route="none" />
|
||||
@endif
|
||||
</div>
|
||||
<div class="hack-actions">
|
||||
<button class="btn primary">
|
||||
<i data-lucide="download"></i> Download
|
||||
</button>
|
||||
<button class="btn">
|
||||
<i data-lucide="message-square"></i> Comments
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="entry-content">
|
||||
@if( $entry->description )
|
||||
<x-entry-section-title label="Description" icon="file-text" />
|
||||
<div class="entry-description">
|
||||
{{ $entry->description }}
|
||||
</div>
|
||||
@endif
|
||||
@if( $entry->hashes->isNotEmpty() )
|
||||
<x-entry-section-title label="Hashes" icon="table-properties" />
|
||||
<div class="entry-description">
|
||||
@foreach( $entry->hashes->all() as $hash )
|
||||
Filename: {{ $hash->filename }}<br>
|
||||
CRC32: {{ $hash->hash_crc32 }}<br>
|
||||
SHA-1: {{ $hash->hash_sha1 }}<br>
|
||||
Verified: {{ $hash->verified }}<br>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
@if( $entry->staff_credits )
|
||||
<h2 class="entry-section-title">
|
||||
<i data-lucide="users-round"></i> Staff credits
|
||||
</h2>
|
||||
<div class="entry-description">
|
||||
{{ $entry->staff_credits }}
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</article>
|
||||
@endsection
|
||||
10
resources/views/home.blade.php
Normal file
10
resources/views/home.blade.php
Normal file
@@ -0,0 +1,10 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@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" />
|
||||
@endsection
|
||||
34
resources/views/layouts/app.blade.php
Normal file
34
resources/views/layouts/app.blade.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
@vite(['resources/css/app.css', 'resources/js/app.js'])
|
||||
@livewireStyles
|
||||
@stack('styles')
|
||||
<title>@yield('page-title', 'Romhack Plaza')</title>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div id="app">
|
||||
@include( 'components.menu' )
|
||||
|
||||
<main id="main-wrapper">
|
||||
@include('components.topbar')
|
||||
@if(session('success'))
|
||||
<x-success-block success-type="custom" :message="session('success')" />
|
||||
@endif
|
||||
@if(session('error'))
|
||||
<x-error-block error-type="custom" :message="session('error')" />
|
||||
@endif
|
||||
|
||||
<div id="content">
|
||||
@yield('content')
|
||||
</div>
|
||||
</main>
|
||||
|
||||
</div>
|
||||
@livewireScripts
|
||||
@stack('scripts')
|
||||
</body>
|
||||
</html>
|
||||
60
resources/views/livewire/authors-selector.blade.php
Normal file
60
resources/views/livewire/authors-selector.blade.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<div>
|
||||
<div class="form-group grid-c2">
|
||||
<div>
|
||||
@if( $newAuthor === false)
|
||||
<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"
|
||||
@focus="$wire.dropdown = $wire.search.length >= 2" @click.outside="$wire.dropdown = false" >
|
||||
</div>
|
||||
@if( $dropdown )
|
||||
<ul class="game-selector-dropdown">
|
||||
@forelse($authors as $author)
|
||||
<li>
|
||||
<button type="button" wire:click="selectAuthor({{ $author->id }}, '{{ addslashes($author->name) }}')"
|
||||
class="dropdown-item">
|
||||
<span class="dropdown-item-name">{{ $author->name }}</span>
|
||||
</button>
|
||||
</li>
|
||||
@empty
|
||||
<li class="dropdown-empty">No author found</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
@endif
|
||||
</div>
|
||||
@else
|
||||
<div class="new-author">
|
||||
<x-form-field-title name="New author" required="true" />
|
||||
<input class="form-input" wire:model="newAuthorName" type="text" autocomplete="off" value="" required>
|
||||
</div>
|
||||
@endif
|
||||
<div style="display:flex;align-items: flex-end;justify-content: right;gap:15px;margin-top:20px;">
|
||||
@if($newAuthor)
|
||||
<button type="button" class="btn primary" wire:click="addNewAuthor" :disabled="$wire.newAuthorName.trim() === ''">
|
||||
Add
|
||||
</button>
|
||||
@endif
|
||||
<button type="button" class="btn {{ $newAuthor ? '' : 'primary' }}" wire:click="switchNewAuthor">{{ $newAuthor ? "Cancel" : "Add an author" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<x-form-field-title name="Selected authors" />
|
||||
<div class="form-group level authors-list">
|
||||
@foreach($selectedAuthors as $i => $author)
|
||||
<div class="author-item">
|
||||
<span>{{ $author['name'] }}</span>
|
||||
<button type="button" class="btn author-item-remove" wire:click="removeAuthor({{ $i }})">
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
@if( $author['id'] )
|
||||
<input type="hidden" name="authors[]" value="{{ $author['id'] }}">
|
||||
@else
|
||||
<input type="hidden" name="new-authors[]" value="{{ $author['name'] }}">
|
||||
@endif
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
98
resources/views/livewire/game-selector.blade.php
Normal file
98
resources/views/livewire/game-selector.blade.php
Normal file
@@ -0,0 +1,98 @@
|
||||
<div x-data='GameSelector()' x-init="init({
|
||||
name: @json(old('new-game-title') ?: null),
|
||||
platformId: @json(old('new-game-platform') ? (string) old('new-game-platform') : null),
|
||||
genreId: @json(old('new-game-genre') ? (string) old('new-game-genre') : null)
|
||||
})">
|
||||
{{--
|
||||
Prefill if server-side error.
|
||||
--}}
|
||||
<div class="form-group grid-c3">
|
||||
|
||||
@if( !$newGame && !$hasOldNewGame )
|
||||
{{-- Search game mode --}}
|
||||
<div class="game-selector">
|
||||
<div class="game-selector-level2">
|
||||
|
||||
<x-form-field-title name="Game" required="true" />
|
||||
<input class="form-input" type="text" wire:model.live.debounce="search" placeholder="Search a game..." autocomplete="off"
|
||||
@focus="$wire.dropdown = $wire.search.length >= {{ $required_chars }}" @click.outside="$wire.dropdown = false" >
|
||||
|
||||
<input type="hidden" name="game_id" value="{{ $gameId ?? '' }}" />
|
||||
|
||||
</div>
|
||||
|
||||
@if( $dropdown )
|
||||
{{-- List games --}}
|
||||
<ul class="game-selector-dropdown">
|
||||
@forelse($games as $game)
|
||||
<li>
|
||||
<button type="button" wire:click="selectGame({{ $game->id }}, '{{ addslashes($game->name) }}')"
|
||||
class="dropdown-item" {{ $gameId === $game->id ? 'selected' : '' }} >
|
||||
<span class="dropdown-item-name">{{ $game->name }}</span>
|
||||
@if($game->platform)
|
||||
<span class="badge">{{ $game->platform->short_name }}</span>
|
||||
@endif
|
||||
@if($game->genre)
|
||||
<span class="badge">{{ $game->genre->name }}</span>
|
||||
@endif
|
||||
</button>
|
||||
</li>
|
||||
@empty
|
||||
<li class="dropdown-empty">No games found</li>
|
||||
@endforelse
|
||||
</ul>
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
<div class="platform-prefilled">
|
||||
<x-form-field-title name="Platform" helper="Prefilled" />
|
||||
<input class="form-input" disabled="disabled" type="text" autocomplete="off" value="{{ $platformName }}">
|
||||
</div>
|
||||
|
||||
<div class="genre-prefilled">
|
||||
<x-form-field-title name="Genre" helper="Prefilled" />
|
||||
<input class="form-input" disabled="disabled" type="text" autocomplete="off" value="{{ $genreName }}" >
|
||||
</div>
|
||||
|
||||
@else {{-- New game --}}
|
||||
|
||||
<div class="new-game-title">
|
||||
<x-form-field-title name="Game" required="true" />
|
||||
<input class="form-input" name="new-game-title" type="text" autocomplete="off" x-model="name" value="{{ old('new-game-title', '') }}" required>
|
||||
</div>
|
||||
|
||||
<div class="new-game-platform">
|
||||
<x-form-field-title name="Platform" required="true" />
|
||||
<select class="form-select" name="new-game-platform" x-model="platformId" required>
|
||||
<option value="" disabled>---</option>
|
||||
@foreach( $platforms as $platform )
|
||||
<option value="{{ (string) $platform->id }}">{{ $platform->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="new-game-genre">
|
||||
<x-form-field-title name="Genre" required="true" />
|
||||
<select class="form-select" name="new-game-genre" x-model="genreId" required>
|
||||
<option value="" disabled>---</option>
|
||||
@foreach( $genres as $genre )
|
||||
<option value="{{ (string) $genre->id }}">{{ $genre->name }}</option>
|
||||
@endforeach
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@endif
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
<div style="display:flex;align-items: flex-end;justify-content: right;gap:15px;">
|
||||
@if($gameId)
|
||||
<button type="button" class="btn" wire:click="clearGame">
|
||||
Remove
|
||||
</button>
|
||||
@endif
|
||||
<button type="button" class="btn primary" wire:click="switchNewGame">{{ $newGame ? "Cancel" : "Add a game" }}</button>
|
||||
</div>
|
||||
</div>
|
||||
66
resources/views/livewire/hashes-upload.blade.php
Normal file
66
resources/views/livewire/hashes-upload.blade.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<div x-data="HashesManager($wire)" x-ref="hashRoot">
|
||||
<x-form-field-title name="Hashes" required="true" />
|
||||
@if( count( $hashes ) > 0)
|
||||
<div class="form-group grid-hashes">
|
||||
|
||||
<div class="hash-filename">
|
||||
<x-form-field-title name="Filename" />
|
||||
</div>
|
||||
|
||||
<div class="hash-crc32">
|
||||
<x-form-field-title name="CRC32" />
|
||||
</div>
|
||||
|
||||
<div class="hash-sha1">
|
||||
<x-form-field-title name="SHA-1" />
|
||||
</div>
|
||||
|
||||
<div class="hash-verified">
|
||||
<x-form-field-title name="Verified" />
|
||||
</div>
|
||||
|
||||
<div class="hash-remove">
|
||||
<x-form-field-title name="Actions" />
|
||||
|
||||
</div>
|
||||
|
||||
@foreach( $hashes as $i => $hash )
|
||||
<div class="hash-filename">
|
||||
<input class="form-input" type="text" autocomplete="off" value="{{ $hash['filename'] }}" disabled>
|
||||
</div>
|
||||
<div class="hash-crc32">
|
||||
<input class="form-input" type="text" autocomplete="off" value="{{ $hash['hash_crc32'] }}" disabled>
|
||||
</div>
|
||||
<div class="hash-sha1">
|
||||
<input class="form-input" type="text" autocomplete="off" value="{{ $hash['hash_sha1'] }}" disabled>
|
||||
</div>
|
||||
<div class="hash-verified">
|
||||
<input class="form-input" type="text" autocomplete="off" value="{{ $hash['verified'] }}" disabled>
|
||||
</div>
|
||||
<div class="hash-remove">
|
||||
<button type="button" class="btn" wire:click="removeHash({{ $i }})">
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<input type="hidden" name="hashes[{{ $i }}][filename]" value="{{ $hash['filename'] }}">
|
||||
<input type="hidden" name="hashes[{{ $i }}][hash_crc32]" value="{{ $hash['hash_crc32'] }}">
|
||||
<input type="hidden" name="hashes[{{ $i }}][hash_sha1]" value="{{ $hash['hash_sha1'] }}">
|
||||
<input type="hidden" name="hashes[{{ $i }}][verified]" value="{{ $hash['verified'] }}">
|
||||
@endforeach
|
||||
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div style="display:flex;align-items: flex-end;justify-content: right;gap:15px;">
|
||||
<span x-show="isCalculating" x-cloak>
|
||||
<i data-lucide="loader-2" class="spin"></i>
|
||||
Please wait...
|
||||
</span>
|
||||
|
||||
<span x-show="error" x-text="error" class="form-error-text" x-cloak></span>
|
||||
|
||||
<button type="button" class="btn primary" :disabled="isCalculating" @click="handleSubmitFile()">Add Hashes</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
7
resources/views/pages/forbidden.blade.php
Normal file
7
resources/views/pages/forbidden.blade.php
Normal file
@@ -0,0 +1,7 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('page-title', "Forbidden - " . config('app.name') )
|
||||
|
||||
@section('content')
|
||||
<x-error-block error-type="page-not-allowed" message="{{ $permission }}" />
|
||||
@endsection
|
||||
9
resources/views/submissions/create.blade.php
Normal file
9
resources/views/submissions/create.blade.php
Normal file
@@ -0,0 +1,9 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('page-title', "Submit - " . config('app.name') )
|
||||
|
||||
@section('content')
|
||||
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
|
||||
<div class="page-title">{{ $words['page_title'] }}</div>
|
||||
@include('submissions.form')
|
||||
@endsection
|
||||
9
resources/views/submissions/edit.blade.php
Normal file
9
resources/views/submissions/edit.blade.php
Normal file
@@ -0,0 +1,9 @@
|
||||
@extends('layouts.app')
|
||||
|
||||
@section('page-title', "Edit $entry->title - " . config('app.name') )
|
||||
|
||||
@section('content')
|
||||
{{ \Diglactic\Breadcrumbs\Breadcrumbs::render() }}
|
||||
<div class="page-title">{{ $words['page_title'] }}</div>
|
||||
@include('submissions.form')
|
||||
@endsection
|
||||
172
resources/views/submissions/form.blade.php
Normal file
172
resources/views/submissions/form.blade.php
Normal file
@@ -0,0 +1,172 @@
|
||||
<?php
|
||||
/** @var \App\Models\Modification $modif */
|
||||
/** @var \App\Models\Status $status */
|
||||
?>
|
||||
|
||||
{{-- Pushed in header. --}}
|
||||
@push('styles')
|
||||
<meta name="fs-section" content="{{ $section }}">
|
||||
<meta name="csrf-token" content="{{ csrf_token() }}">
|
||||
<meta name="submission-has-errors" content="{{ $errors->any() ? '1' : '0' }}">
|
||||
@endpush
|
||||
|
||||
@push('scripts')
|
||||
@vite('resources/js/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('submit.update', [ $section, $entry->id ] ) : route('submit.store', $section ) }}"
|
||||
method="POST" x-data="Submission()" x-init="init()" @submit.prevent="submitForm($event)">
|
||||
@include('submissions.fs-upload')
|
||||
|
||||
<!-- ABOUT THE ENTRY -->
|
||||
<x-form-group-title label="{{ $words['about_the'] }}" icon="puzzle" />
|
||||
|
||||
@if( section_must_not_be( 'translations', $section ) )
|
||||
<div class="form-group">
|
||||
<x-form-field-title name="{{ $words['entry_title'] }}" required="true" />
|
||||
<input class="form-input" type="text" name="entry_title" value="{{ old('entry_title', $entry->title ?? '' ) }}" required>
|
||||
@error('entry_title')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@enderror
|
||||
</div>
|
||||
@else
|
||||
<div class="form-group">
|
||||
<x-form-field-title name="{{ $words['entry_title'] }}" helper="{{ $words['entry_title_helper'] }}" />
|
||||
<input class="form-input" type="text" name="entry_title" value="{{ old('entry_title', $entry->title, '' ) }}">
|
||||
@error('entry_title')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@enderror
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if( section_must_be( 'romhacks', $section ) )
|
||||
<div class="form-group">
|
||||
<x-form-field-title name="{{ $words['type_of_hack'] }}" required="true" />
|
||||
<div class="form-type-of-checkboxes form-group level" id="modifications-group" x-ref="modificationsGroup">
|
||||
@foreach( $modifications as $modif )
|
||||
<label><input class="form-checkbox" type="checkbox" name="modifications[]" value="{{ $modif->id }}" {{ in_array($modif->id, $oldModifications) ? 'checked' : '' }}>{{ $modif->name }}</label>
|
||||
@endforeach
|
||||
</div>
|
||||
<div class="form-error-text" x-show="errorKey === 'noModifications'" x-text="errorMessage"></div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if( section_must_be( ['romhacks', 'translations'], $section ) )
|
||||
<div class="form-group grid-c3">
|
||||
<div>
|
||||
<x-form-field-title name="{{ $words['version'] }}" required="true" />
|
||||
<input class="form-input" type="text" name="version" value="{{ old( 'version', $entry->version ?? '' ) }}" required>
|
||||
</div>
|
||||
<div>
|
||||
<x-form-field-title name="{{ $words['release_date'] }}" helper="{{ $words['release_date_helper'] }}" required="true" />
|
||||
{{-- TODO: Add max to the date --}}
|
||||
<input type="date" class="form-input" name="release-date" value="{{ old('release-date') ?? $entry->release_date?->format('Y-m-d') ?? '' }}" required>
|
||||
</div>
|
||||
<div>
|
||||
<x-form-field-title name="{{ $words['status'] }}" required="true" />
|
||||
<div class="form-status-radio form-group level">
|
||||
@foreach( $statuses as $status )
|
||||
<label><input class="form-radio" type="radio" name="status" value="{{ $status->id }}" {{ old('status', $entry->status_id ) == $status->id ? 'checked' : '' }} required>{{ $status->name }}</label>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@if( section_must_be( 'translations', $section ) )
|
||||
<x-form-field-title name="Languages" required="true" />
|
||||
<x-languages-selector :selected="$oldLanguages" />
|
||||
@endif
|
||||
|
||||
<div class="form-group" x-ref="descriptionField">
|
||||
<x-form-field-title name="{{ $words['description'] }}" required="true" />
|
||||
<x-markdown-textarea name="description" value="{{ old('description', $entry->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="{{ $words['about_game'] }}" icon="gamepad-2" />
|
||||
<div x-ref="gameSelector">
|
||||
<livewire:game-selector
|
||||
:game-id="old('game_id', $entry->game_id ?? null )"
|
||||
:new-game-title="old('new-game-title')"
|
||||
:new-game-platform="old('new-game-platform')"
|
||||
:new-game-genre="old('new-game-genre')"
|
||||
/>
|
||||
</div>
|
||||
<div class="form-error-text" x-show="errorKey === 'noGame'" x-text="errorMessage"></div>
|
||||
@error('game_id')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@enderror
|
||||
@error('new-game-title')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@enderror
|
||||
@error('new-game-platform')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@enderror
|
||||
@error('new-game-genre')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@enderror
|
||||
<livewire:hashes-upload :old-hashes="old('hashes', $entry->hashes->toArray(), [])" />
|
||||
|
||||
@if( section_must_not_be( 'translations', $section ) )
|
||||
<x-form-field-title name="Languages" required="true" />
|
||||
<x-languages-selector :selected="$oldLanguages" />
|
||||
@error('languages')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@enderror
|
||||
@error('languages.*')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@enderror
|
||||
@endif
|
||||
|
||||
<x-form-group-title label="{{ $words['attachments'] }}" icon="paperclip" />
|
||||
<x-main-image-field :old-path="old('main-image', $entry->main_image ?? '')" />
|
||||
<x-gallery-field :old-paths="old('gallery', $entry->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="{{ $words['authors'] }}" icon="users" />
|
||||
<livewire:authors-selector :old-authors="old('authors', $entry->authors->map(fn($a) => ['id' => $a->id, 'name' => $a->name ])->toArray() ?? [])" :old-new-authors="old('new-authors', [])" />
|
||||
@error('authors')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@enderror
|
||||
@error('new-authors')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@enderror
|
||||
<x-staff-credits-field :old-staff-credits="old('staff_credits', $entry->staff_credits ?? null)" />
|
||||
|
||||
<x-form-group-title label="{{ $words['related_links'] }}" icon="link" />
|
||||
<div class="form-group grid-c2">
|
||||
<div>
|
||||
<x-form-field-title name="{{ $words['release_site'] }}" helper="{{ $words['release_site_helper'] }}" required="" />
|
||||
<input class="form-input" type="url" name="release_site" value="{{ old( 'release_site', $entry->relevant_link ?? '' ) }}">
|
||||
</div>
|
||||
<div>
|
||||
<x-form-field-title name="{{ $words['youtube_video'] }}" required="" />
|
||||
<input class="form-input" type="url" name="youtube_video" value="{{ old( 'youtube_video', $entry->youtube_link ?? '' ) }}">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@csrf
|
||||
|
||||
<div class="submit">
|
||||
<x-submit-entry-status :section="$section" />
|
||||
<button id="submit-button" type="submit" class="btn primary" style="padding:1%;">Submit</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
69
resources/views/submissions/fs-upload.blade.php
Normal file
69
resources/views/submissions/fs-upload.blade.php
Normal file
@@ -0,0 +1,69 @@
|
||||
{{-- File Server uploader import --}}
|
||||
<div class="form-group level" x-data="FSUploader()" x-init="init(@js($oldFilesArray))">
|
||||
|
||||
<x-form-group-title label="Download Files" icon="upload-cloud" />
|
||||
<div class="form-group">
|
||||
<x-form-field-title name="Files" helper="test" required="true" />
|
||||
<div class="form-upload" x-ref="uploadTarget" :class="{ 'disabled': isUploading }">
|
||||
<input type="file" multiple :disabled="isUploading" @change="handleSubmitFile($event)">
|
||||
<div class="form-upload-placeholder level">
|
||||
<i data-lucide="file-archive" size="36" style="margin-bottom:15px;color:var(--text2)"></i>
|
||||
<div style="font-size: 1.1rem;color:var(--text);margin-bottom:5px;">
|
||||
Click or drag'n drop files here.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<x-form-error-text message="Don't submit ROMs" />
|
||||
|
||||
{{-- Client-side Errors --}}
|
||||
<div class="form-error-text" x-show="errorKey === 'noFiles' || errorKey === 'uploadError' || errorKey === 'notAllFilesDone'" x-text="errorMessage"></div>
|
||||
|
||||
{{-- Server-side Errors --}}
|
||||
@error('file_ids')
|
||||
<x-form-error-text message="{{ $message }}" />
|
||||
@enderror
|
||||
|
||||
</div>
|
||||
|
||||
{{--
|
||||
File listing. Used for editions or server-side errors.
|
||||
--}}
|
||||
<div class="upload-list" x-show="numberOfFiles > 0" x-cloak>
|
||||
<template x-for="(file,i) in files" :key="i">
|
||||
<div class="upload-item" :class="
|
||||
{
|
||||
'upload-item-uploading': !file.done && !file.error,
|
||||
'upload-item-done': file.done,
|
||||
'upload-item-error': file.error
|
||||
}">
|
||||
<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>
|
||||
|
||||
<div class="upload-item-info">
|
||||
<span class="upload-item-name" x-text="file.name"></span>
|
||||
<div class="progress" x-show="!file.done && !file.error">
|
||||
<div class="progress-bar" :style="{width: file.progressValue + '%' }">
|
||||
<span class="progress-bar-label" x-text="file.progressValue + '% - chunk' + file.currentChunk + ' / ' + file.totalChunks"></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="upload-item-error" x-show="file.error" x-text="file.error"></span>
|
||||
</div>
|
||||
<div class="upload-item-actions">
|
||||
<button type="button" class="btn" x-show="file.error" @click="handleRetryFile(i)">
|
||||
<i data-lucide="refresh-cw"></i>
|
||||
</button>
|
||||
<button type="button" class="btn" x-show="file.done || file.error" @click="handleRemoveFile(i)">
|
||||
<i data-lucide="x"></i>
|
||||
</button>
|
||||
</div>
|
||||
<input type="hidden" name="files_uuid[]" :value="file.uuid" x-show="file.done">
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
223
resources/views/welcome.blade.php
Normal file
223
resources/views/welcome.blade.php
Normal file
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user